Skip to content

Commit ea25472

Browse files
authored
docs(aria/combobox): material select demo (#32336)
* docs(aria/combobox): material select demo * fixup! docs(aria/combobox): material select demo * fixup! docs(aria/combobox): material select demo * fixup! docs(aria/combobox): material select demo * fixup! docs(aria/combobox): material select demo
1 parent b33af6b commit ea25472

File tree

11 files changed

+323
-168
lines changed

11 files changed

+323
-168
lines changed

src/aria/listbox/listbox.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,6 @@ export class Option<V> {
278278
value: this.value,
279279
listbox: this.listbox,
280280
element: () => this.element,
281-
searchTerm: this.searchTerm,
281+
searchTerm: () => this.searchTerm() ?? '',
282282
});
283283
}

src/aria/private/combobox/combobox.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -587,7 +587,10 @@ export class ComboboxPattern<T extends ListItem<V>, V> {
587587
const selectedItem = popupControls
588588
?.items()
589589
.find(i => popupControls?.getSelectedItems().includes(i));
590-
selectedItem ? popupControls?.focus(selectedItem) : this.first();
590+
591+
if (selectedItem) {
592+
popupControls?.focus(selectedItem);
593+
}
591594
}
592595
}
593596

src/components-examples/aria/combobox/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ ng_project(
1616
"//src/aria/combobox",
1717
"//src/aria/listbox",
1818
"//src/aria/tree",
19+
"//src/cdk/overlay",
1920
],
2021
)
2122

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,26 @@
1-
<div ngCombobox #combobox="ngCombobox" class="example-combobox-container" [readonly]="true" [disabled]="true">
2-
<div class="example-combobox-input-container">
3-
<input
4-
ngComboboxInput
5-
class="example-combobox-input"
6-
placeholder="Search..."
7-
[(value)]="searchString"
8-
/>
9-
<span class="material-symbols-outlined example-icon example-arrow-icon">arrow_drop_down</span>
1+
<div ngCombobox readonly disabled>
2+
<div #origin class="example-select">
3+
<span class="example-combobox-text">{{ displayValue() }}</span>
4+
<input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput />
5+
<span class="example-arrow material-symbols-outlined">arrow_drop_down</span>
106
</div>
117

12-
<div popover="manual" #popover class="example-popover">
8+
<ng-template
9+
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
10+
[cdkConnectedOverlayOpen]="true"
11+
>
1312
<ng-template ngComboboxPopupContainer>
14-
<div ngListbox class="example-listbox">
15-
@for (option of options(); track option) {
16-
<div
17-
class="example-option example-selectable example-stateful"
18-
ngOption
19-
[value]="option"
20-
[label]="option"
21-
>
22-
<span>{{option}}</span>
23-
<span
24-
aria-hidden="true"
25-
class="material-symbols-outlined example-icon example-selected-icon"
26-
>check</span
27-
>
28-
</div>
29-
}
13+
<div class="example-popup-container">
14+
<div ngListbox>
15+
@for (label of labels; track label.value) {
16+
<div ngOption [value]="label.value" [label]="label.value">
17+
<span class="example-option-icon material-symbols-outlined">{{label.icon}}</span>
18+
<span class="example-option-text">{{label.value}}</span>
19+
<span class="example-option-check material-symbols-outlined">check</span>
20+
</div>
21+
}
22+
</div>
3023
</div>
3124
</ng-template>
32-
</div>
25+
</ng-template>
3326
</div>

src/components-examples/aria/combobox/combobox-readonly-disabled/combobox-readonly-disabled-example.ts

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,61 +17,78 @@ import {
1717
afterRenderEffect,
1818
ChangeDetectionStrategy,
1919
Component,
20-
ElementRef,
2120
signal,
2221
viewChild,
22+
viewChildren,
2323
} from '@angular/core';
24-
import {FormsModule} from '@angular/forms';
24+
import {OverlayModule} from '@angular/cdk/overlay';
2525

