Skip to content
Merged
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
15 changes: 14 additions & 1 deletion src/aria/combobox/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import {
afterRenderEffect,
booleanAttribute,
computed,
contentChild,
Directive,
Expand Down Expand Up @@ -76,7 +77,7 @@ export class Combobox<V> {
private _hasBeenFocused = signal(false);

/** Whether the combobox is disabled. */
readonly disabled = input(false);
readonly disabled = input(false, {transform: booleanAttribute});

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

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

/** Input element connected to the combobox, if any. */
Expand Down Expand Up @@ -129,6 +131,16 @@ export class Combobox<V> {
}
});
}

/** Expands the combobox popup. */
expand() {
this._pattern.open();
}

/** Collapses the combobox popup. */
collapse() {
this._pattern.close();
}
}

@Directive({
Expand All @@ -137,6 +149,7 @@ export class Combobox<V> {
host: {
'role': 'combobox',
'[value]': 'value()',
'[attr.aria-disabled]': 'combobox._pattern.disabled()',
'[attr.aria-expanded]': 'combobox._pattern.expanded()',
'[attr.aria-activedescendant]': 'combobox._pattern.activeDescendant()',
'[attr.aria-controls]': 'combobox._pattern.popupId()',
Expand Down
5 changes: 4 additions & 1 deletion src/aria/private/combobox/combobox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ export class ComboboxPattern<T extends ListItem<V>, V> {
/** Whether the combobox is expanded. */
expanded = signal(false);

/** Whether the combobox is disabled. */
disabled = () => this.inputs.disabled();

/** The ID of the active item in the combobox. */
activeDescendant = computed(() => {
const popupControls = this.inputs.popupControls();
Expand Down Expand Up @@ -177,7 +180,7 @@ export class ComboboxPattern<T extends ListItem<V>, V> {
hasPopup = computed(() => this.inputs.popupControls()?.role() || null);

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

/** Returns the listbox controls for the combobox. */
listControls = () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<div ngCombobox #combobox="ngCombobox" class="example-combobox-container" disabled>
<div class="example-combobox-input-container">
<span class="material-symbols-outlined example-icon example-search-icon">search</span>
<input
ngComboboxInput
class="example-combobox-input"
placeholder="Search..."
[(value)]="searchString"
/>
</div>

<div popover="manual" #popover class="example-popover">
<ng-template ngComboboxPopupContainer>
<div ngListbox class="example-listbox">
@for (option of options(); track option) {
<div
class="example-option example-selectable example-stateful"
ngOption
[value]="option"
[label]="option"
>
<span>{{option}}</span>
<span
aria-hidden="true"
class="material-symbols-outlined example-icon example-selected-icon"
>check</span
>
</div>
}
</div>
</ng-template>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox';
import {
afterRenderEffect,
ChangeDetectionStrategy,
Component,
computed,
ElementRef,
signal,
viewChild,
} from '@angular/core';
import {FormsModule} from '@angular/forms';

/** @title Disabled combobox example. */
@Component({
selector: 'combobox-disabled-example',
templateUrl: 'combobox-disabled-example.html',
styleUrl: '../combobox-examples.css',
imports: [
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
Listbox,
Option,
FormsModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ComboboxDisabledExample {
popover = viewChild<ElementRef>('popover');
listbox = viewChild<Listbox<any>>(Listbox);
combobox = viewChild<Combobox<any>>(Combobox);

searchString = signal('');

options = computed(() =>
states.filter(state => state.toLowerCase().startsWith(this.searchString().toLowerCase())),
);

constructor() {
afterRenderEffect(() => {
const popover = this.popover()!;
const combobox = this.combobox()!;
combobox.expanded() ? this.showPopover() : popover.nativeElement.hidePopover();

this.listbox()?.scrollActiveItemIntoView();
});
}

showPopover() {
const popover = this.popover()!;
const combobox = this.combobox()!;

const comboboxRect = combobox.inputElement()?.getBoundingClientRect();
const popoverEl = popover.nativeElement;

if (comboboxRect) {
popoverEl.style.width = `${comboboxRect.width}px`;
popoverEl.style.top = `${comboboxRect.bottom + 4}px`;
popoverEl.style.left = `${comboboxRect.left - 1}px`;
}

popover.nativeElement.showPopover();
}
}

const states = [
'Alabama',
'Alaska',
'Arizona',
'Arkansas',
'California',
'Colorado',
'Connecticut',
'Delaware',
'Florida',
'Georgia',
'Hawaii',
'Idaho',
'Illinois',
'Indiana',
'Iowa',
'Kansas',
'Kentucky',
'Louisiana',
'Maine',
'Maryland',
'Massachusetts',
'Michigan',
'Minnesota',
'Mississippi',
'Missouri',
'Montana',
'Nebraska',
'Nevada',
'New Hampshire',
'New Jersey',
'New Mexico',
'New York',
'North Carolina',
'North Dakota',
'Ohio',
'Oklahoma',
'Oregon',
'Pennsylvania',
'Rhode Island',
'South Carolina',
'South Dakota',
'Tennessee',
'Texas',
'Utah',
'Vermont',
'Virginia',
'Washington',
'West Virginia',
'Wisconsin',
'Wyoming',
];
9 changes: 7 additions & 2 deletions src/components-examples/aria/combobox/combobox-examples.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
border-radius: var(--mat-sys-corner-extra-small);
}

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

Expand All @@ -22,7 +22,7 @@
border-radius: var(--mat-sys-corner-extra-small);
}

.example-combobox-input[readonly='true'] {
.example-combobox-input[readonly='true']:not([aria-disabled='true']) {
cursor: pointer;
padding: 0.7rem 1rem;
}
Expand Down Expand Up @@ -182,3 +182,8 @@ ul[role='group'] {
.example-tree-item[aria-selected='true'] .example-selected-icon {
visibility: visible;
}

.example-combobox-container:has([aria-disabled='true']) {
opacity: 0.4;
cursor: default;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<div ngCombobox #combobox="ngCombobox" class="example-combobox-container" [readonly]="true" [disabled]="true">
<div class="example-combobox-input-container">
<input
ngComboboxInput
class="example-combobox-input"
placeholder="Search..."
[(value)]="searchString"
/>
<span class="material-symbols-outlined example-icon example-arrow-icon">arrow_drop_down</span>
</div>

<div popover="manual" #popover class="example-popover">
<ng-template ngComboboxPopupContainer>
<div ngListbox class="example-listbox">
@for (option of options(); track option) {
<div
class="example-option example-selectable example-stateful"
ngOption
[value]="option"
[label]="option"
>
<span>{{option}}</span>
<span
aria-hidden="true"
class="material-symbols-outlined example-icon example-selected-icon"
>check</span
>
</div>
}
</div>
</ng-template>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/

import {
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox';
import {
afterRenderEffect,
ChangeDetectionStrategy,
Component,
ElementRef,
signal,
viewChild,
} from '@angular/core';
import {FormsModule} from '@angular/forms';

/** @title Disabled readonly combobox. */
@Component({
selector: 'combobox-readonly-disabled-example',
templateUrl: 'combobox-readonly-disabled-example.html',
styleUrl: '../combobox-examples.css',
imports: [
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
Listbox,
Option,
FormsModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ComboboxReadonlyDisabledExample {
popover = viewChild<ElementRef>('popover');
listbox = viewChild<Listbox<any>>(Listbox);
combobox = viewChild<Combobox<any>>(Combobox);

options = () => states;
searchString = signal('');

constructor() {
afterRenderEffect(() => {
const popover = this.popover()!;
const combobox = this.combobox()!;
combobox.expanded() ? this.showPopover() : popover.nativeElement.hidePopover();

this.listbox()?.scrollActiveItemIntoView();
});
}

showPopover() {
const popover = this.popover()!;
const combobox = this.combobox()!;

const comboboxRect = combobox.inputElement()?.getBoundingClientRect();
const popoverEl = popover.nativeElement;

if (comboboxRect) {
popoverEl.style.width = `${comboboxRect.width}px`;
popoverEl.style.top = `${comboboxRect.bottom + 4}px`;
popoverEl.style.left = `${comboboxRect.left - 1}px`;
}

popover.nativeElement.showPopover();
}
}

const states = ['Option 1', 'Option 2', 'Option 3'];
3 changes: 3 additions & 0 deletions src/components-examples/aria/combobox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ export {ComboboxDialogExample} from './combobox-dialog/combobox-dialog-example';
export {ComboboxManualExample} from './combobox-manual/combobox-manual-example';
export {ComboboxAutoSelectExample} from './combobox-auto-select/combobox-auto-select-example';
export {ComboboxHighlightExample} from './combobox-highlight/combobox-highlight-example';
export {ComboboxDisabledExample} from './combobox-disabled/combobox-disabled-example';

export {ComboboxReadonlyExample} from './combobox-readonly/combobox-readonly-example';
export {ComboboxReadonlyMultiselectExample} from './combobox-readonly-multiselect/combobox-readonly-multiselect-example';
export {ComboboxReadonlyDisabledExample} from './combobox-readonly-disabled/combobox-readonly-disabled-example';

export {ComboboxTreeManualExample} from './combobox-tree-manual/combobox-tree-manual-example';
export {ComboboxTreeAutoSelectExample} from './combobox-tree-auto-select/combobox-tree-auto-select-example';
Expand Down
Loading
Loading