From dc554f8ba54371d9ea163cd6aee0d8e9c0bc0f17 Mon Sep 17 00:00:00 2001 From: tjshiu <35056071+tjshiu@users.noreply.github.com> Date: Thu, 23 Apr 2026 16:24:46 -0700 Subject: [PATCH 1/2] refactor(aria/combobox): update autocomplete examples to simple-combobox and add softDisabled --- goldens/aria/private/index.api.md | 2 + goldens/aria/simple-combobox/index.api.md | 3 +- .../simple-combobox/simple-combobox.ts | 6 ++ .../simple-combobox/simple-combobox.spec.ts | 34 +++++++++++ src/aria/simple-combobox/simple-combobox.ts | 5 ++ .../aria/autocomplete/BUILD.bazel | 2 +- .../autocomplete-auto-select-example.html | 16 +++--- .../autocomplete-auto-select-example.ts | 57 +++++++------------ .../autocomplete-disabled-example.html | 18 +++--- .../autocomplete-disabled-example.ts | 37 ++++-------- .../autocomplete-highlight-example.html | 19 ++++--- .../autocomplete-highlight-example.ts | 57 +++++++------------ .../autocomplete-manual-example.html | 39 +++++-------- .../autocomplete-manual-example.ts | 57 +++++++------------ .../aria/autocomplete/autocomplete.css | 8 +-- 15 files changed, 172 insertions(+), 188 deletions(-) diff --git a/goldens/aria/private/index.api.md b/goldens/aria/private/index.api.md index e4ecbf2b9529..c353e9443f94 100644 --- a/goldens/aria/private/index.api.md +++ b/goldens/aria/private/index.api.md @@ -671,6 +671,7 @@ export interface SimpleComboboxInputs extends ExpansionItem { element: SignalLike; inlineSuggestion: SignalLike; popup: SignalLike; + softDisabled: SignalLike; value: WritableSignalLike; } @@ -701,6 +702,7 @@ export class SimpleComboboxPattern { onKeydown(event: KeyboardEvent): void; readonly popupId: _angular_core.Signal; readonly popupType: _angular_core.Signal<"listbox" | "tree" | "grid" | "dialog" | undefined>; + readonly softDisabled: () => boolean; readonly value: WritableSignalLike; } diff --git a/goldens/aria/simple-combobox/index.api.md b/goldens/aria/simple-combobox/index.api.md index 1fe9556b60f1..5fff72ac2474 100644 --- a/goldens/aria/simple-combobox/index.api.md +++ b/goldens/aria/simple-combobox/index.api.md @@ -25,10 +25,11 @@ export class Combobox extends DeferredContentAware implements OnInit { readonly _pattern: SimpleComboboxPattern; readonly _popup: _angular_core.WritableSignal; _registerPopup(popup: ComboboxPopup): void; + readonly softDisabled: _angular_core.InputSignalWithTransform; _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 5b21e5120c20..cbeee244eb9f 100644 --- a/src/aria/private/simple-combobox/simple-combobox.ts +++ b/src/aria/private/simple-combobox/simple-combobox.ts @@ -30,6 +30,9 @@ export interface SimpleComboboxInputs extends ExpansionItem { /** Whether the combobox is disabled. */ disabled: SignalLike; + + /** Whether the combobox is soft disabled. */ + softDisabled: SignalLike; } /** Controls the state of a simple combobox. */ @@ -46,6 +49,9 @@ export class SimpleComboboxPattern { /** Whether the combobox is disabled. */ readonly disabled = () => this.inputs.disabled(); + /** Whether the combobox is soft disabled. */ + readonly softDisabled = () => this.inputs.softDisabled(); + /** An inline suggestion to be displayed in the input. */ readonly inlineSuggestion = () => this.inputs.inlineSuggestion(); diff --git a/src/aria/simple-combobox/simple-combobox.spec.ts b/src/aria/simple-combobox/simple-combobox.spec.ts index 236f07c56491..6cd3792efcde 100644 --- a/src/aria/simple-combobox/simple-combobox.spec.ts +++ b/src/aria/simple-combobox/simple-combobox.spec.ts @@ -535,6 +535,36 @@ describe('Combobox', () => { expect(inputElement.getAttribute('aria-expanded')).toBe('true'); }); }); + + describe('Disabled', () => { + beforeEach(() => setupCombobox()); + + it('should keep the input focusable by default when disabled', () => { + fixture.componentInstance.disabled.set(true); + fixture.detectChanges(); + + expect(inputElement.disabled).toBe(false); + expect(inputElement.getAttribute('aria-disabled')).toBe('true'); + }); + + it('should block interactions when disabled', () => { + fixture.componentInstance.disabled.set(true); + fixture.detectChanges(); + + focus(); + keydown('ArrowDown'); + expect(inputElement.getAttribute('aria-expanded')).toBe('false'); + }); + + it('should make the input unfocusable when softDisabled is false', () => { + fixture.componentInstance.disabled.set(true); + fixture.componentInstance.softDisabled.set(false); + fixture.detectChanges(); + + expect(inputElement.disabled).toBe(true); + expect(inputElement.getAttribute('aria-disabled')).toBe('true'); + }); + }); }); describe('with Tree', () => { @@ -1145,6 +1175,8 @@ describe('Combobox', () => { [(value)]="searchString" [(expanded)]="popupExpanded" [readonly]="readonly()" + [disabled]="disabled()" + [softDisabled]="softDisabled()" [alwaysExpanded]="alwaysExpanded()" (focusout)="onBlur()" /> @@ -1168,6 +1200,8 @@ describe('Combobox', () => { }) class ComboboxListboxExample { readonly = signal(false); + disabled = signal(false); + softDisabled = signal(true); alwaysExpanded = signal(false); popupExpanded = signal(false); searchString = signal(''); diff --git a/src/aria/simple-combobox/simple-combobox.ts b/src/aria/simple-combobox/simple-combobox.ts index 0a2986760cfc..a8410b394b24 100644 --- a/src/aria/simple-combobox/simple-combobox.ts +++ b/src/aria/simple-combobox/simple-combobox.ts @@ -51,6 +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.disabled]': 'disabled() && !softDisabled() ? "" : null', '(keydown)': '_pattern.onKeydown($event)', '(focusin)': '_pattern.onFocusin()', '(focusout)': '_pattern.onFocusout($event)', @@ -73,6 +75,9 @@ export class Combobox extends DeferredContentAware implements OnInit { /** Whether the combobox is disabled. */ readonly disabled = input(false, {transform: booleanAttribute}); + /** Whether the combobox is soft disabled (remains focusable). */ + readonly softDisabled = input(true, {transform: booleanAttribute}); + /** Whether the combobox should always remain expanded. */ readonly alwaysExpanded = input(false, {transform: booleanAttribute}); diff --git a/src/components-examples/aria/autocomplete/BUILD.bazel b/src/components-examples/aria/autocomplete/BUILD.bazel index 7b5c57c7ef81..329cbfd90b7b 100644 --- a/src/components-examples/aria/autocomplete/BUILD.bazel +++ b/src/components-examples/aria/autocomplete/BUILD.bazel @@ -13,8 +13,8 @@ ng_project( "//:node_modules/@angular/common", "//:node_modules/@angular/core", "//:node_modules/@angular/forms", - "//src/aria/combobox", "//src/aria/listbox", + "//src/aria/simple-combobox", "//src/cdk/overlay", ], ) diff --git a/src/components-examples/aria/autocomplete/autocomplete-auto-select/autocomplete-auto-select-example.html b/src/components-examples/aria/autocomplete/autocomplete-auto-select/autocomplete-auto-select-example.html index b7ec4065b427..c31b8ba1147a 100644 --- a/src/components-examples/aria/autocomplete/autocomplete-auto-select/autocomplete-auto-select-example.html +++ b/src/components-examples/aria/autocomplete/autocomplete-auto-select/autocomplete-auto-select-example.html @@ -1,10 +1,13 @@ -
+
search
- - - + + +
@if (countries().length === 0) {
No results found
} -
+
@for (country of countries(); track country) {
{{country}} diff --git a/src/components-examples/aria/autocomplete/autocomplete-highlight/autocomplete-highlight-example.ts b/src/components-examples/aria/autocomplete/autocomplete-highlight/autocomplete-highlight-example.ts index fc490c2ed96b..160bb3f30535 100644 --- a/src/components-examples/aria/autocomplete/autocomplete-highlight/autocomplete-highlight-example.ts +++ b/src/components-examples/aria/autocomplete/autocomplete-highlight/autocomplete-highlight-example.ts @@ -6,20 +6,15 @@ * found in the LICENSE file at https://angular.dev/license */ -import { - Combobox, - ComboboxInput, - ComboboxPopup, - ComboboxPopupContainer, -} from '@angular/aria/combobox'; +import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/simple-combobox'; import {Listbox, Option} from '@angular/aria/listbox'; import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, + signal, viewChild, - viewChildren, } from '@angular/core'; import {COUNTRIES} from '../countries'; import {OverlayModule} from '@angular/cdk/overlay'; @@ -30,33 +25,20 @@ import {FormsModule} from '@angular/forms'; selector: 'autocomplete-highlight-example', templateUrl: 'autocomplete-highlight-example.html', styleUrl: '../autocomplete.css', - imports: [ - Combobox, - ComboboxInput, - ComboboxPopup, - ComboboxPopupContainer, - Listbox, - Option, - OverlayModule, - FormsModule, - ], + imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option, OverlayModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, }) export class AutocompleteHighlightExample { /** The selected value of the combobox. */ - listbox = viewChild>(Listbox); - - /** The options available in the listbox. */ - options = viewChildren>(Option); - - /** A reference to the ng aria combobox. */ - combobox = viewChild>(Combobox); + readonly listbox = viewChild(Listbox); + readonly combobox = viewChild(Combobox); - /** A reference to the ng aria combobox input. */ - comboboxInput = viewChild(ComboboxInput); + popupExpanded = signal(false); + searchString = signal(''); + selectedOption = signal([]); /** The query string used to filter the list of countries. */ - query = computed(() => this.comboboxInput()?.value() || ''); + query = computed(() => this.searchString()); /** The list of countries filtered by the query. */ countries = computed(() => @@ -64,26 +46,31 @@ export class AutocompleteHighlightExample { ); constructor() { - // Scrolls to the active item when the active option changes. afterRenderEffect(() => { - if (this.combobox()?.expanded()) { - const option = this.options().find(opt => opt.active()); - option?.element.scrollIntoView({block: 'nearest'}); - } + this.listbox()?.scrollActiveItemIntoView(); }); } /** Clears the query and the listbox value. */ clear(): void { - this.comboboxInput()?.value.set(''); - this.listbox?.()?.value.set([]); + this.searchString.set(''); + this.selectedOption.set([]); + } + + onCommit() { + const selectedOption = this.selectedOption(); + if (selectedOption.length > 0) { + this.searchString.set(selectedOption[0]); + } + this.popupExpanded.set(false); + this.combobox()?.element.focus(); } /** Handles keydown events on the clear button. */ onKeydown(event: KeyboardEvent): void { if (event.key === 'Enter') { this.clear(); - this.combobox?.()?.close(); + this.popupExpanded.set(false); event.stopPropagation(); } } diff --git a/src/components-examples/aria/autocomplete/autocomplete-manual/autocomplete-manual-example.html b/src/components-examples/aria/autocomplete/autocomplete-manual/autocomplete-manual-example.html index 3571019382ec..b033cbaf96fa 100644 --- a/src/components-examples/aria/autocomplete/autocomplete-manual/autocomplete-manual-example.html +++ b/src/components-examples/aria/autocomplete/autocomplete-manual/autocomplete-manual-example.html @@ -1,17 +1,9 @@ -
+
search - -
@@ -20,25 +12,24 @@ {{countries().length === 0 ? 'No results found for ' + query() : ''}}
- - + +
@if (countries().length === 0) { -
No results found
+
No results found
} -
+
@for (country of countries(); track country) { -
- {{country}} - check -
+
+ {{country}} + check +
}
-
+
\ No newline at end of file diff --git a/src/components-examples/aria/autocomplete/autocomplete-manual/autocomplete-manual-example.ts b/src/components-examples/aria/autocomplete/autocomplete-manual/autocomplete-manual-example.ts index 7d1a725ad324..376208860da4 100644 --- a/src/components-examples/aria/autocomplete/autocomplete-manual/autocomplete-manual-example.ts +++ b/src/components-examples/aria/autocomplete/autocomplete-manual/autocomplete-manual-example.ts @@ -6,20 +6,15 @@ * found in the LICENSE file at https://angular.dev/license */ -import { - Combobox, - ComboboxInput, - ComboboxPopup, - ComboboxPopupContainer, -} from '@angular/aria/combobox'; +import {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/simple-combobox'; import {Listbox, Option} from '@angular/aria/listbox'; import { afterRenderEffect, ChangeDetectionStrategy, Component, computed, + signal, viewChild, - viewChildren, } from '@angular/core'; import {COUNTRIES} from '../countries'; import {OverlayModule} from '@angular/cdk/overlay'; @@ -30,33 +25,20 @@ import {FormsModule} from '@angular/forms'; selector: 'autocomplete-manual-example', templateUrl: 'autocomplete-manual-example.html', styleUrl: '../autocomplete.css', - imports: [ - Combobox, - ComboboxInput, - ComboboxPopup, - ComboboxPopupContainer, - Listbox, - Option, - OverlayModule, - FormsModule, - ], + imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option, OverlayModule, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, }) export class AutocompleteManualExample { /** The selected value of the combobox. */ - listbox = viewChild>(Listbox); - - /** The options available in the listbox. */ - options = viewChildren>(Option); - - /** A reference to the ng aria combobox. */ - combobox = viewChild>(Combobox); + readonly listbox = viewChild(Listbox); + readonly combobox = viewChild(Combobox); - /** A reference to the ng aria combobox input. */ - comboboxInput = viewChild(ComboboxInput); + popupExpanded = signal(false); + searchString = signal(''); + selectedOption = signal([]); /** The query string used to filter the list of countries. */ - query = computed(() => this.comboboxInput()?.value() || ''); + query = computed(() => this.searchString()); /** The list of countries filtered by the query. */ countries = computed(() => @@ -64,26 +46,31 @@ export class AutocompleteManualExample { ); constructor() { - // Scrolls to the active item when the active option changes. afterRenderEffect(() => { - if (this.combobox()?.expanded()) { - const option = this.options().find(opt => opt.active()); - option?.element.scrollIntoView({block: 'nearest'}); - } + this.listbox()?.scrollActiveItemIntoView(); }); } /** Clears the query and the listbox value. */ clear(): void { - this.comboboxInput()?.value.set(''); - this.listbox?.()?.value.set([]); + this.searchString.set(''); + this.selectedOption.set([]); + } + + onCommit() { + const selectedOption = this.selectedOption(); + if (selectedOption.length > 0) { + this.searchString.set(selectedOption[0]); + } + this.popupExpanded.set(false); + this.combobox()?.element.focus(); } /** Handles keydown events on the clear button. */ onKeydown(event: KeyboardEvent): void { if (event.key === 'Enter') { this.clear(); - this.combobox?.()?.close(); + this.popupExpanded.set(false); event.stopPropagation(); } } diff --git a/src/components-examples/aria/autocomplete/autocomplete.css b/src/components-examples/aria/autocomplete/autocomplete.css index d0a57a1f7def..ffa2d0bb763c 100644 --- a/src/components-examples/aria/autocomplete/autocomplete.css +++ b/src/components-examples/aria/autocomplete/autocomplete.css @@ -18,7 +18,7 @@ position: absolute; } -[ngComboboxInput] { +input[ngCombobox] { width: 13rem; font-size: 0.9rem; border-radius: var(--mat-sys-corner-extra-small); @@ -28,15 +28,13 @@ background-color: var(--mat-sys-surface); } -[ngComboboxInput][aria-disabled='true'] { +input[ngCombobox][aria-disabled='true'], +input[ngCombobox]:disabled { cursor: default; opacity: 0.5; background-color: var(--mat-sys-surface-dim); } -[ngCombobox]:has([aria-expanded='false']) .example-popup { - display: none; -} .example-clear-button { position: absolute; From 15bf0bf7435b48ab74c4830451cc2112a9899d04 Mon Sep 17 00:00:00 2001 From: tjshiu <35056071+tjshiu@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:22:15 -0700 Subject: [PATCH 2/2] refactor(aria/combobox): replace tabbable with tabIndex in autocomplete examples --- .../autocomplete-auto-select-example.html | 2 +- .../autocomplete-disabled/autocomplete-disabled-example.html | 2 +- .../autocomplete-highlight/autocomplete-highlight-example.html | 2 +- .../autocomplete-manual/autocomplete-manual-example.html | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components-examples/aria/autocomplete/autocomplete-auto-select/autocomplete-auto-select-example.html b/src/components-examples/aria/autocomplete/autocomplete-auto-select/autocomplete-auto-select-example.html index c31b8ba1147a..6a04d3e23612 100644 --- a/src/components-examples/aria/autocomplete/autocomplete-auto-select/autocomplete-auto-select-example.html +++ b/src/components-examples/aria/autocomplete/autocomplete-auto-select/autocomplete-auto-select-example.html @@ -30,7 +30,7 @@
No results found
} -
+
@for (country of countries(); track country) {
{{country}} diff --git a/src/components-examples/aria/autocomplete/autocomplete-disabled/autocomplete-disabled-example.html b/src/components-examples/aria/autocomplete/autocomplete-disabled/autocomplete-disabled-example.html index 725b6ba82e86..4ae9c89500a5 100644 --- a/src/components-examples/aria/autocomplete/autocomplete-disabled/autocomplete-disabled-example.html +++ b/src/components-examples/aria/autocomplete/autocomplete-disabled/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/autocomplete/autocomplete-highlight/autocomplete-highlight-example.html b/src/components-examples/aria/autocomplete/autocomplete-highlight/autocomplete-highlight-example.html index 6cf5fc869cca..617e706877cf 100644 --- a/src/components-examples/aria/autocomplete/autocomplete-highlight/autocomplete-highlight-example.html +++ b/src/components-examples/aria/autocomplete/autocomplete-highlight/autocomplete-highlight-example.html @@ -31,7 +31,7 @@
No results found
} -
+
@for (country of countries(); track country) {
{{country}} diff --git a/src/components-examples/aria/autocomplete/autocomplete-manual/autocomplete-manual-example.html b/src/components-examples/aria/autocomplete/autocomplete-manual/autocomplete-manual-example.html index b033cbaf96fa..e561431c9938 100644 --- a/src/components-examples/aria/autocomplete/autocomplete-manual/autocomplete-manual-example.html +++ b/src/components-examples/aria/autocomplete/autocomplete-manual/autocomplete-manual-example.html @@ -20,7 +20,7 @@
No results found
} -
@for (country of countries(); track country) {