2626
/** @title Disabled readonly combobox. */
2727
@Component({
2828
selector: 'combobox-readonly-disabled-example',
2929
templateUrl: 'combobox-readonly-disabled-example.html',
30-
styleUrl: '../combobox-examples.css',
30+
styleUrl: '../select-examples.css',
3131
imports: [
3232
Combobox,
3333
ComboboxInput,
3434
ComboboxPopup,
3535
ComboboxPopupContainer,
3636
Listbox,
3737
Option,
38-
FormsModule,
38+
OverlayModule,
3939
],
4040
changeDetection: ChangeDetectionStrategy.OnPush,
4141
})
4242
export class ComboboxReadonlyDisabledExample {
43-
popover = viewChild<ElementRef>('popover');
44-
listbox = viewChild<Listbox<any>>(Listbox);
45-
combobox = viewChild<Combobox<any>>(Combobox);
43+
/** The string that is displayed in the combobox. */
44+
displayValue = signal('');
4645

47-
options = () => states;
48-
searchString = signal('');
46+
/** The combobox listbox popup. */
47+
listbox = viewChild<Listbox<string>>(Listbox);
4948

50-
constructor() {
51-
afterRenderEffect(() => {
52-
const popover = this.popover()!;
53-
const combobox = this.combobox()!;
54-
combobox.expanded() ? this.showPopover() : popover.nativeElement.hidePopover();
49+
/** The options available in the listbox. */
50+
options = viewChildren<Option<string>>(Option);
5551

56-
this.listbox()?.scrollActiveItemIntoView();
57-
});
58-
}
52+
/** A reference to the ng aria combobox. */
53+
combobox = viewChild<Combobox<string>>(Combobox);
5954

60-
showPopover() {
61-
const popover = this.popover()!;
62-
const combobox = this.combobox()!;
55+
/** The labels that are available for selection. */
56+
labels = [
57+
{value: 'Important', icon: 'label'},
58+
{value: 'Starred', icon: 'star'},
59+
{value: 'Work', icon: 'work'},
60+
{value: 'Personal', icon: 'person'},
61+
{value: 'To Do', icon: 'checklist'},
62+
{value: 'Later', icon: 'schedule'},
63+
{value: 'Read', icon: 'menu_book'},
64+
{value: 'Travel', icon: 'flight'},
65+
];
6366

64-
const comboboxRect = combobox.inputElement()?.getBoundingClientRect();
65-
const popoverEl = popover.nativeElement;
67+
constructor() {
68+
// Updates the display value when the listbox values change.
69+
afterRenderEffect(() => {
70+
const values = this.listbox()?.values() || [];
71+
if (values.length === 0) {
72+
this.displayValue.set('Select a label');
73+
} else if (values.length === 1) {
74+
this.displayValue.set(values[0]);
75+
} else {
76+
this.displayValue.set(`${values[0]} + ${values.length - 1} more`);
77+
}
78+
});
6679

67-
if (comboboxRect) {
68-
popoverEl.style.width = `${comboboxRect.width}px`;
69-
popoverEl.style.top = `${comboboxRect.bottom + 4}px`;
70-
popoverEl.style.left = `${comboboxRect.left - 1}px`;
71-
}
80+
// Scrolls to the active item when the active option changes.
81+
// The slight delay here is to ensure animations are done before scrolling.
82+
afterRenderEffect(() => {
83+
const option = this.options().find(opt => opt.active());
84+
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
85+
});
7286

73-
popover.nativeElement.showPopover();
87+
// Resets the listbox scroll position when the combobox is closed.
88+
afterRenderEffect(() => {
89+
if (!this.combobox()?.expanded()) {
90+
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
91+
}
92+
});
7493
}
7594
}
76-
77-
const states = ['Option 1', 'Option 2', 'Option 3'];
Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,26 @@
1-
<div ngCombobox #combobox="ngCombobox" class="example-combobox-container" [readonly]="true">
2-
<div class="example-combobox-input-container">
3-
<input
4-
ngComboboxInput
5-
class="example-combobox-input"
6-
placeholder="Search..."
7-
[(value)]="searchString"
8-
/>
9-
<span class="material-symbols-outlined example-icon example-arrow-icon">arrow_drop_down</span>
1+
<div ngCombobox readonly>
2+
<div #origin class="example-select">
3+
<span class="example-combobox-text">{{ displayValue() }}</span>
4+
<input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput />
5+
<span class="example-arrow material-symbols-outlined">arrow_drop_down</span>
106
</div>
117

12-
<div popover="manual" #popover class="example-popover">
8+
<ng-template
9+
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
10+
[cdkConnectedOverlayOpen]="true"
11+
>
1312
<ng-template ngComboboxPopupContainer>
14-
<div ngListbox [multi]="true" class="example-listbox">
15-
@for (option of options(); track option) {
16-
<div class="example-option" ngOption [value]="option" [label]="option">
17-
<span>{{option}}</span>
18-
<span
19-
aria-hidden="true"
20-
class="material-symbols-outlined example-icon example-selected-icon"
21-
>check</span
22-
>
23-
</div>
24-
}
13+
<div class="example-popup-container">
14+
<div ngListbox multi>
15+
@for (label of labels; track label.value) {
16+
<div ngOption [value]="label.value" [label]="label.value">
17+
<span class="example-option-icon material-symbols-outlined">{{label.icon}}</span>
18+
<span class="example-option-text">{{label.value}}</span>
19+
<span class="example-option-check material-symbols-outlined">check</span>
20+
</div>
21+
}
22+
</div>
2523
</div>
2624
</ng-template>
27-
</div>
25+
</ng-template>
2826
</div>

src/components-examples/aria/combobox/combobox-readonly-multiselect/combobox-readonly-multiselect-example.ts

Lines changed: 47 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,65 +13,82 @@ import {
1313
ComboboxPopupContainer,
1414
} from '@angular/aria/combobox';
1515
import {Listbox, Option} from '@angular/aria/listbox';
16+
import {OverlayModule} from '@angular/cdk/overlay';
1617
import {
1718
afterRenderEffect,
1819
ChangeDetectionStrategy,
1920
Component,
20-
ElementRef,
2121
signal,
2222
viewChild,
23+
viewChildren,
2324
} from '@angular/core';
24-
import {FormsModule} from '@angular/forms';
2525

2626
/** @title Readonly multiselectable combobox. */
2727
@Component({
2828
selector: 'combobox-readonly-multiselect-example',
2929
templateUrl: 'combobox-readonly-multiselect-example.html',
30-
styleUrl: '../combobox-examples.css',
30+
styleUrl: '../select-examples.css',
3131
imports: [
3232
Combobox,
3333
ComboboxInput,
3434
ComboboxPopup,
3535
ComboboxPopupContainer,
3636
Listbox,
3737
Option,
38-
FormsModule,
38+
OverlayModule,
3939
],
4040
changeDetection: ChangeDetectionStrategy.OnPush,
4141
})
4242
export class ComboboxReadonlyMultiselectExample {
43-
popover = viewChild<ElementRef>('popover');
44-
listbox = viewChild<Listbox<any>>(Listbox);
45-
combobox = viewChild<Combobox<any>>(Combobox);
43+
/** The string that is displayed in the combobox. */
44+
displayValue = signal('');
4645

47-
options = () => states;
48-
searchString = signal('');
46+
/** The combobox listbox popup. */
47+
listbox = viewChild<Listbox<string>>(Listbox);
4948

50-
constructor() {
51-
afterRenderEffect(() => {
52-
const popover = this.popover()!;
53-
const combobox = this.combobox()!;
54-
combobox._pattern.expanded() ? this.showPopover() : popover.nativeElement.hidePopover();
49+
/** The options available in the listbox. */
50+
options = viewChildren<Option<string>>(Option);
5551

56-
this.listbox()?.scrollActiveItemIntoView();
57-
});
58-
}
52+
/** A reference to the ng aria combobox. */
53+
combobox = viewChild<Combobox<string>>(Combobox);
5954

60-
showPopover() {
61-
const popover = this.popover()!;
62-
const combobox = this.combobox()!;
55+
/** The labels that are available for selection. */
56+
labels = [
57+
{value: 'Important', icon: 'label'},
58+
{value: 'Starred', icon: 'star'},
59+
{value: 'Work', icon: 'work'},
60+
{value: 'Personal', icon: 'person'},
61+
{value: 'To Do', icon: 'checklist'},
62+
{value: 'Later', icon: 'schedule'},
63+
{value: 'Read', icon: 'menu_book'},
64+
{value: 'Travel', icon: 'flight'},
65+
];
6366

64-
const comboboxRect = combobox._pattern.inputs.inputEl()?.getBoundingClientRect();
65-
const popoverEl = popover.nativeElement;
67+
constructor() {
68+
// Updates the display value when the listbox values change.
69+
afterRenderEffect(() => {
70+
const values = this.listbox()?.values() || [];
71+
if (values.length === 0) {
72+
this.displayValue.set('Select a label');
73+
} else if (values.length === 1) {
74+
this.displayValue.set(values[0]);
75+
} else {
76+
this.displayValue.set(`${values[0]} + ${values.length - 1} more`);
77+
}
78+
});
6679

67-
if (comboboxRect) {
68-
popoverEl.style.width = `${comboboxRect.width}px`;
69-
popoverEl.style.top = `${comboboxRect.bottom + 4}px`;
70-
popoverEl.style.left = `${comboboxRect.left - 1}px`;
71-
}
80+
// Scrolls to the active item when the active option changes.
81+
// The slight delay here is to ensure animations are done before scrolling.
82+
afterRenderEffect(() => {
83+
const option = this.options().find(opt => opt.active());
84+
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
85+
});
7286

73-
popover.nativeElement.showPopover();
87+
// Resets the listbox scroll position when the combobox is closed.
88+
afterRenderEffect(() => {
89+
if (!this.combobox()?.expanded()) {
90+
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
91+
}
92+
});
7493
}
7594
}
76-
77-
const states = ['Option 1', 'Option 2', 'Option 3'];

0 commit comments

Comments
 (0)