Skip to content

Commit dc554f8

Browse files
committed
refactor(aria/combobox): update autocomplete examples to simple-combobox and add softDisabled
1 parent a2519f7 commit dc554f8

15 files changed

Lines changed: 172 additions & 188 deletions

File tree

goldens/aria/private/index.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,7 @@ export interface SimpleComboboxInputs extends ExpansionItem {
671671
element: SignalLike<HTMLElement>;
672672
inlineSuggestion: SignalLike<string | undefined>;
673673
popup: SignalLike<SimpleComboboxPopupPattern | undefined>;
674+
softDisabled: SignalLike<boolean>;
674675
value: WritableSignalLike<string>;
675676
}
676677

@@ -701,6 +702,7 @@ export class SimpleComboboxPattern {
701702
onKeydown(event: KeyboardEvent): void;
702703
readonly popupId: _angular_core.Signal<string | undefined>;
703704
readonly popupType: _angular_core.Signal<"listbox" | "tree" | "grid" | "dialog" | undefined>;
705+
readonly softDisabled: () => boolean;
704706
readonly value: WritableSignalLike<string>;
705707
}
706708

goldens/aria/simple-combobox/index.api.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ export class Combobox extends DeferredContentAware implements OnInit {
2525
readonly _pattern: SimpleComboboxPattern;
2626
readonly _popup: _angular_core.WritableSignal<ComboboxPopup | undefined>;
2727
_registerPopup(popup: ComboboxPopup): void;
28+
readonly softDisabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
2829
_unregisterPopup(): void;
2930
readonly value: _angular_core.ModelSignal<string>;
3031
// (undocumented)
31-
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<Combobox, "[ngCombobox]", ["ngCombobox"], { "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "alwaysExpanded": { "alias": "alwaysExpanded"; "required": false; "isSignal": true; }; "expanded": { "alias": "expanded"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; "inlineSuggestion": { "alias": "inlineSuggestion"; "required": false; "isSignal": true; }; }, { "expanded": "expandedChange"; "value": "valueChange"; }, never, never, true, never>;
32+
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<Combobox, "[ngCombobox]", ["ngCombobox"], { "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "softDisabled": { "alias": "softDisabled"; "required": false; "isSignal": true; }; "alwaysExpanded": { "alias": "alwaysExpanded"; "required": false; "isSignal": true; }; "expanded": { "alias": "expanded"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; "inlineSuggestion": { "alias": "inlineSuggestion"; "required": false; "isSignal": true; }; }, { "expanded": "expandedChange"; "value": "valueChange"; }, never, never, true, never>;
3233
// (undocumented)
3334
static ɵfac: _angular_core.ɵɵFactoryDeclaration<Combobox, never>;
3435
}

src/aria/private/simple-combobox/simple-combobox.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ export interface SimpleComboboxInputs extends ExpansionItem {
3030

3131
/** Whether the combobox is disabled. */
3232
disabled: SignalLike<boolean>;
33+
34+
/** Whether the combobox is soft disabled. */
35+
softDisabled: SignalLike<boolean>;
3336
}
3437

3538
/** Controls the state of a simple combobox. */
@@ -46,6 +49,9 @@ export class SimpleComboboxPattern {
4649
/** Whether the combobox is disabled. */
4750
readonly disabled = () => this.inputs.disabled();
4851

52+
/** Whether the combobox is soft disabled. */
53+
readonly softDisabled = () => this.inputs.softDisabled();
54+
4955
/** An inline suggestion to be displayed in the input. */
5056
readonly inlineSuggestion = () => this.inputs.inlineSuggestion();
5157

src/aria/simple-combobox/simple-combobox.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,36 @@ describe('Combobox', () => {
535535
expect(inputElement.getAttribute('aria-expanded')).toBe('true');
536536
});
537537
});
538+
539+
describe('Disabled', () => {
540+
beforeEach(() => setupCombobox());
541+
542+
it('should keep the input focusable by default when disabled', () => {
543+
fixture.componentInstance.disabled.set(true);
544+
fixture.detectChanges();
545+
546+
expect(inputElement.disabled).toBe(false);
547+
expect(inputElement.getAttribute('aria-disabled')).toBe('true');
548+
});
549+
550+
it('should block interactions when disabled', () => {
551+
fixture.componentInstance.disabled.set(true);
552+
fixture.detectChanges();
553+
554+
focus();
555+
keydown('ArrowDown');
556+
expect(inputElement.getAttribute('aria-expanded')).toBe('false');
557+
});
558+
559+
it('should make the input unfocusable when softDisabled is false', () => {
560+
fixture.componentInstance.disabled.set(true);
561+
fixture.componentInstance.softDisabled.set(false);
562+
fixture.detectChanges();
563+
564+
expect(inputElement.disabled).toBe(true);
565+
expect(inputElement.getAttribute('aria-disabled')).toBe('true');
566+
});
567+
});
538568
});
539569

