Skip to content

Commit e52bc52

Browse files
committed
refactor(multiple): add comprehensive simple-combobox examples and refine behavior
- Adds new examples for simple-combobox: auto-select, highlight, disabled, readonly, and dialog popups. - Refactors existing datepicker and grid examples for better interaction patterns. - Introduces alwaysExpanded input to SimpleCombobox for persistent popup states. - Refines selection behavior in ListboxPattern when followFocus is enabled. - Cleans up unused inline-suggestion examples and console logs.
1 parent ac30b9f commit e52bc52

36 files changed

Lines changed: 1408 additions & 223 deletions

File tree

src/aria/private/grid/widget.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ export class GridCellWidgetPattern {
7575
// However, it does need to capture Enter key and trigger a click on the host element
7676
// since the browser won't do it for us in activedescendant mode.
7777
if (this.inputs.widgetType() === 'simple') {
78-
console.log('simple widget keydown');
7978
manager.on('Enter', () => this.element().click());
8079
return manager;
8180
}
@@ -114,7 +113,6 @@ export class GridCellWidgetPattern {
114113
/** Handles keydown events for the widget. */
115114
onKeydown(event: KeyboardEvent): void {
116115
if (this.disabled()) return;
117-
console.log('keydown of widget.ts');
118116

119117
this.keydown().handle(event);
120118
}

src/aria/private/listbox/listbox.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,9 @@ export class ListboxPattern<V> {
264264

265265
if (firstItem) {
266266
this.inputs.activeItem.set(firstItem);
267+
if (this.followFocus()) {
268+
this.listBehavior.select();
269+
}
267270
}
268271
}
269272

src/aria/private/simple-combobox/simple-combobox.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@
77
*/
88

99
import {KeyboardEventManager, PointerEventManager} from '../behaviors/event-manager';
10-
import {computed, signal, untracked} from '@angular/core';
10+
import {afterRenderEffect, computed, signal, untracked} from '@angular/core';
1111
import {SignalLike, WritableSignalLike} from '../behaviors/signal-like/signal-like';
1212
import {ExpansionItem} from '../behaviors/expansion/expansion';
1313

1414
/** Represents the required inputs for a simple combobox. */
1515
export interface SimpleComboboxInputs extends ExpansionItem {
16+
/** Whether the combobox should always remain expanded. */
17+
alwaysExpanded: SignalLike<boolean>;
18+
1619
/** The value of the combobox. */
1720
value: WritableSignalLike<string>;
1821

@@ -123,7 +126,11 @@ export class SimpleComboboxPattern {
123126
.on('Enter', e => this.keyboardEventRelay.set(e))
124127
.on('PageUp', e => this.keyboardEventRelay.set(e))
125128
.on('PageDown', e => this.keyboardEventRelay.set(e))
126-
.on('Escape', () => this.expanded.set(false));
129+
.on('Escape', () => {
130+
if (!this.inputs.alwaysExpanded()) {
131+
this.expanded.set(false);
132+
}
133+
});
127134

128135
if (!this.isEditable()) {
129136
manager
@@ -150,6 +157,12 @@ export class SimpleComboboxPattern {
150157
constructor(readonly inputs: SimpleComboboxInputs) {
151158
this.expanded = inputs.expanded;
152159
this.value = inputs.value;
160+
161+
afterRenderEffect(() => {
162+
if (this.inputs.alwaysExpanded()) {
163+
this.expanded.set(true);
164+
}
165+
});
153166
}
154167

155168
/** Handles keydown events for the combobox. */
@@ -226,7 +239,7 @@ export class SimpleComboboxPattern {
226239
const expanded = this.expanded();
227240
const comboboxFocused = this.isFocused();
228241
const popupFocused = !!this.inputs.popup()?.isFocused();
229-
if (expanded && !comboboxFocused && !popupFocused) {
242+
if (expanded && !this.inputs.alwaysExpanded() && !comboboxFocused && !popupFocused) {
230243
this.expanded.set(false);
231244
}
232245
}

src/aria/simple-combobox/simple-combobox.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ export class Combobox extends DeferredContentAware {
7878
/** Whether the combobox is disabled. */
7979
readonly disabled = input(false, {transform: booleanAttribute});
8080

81+
/** Whether the combobox should always remain expanded. */
82+
readonly alwaysExpanded = input(false, {transform: booleanAttribute});
83+
8184
/** Whether the combobox is expanded. */
8285
readonly expanded = model<boolean>(false);
8386

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
export {SimpleComboboxListboxExample} from './simple-combobox-listbox/simple-combobox-listbox-example';
2-
export {SimpleComboboxListboxInlineExample} from './simple-combobox-listbox-inline/simple-combobox-listbox-inline-example';
32
export {SimpleComboboxTreeExample} from './simple-combobox-tree/simple-combobox-tree-example';
43
export {SimpleComboboxSelectExample} from './simple-combobox-select/simple-combobox-select-example';
54
export {SimpleComboboxGridExample} from './simple-combobox-grid/simple-combobox-grid-example';
65
export {SimpleComboboxDatepickerExample} from './simple-combobox-datepicker/simple-combobox-datepicker-example';
6+
export {SimpleComboboxAutoSelectExample} from './simple-combobox-auto-select/simple-combobox-auto-select-example';
7+
export {SimpleComboboxHighlightExample} from './simple-combobox-highlight/simple-combobox-highlight-example';
8+
export {SimpleComboboxDisabledExample} from './simple-combobox-disabled/simple-combobox-disabled-example';
9+
export {SimpleComboboxReadonlyDisabledExample} from './simple-combobox-readonly-disabled/simple-combobox-readonly-disabled-example';
10+
export {SimpleComboboxReadonlyMultiselectExample} from './simple-combobox-readonly-multiselect/simple-combobox-readonly-multiselect-example';
11+
export {SimpleComboboxDialogExample} from './simple-combobox-dialog/simple-combobox-dialog-example';
12+
export {SimpleComboboxTreeAutoSelectExample} from './simple-combobox-tree-auto-select/simple-combobox-tree-auto-select-example';
13+
export {SimpleComboboxTreeHighlightExample} from './simple-combobox-tree-highlight/simple-combobox-tree-highlight-example';
14+
// Force watcher update
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<div class="example-combobox-container">
2+
<div #origin class="example-combobox-input-container">
3+
<span class="material-symbols-outlined example-icon example-search-icon">search</span>
4+
<input
5+
ngCombobox
6+
#combobox="ngCombobox"
7+
class="example-combobox-input"
8+
placeholder="Search states..."
9+
[(value)]="searchString"
10+
[(expanded)]="popupExpanded"
11+
/>
12+
</div>
13+
14+
<ng-template
15+
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
16+
[cdkConnectedOverlayOpen]="true"
17+
[cdkConnectedOverlayDisableClose]="true"
18+
>
19+
<ng-template ngComboboxPopup [combobox]="combobox">
20+
<div
21+
ngListbox
22+
ngComboboxWidget
23+
class="example-listbox example-popup"
24+
focusMode="activedescendant"
25+
[(value)]="selectedOption"
26+
(click)="onCommit()"
27+
(keydown.enter)="onCommit()"
28+
>
29+
@for (option of options(); track option) {
30+
<div
31+
class="example-option example-selectable example-stateful"
32+
ngOption
33+
[value]="option"
34+
[label]="option"
35+
>
36+
<span>{{option}}</span>
37+
<span
38+
aria-hidden="true"
39+
class="material-symbols-outlined example-icon example-selected-icon"
40+
>check</span
41+
>
42+
</div>
43+
}
44+
</div>
45+
</ng-template>
46+
</ng-template>
47+
</div>
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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 {Combobox, ComboboxPopup, ComboboxWidget} from '@angular/aria/simple-combobox';
10+
import {Listbox, Option} from '@angular/aria/listbox';
11+
import {afterRenderEffect, Component, computed, signal, viewChild, untracked} from '@angular/core';
12+
import {OverlayModule} from '@angular/cdk/overlay';
13+
14+
/** @title Simple Combobox Auto Select */
15+
@Component({
16+
selector: 'simple-combobox-auto-select-example',
17+
templateUrl: 'simple-combobox-auto-select-example.html',
18+
styleUrl: '../simple-combobox-examples.css',
19+
imports: [Combobox, ComboboxPopup, ComboboxWidget, Listbox, Option, OverlayModule],
20+
})
21+
export class SimpleComboboxAutoSelectExample {
22+
readonly listbox = viewChild(Listbox);
23+
24+
popupExpanded = signal(false);
25+
searchString = signal('');
26+
selectedOption = signal<string[]>([]);
27+
28+
options = computed(() =>
29+
states.filter(state => state.toLowerCase().startsWith(this.searchString().toLowerCase())),
30+
);
31+
32+
constructor() {
33+
afterRenderEffect(() => {
34+
this.listbox()?.scrollActiveItemIntoView();
35+
});
36+
}
37+
38+
onCommit() {
39+
const selectedOption = this.selectedOption();
40+
if (selectedOption.length > 0) {
41+
this.searchString.set(selectedOption[0]);
42+
}
43+
this.popupExpanded.set(false);
44+
}
45+
}
46+
47+
const states = [
48+
'Alabama',
49+
'Alaska',
50+
'Arizona',
51+
'Arkansas',
52+
'California',
53+
'Colorado',
54+
'Connecticut',
55+
'Delaware',
56+
'Florida',
57+
'Georgia',
58+
'Hawaii',
59+
'Idaho',
60+
'Illinois',
61+
'Indiana',
62+
'Iowa',
63+
'Kansas',
64+
'Kentucky',
65+
'Louisiana',
66+
'Maine',
67+
'Maryland',
68+
'Massachusetts',
69+
'Michigan',
70+
'Minnesota',
71+
'Mississippi',
72+
'Missouri',
73+
'Montana',
74+
'Nebraska',
75+
'Nevada',
76+
'New Hampshire',
77+
'New Jersey',
78+
'New Mexico',
79+
'New York',
80+
'North Carolina',
81+
'North Dakota',
82+
'Ohio',
83+
'Oklahoma',
84+
'Oregon',
85+
'Pennsylvania',
86+
'Rhode Island',
87+
'South Carolina',
88+
'South Dakota',
89+
'Tennessee',
90+
'Texas',
91+
'Utah',
92+
'Vermont',
93+
'Virginia',
94+
'Washington',
95+
'West Virginia',
96+
'Wisconsin',
97+
'Wyoming',
98+
];

src/components-examples/aria/simple-combobox/simple-combobox-datepicker/simple-combobox-datepicker-example.css

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -85,24 +85,22 @@
8585
background-color: color-mix(in srgb, var(--mat-sys-on-surface) 10%, transparent);
8686
}
8787

88-
/* Show circular focus ring on the day button when active using box-shadow */
89-
/* Subdued grey by default when navigating from the input */
90-
.example-datepicker-cell[data-active='true'] .example-datepicker-day-button {
91-
box-shadow: 0 0 0 2px var(--mat-sys-outline);
88+
.example-datepicker-cell:focus-within {
89+
outline: 2px solid var(--mat-sys-primary);
90+
outline-offset: -2px;
9291
}
9392

94-
/* Highlight circle with primary color when the grid has actual focus */
95-
.example-datepicker-grid:focus .example-datepicker-cell[data-active='true'] .example-datepicker-day-button,
96-
.example-datepicker-grid:focus-within .example-datepicker-cell[data-active='true'] .example-datepicker-day-button {
97-
box-shadow: 0 0 0 2px var(--mat-sys-primary);
98-
}
99-
100-
/* Hide all grid focus indicators when focus is in the header navigation */
101-
.example-datepicker-header:focus-within~.example-datepicker-grid .example-datepicker-cell[data-active='true'] .example-datepicker-day-button {
102-
box-shadow: none;
93+
.example-datepicker-day-button:focus {
94+
outline: none;
10395
}
10496

10597
.example-datepicker-cell[aria-selected='true'] .example-datepicker-day-button {
10698
background-color: var(--mat-sys-primary);
10799
color: var(--mat-sys-on-primary);
108-
}
100+
}
101+
102+
.example-combobox-hint {
103+
font-size: 0.75rem;
104+
color: var(--mat-sys-on-surface-variant);
105+
margin-top: 4px;
106+
}

0 commit comments

Comments
 (0)