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
53 changes: 41 additions & 12 deletions src/dev-app/autocomplete/autocomplete-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,40 @@
<input matInput [matAutocomplete]="reactiveAuto" [formControl]="stateCtrl">
</mat-form-field>
<mat-autocomplete #reactiveAuto="matAutocomplete" [displayWith]="displayFn"
[hideSingleSelectionIndicator]="reactiveHideSingleSelectionIndicator">
<mat-option *ngFor="let state of tempStates" [value]="state">
[hideSingleSelectionIndicator]="reactiveHideSingleSelectionIndicator"
[autoActiveFirstOption]="reactiveAutoActiveFirstOption">
<mat-option *ngFor="let state of tempStates; let index = index" [value]="state"
[disabled]="reactiveIsStateDisabled(state.index)">
<span>{{ state.name }}</span>
<span class="demo-secondary-text"> ({{ state.code }}) </span>
</mat-option>
</mat-autocomplete>

<mat-card-actions>
<p>
<button mat-button (click)="stateCtrl.reset()">RESET</button>
<button mat-button (click)="stateCtrl.setValue(states[10])">SET VALUE</button>
<button mat-button (click)="stateCtrl.enabled ? stateCtrl.disable() : stateCtrl.enable()">
TOGGLE DISABLED
</button>
</mat-card-actions>
<mat-card-actions>
</p>
<p>
<label for="reactive-disable-state-options">Disable States</label>
<select [(ngModel)]="reactiveDisableStateOption" id="reactive-disable-state-options">
<option value="none">None</option>
<option value="first-middle-last">Disable First, Middle and Last States</option>
<option value="all">Disable All States</option>
</select>
</p>
<p>
<mat-checkbox [(ngModel)]="reactiveHideSingleSelectionIndicator">
Hide Single-Selection Indicator
</mat-checkbox>
</mat-card-actions>
</p>
<p>
<mat-checkbox [(ngModel)]="reactiveAutoActiveFirstOption">
Automatically activate first option
</mat-checkbox>
</p>

</mat-card>

Expand All @@ -44,14 +59,16 @@
<input matInput [matAutocomplete]="tdAuto" [(ngModel)]="currentState"
(ngModelChange)="tdStates = filterStates(currentState)" [disabled]="tdDisabled">
<mat-autocomplete #tdAuto="matAutocomplete"
[hideSingleSelectionIndicator]="templateHideSingleSelectionIndicator">
<mat-option *ngFor="let state of tdStates" [value]="state.name">
[hideSingleSelectionIndicator]="templateHideSingleSelectionIndicator"
[autoActiveFirstOption]="templateAutoActiveFirstOption">
<mat-option *ngFor="let state of tdStates" [value]="state.name"
[disabled]="templateIsStateDisabled(state.index)">
<span>{{ state.name }}</span>
</mat-option>
</mat-autocomplete>
</mat-form-field>

<mat-card-actions>
<p>
<button mat-button (click)="modelDir.reset()">RESET</button>
<button mat-button (click)="currentState='California'">SET VALUE</button>
<button mat-button (click)="tdDisabled=!tdDisabled">
Expand All @@ -62,12 +79,24 @@
{{theme.name}}
</option>
</select>
</mat-card-actions>
<mat-card-actions>
</p>
<p>
<mat-checkbox [(ngModel)]="templateHideSingleSelectionIndicator">
Hide Single-Selection Indicator
</mat-checkbox>
</mat-card-actions>
<p>
<mat-checkbox [(ngModel)]="templateAutoActiveFirstOption">
Automatically activate first option
</mat-checkbox>
</p>
<p>
<label for="template-disable-state-options">Disable States</label>
<select [(ngModel)]="templateDisableStateOption" id="template-disable-state-options">
<option value="none">None</option>
<option value="first-middle-last">Disable First, Middle and Last States</option>
<option value="all">Disable All States</option>
</select>
</p>

</mat-card>

Expand Down
36 changes: 33 additions & 3 deletions src/dev-app/autocomplete/autocomplete-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ import {ThemePalette} from '@angular/material/core';
export interface State {
code: string;
name: string;
index: number;
}

export interface StateGroup {
letter: string;
states: State[];
}

type DisableStateOption = 'none' | 'first-middle-last' | 'all';

