From 536c50413daa907438d851be8a7efe1c1cbd08b3 Mon Sep 17 00:00:00 2001 From: tjshiu <35056071+tjshiu@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:16:01 -0700 Subject: [PATCH 1/3] refactor(aria/combobox): allow adding tabIndex and default it to 0 --- goldens/aria/simple-combobox/index.api.md | 3 +- .../simple-combobox/simple-combobox.ts | 3 +- .../simple-combobox/simple-combobox.spec.ts | 33 ++++++++++++ src/aria/simple-combobox/simple-combobox.ts | 6 ++- .../simple-combobox-dialog-example.html | 8 +-- ...le-combobox-readonly-disabled-example.html | 3 +- ...combobox-readonly-multiselect-example.html | 50 ++++++----------- .../simple-combobox-select-example.html | 53 ++++++------------- 8 files changed, 78 insertions(+), 81 deletions(-) diff --git a/goldens/aria/simple-combobox/index.api.md b/goldens/aria/simple-combobox/index.api.md index 5fff72ac2474..94e37dddc477 100644 --- a/goldens/aria/simple-combobox/index.api.md +++ b/goldens/aria/simple-combobox/index.api.md @@ -26,10 +26,11 @@ export class Combobox extends DeferredContentAware implements OnInit { readonly _popup: _angular_core.WritableSignal; _registerPopup(popup: ComboboxPopup): void; readonly softDisabled: _angular_core.InputSignalWithTransform; + readonly tabIndex: _angular_core.InputSignal; _unregisterPopup(): void; readonly value: _angular_core.ModelSignal; // (undocumented) - static ɵdir: _angular_core.ɵɵDirectiveDeclaration; + static ɵdir: _angular_core.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: _angular_core.ɵɵFactoryDeclaration; } diff --git a/src/aria/private/simple-combobox/simple-combobox.ts b/src/aria/private/simple-combobox/simple-combobox.ts index 85cd85fffbc4..32bc23ab728e 100644 --- a/src/aria/private/simple-combobox/simple-combobox.ts +++ b/src/aria/private/simple-combobox/simple-combobox.ts @@ -106,7 +106,8 @@ export class SimpleComboboxPattern { manager.on('ArrowDown', () => this.inputs.expanded.set(true)); if (!this.isEditable()) { - manager.on(/^(Enter| )$/, () => this.inputs.expanded.set(true)); + manager.on('Enter', () => this.inputs.expanded.set(true)); + manager.on(' ', () => this.inputs.expanded.set(true)); } return manager; diff --git a/src/aria/simple-combobox/simple-combobox.spec.ts b/src/aria/simple-combobox/simple-combobox.spec.ts index 6cd3792efcde..3ad2adfc2d0b 100644 --- a/src/aria/simple-combobox/simple-combobox.spec.ts +++ b/src/aria/simple-combobox/simple-combobox.spec.ts @@ -544,6 +544,7 @@ describe('Combobox', () => { fixture.detectChanges(); expect(inputElement.disabled).toBe(false); + expect(inputElement.getAttribute('disabled')).toBeNull(); expect(inputElement.getAttribute('aria-disabled')).toBe('true'); }); @@ -562,8 +563,38 @@ describe('Combobox', () => { fixture.detectChanges(); expect(inputElement.disabled).toBe(true); + expect(inputElement.getAttribute('disabled')).toBe(''); expect(inputElement.getAttribute('aria-disabled')).toBe('true'); }); + + it('should respect user-defined tabindex when softDisabled is true', () => { + fixture.componentInstance.disabled.set(true); + fixture.componentInstance.tabIndex.set(0); + fixture.detectChanges(); + + expect(inputElement.getAttribute('tabindex')).toBe('0'); + }); + + it('should respect user-defined tabindex when not disabled', () => { + fixture.componentInstance.tabIndex.set(0); + fixture.detectChanges(); + + expect(inputElement.getAttribute('tabindex')).toBe('0'); + }); + + it('should default to tabindex 0 when not disabled', () => { + fixture.detectChanges(); + expect(inputElement.getAttribute('tabindex')).toBe('0'); + }); + + it('should force tabindex to -1 when hard-disabled, ignoring user-defined tabindex', () => { + fixture.componentInstance.disabled.set(true); + fixture.componentInstance.softDisabled.set(false); + fixture.componentInstance.tabIndex.set(0); + fixture.detectChanges(); + + expect(inputElement.getAttribute('tabindex')).toBe('-1'); + }); }); }); @@ -1178,6 +1209,7 @@ describe('Combobox', () => { [disabled]="disabled()" [softDisabled]="softDisabled()" [alwaysExpanded]="alwaysExpanded()" + [tabIndex]="tabIndex()" (focusout)="onBlur()" /> @@ -1203,6 +1235,7 @@ class ComboboxListboxExample { disabled = signal(false); softDisabled = signal(true); alwaysExpanded = signal(false); + tabIndex = signal(undefined); popupExpanded = signal(false); searchString = signal(''); value = signal([]); diff --git a/src/aria/simple-combobox/simple-combobox.ts b/src/aria/simple-combobox/simple-combobox.ts index a8410b394b24..c5d1bcddd644 100644 --- a/src/aria/simple-combobox/simple-combobox.ts +++ b/src/aria/simple-combobox/simple-combobox.ts @@ -51,7 +51,8 @@ import type {ComboboxPopup} from './simple-combobox-popup'; '[attr.aria-activedescendant]': '_pattern.activeDescendant()', '[attr.aria-controls]': '_pattern.popupId()', '[attr.aria-haspopup]': '_pattern.popupType()', - '[attr.tabindex]': 'disabled() && !softDisabled() ? -1 : null', + '[attr.tabindex]': + 'disabled() && !softDisabled() ? -1 : (tabIndex() !== undefined ? tabIndex() : 0)', '[attr.disabled]': 'disabled() && !softDisabled() ? "" : null', '(keydown)': '_pattern.onKeydown($event)', '(focusin)': '_pattern.onFocusin()', @@ -81,6 +82,9 @@ export class Combobox extends DeferredContentAware implements OnInit { /** Whether the combobox should always remain expanded. */ readonly alwaysExpanded = input(false, {transform: booleanAttribute}); + /** The tabindex of the combobox. */ + readonly tabIndex = input(undefined); + /** Whether the combobox is expanded. */ readonly expanded = model(false); diff --git a/src/components-examples/aria/simple-combobox/simple-combobox-dialog/simple-combobox-dialog-example.html b/src/components-examples/aria/simple-combobox/simple-combobox-dialog/simple-combobox-dialog-example.html index a9b0e512cca5..0e2b70dbb149 100644 --- a/src/components-examples/aria/simple-combobox/simple-combobox-dialog/simple-combobox-dialog-example.html +++ b/src/components-examples/aria/simple-combobox/simple-combobox-dialog/simple-combobox-dialog-example.html @@ -1,4 +1,4 @@ -
+
@@ -19,9 +19,9 @@ (keydown.escape)="onSearchEscape($event)" />
-
+
@for (option of options(); track option) {
diff --git a/src/components-examples/aria/simple-combobox/simple-combobox-readonly-disabled/simple-combobox-readonly-disabled-example.html b/src/components-examples/aria/simple-combobox/simple-combobox-readonly-disabled/simple-combobox-readonly-disabled-example.html index a332bd10109a..d970a59f8501 100644 --- a/src/components-examples/aria/simple-combobox/simple-combobox-readonly-disabled/simple-combobox-readonly-disabled-example.html +++ b/src/components-examples/aria/simple-combobox/simple-combobox-readonly-disabled/simple-combobox-readonly-disabled-example.html @@ -1,5 +1,4 @@ -
+
{{value()}} arrow_drop_down
diff --git a/src/components-examples/aria/simple-combobox/simple-combobox-readonly-multiselect/simple-combobox-readonly-multiselect-example.html b/src/components-examples/aria/simple-combobox/simple-combobox-readonly-multiselect/simple-combobox-readonly-multiselect-example.html index a84980f220ad..84318269f1d2 100644 --- a/src/components-examples/aria/simple-combobox/simple-combobox-readonly-multiselect/simple-combobox-readonly-multiselect-example.html +++ b/src/components-examples/aria/simple-combobox/simple-combobox-readonly-multiselect/simple-combobox-readonly-multiselect-example.html @@ -1,46 +1,26 @@ -
+
{{value()}} arrow_drop_down
- +
-
+
@for (option of options(); track option.value) { -
- @if (option.icon) { - {{option.icon}} - } - {{option.value}} - @if (selectedValues().includes(option.value)) { - - } -
+
+ @if (option.icon) { + {{option.icon}} + } + {{option.value}} + @if (selectedValues().includes(option.value)) { + + } +
}
- + \ No newline at end of file diff --git a/src/components-examples/aria/simple-combobox/simple-combobox-select/simple-combobox-select-example.html b/src/components-examples/aria/simple-combobox/simple-combobox-select/simple-combobox-select-example.html index e679d9412d98..9578ae399b6c 100644 --- a/src/components-examples/aria/simple-combobox/simple-combobox-select/simple-combobox-select-example.html +++ b/src/components-examples/aria/simple-combobox/simple-combobox-select/simple-combobox-select-example.html @@ -1,48 +1,27 @@ -
+
{{value()}} arrow_drop_down
- +
-
+
@for (option of options(); track option.value) { -
- @if (option.icon) { - {{option.icon}} - } - {{option.value}} - @if (selectedValues().includes(option.value)) { - - } -
+
+ @if (option.icon) { + {{option.icon}} + } + {{option.value}} + @if (selectedValues().includes(option.value)) { + + } +
}
- + \ No newline at end of file From b0263a0f5179bf30a45aed0a3af35996de4a33ea Mon Sep 17 00:00:00 2001 From: tjshiu <35056071+tjshiu@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:55:56 -0700 Subject: [PATCH 2/3] refactor(multiple): add tabindex alias for tabIndex input Adds the 'tabindex' alias to the 'tabIndex' input signal in Grid, Listbox, Combobox, and Tree components. This allows the input to be bound using the more idiomatic lowercase 'tabindex' attribute in templates. Related tests and examples have also been updated to use the new alias. --- goldens/aria/grid/index.api.md | 2 +- goldens/aria/listbox/index.api.md | 2 +- goldens/aria/simple-combobox/index.api.md | 2 +- goldens/aria/tree/index.api.md | 2 +- src/aria/grid/grid.spec.ts | 2 +- src/aria/grid/grid.ts | 1 + src/aria/listbox/listbox.ts | 1 + src/aria/simple-combobox/simple-combobox.spec.ts | 2 +- src/aria/simple-combobox/simple-combobox.ts | 7 ++++++- src/aria/tree/tree.ts | 1 + .../simple-combobox-auto-select-example.html | 2 +- .../simple-combobox-autocomplete-auto-select-example.html | 3 ++- .../simple-combobox-autocomplete-disabled-example.html | 2 +- .../simple-combobox-autocomplete-highlight-example.html | 3 ++- .../simple-combobox-autocomplete-manual-example.html | 2 +- .../simple-combobox-dialog-example.html | 4 ++-- .../simple-combobox-disabled-example.html | 3 ++- .../simple-combobox-grid/simple-combobox-grid-example.html | 2 +- .../simple-combobox-highlight-example.html | 2 +- .../simple-combobox-listbox-example.html | 2 +- .../simple-combobox-readonly-disabled-example.html | 2 +- .../simple-combobox-readonly-multiselect-example.html | 2 +- .../simple-combobox-select-example.html | 2 +- .../simple-combobox-tree-auto-select-example.html | 2 +- .../simple-combobox-tree-highlight-example.html | 2 +- .../simple-combobox-tree/simple-combobox-tree-example.html | 2 +- 26 files changed, 35 insertions(+), 24 deletions(-) diff --git a/goldens/aria/grid/index.api.md b/goldens/aria/grid/index.api.md index 770bc0fc199a..ccb6923cb765 100644 --- a/goldens/aria/grid/index.api.md +++ b/goldens/aria/grid/index.api.md @@ -28,7 +28,7 @@ export class Grid { readonly tabIndex: _angular_core.InputSignalWithTransform; readonly textDirection: _angular_core.WritableSignal<_angular_cdk_bidi.Direction>; // (undocumented) - static ɵdir: _angular_core.ɵɵDirectiveDeclaration; + static ɵdir: _angular_core.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: _angular_core.ɵɵFactoryDeclaration; } diff --git a/goldens/aria/listbox/index.api.md b/goldens/aria/listbox/index.api.md index d3c984e40aae..da48a7d889cf 100644 --- a/goldens/aria/listbox/index.api.md +++ b/goldens/aria/listbox/index.api.md @@ -36,7 +36,7 @@ export class Listbox implements OnDestroy { readonly value: _angular_core.ModelSignal; readonly wrap: _angular_core.InputSignalWithTransform; // (undocumented) - static ɵdir: _angular_core.ɵɵDirectiveDeclaration, "[ngListbox]", ["ngListbox"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "orientation": { "alias": "orientation"; "required": false; "isSignal": true; }; "multi": { "alias": "multi"; "required": false; "isSignal": true; }; "wrap": { "alias": "wrap"; "required": false; "isSignal": true; }; "softDisabled": { "alias": "softDisabled"; "required": false; "isSignal": true; }; "focusMode": { "alias": "focusMode"; "required": false; "isSignal": true; }; "selectionMode": { "alias": "selectionMode"; "required": false; "isSignal": true; }; "typeaheadDelay": { "alias": "typeaheadDelay"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "readonly": { "alias": "readonly"; "required": false; "isSignal": true; }; "tabIndex": { "alias": "tabIndex"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; }, never, never, true, [{ directive: typeof ComboboxPopup; inputs: {}; outputs: {}; }]>; + static ɵdir: _angular_core.ɵɵDirectiveDeclaration, "[ngListbox]", ["ngListbox"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "orientation": { "alias": "orientation"; "required": false; "isSignal": true; }; "multi": { "alias": "multi"; "required": false; "isSignal": true; }; "wrap": { "alias": "wrap"; "required": false; "isSignal": true; }; "softDisabled": { "alias": "softDisabled"; "required": false; "isSignal": true; }; "focusMode": { "alias": "focusMode"; "required": false; "isSignal": true; }; "selectionMode": { "alias": "selectionMode"; "required": false; "isSignal": true; }; "typeaheadDelay": { "alias": "typeaheadDelay"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "readonly": { "alias": "readonly"; "required": false; "isSignal": true; }; "tabIndex": { "alias": "tabindex"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; }, never, never, true, [{ directive: typeof ComboboxPopup; inputs: {}; outputs: {}; }]>; // (undocumented) static ɵfac: _angular_core.ɵɵFactoryDeclaration, never>; } diff --git a/goldens/aria/simple-combobox/index.api.md b/goldens/aria/simple-combobox/index.api.md index 94e37dddc477..3fa104d6bb40 100644 --- a/goldens/aria/simple-combobox/index.api.md +++ b/goldens/aria/simple-combobox/index.api.md @@ -30,7 +30,7 @@ export class Combobox extends DeferredContentAware implements OnInit { _unregisterPopup(): void; readonly value: _angular_core.ModelSignal; // (undocumented) - static ɵdir: _angular_core.ɵɵDirectiveDeclaration; + static ɵdir: _angular_core.ɵɵDirectiveDeclaration; // (undocumented) static ɵfac: _angular_core.ɵɵFactoryDeclaration; } diff --git a/goldens/aria/tree/index.api.md b/goldens/aria/tree/index.api.md index e4dc6095b226..5ba758c969b8 100644 --- a/goldens/aria/tree/index.api.md +++ b/goldens/aria/tree/index.api.md @@ -37,7 +37,7 @@ export class Tree { readonly value: _angular_core.ModelSignal; readonly wrap: _angular_core.InputSignalWithTransform; // (undocumented) - static ɵdir: _angular_core.ɵɵDirectiveDeclaration, "[ngTree]", ["ngTree"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "orientation": { "alias": "orientation"; "required": false; "isSignal": true; }; "multi": { "alias": "multi"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "selectionMode": { "alias": "selectionMode"; "required": false; "isSignal": true; }; "focusMode": { "alias": "focusMode"; "required": false; "isSignal": true; }; "wrap": { "alias": "wrap"; "required": false; "isSignal": true; }; "softDisabled": { "alias": "softDisabled"; "required": false; "isSignal": true; }; "typeaheadDelay": { "alias": "typeaheadDelay"; "required": false; "isSignal": true; }; "tabIndex": { "alias": "tabIndex"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; "nav": { "alias": "nav"; "required": false; "isSignal": true; }; "currentType": { "alias": "currentType"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; }, never, never, true, [{ directive: typeof ComboboxPopup; inputs: {}; outputs: {}; }]>; + static ɵdir: _angular_core.ɵɵDirectiveDeclaration, "[ngTree]", ["ngTree"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "orientation": { "alias": "orientation"; "required": false; "isSignal": true; }; "multi": { "alias": "multi"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "selectionMode": { "alias": "selectionMode"; "required": false; "isSignal": true; }; "focusMode": { "alias": "focusMode"; "required": false; "isSignal": true; }; "wrap": { "alias": "wrap"; "required": false; "isSignal": true; }; "softDisabled": { "alias": "softDisabled"; "required": false; "isSignal": true; }; "typeaheadDelay": { "alias": "typeaheadDelay"; "required": false; "isSignal": true; }; "tabIndex": { "alias": "tabindex"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; "nav": { "alias": "nav"; "required": false; "isSignal": true; }; "currentType": { "alias": "currentType"; "required": false; "isSignal": true; }; }, { "value": "valueChange"; }, never, never, true, [{ directive: typeof ComboboxPopup; inputs: {}; outputs: {}; }]>; // (undocumented) static ɵfac: _angular_core.ɵɵFactoryDeclaration, never>; } diff --git a/src/aria/grid/grid.spec.ts b/src/aria/grid/grid.spec.ts index a691f86f9883..2fb873abdd41 100644 --- a/src/aria/grid/grid.spec.ts +++ b/src/aria/grid/grid.spec.ts @@ -974,7 +974,7 @@ describe('Grid directives', () => { [softDisabled]="softDisabled()" [enableSelection]="enableSelection()" [selectionMode]="selectionMode()" - [tabIndex]="tabIndex()"> + [tabindex]="tabIndex()"> @for (row of gridData(); track $index; let rIndex = $index) { @for (cell of row.cells; track $index; let cIndex = $index) { diff --git a/src/aria/grid/grid.ts b/src/aria/grid/grid.ts index 8f7db0d690f9..3a2144820600 100644 --- a/src/aria/grid/grid.ts +++ b/src/aria/grid/grid.ts @@ -123,6 +123,7 @@ export class Grid { /** The tabindex of the grid. */ readonly tabIndex = input(undefined, { + alias: 'tabindex', transform: (v: string | number | undefined) => v === undefined ? undefined : numberAttribute(v), }); diff --git a/src/aria/listbox/listbox.ts b/src/aria/listbox/listbox.ts index 11431b3d64d1..d5ae9abfdb2e 100644 --- a/src/aria/listbox/listbox.ts +++ b/src/aria/listbox/listbox.ts @@ -133,6 +133,7 @@ export class Listbox implements OnDestroy { /** The tabindex of the listbox. */ readonly tabIndex = input(undefined, { + alias: 'tabindex', transform: (v: string | number | undefined) => v === undefined ? undefined : numberAttribute(v), }); diff --git a/src/aria/simple-combobox/simple-combobox.spec.ts b/src/aria/simple-combobox/simple-combobox.spec.ts index 3ad2adfc2d0b..db7e8aab0204 100644 --- a/src/aria/simple-combobox/simple-combobox.spec.ts +++ b/src/aria/simple-combobox/simple-combobox.spec.ts @@ -1209,7 +1209,7 @@ describe('Combobox', () => { [disabled]="disabled()" [softDisabled]="softDisabled()" [alwaysExpanded]="alwaysExpanded()" - [tabIndex]="tabIndex()" + [tabindex]="tabIndex()" (focusout)="onBlur()" /> diff --git a/src/aria/simple-combobox/simple-combobox.ts b/src/aria/simple-combobox/simple-combobox.ts index c5d1bcddd644..82867b7dcbd9 100644 --- a/src/aria/simple-combobox/simple-combobox.ts +++ b/src/aria/simple-combobox/simple-combobox.ts @@ -15,6 +15,7 @@ import { inject, input, model, + numberAttribute, OnInit, signal, Renderer2, @@ -83,7 +84,11 @@ export class Combobox extends DeferredContentAware implements OnInit { readonly alwaysExpanded = input(false, {transform: booleanAttribute}); /** The tabindex of the combobox. */ - readonly tabIndex = input(undefined); + readonly tabIndex = input(undefined, { + alias: 'tabindex', + transform: (v: string | number | undefined) => + v === undefined ? undefined : numberAttribute(v), + }); /** Whether the combobox is expanded. */ readonly expanded = model(false); diff --git a/src/aria/tree/tree.ts b/src/aria/tree/tree.ts index f3b59a844129..c5a16258eedb 100644 --- a/src/aria/tree/tree.ts +++ b/src/aria/tree/tree.ts @@ -134,6 +134,7 @@ export class Tree { /** The tabindex of the tree. */ readonly tabIndex = input(undefined, { + alias: 'tabindex', transform: (v: string | number | undefined) => v === undefined ? undefined : numberAttribute(v), }); diff --git a/src/components-examples/aria/simple-combobox/simple-combobox-auto-select/simple-combobox-auto-select-example.html b/src/components-examples/aria/simple-combobox/simple-combobox-auto-select/simple-combobox-auto-select-example.html index ed6af34585d9..1b4cc4335c7a 100644 --- a/src/components-examples/aria/simple-combobox/simple-combobox-auto-select/simple-combobox-auto-select-example.html +++ b/src/components-examples/aria/simple-combobox/simple-combobox-auto-select/simple-combobox-auto-select-example.html @@ -9,7 +9,7 @@ [cdkConnectedOverlayDisableClose]="true">
@for (option of options(); track option) {
diff --git a/src/components-examples/aria/simple-combobox/simple-combobox-autocomplete-auto-select/simple-combobox-autocomplete-auto-select-example.html b/src/components-examples/aria/simple-combobox/simple-combobox-autocomplete-auto-select/simple-combobox-autocomplete-auto-select-example.html index 6a04d3e23612..bca340602aa2 100644 --- a/src/components-examples/aria/simple-combobox/simple-combobox-autocomplete-auto-select/simple-combobox-autocomplete-auto-select-example.html +++ b/src/components-examples/aria/simple-combobox/simple-combobox-autocomplete-auto-select/simple-combobox-autocomplete-auto-select-example.html @@ -30,7 +30,8 @@
No results found
} -
+
@for (country of countries(); track country) {
{{country}} diff --git a/src/components-examples/aria/simple-combobox/simple-combobox-autocomplete-disabled/simple-combobox-autocomplete-disabled-example.html b/src/components-examples/aria/simple-combobox/simple-combobox-autocomplete-disabled/simple-combobox-autocomplete-disabled-example.html index 4ae9c89500a5..dd764213595a 100644 --- a/src/components-examples/aria/simple-combobox/simple-combobox-autocomplete-disabled/simple-combobox-autocomplete-disabled-example.html +++ b/src/components-examples/aria/simple-combobox/simple-combobox-autocomplete-disabled/simple-combobox-autocomplete-disabled-example.html @@ -20,7 +20,7 @@
No results found
} -
+
@for (country of countries(); track country) {
{{country}} diff --git a/src/components-examples/aria/simple-combobox/simple-combobox-autocomplete-highlight/simple-combobox-autocomplete-highlight-example.html b/src/components-examples/aria/simple-combobox/simple-combobox-autocomplete-highlight/simple-combobox-autocomplete-highlight-example.html index 617e706877cf..6af061413cb1 100644 --- a/src/components-examples/aria/simple-combobox/simple-combobox-autocomplete-highlight/simple-combobox-autocomplete-highlight-example.html +++ b/src/components-examples/aria/simple-combobox/simple-combobox-autocomplete-highlight/simple-combobox-autocomplete-highlight-example.html @@ -31,7 +31,8 @@
No results found
} -
+
@for (country of countries(); track country) {
{{country}} diff --git a/src/components-examples/aria/simple-combobox/simple-combobox-autocomplete-manual/simple-combobox-autocomplete-manual-example.html b/src/components-examples/aria/simple-combobox/simple-combobox-autocomplete-manual/simple-combobox-autocomplete-manual-example.html index 5866ba3415b1..8fa8048f8ac9 100644 --- a/src/components-examples/aria/simple-combobox/simple-combobox-autocomplete-manual/simple-combobox-autocomplete-manual-example.html +++ b/src/components-examples/aria/simple-combobox/simple-combobox-autocomplete-manual/simple-combobox-autocomplete-manual-example.html @@ -20,7 +20,7 @@
No results found
} -
@for (country of countries(); track country) {
diff --git a/src/components-examples/aria/simple-combobox/simple-combobox-dialog/simple-combobox-dialog-example.html b/src/components-examples/aria/simple-combobox/simple-combobox-dialog/simple-combobox-dialog-example.html index 0e2b70dbb149..5bcfc7a3efca 100644 --- a/src/components-examples/aria/simple-combobox/simple-combobox-dialog/simple-combobox-dialog-example.html +++ b/src/components-examples/aria/simple-combobox/simple-combobox-dialog/simple-combobox-dialog-example.html @@ -1,7 +1,7 @@
+ [readonly]="true" [tabindex]="-1" /> arrow_drop_down
@@ -20,7 +20,7 @@
@for (option of options(); track option) {
-
@for (option of options(); track option) { diff --git a/src/components-examples/aria/simple-combobox/simple-combobox-grid/simple-combobox-grid-example.html b/src/components-examples/aria/simple-combobox/simple-combobox-grid/simple-combobox-grid-example.html index e2ba93642012..119e4e26d2d0 100644 --- a/src/components-examples/aria/simple-combobox/simple-combobox-grid/simple-combobox-grid-example.html +++ b/src/components-examples/aria/simple-combobox/simple-combobox-grid/simple-combobox-grid-example.html @@ -8,7 +8,7 @@ -
@for (item of filteredItems(); track item.label; let i = $index) {
diff --git a/src/components-examples/aria/simple-combobox/simple-combobox-highlight/simple-combobox-highlight-example.html b/src/components-examples/aria/simple-combobox/simple-combobox-highlight/simple-combobox-highlight-example.html index ebf1ebc9d044..57fd48ad76fb 100644 --- a/src/components-examples/aria/simple-combobox/simple-combobox-highlight/simple-combobox-highlight-example.html +++ b/src/components-examples/aria/simple-combobox/simple-combobox-highlight/simple-combobox-highlight-example.html @@ -10,7 +10,7 @@ [cdkConnectedOverlayDisableClose]="true">
@for (option of options(); track option.name) {
@for (option of options(); track option) {
diff --git a/src/components-examples/aria/simple-combobox/simple-combobox-readonly-disabled/simple-combobox-readonly-disabled-example.html b/src/components-examples/aria/simple-combobox/simple-combobox-readonly-disabled/simple-combobox-readonly-disabled-example.html index d970a59f8501..d0f14cf33047 100644 --- a/src/components-examples/aria/simple-combobox/simple-combobox-readonly-disabled/simple-combobox-readonly-disabled-example.html +++ b/src/components-examples/aria/simple-combobox/simple-combobox-readonly-disabled/simple-combobox-readonly-disabled-example.html @@ -7,7 +7,7 @@ [cdkConnectedOverlayOpen]="true" [cdkConnectedOverlayDisableClose]="true">
-
@for (option of options(); track option.value) { diff --git a/src/components-examples/aria/simple-combobox/simple-combobox-readonly-multiselect/simple-combobox-readonly-multiselect-example.html b/src/components-examples/aria/simple-combobox/simple-combobox-readonly-multiselect/simple-combobox-readonly-multiselect-example.html index 84318269f1d2..0513d3e6481b 100644 --- a/src/components-examples/aria/simple-combobox/simple-combobox-readonly-multiselect/simple-combobox-readonly-multiselect-example.html +++ b/src/components-examples/aria/simple-combobox/simple-combobox-readonly-multiselect/simple-combobox-readonly-multiselect-example.html @@ -7,7 +7,7 @@ [cdkConnectedOverlayOpen]="true" [cdkConnectedOverlayDisableClose]="true">
-
@for (option of options(); track option.value) {
diff --git a/src/components-examples/aria/simple-combobox/simple-combobox-select/simple-combobox-select-example.html b/src/components-examples/aria/simple-combobox/simple-combobox-select/simple-combobox-select-example.html index 9578ae399b6c..fe5226516b00 100644 --- a/src/components-examples/aria/simple-combobox/simple-combobox-select/simple-combobox-select-example.html +++ b/src/components-examples/aria/simple-combobox/simple-combobox-select/simple-combobox-select-example.html @@ -7,7 +7,7 @@ [cdkConnectedOverlayOpen]="true" [cdkConnectedOverlayDisableClose]="true">
-
@for (option of options(); track option.value) { diff --git a/src/components-examples/aria/simple-combobox/simple-combobox-tree-auto-select/simple-combobox-tree-auto-select-example.html b/src/components-examples/aria/simple-combobox/simple-combobox-tree-auto-select/simple-combobox-tree-auto-select-example.html index 0dd6ccbca5c7..96d40f1c5377 100644 --- a/src/components-examples/aria/simple-combobox/simple-combobox-tree-auto-select/simple-combobox-tree-auto-select-example.html +++ b/src/components-examples/aria/simple-combobox/simple-combobox-tree-auto-select/simple-combobox-tree-auto-select-example.html @@ -8,7 +8,7 @@ -
    -
      -
        From eafe5fbe23f62b3897bea3a707f260ffed0b292f Mon Sep 17 00:00:00 2001 From: tjshiu <35056071+tjshiu@users.noreply.github.com> Date: Thu, 30 Apr 2026 21:00:38 -0700 Subject: [PATCH 3/3] refactor(multiple): create a private util for the transform function --- goldens/aria/private/index.api.md | 3 +++ goldens/aria/simple-combobox/index.api.md | 2 +- src/aria/grid/grid.ts | 6 ++---- src/aria/listbox/listbox.ts | 11 +++++++---- src/aria/private/public-api.ts | 1 + src/aria/private/utils/BUILD.bazel | 1 + src/aria/private/utils/transforms.ts | 17 +++++++++++++++++ src/aria/simple-combobox/simple-combobox.ts | 10 ++++++---- src/aria/tree/tree.ts | 12 ++++++++---- 9 files changed, 46 insertions(+), 17 deletions(-) create mode 100644 src/aria/private/utils/transforms.ts diff --git a/goldens/aria/private/index.api.md b/goldens/aria/private/index.api.md index 98f532b6403f..6f48bc4ee15e 100644 --- a/goldens/aria/private/index.api.md +++ b/goldens/aria/private/index.api.md @@ -746,6 +746,9 @@ export class SortedCollection { unregister(item: T): void; } +// @public +export function tabIndexTransform(v: string | number | undefined): number | undefined; + // @public export interface TabInputs extends Omit, Omit { tabList: SignalLike; diff --git a/goldens/aria/simple-combobox/index.api.md b/goldens/aria/simple-combobox/index.api.md index 3fa104d6bb40..706075dce478 100644 --- a/goldens/aria/simple-combobox/index.api.md +++ b/goldens/aria/simple-combobox/index.api.md @@ -26,7 +26,7 @@ export class Combobox extends DeferredContentAware implements OnInit { readonly _popup: _angular_core.WritableSignal; _registerPopup(popup: ComboboxPopup): void; readonly softDisabled: _angular_core.InputSignalWithTransform; - readonly tabIndex: _angular_core.InputSignal; + readonly tabIndex: _angular_core.InputSignalWithTransform; _unregisterPopup(): void; readonly value: _angular_core.ModelSignal; // (undocumented) diff --git a/src/aria/grid/grid.ts b/src/aria/grid/grid.ts index 3a2144820600..ecf209a74a9b 100644 --- a/src/aria/grid/grid.ts +++ b/src/aria/grid/grid.ts @@ -15,11 +15,10 @@ import { ElementRef, inject, input, - numberAttribute, Signal, } from '@angular/core'; import {Directionality} from '@angular/cdk/bidi'; -import {GridPattern, GridCellPattern} from '../private'; +import {GridPattern, GridCellPattern, tabIndexTransform} from '../private'; import {GRID_ROW} from './grid-tokens'; /** @@ -124,8 +123,7 @@ export class Grid { /** The tabindex of the grid. */ readonly tabIndex = input(undefined, { alias: 'tabindex', - transform: (v: string | number | undefined) => - v === undefined ? undefined : numberAttribute(v), + transform: tabIndexTransform, }); /** The UI pattern for the grid. */ diff --git a/src/aria/listbox/listbox.ts b/src/aria/listbox/listbox.ts index d5ae9abfdb2e..d237e66aa3c8 100644 --- a/src/aria/listbox/listbox.ts +++ b/src/aria/listbox/listbox.ts @@ -16,7 +16,6 @@ import { inject, input, model, - numberAttribute, OnDestroy, signal, Signal, @@ -24,7 +23,12 @@ import { } from '@angular/core'; import {Directionality} from '@angular/cdk/bidi'; import {_IdGenerator} from '@angular/cdk/a11y'; -import {ComboboxListboxPattern, ListboxPattern, SortedCollection} from '../private'; +import { + ComboboxListboxPattern, + ListboxPattern, + SortedCollection, + tabIndexTransform, +} from '../private'; import {ComboboxPopup} from '../combobox'; import {Option} from './option'; import {LISTBOX} from './tokens'; @@ -134,8 +138,7 @@ export class Listbox implements OnDestroy { /** The tabindex of the listbox. */ readonly tabIndex = input(undefined, { alias: 'tabindex', - transform: (v: string | number | undefined) => - v === undefined ? undefined : numberAttribute(v), + transform: tabIndexTransform, }); /** The values of the currently selected items. */ diff --git a/src/aria/private/public-api.ts b/src/aria/private/public-api.ts index 870df47ec199..631c3b210125 100644 --- a/src/aria/private/public-api.ts +++ b/src/aria/private/public-api.ts @@ -28,4 +28,5 @@ export * from './deferred-content'; export * from './utils/collection'; export * from './utils/element'; export * from './utils/element-resolver'; +export * from './utils/transforms'; export * from './simple-combobox/simple-combobox'; diff --git a/src/aria/private/utils/BUILD.bazel b/src/aria/private/utils/BUILD.bazel index 373946bc092b..db82bca1e81e 100644 --- a/src/aria/private/utils/BUILD.bazel +++ b/src/aria/private/utils/BUILD.bazel @@ -8,6 +8,7 @@ ts_project( "collection.ts", "element.ts", "element-resolver.ts", + "transforms.ts", ], deps = [ "//:node_modules/@angular/core", diff --git a/src/aria/private/utils/transforms.ts b/src/aria/private/utils/transforms.ts new file mode 100644 index 000000000000..241273994bd0 --- /dev/null +++ b/src/aria/private/utils/transforms.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import {numberAttribute} from '@angular/core'; + +/** + * Transform function for tabIndex inputs. + * Returns undefined if the value is undefined, otherwise converts it to a number. + */ +export function tabIndexTransform(v: string | number | undefined): number | undefined { + return v === undefined ? undefined : numberAttribute(v); +} diff --git a/src/aria/simple-combobox/simple-combobox.ts b/src/aria/simple-combobox/simple-combobox.ts index 82867b7dcbd9..7746780d8538 100644 --- a/src/aria/simple-combobox/simple-combobox.ts +++ b/src/aria/simple-combobox/simple-combobox.ts @@ -15,12 +15,15 @@ import { inject, input, model, - numberAttribute, OnInit, signal, Renderer2, } from '@angular/core'; -import {DeferredContentAware, SimpleComboboxPattern} from '@angular/aria/private'; +import { + DeferredContentAware, + SimpleComboboxPattern, + tabIndexTransform, +} from '@angular/aria/private'; import type {ComboboxPopup} from './simple-combobox-popup'; /** @@ -86,8 +89,7 @@ export class Combobox extends DeferredContentAware implements OnInit { /** The tabindex of the combobox. */ readonly tabIndex = input(undefined, { alias: 'tabindex', - transform: (v: string | number | undefined) => - v === undefined ? undefined : numberAttribute(v), + transform: tabIndexTransform, }); /** Whether the combobox is expanded. */ diff --git a/src/aria/tree/tree.ts b/src/aria/tree/tree.ts index c5a16258eedb..3b31b51eb3aa 100644 --- a/src/aria/tree/tree.ts +++ b/src/aria/tree/tree.ts @@ -15,14 +15,19 @@ import { inject, input, model, - numberAttribute, signal, Signal, untracked, } from '@angular/core'; import {_IdGenerator} from '@angular/cdk/a11y'; import {Directionality} from '@angular/cdk/bidi'; -import {ComboboxTreePattern, TreeItemPattern, TreePattern, sortDirectives} from '../private'; +import { + ComboboxTreePattern, + TreeItemPattern, + TreePattern, + sortDirectives, + tabIndexTransform, +} from '../private'; import {ComboboxPopup} from '../combobox'; import type {TreeItem} from './tree-item'; @@ -135,8 +140,7 @@ export class Tree { /** The tabindex of the tree. */ readonly tabIndex = input(undefined, { alias: 'tabindex', - transform: (v: string | number | undefined) => - v === undefined ? undefined : numberAttribute(v), + transform: tabIndexTransform, }); /** The values of the currently selected items. */