Skip to content

Commit 1cdbb5d

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

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
@@ -663,6 +663,7 @@ export interface SimpleComboboxInputs extends ExpansionItem {
663663
element: SignalLike<HTMLElement>;
664664
inlineSuggestion: SignalLike<string | undefined>;
665665
popup: SignalLike<SimpleComboboxPopupPattern | undefined>;
666+
softDisabled: SignalLike<boolean>;
666667
value: WritableSignalLike<string>;
667668
}
668669

@@ -693,6 +694,7 @@ export class SimpleComboboxPattern {
693694
onKeydown(event: KeyboardEvent): void;
694695
readonly popupId: _angular_core.Signal<string | undefined>;
695696
readonly popupType: _angular_core.Signal<"listbox" | "tree" | "grid" | "dialog" | undefined>;
697+
readonly softDisabled: () => boolean;
696698
readonly value: WritableSignalLike<string>;
697699
}
698700

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
@@ -580,6 +580,36 @@ describe('Combobox', () => {
580580
expect(inputElement.getAttribute('aria-expanded')).toBe('true');
581581
});
582582
});
583+
584+
describe('Disabled', () => {
585+
beforeEach(() => setupCombobox());
586+
587+
it('should keep the input focusable by default when disabled', () => {
588+
fixture.componentInstance.disabled.set(true);
589+
fixture.detectChanges();
590+
591+
expect(inputElement.disabled).toBe(false);
592+
expect(inputElement.getAttribute('aria-disabled')).toBe('true');
593+
});
594+
595+
it('should block interactions when disabled', () => {
596+
fixture.componentInstance.disabled.set(true);
597+
fixture.detectChanges();
598+
599+
focus();
600+
keydown('ArrowDown');
601+
expect(inputElement.getAttribute('aria-expanded')).toBe('false');
602+
});
603+
604+
it('should make the input unfocusable when softDisabled is false', () => {
605+
fixture.componentInstance.disabled.set(true);
606+
fixture.componentInstance.softDisabled.set(false);
607+
fixture.detectChanges();
608+
609+
expect(inputElement.disabled).toBe(true);
610+
expect(inputElement.getAttribute('aria-disabled')).toBe('true');
611+
});
612+
});
583613
});
584614

585615
describe('with Tree', () => {
@@ -1209,6 +1239,8 @@ describe('Combobox', () => {
12091239
[(value)]="searchString"
12101240
[(expanded)]="popupExpanded"
12111241
[readonly]="readonly()"
1242+
[disabled]="disabled()"
1243+
[softDisabled]="softDisabled()"
12121244
[alwaysExpanded]="alwaysExpanded()"
12131245
(focusout)="onBlur()"
12141246
/>
@@ -1233,6 +1265,8 @@ describe('Combobox', () => {
12331265
})
12341266
class ComboboxListboxExample {
12351267
readonly = signal(false);
1268+
disabled = signal(false);
1269+
softDisabled = signal(true);
12361270
alwaysExpanded = signal(false);
12371271
popupExpanded = signal(false);
12381272
searchString = signal('');

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ import {
5656
'[attr.aria-activedescendant]': '_pattern.activeDescendant()',
5757
'[attr.aria-controls]': '_pattern.popupId()',
5858
'[attr.aria-haspopup]': '_pattern.popupType()',
59+
'[attr.tabindex]': 'disabled() && !softDisabled() ? -1 : null',
60+
'[attr.disabled]': 'disabled() && !softDisabled() ? "" : null',
5961
'(keydown)': '_pattern.onKeydown($event)',
6062
'(focusin)': '_pattern.onFocusin()',
6163
'(focusout)': '_pattern.onFocusout($event)',
@@ -78,6 +80,9 @@ export class Combobox extends DeferredContentAware implements OnInit {
7880
/** Whether the combobox is disabled. */
7981
readonly disabled = input(false, {transform: booleanAttribute});
8082

83+
/** Whether the combobox is soft disabled (remains focusable). */
84+
readonly softDisabled = input(true, {transform: booleanAttribute});
85+
8186
/** Whether the combobox should always remain expanded. */
8287
readonly alwaysExpanded = input(false, {transform: booleanAttribute});
8388

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)