From dedc4643f202bf30cb2a1734a3cde1e0d4a3475d Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Mon, 8 Dec 2025 15:30:53 -0500 Subject: [PATCH 1/3] docs: create aria autocomplete docs --- .../aria/autocomplete/BUILD.bazel | 29 +++ .../autocomplete-auto-select-example.html | 33 +++ .../autocomplete-auto-select-example.ts | 70 +++++++ .../autocomplete-disabled-example.html | 33 +++ .../autocomplete-disabled-example.ts | 70 +++++++ .../autocomplete-highlight-example.html | 33 +++ .../autocomplete-highlight-example.ts | 70 +++++++ .../autocomplete-manual-example.html | 33 +++ .../autocomplete-manual-example.ts | 70 +++++++ .../aria/autocomplete/autocomplete.css | 103 +++++++++ .../aria/autocomplete/countries.ts | 197 ++++++++++++++++++ .../aria/autocomplete/index.ts | 4 + src/dev-app/BUILD.bazel | 1 + src/dev-app/aria-autocomplete/BUILD.bazel | 17 ++ .../aria-autocomplete/autocomplete-demo.css | 9 + .../aria-autocomplete/autocomplete-demo.html | 19 ++ .../aria-autocomplete/autocomplete-demo.ts | 21 ++ src/dev-app/dev-app/dev-app-layout.ts | 1 + src/dev-app/routes.ts | 5 + 19 files changed, 818 insertions(+) create mode 100644 src/components-examples/aria/autocomplete/BUILD.bazel create mode 100644 src/components-examples/aria/autocomplete/autocomplete-auto-select/autocomplete-auto-select-example.html create mode 100644 src/components-examples/aria/autocomplete/autocomplete-auto-select/autocomplete-auto-select-example.ts create mode 100644 src/components-examples/aria/autocomplete/autocomplete-disabled/autocomplete-disabled-example.html create mode 100644 src/components-examples/aria/autocomplete/autocomplete-disabled/autocomplete-disabled-example.ts create mode 100644 src/components-examples/aria/autocomplete/autocomplete-highlight/autocomplete-highlight-example.html create mode 100644 src/components-examples/aria/autocomplete/autocomplete-highlight/autocomplete-highlight-example.ts create mode 100644 src/components-examples/aria/autocomplete/autocomplete-manual/autocomplete-manual-example.html create mode 100644 src/components-examples/aria/autocomplete/autocomplete-manual/autocomplete-manual-example.ts create mode 100644 src/components-examples/aria/autocomplete/autocomplete.css create mode 100644 src/components-examples/aria/autocomplete/countries.ts create mode 100644 src/components-examples/aria/autocomplete/index.ts create mode 100644 src/dev-app/aria-autocomplete/BUILD.bazel create mode 100644 src/dev-app/aria-autocomplete/autocomplete-demo.css create mode 100644 src/dev-app/aria-autocomplete/autocomplete-demo.html create mode 100644 src/dev-app/aria-autocomplete/autocomplete-demo.ts diff --git a/src/components-examples/aria/autocomplete/BUILD.bazel b/src/components-examples/aria/autocomplete/BUILD.bazel new file mode 100644 index 000000000000..7b5c57c7ef81 --- /dev/null +++ b/src/components-examples/aria/autocomplete/BUILD.bazel @@ -0,0 +1,29 @@ +load("//tools:defaults.bzl", "ng_project") + +package(default_visibility = ["//visibility:public"]) + +ng_project( + name = "autocomplete", + srcs = glob(["**/*.ts"]), + assets = glob([ + "**/*.html", + "**/*.css", + ]), + deps = [ + "//:node_modules/@angular/common", + "//:node_modules/@angular/core", + "//:node_modules/@angular/forms", + "//src/aria/combobox", + "//src/aria/listbox", + "//src/cdk/overlay", + ], +) + +filegroup( + name = "source-files", + srcs = glob([ + "**/*.html", + "**/*.css", + "**/*.ts", + ]), +) 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 new file mode 100644 index 000000000000..a21028c4370d --- /dev/null +++ b/src/components-examples/aria/autocomplete/autocomplete-auto-select/autocomplete-auto-select-example.html @@ -0,0 +1,33 @@ +
+
+ search + +
+ + + +
+ @if (countries().length === 0) { +
No results found
+ } + +
+ @for (country of countries(); track country) { +
+ {{country}} + check +
+ } +
+
+
+
+
diff --git a/src/components-examples/aria/autocomplete/autocomplete-auto-select/autocomplete-auto-select-example.ts b/src/components-examples/aria/autocomplete/autocomplete-auto-select/autocomplete-auto-select-example.ts new file mode 100644 index 000000000000..d7bddf758585 --- /dev/null +++ b/src/components-examples/aria/autocomplete/autocomplete-auto-select/autocomplete-auto-select-example.ts @@ -0,0 +1,70 @@ +/** + * @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 { + Combobox, + ComboboxInput, + ComboboxPopup, + ComboboxPopupContainer, +} from '@angular/aria/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'; +import {FormsModule} from '@angular/forms'; + +/** @title Autocomplete with auto-select filtering. */ +@Component({ + selector: 'autocomplete-auto-select-example', + templateUrl: 'autocomplete-auto-select-example.html', + styleUrl: '../autocomplete.css', + imports: [ + Combobox, + ComboboxInput, + ComboboxPopup, + ComboboxPopupContainer, + Listbox, + Option, + OverlayModule, + FormsModule, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AutocompleteAutoSelectExample { + /** The options available in the listbox. */ + options = viewChildren>(Option); + + /** A reference to the ng aria combobox. */ + combobox = viewChild>(Combobox); + + /** The query string used to filter the list of countries. */ + query = signal(''); + + /** The list of countries filtered by the query. */ + countries = computed(() => + COUNTRIES.filter(country => country.toLowerCase().startsWith(this.query().toLowerCase())), + ); + + 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'}); + } + }); + } +} 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 new file mode 100644 index 000000000000..2fb1aed3365c --- /dev/null +++ b/src/components-examples/aria/autocomplete/autocomplete-disabled/autocomplete-disabled-example.html @@ -0,0 +1,33 @@ +
+
+ search + +
+ + + +
+ @if (countries().length === 0) { +
No results found
+ } + +
+ @for (country of countries(); track country) { +
+ {{country}} + check +
+ } +
+
+
+
+
diff --git a/src/components-examples/aria/autocomplete/autocomplete-disabled/autocomplete-disabled-example.ts b/src/components-examples/aria/autocomplete/autocomplete-disabled/autocomplete-disabled-example.ts new file mode 100644 index 000000000000..ed590c7f3411 --- /dev/null +++ b/src/components-examples/aria/autocomplete/autocomplete-disabled/autocomplete-disabled-example.ts @@ -0,0 +1,70 @@ +/** + * @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 { + Combobox, + ComboboxInput, + ComboboxPopup, + ComboboxPopupContainer, +} from '@angular/aria/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'; +import {FormsModule} from '@angular/forms'; + +/** @title Disabled autocomplete. */ +@Component({ + selector: 'autocomplete-disabled-example', + templateUrl: 'autocomplete-disabled-example.html', + styleUrl: '../autocomplete.css', + imports: [ + Combobox, + ComboboxInput, + ComboboxPopup, + ComboboxPopupContainer, + Listbox, + Option, + OverlayModule, + FormsModule, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AutocompleteDisabledExample { + /** The options available in the listbox. */ + options = viewChildren>(Option); + + /** A reference to the ng aria combobox. */ + combobox = viewChild>(Combobox); + + /** The query string used to filter the list of countries. */ + query = signal('United States of America'); + + /** The list of countries filtered by the query. */ + countries = computed(() => + COUNTRIES.filter(country => country.toLowerCase().startsWith(this.query().toLowerCase())), + ); + + 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'}); + } + }); + } +} 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 new file mode 100644 index 000000000000..342963e9f6a4 --- /dev/null +++ b/src/components-examples/aria/autocomplete/autocomplete-highlight/autocomplete-highlight-example.html @@ -0,0 +1,33 @@ +
+
+ search + +
+ + + +
+ @if (countries().length === 0) { +
No results found
+ } + +
+ @for (country of countries(); track country) { +
+ {{country}} + check +
+ } +
+
+
+
+
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 new file mode 100644 index 000000000000..717810e50200 --- /dev/null +++ b/src/components-examples/aria/autocomplete/autocomplete-highlight/autocomplete-highlight-example.ts @@ -0,0 +1,70 @@ +/** + * @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 { + Combobox, + ComboboxInput, + ComboboxPopup, + ComboboxPopupContainer, +} from '@angular/aria/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'; +import {FormsModule} from '@angular/forms'; + +/** @title Autocomplete with highlighted filtering. */ +@Component({ + selector: 'autocomplete-highlight-example', + templateUrl: 'autocomplete-highlight-example.html', + styleUrl: '../autocomplete.css', + imports: [ + Combobox, + ComboboxInput, + ComboboxPopup, + ComboboxPopupContainer, + Listbox, + Option, + OverlayModule, + FormsModule, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AutocompleteHighlightExample { + /** The options available in the listbox. */ + options = viewChildren>(Option); + + /** A reference to the ng aria combobox. */ + combobox = viewChild>(Combobox); + + /** The query string used to filter the list of countries. */ + query = signal(''); + + /** The list of countries filtered by the query. */ + countries = computed(() => + COUNTRIES.filter(country => country.toLowerCase().startsWith(this.query().toLowerCase())), + ); + + 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'}); + } + }); + } +} 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 new file mode 100644 index 000000000000..3f1ce2951cd6 --- /dev/null +++ b/src/components-examples/aria/autocomplete/autocomplete-manual/autocomplete-manual-example.html @@ -0,0 +1,33 @@ +
+
+ search + +
+ + + +
+ @if (countries().length === 0) { +
No results found
+ } + +
+ @for (country of countries(); track country) { +
+ {{country}} + check +
+ } +
+
+
+
+
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 new file mode 100644 index 000000000000..9de89de92005 --- /dev/null +++ b/src/components-examples/aria/autocomplete/autocomplete-manual/autocomplete-manual-example.ts @@ -0,0 +1,70 @@ +/** + * @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 { + Combobox, + ComboboxInput, + ComboboxPopup, + ComboboxPopupContainer, +} from '@angular/aria/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'; +import {FormsModule} from '@angular/forms'; + +/** @title Autocomplete with manual filtering. */ +@Component({ + selector: 'autocomplete-manual-example', + templateUrl: 'autocomplete-manual-example.html', + styleUrl: '../autocomplete.css', + imports: [ + Combobox, + ComboboxInput, + ComboboxPopup, + ComboboxPopupContainer, + Listbox, + Option, + OverlayModule, + FormsModule, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AutocompleteManualExample { + /** The options available in the listbox. */ + options = viewChildren>(Option); + + /** A reference to the ng aria combobox. */ + combobox = viewChild>(Combobox); + + /** The query string used to filter the list of countries. */ + query = signal(''); + + /** The list of countries filtered by the query. */ + countries = computed(() => + COUNTRIES.filter(country => country.toLowerCase().startsWith(this.query().toLowerCase())), + ); + + 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'}); + } + }); + } +} diff --git a/src/components-examples/aria/autocomplete/autocomplete.css b/src/components-examples/aria/autocomplete/autocomplete.css new file mode 100644 index 000000000000..2d04ad51f388 --- /dev/null +++ b/src/components-examples/aria/autocomplete/autocomplete.css @@ -0,0 +1,103 @@ +.example-autocomplete { + display: flex; + position: relative; + align-items: center; + + /* stylelint-disable-next-line material/no-prefixes -- Valid in all remotely recent browsers. */ + width: fit-content; +} + +.example-search-icon, +.example-check-icon { + font-size: 1.25rem; + pointer-events: none; +} + +.example-search-icon { + left: 0.75rem; + position: absolute; +} + +[ngComboboxInput] { + width: 13rem; + font-size: 0.9rem; + border-radius: var(--mat-sys-corner-extra-small); + padding: 0.7rem 0.7rem 0.7rem 2.5rem; + outline-color: var(--mat-sys-primary); + border: 1px solid var(--mat-sys-outline); + background-color: var(--mat-sys-surface); +} + +[ngComboboxInput][aria-disabled='true'] { + cursor: default; + opacity: 0.5; + background-color: var(--mat-sys-surface-dim); +} + +[ngCombobox]:has([aria-expanded='false']) .example-popup { + display: none; +} + +.example-popup { + width: 100%; + margin-top: 2px; + padding: 0.1rem; + max-height: 11rem; + border-radius: var(--mat-sys-corner-extra-small); + background-color: var(--mat-sys-surface); + border: 1px solid var(--mat-sys-outline); +} + +.example-no-results { + padding: 1rem; +} + +[ngListbox] { + gap: 2px; + height: 100%; + display: flex; + overflow: auto; + flex-direction: column; +} + +[ngOption] { + display: flex; + cursor: pointer; + align-items: center; + margin: 1px; + font-size: 0.9rem; + padding: 0.7rem; + border-radius: var(--mat-sys-corner-extra-small); +} + +[ngOption][aria-disabled='true'] { + cursor: default; + opacity: 0.5; + background-color: var(--mat-sys-surface-dim); +} + +[ngOption]:hover { + background-color: color-mix(in srgb, var(--mat-sys-primary) 5%, transparent); +} + +[ngOption][data-active='true'] { + outline-offset: -2px; + outline: 2px solid var(--mat-sys-primary); +} + +[ngOption][aria-selected='true'] { + color: var(--mat-sys-primary); + background-color: color-mix(in srgb, var(--mat-sys-primary) 10%, transparent); +} + +[ngOption]:not([aria-selected='true']) .example-check-icon { + display: none; +} + +.example-option-label { + flex: 1; +} + +.example-check-icon { + font-size: 0.9rem; +} diff --git a/src/components-examples/aria/autocomplete/countries.ts b/src/components-examples/aria/autocomplete/countries.ts new file mode 100644 index 000000000000..3d9c4ba4f079 --- /dev/null +++ b/src/components-examples/aria/autocomplete/countries.ts @@ -0,0 +1,197 @@ +export const COUNTRIES = [ + 'Afghanistan', + 'Albania', + 'Algeria', + 'Andorra', + 'Angola', + 'Antigua and Barbuda', + 'Argentina', + 'Armenia', + 'Australia', + 'Austria', + 'Azerbaijan', + 'Bahamas', + 'Bahrain', + 'Bangladesh', + 'Barbados', + 'Belarus', + 'Belgium', + 'Belize', + 'Benin', + 'Bhutan', + 'Bolivia', + 'Bosnia and Herzegovina', + 'Botswana', + 'Brazil', + 'Brunei', + 'Bulgaria', + 'Burkina Faso', + 'Burundi', + 'Cabo Verde', + 'Cambodia', + 'Cameroon', + 'Canada', + 'Central African Republic', + 'Chad', + 'Chile', + 'China', + 'Colombia', + 'Comoros', + 'Congo (Congo-Brazzaville)', + 'Costa Rica', + "Côte d'Ivoire", + 'Croatia', + 'Cuba', + 'Cyprus', + 'Czechia (Czech Republic)', + 'Democratic Republic of the Congo', + 'Denmark', + 'Djibouti', + 'Dominica', + 'Dominican Republic', + 'Ecuador', + 'Egypt', + 'El Salvador', + 'Equatorial Guinea', + 'Eritrea', + 'Estonia', + 'Eswatini (fmr. ""Swaziland"")', + 'Ethiopia', + 'Fiji', + 'Finland', + 'France', + 'Gabon', + 'Gambia', + 'Georgia', + 'Germany', + 'Ghana', + 'Greece', + 'Grenada', + 'Guatemala', + 'Guinea', + 'Guinea-Bissau', + 'Guyana', + 'Haiti', + 'Holy See', + 'Honduras', + 'Hungary', + 'Iceland', + 'India', + 'Indonesia', + 'Iran', + 'Iraq', + 'Ireland', + 'Israel', + 'Italy', + 'Jamaica', + 'Japan', + 'Jordan', + 'Kazakhstan', + 'Kenya', + 'Kiribati', + 'Kuwait', + 'Kyrgyzstan', + 'Laos', + 'Latvia', + 'Lebanon', + 'Lesotho', + 'Liberia', + 'Libya', + 'Liechtenstein', + 'Lithuania', + 'Luxembourg', + 'Madagascar', + 'Malawi', + 'Malaysia', + 'Maldives', + 'Mali', + 'Malta', + 'Marshall Islands', + 'Mauritania', + 'Mauritius', + 'Mexico', + 'Micronesia', + 'Moldova', + 'Monaco', + 'Mongolia', + 'Montenegro', + 'Morocco', + 'Mozambique', + 'Myanmar (formerly Burma)', + 'Namibia', + 'Nauru', + 'Nepal', + 'Netherlands', + 'New Zealand', + 'Nicaragua', + 'Niger', + 'Nigeria', + 'North Korea', + 'North Macedonia', + 'Norway', + 'Oman', + 'Pakistan', + 'Palau', + 'Palestine State', + 'Panama', + 'Papua New Guinea', + 'Paraguay', + 'Peru', + 'Philippines', + 'Poland', + 'Portugal', + 'Qatar', + 'Romania', + 'Russia', + 'Rwanda', + 'Saint Kitts and Nevis', + 'Saint Lucia', + 'Saint Vincent and the Grenadines', + 'Samoa', + 'San Marino', + 'Sao Tome and Principe', + 'Saudi Arabia', + 'Senegal', + 'Serbia', + 'Seychelles', + 'Sierra Leone', + 'Singapore', + 'Slovakia', + 'Slovenia', + 'Solomon Islands', + 'Somalia', + 'South Africa', + 'South Korea', + 'South Sudan', + 'Spain', + 'Sri Lanka', + 'Sudan', + 'Suriname', + 'Sweden', + 'Switzerland', + 'Syria', + 'Tajikistan', + 'Tanzania', + 'Thailand', + 'Timor-Leste', + 'Togo', + 'Tonga', + 'Trinidad and Tobago', + 'Tunisia', + 'Turkey', + 'Turkmenistan', + 'Tuvalu', + 'Uganda', + 'Ukraine', + 'United Arab Emirates', + 'United Kingdom', + 'United States of America', + 'Uruguay', + 'Uzbekistan', + 'Vanuatu', + 'Venezuela', + 'Vietnam', + 'Yemen', + 'Zambia', + 'Zimbabwe', +]; diff --git a/src/components-examples/aria/autocomplete/index.ts b/src/components-examples/aria/autocomplete/index.ts new file mode 100644 index 000000000000..15cb925c5e28 --- /dev/null +++ b/src/components-examples/aria/autocomplete/index.ts @@ -0,0 +1,4 @@ +export {AutocompleteAutoSelectExample} from './autocomplete-auto-select/autocomplete-auto-select-example'; +export {AutocompleteManualExample} from './autocomplete-manual/autocomplete-manual-example'; +export {AutocompleteHighlightExample} from './autocomplete-highlight/autocomplete-highlight-example'; +export {AutocompleteDisabledExample} from './autocomplete-disabled/autocomplete-disabled-example'; diff --git a/src/dev-app/BUILD.bazel b/src/dev-app/BUILD.bazel index 51fc01080c97..faf459be208c 100644 --- a/src/dev-app/BUILD.bazel +++ b/src/dev-app/BUILD.bazel @@ -26,6 +26,7 @@ ng_project( "//src/cdk/bidi", "//src/cdk/overlay", "//src/dev-app/aria-accordion", + "//src/dev-app/aria-autocomplete", "//src/dev-app/aria-combobox", "//src/dev-app/aria-grid", "//src/dev-app/aria-listbox", diff --git a/src/dev-app/aria-autocomplete/BUILD.bazel b/src/dev-app/aria-autocomplete/BUILD.bazel new file mode 100644 index 000000000000..571ec2743531 --- /dev/null +++ b/src/dev-app/aria-autocomplete/BUILD.bazel @@ -0,0 +1,17 @@ +load("//tools:defaults.bzl", "ng_project") + +package(default_visibility = ["//visibility:public"]) + +ng_project( + name = "aria-autocomplete", + srcs = glob(["**/*.ts"]), + assets = [ + "autocomplete-demo.css", + "autocomplete-demo.html", + ], + deps = [ + "//:node_modules/@angular/core", + "//:node_modules/@angular/forms", + "//src/components-examples/aria/autocomplete", + ], +) diff --git a/src/dev-app/aria-autocomplete/autocomplete-demo.css b/src/dev-app/aria-autocomplete/autocomplete-demo.css new file mode 100644 index 000000000000..e91c57a78bd9 --- /dev/null +++ b/src/dev-app/aria-autocomplete/autocomplete-demo.css @@ -0,0 +1,9 @@ +:host { + display: flex; + flex-wrap: wrap; + gap: 10rem; +} + +.example-container { + width: 250px; +} diff --git a/src/dev-app/aria-autocomplete/autocomplete-demo.html b/src/dev-app/aria-autocomplete/autocomplete-demo.html new file mode 100644 index 000000000000..a010a9c87585 --- /dev/null +++ b/src/dev-app/aria-autocomplete/autocomplete-demo.html @@ -0,0 +1,19 @@ +
+

Auto select

+ +
+ +
+

Manual selection

+ +
+ +
+

Highlighted auto selection

+ +
+ +
+

Disabled autocomplete

+ +
diff --git a/src/dev-app/aria-autocomplete/autocomplete-demo.ts b/src/dev-app/aria-autocomplete/autocomplete-demo.ts new file mode 100644 index 000000000000..38179f50296e --- /dev/null +++ b/src/dev-app/aria-autocomplete/autocomplete-demo.ts @@ -0,0 +1,21 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import { + AutocompleteAutoSelectExample, + AutocompleteManualExample, + AutocompleteHighlightExample, + AutocompleteDisabledExample, +} from '@angular/components-examples/aria/autocomplete'; + +@Component({ + selector: 'autocomplete-demo', + templateUrl: 'autocomplete-demo.html', + styleUrl: 'autocomplete-demo.css', + imports: [ + AutocompleteAutoSelectExample, + AutocompleteManualExample, + AutocompleteHighlightExample, + AutocompleteDisabledExample, + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AutocompleteDemo {} diff --git a/src/dev-app/dev-app/dev-app-layout.ts b/src/dev-app/dev-app/dev-app-layout.ts index f3b4d991773f..fa06844c3584 100644 --- a/src/dev-app/dev-app/dev-app-layout.ts +++ b/src/dev-app/dev-app/dev-app-layout.ts @@ -64,6 +64,7 @@ export class DevAppLayout { {name: 'CDK Dialog', route: '/cdk-dialog'}, {name: 'Aria Accordion', route: '/aria-accordion'}, {name: 'Aria Combobox', route: '/aria-combobox'}, + {name: 'Aria Autocomplete', route: '/aria-autocomplete'}, {name: 'Aria Grid', route: '/aria-grid'}, {name: 'Aria Listbox', route: '/aria-listbox'}, {name: 'Aria Menu', route: '/aria-menu'}, diff --git a/src/dev-app/routes.ts b/src/dev-app/routes.ts index f719de30b300..03fefd5abd7c 100644 --- a/src/dev-app/routes.ts +++ b/src/dev-app/routes.ts @@ -72,6 +72,11 @@ export const DEV_APP_ROUTES: Routes = [ path: 'aria-tree', loadComponent: () => import('./aria-tree/tree-demo').then(m => m.TreeDemo), }, + { + path: 'aria-autocomplete', + loadComponent: () => + import('./aria-autocomplete/autocomplete-demo').then(m => m.AutocompleteDemo), + }, { path: 'aria-toolbar', loadComponent: () => import('./aria-toolbar/toolbar-demo').then(m => m.ToolbarDemo), From cd7556d9eff7f1c14df6a5308048caca7bab21e3 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Tue, 9 Dec 2025 10:07:09 -0500 Subject: [PATCH 2/3] fix(aria/combobox): focus out selection bug * Navigating away from an auto-select or highlight combobox input should only trigger selection if the combobox popup is expanded. --- src/aria/private/combobox/combobox.spec.ts | 7 +++++++ src/aria/private/combobox/combobox.ts | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/src/aria/private/combobox/combobox.spec.ts b/src/aria/private/combobox/combobox.spec.ts index 2922a50e61fc..dfa8779b4e36 100644 --- a/src/aria/private/combobox/combobox.spec.ts +++ b/src/aria/private/combobox/combobox.spec.ts @@ -488,6 +488,13 @@ describe('Combobox with Listbox Pattern', () => { combobox.onFocusOut(new FocusEvent('focusout')); expect(inputEl.value).toBe('Apple'); }); + + it('should not commit an option on focusout if the popup is closed', () => { + type('A'); + combobox.onKeydown(escape()); + combobox.onFocusOut(new FocusEvent('focusout')); + expect(inputEl.value).toBe('A'); + }); }); describe('when filterMode is "highlight"', () => { diff --git a/src/aria/private/combobox/combobox.ts b/src/aria/private/combobox/combobox.ts index 63027759d849..1983b4741424 100644 --- a/src/aria/private/combobox/combobox.ts +++ b/src/aria/private/combobox/combobox.ts @@ -390,6 +390,10 @@ export class ComboboxPattern, V> { ) { this.isFocused.set(false); + if (!this.expanded()) { + return; + } + if (this.readonly()) { this.close(); return; From 6f0a96615fd3bd398dada47cde4671cf52917005 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Tue, 9 Dec 2025 10:39:22 -0500 Subject: [PATCH 3/3] fix(aria/combobox): disabled options bug * The combobox should not close the popup if the user tries to select a disabled option. --- src/aria/private/combobox/combobox.spec.ts | 9 +++++++++ src/aria/private/combobox/combobox.ts | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/src/aria/private/combobox/combobox.spec.ts b/src/aria/private/combobox/combobox.spec.ts index dfa8779b4e36..5c374f5e2fc2 100644 --- a/src/aria/private/combobox/combobox.spec.ts +++ b/src/aria/private/combobox/combobox.spec.ts @@ -327,6 +327,15 @@ describe('Combobox with Listbox Pattern', () => { expect(combobox.expanded()).toBe(false); }); + it('should not close on Enter if the option is disabled', () => { + const {combobox, options} = getPatterns(); + options()[0].disabled.set(true); + combobox.onKeydown(down()); + expect(combobox.expanded()).toBe(true); + combobox.onKeydown(enter()); + expect(combobox.expanded()).toBe(true); + }); + it('should close on focusout', () => { const {combobox} = getPatterns(); combobox.onKeydown(down()); diff --git a/src/aria/private/combobox/combobox.ts b/src/aria/private/combobox/combobox.ts index 1983b4741424..9c182f0ad62e 100644 --- a/src/aria/private/combobox/combobox.ts +++ b/src/aria/private/combobox/combobox.ts @@ -637,6 +637,12 @@ export class ComboboxPattern, V> { select(opts: {item?: T; commit?: boolean; close?: boolean} = {}) { const controls = this.listControls(); + const item = opts.item ?? controls?.getActiveItem(); + + if (item?.disabled()) { + return; + } + if (opts.item) { controls?.focus(opts.item, {focusElement: false}); }