Skip to content

Commit 7bdc028

Browse files
committed
docs: create aria autocomplete docs (#32499)
* docs: create aria autocomplete docs * 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. * fix(aria/combobox): disabled options bug * The combobox should not close the popup if the user tries to select a disabled option. (cherry picked from commit ad3e7d2)
1 parent 6977081 commit 7bdc028

21 files changed

+844
-0
lines changed

src/aria/private/combobox/combobox.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,15 @@ describe('Combobox with Listbox Pattern', () => {
327327
expect(combobox.expanded()).toBe(false);
328328
});
329329

330+
it('should not close on Enter if the option is disabled', () => {
331+
const {combobox, options} = getPatterns();
332+
options()[0].disabled.set(true);
333+
combobox.onKeydown(down());
334+
expect(combobox.expanded()).toBe(true);
335+
combobox.onKeydown(enter());
336+
expect(combobox.expanded()).toBe(true);
337+
});
338+
330339
it('should close on focusout', () => {
331340
const {combobox} = getPatterns();
332341
combobox.onKeydown(down());
@@ -488,6 +497,13 @@ describe('Combobox with Listbox Pattern', () => {
488497
combobox.onFocusOut(new FocusEvent('focusout'));
489498
expect(inputEl.value).toBe('Apple');
490499
});
500+
501+
it('should not commit an option on focusout if the popup is closed', () => {
502+
type('A');
503+
combobox.onKeydown(escape());
504+
combobox.onFocusOut(new FocusEvent('focusout'));
505+
expect(inputEl.value).toBe('A');
506+
});
491507
});
492508

493509
describe('when filterMode is "highlight"', () => {

src/aria/private/combobox/combobox.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,10 @@ export class ComboboxPattern<T extends ListItem<V>, V> {
390390
) {
391391
this.isFocused.set(false);
392392

393+
if (!this.expanded()) {
394+
return;
395+
}
396+
393397
if (this.readonly()) {
394398
this.close();
395399
return;
@@ -633,6 +637,12 @@ export class ComboboxPattern<T extends ListItem<V>, V> {
633637
select(opts: {item?: T; commit?: boolean; close?: boolean} = {}) {
634638
const controls = this.listControls();
635639

640+
const item = opts.item ?? controls?.getActiveItem();
641+
642+
if (item?.disabled()) {
643+
return;
644+
}
645+
636646
if (opts.item) {
637647
controls?.focus(opts.item, {focusElement: false});
638648
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
load("//tools:defaults.bzl", "ng_project")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ng_project(
6+
name = "autocomplete",
7+
srcs = glob(["**/*.ts"]),
8+
assets = glob([
9+
"**/*.html",
10+
"**/*.css",
11+
]),
12+
deps = [
13+
"//:node_modules/@angular/common",
14+
"//:node_modules/@angular/core",
15+
"//:node_modules/@angular/forms",
16+
"//src/aria/combobox",
17+
"//src/aria/listbox",
18+
"//src/cdk/overlay",
19+
],
20+
)
21+
22+
filegroup(
23+
name = "source-files",
24+
srcs = glob([
25+
"**/*.html",
26+
"**/*.css",
27+
"**/*.ts",
28+
]),
29+
)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<div ngCombobox filterMode="auto-select">
2+
<div #origin class="example-autocomplete">
3+
<span class="example-search-icon material-symbols-outlined" translate="no">search</span>
4+
<input
5+
aria-label="Label dropdown"
6+
placeholder="Select a country"
7+
[(ngModel)]="query"
8+
ngComboboxInput
9+
/>
10+
</div>
11+
12+
<ng-template ngComboboxPopupContainer>
13+
<ng-template
14+
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
15+
[cdkConnectedOverlayOpen]="true"
16+
>
17+
<div class="example-popup">
18+
@if (countries().length === 0) {
19+
<div class="example-no-results">No results found</div>
20+
}
21+
22+
<div ngListbox>
23+
@for (country of countries(); track country) {
24+
<div ngOption [value]="country" [label]="country" [disabled]="country === 'Brazil'">
25+
<span class="example-option-label">{{country}}</span>
26+
<span class="example-check-icon material-symbols-outlined" translate="no">check</span>
27+
</div>
28+
}
29+
</div>
30+
</div>
31+
</ng-template>
32+
</ng-template>
33+
</div>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
Combobox,
11+
ComboboxInput,
12+
ComboboxPopup,
13+
ComboboxPopupContainer,
14+
} from '@angular/aria/combobox';
15+
import {Listbox, Option} from '@angular/aria/listbox';
16+
import {
17+
afterRenderEffect,
18+
ChangeDetectionStrategy,
19+
Component,
20+
computed,
21+
signal,
22+
viewChild,
23+
viewChildren,
24+
} from '@angular/core';
25+
import {COUNTRIES} from '../countries';
26+
import {OverlayModule} from '@angular/cdk/overlay';
27+
import {FormsModule} from '@angular/forms';
28+
29+
/** @title Autocomplete with auto-select filtering. */
30+
@Component({
31+
selector: 'autocomplete-auto-select-example',
32+
templateUrl: 'autocomplete-auto-select-example.html',
33+
styleUrl: '../autocomplete.css',
34+
imports: [
35+
Combobox,
36+
ComboboxInput,
37+
ComboboxPopup,
38+
ComboboxPopupContainer,
39+
Listbox,
40+
Option,
41+
OverlayModule,
42+
FormsModule,
43+
],
44+
changeDetection: ChangeDetectionStrategy.OnPush,
45+
})
46+
export class AutocompleteAutoSelectExample {
47+
/** The options available in the listbox. */
48+
options = viewChildren<Option<string>>(Option);
49+
50+
/** A reference to the ng aria combobox. */
51+
combobox = viewChild<Combobox<string>>(Combobox);
52+
53+
/** The query string used to filter the list of countries. */
54+
query = signal('');
55+
56+
/** The list of countries filtered by the query. */
57+
countries = computed(() =>
58+
COUNTRIES.filter(country => country.toLowerCase().startsWith(this.query().toLowerCase())),
59+
);
60+
61+
constructor() {
62+
// Scrolls to the active item when the active option changes.
63+
afterRenderEffect(() => {
64+
if (this.combobox()?.expanded()) {
65+
const option = this.options().find(opt => opt.active());
66+
option?.element.scrollIntoView({block: 'nearest'});
67+
}
68+
});
69+
}
70+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<div ngCombobox disabled>
2+
<div #origin class="example-autocomplete">
3+
<span class="example-search-icon material-symbols-outlined" translate="no">search</span>
4+
<input
5+
aria-label="Label dropdown"
6+
placeholder="Select a country"
7+
[(ngModel)]="query"
8+
ngComboboxInput
9+
/>
10+
</div>
11+
12+
<ng-template ngComboboxPopupContainer>
13+
<ng-template
14+
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
15+
[cdkConnectedOverlayOpen]="true"
16+
>
17+
<div class="example-popup">
18+
@if (countries().length === 0) {
19+
<div class="example-no-results">No results found</div>
20+
}
21+
22+
<div ngListbox>
23+
@for (country of countries(); track country) {
24+
<div ngOption [value]="country" [label]="country" [disabled]="country === 'Brazil'">
25+
<span class="example-option-label">{{country}}</span>
26+
<span class="example-check-icon material-symbols-outlined" translate="no">check</span>
27+
</div>
28+
}
29+
</div>
30+
</div>
31+
</ng-template>
32+
</ng-template>
33+
</div>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
Combobox,
11+
ComboboxInput,
12+
ComboboxPopup,
13+
ComboboxPopupContainer,
14+
} from '@angular/aria/combobox';
15+
import {Listbox, Option} from '@angular/aria/listbox';
16+
import {
17+
afterRenderEffect,
18+
ChangeDetectionStrategy,
19+
Component,
20+
computed,
21+
signal,
22+
viewChild,
23+
viewChildren,
24+
} from '@angular/core';
25+
import {COUNTRIES} from '../countries';
26+
import {OverlayModule} from '@angular/cdk/overlay';
27+
import {FormsModule} from '@angular/forms';
28+
29+
/** @title Disabled autocomplete. */
30+
@Component({
31+
selector: 'autocomplete-disabled-example',
32+
templateUrl: 'autocomplete-disabled-example.html',
33+
styleUrl: '../autocomplete.css',
34+
imports: [
35+
Combobox,
36+
ComboboxInput,
37+
ComboboxPopup,
38+
ComboboxPopupContainer,
39+
Listbox,
40+
Option,
41+
OverlayModule,
42+
FormsModule,
43+
],
44+
changeDetection: ChangeDetectionStrategy.OnPush,
45+
})
46+
export class AutocompleteDisabledExample {
47+
/** The options available in the listbox. */
48+
options = viewChildren<Option<string>>(Option);
49+
50+
/** A reference to the ng aria combobox. */
51+
combobox = viewChild<Combobox<string>>(Combobox);
52+
53+
/** The query string used to filter the list of countries. */
54+
query = signal('United States of America');
55+
56+
/** The list of countries filtered by the query. */
57+
countries = computed(() =>
58+
COUNTRIES.filter(country => country.toLowerCase().startsWith(this.query().toLowerCase())),
59+
);
60+
61+
constructor() {
62+
// Scrolls to the active item when the active option changes.
63+
afterRenderEffect(() => {
64+
if (this.combobox()?.expanded()) {
65+
const option = this.options().find(opt => opt.active());
66+
option?.element.scrollIntoView({block: 'nearest'});
67+
}
68+
});
69+
}
70+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<div ngCombobox filterMode="highlight">
2+
<div #origin class="example-autocomplete">
3+
<span class="example-search-icon material-symbols-outlined" translate="no">search</span>
4+
<input
5+
aria-label="Label dropdown"
6+
placeholder="Select a country"
7+
[(ngModel)]="query"
8+
ngComboboxInput
9+
/>
10+
</div>
11+
12+
<ng-template ngComboboxPopupContainer>
13+
<ng-template
14+
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
15+
[cdkConnectedOverlayOpen]="true"
16+
>
17+
<div class="example-popup">
18+
@if (countries().length === 0) {
19+
<div class="example-no-results">No results found</div>
20+
}
21+
22+
<div ngListbox>
23+
@for (country of countries(); track country) {
24+
<div ngOption [value]="country" [label]="country" [disabled]="country === 'Brazil'">
25+
<span class="example-option-label">{{country}}</span>
26+
<span class="example-check-icon material-symbols-outlined" translate="no">check</span>
27+
</div>
28+
}
29+
</div>
30+
</div>
31+
</ng-template>
32+
</ng-template>
33+
</div>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {
10+
Combobox,
11+
ComboboxInput,
12+
ComboboxPopup,
13+
ComboboxPopupContainer,
14+
} from '@angular/aria/combobox';
15+
import {Listbox, Option} from '@angular/aria/listbox';
16+
import {
17+
afterRenderEffect,
18+
ChangeDetectionStrategy,
19+
Component,
20+
computed,
21+
signal,
22+
viewChild,
23+
viewChildren,
24+
} from '@angular/core';
25+
import {COUNTRIES} from '../countries';
26+
import {OverlayModule} from '@angular/cdk/overlay';
27+
import {FormsModule} from '@angular/forms';
28+
29+
/** @title Autocomplete with highlighted filtering. */
30+
@Component({
31+
selector: 'autocomplete-highlight-example',
32+
templateUrl: 'autocomplete-highlight-example.html',
33+
styleUrl: '../autocomplete.css',
34+
imports: [
35+
Combobox,
36+
ComboboxInput,
37+
ComboboxPopup,
38+
ComboboxPopupContainer,
39+
Listbox,
40+
Option,
41+
OverlayModule,
42+
FormsModule,
43+
],
44+
changeDetection: ChangeDetectionStrategy.OnPush,
45+
})
46+
export class AutocompleteHighlightExample {
47+
/** The options available in the listbox. */
48+
options = viewChildren<Option<string>>(Option);
49+
50+
/** A reference to the ng aria combobox. */
51+
combobox = viewChild<Combobox<string>>(Combobox);
52+
53+
/** The query string used to filter the list of countries. */
54+
query = signal('');
55+
56+
/** The list of countries filtered by the query. */
57+
countries = computed(() =>
58+
COUNTRIES.filter(country => country.toLowerCase().startsWith(this.query().toLowerCase())),
59+
);
60+
61+
constructor() {
62+
// Scrolls to the active item when the active option changes.
63+
afterRenderEffect(() => {
64+
if (this.combobox()?.expanded()) {
65+
const option = this.options().find(opt => opt.active());
66+
option?.element.scrollIntoView({block: 'nearest'});
67+
}
68+
});
69+
}
70+
}

0 commit comments

Comments
 (0)