Skip to content

Commit 8c3f9b0

Browse files
authored
Autocomplete - Uses simple-combobox example (#33155)
* refactor(aria/combobox): update autocomplete examples to simple-combobox and add softDisabled * refactor(aria/combobox): replace tabbable with tabIndex in autocomplete examples * refactor(aria/combobox): revert autocomplete examples and move simple-combobox variants * refactor(aria/combobox): update the goldens
1 parent 75718e4 commit 8c3f9b0

20 files changed

Lines changed: 881 additions & 18 deletions

goldens/aria/private/index.api.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,7 @@ export interface SimpleComboboxInputs extends ExpansionItem {
671671
element: SignalLike<HTMLElement>;
672672
inlineSuggestion: SignalLike<string | undefined>;
673673
popup: SignalLike<SimpleComboboxPopupPattern | undefined>;
674+
softDisabled?: SignalLike<boolean>;
674675
value: WritableSignalLike<string>;
675676
}
676677

@@ -701,6 +702,7 @@ export class SimpleComboboxPattern {
701702
onKeydown(event: KeyboardEvent): void;
702703
readonly popupId: _angular_core.Signal<string | undefined>;
703704
readonly popupType: _angular_core.Signal<"listbox" | "tree" | "grid" | "dialog" | undefined>;
705+
readonly softDisabled: () => boolean;
704706
readonly value: WritableSignalLike<string>;
705707
}
706708

goldens/aria/simple-combobox/index.api.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ export class Combobox extends DeferredContentAware implements OnInit {
2525
readonly _pattern: SimpleComboboxPattern;
2626
readonly _popup: _angular_core.WritableSignal<ComboboxPopup | undefined>;
2727
_registerPopup(popup: ComboboxPopup): void;
28+
readonly softDisabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
2829
_unregisterPopup(): void;
2930
readonly value: _angular_core.ModelSignal<string>;
3031
// (undocumented)
31-
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<Combobox, "[ngCombobox]", ["ngCombobox"], { "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "alwaysExpanded": { "alias": "alwaysExpanded"; "required": false; "isSignal": true; }; "expanded": { "alias": "expanded"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; "inlineSuggestion": { "alias": "inlineSuggestion"; "required": false; "isSignal": true; }; }, { "expanded": "expandedChange"; "value": "valueChange"; }, never, never, true, never>;
32+
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<Combobox, "[ngCombobox]", ["ngCombobox"], { "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "softDisabled": { "alias": "softDisabled"; "required": false; "isSignal": true; }; "alwaysExpanded": { "alias": "alwaysExpanded"; "required": false; "isSignal": true; }; "expanded": { "alias": "expanded"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; "inlineSuggestion": { "alias": "inlineSuggestion"; "required": false; "isSignal": true; }; }, { "expanded": "expandedChange"; "value": "valueChange"; }, never, never, true, never>;
3233
// (undocumented)
3334
static ɵfac: _angular_core.ɵɵFactoryDeclaration<Combobox, never>;
3435
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ export interface SimpleComboboxInputs extends ExpansionItem {
3030

3131
/** Whether the combobox is disabled. */
3232
disabled: SignalLike<boolean>;
33+
34+
/** Whether the combobox is soft disabled. */
35+
softDisabled?: SignalLike<boolean>;
3336
}
3437

3538
/** Controls the state of a simple combobox. */
@@ -46,6 +49,9 @@ export class SimpleComboboxPattern {
4649
/** Whether the combobox is disabled. */
4750
readonly disabled = () => this.inputs.disabled();
4851

52+
/** Whether the combobox is soft disabled. */
53+
readonly softDisabled = () => this.inputs.softDisabled?.() ?? true;
54+
4955
/** An inline suggestion to be displayed in the input. */
5056
readonly inlineSuggestion = () => this.inputs.inlineSuggestion();
5157

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,36 @@ describe('Combobox', () => {
535535
expect(inputElement.getAttribute('aria-expanded')).toBe('true');
536536
});
537537
});
538+
539+
describe('Disabled', () => {
540+
beforeEach(() => setupCombobox());
541+
542+
it('should keep the input focusable by default when disabled', () => {
543+
fixture.componentInstance.disabled.set(true);
544+
fixture.detectChanges();
545+
546+
expect(inputElement.disabled).toBe(false);
547+
expect(inputElement.getAttribute('aria-disabled')).toBe('true');
548+
});
549+
550+
it('should block interactions when disabled', () => {
551+
fixture.componentInstance.disabled.set(true);
552+
fixture.detectChanges();
553+
554+
focus();
555+
keydown('ArrowDown');
556+
expect(inputElement.getAttribute('aria-expanded')).toBe('false');
557+
});
558+
559+
it('should make the input unfocusable when softDisabled is false', () => {
560+
fixture.componentInstance.disabled.set(true);
561+
fixture.componentInstance.softDisabled.set(false);
562+
fixture.detectChanges();
563+
564+
expect(inputElement.disabled).toBe(true);
565+
expect(inputElement.getAttribute('aria-disabled')).toBe('true');
566+
});
567+
});
538568
});
539569

540570
describe('with Tree', () => {
@@ -1145,6 +1175,8 @@ describe('Combobox', () => {
11451175
[(value)]="searchString"
11461176
[(expanded)]="popupExpanded"
11471177
[readonly]="readonly()"
1178+
[disabled]="disabled()"
1179+
[softDisabled]="softDisabled()"
11481180
[alwaysExpanded]="alwaysExpanded()"
11491181
(focusout)="onBlur()"
11501182
/>
@@ -1168,6 +1200,8 @@ describe('Combobox', () => {
11681200
})
11691201
class ComboboxListboxExample {
11701202
readonly = signal(false);
1203+
disabled = signal(false);
1204+
softDisabled = signal(true);
11711205
alwaysExpanded = signal(false);
11721206
popupExpanded = signal(false);
11731207
searchString = signal('');

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ import type {ComboboxPopup} from './simple-combobox-popup';
5151
'[attr.aria-activedescendant]': '_pattern.activeDescendant()',
5252
'[attr.aria-controls]': '_pattern.popupId()',
5353
'[attr.aria-haspopup]': '_pattern.popupType()',
54+
'[attr.tabindex]': 'disabled() && !softDisabled() ? -1 : null',
55+
'[attr.disabled]': 'disabled() && !softDisabled() ? "" : null',
5456
'(keydown)': '_pattern.onKeydown($event)',
5557
'(focusin)': '_pattern.onFocusin()',
5658
'(focusout)': '_pattern.onFocusout($event)',
@@ -73,6 +75,9 @@ export class Combobox extends DeferredContentAware implements OnInit {
7375
/** Whether the combobox is disabled. */
7476
readonly disabled = input(false, {transform: booleanAttribute});
7577

78+
/** Whether the combobox is soft disabled (remains focusable). */
79+
readonly softDisabled = input(true, {transform: booleanAttribute});
80+
7681
/** Whether the combobox should always remain expanded. */
7782
readonly alwaysExpanded = input(false, {transform: booleanAttribute});
7883

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
.example-autocomplete {
2+
display: flex;
3+
position: relative;
4+
align-items: center;
5+
6+
/* stylelint-disable-next-line material/no-prefixes -- Valid in all remotely recent browsers. */
7+
width: fit-content;
8+
}
9+
10+
.example-search-icon,
11+
.example-check-icon {
12+
font-size: 1.25rem;
13+
pointer-events: none;
14+
}
15+
16+
.example-search-icon {
17+
left: 0.75rem;
18+
position: absolute;
19+
}
20+
21+
input[ngCombobox] {
22+
width: 13rem;
23+
font-size: 0.9rem;
24+
border-radius: var(--mat-sys-corner-extra-small);
25+
padding: 0.7rem 2.5rem;
26+
outline-color: var(--mat-sys-primary);
27+
border: 1px solid var(--mat-sys-outline);
28+
background-color: var(--mat-sys-surface);
29+
}
30+
31+
input[ngCombobox][aria-disabled='true'],
32+
input[ngCombobox]:disabled {
33+
cursor: default;
34+
opacity: 0.5;
35+
background-color: var(--mat-sys-surface-dim);
36+
}
37+
38+
39+
.example-clear-button {
40+
position: absolute;
41+
right: 0.5rem;
42+
background-color: transparent;
43+
border: none;
44+
display: flex;
45+
width: 2rem;
46+
height: 2rem;
47+
align-items: center;
48+
cursor: pointer;
49+
}
50+
51+
.example-clear-icon {
52+
font-size: 1.25rem;
53+
}
54+
55+
.example-popup {
56+
width: 100%;
57+
margin-top: 2px;
58+
padding: 0.1rem;
59+
max-height: 11rem;
60+
border-radius: var(--mat-sys-corner-extra-small);
61+
background-color: var(--mat-sys-surface);
62+
border: 1px solid var(--mat-sys-outline);
63+
}
64+
65+
.example-no-results {
66+
padding: 1rem;
67+
}
68+
69+
[ngListbox] {
70+
gap: 2px;
71+
height: 100%;
72+
display: flex;
73+
overflow: auto;
74+
flex-direction: column;
75+
}
76+
77+
[ngOption] {
78+
display: flex;
79+
cursor: pointer;
80+
align-items: center;
81+
margin: 1px;
82+
font-size: 0.9rem;
83+
padding: 0.7rem;
84+
border-radius: var(--mat-sys-corner-extra-small);
85+
}
86+
87+
[ngOption][aria-disabled='true'] {
88+
cursor: default;
89+
opacity: 0.5;
90+
background-color: var(--mat-sys-surface-dim);
91+
}
92+
93+
[ngOption]:hover {
94+
background-color: color-mix(in srgb, var(--mat-sys-primary) 5%, transparent);
95+
}
96+
97+
[ngOption][data-active='true'] {
98+
outline-offset: -2px;
99+
outline: 2px solid var(--mat-sys-primary);
100+
}
101+
102+
[ngOption][aria-selected='true'] {
103+
color: var(--mat-sys-primary);
104+
background-color: color-mix(in srgb, var(--mat-sys-primary) 10%, transparent);
105+
}
106+
107+
[ngOption]:not([aria-selected='true']) .example-check-icon {
108+
display: none;
109+
}
110+
111+
.example-option-label {
112+
flex: 1;
113+
}
114+
115+
.example-check-icon {
116+
font-size: 0.9rem;
117+
}
118+
119+
/* stylelint-disable material/no-prefixes -- Provides all prefixes for ::placeholder */
120+
input::-webkit-input-placeholder,
121+
input::-moz-placeholder, /* Firefox 19+ */
122+
input:-ms-input-placeholder, /* IE 10+ */
123+
input:-moz-placeholder, /* Firefox 18- */
124+
input::placeholder {
125+
color: var(--mat-sys-on-surface-variant);
126+
}
127+
/* stylelint-enable material/no-prefixes */

0 commit comments

Comments
 (0)