Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions goldens/aria/private/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,7 @@ export interface SimpleComboboxInputs extends ExpansionItem {
element: SignalLike<HTMLElement>;
inlineSuggestion: SignalLike<string | undefined>;
popup: SignalLike<SimpleComboboxPopupPattern | undefined>;
softDisabled?: SignalLike<boolean>;
value: WritableSignalLike<string>;
}

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

Expand Down
3 changes: 2 additions & 1 deletion goldens/aria/simple-combobox/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ export class Combobox extends DeferredContentAware implements OnInit {
readonly _pattern: SimpleComboboxPattern;
readonly _popup: _angular_core.WritableSignal<ComboboxPopup | undefined>;
_registerPopup(popup: ComboboxPopup): void;
readonly softDisabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
_unregisterPopup(): void;
readonly value: _angular_core.ModelSignal<string>;
// (undocumented)
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>;
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>;
// (undocumented)
static ɵfac: _angular_core.ɵɵFactoryDeclaration<Combobox, never>;
}
Expand Down
6 changes: 6 additions & 0 deletions src/aria/private/simple-combobox/simple-combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export interface SimpleComboboxInputs extends ExpansionItem {

/** Whether the combobox is disabled. */
disabled: SignalLike<boolean>;

/** Whether the combobox is soft disabled. */
softDisabled?: SignalLike<boolean>;
}

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

/** Whether the combobox is soft disabled. */
readonly softDisabled = () => this.inputs.softDisabled?.() ?? true;

/** An inline suggestion to be displayed in the input. */
readonly inlineSuggestion = () => this.inputs.inlineSuggestion();

Expand Down
34 changes: 34 additions & 0 deletions src/aria/simple-combobox/simple-combobox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,36 @@ describe('Combobox', () => {
expect(inputElement.getAttribute('aria-expanded')).toBe('true');
});
});

describe('Disabled', () => {
beforeEach(() => setupCombobox());

it('should keep the input focusable by default when disabled', () => {
fixture.componentInstance.disabled.set(true);
fixture.detectChanges();

expect(inputElement.disabled).toBe(false);
expect(inputElement.getAttribute('aria-disabled')).toBe('true');
});

it('should block interactions when disabled', () => {
fixture.componentInstance.disabled.set(true);
fixture.detectChanges();

focus();
keydown('ArrowDown');
expect(inputElement.getAttribute('aria-expanded')).toBe('false');
});

it('should make the input unfocusable when softDisabled is false', () => {
fixture.componentInstance.disabled.set(true);
fixture.componentInstance.softDisabled.set(false);
fixture.detectChanges();

expect(inputElement.disabled).toBe(true);
expect(inputElement.getAttribute('aria-disabled')).toBe('true');
});
});
});

describe('with Tree', () => {
Expand Down Expand Up @@ -1145,6 +1175,8 @@ describe('Combobox', () => {
[(value)]="searchString"
[(expanded)]="popupExpanded"
[readonly]="readonly()"
[disabled]="disabled()"
[softDisabled]="softDisabled()"
[alwaysExpanded]="alwaysExpanded()"
(focusout)="onBlur()"
/>
Expand All @@ -1168,6 +1200,8 @@ describe('Combobox', () => {
})
class ComboboxListboxExample {
readonly = signal(false);
disabled = signal(false);
softDisabled = signal(true);
alwaysExpanded = signal(false);
popupExpanded = signal(false);
searchString = signal('');
Expand Down
5 changes: 5 additions & 0 deletions src/aria/simple-combobox/simple-combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ import type {ComboboxPopup} from './simple-combobox-popup';
'[attr.aria-activedescendant]': '_pattern.activeDescendant()',
'[attr.aria-controls]': '_pattern.popupId()',
'[attr.aria-haspopup]': '_pattern.popupType()',
'[attr.tabindex]': 'disabled() && !softDisabled() ? -1 : null',
'[attr.disabled]': 'disabled() && !softDisabled() ? "" : null',
'(keydown)': '_pattern.onKeydown($event)',
'(focusin)': '_pattern.onFocusin()',
'(focusout)': '_pattern.onFocusout($event)',
Expand All @@ -73,6 +75,9 @@ export class Combobox extends DeferredContentAware implements OnInit {
/** Whether the combobox is disabled. */
readonly disabled = input(false, {transform: booleanAttribute});

/** Whether the combobox is soft disabled (remains focusable). */
readonly softDisabled = input(true, {transform: booleanAttribute});

/** Whether the combobox should always remain expanded. */
readonly alwaysExpanded = input(false, {transform: booleanAttribute});

Expand Down
127 changes: 127 additions & 0 deletions src/components-examples/aria/simple-combobox/autocomplete.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
.example-autocomplete {
display: flex;
position: relative;
align-items: center;

/* stylelint-disable-next-line material/no-prefixes -- Valid in all remotely recent browsers. */
width: fit-content;
}

.example-search-icon,
.example-check-icon {
font-size: 1.25rem;
pointer-events: none;
}

.example-search-icon {
left: 0.75rem;
position: absolute;
}

input[ngCombobox] {
width: 13rem;
font-size: 0.9rem;
border-radius: var(--mat-sys-corner-extra-small);
padding: 0.7rem 2.5rem;
outline-color: var(--mat-sys-primary);
border: 1px solid var(--mat-sys-outline);
background-color: var(--mat-sys-surface);
}

input[ngCombobox][aria-disabled='true'],
input[ngCombobox]:disabled {
cursor: default;
opacity: 0.5;
background-color: var(--mat-sys-surface-dim);
}


.example-clear-button {
position: absolute;
right: 0.5rem;
background-color: transparent;
border: none;
display: flex;
width: 2rem;
height: 2rem;
align-items: center;
cursor: pointer;
}

.example-clear-icon {
font-size: 1.25rem;
}

.example-popup {
width: 100%;
margin-top: 2px;
padding: 0.1rem;
max-height: 11rem;
border-radius: var(--mat-sys-corner-extra-small);
background-color: var(--mat-sys-surface);
border: 1px solid var(--mat-sys-outline);
}

.example-no-results {
padding: 1rem;
}

[ngListbox] {
gap: 2px;
height: 100%;
display: flex;
overflow: auto;
flex-direction: column;
}

[ngOption] {
display: flex;
cursor: pointer;
align-items: center;
margin: 1px;
font-size: 0.9rem;
padding: 0.7rem;
border-radius: var(--mat-sys-corner-extra-small);
}

[ngOption][aria-disabled='true'] {
cursor: default;
opacity: 0.5;
background-color: var(--mat-sys-surface-dim);
}

[ngOption]:hover {
background-color: color-mix(in srgb, var(--mat-sys-primary) 5%, transparent);
}

[ngOption][data-active='true'] {
outline-offset: -2px;
outline: 2px solid var(--mat-sys-primary);
}

[ngOption][aria-selected='true'] {
color: var(--mat-sys-primary);
background-color: color-mix(in srgb, var(--mat-sys-primary) 10%, transparent);
}

[ngOption]:not([aria-selected='true']) .example-check-icon {
display: none;
}

.example-option-label {
flex: 1;
}

.example-check-icon {
font-size: 0.9rem;
}

/* stylelint-disable material/no-prefixes -- Provides all prefixes for ::placeholder */
input::-webkit-input-placeholder,
input::-moz-placeholder, /* Firefox 19+ */
input:-ms-input-placeholder, /* IE 10+ */
input:-moz-placeholder, /* Firefox 18- */
input::placeholder {
color: var(--mat-sys-on-surface-variant);
}
/* stylelint-enable material/no-prefixes */
Loading
Loading