540570
describe('with Tree', () => {
@@ -1145,6 +1175,8 @@ describe('Combobox', () => {
11451175
[(value)]="searchString"
11461176
[(expanded)]="popupExpanded"
11471177
[readonly]="readonly()"
1178+
[disabled]="disabled()"
1179+
[softDisabled]="softDisabled()"
11481180
[alwaysExpanded]="alwaysExpanded()"
11491181
(focusout)="onBlur()"
11501182
/>
@@ -1168,6 +1200,8 @@ describe('Combobox', () => {
11681200
})
11691201
class ComboboxListboxExample {
11701202
readonly = signal(false);
1203+
disabled = signal(false);
1204+
softDisabled = signal(true);
11711205
alwaysExpanded = signal(false);
11721206
popupExpanded = signal(false);
11731207
searchString = signal('');

src/aria/simple-combobox/simple-combobox.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ import type {ComboboxPopup} from './simple-combobox-popup';
5151
'[attr.aria-activedescendant]': '_pattern.activeDescendant()',
5252
'[attr.aria-controls]': '_pattern.popupId()',
5353
'[attr.aria-haspopup]': '_pattern.popupType()',
54+
'[attr.tabindex]': 'disabled() && !softDisabled() ? -1 : null',
55+
'[attr.disabled]': 'disabled() && !softDisabled() ? "" : null',
5456
'(keydown)': '_pattern.onKeydown($event)',
5557
'(focusin)': '_pattern.onFocusin()',
5658
'(focusout)': '_pattern.onFocusout($event)',
@@ -73,6 +75,9 @@ export class Combobox extends DeferredContentAware implements OnInit {
7375
/** Whether the combobox is disabled. */
7476
readonly disabled = input(false, {transform: booleanAttribute});
7577

78+
/** Whether the combobox is soft disabled (remains focusable). */
79+
readonly softDisabled = input(true, {transform: booleanAttribute});
80+
7681
/** Whether the combobox should always remain expanded. */
7782
readonly alwaysExpanded = input(false, {transform: booleanAttribute});
7883

src/components-examples/aria/autocomplete/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ ng_project(
1313
"//:node_modules/@angular/common",
1414
"//:node_modules/@angular/core",
1515
"//:node_modules/@angular/forms",
16-
"//src/aria/combobox",
1716
"//src/aria/listbox",
17+
"//src/aria/simple-combobox",
1818
"//src/cdk/overlay",
1919
],
2020
)

src/components-examples/aria/autocomplete/autocomplete-auto-select/autocomplete-auto-select-example.html

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
<div ngCombobox filterMode="auto-select">
1+
<div class="example-combobox-container">
22
<div #origin class="example-autocomplete">
33
<span class="example-search-icon material-symbols-outlined" translate="no">search</span>
44
<input
5+
ngCombobox
6+
#combobox="ngCombobox"
57
aria-label="Label dropdown"
68
placeholder="Select a country"
7-
ngComboboxInput
9+
[(value)]="searchString"
10+
[(expanded)]="popupExpanded"
811
/>
912
<button
1013
class="example-clear-button"
@@ -20,17 +23,14 @@
2023
{{countries().length === 0 ? 'No results found for ' + query() : ''}}
2124
</div>
2225

23-
<ng-template ngComboboxPopupContainer>
24-
<ng-template
25-
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
26-
[cdkConnectedOverlayOpen]="true"
27-
>
26+
<ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true">
27+
<ng-template ngComboboxPopup [combobox]="combobox">
2828
<div class="example-popup">
2929
@if (countries().length === 0) {
3030
<div class="example-no-results">No results found</div>
3131
}
3232

33-
<div ngListbox>
33+
<div ngListbox ngComboboxWidget focusMode="activedescendant" [tabbable]="false" [(value)]="selectedOption" (click)="onCommit()" (keydown.enter)="onCommit()">
3434
@for (country of countries(); track country) {
3535
<div ngOption [value]="country" [label]="country" [disabled]="country === 'Brazil'">
3636
<span class="example-option-label">{{country}}</span>

src/components-examples/aria/autocomplete/autocomplete-auto-select/autocomplete-auto-select-example.ts

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,15 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {
10-
Combobox,
11-
ComboboxInput,
12-
ComboboxPopup,
13-
ComboboxPopupContainer,
14-
} from '@angular/aria/combobox';
9+
import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/simple-combobox';
1510
import {Listbox, Option} from '@angular/aria/listbox';
1611
import {
1712
afterRenderEffect,
1813
ChangeDetectionStrategy,
1914
Component,
2015
computed,
16+
signal,
2117
viewChild,
22-
viewChildren,
2318
} from '@angular/core';
2419
import {COUNTRIES} from '../countries';
2520
import {OverlayModule} from '@angular/cdk/overlay';
@@ -30,60 +25,52 @@ import {FormsModule} from '@angular/forms';
3025
selector: 'autocomplete-auto-select-example',
3126
templateUrl: 'autocomplete-auto-select-example.html',
3227
styleUrl: '../autocomplete.css',
33-
imports: [
34-
Combobox,
35-
ComboboxInput,
36-
ComboboxPopup,
37-
ComboboxPopupContainer,
38-
Listbox,
39-
Option,
40-
OverlayModule,
41-
FormsModule,
42-
],
28+
imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option, OverlayModule, FormsModule],
4329
changeDetection: ChangeDetectionStrategy.OnPush,
4430
})
4531
export class AutocompleteAutoSelectExample {
4632
/** The selected value of the combobox. */
47-
listbox = viewChild<Listbox<string>>(Listbox);
48-
49-
/** The options available in the listbox. */
50-
options = viewChildren<Option<string>>(Option);
51-
52-
/** A reference to the ng aria combobox. */
53-
combobox = viewChild<Combobox<string>>(Combobox);
33+
readonly listbox = viewChild(Listbox);
34+
readonly combobox = viewChild(Combobox);
5435

55-
/** A reference to the ng aria combobox input. */
56-
comboboxInput = viewChild<ComboboxInput>(ComboboxInput);
36+
popupExpanded = signal(false);
37+
searchString = signal('');
38+
selectedOption = signal<string[]>([]);
5739

5840
/** The query string used to filter the list of countries. */
59-
query = computed(() => this.comboboxInput()?.value() || '');
41+
query = computed(() => this.searchString());
6042

6143
/** The list of countries filtered by the query. */
6244
countries = computed(() =>
6345
COUNTRIES.filter(country => country.toLowerCase().startsWith(this.query().toLowerCase())),
6446
);
6547

6648
constructor() {
67-
// Scrolls to the active item when the active option changes.
6849
afterRenderEffect(() => {
69-
if (this.combobox()?.expanded()) {
70-
const option = this.options().find(opt => opt.active());
71-
option?.element.scrollIntoView({block: 'nearest'});
72-
}
50+
this.listbox()?.scrollActiveItemIntoView();
7351
});
7452
}
7553

7654
/** Clears the query and the listbox value. */
7755
clear(): void {
78-
this.comboboxInput()?.value.set('');
79-
this.listbox?.()?.value.set([]);
56+
this.searchString.set('');
57+
this.selectedOption.set([]);
58+
}
59+
60+
onCommit() {
61+
const selectedOption = this.selectedOption();
62+
if (selectedOption.length > 0) {
63+
this.searchString.set(selectedOption[0]);
64+
}
65+
this.popupExpanded.set(false);
66+
this.combobox()?.element.focus();
8067
}
8168

8269
/** Handles keydown events on the clear button. */
8370
onKeydown(event: KeyboardEvent): void {
8471
if (event.key === 'Enter') {
8572
this.clear();
86-
this.combobox?.()?.close();
73+
this.popupExpanded.set(false);
8774
event.stopPropagation();
8875
}
8976
}

src/components-examples/aria/autocomplete/autocomplete-disabled/autocomplete-disabled-example.html

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
1-
<div ngCombobox disabled>
1+
<div class="example-combobox-container">
22
<div #origin class="example-autocomplete">
33
<span class="example-search-icon material-symbols-outlined" translate="no">search</span>
44
<input
5+
ngCombobox
6+
#combobox="ngCombobox"
57
aria-label="Label dropdown"
68
placeholder="Select a country"
7-
ngComboboxInput
9+
[(value)]="searchString"
10+
[(expanded)]="popupExpanded"
11+
disabled
12+
readonly
813
/>
914
</div>
1015

11-
<ng-template ngComboboxPopupContainer>
12-
<ng-template
13-
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
14-
[cdkConnectedOverlayOpen]="true"
15-
>
16+
<ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}" [cdkConnectedOverlayOpen]="true">
17+
<ng-template ngComboboxPopup [combobox]="combobox">
1618
<div class="example-popup">
1719
@if (countries().length === 0) {
1820
<div class="example-no-results">No results found</div>
1921
}
2022

21-
<div ngListbox>
23+
<div ngListbox ngComboboxWidget focusMode="activedescendant" [tabbable]="false" [(value)]="selectedOption">
2224
@for (country of countries(); track country) {
2325
<div ngOption [value]="country" [label]="country" [disabled]="country === 'Brazil'">
2426
<span class="example-option-label">{{country}}</span>

src/components-examples/aria/autocomplete/autocomplete-disabled/autocomplete-disabled-example.ts

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {
10-
Combobox,
11-
ComboboxInput,
12-
ComboboxPopup,
13-
ComboboxPopupContainer,
14-
} from '@angular/aria/combobox';
9+
import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/simple-combobox';
1510
import {Listbox, Option} from '@angular/aria/listbox';
1611
import {
1712
afterRenderEffect,
@@ -20,7 +15,6 @@ import {
2015
computed,
2116
signal,
2217
viewChild,
23-
viewChildren,
2418
} from '@angular/core';
2519
import {COUNTRIES} from '../countries';
2620
import {OverlayModule} from '@angular/cdk/overlay';
@@ -31,40 +25,29 @@ import {FormsModule} from '@angular/forms';
3125
selector: 'autocomplete-disabled-example',
3226
templateUrl: 'autocomplete-disabled-example.html',
3327
styleUrl: '../autocomplete.css',
34-
imports: [
35-
Combobox,
36-
ComboboxInput,
37-
ComboboxPopup,
38-
ComboboxPopupContainer,
39-
Listbox,
40-
Option,
41-
OverlayModule,
42-
FormsModule,
43-
],
28+
imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option, OverlayModule, FormsModule],
4429
changeDetection: ChangeDetectionStrategy.OnPush,
4530
})
4631
export class AutocompleteDisabledExample {
47-
/** The options available in the listbox. */
48-
options = viewChildren<Option<string>>(Option);
32+
/** The selected value of the combobox. */
33+
readonly listbox = viewChild(Listbox);
34+
readonly combobox = viewChild(Combobox);
4935

50-
/** A reference to the ng aria combobox. */
51-
combobox = viewChild<Combobox<string>>(Combobox);
36+
popupExpanded = signal(false);
37+
searchString = signal('United States of America');
38+
selectedOption = signal<string[]>([]);
5239

5340
/** The query string used to filter the list of countries. */
54-
query = signal('United States of America');
41+
query = computed(() => this.searchString());
5542

5643
/** The list of countries filtered by the query. */
5744
countries = computed(() =>
5845
COUNTRIES.filter(country => country.toLowerCase().startsWith(this.query().toLowerCase())),
5946
);
6047

6148
constructor() {
62-
// Scrolls to the active item when the active option changes.
6349
afterRenderEffect(() => {
64-
if (this.combobox()?.expanded()) {
65-
const option = this.options().find(opt => opt.active());
66-
option?.element.scrollIntoView({block: 'nearest'});
67-
}
50+
this.listbox()?.scrollActiveItemIntoView();
6851
});
6952
}
7053
}

0 commit comments

Comments
 (0)