@Component({
selector: 'autocomplete-demo',
templateUrl: 'autocomplete-demo.html',
Expand Down Expand Up @@ -55,7 +58,6 @@ export class AutocompleteDemo {

tdDisabled = false;
hideSingleSelectionIndicators = false;

reactiveStatesTheme: ThemePalette = 'primary';
templateStatesTheme: ThemePalette = 'primary';

Expand All @@ -68,6 +70,12 @@ export class AutocompleteDemo {
reactiveHideSingleSelectionIndicator = false;
templateHideSingleSelectionIndicator = false;

reactiveAutoActiveFirstOption = false;
templateAutoActiveFirstOption = false;

reactiveDisableStateOption: DisableStateOption = 'none';
templateDisableStateOption: DisableStateOption = 'none';

@ViewChild(NgModel) modelDir: NgModel;

groupedStates: StateGroup[];
Expand Down Expand Up @@ -123,7 +131,7 @@ export class AutocompleteDemo {
{code: 'WV', name: 'West Virginia'},
{code: 'WI', name: 'Wisconsin'},
{code: 'WY', name: 'Wyoming'},
];
].map((state, index) => ({...state, index}));

constructor() {
this.tdStates = this.states;
Expand All @@ -142,7 +150,7 @@ export class AutocompleteDemo {
groups.push(group);
}

group.states.push({code: state.code, name: state.name});
group.states.push({...state});

return groups;
},
Expand Down Expand Up @@ -172,4 +180,26 @@ export class AutocompleteDemo {
const filterValue = val.toLowerCase();
return states.filter(state => state.name.toLowerCase().startsWith(filterValue));
}

reactiveIsStateDisabled(index: number) {
return this._isStateDisabled(index, this.reactiveDisableStateOption);
}

templateIsStateDisabled(index: number) {
return this._isStateDisabled(index, this.templateDisableStateOption);
}

private _isStateDisabled(stateIndex: number, disableStateOption: DisableStateOption) {
if (disableStateOption === 'all') {
return true;
}
if (disableStateOption === 'first-middle-last') {
return (
stateIndex === 0 ||
stateIndex === this.states.length - 1 ||
stateIndex === Math.floor(this.states.length / 2)
);
}
return false;
}
}
14 changes: 11 additions & 3 deletions src/dev-app/select/select-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
<mat-label>Drink</mat-label>
<mat-select [(ngModel)]="currentDrink" [required]="drinksRequired"
[disabled]="drinksDisabled" #drinkControl="ngModel">
<mat-option [disabled]="drinksOptionsDisabled">None</mat-option>
<mat-option *ngFor="let drink of drinks" [value]="drink.value" [disabled]="drinksOptionsDisabled">
<mat-option [disabled]="drinksOptionsDisabled === 'all'">None</mat-option>
<mat-option *ngFor="let drink of drinks; let index = index" [value]="drink.value"
[disabled]="isDrinkOptionDisabled(index)">
{{ drink.viewValue }}
</mat-option>
</mat-select>
Expand Down Expand Up @@ -48,11 +49,18 @@
</option>
</select>
</p>
<p>
<label for="drinks-disabled-options">Disabled options:</label>
<select [(ngModel)]="drinksOptionsDisabled" id="drinks-disabled-options">
<option value="none">None</option>
<option value="first-middle-last">Disable First, Middle, and Last Options</option>
<option value="all">Disable All Options</option>
</select>
</p>

<button mat-button (click)="currentDrink='water-2'">SET VALUE</button>
<button mat-button (click)="drinksRequired=!drinksRequired">TOGGLE REQUIRED</button>
<button mat-button (click)="drinksDisabled=!drinksDisabled">TOGGLE DISABLED</button>
<button mat-button (click)="drinksOptionsDisabled=!drinksOptionsDisabled">TOGGLE DISABLED OPTIONS</button>
<button mat-button (click)="drinkControl.reset()">RESET</button>
</mat-card-content>
</mat-card>
Expand Down
18 changes: 17 additions & 1 deletion src/dev-app/select/select-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export class MyErrorStateMatcher implements ErrorStateMatcher {
}
}

type DisableDrinkOption = 'none' | 'first-middle-last' | 'all';

@Component({
selector: 'select-demo',
templateUrl: 'select-demo.html',
Expand All @@ -50,7 +52,7 @@ export class SelectDemo {
drinkObjectRequired = false;
pokemonRequired = false;
drinksDisabled = false;
drinksOptionsDisabled = false;
drinksOptionsDisabled: DisableDrinkOption = 'none';
pokemonDisabled = false;
pokemonOptionsDisabled = false;
showSelect = false;
Expand Down Expand Up @@ -204,4 +206,18 @@ export class SelectDemo {
toggleSelected() {
this.currentAppearanceValue = this.currentAppearanceValue ? null : this.digimon[0].value;
}

isDrinkOptionDisabled(index: number) {
if (this.drinksOptionsDisabled === 'all') {
return true;
}
if (this.drinksOptionsDisabled === 'first-middle-last') {
return (
index === 0 ||
index === this.drinks.length - 1 ||
index === Math.floor(this.drinks.length / 2)
);
}
return false;
}
}
23 changes: 18 additions & 5 deletions src/material/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -748,16 +748,29 @@ export abstract class _MatAutocompleteTriggerBase
}

/**
* Resets the active item to -1 so arrow events will activate the
* correct options, or to 0 if the consumer opted into it.
* Reset the active item to -1. This is so that pressing arrow keys will activate the correct
* option.
*
* If the consumer opted-in to automatically activatating the first option, activate the first
* *enabled* option.
*/
private _resetActiveItem(): void {
const autocomplete = this.autocomplete;

if (autocomplete.autoActiveFirstOption) {
// Note that we go through `setFirstItemActive`, rather than `setActiveItem(0)`, because
// the former will find the next enabled option, if the first one is disabled.
autocomplete._keyManager.setFirstItemActive();
// Find the index of the first *enabled* option. Avoid calling `_keyManager.setActiveItem`
// because it activates the first option that passes the skip predicate, rather than the
// first *enabled* option.
let firstEnabledOptionIndex = -1;

for (let index = 0; index < autocomplete.options.length; index++) {
const option = autocomplete.options.get(index)!;
if (!option.disabled) {
firstEnabledOptionIndex = index;
break;
}
}
autocomplete._keyManager.setActiveItem(firstEnabledOptionIndex);
} else {
autocomplete._keyManager.setActiveItem(-1);
}
Expand Down
17 changes: 17 additions & 0 deletions src/material/autocomplete/autocomplete.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2307,6 +2307,23 @@ describe('MDC-based MatAutocomplete', () => {
}),
);

it('should not activate any option if all options are disabled', fakeAsync(() => {
const testComponent = fixture.componentInstance;
testComponent.trigger.autocomplete.autoActiveFirstOption = true;
for (const state of testComponent.states) {
state.disabled = true;
}
testComponent.trigger.openPanel();
fixture.detectChanges();
zone.simulateZoneExit();
fixture.detectChanges();

const selectedOptions = overlayContainerElement.querySelectorAll(
'mat-option.mat-mdc-option-active',
);
expect(selectedOptions.length).withContext('expected no options to be active').toBe(0);
}));

it('should remove aria-activedescendant when panel is closed with autoActiveFirstOption', fakeAsync(() => {
const input: HTMLElement = fixture.nativeElement.querySelector('input');

Expand Down
26 changes: 25 additions & 1 deletion src/material/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,9 @@ export abstract class _MatAutocompleteBase
}

ngAfterContentInit() {
this._keyManager = new ActiveDescendantKeyManager<_MatOptionBase>(this.options).withWrap();
this._keyManager = new ActiveDescendantKeyManager<_MatOptionBase>(this.options)
.withWrap()
.skipPredicate(this._skipPredicate);
this._activeOptionChanges = this._keyManager.change.subscribe(index => {
if (this.isOpen) {
this.optionActivated.emit({source: this, option: this.options.toArray()[index] || null});
Expand Down Expand Up @@ -318,6 +320,10 @@ export abstract class _MatAutocompleteBase
classList['mat-warn'] = this._color === 'warn';
classList['mat-accent'] = this._color === 'accent';
}

protected _skipPredicate(option: _MatOptionBase) {
return option.disabled;
}
}

@Component({
Expand Down Expand Up @@ -363,4 +369,22 @@ export class MatAutocomplete extends _MatAutocompleteBase {
}
}
}

// `skipPredicate` determines if key manager should avoid putting a given option in the tab
// order. Allow disabled list items to receive focus via keyboard to align with WAI ARIA
// recommendation.
//
// Normally WAI ARIA's instructions are to exclude disabled items from the tab order, but it
// makes a few exceptions for compound widgets.
//
// From [Developing a Keyboard Interface](
// https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/):
// "For the following composite widget elements, keep them focusable when disabled: Options in a
// Listbox..."
//
// The user can focus disabled options using the keyboard, but the user cannot click disabled
// options.
protected override _skipPredicate(_option: _MatOptionBase) {
return false;
}
}
5 changes: 3 additions & 2 deletions src/material/core/option/_option-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
// we have explicitly set the default color.
@include mdc-theme.prop(color, text-primary-on-background);

// Increase specificity to override styles from list theme.
&:hover:not(.mdc-list-item--disabled),
&:focus:not(.mdc-list-item--disabled),
&.mat-mdc-option-active,
&:focus.mdc-list-item,
&.mat-mdc-option-active.mdc-list-item,

// In multiple mode there is a checkbox to show that the option is selected.
&.mdc-list-item--selected:not(.mat-mdc-option-multiple):not(.mdc-list-item--disabled) {
Expand Down
17 changes: 16 additions & 1 deletion src/material/core/option/option.scss
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,23 @@
&.mdc-list-item--disabled {
// This is the same as `mdc-list-mixins.list-disabled-opacity` which
// we can't use directly, because it comes with some selectors.
opacity: mdc-list-variables.$content-disabled-opacity;
cursor: default;

// Give the visual content of this list item a lower opacity. This creates the "gray" appearance
// for disabled state. Set the opacity on the pseudo checkbox and projected content. Set
// opacity only on the visual content rather than the entire list-item so we don't affect the
// focus ring from `.mat-mdc-focus-indicator`.
//
// MatOption uses a child `<div>` element for its focus state to align with how ListItem does
// its focus state.
.mat-mdc-option-pseudo-checkbox, .mdc-list-item__primary-text, > mat-icon {
opacity: mdc-list-variables.$content-disabled-opacity;
}

// Prevent clicking on disabled options with mouse. Support focusing on disabled option using
// keyboard, but not with mouse.
pointer-events: none;

}

// Note that we bump the padding here, rather than padding inside the
Expand Down
Loading