Skip to content

Commit a47ebeb

Browse files
committed
fix(aria/combobox): disabled state (#32308)
(cherry picked from commit 8ff0e79)
1 parent 4731b9b commit a47ebeb

File tree

10 files changed

+317
-4
lines changed

10 files changed

+317
-4
lines changed

src/aria/combobox/combobox.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {
1010
afterRenderEffect,
11+
booleanAttribute,
1112
computed,
1213
contentChild,
1314
Directive,
@@ -73,7 +74,7 @@ export class Combobox<V> {
7374
filterMode = input<'manual' | 'auto-select' | 'highlight'>('manual');
7475

7576
/** Whether the combobox is disabled. */
76-
readonly disabled = input(false);
77+
readonly disabled = input(false, {transform: booleanAttribute});
7778

7879
/** Whether the combobox is read-only. */
7980
readonly readonly = input(false);
@@ -87,6 +88,7 @@ export class Combobox<V> {
8788
// TODO: Maybe make expanded a signal that can be passed in?
8889
// Or an "always expanded" option?
8990

91+
/** Whether the combobox popup is always expanded. */
9092
readonly alwaysExpanded = input(false);
9193

9294
/** Input element connected to the combobox, if any. */
@@ -130,6 +132,16 @@ export class Combobox<V> {
130132
close() {
131133
this._pattern.close();
132134
}
135+
136+
/** Expands the combobox popup. */
137+
expand() {
138+
this._pattern.open();
139+
}
140+
141+
/** Collapses the combobox popup. */
142+
collapse() {
143+
this._pattern.close();
144+
}
133145
}
134146

135147
/**
@@ -141,6 +153,7 @@ export class Combobox<V> {
141153
host: {
142154
'role': 'combobox',
143155
'[value]': 'value()',
156+
'[attr.aria-disabled]': 'combobox._pattern.disabled()',
144157
'[attr.aria-expanded]': 'combobox._pattern.expanded()',
145158
'[attr.aria-activedescendant]': 'combobox._pattern.activeDescendant()',
146159
'[attr.aria-controls]': 'combobox._pattern.popupId()',

src/aria/private/combobox/combobox.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ export class ComboboxPattern<T extends ListItem<V>, V> {
137137
/** Whether the combobox is expanded. */
138138
expanded = signal(false);
139139

140+
/** Whether the combobox is disabled. */
141+
disabled = () => this.inputs.disabled();
142+
140143
/** The ID of the active item in the combobox. */
141144
activeDescendant = computed(() => {
142145
const popupControls = this.inputs.popupControls();
@@ -177,7 +180,7 @@ export class ComboboxPattern<T extends ListItem<V>, V> {
177180
hasPopup = computed(() => this.inputs.popupControls()?.role() || null);
178181

179182
/** Whether the combobox is read-only. */
180-
readonly = computed(() => this.inputs.readonly() || null);
183+
readonly = computed(() => this.inputs.readonly() || this.inputs.disabled() || null);
181184

182185
/** Returns the listbox controls for the combobox. */
183186
listControls = () => {
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<div ngCombobox #combobox="ngCombobox" class="example-combobox-container" disabled>
2+
<div class="example-combobox-input-container">
3+
<span class="material-symbols-outlined example-icon example-search-icon">search</span>
4+
<input
5+
ngComboboxInput
6+
class="example-combobox-input"
7+
placeholder="Search..."
8+
[(value)]="searchString"
9+
/>
10+
</div>
11+
12+
<div popover="manual" #popover class="example-popover">
13+
<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+
}
30+
</div>
31+
</ng-template>
32+
</div>
33+
</div>
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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+
ElementRef,
22+
signal,
23+
viewChild,
24+
} from '@angular/core';
25+
import {FormsModule} from '@angular/forms';
26+
27+
/** @title Disabled combobox example. */
28+
@Component({
29+
selector: 'combobox-disabled-example',
30+
templateUrl: 'combobox-disabled-example.html',
31+
styleUrl: '../combobox-examples.css',
32+
imports: [
33+
Combobox,
34+
ComboboxInput,
35+
ComboboxPopup,
36+
ComboboxPopupContainer,
37+
Listbox,
38+
Option,
39+
FormsModule,
40+
],
41+
changeDetection: ChangeDetectionStrategy.OnPush,
42+
})
43+
export class ComboboxDisabledExample {
44+
popover = viewChild<ElementRef>('popover');
45+
listbox = viewChild<Listbox<any>>(Listbox);
46+
combobox = viewChild<Combobox<any>>(Combobox);
47+
48+
searchString = signal('');
49+
50+
options = computed(() =>
51+
states.filter(state => state.toLowerCase().startsWith(this.searchString().toLowerCase())),
52+
);
53+
54+
constructor() {
55+
afterRenderEffect(() => {
56+
const popover = this.popover()!;
57+
const combobox = this.combobox()!;
58+
combobox.expanded() ? this.showPopover() : popover.nativeElement.hidePopover();
59+
60+
this.listbox()?.scrollActiveItemIntoView();
61+
});
62+
}
63+
64+
showPopover() {
65+
const popover = this.popover()!;
66+
const combobox = this.combobox()!;
67+
68+
const comboboxRect = combobox.inputElement()?.getBoundingClientRect();
69+
const popoverEl = popover.nativeElement;
70+
71+
if (comboboxRect) {
72+
popoverEl.style.width = `${comboboxRect.width}px`;
73+
popoverEl.style.top = `${comboboxRect.bottom + 4}px`;
74+
popoverEl.style.left = `${comboboxRect.left - 1}px`;
75+
}
76+
77+
popover.nativeElement.showPopover();
78+
}
79+
}
80+
81+
const states = [
82+
'Alabama',
83+
'Alaska',
84+
'Arizona',
85+
'Arkansas',
86+
'California',
87+
'Colorado',
88+
'Connecticut',
89+
'Delaware',
90+
'Florida',
91+
'Georgia',
92+
'Hawaii',
93+
'Idaho',
94+
'Illinois',
95+
'Indiana',
96+
'Iowa',
97+
'Kansas',
98+
'Kentucky',
99+
'Louisiana',
100+
'Maine',
101+
'Maryland',
102+
'Massachusetts',
103+
'Michigan',
104+
'Minnesota',
105+
'Mississippi',
106+
'Missouri',
107+
'Montana',
108+
'Nebraska',
109+
'Nevada',
110+
'New Hampshire',
111+
'New Jersey',
112+
'New Mexico',
113+
'New York',
114+
'North Carolina',
115+
'North Dakota',
116+
'Ohio',
117+
'Oklahoma',
118+
'Oregon',
119+
'Pennsylvania',
120+
'Rhode Island',
121+
'South Carolina',
122+
'South Dakota',
123+
'Tennessee',
124+
'Texas',
125+
'Utah',
126+
'Vermont',
127+
'Virginia',
128+
'Washington',
129+
'West Virginia',
130+
'Wisconsin',
131+
'Wyoming',
132+
];

src/components-examples/aria/combobox/combobox-examples.css

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
border-radius: var(--mat-sys-corner-extra-small);
88
}
99

10-
.example-combobox-container:has([readonly='true']) {
10+
.example-combobox-container:has([readonly='true']:not([aria-disabled='true'])) {
1111
width: 200px;
1212
}
1313

@@ -22,7 +22,7 @@
2222
border-radius: var(--mat-sys-corner-extra-small);
2323
}
2424

25-
.example-combobox-input[readonly='true'] {
25+
.example-combobox-input[readonly='true']:not([aria-disabled='true']) {
2626
cursor: pointer;
2727
padding: 0.7rem 1rem;
2828
}
@@ -182,3 +182,8 @@ ul[role='group'] {
182182
.example-tree-item[aria-selected='true'] .example-selected-icon {
183183
visibility: visible;
184184
}
185+
186+
.example-combobox-container:has([aria-disabled='true']) {
187+
opacity: 0.4;
188+
cursor: default;
189+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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>
10+
</div>
11+
12+
<div popover="manual" #popover class="example-popover">
13+
<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+
}
30+
</div>
31+
</ng-template>
32+
</div>
33+
</div>
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
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+
ElementRef,
21+
signal,
22+
viewChild,
23+
} from '@angular/core';
24+
import {FormsModule} from '@angular/forms';
25+
26+
/** @title Disabled readonly combobox. */
27+
@Component({
28+
selector: 'combobox-readonly-disabled-example',
29+
templateUrl: 'combobox-readonly-disabled-example.html',
30+
styleUrl: '../combobox-examples.css',
31+
imports: [
32+
Combobox,
33+
ComboboxInput,
34+
ComboboxPopup,
35+
ComboboxPopupContainer,
36+
Listbox,
37+
Option,
38+
FormsModule,
39+
],
40+
changeDetection: ChangeDetectionStrategy.OnPush,
41+
})
42+
export class ComboboxReadonlyDisabledExample {
43+
popover = viewChild<ElementRef>('popover');
44+
listbox = viewChild<Listbox<any>>(Listbox);
45+
combobox = viewChild<Combobox<any>>(Combobox);
46+
47+
options = () => states;
48+
searchString = signal('');
49+
50+
constructor() {
51+
afterRenderEffect(() => {
52+
const popover = this.popover()!;
53+
const combobox = this.combobox()!;
54+
combobox.expanded() ? this.showPopover() : popover.nativeElement.hidePopover();
55+
56+
this.listbox()?.scrollActiveItemIntoView();
57+
});
58+
}
59+
60+
showPopover() {
61+
const popover = this.popover()!;
62+
const combobox = this.combobox()!;
63+
64+
const comboboxRect = combobox.inputElement()?.getBoundingClientRect();
65+
const popoverEl = popover.nativeElement;
66+
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+
}
72+
73+
popover.nativeElement.showPopover();
74+
}
75+
}
76+
77+
const states = ['Option 1', 'Option 2', 'Option 3'];

src/components-examples/aria/combobox/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ export {ComboboxDialogExample} from './combobox-dialog/combobox-dialog-example';
22
export {ComboboxManualExample} from './combobox-manual/combobox-manual-example';
33
export {ComboboxAutoSelectExample} from './combobox-auto-select/combobox-auto-select-example';
44
export {ComboboxHighlightExample} from './combobox-highlight/combobox-highlight-example';
5+
export {ComboboxDisabledExample} from './combobox-disabled/combobox-disabled-example';
6+
57
export {ComboboxReadonlyExample} from './combobox-readonly/combobox-readonly-example';
68
export {ComboboxReadonlyMultiselectExample} from './combobox-readonly-multiselect/combobox-readonly-multiselect-example';
9+
export {ComboboxReadonlyDisabledExample} from './combobox-readonly-disabled/combobox-readonly-disabled-example';
710

811
export {ComboboxTreeManualExample} from './combobox-tree-manual/combobox-tree-manual-example';
912
export {ComboboxTreeAutoSelectExample} from './combobox-tree-auto-select/combobox-tree-auto-select-example';

0 commit comments

Comments
 (0)