diff --git a/scripts/check-mdc-tests-config.ts b/scripts/check-mdc-tests-config.ts
index 40c3a43d9599..6ece0271e3c0 100644
--- a/scripts/check-mdc-tests-config.ts
+++ b/scripts/check-mdc-tests-config.ts
@@ -36,6 +36,12 @@ export const config = {
// This test checks something that isn't supported in the MDC form field.
'should propagate the dynamic `placeholder` value to the form field',
+
+ // Disabled, because the MDC-based chip input doesn't deal with focus escaping anymore.
+ 'should not allow focus to escape when tabbing backwards',
+
+ // Disabled, because preventing the default action isn't required.
+ 'should prevent the default click action when the chip is disabled',
],
'mdc-dialog': [
// These tests are verifying implementation details that are not relevant for MDC.
diff --git a/src/dev-app/chips/chips-demo.html b/src/dev-app/chips/chips-demo.html
index 77bd05a2341b..b5ebf78d29b4 100644
--- a/src/dev-app/chips/chips-demo.html
+++ b/src/dev-app/chips/chips-demo.html
@@ -70,12 +70,12 @@
With avatar and icons
-
+
Mal
-
+
Husi
cancel
diff --git a/src/dev-app/mdc-chips/mdc-chips-demo.html b/src/dev-app/mdc-chips/mdc-chips-demo.html
index d5f947051f45..228e8ea2f88f 100644
--- a/src/dev-app/mdc-chips/mdc-chips-demo.html
+++ b/src/dev-app/mdc-chips/mdc-chips-demo.html
@@ -55,12 +55,12 @@ With avatar, icons, and color
-
+
Mal
-
+
Husi
cancel
diff --git a/src/material-experimental/mdc-chips/BUILD.bazel b/src/material-experimental/mdc-chips/BUILD.bazel
index 89bf3fd154be..02ae91477dea 100644
--- a/src/material-experimental/mdc-chips/BUILD.bazel
+++ b/src/material-experimental/mdc-chips/BUILD.bazel
@@ -17,7 +17,10 @@ ng_module(
"**/*.spec.ts",
],
),
- assets = [":chips_scss"] + glob(["**/*.html"]),
+ assets = [
+ ":chip_scss",
+ ":chip_set_scss",
+ ] + glob(["**/*.html"]),
deps = [
"//src:dev_mode_types",
"//src/material-experimental/mdc-core",
@@ -40,8 +43,21 @@ sass_library(
)
sass_binary(
- name = "chips_scss",
- src = "chips.scss",
+ name = "chip_scss",
+ src = "chip.scss",
+ include_paths = [
+ "external/npm/node_modules",
+ ],
+ deps = [
+ "//src/material-experimental/mdc-helpers:mdc_helpers_scss_lib",
+ "//src/material-experimental/mdc-helpers:mdc_scss_deps_lib",
+ "//src/material/core:core_scss_lib",
+ ],
+)
+
+sass_binary(
+ name = "chip_set_scss",
+ src = "chip-set.scss",
include_paths = [
"external/npm/node_modules",
],
diff --git a/src/material-experimental/mdc-chips/_chips-theme.scss b/src/material-experimental/mdc-chips/_chips-theme.scss
index 73f54869d0f3..e4b6b9cd73c0 100644
--- a/src/material-experimental/mdc-chips/_chips-theme.scss
+++ b/src/material-experimental/mdc-chips/_chips-theme.scss
@@ -1,93 +1,93 @@
-@use '@material/chips/deprecated' as mdc-chips;
+@use '@material/chips/chip' as mdc-chip;
+@use '@material/chips/chip-theme' as mdc-chip-theme;
+@use '@material/chips/chip-set' as mdc-chip-set;
@use '@material/theme/theme-color' as mdc-theme-color;
+@use '@material/theme/color-palette' as mdc-color-palette;
@use 'sass:color';
@use 'sass:map';
@use '../mdc-helpers/mdc-helpers';
@use '../../material/core/typography/typography';
@use '../../material/core/theming/theming';
-@mixin _selected-color($color) {
- @include mdc-chips.fill-color($color, $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-chips.ink-color(text-primary-on-dark, $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-chips.selected-ink-color-without-ripple_(
- text-primary-on-dark,
- $query: mdc-helpers.$mat-theme-styles-query
- );
- @include mdc-chips.leading-icon-color(text-primary-on-dark,
- $query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-chips.trailing-icon-color(text-primary-on-dark,
- $query: mdc-helpers.$mat-theme-styles-query);
+// Customizes the appearance of a chip. Note that ideally we would be doing this using the
+// `theme-styles` mixin, however it has the following problems:
+// 1. Some of MDC's base styles have **very** high specificity. E.g. setting the background of a
+// non-selected, enabled chip uses a selector like `.chip:not(.selected):not(.disabled)` instead of
+// just `.chip`. This specificity increase has a ripple effect over all other components that are
+// built on top of ours, making overrides extremely difficult and brittle.
+// 2. Including the individual mixins allows us to avoid a lot of unnecessary CSS (~35kb in the
+// dev app theme).
+@mixin _chip-variant($background, $foreground) {
+ @include mdc-chip-theme.container-color($background);
+ @include mdc-chip-theme.icon-color($foreground);
+ @include mdc-chip-theme.trailing-action-color($foreground);
+ @include mdc-chip-theme.checkmark-color($foreground);
+ @include mdc-chip-theme.text-label-color($foreground);
+
+ // Technically the avatar is only supposed to have an image, but we also allow for icons.
+ // Set the color so the icons inherit the correct color.
+ .mat-mdc-chip-avatar {
+ color: $foreground;
+ }
+}
+
+@mixin _colored-chip($palette) {
+ $background: theming.get-color-from-palette($palette);
+ $foreground: theming.get-color-from-palette($palette, default-contrast);
+
+ &.mat-mdc-chip-selected,
+ &.mat-mdc-chip-highlighted {
+ @include _chip-variant($background, $foreground);
+ }
}
@mixin color($config-or-theme) {
$config: theming.get-color-config($config-or-theme);
- $primary: theming.get-color-from-palette(map.get($config, primary));
- $accent: theming.get-color-from-palette(map.get($config, accent));
- $warn: theming.get-color-from-palette(map.get($config, warn));
- $background: map.get($config, background);
- $unselected-background: theming.get-color-from-palette($background, unselected-chip);
-
- // Save original values of MDC global variables. We need to save these so we can restore the
- // variables to their original values and prevent unintended side effects from using this mixin.
- $orig-mdc-chips-fill-color-default: mdc-chips.$fill-color-default;
- $orig-mdc-chips-ink-color-default: mdc-chips.$ink-color-default;
- $orig-mdc-chips-icon-color: mdc-chips.$icon-color;
+ $primary: map.get($config, primary);
+ $accent: map.get($config, accent);
+ $warn: map.get($config, warn);
+ $foreground: map.get($config, foreground);
+ $is-dark: map.get($config, is-dark);
@include mdc-helpers.mat-using-mdc-theme($config) {
- mdc-chips.$fill-color-default:
- color.mix(mdc-theme-color.prop-value(on-surface), mdc-theme-color.prop-value(surface), 12%);
- mdc-chips.$ink-color-default: rgba(mdc-theme-color.prop-value(on-surface), 0.87);
- mdc-chips.$icon-color: mdc-theme-color.prop-value(on-surface);
-
- @include mdc-chips.set-core-styles($query: mdc-helpers.$mat-theme-styles-query);
- @include mdc-chips.without-ripple($query: mdc-helpers.$mat-theme-styles-query);
+ .mat-mdc-standard-chip {
+ @include _chip-variant(
+ color.mix(mdc-theme-color.prop-value(on-surface), mdc-theme-color.prop-value(surface), 12%),
+ if($is-dark, mdc-color-palette.$grey-50, mdc-color-palette.$grey-900)
+ );
- .mat-mdc-chip {
- @include mdc-chips.fill-color-accessible($unselected-background,
- $query: mdc-helpers.$mat-theme-styles-query);
-
- // mdc-chip-fill-color-accessible includes mdc-chip-selected-ink-color which overrides the
- // opacity so selected chips always show a ripple.
- // Include the same mixins but use mdc-chip-selected-ink-color-without-ripple
&.mat-primary {
- &.mdc-chip--selected, &.mat-mdc-chip-highlighted {
- @include _selected-color($primary);
- }
+ @include _colored-chip($primary);
}
&.mat-accent {
- &.mdc-chip--selected, &.mat-mdc-chip-highlighted {
- @include _selected-color($accent);
- }
+ @include _colored-chip($accent);
}
&.mat-warn {
- &.mdc-chip--selected, &.mat-mdc-chip-highlighted {
- @include _selected-color($warn);
- }
+ @include _colored-chip($warn);
}
}
}
- // Restore original values of MDC global variables.
- mdc-chips.$fill-color-default: $orig-mdc-chips-fill-color-default;
- mdc-chips.$ink-color-default: $orig-mdc-chips-ink-color-default;
- mdc-chips.$icon-color: $orig-mdc-chips-icon-color;
+ .mat-mdc-chip-focus-overlay {
+ background: map.get($foreground, base);
+ }
}
@mixin typography($config-or-theme) {
$config: typography.private-typography-to-2018-config(
theming.get-typography-config($config-or-theme));
- @include mdc-chips.set-core-styles($query: mdc-helpers.$mat-typography-styles-query);
+ @include mdc-chip-set.core-styles($query: mdc-helpers.$mat-typography-styles-query);
@include mdc-helpers.mat-using-mdc-typography($config) {
- @include mdc-chips.without-ripple($query: mdc-helpers.$mat-typography-styles-query);
+ @include mdc-chip.without-ripple-styles($query: mdc-helpers.$mat-typography-styles-query);
}
}
@mixin density($config-or-theme) {
$density-scale: theming.get-density-config($config-or-theme);
.mat-mdc-chip {
- @include mdc-chips.density($density-scale, $query: mdc-helpers.$mat-base-styles-query);
+ @include mdc-chip-theme.density($density-scale, $query: mdc-helpers.$mat-base-styles-query);
}
}
diff --git a/src/material-experimental/mdc-chips/chip-action.ts b/src/material-experimental/mdc-chips/chip-action.ts
new file mode 100644
index 000000000000..d19bb24409b5
--- /dev/null
+++ b/src/material-experimental/mdc-chips/chip-action.ts
@@ -0,0 +1,151 @@
+/**
+ * @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.io/license
+ */
+
+import {
+ AfterViewInit,
+ ChangeDetectorRef,
+ Directive,
+ ElementRef,
+ Inject,
+ Input,
+ OnChanges,
+ OnDestroy,
+ SimpleChanges,
+} from '@angular/core';
+import {DOCUMENT} from '@angular/common';
+import {
+ MDCChipActionAdapter,
+ MDCChipActionFoundation,
+ MDCChipActionType,
+ MDCChipPrimaryActionFoundation,
+} from '@material/chips';
+import {emitCustomEvent} from './emit-event';
+import {
+ CanDisable,
+ HasTabIndex,
+ mixinDisabled,
+ mixinTabIndex,
+} from '@angular/material-experimental/mdc-core';
+
+const _MatChipActionMixinBase = mixinTabIndex(mixinDisabled(class {}), -1);
+
+/**
+ * Interactive element within a chip.
+ * @docs-private
+ */
+@Directive({
+ selector: '[matChipAction]',
+ inputs: ['disabled', 'tabIndex'],
+ host: {
+ 'class': 'mdc-evolution-chip__action mat-mdc-chip-action',
+ '[class.mdc-evolution-chip__action--primary]': `_getFoundation().actionType() === ${MDCChipActionType.PRIMARY}`,
+ // Note that while our actions are interactive, we have to add the `--presentational` class,
+ // in order to avoid some super-specific `:hover` styles from MDC.
+ '[class.mdc-evolution-chip__action--presentational]': `_getFoundation().actionType() === ${MDCChipActionType.PRIMARY}`,
+ '[class.mdc-evolution-chip__action--trailing]': `_getFoundation().actionType() === ${MDCChipActionType.TRAILING}`,
+ '[attr.tabindex]': '(disabled || !isInteractive) ? null : tabIndex',
+ '[attr.disabled]': "disabled ? '' : null",
+ '[attr.aria-disabled]': 'disabled',
+ '(click)': '_handleClick($event)',
+ '(keydown)': '_handleKeydown($event)',
+ },
+})
+export class MatChipAction
+ extends _MatChipActionMixinBase
+ implements AfterViewInit, OnDestroy, CanDisable, HasTabIndex, OnChanges
+{
+ private _document: Document;
+ private _foundation: MDCChipActionFoundation;
+ private _adapter: MDCChipActionAdapter = {
+ focus: () => this.focus(),
+ getAttribute: (name: string) => this._elementRef.nativeElement.getAttribute(name),
+ setAttribute: (name: string, value: string) => {
+ // MDC tries to update the tabindex directly in the DOM when navigating using the keyboard
+ // which overrides our own handling. If we detect such a case, assign it to the same property
+ // as the Angular binding in order to maintain consistency.
+ if (name === 'tabindex') {
+ this._updateTabindex(parseInt(value));
+ } else {
+ this._elementRef.nativeElement.setAttribute(name, value);
+ }
+ },
+ removeAttribute: (name: string) => {
+ if (name !== 'tabindex') {
+ this._elementRef.nativeElement.removeAttribute(name);
+ }
+ },
+ getElementID: () => this._elementRef.nativeElement.id,
+ emitEvent: (eventName: string, data: T) => {
+ emitCustomEvent(this._elementRef.nativeElement, this._document, eventName, data, true);
+ },
+ };
+
+ /** Whether the action is interactive. */
+ @Input() isInteractive = true;
+
+ _handleClick(_event: MouseEvent) {
+ // Usually these events can't happen while the chip is disabled since the browser won't
+ // allow them which is what MDC seems to rely on, however the event can be faked in tests.
+ if (!this.disabled && this.isInteractive) {
+ this._foundation.handleClick();
+ }
+ }
+
+ _handleKeydown(event: KeyboardEvent) {
+ // Usually these events can't happen while the chip is disabled since the browser won't
+ // allow them which is what MDC seems to rely on, however the event can be faked in tests.
+ if (!this.disabled && this.isInteractive) {
+ this._foundation.handleKeydown(event);
+ }
+ }
+
+ protected _createFoundation(adapter: MDCChipActionAdapter): MDCChipActionFoundation {
+ return new MDCChipPrimaryActionFoundation(adapter);
+ }
+
+ constructor(
+ public _elementRef: ElementRef,
+ @Inject(DOCUMENT) _document: any,
+ private _changeDetectorRef: ChangeDetectorRef,
+ ) {
+ super();
+ this._foundation = this._createFoundation(this._adapter);
+
+ if (_elementRef.nativeElement.nodeName === 'BUTTON') {
+ _elementRef.nativeElement.setAttribute('type', 'button');
+ }
+ }
+
+ ngAfterViewInit() {
+ this._foundation.init();
+ this._foundation.setDisabled(this.disabled);
+ }
+
+ ngOnChanges(changes: SimpleChanges) {
+ if (changes['disabled']) {
+ this._foundation.setDisabled(this.disabled);
+ }
+ }
+
+ ngOnDestroy() {
+ this._foundation.destroy();
+ }
+
+ focus() {
+ this._elementRef.nativeElement.focus();
+ }
+
+ _getFoundation() {
+ return this._foundation;
+ }
+
+ _updateTabindex(value: number) {
+ this.tabIndex = value;
+ this._changeDetectorRef.markForCheck();
+ }
+}
diff --git a/src/material-experimental/mdc-chips/chip-edit-input.spec.ts b/src/material-experimental/mdc-chips/chip-edit-input.spec.ts
index 6670c129de76..4d46370e978c 100644
--- a/src/material-experimental/mdc-chips/chip-edit-input.spec.ts
+++ b/src/material-experimental/mdc-chips/chip-edit-input.spec.ts
@@ -46,6 +46,6 @@ describe('MDC-based MatChipEditInput', () => {
});
@Component({
- template: ` `,
+ template: ` `,
})
class ChipEditInputContainer {}
diff --git a/src/material-experimental/mdc-chips/chip-edit-input.ts b/src/material-experimental/mdc-chips/chip-edit-input.ts
index 32410b20f948..9f6ffc6e8437 100644
--- a/src/material-experimental/mdc-chips/chip-edit-input.ts
+++ b/src/material-experimental/mdc-chips/chip-edit-input.ts
@@ -16,7 +16,7 @@ import {DOCUMENT} from '@angular/common';
@Directive({
selector: 'span[matChipEditInput]',
host: {
- 'class': 'mdc-chip__primary-action mat-chip-edit-input',
+ 'class': 'mat-chip-edit-input',
'role': 'textbox',
'tabindex': '-1',
'contenteditable': 'true',
diff --git a/src/material-experimental/mdc-chips/chip-grid.spec.ts b/src/material-experimental/mdc-chips/chip-grid.spec.ts
index 8e4874891730..bc8a9af2a4bc 100644
--- a/src/material-experimental/mdc-chips/chip-grid.spec.ts
+++ b/src/material-experimental/mdc-chips/chip-grid.spec.ts
@@ -13,59 +13,46 @@ import {
TAB,
} from '@angular/cdk/keycodes';
import {
- createKeyboardEvent,
- dispatchEvent,
dispatchFakeEvent,
dispatchKeyboardEvent,
- dispatchMouseEvent,
MockNgZone,
typeInElement,
-} from '../../cdk/testing/private';
+} from '@angular/cdk/testing/private';
import {
Component,
DebugElement,
NgZone,
- Provider,
QueryList,
Type,
ViewChild,
ViewChildren,
+ EventEmitter,
} from '@angular/core';
-import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
+import {ComponentFixture, fakeAsync, flush, TestBed, tick} from '@angular/core/testing';
import {FormControl, FormsModule, NgForm, ReactiveFormsModule, Validators} from '@angular/forms';
import {MatFormFieldModule} from '@angular/material-experimental/mdc-form-field';
import {MatInputModule} from '@angular/material-experimental/mdc-input';
import {By} from '@angular/platform-browser';
import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations';
-import {Subject} from 'rxjs';
-import {GridFocusKeyManager} from './grid-focus-key-manager';
-import {
- MatChipEvent,
- MatChipGrid,
- MatChipInputEvent,
- MatChipRemove,
- MatChipRow,
- MatChipsModule,
-} from './index';
+import {MDCChipAnimation} from '@material/chips';
+import {MatChipEvent, MatChipGrid, MatChipInputEvent, MatChipRow, MatChipsModule} from './index';
describe('MDC-based MatChipGrid', () => {
let chipGridDebugElement: DebugElement;
let chipGridNativeElement: HTMLElement;
let chipGridInstance: MatChipGrid;
let chips: QueryList;
- let manager: GridFocusKeyManager;
let zone: MockNgZone;
let testComponent: StandardChipGrid;
- let dirChange: Subject;
+ let directionality: {value: Direction; change: EventEmitter};
+ let primaryActions: NodeListOf;
const expectNoCellFocused = () => {
- expect(manager.activeRowIndex).toBe(-1);
- expect(manager.activeColumnIndex).toBe(-1);
+ expect(Array.from(primaryActions)).not.toContain(document.activeElement as HTMLElement);
};
const expectLastCellFocused = () => {
- expect(manager.activeRowIndex).toBe(chips.length - 1);
- expect(manager.activeColumnIndex).toBe(0);
+ expect(document.activeElement).toBe(primaryActions[primaryActions.length - 1]);
};
describe('StandardChipGrid', () => {
@@ -73,7 +60,7 @@ describe('MDC-based MatChipGrid', () => {
let fixture: ComponentFixture;
beforeEach(() => {
- fixture = setupStandardGrid();
+ fixture = createComponent(StandardChipGrid);
});
it('should add the `mat-mdc-chip-set` class', () => {
@@ -124,24 +111,21 @@ describe('MDC-based MatChipGrid', () => {
| ComponentFixture;
beforeEach(() => {
- fixture = setupStandardGrid();
+ fixture = createComponent(StandardChipGrid);
});
it('should focus the first chip on focus', () => {
chipGridInstance.focus();
fixture.detectChanges();
- expect(manager.activeRowIndex).toBe(0);
- expect(manager.activeColumnIndex).toBe(0);
+ expect(document.activeElement).toBe(primaryActions[0]);
});
- it('should watch for chip focus', () => {
- const lastIndex = chips.length - 1;
-
+ it('should focus the primary action when calling the `focus` method', () => {
chips.last.focus();
fixture.detectChanges();
- expect(manager.activeRowIndex).toBe(lastIndex);
+ expect(document.activeElement).toBe(primaryActions[primaryActions.length - 1]);
});
it('should not be able to become focused when disabled', () => {
@@ -180,13 +164,11 @@ describe('MDC-based MatChipGrid', () => {
testComponent.chips.splice(2, 1);
fixture.detectChanges();
- // It focuses the 4th item (now at index 2)
- expect(manager.activeRowIndex).toEqual(2);
+ // It focuses the 4th item
+ expect(document.activeElement).toBe(primaryActions[3]);
});
it('should focus the previous item', () => {
- const lastIndex = chips.length - 1;
-
// Focus the last item
chips.last.focus();
@@ -195,7 +177,7 @@ describe('MDC-based MatChipGrid', () => {
fixture.detectChanges();
// It focuses the next-to-last item
- expect(manager.activeRowIndex).toEqual(lastIndex - 1);
+ expect(document.activeElement).toBe(primaryActions[primaryActions.length - 2]);
});
it('should not focus if chip grid is not focused', fakeAsync(() => {
@@ -212,7 +194,7 @@ describe('MDC-based MatChipGrid', () => {
fixture.detectChanges();
// Should not have focus
- expect(chipGridInstance._keyManager.activeRowIndex).toEqual(-1);
+ expect(chipGridNativeElement.contains(document.activeElement)).toBe(false);
}));
it('should focus the grid if the last focused item is removed', () => {
@@ -234,93 +216,81 @@ describe('MDC-based MatChipGrid', () => {
fixture.destroy();
TestBed.resetTestingModule();
- fixture = createComponent(StandardChipGridWithAnimations, [], BrowserAnimationsModule);
+ fixture = createComponent(StandardChipGridWithAnimations, BrowserAnimationsModule);
chips.last.focus();
fixture.detectChanges();
- expect(chipGridInstance._keyManager.activeRowIndex).toBe(chips.length - 1);
+ expect(document.activeElement).toBe(primaryActions[primaryActions.length - 1]);
dispatchKeyboardEvent(chips.last._elementRef.nativeElement, 'keydown', BACKSPACE);
fixture.detectChanges();
tick(500);
- expect(chipGridInstance._keyManager.activeRowIndex).toBe(chips.length - 1);
- expect(chipGridInstance._keyManager.activeColumnIndex).toBe(0);
+ expect(document.activeElement).toBe(primaryActions[primaryActions.length - 2]);
}),
);
});
it('should have a focus indicator', () => {
- const focusableTextNativeElements = Array.from(
- chipGridNativeElement.querySelectorAll('.mat-mdc-chip-row-focusable-text-content'),
+ const focusIndicators = chipGridNativeElement.querySelectorAll(
+ '.mat-mdc-chip-primary-focus-indicator',
);
-
- expect(
- focusableTextNativeElements.every(element =>
- element.classList.contains('mat-mdc-focus-indicator'),
- ),
- ).toBe(true);
+ expect(focusIndicators.length).toBeGreaterThan(0);
+ expect(focusIndicators.length).toBe(chips.length);
});
});
describe('keyboard behavior', () => {
describe('LTR (default)', () => {
let fixture: ComponentFixture;
+ let trailingActions: NodeListOf;
- beforeEach(() => {
+ beforeEach(fakeAsync(() => {
fixture = createComponent(ChipGridWithRemove);
- });
+ flush();
+ trailingActions = chipGridNativeElement.querySelectorAll(
+ '.mdc-evolution-chip__action--trailing',
+ );
+ }));
it('should focus previous column when press LEFT ARROW', () => {
- let nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row');
- let lastNativeChip = nativeChips[nativeChips.length - 1] as HTMLElement;
-
- const lastRowIndex = chips.length - 1;
+ const lastIndex = primaryActions.length - 1;
// Focus the first column of the last chip in the array
chips.last.focus();
- expectLastCellFocused();
+ expect(document.activeElement).toBe(primaryActions[lastIndex]);
// Press the LEFT arrow
- dispatchKeyboardEvent(lastNativeChip, 'keydown', LEFT_ARROW);
-
- chipGridInstance._blur(); // Simulate focus leaving the list and going to the chip.
+ dispatchKeyboardEvent(primaryActions[lastIndex], 'keydown', LEFT_ARROW);
fixture.detectChanges();
// It focuses the last column of the previous chip
- expect(manager.activeRowIndex).toEqual(lastRowIndex - 1);
- expect(manager.activeColumnIndex).toEqual(1);
+ expect(document.activeElement).toBe(trailingActions[lastIndex - 1]);
});
it('should focus next column when press RIGHT ARROW', () => {
- let nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row');
- let firstNativeChip = nativeChips[0] as HTMLElement;
-
// Focus the first column of the first chip in the array
chips.first.focus();
- expect(manager.activeRowIndex).toEqual(0);
- expect(manager.activeColumnIndex).toEqual(0);
+ expect(document.activeElement).toBe(primaryActions[0]);
// Press the RIGHT arrow
- dispatchKeyboardEvent(firstNativeChip, 'keydown', RIGHT_ARROW);
- chipGridInstance._blur(); // Simulate focus leaving the list and going to the chip.
+ dispatchKeyboardEvent(primaryActions[0], 'keydown', RIGHT_ARROW);
fixture.detectChanges();
// It focuses the next column of the chip
- expect(manager.activeRowIndex).toEqual(0);
- expect(manager.activeColumnIndex).toEqual(1);
+ expect(document.activeElement).toBe(trailingActions[0]);
});
it('should not handle arrow key events from non-chip elements', () => {
- const initialActiveIndex = manager.activeRowIndex;
+ const previousActiveElement = document.activeElement;
dispatchKeyboardEvent(chipGridNativeElement, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
- expect(manager.activeRowIndex)
+ expect(document.activeElement)
.withContext('Expected focused item not to have changed.')
- .toBe(initialActiveIndex);
+ .toBe(previousActiveElement);
});
});
@@ -328,46 +298,35 @@ describe('MDC-based MatChipGrid', () => {
let fixture: ComponentFixture;
beforeEach(() => {
- fixture = setupStandardGrid('rtl');
+ fixture = createComponent(StandardChipGrid, undefined, 'rtl');
});
it('should focus previous column when press RIGHT ARROW', () => {
- let nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row');
- let lastNativeChip = nativeChips[nativeChips.length - 1] as HTMLElement;
-
- const lastRowIndex = chips.length - 1;
+ const lastIndex = primaryActions.length - 1;
// Focus the first column of the last chip in the array
chips.last.focus();
- expectLastCellFocused();
+ expect(document.activeElement).toBe(primaryActions[lastIndex]);
// Press the RIGHT arrow
- dispatchKeyboardEvent(lastNativeChip, 'keydown', RIGHT_ARROW);
- chipGridInstance._blur(); // Simulate focus leaving the list and going to the chip.
+ dispatchKeyboardEvent(primaryActions[lastIndex], 'keydown', RIGHT_ARROW);
fixture.detectChanges();
// It focuses the last column of the previous chip
- expect(manager.activeRowIndex).toEqual(lastRowIndex - 1);
- expect(manager.activeColumnIndex).toEqual(0);
+ expect(document.activeElement).toBe(primaryActions[lastIndex - 1]);
});
it('should focus next column when press LEFT ARROW', () => {
- let nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row');
- let firstNativeChip = nativeChips[0] as HTMLElement;
-
// Focus the first column of the first chip in the array
chips.first.focus();
- expect(manager.activeRowIndex).toEqual(0);
- expect(manager.activeColumnIndex).toEqual(0);
+ expect(document.activeElement).toBe(primaryActions[0]);
// Press the LEFT arrow
- dispatchKeyboardEvent(firstNativeChip, 'keydown', LEFT_ARROW);
- chipGridInstance._blur(); // Simulate focus leaving the list and going to the chip.
+ dispatchKeyboardEvent(primaryActions[0], 'keydown', LEFT_ARROW);
fixture.detectChanges();
// It focuses the next column of the chip
- expect(manager.activeRowIndex).toEqual(1);
- expect(manager.activeColumnIndex).toEqual(0);
+ expect(document.activeElement).toBe(primaryActions[1]);
});
it('should allow focus to escape when tabbing away', fakeAsync(() => {
@@ -380,7 +339,7 @@ describe('MDC-based MatChipGrid', () => {
.withContext('Expected tabIndex to be set to -1 temporarily.')
.toBe(-1);
- tick();
+ flush();
expect(chipGridInstance.tabIndex)
.withContext('Expected tabIndex to be reset back to 0')
@@ -389,7 +348,6 @@ describe('MDC-based MatChipGrid', () => {
it(`should use user defined tabIndex`, fakeAsync(() => {
chipGridInstance.tabIndex = 4;
-
fixture.detectChanges();
expect(chipGridInstance.tabIndex)
@@ -404,7 +362,7 @@ describe('MDC-based MatChipGrid', () => {
.withContext('Expected tabIndex to be set to -1 temporarily.')
.toBe(-1);
- tick();
+ flush();
expect(chipGridInstance.tabIndex)
.withContext('Expected tabIndex to be reset back to 4')
@@ -416,95 +374,71 @@ describe('MDC-based MatChipGrid', () => {
let fixture: ComponentFixture;
beforeEach(() => {
- fixture = setupStandardGrid();
+ fixture = createComponent(StandardChipGrid);
});
it('should account for the direction changing', () => {
- const firstNativeChip = chipGridNativeElement.querySelectorAll(
- 'mat-chip-row',
- )[0] as HTMLElement;
-
- const RIGHT_EVENT = createKeyboardEvent('keydown', RIGHT_ARROW);
-
chips.first.focus();
- expect(manager.activeRowIndex).toBe(0);
- expect(manager.activeColumnIndex).toBe(0);
+ expect(document.activeElement).toBe(primaryActions[0]);
- dispatchEvent(firstNativeChip, RIGHT_EVENT);
- chipGridInstance._blur();
+ dispatchKeyboardEvent(primaryActions[0], 'keydown', RIGHT_ARROW);
fixture.detectChanges();
- expect(manager.activeRowIndex).toBe(1);
- expect(manager.activeColumnIndex).toBe(0);
+ expect(document.activeElement).toBe(primaryActions[1]);
- dirChange.next('rtl');
+ directionality.value = 'rtl';
fixture.detectChanges();
- chipGridInstance._keydown(RIGHT_EVENT);
- chipGridInstance._blur();
+ dispatchKeyboardEvent(primaryActions[1], 'keydown', RIGHT_ARROW);
fixture.detectChanges();
- expect(manager.activeRowIndex).toBe(0);
- expect(manager.activeColumnIndex).toBe(0);
+ expect(document.activeElement).toBe(primaryActions[0]);
});
it('should move focus to the first chip when pressing HOME', () => {
- const nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row');
- const lastNativeChip = nativeChips[nativeChips.length - 1] as HTMLElement;
-
- const HOME_EVENT = createKeyboardEvent('keydown', HOME);
chips.last.focus();
+ expect(document.activeElement).toBe(primaryActions[4]);
- expect(manager.activeRowIndex).toBe(4);
- expect(manager.activeColumnIndex).toBe(0);
-
- dispatchEvent(lastNativeChip, HOME_EVENT);
+ const event = dispatchKeyboardEvent(primaryActions[4], 'keydown', HOME);
fixture.detectChanges();
- expect(HOME_EVENT.defaultPrevented).toBe(true);
- expect(manager.activeRowIndex).toBe(0);
- expect(manager.activeColumnIndex).toBe(0);
+ expect(event.defaultPrevented).toBe(true);
+ expect(document.activeElement).toBe(primaryActions[0]);
});
it('should move focus to the last chip when pressing END', () => {
- const nativeChips = chipGridNativeElement.querySelectorAll('mat-chip-row');
- const firstNativeChip = nativeChips[0] as HTMLElement;
-
- const END_EVENT = createKeyboardEvent('keydown', END);
chips.first.focus();
+ expect(document.activeElement).toBe(primaryActions[0]);
- expect(manager.activeRowIndex).toBe(0);
- expect(manager.activeColumnIndex).toBe(0);
-
- dispatchEvent(firstNativeChip, END_EVENT);
+ const event = dispatchKeyboardEvent(primaryActions[0], 'keydown', END);
fixture.detectChanges();
- expect(END_EVENT.defaultPrevented).toBe(true);
- expect(manager.activeRowIndex).toBe(4);
- expect(manager.activeColumnIndex).toBe(0);
+ expect(event.defaultPrevented).toBe(true);
+ expect(document.activeElement).toBe(primaryActions[4]);
});
- it('should ignore all non-tab navigation keyboard events from an editing chip', () => {
+ it('should ignore all non-tab navigation keyboard events from an editing chip', fakeAsync(() => {
testComponent.editable = true;
fixture.detectChanges();
chips.first.focus();
+ expect(document.activeElement).toBe(primaryActions[0]);
- dispatchKeyboardEvent(document.activeElement!, 'keydown', ENTER, 'Enter');
+ dispatchKeyboardEvent(document.activeElement!, 'keydown', ENTER);
fixture.detectChanges();
+ flush();
- const activeRowIndex = manager.activeRowIndex;
- const activeColumnIndex = manager.activeColumnIndex;
+ const previousActiveElement = document.activeElement;
+ const keysToIgnore = [HOME, END, LEFT_ARROW, RIGHT_ARROW];
- const KEYS_TO_IGNORE = [HOME, END, LEFT_ARROW, RIGHT_ARROW];
- for (const key of KEYS_TO_IGNORE) {
+ for (const key of keysToIgnore) {
dispatchKeyboardEvent(document.activeElement!, 'keydown', key);
fixture.detectChanges();
+ flush();
- expect(manager.activeRowIndex).toBe(activeRowIndex);
- expect(manager.activeColumnIndex).toBe(activeColumnIndex);
+ expect(document.activeElement).toBe(previousActiveElement);
}
- });
+ }));
});
});
});
@@ -513,14 +447,15 @@ describe('MDC-based MatChipGrid', () => {
let fixture: ComponentFixture;
beforeEach(() => {
- fixture = setupInputGrid();
+ fixture = createComponent(FormFieldChipGrid);
});
describe('keyboard behavior', () => {
it('should maintain focus if the active chip is deleted', () => {
const secondChip = fixture.nativeElement.querySelectorAll('.mat-mdc-chip')[1];
+ const secondChipAction = secondChip.querySelector('.mdc-evolution-chip__action--primary');
- secondChip.focus();
+ secondChipAction.focus();
fixture.detectChanges();
expect(chipGridInstance._chips.toArray().findIndex(chip => chip._hasFocus())).toBe(1);
@@ -601,27 +536,36 @@ describe('MDC-based MatChipGrid', () => {
describe('with chip remove', () => {
let fixture: ComponentFixture;
- let chipGrid: MatChipGrid;
- let chipRemoveDebugElements: DebugElement[];
+ let trailingActions: NodeListOf;
- beforeEach(() => {
+ beforeEach(fakeAsync(() => {
fixture = createComponent(ChipGridWithRemove);
+ flush();
+ trailingActions = chipGridNativeElement.querySelectorAll(
+ '.mdc-evolution-chip__action--trailing',
+ );
+ }));
- chipGrid = fixture.debugElement.query(By.directive(MatChipGrid))!.componentInstance;
- chipRemoveDebugElements = fixture.debugElement.queryAll(By.directive(MatChipRemove));
- });
-
- it('should properly focus next item if chip is removed through click', () => {
- chips.get(2)!.focus();
+ it('should properly focus next item if chip is removed through click', fakeAsync(() => {
+ const chip = chips.get(2)!;
+ chip.focus();
+ fixture.detectChanges();
// Destroy the third focused chip by dispatching a bubbling click event on the
// associated chip remove element.
- dispatchMouseEvent(chipRemoveDebugElements[2].nativeElement, 'click');
+ trailingActions[2].click();
+ fixture.detectChanges();
+ (chip as any)._handleAnimationend({
+ animationName: MDCChipAnimation.EXIT,
+ target: chip._elementRef.nativeElement,
+ });
+ flush();
+ (chip as any)._handleTransitionend({target: chip._elementRef.nativeElement});
+ flush();
fixture.detectChanges();
- expect(chips.get(2)!.value).not.toBe(2, 'Expected the third chip to be removed.');
- expect(chipGrid._keyManager.activeRowIndex).toBe(2);
- });
+ expect(document.activeElement).toBe(primaryActions[3]);
+ }));
});
describe('chip grid with chip input', () => {
@@ -671,10 +615,10 @@ describe('MDC-based MatChipGrid', () => {
fixture.detectChanges();
dispatchKeyboardEvent(nativeInput, 'keydown', ENTER);
fixture.detectChanges();
- tick();
+ flush();
dispatchFakeEvent(nativeInput, 'blur');
- tick();
+ flush();
expect(fixture.componentInstance.control.value).toContain('123-8');
}));
@@ -727,10 +671,10 @@ describe('MDC-based MatChipGrid', () => {
fixture.detectChanges();
dispatchKeyboardEvent(nativeInput, 'keydown', ENTER);
fixture.detectChanges();
- tick();
+ flush();
dispatchFakeEvent(nativeInput, 'blur');
- tick();
+ flush();
expect(fixture.componentInstance.control.dirty)
.withContext(`Expected control to be dirty after value was changed by user.`)
@@ -832,10 +776,10 @@ describe('MDC-based MatChipGrid', () => {
fixture.detectChanges();
dispatchKeyboardEvent(input, 'keydown', ENTER);
fixture.detectChanges();
- tick();
+ flush();
dispatchFakeEvent(input, 'blur');
- tick();
+ flush();
fixture.detectChanges();
expect(input.getAttribute('aria-invalid')).toBe('false');
@@ -1036,11 +980,16 @@ describe('MDC-based MatChipGrid', () => {
function createComponent(
component: Type,
- providers: Provider[] = [],
animationsModule:
| Type
| Type = NoopAnimationsModule,
+ direction: Direction = 'ltr',
): ComponentFixture {
+ directionality = {
+ value: direction,
+ change: new EventEmitter(),
+ } as Directionality;
+
TestBed.configureTestingModule({
imports: [
FormsModule,
@@ -1052,11 +1001,8 @@ describe('MDC-based MatChipGrid', () => {
],
declarations: [component],
providers: [
- {
- provide: NgZone,
- useFactory: () => (zone = new MockNgZone()),
- },
- ...providers,
+ {provide: NgZone, useFactory: () => (zone = new MockNgZone())},
+ {provide: Directionality, useValue: directionality},
],
}).compileComponents();
@@ -1068,28 +1014,12 @@ describe('MDC-based MatChipGrid', () => {
chipGridInstance = chipGridDebugElement.componentInstance;
testComponent = fixture.debugElement.componentInstance;
chips = chipGridInstance._chips;
- manager = chipGridInstance._keyManager;
+ primaryActions = chipGridNativeElement.querySelectorAll(
+ '.mdc-evolution-chip__action--primary',
+ );
return fixture;
}
-
- function setupStandardGrid(direction: Direction = 'ltr') {
- dirChange = new Subject();
-
- return createComponent(StandardChipGrid, [
- {
- provide: Directionality,
- useFactory: () => ({
- value: direction.toLowerCase(),
- change: dirChange,
- }),
- },
- ]);
- }
-
- function setupInputGrid() {
- return createComponent(FormFieldChipGrid);
- }
});
@Component({
diff --git a/src/material-experimental/mdc-chips/chip-grid.ts b/src/material-experimental/mdc-chips/chip-grid.ts
index 501b41987867..0b2ae83abd14 100644
--- a/src/material-experimental/mdc-chips/chip-grid.ts
+++ b/src/material-experimental/mdc-chips/chip-grid.ts
@@ -6,7 +6,6 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {Directionality} from '@angular/cdk/bidi';
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
import {TAB} from '@angular/cdk/keycodes';
import {
@@ -19,6 +18,7 @@ import {
DoCheck,
ElementRef,
EventEmitter,
+ Inject,
Input,
OnDestroy,
Optional,
@@ -34,19 +34,20 @@ import {
NgForm,
Validators,
} from '@angular/forms';
+import {DOCUMENT} from '@angular/common';
import {
CanUpdateErrorState,
ErrorStateMatcher,
mixinErrorState,
} from '@angular/material-experimental/mdc-core';
import {MatFormFieldControl} from '@angular/material-experimental/mdc-form-field';
+import {LiveAnnouncer} from '@angular/cdk/a11y';
import {MatChipTextControl} from './chip-text-control';
-import {merge, Observable, Subscription} from 'rxjs';
+import {Observable} from 'rxjs';
import {startWith, takeUntil} from 'rxjs/operators';
import {MatChipEvent} from './chip';
import {MatChipRow} from './chip-row';
import {MatChipSet} from './chip-set';
-import {GridFocusKeyManager} from './grid-focus-key-manager';
/** Change event object that is emitted when the chip grid value has changed. */
export class MatChipGridChange {
@@ -64,16 +65,17 @@ export class MatChipGridChange {
*/
class MatChipGridBase extends MatChipSet {
constructor(
- _elementRef: ElementRef,
- _changeDetectorRef: ChangeDetectorRef,
- _dir: Directionality,
+ liveAnnouncer: LiveAnnouncer,
+ document: any,
+ elementRef: ElementRef,
+ changeDetectorRef: ChangeDetectorRef,
public _defaultErrorStateMatcher: ErrorStateMatcher,
public _parentForm: NgForm,
public _parentFormGroup: FormGroupDirective,
/** @docs-private */
public ngControl: NgControl,
) {
- super(_elementRef, _changeDetectorRef, _dir);
+ super(liveAnnouncer, document, elementRef, changeDetectorRef);
}
}
const _MatChipGridMixinBase = mixinErrorState(MatChipGridBase);
@@ -84,11 +86,15 @@ const _MatChipGridMixinBase = mixinErrorState(MatChipGridBase);
*/
@Component({
selector: 'mat-chip-grid',
- template: ' ',
- styleUrls: ['chips.css'],
+ template: `
+
+
+
+ `,
+ styleUrls: ['chip-set.css'],
inputs: ['tabIndex'],
host: {
- 'class': 'mat-mdc-chip-set mat-mdc-chip-grid mdc-chip-set',
+ 'class': 'mat-mdc-chip-set mat-mdc-chip-grid mdc-evolution-chip-set',
'[attr.role]': 'role',
'[tabIndex]': '_chips && _chips.length === 0 ? -1 : tabIndex',
// TODO: replace this binding with use of AriaDescriber
@@ -101,7 +107,6 @@ const _MatChipGridMixinBase = mixinErrorState(MatChipGridBase);
'(focus)': 'focus()',
'(blur)': '_blur()',
'(keydown)': '_keydown($event)',
- '[id]': '_uid',
},
providers: [{provide: MatFormFieldControl, useExisting: MatChipGrid}],
encapsulation: ViewEncapsulation.None,
@@ -124,12 +129,6 @@ export class MatChipGrid
*/
readonly controlType: string = 'mat-chip-grid';
- /** Subscription to focus changes in the chips. */
- private _chipFocusSubscription: Subscription | null;
-
- /** Subscription to blur changes in the chips. */
- private _chipBlurSubscription: Subscription | null;
-
/** The chip input to add more chips */
protected _chipInput: MatChipTextControl;
@@ -145,9 +144,6 @@ export class MatChipGrid
*/
_onChange: (value: any) => void = () => {};
- /** The GridFocusKeyManager which handles focus. */
- _keyManager: GridFocusKeyManager;
-
/**
* Implemented as part of MatFormFieldControl.
* @docs-private
@@ -243,12 +239,12 @@ export class MatChipGrid
/** Combined stream of all of the child chips' blur events. */
get chipBlurChanges(): Observable {
- return merge(...this._chips.map(chip => chip._onBlur));
+ return this._getChipStream(chip => chip._onBlur);
}
/** Combined stream of all of the child chips' focus events. */
get chipFocusChanges(): Observable {
- return merge(...this._chips.map(chip => chip._onFocus));
+ return this._getChipStream(chip => chip._onFocus);
}
/** Emits when the chip grid value has been changed by the user. */
@@ -270,21 +266,23 @@ export class MatChipGrid
override _chips: QueryList;
constructor(
- _elementRef: ElementRef,
- _changeDetectorRef: ChangeDetectorRef,
- @Optional() _dir: Directionality,
- @Optional() _parentForm: NgForm,
- @Optional() _parentFormGroup: FormGroupDirective,
- _defaultErrorStateMatcher: ErrorStateMatcher,
+ liveAnnouncer: LiveAnnouncer,
+ @Inject(DOCUMENT) document: any,
+ elementRef: ElementRef,
+ changeDetectorRef: ChangeDetectorRef,
+ @Optional() parentForm: NgForm,
+ @Optional() parentFormGroup: FormGroupDirective,
+ defaultErrorStateMatcher: ErrorStateMatcher,
@Optional() @Self() ngControl: NgControl,
) {
super(
- _elementRef,
- _changeDetectorRef,
- _dir,
- _defaultErrorStateMatcher,
- _parentForm,
- _parentFormGroup,
+ liveAnnouncer,
+ document,
+ elementRef,
+ changeDetectorRef,
+ defaultErrorStateMatcher,
+ parentForm,
+ parentFormGroup,
ngControl,
);
if (this.ngControl) {
@@ -294,12 +292,15 @@ export class MatChipGrid
override ngAfterContentInit() {
super.ngAfterContentInit();
- this._initKeyManager();
this._chips.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => {
// Check to see if we have a destroyed chip and need to refocus
this._updateFocusForDestroyedChips();
+ this.stateChanges.next();
+ });
+ this.chipBlurChanges.pipe(takeUntil(this._destroyed)).subscribe(() => {
+ this._blur();
this.stateChanges.next();
});
}
@@ -328,7 +329,6 @@ export class MatChipGrid
/** Associates an HTML input element with this chip grid. */
registerInput(inputElement: MatChipTextControl): void {
this._chipInput = inputElement;
- this._setMdcClass('mdc-chip-set--input', true);
}
/**
@@ -336,7 +336,7 @@ export class MatChipGrid
* @docs-private
*/
onContainerClick(event: MouseEvent) {
- if (!this._originatesFromChip(event) && !this.disabled) {
+ if (!this.disabled && !this._originatesFromChip(event)) {
this.focus();
}
}
@@ -351,7 +351,11 @@ export class MatChipGrid
}
if (this._chips.length > 0) {
- this._keyManager.setFirstCellActive();
+ // MDC sets the tabindex directly on the DOM node when the user is navigating which means
+ // that we may end up with a `0` value from a previous interaction. We reset it manually
+ // here to ensure that the state is correct.
+ this._chips.forEach(chip => chip.primaryAction._updateTabindex(-1));
+ this._chips.first.focus();
} else {
this._focusInput();
}
@@ -413,7 +417,6 @@ export class MatChipGrid
// Timeout is needed to wait for the focus() event trigger on chip input.
setTimeout(() => {
if (!this.focused) {
- this._keyManager.setActiveCell({row: -1, column: -1});
this._propagateChanges();
this._markAsTouched();
}
@@ -425,93 +428,27 @@ export class MatChipGrid
* user to tab out of it. This prevents the grid from capturing focus and redirecting
* it back to the first chip, creating a focus trap, if it user tries to tab away.
*/
- _allowFocusEscape() {
- if (this._chipInput.focused) {
- return;
- }
-
- const previousTabIndex = this.tabIndex;
-
- if (this.tabIndex !== -1) {
- this.tabIndex = -1;
-
- setTimeout(() => {
- this.tabIndex = previousTabIndex;
- this._changeDetectorRef.markForCheck();
- });
+ protected override _allowFocusEscape() {
+ if (!this._chipInput.focused) {
+ super._allowFocusEscape();
}
}
/** Handles custom keyboard events. */
_keydown(event: KeyboardEvent) {
- const target = event.target as HTMLElement;
- const keyCode = event.keyCode;
- const manager = this._keyManager;
-
- if (keyCode === TAB && target.id !== this._chipInput!.id) {
+ if (event.keyCode === TAB && (event.target as HTMLElement).id !== this._chipInput.id) {
this._allowFocusEscape();
- } else if (this._originatesFromEditingChip(event)) {
- // No-op, let the editing chip handle all keyboard events except for Tab.
- } else if (this._originatesFromChip(event)) {
- manager.onKeydown(event);
}
this.stateChanges.next();
}
- /** Unsubscribes from all chip events. */
- protected override _dropSubscriptions() {
- super._dropSubscriptions();
- if (this._chipBlurSubscription) {
- this._chipBlurSubscription.unsubscribe();
- this._chipBlurSubscription = null;
- }
-
- if (this._chipFocusSubscription) {
- this._chipFocusSubscription.unsubscribe();
- this._chipFocusSubscription = null;
- }
- }
-
- /** Subscribes to events on the child chips. */
- protected override _subscribeToChipEvents() {
- super._subscribeToChipEvents();
- this._listenToChipsFocus();
- this._listenToChipsBlur();
- }
-
- /** Initializes the key manager to manage focus. */
- private _initKeyManager() {
- this._keyManager = new GridFocusKeyManager(this._chips)
- .withHomeAndEnd()
- .withDirectionality(this._dir ? this._dir.value : 'ltr');
-
- if (this._dir) {
- this._dir.change
- .pipe(takeUntil(this._destroyed))
- .subscribe(dir => this._keyManager.withDirectionality(dir));
+ _focusLastChip() {
+ if (this._chips.length) {
+ this._chips.last.primaryAction.focus();
}
}
- /** Subscribes to chip focus events. */
- private _listenToChipsFocus(): void {
- this._chipFocusSubscription = this.chipFocusChanges.subscribe((event: MatChipEvent) => {
- let chipIndex: number = this._chips.toArray().indexOf(event.chip as MatChipRow);
-
- if (this._isValidIndex(chipIndex)) {
- this._keyManager.updateActiveCell({row: chipIndex, column: 0});
- }
- });
- }
-
- /** Subscribes to chip blur events. */
- private _listenToChipsBlur(): void {
- this._chipBlurSubscription = this.chipBlurChanges.subscribe(() => {
- this._blur();
- this.stateChanges.next();
- });
- }
-
/** Emits change event to set the model value. */
private _propagateChanges(): void {
const valueToEmit = this._chips.length ? this._chips.toArray().map(chip => chip.value) : [];
@@ -537,10 +474,7 @@ export class MatChipGrid
if (this._lastDestroyedChipIndex != null) {
if (this._chips.length) {
const newChipIndex = Math.min(this._lastDestroyedChipIndex, this._chips.length - 1);
- this._keyManager.setActiveCell({
- row: newChipIndex,
- column: Math.max(this._keyManager.activeColumnIndex, 0),
- });
+ this._chips.toArray()[newChipIndex].focus();
} else {
this.focus();
}
diff --git a/src/material-experimental/mdc-chips/chip-icons.ts b/src/material-experimental/mdc-chips/chip-icons.ts
index 3a21ca09bc3e..8e7b79c3c9e7 100644
--- a/src/material-experimental/mdc-chips/chip-icons.ts
+++ b/src/material-experimental/mdc-chips/chip-icons.ts
@@ -6,15 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {ChangeDetectorRef, Directive, ElementRef, InjectionToken, OnDestroy} from '@angular/core';
-import {
- CanDisable,
- HasTabIndex,
- mixinDisabled,
- mixinTabIndex,
-} from '@angular/material-experimental/mdc-core';
-import {deprecated} from '@material/chips';
-import {Subject} from 'rxjs';
+import {Directive, InjectionToken} from '@angular/core';
+import {MDCChipActionAdapter, MDCChipTrailingActionFoundation} from '@material/chips';
+import {MatChipAction} from './chip-action';
/**
* Injection token that can be used to reference instances of `MatChipAvatar`. It serves as
@@ -30,23 +24,12 @@ export const MAT_CHIP_AVATAR = new InjectionToken('MatChipAvatar'
@Directive({
selector: 'mat-chip-avatar, [matChipAvatar]',
host: {
- 'class': 'mat-mdc-chip-avatar mdc-chip__icon mdc-chip__icon--leading',
+ 'class': 'mat-mdc-chip-avatar mdc-evolution-chip__icon mdc-evolution-chip__icon--primary',
'role': 'img',
},
providers: [{provide: MAT_CHIP_AVATAR, useExisting: MatChipAvatar}],
})
-export class MatChipAvatar {
- constructor(
- private _changeDetectorRef: ChangeDetectorRef,
- private _elementRef: ElementRef,
- ) {}
-
- /** Sets whether the given CSS class should be applied to the leading icon. */
- setClass(cssClass: string, active: boolean) {
- this._elementRef.nativeElement.classList.toggle(cssClass, active);
- this._changeDetectorRef.markForCheck();
- }
-}
+export class MatChipAvatar {}
/**
* Injection token that can be used to reference instances of `MatChipTrailingIcon`. It serves as
@@ -64,60 +47,21 @@ export const MAT_CHIP_TRAILING_ICON = new InjectionToken(
@Directive({
selector: 'mat-chip-trailing-icon, [matChipTrailingIcon]',
host: {
- 'class': 'mat-mdc-chip-trailing-icon mdc-chip__icon mdc-chip__icon--trailing',
- 'tabindex': '-1',
+ 'class':
+ 'mat-mdc-chip-trailing-icon mdc-evolution-chip__icon mdc-evolution-chip__icon--trailing',
'aria-hidden': 'true',
},
providers: [{provide: MAT_CHIP_TRAILING_ICON, useExisting: MatChipTrailingIcon}],
})
-export class MatChipTrailingIcon implements OnDestroy {
- private _foundation: deprecated.MDCChipTrailingActionFoundation;
- private _adapter: deprecated.MDCChipTrailingActionAdapter = {
- focus: () => this._elementRef.nativeElement.focus(),
- getAttribute: (name: string) => this._elementRef.nativeElement.getAttribute(name),
- setAttribute: (name: string, value: string) => {
- this._elementRef.nativeElement.setAttribute(name, value);
- },
- // TODO(crisbeto): there's also a `trigger` parameter that the chip isn't
- // handling yet. Consider passing it along once MDC start using it.
- notifyInteraction: () => {
- // TODO(crisbeto): uncomment this code once we've inverted the
- // dependency on `MatChip`. this._chip._notifyInteraction();
- },
-
- // TODO(crisbeto): there's also a `key` parameter that the chip isn't
- // handling yet. Consider passing it along once MDC start using it.
- notifyNavigation: () => {
- // TODO(crisbeto): uncomment this code once we've inverted the
- // dependency on `MatChip`. this._chip._notifyNavigation();
- },
- };
-
- constructor(
- // TODO(crisbeto): currently the chip needs a reference to the trailing
- // icon for the deprecated `setTrailingActionAttr` method. Until the
- // method is removed, we can't use the chip here, because it causes a
- // circular import. private _chip: MatChip
- public _elementRef: ElementRef,
- ) {
- this._foundation = new deprecated.MDCChipTrailingActionFoundation(this._adapter);
- }
-
- ngOnDestroy() {
- this._foundation.destroy();
- }
-
- focus() {
- this._elementRef.nativeElement.focus();
- }
-
- /** Sets an attribute on the icon. */
- setAttribute(name: string, value: string) {
- this._elementRef.nativeElement.setAttribute(name, value);
- }
+export class MatChipTrailingIcon extends MatChipAction {
+ /**
+ * MDC considers all trailing actions as a remove icon,
+ * but we support non-interactive trailing icons.
+ */
+ override isInteractive = false;
- isNavigable() {
- return this._foundation.isNavigable();
+ protected override _createFoundation(adapter: MDCChipActionAdapter) {
+ return new MDCChipTrailingActionFoundation(adapter);
}
}
@@ -128,18 +72,6 @@ export class MatChipTrailingIcon implements OnDestroy {
*/
export const MAT_CHIP_REMOVE = new InjectionToken('MatChipRemove');
-/**
- * Boilerplate for applying mixins to MatChipRemove.
- * @docs-private
- */
-class MatChipRemoveBase extends MatChipTrailingIcon {
- constructor(elementRef: ElementRef) {
- super(elementRef);
- }
-}
-
-const _MatChipRemoveMixinBase = mixinTabIndex(mixinDisabled(MatChipRemoveBase), 0);
-
/**
* Directive to remove the parent chip when the trailing icon is clicked or
* when the ENTER key is pressed on it.
@@ -155,45 +87,32 @@ const _MatChipRemoveMixinBase = mixinTabIndex(mixinDisabled(MatChipRemoveBase),
*
* ```
*/
+
@Directive({
selector: '[matChipRemove]',
- inputs: ['disabled', 'tabIndex'],
host: {
- 'class': `mat-mdc-chip-remove mat-mdc-chip-trailing-icon mat-mdc-focus-indicator
- mdc-chip__icon mdc-chip__icon--trailing`,
- '[tabIndex]': 'tabIndex',
+ 'class':
+ 'mat-mdc-chip-remove mat-mdc-chip-trailing-icon mat-mdc-focus-indicator ' +
+ 'mdc-evolution-chip__icon mdc-evolution-chip__icon--trailing',
'role': 'button',
- '(click)': '_handleClick($event)',
- '(keydown)': 'interaction.next($event)',
-
- // We need to remove this explicitly, because it gets inherited from MatChipTrailingIcon.
'[attr.aria-hidden]': 'null',
},
providers: [{provide: MAT_CHIP_REMOVE, useExisting: MatChipRemove}],
})
-export class MatChipRemove extends _MatChipRemoveMixinBase implements CanDisable, HasTabIndex {
- /**
- * Emits when the user interacts with the icon.
- * @docs-private
- */
- readonly interaction = new Subject();
-
- constructor(elementRef: ElementRef) {
- super(elementRef);
-
- if (elementRef.nativeElement.nodeName === 'BUTTON') {
- elementRef.nativeElement.setAttribute('type', 'button');
- }
+export class MatChipRemove extends MatChipAction {
+ protected override _createFoundation(adapter: MDCChipActionAdapter) {
+ return new MDCChipTrailingActionFoundation(adapter);
}
- /** Emits a MouseEvent when the user clicks on the remove icon. */
- _handleClick(event: MouseEvent): void {
- this.interaction.next(event);
-
+ override _handleClick(event: MouseEvent) {
+ // Some consumers bind `click` events directly on the chip
+ // which will also pick up clicks on the remove button.
event.stopPropagation();
+ super._handleClick(event);
}
- override focus() {
- this._elementRef.nativeElement.focus();
+ override _handleKeydown(event: KeyboardEvent) {
+ event.stopPropagation();
+ super._handleKeydown(event);
}
}
diff --git a/src/material-experimental/mdc-chips/chip-input.spec.ts b/src/material-experimental/mdc-chips/chip-input.spec.ts
index 90e14f51677f..e83af9f8d9f6 100644
--- a/src/material-experimental/mdc-chips/chip-input.spec.ts
+++ b/src/material-experimental/mdc-chips/chip-input.spec.ts
@@ -3,7 +3,7 @@ import {COMMA, ENTER, TAB} from '@angular/cdk/keycodes';
import {PlatformModule} from '@angular/cdk/platform';
import {dispatchKeyboardEvent} from '../../cdk/testing/private';
import {Component, DebugElement, ViewChild} from '@angular/core';
-import {waitForAsync, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
+import {waitForAsync, ComponentFixture, fakeAsync, TestBed, flush} from '@angular/core/testing';
import {MatFormFieldModule} from '@angular/material-experimental/mdc-form-field';
import {By} from '@angular/platform-browser';
import {NoopAnimationsModule} from '@angular/platform-browser/animations';
@@ -114,14 +114,14 @@ describe('MDC-based MatChipInput', () => {
expect(gridElement.getAttribute('tabindex')).toBe('0');
- dispatchKeyboardEvent(inputNativeElement, 'keydown', TAB);
+ dispatchKeyboardEvent(gridElement, 'keydown', TAB);
fixture.detectChanges();
expect(gridElement.getAttribute('tabindex'))
.withContext('Expected tabIndex to be set to -1 temporarily.')
.toBe('-1');
- tick();
+ flush();
fixture.detectChanges();
expect(gridElement.getAttribute('tabindex'))
@@ -129,26 +129,6 @@ describe('MDC-based MatChipInput', () => {
.toBe('0');
}));
- it('should not allow focus to escape when tabbing backwards', fakeAsync(() => {
- const gridElement: HTMLElement = fixture.nativeElement.querySelector('mat-chip-grid');
-
- expect(gridElement.getAttribute('tabindex')).toBe('0');
-
- dispatchKeyboardEvent(inputNativeElement, 'keydown', TAB, undefined, {shift: true});
- fixture.detectChanges();
-
- expect(gridElement.getAttribute('tabindex'))
- .withContext('Expected tabindex to remain 0')
- .toBe('0');
-
- tick();
- fixture.detectChanges();
-
- expect(gridElement.getAttribute('tabindex'))
- .withContext('Expected tabindex to remain 0')
- .toBe('0');
- }));
-
it('should set input styling classes', () => {
expect(inputNativeElement.classList).toContain('mat-mdc-input-element');
expect(inputNativeElement.classList).toContain('mat-mdc-form-field-input-control');
diff --git a/src/material-experimental/mdc-chips/chip-input.ts b/src/material-experimental/mdc-chips/chip-input.ts
index 128c77f58c06..d69d027e1f72 100644
--- a/src/material-experimental/mdc-chips/chip-input.ts
+++ b/src/material-experimental/mdc-chips/chip-input.ts
@@ -7,7 +7,7 @@
*/
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
-import {BACKSPACE, hasModifierKey, TAB} from '@angular/cdk/keycodes';
+import {BACKSPACE, hasModifierKey} from '@angular/cdk/keycodes';
import {
AfterContentInit,
Directive,
@@ -165,19 +165,11 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha
/** Utility method to make host definition/tests more clear. */
_keydown(event?: KeyboardEvent) {
if (event) {
- // Allow the user's focus to escape when they're tabbing forward. Note that we don't
- // want to do this when going backwards, because focus should go back to the first chip.
- if (event.keyCode === TAB && !hasModifierKey(event, 'shiftKey')) {
- this._chipGrid._allowFocusEscape();
- }
-
// To prevent the user from accidentally deleting chips when pressing BACKSPACE continuously,
// We focus the last chip on backspace only after the user has released the backspace button,
// And the input is empty (see behaviour in _keyup)
if (event.keyCode === BACKSPACE && this._focusLastChipOnBackspace) {
- if (this._chipGrid._chips.length) {
- this._chipGrid._keyManager.setLastCellActive();
- }
+ this._chipGrid._focusLastChip();
event.preventDefault();
return;
} else {
diff --git a/src/material-experimental/mdc-chips/chip-listbox.spec.ts b/src/material-experimental/mdc-chips/chip-listbox.spec.ts
index 914afd3f31b1..105e2e45cbe4 100644
--- a/src/material-experimental/mdc-chips/chip-listbox.spec.ts
+++ b/src/material-experimental/mdc-chips/chip-listbox.spec.ts
@@ -1,28 +1,20 @@
-import {FocusKeyManager} from '@angular/cdk/a11y';
import {Direction, Directionality} from '@angular/cdk/bidi';
import {END, HOME, LEFT_ARROW, RIGHT_ARROW, SPACE, TAB} from '@angular/cdk/keycodes';
-import {
- createKeyboardEvent,
- dispatchEvent,
- dispatchFakeEvent,
- dispatchKeyboardEvent,
- MockNgZone,
-} from '../../cdk/testing/private';
+import {dispatchFakeEvent, dispatchKeyboardEvent, MockNgZone} from '../../cdk/testing/private';
import {
Component,
DebugElement,
NgZone,
- Provider,
QueryList,
Type,
ViewChild,
ViewChildren,
+ EventEmitter,
} from '@angular/core';
-import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing';
+import {ComponentFixture, fakeAsync, flush, TestBed, tick} from '@angular/core/testing';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {By} from '@angular/platform-browser';
-import {Subject} from 'rxjs';
-import {MatChip, MatChipListbox, MatChipOption, MatChipsModule} from './index';
+import {MatChipListbox, MatChipOption, MatChipsModule} from './index';
describe('MDC-based MatChipListbox', () => {
let fixture: ComponentFixture;
@@ -31,14 +23,14 @@ describe('MDC-based MatChipListbox', () => {
let chipListboxInstance: MatChipListbox;
let testComponent: StandardChipListbox;
let chips: QueryList;
- let manager: FocusKeyManager;
let zone: MockNgZone;
- let dirChange: Subject;
+ let directionality: {value: Direction; change: EventEmitter};
+ let primaryActions: NodeListOf;
describe('StandardChipList', () => {
describe('basic behaviors', () => {
beforeEach(() => {
- setupStandardListbox();
+ createComponent(StandardChipListbox);
});
it('should add the `mat-mdc-chip-set` class', () => {
@@ -109,9 +101,6 @@ describe('MDC-based MatChipListbox', () => {
describe('with selected chips', () => {
beforeEach(() => {
fixture = createComponent(SelectedChipListbox);
- fixture.detectChanges();
- chipListboxDebugElement = fixture.debugElement.query(By.directive(MatChipListbox))!;
- chipListboxNativeElement = chipListboxDebugElement.nativeElement;
});
it('should not override chips selected', () => {
@@ -144,26 +133,21 @@ describe('MDC-based MatChipListbox', () => {
describe('focus behaviors', () => {
beforeEach(() => {
- setupStandardListbox();
- manager = chipListboxInstance._keyManager;
+ createComponent(StandardChipListbox);
});
it('should focus the first chip on focus', () => {
chipListboxInstance.focus();
fixture.detectChanges();
- expect(manager.activeItemIndex).toBe(0);
+ expect(document.activeElement).toBe(primaryActions[0]);
});
- it('should watch for chip focus', () => {
- let array = chips.toArray();
- let lastIndex = array.length - 1;
- let lastItem = array[lastIndex];
-
- lastItem.focus();
+ it('should focus the primary action when calling the `focus` method', () => {
+ chips.last.focus();
fixture.detectChanges();
- expect(manager.activeItemIndex).toBe(lastIndex);
+ expect(document.activeElement).toBe(primaryActions[primaryActions.length - 1]);
});
it('should not be able to become focused when disabled', () => {
@@ -193,8 +177,7 @@ describe('MDC-based MatChipListbox', () => {
describe('on chip destroy', () => {
it('should focus the next item', () => {
- let array = chips.toArray();
- let midItem = array[2];
+ const midItem = chips.get(2)!;
// Focus the middle item
midItem.focus();
@@ -203,41 +186,39 @@ describe('MDC-based MatChipListbox', () => {
testComponent.chips.splice(2, 1);
fixture.detectChanges();
- // It focuses the 4th item (now at index 2)
- expect(manager.activeItemIndex).toEqual(2);
+ // It focuses the 4th item
+ expect(document.activeElement).toBe(primaryActions[3]);
});
it('should focus the previous item', () => {
- let array = chips.toArray();
- let lastIndex = array.length - 1;
- let lastItem = array[lastIndex];
-
// Focus the last item
- lastItem.focus();
+ chips.last.focus();
// Destroy the last item
testComponent.chips.pop();
fixture.detectChanges();
+
// It focuses the next-to-last item
- expect(manager.activeItemIndex).toEqual(lastIndex - 1);
+ expect(document.activeElement).toBe(primaryActions[primaryActions.length - 2]);
});
- it('should not focus if chip listbox is not focused', () => {
- let array = chips.toArray();
- let midItem = array[2];
+ it('should not focus if chip listbox is not focused', fakeAsync(() => {
+ const midItem = chips.get(2)!;
// Focus and blur the middle item
midItem.focus();
- midItem._blur();
+ (document.activeElement as HTMLElement).blur();
+ tick();
zone.simulateZoneExit();
// Destroy the middle item
testComponent.chips.splice(2, 1);
fixture.detectChanges();
+ tick();
// Should not have focus
- expect(chipListboxInstance._keyManager.activeItemIndex).toEqual(-1);
- });
+ expect(chipListboxNativeElement.contains(document.activeElement)).toBe(false);
+ }));
it('should focus the listbox if the last focused item is removed', () => {
testComponent.chips = [0];
@@ -257,155 +238,120 @@ describe('MDC-based MatChipListbox', () => {
describe('keyboard behavior', () => {
describe('LTR (default)', () => {
beforeEach(() => {
- setupStandardListbox();
- manager = chipListboxInstance._keyManager;
+ createComponent(StandardChipListbox);
});
it('should focus previous item when press LEFT ARROW', () => {
- let nativeChips = chipListboxNativeElement.querySelectorAll('mat-chip-option');
- let lastNativeChip = nativeChips[nativeChips.length - 1] as HTMLElement;
-
- let array = chips.toArray();
- let lastIndex = array.length - 1;
- let lastItem = array[lastIndex];
+ const lastIndex = primaryActions.length - 1;
// Focus the last item in the array
- lastItem.focus();
- expect(manager.activeItemIndex).toEqual(lastIndex);
+ chips.last.focus();
+ expect(document.activeElement).toBe(primaryActions[lastIndex]);
// Press the LEFT arrow
- dispatchKeyboardEvent(lastNativeChip, 'keydown', LEFT_ARROW);
- chipListboxInstance._blur(); // Simulate focus leaving the listbox and going to the chip.
+ dispatchKeyboardEvent(primaryActions[lastIndex], 'keydown', LEFT_ARROW);
fixture.detectChanges();
// It focuses the next-to-last item
- expect(manager.activeItemIndex).toEqual(lastIndex - 1);
+ expect(document.activeElement).toBe(primaryActions[lastIndex - 1]);
});
it('should focus next item when press RIGHT ARROW', () => {
- let nativeChips = chipListboxNativeElement.querySelectorAll('mat-chip-option');
- let firstNativeChip = nativeChips[0] as HTMLElement;
-
- let array = chips.toArray();
- let firstItem = array[0];
-
// Focus the last item in the array
- firstItem.focus();
- expect(manager.activeItemIndex).toEqual(0);
+ chips.first.focus();
+ expect(document.activeElement).toBe(primaryActions[0]);
// Press the RIGHT arrow
- dispatchKeyboardEvent(firstNativeChip, 'keydown', RIGHT_ARROW);
- chipListboxInstance._blur(); // Simulate focus leaving the listbox and going to the chip.
+ dispatchKeyboardEvent(primaryActions[0], 'keydown', RIGHT_ARROW);
fixture.detectChanges();
// It focuses the next-to-last item
- expect(manager.activeItemIndex).toEqual(1);
+ expect(document.activeElement).toBe(primaryActions[1]);
});
it('should not handle arrow key events from non-chip elements', () => {
- const initialActiveIndex = manager.activeItemIndex;
+ const previousActiveElement = document.activeElement;
dispatchKeyboardEvent(chipListboxNativeElement, 'keydown', RIGHT_ARROW);
fixture.detectChanges();
- expect(manager.activeItemIndex)
+ expect(document.activeElement)
.withContext('Expected focused item not to have changed.')
- .toBe(initialActiveIndex);
+ .toBe(previousActiveElement);
});
it('should focus the first item when pressing HOME', () => {
- const nativeChips = chipListboxNativeElement.querySelectorAll('mat-chip-option');
- const lastNativeChip = nativeChips[nativeChips.length - 1] as HTMLElement;
- const HOME_EVENT = createKeyboardEvent('keydown', HOME);
- const array = chips.toArray();
- const lastItem = array[array.length - 1];
-
- lastItem.focus();
- expect(manager.activeItemIndex).toBe(array.length - 1);
+ const lastAction = primaryActions[primaryActions.length - 1];
+ chips.last.focus();
+ expect(document.activeElement).toBe(lastAction);
- dispatchEvent(lastNativeChip, HOME_EVENT);
+ const event = dispatchKeyboardEvent(lastAction, 'keydown', HOME);
fixture.detectChanges();
- expect(manager.activeItemIndex).toBe(0);
- expect(HOME_EVENT.defaultPrevented).toBe(true);
+ expect(document.activeElement).toBe(primaryActions[0]);
+ expect(event.defaultPrevented).toBe(true);
});
it('should focus the last item when pressing END', () => {
- const nativeChips = chipListboxNativeElement.querySelectorAll('mat-chip-option');
- const END_EVENT = createKeyboardEvent('keydown', END);
-
- expect(manager.activeItemIndex).toBe(-1);
+ chips.first.focus();
+ expect(document.activeElement).toBe(primaryActions[0]);
- dispatchEvent(nativeChips[0], END_EVENT);
+ const event = dispatchKeyboardEvent(primaryActions[0], 'keydown', END);
fixture.detectChanges();
- expect(manager.activeItemIndex).toBe(chips.length - 1);
- expect(END_EVENT.defaultPrevented).toBe(true);
+ expect(document.activeElement).toBe(primaryActions[primaryActions.length - 1]);
+ expect(event.defaultPrevented).toBe(true);
});
});
describe('RTL', () => {
beforeEach(() => {
- setupStandardListbox('rtl');
- manager = chipListboxInstance._keyManager;
+ createComponent(StandardChipListbox, 'rtl');
});
it('should focus previous item when press RIGHT ARROW', () => {
- let nativeChips = chipListboxNativeElement.querySelectorAll('mat-chip-option');
- let lastNativeChip = nativeChips[nativeChips.length - 1] as HTMLElement;
-
- let array = chips.toArray();
- let lastIndex = array.length - 1;
- let lastItem = array[lastIndex];
+ const lastIndex = primaryActions.length - 1;
// Focus the last item in the array
- lastItem.focus();
- expect(manager.activeItemIndex).toEqual(lastIndex);
+ chips.last.focus();
+ expect(document.activeElement).toBe(primaryActions[lastIndex]);
// Press the RIGHT arrow
- dispatchKeyboardEvent(lastNativeChip, 'keydown', RIGHT_ARROW);
- chipListboxInstance._blur(); // Simulate focus leaving the listbox and going to the chip.
+ dispatchKeyboardEvent(primaryActions[lastIndex], 'keydown', RIGHT_ARROW);
fixture.detectChanges();
// It focuses the next-to-last item
- expect(manager.activeItemIndex).toEqual(lastIndex - 1);
+ expect(document.activeElement).toBe(primaryActions[lastIndex - 1]);
});
it('should focus next item when press LEFT ARROW', () => {
- let nativeChips = chipListboxNativeElement.querySelectorAll('mat-chip-option');
- let firstNativeChip = nativeChips[0] as HTMLElement;
-
- let array = chips.toArray();
- let firstItem = array[0];
-
// Focus the last item in the array
- firstItem.focus();
- expect(manager.activeItemIndex).toEqual(0);
+ chips.first.focus();
+ expect(document.activeElement).toBe(primaryActions[0]);
// Press the LEFT arrow
- dispatchKeyboardEvent(firstNativeChip, 'keydown', LEFT_ARROW);
- chipListboxInstance._blur(); // Simulate focus leaving the listbox and going to the chip.
+ dispatchKeyboardEvent(primaryActions[0], 'keydown', LEFT_ARROW);
fixture.detectChanges();
// It focuses the next-to-last item
- expect(manager.activeItemIndex).toEqual(1);
+ expect(document.activeElement).toBe(primaryActions[1]);
});
it('should allow focus to escape when tabbing away', fakeAsync(() => {
- chipListboxInstance._keyManager.onKeydown(createKeyboardEvent('keydown', TAB));
+ dispatchKeyboardEvent(chipListboxNativeElement, 'keydown', TAB);
expect(chipListboxInstance.tabIndex)
.withContext('Expected tabIndex to be set to -1 temporarily.')
.toBe(-1);
- tick();
+ flush();
expect(chipListboxInstance.tabIndex)
.withContext('Expected tabIndex to be reset back to 0')
.toBe(0);
}));
- it(`should use user defined tabIndex`, fakeAsync(() => {
+ it('should use user defined tabIndex', fakeAsync(() => {
chipListboxInstance.tabIndex = 4;
fixture.detectChanges();
@@ -414,13 +360,13 @@ describe('MDC-based MatChipListbox', () => {
.withContext('Expected tabIndex to be set to user defined value 4.')
.toBe(4);
- chipListboxInstance._keyManager.onKeydown(createKeyboardEvent('keydown', TAB));
+ dispatchKeyboardEvent(chipListboxNativeElement, 'keydown', TAB);
expect(chipListboxInstance.tabIndex)
.withContext('Expected tabIndex to be set to -1 temporarily.')
.toBe(-1);
- tick();
+ flush();
expect(chipListboxInstance.tabIndex)
.withContext('Expected tabIndex to be reset back to 4')
@@ -429,56 +375,35 @@ describe('MDC-based MatChipListbox', () => {
});
it('should account for the direction changing', () => {
- setupStandardListbox();
- manager = chipListboxInstance._keyManager;
-
- let nativeChips = chipListboxNativeElement.querySelectorAll('mat-chip-option');
- let firstNativeChip = nativeChips[0] as HTMLElement;
-
- let array = chips.toArray();
- let firstItem = array[0];
+ createComponent(StandardChipListbox);
- firstItem.focus();
- expect(manager.activeItemIndex).toBe(0);
+ chips.first.focus();
+ expect(document.activeElement).toBe(primaryActions[0]);
- dispatchKeyboardEvent(firstNativeChip, 'keydown', RIGHT_ARROW);
- chipListboxInstance._blur();
+ dispatchKeyboardEvent(primaryActions[0], 'keydown', RIGHT_ARROW);
fixture.detectChanges();
- expect(manager.activeItemIndex).toBe(1);
+ expect(document.activeElement).toBe(primaryActions[1]);
- dirChange.next('rtl');
+ directionality.value = 'rtl';
fixture.detectChanges();
- dispatchKeyboardEvent(firstNativeChip, 'keydown', RIGHT_ARROW);
- chipListboxInstance._blur();
+ dispatchKeyboardEvent(primaryActions[1], 'keydown', RIGHT_ARROW);
fixture.detectChanges();
- expect(manager.activeItemIndex).toBe(0);
+ expect(document.activeElement).toBe(primaryActions[0]);
});
});
describe('selection logic', () => {
- let nativeChips: HTMLElement[];
-
beforeEach(() => {
fixture = createComponent(BasicChipListbox);
- fixture.detectChanges();
-
- nativeChips = fixture.debugElement
- .queryAll(By.css('mat-chip-option'))
- .map(chip => chip.nativeElement);
-
- chipListboxDebugElement = fixture.debugElement.query(By.directive(MatChipListbox))!;
- chipListboxInstance = chipListboxDebugElement.componentInstance;
- chips = chipListboxInstance._chips;
});
it('should remove selection if chip has been removed', fakeAsync(() => {
const instanceChips = fixture.componentInstance.chips;
const chipListbox = fixture.componentInstance.chipListbox;
- const firstChip = nativeChips[0];
- dispatchKeyboardEvent(firstChip, 'keydown', SPACE);
+ dispatchKeyboardEvent(primaryActions[0], 'keydown', SPACE);
fixture.detectChanges();
expect(instanceChips.first.selected)
@@ -501,11 +426,11 @@ describe('MDC-based MatChipListbox', () => {
fixture.componentInstance.foods.push({viewValue: 'Potatoes', value: 'potatoes-8'});
fixture.detectChanges();
- nativeChips = fixture.debugElement
- .queryAll(By.css('mat-chip-option'))
- .map(chip => chip.nativeElement);
- const lastChip = nativeChips[8];
- dispatchKeyboardEvent(lastChip, 'keydown', SPACE);
+ primaryActions = chipListboxNativeElement.querySelectorAll(
+ '.mdc-evolution-chip__action--primary',
+ );
+
+ dispatchKeyboardEvent(primaryActions[8], 'keydown', SPACE);
fixture.detectChanges();
expect(fixture.componentInstance.chipListbox.value)
@@ -518,8 +443,7 @@ describe('MDC-based MatChipListbox', () => {
it('should not select disabled chips', () => {
const array = chips.toArray();
- const disabledChip = nativeChips[2];
- dispatchKeyboardEvent(disabledChip, 'keydown', SPACE);
+ dispatchKeyboardEvent(primaryActions[2], 'keydown', SPACE);
fixture.detectChanges();
expect(fixture.componentInstance.chipListbox.value)
@@ -533,17 +457,9 @@ describe('MDC-based MatChipListbox', () => {
});
describe('chip list with chip input', () => {
- let nativeChips: HTMLElement[];
-
describe('single selection', () => {
beforeEach(() => {
fixture = createComponent(BasicChipListbox);
- fixture.detectChanges();
-
- nativeChips = fixture.debugElement
- .queryAll(By.css('mat-chip-option'))
- .map(chip => chip.nativeElement);
- chips = fixture.componentInstance.chips;
});
it('should take an initial view value with reactive forms', fakeAsync(() => {
@@ -554,8 +470,9 @@ describe('MDC-based MatChipListbox', () => {
expect(array[1].selected).withContext('Expect pizza-1 chip to be selected').toBeTruthy();
- dispatchKeyboardEvent(nativeChips[1], 'keydown', SPACE);
+ dispatchKeyboardEvent(primaryActions[1], 'keydown', SPACE);
fixture.detectChanges();
+ flush();
expect(array[1].selected)
.withContext('Expect chip to be not selected after toggle selected')
@@ -581,10 +498,9 @@ describe('MDC-based MatChipListbox', () => {
.withContext(`Expected the control's value to be empty initially.`)
.toEqual(null);
- dispatchKeyboardEvent(nativeChips[0], 'keydown', SPACE);
+ dispatchKeyboardEvent(primaryActions[0], 'keydown', SPACE);
fixture.detectChanges();
-
- tick();
+ flush();
expect(fixture.componentInstance.control.value)
.withContext(`Expected control's value to be set to the new option.`)
@@ -662,7 +578,7 @@ describe('MDC-based MatChipListbox', () => {
.withContext(`Expected control to start out pristine.`)
.toEqual(false);
- dispatchKeyboardEvent(nativeChips[1], 'keydown', SPACE);
+ dispatchKeyboardEvent(primaryActions[1], 'keydown', SPACE);
fixture.detectChanges();
expect(fixture.componentInstance.control.dirty)
@@ -687,8 +603,6 @@ describe('MDC-based MatChipListbox', () => {
TestBed.resetTestingModule();
const falsyFixture = createComponent(FalsyValueChipListbox);
- falsyFixture.detectChanges();
-
falsyFixture.componentInstance.control.setValue([0]);
falsyFixture.detectChanges();
falsyFixture.detectChanges();
@@ -713,11 +627,6 @@ describe('MDC-based MatChipListbox', () => {
describe('multiple selection', () => {
beforeEach(() => {
fixture = createComponent(MultiSelectionChipListbox);
- fixture.detectChanges();
-
- nativeChips = fixture.debugElement
- .queryAll(By.css('mat-chip-option'))
- .map(chip => chip.nativeElement);
chips = fixture.componentInstance.chips;
});
@@ -729,7 +638,7 @@ describe('MDC-based MatChipListbox', () => {
expect(array[1].selected).withContext('Expect pizza-1 chip to be selected').toBeTruthy();
- dispatchKeyboardEvent(nativeChips[1], 'keydown', SPACE);
+ dispatchKeyboardEvent(primaryActions[1], 'keydown', SPACE);
fixture.detectChanges();
expect(array[1].selected)
@@ -756,7 +665,7 @@ describe('MDC-based MatChipListbox', () => {
.withContext(`Expected the control's value to be empty initially.`)
.toEqual(null);
- dispatchKeyboardEvent(nativeChips[0], 'keydown', SPACE);
+ dispatchKeyboardEvent(primaryActions[0], 'keydown', SPACE);
fixture.detectChanges();
expect(fixture.componentInstance.control.value)
@@ -800,27 +709,25 @@ describe('MDC-based MatChipListbox', () => {
});
});
- function createComponent(component: Type, providers: Provider[] = []): ComponentFixture {
+ function createComponent(
+ component: Type,
+ direction: Direction = 'ltr',
+ ): ComponentFixture {
+ directionality = {
+ value: direction,
+ change: new EventEmitter(),
+ };
+
TestBed.configureTestingModule({
imports: [FormsModule, ReactiveFormsModule, MatChipsModule],
declarations: [component],
- providers: [{provide: NgZone, useFactory: () => (zone = new MockNgZone())}, ...providers],
+ providers: [
+ {provide: NgZone, useFactory: () => (zone = new MockNgZone())},
+ {provide: Directionality, useValue: directionality},
+ ],
}).compileComponents();
- return TestBed.createComponent(component);
- }
-
- function setupStandardListbox(direction: Direction = 'ltr') {
- dirChange = new Subject();
- fixture = createComponent(StandardChipListbox, [
- {
- provide: Directionality,
- useFactory: () => ({
- value: direction.toLowerCase(),
- change: dirChange,
- }),
- },
- ]);
+ fixture = TestBed.createComponent(component);
fixture.detectChanges();
chipListboxDebugElement = fixture.debugElement.query(By.directive(MatChipListbox))!;
@@ -828,6 +735,11 @@ describe('MDC-based MatChipListbox', () => {
chipListboxInstance = chipListboxDebugElement.componentInstance;
testComponent = fixture.debugElement.componentInstance;
chips = chipListboxInstance._chips;
+ primaryActions = chipListboxNativeElement.querySelectorAll(
+ '.mdc-evolution-chip__action--primary',
+ );
+
+ return fixture;
}
});
diff --git a/src/material-experimental/mdc-chips/chip-listbox.ts b/src/material-experimental/mdc-chips/chip-listbox.ts
index 9b9dad57dff1..c3ee1d4ae724 100644
--- a/src/material-experimental/mdc-chips/chip-listbox.ts
+++ b/src/material-experimental/mdc-chips/chip-listbox.ts
@@ -6,27 +6,24 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {FocusKeyManager} from '@angular/cdk/a11y';
-import {Directionality} from '@angular/cdk/bidi';
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
+import {TAB} from '@angular/cdk/keycodes';
import {
AfterContentInit,
ChangeDetectionStrategy,
- ChangeDetectorRef,
Component,
ContentChildren,
- ElementRef,
EventEmitter,
forwardRef,
Input,
- Optional,
+ OnDestroy,
Output,
QueryList,
ViewEncapsulation,
} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
-import {deprecated} from '@material/chips';
-import {merge, Observable, Subscription} from 'rxjs';
+import {MDCChipActionType} from '@material/chips';
+import {Observable} from 'rxjs';
import {startWith, takeUntil} from 'rxjs/operators';
import {MatChip, MatChipEvent} from './chip';
import {MatChipOption, MatChipSelectionChange} from './chip-option';
@@ -59,11 +56,15 @@ export const MAT_CHIP_LISTBOX_CONTROL_VALUE_ACCESSOR: any = {
*/
@Component({
selector: 'mat-chip-listbox',
- template: ' ',
- styleUrls: ['chips.css'],
+ template: `
+
+
+
+ `,
+ styleUrls: ['chip-set.css'],
inputs: ['tabIndex'],
host: {
- 'class': 'mat-mdc-chip-set mat-mdc-chip-listbox mdc-chip-set',
+ 'class': 'mdc-evolution-chip-set mat-mdc-chip-listbox',
'[attr.role]': 'role',
'[tabIndex]': 'empty ? -1 : tabIndex',
// TODO: replace this binding with use of AriaDescriber
@@ -77,25 +78,15 @@ export const MAT_CHIP_LISTBOX_CONTROL_VALUE_ACCESSOR: any = {
'(focus)': 'focus()',
'(blur)': '_blur()',
'(keydown)': '_keydown($event)',
- '[id]': '_uid',
},
providers: [MAT_CHIP_LISTBOX_CONTROL_VALUE_ACCESSOR],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class MatChipListbox extends MatChipSet implements AfterContentInit, ControlValueAccessor {
- /** Subscription to selection changes in the chips. */
- private _chipSelectionSubscription: Subscription | null;
-
- /** Subscription to blur changes in the chips. */
- private _chipBlurSubscription: Subscription | null;
-
- /** Subscription to focus changes in the chips. */
- private _chipFocusSubscription: Subscription | null;
-
- /** The FocusKeyManager which handles focus. */
- _keyManager: FocusKeyManager;
-
+export class MatChipListbox
+ extends MatChipSet
+ implements AfterContentInit, OnDestroy, ControlValueAccessor
+{
/**
* Function when touched. Set as part of ControlValueAccessor implementation.
* @docs-private
@@ -109,6 +100,7 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont
_onChange: (value: any) => void = () => {};
/** The ARIA role applied to the chip listbox. */
+ // TODO: MDC uses `grid` here
override get role(): string | null {
return this.empty ? null : 'listbox';
}
@@ -120,7 +112,6 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont
}
set multiple(value: BooleanInput) {
this._multiple = coerceBooleanProperty(value);
- this._updateMdcSelectionClasses();
this._syncListboxProperties();
}
private _multiple: boolean = false;
@@ -146,7 +137,6 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont
}
set selectable(value: BooleanInput) {
this._selectable = coerceBooleanProperty(value);
- this._updateMdcSelectionClasses();
this._syncListboxProperties();
}
protected _selectable: boolean = true;
@@ -178,17 +168,17 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont
/** Combined stream of all of the child chips' selection change events. */
get chipSelectionChanges(): Observable {
- return merge(...this._chips.map(chip => chip.selectionChange));
+ return this._getChipStream(chip => chip.selectionChange);
}
/** Combined stream of all of the child chips' focus events. */
get chipFocusChanges(): Observable {
- return merge(...this._chips.map(chip => chip._onFocus));
+ return this._getChipStream(chip => chip._onFocus);
}
/** Combined stream of all of the child chips' blur events. */
get chipBlurChanges(): Observable {
- return merge(...this._chips.map(chip => chip._onBlur));
+ return this._getChipStream(chip => chip._onBlur);
}
/** The value of the listbox, which is the combined value of the selected chips. */
@@ -213,23 +203,8 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont
})
override _chips: QueryList;
- constructor(
- elementRef: ElementRef,
- changeDetectorRef: ChangeDetectorRef,
- @Optional() _dir: Directionality,
- ) {
- super(elementRef, changeDetectorRef, _dir);
- this._chipSetAdapter.selectChipAtIndex = (index: number, selected: boolean) => {
- this._setSelected(index, selected);
- };
- // Reinitialize the foundation with our overridden adapter
- this._chipSetFoundation = new deprecated.MDCChipSetFoundation(this._chipSetAdapter);
- this._updateMdcSelectionClasses();
- }
-
override ngAfterContentInit() {
super.ngAfterContentInit();
- this._initKeyManager();
this._chips.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => {
// Update listbox selectable/multiple properties on chips
@@ -241,6 +216,13 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont
// Check to see if we have a destroyed chip and need to refocus
this._updateFocusForDestroyedChips();
});
+
+ this.chipBlurChanges.pipe(takeUntil(this._destroyed)).subscribe(() => this._blur());
+ this.chipSelectionChanges.pipe(takeUntil(this._destroyed)).subscribe(event => {
+ if (event.isUserInput) {
+ this._propagateChanges();
+ }
+ });
}
/**
@@ -255,10 +237,13 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont
const firstSelectedChip = this._getFirstSelectedChip();
if (firstSelectedChip) {
- const firstSelectedChipIndex = this._chips.toArray().indexOf(firstSelectedChip);
- this._keyManager.setActiveItem(firstSelectedChipIndex);
+ firstSelectedChip.focus();
} else if (this._chips.length > 0) {
- this._keyManager.setFirstItemActive();
+ // MDC sets the tabindex directly on the DOM node when the user is navigating which means
+ // that we may end up with a `0` value from a previous interaction. We reset it manually
+ // here to ensure that the state is correct.
+ this._chips.forEach(chip => chip.primaryAction._updateTabindex(-1));
+ this._chips.first.primaryAction.focus();
}
}
@@ -303,69 +288,26 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont
if (Array.isArray(value)) {
value.forEach(currentValue => this._selectValue(currentValue, isUserInput));
} else {
- const correspondingChip = this._selectValue(value, isUserInput);
-
- // Shift focus to the active item. Note that we shouldn't do this in multiple
- // mode, because we don't know what chip the user interacted with last.
- if (correspondingChip) {
- if (isUserInput) {
- this._keyManager.setActiveItem(correspondingChip);
- }
- }
- }
- }
-
- /** Selects or deselects a chip by id. */
- _setSelected(index: number, selected: boolean) {
- const chip = this._chips.toArray()[index];
- if (chip && chip.selected != selected) {
- chip.toggleSelected(true);
+ this._selectValue(value, isUserInput);
}
}
/** When blurred, marks the field as touched when focus moved outside the chip listbox. */
_blur() {
- if (this.disabled) {
- return;
- }
-
- if (!this.focused) {
- this._keyManager.setActiveItem(-1);
- }
-
- // Wait to see if focus moves to an indivdual chip.
- setTimeout(() => {
- if (!this.focused) {
- this._propagateChanges();
- this._markAsTouched();
- }
- });
- }
-
- /**
- * Removes the `tabindex` from the chip listbox and resets it back afterwards, allowing the
- * user to tab out of it. This prevents the listbox from capturing focus and redirecting
- * it back to the first chip, creating a focus trap, if it user tries to tab away.
- */
- _allowFocusEscape() {
- const previousTabIndex = this.tabIndex;
-
- if (this.tabIndex !== -1) {
- this.tabIndex = -1;
-
+ if (!this.disabled) {
+ // Wait to see if focus moves to an individual chip.
setTimeout(() => {
- this.tabIndex = previousTabIndex;
- this._changeDetectorRef.markForCheck();
+ if (!this.focused) {
+ this._propagateChanges();
+ this._markAsTouched();
+ }
});
}
}
- /**
- * Handles custom keyboard shortcuts, and passes other keyboard events to the keyboard manager.
- */
_keydown(event: KeyboardEvent) {
- if (this._originatesFromChip(event)) {
- this._keyManager.onKeydown(event);
+ if (event.keyCode === TAB) {
+ super._allowFocusEscape();
}
}
@@ -376,13 +318,13 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont
}
/** Emits change event to set the model value. */
- private _propagateChanges(fallbackValue?: any): void {
+ private _propagateChanges(): void {
let valueToEmit: any = null;
if (Array.isArray(this.selected)) {
valueToEmit = this.selected.map(chip => chip.value);
} else {
- valueToEmit = this.selected ? this.selected.value : fallbackValue;
+ valueToEmit = this.selected ? this.selected.value : undefined;
}
this._value = valueToEmit;
this.change.emit(new MatChipListboxChange(this, valueToEmit));
@@ -397,9 +339,9 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont
setTimeout(() => {
// Defer setting the value in order to avoid the "Expression
// has changed after it was checked" errors from Angular.
- this._chips.forEach(chip => {
+ this._chips.forEach((chip, index) => {
if (chip.selected) {
- this._chipSetFoundation.select(chip.id);
+ this._chipSetFoundation.setChipSelected(index, MDCChipActionType.PRIMARY, true);
}
});
});
@@ -448,31 +390,6 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont
}
}
- /** Sets the mdc classes for single vs multi selection. */
- private _updateMdcSelectionClasses() {
- this._setMdcClass('mdc-chip-set--filter', this.selectable && this.multiple);
- this._setMdcClass('mdc-chip-set--choice', this.selectable && !this.multiple);
- }
-
- /** Initializes the key manager to manage focus. */
- private _initKeyManager() {
- this._keyManager = new FocusKeyManager(this._chips)
- .withWrap()
- .withVerticalOrientation()
- .withHomeAndEnd()
- .withHorizontalOrientation(this._dir ? this._dir.value : 'ltr');
-
- if (this._dir) {
- this._dir.change
- .pipe(takeUntil(this._destroyed))
- .subscribe(dir => this._keyManager.withHorizontalOrientation(dir));
- }
-
- this._keyManager.tabOut.pipe(takeUntil(this._destroyed)).subscribe(() => {
- this._allowFocusEscape();
- });
- }
-
/** Returns the first selected chip in this listbox, or undefined if no chips are selected. */
private _getFirstSelectedChip(): MatChipOption | undefined {
if (Array.isArray(this.selected)) {
@@ -482,67 +399,6 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont
}
}
- /** Unsubscribes from all chip events. */
- protected override _dropSubscriptions() {
- super._dropSubscriptions();
- if (this._chipSelectionSubscription) {
- this._chipSelectionSubscription.unsubscribe();
- this._chipSelectionSubscription = null;
- }
-
- if (this._chipBlurSubscription) {
- this._chipBlurSubscription.unsubscribe();
- this._chipBlurSubscription = null;
- }
-
- if (this._chipFocusSubscription) {
- this._chipFocusSubscription.unsubscribe();
- this._chipFocusSubscription = null;
- }
- }
-
- /** Subscribes to events on the child chips. */
- protected override _subscribeToChipEvents() {
- super._subscribeToChipEvents();
- this._listenToChipsSelection();
- this._listenToChipsFocus();
- this._listenToChipsBlur();
- }
-
- /** Subscribes to chip focus events. */
- private _listenToChipsFocus(): void {
- this._chipFocusSubscription = this.chipFocusChanges.subscribe((event: MatChipEvent) => {
- let chipIndex: number = this._chips.toArray().indexOf(event.chip as MatChipOption);
-
- if (this._isValidIndex(chipIndex)) {
- this._keyManager.updateActiveItem(chipIndex);
- }
- });
- }
-
- /** Subscribes to chip blur events. */
- private _listenToChipsBlur(): void {
- this._chipBlurSubscription = this.chipBlurChanges.subscribe(() => {
- this._blur();
- });
- }
-
- /** Subscribes to selection changes in the option chips. */
- private _listenToChipsSelection(): void {
- this._chipSelectionSubscription = this.chipSelectionChanges.subscribe(
- (chipSelectionChange: MatChipSelectionChange) => {
- this._chipSetFoundation.handleChipSelection({
- chipId: chipSelectionChange.source.id,
- selected: chipSelectionChange.selected,
- shouldIgnore: false,
- });
- if (chipSelectionChange.isUserInput) {
- this._propagateChanges();
- }
- },
- );
- }
-
/**
* If the amount of chips changed, we need to update the
* key manager state and focus the next closest chip.
@@ -552,7 +408,7 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont
if (this._lastDestroyedChipIndex != null) {
if (this._chips.length) {
const newChipIndex = Math.min(this._lastDestroyedChipIndex, this._chips.length - 1);
- this._keyManager.setActiveItem(newChipIndex);
+ this._chips.toArray()[newChipIndex].focus();
} else {
this.focus();
}
diff --git a/src/material-experimental/mdc-chips/chip-option.html b/src/material-experimental/mdc-chips/chip-option.html
index c990c6e891ca..262203c58f69 100644
--- a/src/material-experimental/mdc-chips/chip-option.html
+++ b/src/material-experimental/mdc-chips/chip-option.html
@@ -1,16 +1,37 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
diff --git a/src/material-experimental/mdc-chips/chip-option.spec.ts b/src/material-experimental/mdc-chips/chip-option.spec.ts
index 6a66bc9703f2..626f8f58494e 100644
--- a/src/material-experimental/mdc-chips/chip-option.spec.ts
+++ b/src/material-experimental/mdc-chips/chip-option.spec.ts
@@ -1,6 +1,5 @@
import {Directionality} from '@angular/cdk/bidi';
-import {SPACE} from '@angular/cdk/keycodes';
-import {createKeyboardEvent, dispatchFakeEvent} from '../../cdk/testing/private';
+import {dispatchFakeEvent, dispatchKeyboardEvent} from '@angular/cdk/testing/private';
import {Component, DebugElement, ViewChild} from '@angular/core';
import {waitForAsync, ComponentFixture, fakeAsync, flush, TestBed} from '@angular/core/testing';
import {
@@ -8,7 +7,6 @@ import {
RippleGlobalOptions,
} from '@angular/material-experimental/mdc-core';
import {By} from '@angular/platform-browser';
-import {deprecated} from '@material/chips';
import {Subject} from 'rxjs';
import {
MatChipEvent,
@@ -17,11 +15,13 @@ import {
MatChipSelectionChange,
MatChipsModule,
} from './index';
+import {SPACE} from '@angular/cdk/keycodes';
describe('MDC-based Option Chips', () => {
let fixture: ComponentFixture;
let chipDebugElement: DebugElement;
let chipNativeElement: HTMLElement;
+ let primaryAction: HTMLElement;
let chipInstance: MatChipOption;
let globalRippleOptions: RippleGlobalOptions;
let dir = 'ltr';
@@ -58,6 +58,7 @@ describe('MDC-based Option Chips', () => {
chipDebugElement = fixture.debugElement.query(By.directive(MatChipOption))!;
chipNativeElement = chipDebugElement.nativeElement;
chipInstance = chipDebugElement.injector.get(MatChipOption);
+ primaryAction = chipNativeElement.querySelector('.mdc-evolution-chip__action--primary')!;
testComponent = fixture.debugElement.componentInstance;
});
@@ -72,8 +73,8 @@ describe('MDC-based Option Chips', () => {
counter++;
});
- chipNativeElement.focus();
- chipNativeElement.focus();
+ primaryAction.focus();
+ primaryAction.focus();
fixture.detectChanges();
expect(counter).toBe(1);
@@ -121,16 +122,6 @@ describe('MDC-based Option Chips', () => {
expect(event.defaultPrevented).toBe(false);
});
- it('should prevent the default click action when the chip is disabled', () => {
- chipInstance.disabled = true;
- fixture.detectChanges();
-
- const event = dispatchFakeEvent(chipNativeElement, 'click');
- fixture.detectChanges();
-
- expect(event.defaultPrevented).toBe(true);
- });
-
it('should not dispatch `selectionChange` event when deselecting a non-selected chip', () => {
chipInstance.deselect();
@@ -204,7 +195,6 @@ describe('MDC-based Option Chips', () => {
});
it('should selects/deselects the currently focused chip on SPACE', () => {
- const SPACE_EVENT = createKeyboardEvent('keydown', SPACE);
const CHIP_SELECTED_EVENT: MatChipSelectionChange = {
source: chipInstance,
isUserInput: true,
@@ -220,7 +210,7 @@ describe('MDC-based Option Chips', () => {
spyOn(testComponent, 'chipSelectionChange');
// Use the spacebar to select the chip
- chipInstance._keydown(SPACE_EVENT);
+ dispatchKeyboardEvent(primaryAction, 'keydown', SPACE);
fixture.detectChanges();
expect(chipInstance.selected).toBeTruthy();
@@ -228,7 +218,7 @@ describe('MDC-based Option Chips', () => {
expect(testComponent.chipSelectionChange).toHaveBeenCalledWith(CHIP_SELECTED_EVENT);
// Use the spacebar to deselect the chip
- chipInstance._keydown(SPACE_EVENT);
+ dispatchKeyboardEvent(primaryAction, 'keydown', SPACE);
fixture.detectChanges();
expect(chipInstance.selected).toBeFalsy();
@@ -237,24 +227,24 @@ describe('MDC-based Option Chips', () => {
});
it('should have correct aria-selected in single selection mode', () => {
- expect(chipNativeElement.hasAttribute('aria-selected')).toBe(false);
+ expect(primaryAction.hasAttribute('aria-selected')).toBe(false);
testComponent.selected = true;
fixture.detectChanges();
- expect(chipNativeElement.getAttribute('aria-selected')).toBe('true');
+ expect(primaryAction.getAttribute('aria-selected')).toBe('true');
});
it('should have the correct aria-selected in multi-selection mode', fakeAsync(() => {
testComponent.chipList.multiple = true;
flush();
fixture.detectChanges();
- expect(chipNativeElement.getAttribute('aria-selected')).toBe('false');
+ expect(primaryAction.getAttribute('aria-selected')).toBe('false');
testComponent.selected = true;
fixture.detectChanges();
- expect(chipNativeElement.getAttribute('aria-selected')).toBe('true');
+ expect(primaryAction.getAttribute('aria-selected')).toBe('true');
}));
it('should disable focus on the checkmark', fakeAsync(() => {
@@ -263,7 +253,7 @@ describe('MDC-based Option Chips', () => {
flush();
fixture.detectChanges();
- const checkmark = chipNativeElement.querySelector('.mdc-chip__checkmark-svg')!;
+ const checkmark = chipNativeElement.querySelector('.mdc-evolution-chip__checkmark-svg')!;
expect(checkmark.getAttribute('focusable')).toBe('false');
}));
});
@@ -275,51 +265,34 @@ describe('MDC-based Option Chips', () => {
});
it('SPACE ignores selection', () => {
- const SPACE_EVENT = createKeyboardEvent('keydown', SPACE);
-
spyOn(testComponent, 'chipSelectionChange');
// Use the spacebar to attempt to select the chip
- chipInstance._keydown(SPACE_EVENT);
+ dispatchKeyboardEvent(primaryAction, 'keydown', SPACE);
fixture.detectChanges();
- expect(chipInstance.selected).toBeFalsy();
+ expect(chipInstance.selected).toBe(false);
expect(testComponent.chipSelectionChange).not.toHaveBeenCalled();
});
it('should not have the aria-selected attribute', () => {
- expect(chipNativeElement.hasAttribute('aria-selected')).toBe(false);
+ expect(primaryAction.hasAttribute('aria-selected')).toBe(false);
});
});
- it('should update the aria-label for disabled chips', () => {
- expect(chipNativeElement.getAttribute('aria-disabled')).toBe('false');
+ it('should update the aria-disabled for disabled chips', () => {
+ expect(primaryAction.getAttribute('aria-disabled')).toBe('false');
testComponent.disabled = true;
fixture.detectChanges();
- expect(chipNativeElement.getAttribute('aria-disabled')).toBe('true');
+ expect(primaryAction.getAttribute('aria-disabled')).toBe('true');
});
});
- it('should hide the leading icon when initialized as selected', () => {
- // We need to recreate the fixture before change detection has
- // run so we can capture the behavior we're testing for.
- fixture.destroy();
- fixture = TestBed.createComponent(SingleChip);
- testComponent = fixture.debugElement.componentInstance;
- testComponent.selected = true;
- fixture.detectChanges();
- chipDebugElement = fixture.debugElement.query(By.directive(MatChipOption))!;
- chipNativeElement = chipDebugElement.nativeElement;
- chipInstance = chipDebugElement.injector.get(MatChipOption);
-
- const avatar = fixture.nativeElement.querySelector('.avatar');
- expect(avatar.classList).toContain(deprecated.chipCssClasses.HIDDEN_LEADING_ICON);
- });
-
- it('should have a focus indicator', () => {
- expect(chipNativeElement.classList.contains('mat-mdc-focus-indicator')).toBe(true);
+ it('should contain a focus indicator inside the text label', () => {
+ const label = chipNativeElement.querySelector('.mdc-evolution-chip__text-label');
+ expect(label?.querySelector('.mat-mdc-focus-indicator')).toBeTruthy();
});
});
});
diff --git a/src/material-experimental/mdc-chips/chip-option.ts b/src/material-experimental/mdc-chips/chip-option.ts
index 3b9362ce17ac..dfca8a135d6c 100644
--- a/src/material-experimental/mdc-chips/chip-option.ts
+++ b/src/material-experimental/mdc-chips/chip-option.ts
@@ -7,7 +7,6 @@
*/
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
-import {SPACE} from '@angular/cdk/keycodes';
import {
ChangeDetectionStrategy,
Component,
@@ -15,9 +14,15 @@ import {
Input,
Output,
ViewEncapsulation,
- AfterContentInit,
+ AfterViewInit,
+ OnInit,
} from '@angular/core';
-import {deprecated} from '@material/chips';
+import {
+ ActionInteractionEvent,
+ MDCChipActionInteractionTrigger,
+ MDCChipActionType,
+ MDCChipCssClasses,
+} from '@material/chips';
import {take} from 'rxjs/operators';
import {MatChip} from './chip';
@@ -40,32 +45,41 @@ export class MatChipSelectionChange {
@Component({
selector: 'mat-basic-chip-option, mat-chip-option',
templateUrl: 'chip-option.html',
- styleUrls: ['chips.css'],
+ styleUrls: ['chip.css'],
inputs: ['color', 'disableRipple', 'tabIndex'],
host: {
- 'role': 'option',
- 'class': 'mat-mdc-focus-indicator mat-mdc-chip-option',
- '[class.mat-mdc-chip-disabled]': 'disabled',
- '[class.mat-mdc-chip-highlighted]': 'highlighted',
- '[class.mat-mdc-chip-with-avatar]': 'leadingIcon',
- '[class.mat-mdc-chip-with-trailing-icon]': 'trailingIcon || removeIcon',
+ 'class': 'mat-mdc-chip mat-mdc-chip-option mdc-evolution-chip mdc-evolution-chip--filter',
'[class.mat-mdc-chip-selected]': 'selected',
'[class.mat-mdc-chip-multiple]': '_chipListMultiple',
+ '[class.mat-mdc-chip-disabled]': 'disabled',
+ '[class.mat-mdc-chip-with-avatar]': 'leadingIcon',
+ '[class.mdc-evolution-chip--selectable]': 'selectable',
+ '[class.mdc-evolution-chip--disabled]': 'disabled',
+ '[class.mdc-evolution-chip--with-trailing-action]': '_hasTrailingIcon()',
+ '[class.mdc-evolution-chip--with-primary-graphic]': '_hasLeadingGraphic()',
+ '[class.mdc-evolution-chip--with-primary-icon]': 'leadingIcon',
+ '[class.mdc-evolution-chip--with-avatar]': 'leadingIcon',
+ '[class.mat-mdc-chip-highlighted]': 'highlighted',
+ '[class.mat-mdc-chip-with-trailing-icon]': '_hasTrailingIcon()',
+ '[attr.tabindex]': 'null',
+ '[attr.aria-label]': 'null',
+ '[attr.role]': 'role',
'[id]': 'id',
- '[tabIndex]': 'tabIndex',
- '[attr.disabled]': 'disabled || null',
- '[attr.aria-disabled]': 'disabled.toString()',
- '[attr.aria-selected]': 'ariaSelected',
- '(click)': '_click($event)',
- '(keydown)': '_keydown($event)',
- '(focus)': 'focus()',
- '(blur)': '_blur()',
},
providers: [{provide: MatChip, useExisting: MatChipOption}],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class MatChipOption extends MatChip implements AfterContentInit {
+export class MatChipOption extends MatChip implements OnInit, AfterViewInit {
+ /** Whether the component is done initializing. */
+ private _isInitialized: boolean;
+
+ /**
+ * Selected state that was assigned before the component was initializing
+ * and which needs to be synced back up with the foundation.
+ */
+ private _pendingSelectedState: boolean | undefined;
+
/** Whether the chip list is selectable. */
chipListSelectable: boolean = true;
@@ -91,16 +105,19 @@ export class MatChipOption extends MatChip implements AfterContentInit {
/** Whether the chip is selected. */
@Input()
get selected(): boolean {
- return this._chipFoundation.isSelected();
+ return (
+ this._pendingSelectedState ?? this._chipFoundation.isActionSelected(MDCChipActionType.PRIMARY)
+ );
}
set selected(value: BooleanInput) {
- if (!this.selectable) {
- return;
- }
- const coercedValue = coerceBooleanProperty(value);
- if (coercedValue != this._chipFoundation.isSelected()) {
- this._chipFoundation.setSelected(coerceBooleanProperty(value));
- this._dispatchSelectionChange();
+ if (this.selectable) {
+ const coercedValue = coerceBooleanProperty(value);
+
+ if (this._isInitialized) {
+ this._setSelectedState(coercedValue, false);
+ } else {
+ this._pendingSelectedState = coercedValue;
+ }
}
}
@@ -120,77 +137,53 @@ export class MatChipOption extends MatChip implements AfterContentInit {
@Output() readonly selectionChange: EventEmitter =
new EventEmitter();
- override ngAfterContentInit() {
- super.ngAfterContentInit();
+ ngOnInit() {
+ this.role = 'presentation';
+ }
+
+ override ngAfterViewInit() {
+ super.ngAfterViewInit();
+ this._isInitialized = true;
- if (this.selected && this.leadingIcon) {
- this.leadingIcon.setClass(deprecated.chipCssClasses.HIDDEN_LEADING_ICON, true);
+ if (this._pendingSelectedState != null) {
+ // Note that we want to clear the pending state before calling `_setSelectedState`, because
+ // we want it to read the actual selected state instead falling back to the pending one.
+ const selectedState = this._pendingSelectedState;
+ this._pendingSelectedState = undefined;
+ this._setSelectedState(selectedState, false);
}
}
/** Selects the chip. */
select(): void {
- if (!this.selectable) {
- return;
- } else if (!this.selected) {
- this._chipFoundation.setSelected(true);
- this._dispatchSelectionChange();
+ if (this.selectable) {
+ this._setSelectedState(true, false);
}
}
/** Deselects the chip. */
deselect(): void {
- if (!this.selectable) {
- return;
- } else if (this.selected) {
- this._chipFoundation.setSelected(false);
- this._dispatchSelectionChange();
+ if (this.selectable) {
+ this._setSelectedState(false, false);
}
}
/** Selects this chip and emits userInputSelection event */
selectViaInteraction(): void {
- if (!this.selectable) {
- return;
- } else if (!this.selected) {
- this._chipFoundation.setSelected(true);
- this._dispatchSelectionChange(true);
+ if (this.selectable) {
+ this._setSelectedState(true, true);
}
}
/** Toggles the current selected state of this chip. */
toggleSelected(isUserInput: boolean = false): boolean {
- if (!this.selectable) {
- return this.selected;
+ if (this.selectable) {
+ this._setSelectedState(!this.selected, isUserInput);
}
- this._chipFoundation.setSelected(!this.selected);
- this._dispatchSelectionChange(isUserInput);
return this.selected;
}
- /** Emits a selection change event. */
- private _dispatchSelectionChange(isUserInput = false) {
- this.selectionChange.emit({
- source: this,
- isUserInput,
- selected: this.selected,
- });
- }
-
- /** Allows for programmatic focusing of the chip. */
- focus(): void {
- if (this.disabled) {
- return;
- }
-
- if (!this._hasFocus()) {
- this._elementRef.nativeElement.focus();
- this._onFocus.next({chip: this});
- }
- this._hasFocusInternal = true;
- }
-
/** Resets the state of the chip when it loses focus. */
_blur(): void {
// When animations are enabled, Angular may end up removing the chip from the DOM a little
@@ -205,31 +198,47 @@ export class MatChipOption extends MatChip implements AfterContentInit {
});
}
- /** Handles click events on the chip. */
- _click(event: MouseEvent) {
- if (this.disabled) {
- event.preventDefault();
- } else {
- this._handleInteraction(event);
- event.stopPropagation();
+ protected override _onChipInteraction(event: ActionInteractionEvent) {
+ const {trigger, source} = event.detail;
+
+ // Non-selection interactions should work the same as other chips.
+ if (
+ source !== MDCChipActionType.PRIMARY ||
+ (trigger !== MDCChipActionInteractionTrigger.CLICK &&
+ trigger !== MDCChipActionInteractionTrigger.ENTER_KEY &&
+ trigger !== MDCChipActionInteractionTrigger.SPACEBAR_KEY)
+ ) {
+ super._onChipInteraction(event);
+ } else if (this.selectable && !this.disabled) {
+ // Otherwise only let the event through if the chip is enabled and selectable.
+ this._chipFoundation.handleActionInteraction(event);
+ this.selectionChange.emit({
+ source: this,
+ isUserInput: true,
+ selected: this.selected,
+ });
}
}
- /** Handles custom key presses. */
- _keydown(event: KeyboardEvent): void {
- if (this.disabled) {
- return;
- }
+ _hasLeadingGraphic() {
+ // The checkmark graphic is built in for multi-select chip lists.
+ return this.leadingIcon || (this._chipListMultiple && this.selectable);
+ }
- switch (event.keyCode) {
- case SPACE:
- this.toggleSelected(true);
+ private _setSelectedState(isSelected: boolean, isUserInput: boolean) {
+ if (isSelected !== this.selected) {
+ this._chipFoundation.setActionSelected(MDCChipActionType.PRIMARY, isSelected);
+ this.selectionChange.emit({
+ source: this,
+ isUserInput,
+ selected: this.selected,
+ });
+ }
- // Always prevent space from scrolling the page since the list has focus
- event.preventDefault();
- break;
- default:
- this._handleInteraction(event);
+ // MDC won't assign the selected class until the animation finishes, but that may not
+ // happen if animations are disabled. If we detect such a case, assign the class manually.
+ if (this._animationsDisabled) {
+ this._elementRef.nativeElement.classList.toggle(MDCChipCssClasses.SELECTED, isSelected);
}
}
}
diff --git a/src/material-experimental/mdc-chips/chip-remove.spec.ts b/src/material-experimental/mdc-chips/chip-remove.spec.ts
index 17ebd0dae4e7..02060a4dce04 100644
--- a/src/material-experimental/mdc-chips/chip-remove.spec.ts
+++ b/src/material-experimental/mdc-chips/chip-remove.spec.ts
@@ -1,14 +1,15 @@
-import {dispatchKeyboardEvent, createKeyboardEvent, dispatchEvent} from '../../cdk/testing/private';
-import {Component, DebugElement} from '@angular/core';
+import {Component} from '@angular/core';
+import {waitForAsync, ComponentFixture, TestBed, fakeAsync, flush} from '@angular/core/testing';
+import {dispatchKeyboardEvent} from '@angular/cdk/testing/private';
import {By} from '@angular/platform-browser';
-import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing';
-import {SPACE, ENTER, TAB} from '@angular/cdk/keycodes';
+import {SPACE, ENTER} from '@angular/cdk/keycodes';
+import {MDCChipAnimation, MDCChipCssClasses} from '@material/chips/chip';
import {MatChip, MatChipsModule} from './index';
describe('MDC-based Chip Remove', () => {
let fixture: ComponentFixture;
let testChip: TestChip;
- let chipDebugElement: DebugElement;
+ let chipInstance: MatChip;
let chipNativeElement: HTMLElement;
beforeEach(
@@ -28,148 +29,120 @@ describe('MDC-based Chip Remove', () => {
testChip = fixture.debugElement.componentInstance;
fixture.detectChanges();
- chipDebugElement = fixture.debugElement.query(By.directive(MatChip))!;
+ const chipDebugElement = fixture.debugElement.query(By.directive(MatChip))!;
chipNativeElement = chipDebugElement.nativeElement;
+ chipInstance = chipDebugElement.componentInstance;
}),
);
- describe('basic behavior', () => {
- it('should apply a CSS class to the remove icon', () => {
- let buttonElement = chipNativeElement.querySelector('button')!;
+ function triggerRemoveSequence() {
+ // At the time of writing, MDC's removal sequence requires the following to happen:
+ // 1. Button is clicked, triggering the animation.
+ // 2. Before the animation has finished, the `--hidden` class is added.
+ // 3. Animation callback fires at some point. It doesn't really matter for the test,
+ // but it does queue up some `requestAnimationFrame` calls that we need to flush.
+ // 4. `transitionend` callback fires and finishes the removal sequence if the
+ // `--hidden` class exists.
+ fixture.detectChanges();
+ (chipInstance as any)._handleAnimationend({
+ animationName: MDCChipAnimation.EXIT,
+ target: chipNativeElement,
+ });
+ flush();
+ (chipInstance as any)._handleTransitionend({target: chipNativeElement});
+ flush();
+ fixture.detectChanges();
+ }
+ describe('basic behavior', () => {
+ it('should apply a CSS class to the remove icon', fakeAsync(() => {
+ const buttonElement = chipNativeElement.querySelector('.mdc-evolution-chip__icon--trailing')!;
expect(buttonElement.classList).toContain('mat-mdc-chip-remove');
- });
+ }));
- it('should ensure that the button cannot submit its parent form', () => {
+ it('should ensure that the button cannot submit its parent form', fakeAsync(() => {
const buttonElement = chipNativeElement.querySelector('button')!;
-
expect(buttonElement.getAttribute('type')).toBe('button');
- });
+ }));
- it('should not set the `type` attribute on non-button elements', () => {
+ it('should not set the `type` attribute on non-button elements', fakeAsync(() => {
const buttonElement = chipNativeElement.querySelector('span.mat-mdc-chip-remove')!;
-
expect(buttonElement.hasAttribute('type')).toBe(false);
- });
-
- it('should emit (removed) event when exit animation is complete', () => {
- let buttonElement = chipNativeElement.querySelector('button')!;
+ }));
+ it('should emit (removed) event when exit animation is complete', fakeAsync(() => {
testChip.removable = true;
fixture.detectChanges();
- spyOn(testChip, 'didRemove');
- buttonElement.click();
- fixture.detectChanges();
+ chipNativeElement.querySelector('button')!.click();
+ triggerRemoveSequence();
expect(testChip.didRemove).toHaveBeenCalled();
- });
-
- it('should not start MDC exit animation if parent chip is disabled', () => {
- let buttonElement = chipNativeElement.querySelector('button')!;
+ }));
+ it('should not start MDC exit animation if parent chip is disabled', fakeAsync(() => {
testChip.removable = true;
testChip.disabled = true;
fixture.detectChanges();
- buttonElement.click();
- fixture.detectChanges();
+ chipNativeElement.querySelector('button')!.click();
- expect(chipNativeElement.classList.contains('mdc-chip--exit')).toBe(false);
- });
+ expect(chipNativeElement.classList.contains(MDCChipCssClasses.HIDDEN)).toBe(false);
+ }));
- it('should not make the element aria-hidden when it is focusable', () => {
+ it('should not make the element aria-hidden when it is focusable', fakeAsync(() => {
const buttonElement = chipNativeElement.querySelector('button')!;
- expect(buttonElement.getAttribute('tabindex')).toBe('0');
+ expect(buttonElement.getAttribute('tabindex')).toBe('-1');
expect(buttonElement.hasAttribute('aria-hidden')).toBe(false);
- });
+ }));
- it('should prevent the default SPACE action', () => {
+ it('should prevent the default SPACE action', fakeAsync(() => {
const buttonElement = chipNativeElement.querySelector('button')!;
testChip.removable = true;
fixture.detectChanges();
const event = dispatchKeyboardEvent(buttonElement, 'keydown', SPACE);
- fixture.detectChanges();
+ triggerRemoveSequence();
expect(event.defaultPrevented).toBe(true);
- });
+ }));
- it('should not prevent the default SPACE action when a modifier key is pressed', () => {
- const buttonElement = chipNativeElement.querySelector('button')!;
-
- testChip.removable = true;
- fixture.detectChanges();
-
- const event = createKeyboardEvent('keydown', SPACE, undefined, {shift: true});
- dispatchEvent(buttonElement, event);
- fixture.detectChanges();
-
- expect(event.defaultPrevented).toBe(false);
- });
-
- it('should prevent the default ENTER action', () => {
+ it('should prevent the default ENTER action', fakeAsync(() => {
const buttonElement = chipNativeElement.querySelector('button')!;
testChip.removable = true;
fixture.detectChanges();
const event = dispatchKeyboardEvent(buttonElement, 'keydown', ENTER);
- fixture.detectChanges();
+ triggerRemoveSequence();
expect(event.defaultPrevented).toBe(true);
- });
-
- it('should not prevent the default ENTER action when a modifier key is pressed', () => {
- const buttonElement = chipNativeElement.querySelector('button')!;
-
- testChip.removable = true;
- fixture.detectChanges();
-
- const event = createKeyboardEvent('keydown', ENTER, undefined, {shift: true});
- dispatchEvent(buttonElement, event);
- fixture.detectChanges();
-
- expect(event.defaultPrevented).toBe(false);
- });
-
- it('should not remove on any key press', () => {
- let buttonElement = chipNativeElement.querySelector('button')!;
-
- testChip.removable = true;
- fixture.detectChanges();
-
- spyOn(testChip, 'didRemove');
- dispatchKeyboardEvent(buttonElement, 'keydown', TAB);
- fixture.detectChanges();
-
- expect(testChip.didRemove).not.toHaveBeenCalled();
- });
-
- it('should have a focus indicator', () => {
- const buttonElement = chipNativeElement.querySelector('button')!;
+ }));
+ it('should have a focus indicator', fakeAsync(() => {
+ const buttonElement = chipNativeElement.querySelector('.mdc-evolution-chip__icon--trailing')!;
expect(buttonElement.classList.contains('mat-mdc-focus-indicator')).toBe(true);
- });
+ }));
});
});
@Component({
template: `
-
-
-
-
+
+
+
+
+
+
`,
})
class TestChip {
removable: boolean;
disabled = false;
-
- didRemove() {}
+ didRemove = jasmine.createSpy('didRemove spy');
}
diff --git a/src/material-experimental/mdc-chips/chip-row.html b/src/material-experimental/mdc-chips/chip-row.html
index ebc293d08a2d..2c8970d46aec 100644
--- a/src/material-experimental/mdc-chips/chip-row.html
+++ b/src/material-experimental/mdc-chips/chip-row.html
@@ -1,31 +1,37 @@
-
-
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
diff --git a/src/material-experimental/mdc-chips/chip-row.spec.ts b/src/material-experimental/mdc-chips/chip-row.spec.ts
index bad5387bc2b0..c130cda43d36 100644
--- a/src/material-experimental/mdc-chips/chip-row.spec.ts
+++ b/src/material-experimental/mdc-chips/chip-row.spec.ts
@@ -1,6 +1,11 @@
import {Directionality} from '@angular/cdk/bidi';
-import {BACKSPACE, DELETE, RIGHT_ARROW, ENTER} from '@angular/cdk/keycodes';
-import {createKeyboardEvent, dispatchEvent, dispatchFakeEvent} from '../../cdk/testing/private';
+import {BACKSPACE, DELETE, ENTER} from '@angular/cdk/keycodes';
+import {
+ createKeyboardEvent,
+ dispatchEvent,
+ dispatchFakeEvent,
+ dispatchKeyboardEvent,
+} from '../../cdk/testing/private';
import {Component, DebugElement, ElementRef, ViewChild} from '@angular/core';
import {waitForAsync, ComponentFixture, TestBed, flush, fakeAsync} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
@@ -10,7 +15,6 @@ import {
MatChipEditInput,
MatChipEvent,
MatChipGrid,
- MatChipRemove,
MatChipRow,
MatChipsModule,
} from './index';
@@ -20,7 +24,6 @@ describe('MDC-based Row Chips', () => {
let chipDebugElement: DebugElement;
let chipNativeElement: HTMLElement;
let chipInstance: MatChipRow;
- let removeIconInstance: MatChipRemove;
let dir = 'ltr';
@@ -55,9 +58,6 @@ describe('MDC-based Row Chips', () => {
chipNativeElement = chipDebugElement.nativeElement;
chipInstance = chipDebugElement.injector.get(MatChipRow);
testComponent = fixture.debugElement.componentInstance;
-
- const removeIconDebugElement = fixture.debugElement.query(By.directive(MatChipRemove))!;
- removeIconInstance = removeIconDebugElement.injector.get(MatChipRemove);
});
describe('basic behaviors', () => {
@@ -134,17 +134,6 @@ describe('MDC-based Row Chips', () => {
expect(testComponent.chipRemove).toHaveBeenCalled();
});
-
- it('arrow key navigation does not emit the (removed) event', () => {
- const ARROW_KEY_EVENT = createKeyboardEvent('keydown', RIGHT_ARROW);
-
- spyOn(testComponent, 'chipRemove');
-
- removeIconInstance.interaction.next(ARROW_KEY_EVENT);
- fixture.detectChanges();
-
- expect(testComponent.chipRemove).not.toHaveBeenCalled();
- });
});
describe('when removable is false', () => {
@@ -178,12 +167,16 @@ describe('MDC-based Row Chips', () => {
});
it('should update the aria-label for disabled chips', () => {
- expect(chipNativeElement.getAttribute('aria-disabled')).toBe('false');
+ const primaryActionElement = chipNativeElement.querySelector(
+ '.mdc-evolution-chip__action--primary',
+ )!;
+
+ expect(primaryActionElement.getAttribute('aria-disabled')).toBe('false');
testComponent.disabled = true;
fixture.detectChanges();
- expect(chipNativeElement.getAttribute('aria-disabled')).toBe('true');
+ expect(primaryActionElement.getAttribute('aria-disabled')).toBe('true');
});
describe('focus management', () => {
@@ -191,7 +184,7 @@ describe('MDC-based Row Chips', () => {
dispatchFakeEvent(chipNativeElement, 'mousedown');
fixture.detectChanges();
- expect(document.activeElement).toHaveClass('mat-mdc-chip-row-focusable-text-content');
+ expect(document.activeElement).toHaveClass('mdc-evolution-chip__action--primary');
});
it('emits focus only once for multiple focus() calls', () => {
@@ -215,46 +208,46 @@ describe('MDC-based Row Chips', () => {
fixture.detectChanges();
});
- it('should apply the mdc-chip--editable class', () => {
- expect(chipNativeElement.classList).toContain('mdc-chip--editable');
- });
-
it('should begin editing on double click', () => {
+ expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
dispatchFakeEvent(chipNativeElement, 'dblclick');
- expect(chipNativeElement.classList).toContain('mdc-chip--editing');
+ fixture.detectChanges();
+ expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeTruthy();
});
it('should begin editing on ENTER', () => {
- chipInstance.focus();
- const primaryActionElement = chipNativeElement.querySelector('.mdc-chip__primary-action')!;
- const enterEvent = createKeyboardEvent('keydown', ENTER, 'Enter');
- dispatchEvent(primaryActionElement, enterEvent);
- expect(chipNativeElement.classList).toContain('mdc-chip--editing');
+ expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
+ dispatchKeyboardEvent(chipNativeElement, 'keydown', ENTER);
+ fixture.detectChanges();
+ expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeTruthy();
});
});
describe('editing behavior', () => {
let editInputInstance: MatChipEditInput;
- let chipContentElement: HTMLElement;
+ let primaryAction: HTMLElement;
- beforeEach(() => {
+ beforeEach(fakeAsync(() => {
testComponent.editable = true;
fixture.detectChanges();
dispatchFakeEvent(chipNativeElement, 'dblclick');
- spyOn(testComponent, 'chipEdit');
fixture.detectChanges();
+ flush();
+ spyOn(testComponent, 'chipEdit');
const editInputDebugElement = fixture.debugElement.query(By.directive(MatChipEditInput))!;
editInputInstance = editInputDebugElement.injector.get(MatChipEditInput);
-
- const chipContentSelector = '.mat-mdc-chip-row-focusable-text-content';
- chipContentElement = chipNativeElement.querySelector(chipContentSelector) as HTMLElement;
- });
+ primaryAction = chipNativeElement.querySelector('.mdc-evolution-chip__action--primary')!;
+ }));
function keyDownOnPrimaryAction(keyCode: number, key: string) {
- const primaryActionElement = chipNativeElement.querySelector('.mdc-chip__primary-action')!;
const keyDownEvent = createKeyboardEvent('keydown', keyCode, key);
- dispatchEvent(primaryActionElement, keyDownEvent);
+ dispatchEvent(primaryAction, keyDownEvent);
+ fixture.detectChanges();
+ }
+
+ function getEditInput(): HTMLElement {
+ return chipNativeElement.querySelector('.mat-chip-edit-input')!;
}
it('should not delete the chip on DELETE or BACKSPACE', () => {
@@ -264,33 +257,27 @@ describe('MDC-based Row Chips', () => {
expect(testComponent.chipDestroy).not.toHaveBeenCalled();
});
- it('should ignore mousedown events', () => {
- spyOn(testComponent, 'chipFocus');
- dispatchFakeEvent(chipNativeElement, 'mousedown');
- expect(testComponent.chipFocus).not.toHaveBeenCalled();
- });
-
it('should stop editing on focusout', fakeAsync(() => {
- const primaryActionElement = chipNativeElement.querySelector('.mdc-chip__primary-action')!;
- dispatchFakeEvent(primaryActionElement, 'focusout', true);
+ dispatchFakeEvent(primaryAction, 'focusout', true);
flush();
- expect(chipNativeElement.classList).not.toContain('mdc-chip--editing');
expect(testComponent.chipEdit).toHaveBeenCalled();
}));
- it('should stop editing on ENTER', () => {
- keyDownOnPrimaryAction(ENTER, 'Enter');
- expect(chipNativeElement.classList).not.toContain('mdc-chip--editing');
+ it('should stop editing on ENTER', fakeAsync(() => {
+ dispatchKeyboardEvent(getEditInput(), 'keydown', ENTER);
+ fixture.detectChanges();
+ flush();
expect(testComponent.chipEdit).toHaveBeenCalled();
- });
+ }));
- it('should emit the new chip value when editing completes', () => {
+ it('should emit the new chip value when editing completes', fakeAsync(() => {
const chipValue = 'chip value';
editInputInstance.setValue(chipValue);
- keyDownOnPrimaryAction(ENTER, 'Enter');
+ dispatchKeyboardEvent(getEditInput(), 'keydown', ENTER);
+ flush();
const expectedValue = jasmine.objectContaining({value: chipValue});
expect(testComponent.chipEdit).toHaveBeenCalledWith(expectedValue);
- });
+ }));
it('should use the projected edit input if provided', () => {
expect(editInputInstance.getNativeElement()).toHaveClass('projected-edit-input');
@@ -308,28 +295,33 @@ describe('MDC-based Row Chips', () => {
expect(editInputNoProject.getNativeElement()).not.toHaveClass('projected-edit-input');
});
- it('should focus the chip content if the edit input has focus on completion', () => {
+ it('should focus the chip content if the edit input has focus on completion', fakeAsync(() => {
const chipValue = 'chip value';
editInputInstance.setValue(chipValue);
- keyDownOnPrimaryAction(ENTER, 'Enter');
- expect(document.activeElement).toBe(chipContentElement);
- });
+ dispatchKeyboardEvent(getEditInput(), 'keydown', ENTER);
+ fixture.detectChanges();
+ flush();
+ expect(document.activeElement).toBe(primaryAction);
+ }));
- it('should focus the chip content if the body has focus on completion', () => {
+ it('should focus the chip content if the body has focus on completion', fakeAsync(() => {
const chipValue = 'chip value';
editInputInstance.setValue(chipValue);
(document.activeElement as HTMLElement).blur();
- keyDownOnPrimaryAction(ENTER, 'Enter');
- expect(document.activeElement).toBe(chipContentElement);
- });
+ dispatchKeyboardEvent(getEditInput(), 'keydown', ENTER);
+ fixture.detectChanges();
+ flush();
+ expect(document.activeElement).toBe(primaryAction);
+ }));
- it('should not change focus if another element has focus on completion', () => {
+ it('should not change focus if another element has focus on completion', fakeAsync(() => {
const chipValue = 'chip value';
editInputInstance.setValue(chipValue);
testComponent.chipInput.nativeElement.focus();
keyDownOnPrimaryAction(ENTER, 'Enter');
- expect(document.activeElement).not.toBe(chipContentElement);
- });
+ flush();
+ expect(document.activeElement).not.toBe(primaryAction);
+ }));
});
});
});
@@ -340,7 +332,7 @@ describe('MDC-based Row Chips', () => {
{{name}}
x
@@ -361,7 +353,6 @@ class SingleChip {
editable: boolean = false;
useCustomEditInput: boolean = true;
- chipFocus: (event?: MatChipEvent) => void = () => {};
chipDestroy: (event?: MatChipEvent) => void = () => {};
chipRemove: (event?: MatChipEvent) => void = () => {};
chipEdit: (event?: MatChipEditedEvent) => void = () => {};
diff --git a/src/material-experimental/mdc-chips/chip-row.ts b/src/material-experimental/mdc-chips/chip-row.ts
index 08c57d940136..46a5308c7948 100644
--- a/src/material-experimental/mdc-chips/chip-row.ts
+++ b/src/material-experimental/mdc-chips/chip-row.ts
@@ -7,11 +7,11 @@
*/
import {Directionality} from '@angular/cdk/bidi';
-import {BACKSPACE, DELETE} from '@angular/cdk/keycodes';
+import {BACKSPACE, DELETE, ENTER} from '@angular/cdk/keycodes';
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
import {
- AfterContentInit,
AfterViewInit,
+ Attribute,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
@@ -31,9 +31,9 @@ import {
MAT_RIPPLE_GLOBAL_OPTIONS,
RippleGlobalOptions,
} from '@angular/material-experimental/mdc-core';
+import {FocusMonitor} from '@angular/cdk/a11y';
import {MatChip, MatChipEvent} from './chip';
import {MatChipEditInput} from './chip-edit-input';
-import {GridKeyManagerRow} from './grid-key-manager';
/** Represents an event fired on an individual `mat-chip` when it is edited. */
export interface MatChipEditedEvent extends MatChipEvent {
@@ -48,23 +48,28 @@ export interface MatChipEditedEvent extends MatChipEvent {
@Component({
selector: 'mat-chip-row, mat-basic-chip-row',
templateUrl: 'chip-row.html',
- styleUrls: ['chips.css'],
+ styleUrls: ['chip.css'],
inputs: ['color', 'disableRipple', 'tabIndex'],
host: {
- 'role': 'row',
- 'class': 'mat-mdc-chip-row',
+ 'class': 'mat-mdc-chip mat-mdc-chip-row mdc-evolution-chip',
+ '[class.mat-mdc-chip-with-avatar]': 'leadingIcon',
'[class.mat-mdc-chip-disabled]': 'disabled',
+ '[class.mat-mdc-chip-editing]': '_isEditing',
+ '[class.mat-mdc-chip-editable]': 'editable',
+ '[class.mdc-evolution-chip--disabled]': 'disabled',
+ '[class.mdc-evolution-chip--with-trailing-action]': '_hasTrailingIcon()',
+ '[class.mdc-evolution-chip--with-primary-graphic]': 'leadingIcon',
+ '[class.mdc-evolution-chip--with-primary-icon]': 'leadingIcon',
+ '[class.mdc-evolution-chip--with-avatar]': 'leadingIcon',
'[class.mat-mdc-chip-highlighted]': 'highlighted',
- '[class.mat-mdc-chip-with-avatar]': 'leadingIcon',
- '[class.mat-mdc-chip-with-trailing-icon]': 'trailingIcon || removeIcon',
- '[class.mdc-chip--editable]': 'editable',
+ '[class.mat-mdc-chip-with-trailing-icon]': '_hasTrailingIcon()',
'[id]': 'id',
- '[attr.disabled]': 'disabled || null',
- '[attr.aria-disabled]': 'disabled.toString()',
- '[tabIndex]': 'tabIndex',
+ '[attr.tabindex]': 'null',
+ '[attr.aria-label]': 'null',
+ '[attr.role]': 'role',
'(mousedown)': '_mousedown($event)',
- '(dblclick)': '_dblclick($event)',
'(keydown)': '_keydown($event)',
+ '(dblclick)': '_doubleclick()',
'(focusin)': '_focusin($event)',
'(focusout)': '_focusout($event)',
},
@@ -72,10 +77,7 @@ export interface MatChipEditedEvent extends MatChipEvent {
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class MatChipRow
- extends MatChip
- implements AfterContentInit, AfterViewInit, GridKeyManagerRow
-{
+export class MatChipRow extends MatChip implements AfterViewInit {
protected override basicChipAttrName = 'mat-basic-chip-row';
@Input() editable: boolean = false;
@@ -84,20 +86,13 @@ export class MatChipRow
@Output() readonly edited: EventEmitter =
new EventEmitter();
- /**
- * The focusable wrapper element in the first gridcell, which contains all
- * chip content other than the remove icon.
- */
- @ViewChild('chipContent') chipContent: ElementRef;
-
/** The default chip edit input that is used if none is projected into this chip row. */
@ViewChild(MatChipEditInput) defaultEditInput?: MatChipEditInput;
/** The projected chip edit input. */
@ContentChild(MatChipEditInput) contentEditInput?: MatChipEditInput;
- /** The focusable grid cells for this row. Implemented as part of GridKeyManagerRow. */
- cells!: HTMLElement[];
+ _isEditing = false;
/**
* Timeout used to give some time between `focusin` and `focusout`
@@ -106,100 +101,77 @@ export class MatChipRow
private _focusoutTimeout: number | null;
constructor(
- @Inject(DOCUMENT) private readonly _document: any,
changeDetectorRef: ChangeDetectorRef,
elementRef: ElementRef,
ngZone: NgZone,
+ focusMonitor: FocusMonitor,
+ @Inject(DOCUMENT) _document: any,
@Optional() dir: Directionality,
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string,
@Optional()
@Inject(MAT_RIPPLE_GLOBAL_OPTIONS)
globalRippleOptions?: RippleGlobalOptions,
+ @Attribute('tabindex') tabIndex?: string,
) {
- super(changeDetectorRef, elementRef, ngZone, dir, animationMode, globalRippleOptions);
- }
-
- override ngAfterContentInit() {
- super.ngAfterContentInit();
-
- if (this.removeIcon) {
- // Defer setting the value in order to avoid the "Expression
- // has changed after it was checked" errors from Angular.
- setTimeout(() => {
- // removeIcon has tabIndex 0 for regular chips, but should only be focusable by
- // the GridFocusKeyManager for row chips.
- this.removeIcon.tabIndex = -1;
- });
- }
+ super(
+ changeDetectorRef,
+ elementRef,
+ ngZone,
+ focusMonitor,
+ _document,
+ dir,
+ animationMode,
+ globalRippleOptions,
+ tabIndex,
+ );
+
+ this.role = 'row';
}
- override ngAfterViewInit() {
- super.ngAfterViewInit();
- this.cells = this.removeIcon
- ? [this.chipContent.nativeElement, this.removeIcon._elementRef.nativeElement]
- : [this.chipContent.nativeElement];
- }
-
- /**
- * Allows for programmatic focusing of the chip.
- * Sends focus to the first grid cell. The row chip element itself
- * is never focused.
- */
- focus(): void {
- if (this.disabled) {
- return;
- }
-
- if (!this._hasFocusInternal) {
- this._onFocus.next({chip: this});
- }
-
- this.chipContent.nativeElement.focus();
+ override _hasTrailingIcon() {
+ // The trailing icon is hidden while editing.
+ return !this._isEditing && super._hasTrailingIcon();
}
/**
* Emits a blur event when one of the gridcells loses focus, unless focus moved
* to the other gridcell.
*/
- _focusout(event: FocusEvent) {
+ _focusout() {
if (this._focusoutTimeout) {
clearTimeout(this._focusoutTimeout);
}
// Wait to see if focus moves to the other gridcell
this._focusoutTimeout = window.setTimeout(() => {
+ if (this._isEditing) {
+ this._onEditFinish();
+ }
+
this._hasFocusInternal = false;
this._onBlur.next({chip: this});
- this._handleInteraction(event);
});
}
/** Records that the chip has focus when one of the gridcells is focused. */
- _focusin(event: FocusEvent) {
+ _focusin() {
if (this._focusoutTimeout) {
clearTimeout(this._focusoutTimeout);
this._focusoutTimeout = null;
}
this._hasFocusInternal = true;
- this._handleInteraction(event);
}
/** Sends focus to the first gridcell when the user clicks anywhere inside the chip. */
_mousedown(event: MouseEvent) {
- if (this._isEditing()) {
- return;
- }
+ if (!this._isEditing) {
+ if (!this.disabled) {
+ this.focus();
+ }
- if (!this.disabled) {
- this.focus();
+ event.preventDefault();
}
-
- event.preventDefault();
- }
-
- _dblclick(event: MouseEvent) {
- this._handleInteraction(event);
}
/** Handles custom key presses. */
@@ -207,44 +179,60 @@ export class MatChipRow
if (this.disabled) {
return;
}
- if (this._isEditing()) {
- this._handleInteraction(event);
- return;
- }
+
switch (event.keyCode) {
+ case ENTER:
+ if (this._isEditing) {
+ event.preventDefault();
+ // Wrap in a timeout so the timing is consistent as when it is emitted in `focusout`.
+ setTimeout(() => this._onEditFinish());
+ } else if (this.editable) {
+ this._startEditing();
+ }
+ break;
case DELETE:
case BACKSPACE:
- // Remove the focused chip
- this.remove();
- // Always prevent so page navigation does not occur
- event.preventDefault();
+ if (!this._isEditing) {
+ // Remove the focused chip
+ this.remove();
+ // Always prevent so page navigation does not occur
+ event.preventDefault();
+ }
break;
- default:
- this._handleInteraction(event);
}
}
- _isEditing() {
- return this._chipFoundation.isEditing();
+ _doubleclick() {
+ if (!this.disabled && this.editable) {
+ this._startEditing();
+ }
}
- protected override _onEditStart() {
+ private _startEditing() {
+ // The value depends on the DOM so we need to extract it before we flip the flag.
+ const value = this.value;
+
+ // Make the primary action non-interactive so that it doesn't
+ // navigate when the user presses the arrow keys while editing.
+ this.primaryAction.isInteractive = false;
+ this._isEditing = true;
+
// Defer initializing the input so it has time to be added to the DOM.
- setTimeout(() => {
- this._getEditInput().initialize(this.value);
- });
+ setTimeout(() => this._getEditInput().initialize(value));
}
- protected override _onEditFinish() {
+ private _onEditFinish() {
// If the edit input is still focused or focus was returned to the body after it was destroyed,
// return focus to the chip contents.
if (
this._document.activeElement === this._getEditInput().getNativeElement() ||
this._document.activeElement === this._document.body
) {
- this.chipContent.nativeElement.focus();
+ this.primaryAction.focus();
}
this.edited.emit({chip: this, value: this._getEditInput().getValue()});
+ this.primaryAction.isInteractive = true;
+ this._isEditing = false;
}
/**
diff --git a/src/material-experimental/mdc-chips/chip-set.scss b/src/material-experimental/mdc-chips/chip-set.scss
new file mode 100644
index 000000000000..4c4fc242066b
--- /dev/null
+++ b/src/material-experimental/mdc-chips/chip-set.scss
@@ -0,0 +1,32 @@
+@use '@material/chips/chip-set' as mdc-chip-set;
+@use '../mdc-helpers/mdc-helpers';
+
+@include mdc-chip-set.core-styles($query: mdc-helpers.$mat-base-styles-query);
+
+// Ensures that the internal chip container spans the entire outer container width, if the
+// outer container width is customized. This is used by some wrapper components in g3.
+.mat-mdc-chip-set {
+ .mdc-evolution-chip-set__chips {
+ min-width: 100%;
+ }
+}
+
+// Angular Material supports vertically-stacked chips, which MDC does not.
+.mat-mdc-chip-set-stacked {
+ flex-direction: column;
+ align-items: flex-start;
+
+ .mat-mdc-chip {
+ width: 100%;
+ }
+}
+
+input.mat-mdc-chip-input {
+ flex: 1 0 150px;
+ margin-left: 8px;
+
+ [dir='rtl'] & {
+ margin-left: 0;
+ margin-right: 8px;
+ }
+}
diff --git a/src/material-experimental/mdc-chips/chip-set.ts b/src/material-experimental/mdc-chips/chip-set.ts
index ddbb16bada01..1b8787de063a 100644
--- a/src/material-experimental/mdc-chips/chip-set.ts
+++ b/src/material-experimental/mdc-chips/chip-set.ts
@@ -6,8 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {Directionality} from '@angular/cdk/bidi';
+import {LiveAnnouncer} from '@angular/cdk/a11y';
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
+import {DOCUMENT} from '@angular/common';
import {
AfterContentInit,
AfterViewInit,
@@ -16,19 +17,27 @@ import {
Component,
ContentChildren,
ElementRef,
+ Inject,
Input,
OnDestroy,
- Optional,
QueryList,
ViewEncapsulation,
} from '@angular/core';
import {HasTabIndex, mixinTabIndex} from '@angular/material-experimental/mdc-core';
-import {deprecated} from '@material/chips';
-import {merge, Observable, Subject, Subscription} from 'rxjs';
-import {startWith, takeUntil} from 'rxjs/operators';
+import {
+ MDCChipSetFoundation,
+ MDCChipSetAdapter,
+ MDCChipFoundation,
+ MDCChipEvents,
+ ChipAnimationEvent,
+ ChipInteractionEvent,
+ ChipNavigationEvent,
+ MDCChipActionType,
+} from '@material/chips';
+import {merge, Observable, Subject} from 'rxjs';
+import {startWith, switchMap, takeUntil} from 'rxjs/operators';
import {MatChip, MatChipEvent} from './chip';
-
-let uid = 0;
+import {emitCustomEvent} from './emit-event';
/**
* Boilerplate for applying mixins to MatChipSet.
@@ -47,14 +56,17 @@ const _MatChipSetMixinBase = mixinTabIndex(MatChipSetBase);
*/
@Component({
selector: 'mat-chip-set',
- template: ' ',
- styleUrls: ['chips.css'],
+ template: `
+
+
+
+ `,
+ styleUrls: ['chip-set.css'],
host: {
- 'class': 'mat-mdc-chip-set mdc-chip-set',
+ 'class': 'mat-mdc-chip-set mdc-evolution-chip-set',
'[attr.role]': 'role',
// TODO: replace this binding with use of AriaDescriber
'[attr.aria-describedby]': '_ariaDescribedby || null',
- '[id]': '_uid',
},
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -63,15 +75,6 @@ export class MatChipSet
extends _MatChipSetMixinBase
implements AfterContentInit, AfterViewInit, HasTabIndex, OnDestroy
{
- /** Subscription to remove changes in chips. */
- private _chipRemoveSubscription: Subscription | null;
-
- /** Subscription to destroyed events in chips. */
- private _chipDestroyedSubscription: Subscription | null;
-
- /** Subscription to chip interactions. */
- private _chipInteractionSubscription: Subscription | null;
-
/**
* When a chip is destroyed, we store the index of the destroyed chip until the chips
* query list notifies about the update. This is necessary because we cannot determine an
@@ -80,38 +83,61 @@ export class MatChipSet
protected _lastDestroyedChipIndex: number | null = null;
/** The MDC foundation containing business logic for MDC chip-set. */
- protected _chipSetFoundation: deprecated.MDCChipSetFoundation;
+ protected _chipSetFoundation: MDCChipSetFoundation;
/** Subject that emits when the component has been destroyed. */
protected _destroyed = new Subject();
+ /** Combined stream of all of the child chips' remove events. */
+ get chipDestroyedChanges(): Observable {
+ return this._getChipStream(chip => chip.destroyed);
+ }
+
/**
* Implementation of the MDC chip-set adapter interface.
* These methods are called by the chip set foundation.
*/
- protected _chipSetAdapter: deprecated.MDCChipSetAdapter = {
- hasClass: className => this._hasMdcClass(className),
- // No-op. We keep track of chips via ContentChildren, which will be updated when a chip is
- // removed.
- removeChipAtIndex: () => {},
- // No-op for base chip set. MatChipListbox overrides the adapter to provide this method.
- selectChipAtIndex: () => {},
- getIndexOfChipById: (id: string) => this._chips.toArray().findIndex(chip => chip.id === id),
- focusChipPrimaryActionAtIndex: () => {},
- focusChipTrailingActionAtIndex: () => {},
- removeFocusFromChipAtIndex: () => {},
- isRTL: () => !!this._dir && this._dir.value === 'rtl',
- getChipListCount: () => this._chips.length,
- // TODO(mmalerba): Implement using LiveAnnouncer.
- announceMessage: () => {},
+ protected _chipSetAdapter: MDCChipSetAdapter = {
+ announceMessage: message => this._liveAnnouncer.announce(message),
+ emitEvent: (eventName, eventDetail) => {
+ emitCustomEvent(this._elementRef.nativeElement, this._document, eventName, eventDetail, true);
+ },
+ getAttribute: name => this._elementRef.nativeElement.getAttribute(name),
+ getChipActionsAtIndex: index => this._chipFoundation(index)?.getActions() || [],
+ getChipCount: () => this._chips.length,
+ getChipIdAtIndex: index => this._chipFoundation(index)?.getElementID() || '',
+ getChipIndexById: id => {
+ return this._chips.toArray().findIndex(chip => chip._getFoundation().getElementID() === id);
+ },
+ isChipFocusableAtIndex: (index, actionType) => {
+ return this._chipFoundation(index)?.isActionFocusable(actionType) || false;
+ },
+ isChipSelectableAtIndex: (index, actionType) => {
+ return this._chipFoundation(index)?.isActionSelectable(actionType) || false;
+ },
+ isChipSelectedAtIndex: (index, actionType) => {
+ return this._chipFoundation(index)?.isActionSelected(actionType) || false;
+ },
+ removeChipAtIndex: index => this._chips.toArray()[index]?.remove(),
+ setChipFocusAtIndex: (index, action, behavior) => {
+ this._chipFoundation(index)?.setActionFocus(action, behavior);
+ },
+ setChipSelectedAtIndex: (index, actionType, isSelected) => {
+ // Setting the trailing action as deselected ends up deselecting the entire chip.
+ // This is working as expected, but it's not something we want so we only apply the
+ // selected state to the primary chip.
+ if (actionType === MDCChipActionType.PRIMARY) {
+ this._chipFoundation(index)?.setActionSelected(actionType, isSelected);
+ }
+ },
+ startChipAnimationAtIndex: (index, animation) => {
+ this._chipFoundation(index)?.startAnimation(animation);
+ },
};
/** The aria-describedby attribute on the chip list for improved a11y. */
_ariaDescribedby: string;
- /** Uid of the chip set */
- _uid: string = `mat-mdc-chip-set-${uid++}`;
-
/**
* Map from class to whether the class is enabled.
* Enabled classes are set on the MDC chip-set div.
@@ -154,21 +180,6 @@ export class MatChipSet
return this._hasFocusedChip();
}
- /** Combined stream of all of the child chips' remove events. */
- get chipRemoveChanges(): Observable {
- return merge(...this._chips.map(chip => chip.removed));
- }
-
- /** Combined stream of all of the child chips' remove events. */
- get chipDestroyedChanges(): Observable {
- return merge(...this._chips.map(chip => chip.destroyed));
- }
-
- /** Combined stream of all of the child chips' interaction events. */
- get chipInteractionChanges(): Observable {
- return merge(...this._chips.map(chip => chip.interaction));
- }
-
/** The chips that are part of this chip set. */
@ContentChildren(MatChip, {
// We need to use `descendants: true`, because Ivy will no longer match
@@ -178,12 +189,17 @@ export class MatChipSet
_chips: QueryList;
constructor(
- protected _elementRef: ElementRef,
+ private _liveAnnouncer: LiveAnnouncer,
+ @Inject(DOCUMENT) private _document: any,
+ protected _elementRef: ElementRef,
protected _changeDetectorRef: ChangeDetectorRef,
- @Optional() protected _dir: Directionality,
) {
super(_elementRef);
- this._chipSetFoundation = new deprecated.MDCChipSetFoundation(this._chipSetAdapter);
+ const element = _elementRef.nativeElement;
+ this._chipSetFoundation = new MDCChipSetFoundation(this._chipSetAdapter);
+ element.addEventListener(MDCChipEvents.ANIMATION, this._handleChipAnimation);
+ element.addEventListener(MDCChipEvents.INTERACTION, this._handleChipInteraction);
+ element.addEventListener(MDCChipEvents.NAVIGATION, this._handleChipNavigation);
}
ngAfterViewInit() {
@@ -199,13 +215,26 @@ export class MatChipSet
this._syncChipsState();
});
}
+ });
+
+ this.chipDestroyedChanges.pipe(takeUntil(this._destroyed)).subscribe((event: MatChipEvent) => {
+ const chip = event.chip;
+ const chipIndex = this._chips.toArray().indexOf(event.chip);
- this._resetChips();
+ // In case the chip that will be removed is currently focused, we temporarily store
+ // the index in order to be able to determine an appropriate sibling chip that will
+ // receive focus.
+ if (this._isValidIndex(chipIndex) && chip._hasFocus()) {
+ this._lastDestroyedChipIndex = chipIndex;
+ }
});
}
ngOnDestroy() {
- this._dropSubscriptions();
+ const element = this._elementRef.nativeElement;
+ element.removeEventListener(MDCChipEvents.ANIMATION, this._handleChipAnimation);
+ element.removeEventListener(MDCChipEvents.INTERACTION, this._handleChipInteraction);
+ element.removeEventListener(MDCChipEvents.NAVIGATION, this._handleChipNavigation);
this._destroyed.next();
this._destroyed.complete();
this._chipSetFoundation.destroy();
@@ -226,82 +255,6 @@ export class MatChipSet
}
}
- /** Sets whether the given CSS class should be applied to the MDC chip. */
- protected _setMdcClass(cssClass: string, active: boolean) {
- const classes = this._elementRef.nativeElement.classList;
- active ? classes.add(cssClass) : classes.remove(cssClass);
- this._changeDetectorRef.markForCheck();
- }
-
- /** Adapter method that returns true if the chip set has the given MDC class. */
- protected _hasMdcClass(className: string) {
- return this._elementRef.nativeElement.classList.contains(className);
- }
-
- /** Updates subscriptions to chip events. */
- private _resetChips() {
- this._dropSubscriptions();
- this._subscribeToChipEvents();
- }
-
- /** Subscribes to events on the child chips. */
- protected _subscribeToChipEvents() {
- this._listenToChipsRemove();
- this._listenToChipsDestroyed();
- this._listenToChipsInteraction();
- }
-
- /** Subscribes to chip removal events. */
- private _listenToChipsRemove() {
- this._chipRemoveSubscription = this.chipRemoveChanges.subscribe((event: MatChipEvent) => {
- this._chipSetFoundation.handleChipRemoval({
- chipId: event.chip.id,
- // TODO(mmalerba): Add removal message.
- removedAnnouncement: null,
- });
- });
- }
-
- /** Subscribes to chip destroyed events. */
- private _listenToChipsDestroyed() {
- this._chipDestroyedSubscription = this.chipDestroyedChanges.subscribe((event: MatChipEvent) => {
- const chip = event.chip;
- const chipIndex: number = this._chips.toArray().indexOf(event.chip);
-
- // In case the chip that will be removed is currently focused, we temporarily store
- // the index in order to be able to determine an appropriate sibling chip that will
- // receive focus.
- if (this._isValidIndex(chipIndex) && chip._hasFocus()) {
- this._lastDestroyedChipIndex = chipIndex;
- }
- });
- }
-
- /** Subscribes to chip interaction events. */
- private _listenToChipsInteraction() {
- this._chipInteractionSubscription = this.chipInteractionChanges.subscribe((id: string) => {
- this._chipSetFoundation.handleChipInteraction({chipId: id});
- });
- }
-
- /** Unsubscribes from all chip events. */
- protected _dropSubscriptions() {
- if (this._chipRemoveSubscription) {
- this._chipRemoveSubscription.unsubscribe();
- this._chipRemoveSubscription = null;
- }
-
- if (this._chipInteractionSubscription) {
- this._chipInteractionSubscription.unsubscribe();
- this._chipInteractionSubscription = null;
- }
-
- if (this._chipDestroyedSubscription) {
- this._chipDestroyedSubscription.unsubscribe();
- this._chipDestroyedSubscription = null;
- }
- }
-
/** Dummy method for subclasses to override. Base chip set cannot be focused. */
focus() {}
@@ -317,18 +270,41 @@ export class MatChipSet
/** Checks whether an event comes from inside a chip element. */
protected _originatesFromChip(event: Event): boolean {
- return this._checkForClassInHierarchy(event, 'mdc-chip');
+ return this._checkForClassInHierarchy(event, 'mdc-evolution-chip');
}
/**
- * Checks whether an event comes from inside a chip element in the editing
- * state.
+ * Removes the `tabindex` from the chip grid and resets it back afterwards, allowing the
+ * user to tab out of it. This prevents the grid from capturing focus and redirecting
+ * it back to the first chip, creating a focus trap, if it user tries to tab away.
*/
- protected _originatesFromEditingChip(event: Event): boolean {
- return this._checkForClassInHierarchy(event, 'mdc-chip--editing');
+ protected _allowFocusEscape() {
+ const previousTabIndex = this.tabIndex;
+
+ if (this.tabIndex !== -1) {
+ this.tabIndex = -1;
+
+ setTimeout(() => {
+ this.tabIndex = previousTabIndex;
+ this._changeDetectorRef.markForCheck();
+ });
+ }
}
- private _checkForClassInHierarchy(event: Event, className: string) {
+ /**
+ * Gets a stream of events from all the chips within the set.
+ * The stream will automatically incorporate any newly-added chips.
+ */
+ protected _getChipStream(
+ mappingFunction: (chip: C) => Observable,
+ ): Observable {
+ return this._chips.changes.pipe(
+ startWith(null),
+ switchMap(() => merge(...(this._chips as QueryList).map(mappingFunction))),
+ );
+ }
+
+ protected _checkForClassInHierarchy(event: Event, className: string) {
let currentElement = event.target as HTMLElement | null;
while (currentElement && currentElement !== this._elementRef.nativeElement) {
@@ -342,4 +318,20 @@ export class MatChipSet
return false;
}
+
+ private _chipFoundation(index: number): MDCChipFoundation | undefined {
+ return this._chips.toArray()[index]?._getFoundation();
+ }
+
+ private _handleChipAnimation = (event: Event) => {
+ this._chipSetFoundation.handleChipAnimation(event as ChipAnimationEvent);
+ };
+
+ private _handleChipInteraction = (event: Event) => {
+ this._chipSetFoundation.handleChipInteraction(event as ChipInteractionEvent);
+ };
+
+ private _handleChipNavigation = (event: Event) => {
+ this._chipSetFoundation.handleChipNavigation(event as ChipNavigationEvent);
+ };
}
diff --git a/src/material-experimental/mdc-chips/chip.html b/src/material-experimental/mdc-chips/chip.html
index fd8b2c0278de..35283fd77ea0 100644
--- a/src/material-experimental/mdc-chips/chip.html
+++ b/src/material-experimental/mdc-chips/chip.html
@@ -1,12 +1,23 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
diff --git a/src/material-experimental/mdc-chips/chip.scss b/src/material-experimental/mdc-chips/chip.scss
new file mode 100644
index 000000000000..4fa66e348c5f
--- /dev/null
+++ b/src/material-experimental/mdc-chips/chip.scss
@@ -0,0 +1,208 @@
+@use '@material/chips/chip' as mdc-chip;
+@use '@material/chips/chip-theme' as mdc-chip-theme;
+@use '../../material/core/style/layout-common';
+@use '../../cdk/a11y';
+@use '../mdc-helpers/mdc-helpers';
+
+@include mdc-chip.without-ripple-styles($query: mdc-helpers.$mat-base-styles-query);
+
+.mat-mdc-standard-chip {
+ @include mdc-chip-theme.theme-styles((
+ with-avatar-avatar-shape: (
+ family: 'rounded',
+ radius: (14px, 14px, 14px, 14px)
+ ),
+ with-icon-icon-size: 18px,
+ with-leading-icon-disabled-leading-icon-opacity: 0.38,
+ with-leading-icon-leading-icon-size: 20px,
+ with-trailing-icon-disabled-trailing-icon-opacity: 0.38,
+ with-avatar-avatar-size: 28px,
+ with-avatar-disabled-avatar-opacity: 0.38,
+ with-icon-disabled-icon-opacity: 0.38,
+ with-trailing-icon-trailing-icon-size: 18px,
+ flat-disabled-outline-opacity: 0.12,
+ flat-disabled-unselected-outline-opacity: 0.12,
+ flat-selected-outline-width: 0,
+ outline-width: 1px,
+ flat-unselected-outline-width: 1px,
+ flat-outline-width: 1px,
+ disabled-label-text-opacity: 0.38,
+ disabled-outline-opacity: 0.12,
+ elevated-disabled-container-opacity: 0.12,
+ container-height: 32px,
+ container-shape: (
+ family: 'rounded',
+ radius: (16px, 16px, 16px, 16px)
+ ),
+ ));
+
+ @include a11y.high-contrast(active, off) {
+ outline: solid 1px;
+
+ &.cdk-focused {
+ // Use 2px here since the dotted outline is a little thinner.
+ outline: dotted 2px;
+ }
+
+ .mdc-evolution-chip__checkmark-path {
+ // SVG colors won't be changed in high contrast mode and since the checkmark is white
+ // by default, it'll blend in with the background in black-on-white mode. Override the
+ // color to ensure that it's visible. We need !important, because the theme styles are
+ // very specific.
+ stroke: #000 !important;
+ }
+ }
+
+ // Angular Material supports disabled chips, which MDC does not.
+ // Dim the disabled chips and stop MDC from changing the icon color on click.
+ &.mdc-evolution-chip--disabled {
+ opacity: 0.4;
+ }
+
+ // MDC sets `overflow: hidden` on these elements in order to truncate the text. This is
+ // unnecessary since our chips don't truncate their text and it makes it difficult to style
+ // the strong focus indicators so we need to override it.
+ .mdc-evolution-chip__cell--primary,
+ .mdc-evolution-chip__action--primary,
+ .mat-mdc-chip-action-label {
+ overflow: visible;
+ }
+
+ // Ensures that the trailing icon is pushed to the end if the chip has a set width.
+ .mdc-evolution-chip__cell--primary {
+ width: 100%;
+ }
+
+ // MDC sizes and positions this element using `width`, `height` and `padding`.
+ // This usually works, but it's common for apps to add `box-sizing: border-box`
+ // to all elements on the page which can cause the graphic to be clipped.
+ // Set an explicit `box-sizing` in order to avoid these issues.
+ .mat-mdc-chip-graphic,
+ .mat-mdc-chip-trailing-icon {
+ box-sizing: content-box;
+ }
+
+
+ &._mat-animation-noopable {
+ &,
+ .mdc-evolution-chip__graphic,
+ .mdc-evolution-chip__checkmark,
+ .mdc-evolution-chip__checkmark-path {
+ // MDC listens to `transitionend` events on some of these
+ // elements so we can't disable the transitions completely.
+ transition-duration: 1ms;
+ animation-duration: 1ms;
+ }
+ }
+}
+
+// MDC's focus and hover indication is handled through their ripple which we currently
+// don't use due to size concerns so we have to re-implement it ourselves.
+.mat-mdc-chip-focus-overlay {
+ @include layout-common.fill;
+ pointer-events: none;
+ opacity: 0;
+ border-radius: inherit;
+ transition: opacity 150ms linear;
+
+ ._mat-animation-noopable & {
+ transition: none;
+ }
+
+ .mat-mdc-basic-chip & {
+ display: none;
+ }
+
+ .mat-mdc-chip:hover & {
+ opacity: 0.04;
+ }
+
+ .mat-mdc-chip.cdk-focused & {
+ opacity: 0.12;
+ }
+}
+
+// The ripple container should match the bounds of the entire chip.
+.mat-mdc-chip-ripple {
+ @include layout-common.fill;
+
+ // Disable pointer events for the ripple container and state overlay because the container
+ // will overlay the user content and we don't want to disable mouse events on the user content.
+ // Pointer events can be safely disabled because the ripple trigger element is the host element.
+ pointer-events: none;
+
+ // Inherit the border radius from the parent so that state overlay and ripples don't exceed the
+ // parent button boundaries.
+ border-radius: inherit;
+}
+
+.mat-mdc-chip-avatar {
+ // In case an icon or text is used as an avatar.
+ text-align: center;
+ line-height: 1;
+}
+
+// Required for the strong focus indicator to fill the chip.
+.mat-mdc-chip {
+ position: relative;
+}
+
+.mat-mdc-chip-action-label {
+ // MDC centers the text, but we have a lot of internal customers who have it at the start.
+ text-align: left;
+
+ [dir='rtl'] & {
+ text-align: right;
+ }
+
+ // When a chip has a trailing action, it'll have two focusable elements when navigating with
+ // the arrow keys: the primary action and the trailing one. If that's the case, we apply
+ // `position: relative` to the primary action container so that the indicators is only around
+ // the text label. This allows for it to be distinguished from the indicator on the trailing icon.
+ .mat-mdc-chip.mdc-evolution-chip--with-trailing-action & {
+ position: relative;
+ }
+
+ .mat-mdc-chip-primary-focus-indicator {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ pointer-events: none;
+ }
+}
+
+.mat-mdc-chip-remove {
+ .mat-icon {
+ width: inherit;
+ height: inherit;
+ font-size: inherit;
+ box-sizing: content-box;
+ }
+}
+
+// Fades out the trailing icon slightly so that it can become darker when focused.
+// The MDC theming has variables for this, but the focus/hover states don't seem to work.
+.mat-mdc-chip-remove {
+ opacity: 0.54;
+
+ &:focus {
+ opacity: 1;
+ }
+}
+
+.mat-chip-edit-input {
+ cursor: text;
+ display: inline-block;
+ color: inherit;
+ outline: 0;
+}
+
+// Single-selection chips show their selected state using a background color which won't be visible
+// in high contrast mode. This isn't necessary in multi-selection since there's a checkmark.
+.mat-mdc-chip-selected:not(.mat-mdc-chip-multiple) {
+ @include a11y.high-contrast(active, off) {
+ outline-width: 3px;
+ }
+}
diff --git a/src/material-experimental/mdc-chips/chip.spec.ts b/src/material-experimental/mdc-chips/chip.spec.ts
index eba8e531fef8..47390203c972 100644
--- a/src/material-experimental/mdc-chips/chip.spec.ts
+++ b/src/material-experimental/mdc-chips/chip.spec.ts
@@ -58,19 +58,12 @@ describe('MDC-based MatChip', () => {
expect(chip.getAttribute('tabindex')).toBe('3');
});
- it('should be able to set a static tabindex', () => {
- fixture = TestBed.createComponent(BasicChipWithStaticTabindex);
- fixture.detectChanges();
-
- const chip = fixture.nativeElement.querySelector('mat-basic-chip');
- expect(chip.getAttribute('tabindex')).toBe('3');
- });
-
it('should be able to set a dynamic tabindex', () => {
fixture = TestBed.createComponent(BasicChipWithBoundTabindex);
fixture.detectChanges();
const chip = fixture.nativeElement.querySelector('mat-basic-chip');
+
expect(chip.getAttribute('tabindex')).toBe('12');
fixture.componentInstance.tabindex = 15;
@@ -93,6 +86,7 @@ describe('MDC-based MatChip', () => {
describe('MatChip', () => {
let testComponent: SingleChip;
+ let primaryAction: HTMLElement;
beforeEach(() => {
fixture = TestBed.createComponent(SingleChip);
@@ -104,6 +98,7 @@ describe('MDC-based MatChip', () => {
chipRippleDebugElement = chipDebugElement.query(By.directive(MatRipple))!;
chipRippleInstance = chipRippleDebugElement.injector.get(MatRipple);
testComponent = fixture.debugElement.componentInstance;
+ primaryAction = chipNativeElement.querySelector('.mdc-evolution-chip__action--primary')!;
});
it('adds the `mat-chip` class', () => {
@@ -169,17 +164,10 @@ describe('MDC-based MatChip', () => {
.toBe(true);
});
- it('should update the aria-label for disabled chips', () => {
- expect(chipNativeElement.getAttribute('aria-disabled')).toBe('false');
-
+ it('should make disabled chips non-focusable', () => {
testComponent.disabled = true;
fixture.detectChanges();
-
- expect(chipNativeElement.getAttribute('aria-disabled')).toBe('true');
- });
-
- it('should make disabled chips non-focusable', () => {
- expect(chipNativeElement.getAttribute('tabindex')).toBeFalsy();
+ expect(primaryAction.hasAttribute('tabindex')).toBe(false);
});
it('should return the chip text if value is undefined', () => {
@@ -236,12 +224,12 @@ class SingleChip {
class BasicChip {}
@Component({
- template: `Hello `,
+ template: `Hello `,
})
class BasicChipWithStaticTabindex {}
@Component({
- template: `Hello `,
+ template: `Hello `,
})
class BasicChipWithBoundTabindex {
tabindex = 12;
diff --git a/src/material-experimental/mdc-chips/chip.ts b/src/material-experimental/mdc-chips/chip.ts
index ecac78189c33..87af59368d6c 100644
--- a/src/material-experimental/mdc-chips/chip.ts
+++ b/src/material-experimental/mdc-chips/chip.ts
@@ -10,13 +10,11 @@ import {Directionality} from '@angular/cdk/bidi';
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
import {
- AfterContentInit,
AfterViewInit,
Component,
ChangeDetectionStrategy,
ChangeDetectorRef,
ContentChild,
- Directive,
ElementRef,
EventEmitter,
Inject,
@@ -27,7 +25,9 @@ import {
Output,
ViewEncapsulation,
ViewChild,
+ Attribute,
} from '@angular/core';
+import {DOCUMENT} from '@angular/common';
import {
CanColor,
CanDisable,
@@ -40,10 +40,19 @@ import {
mixinTabIndex,
RippleGlobalOptions,
} from '@angular/material-experimental/mdc-core';
-import {deprecated} from '@material/chips';
-import {SPACE, ENTER, hasModifierKey} from '@angular/cdk/keycodes';
+import {
+ MDCChipFoundation,
+ MDCChipAdapter,
+ MDCChipActionType,
+ MDCChipActionFocusBehavior,
+ MDCChipActionFoundation,
+ MDCChipActionEvents,
+ ActionInteractionEvent,
+ ActionNavigationEvent,
+ MDCChipActionInteractionTrigger,
+} from '@material/chips';
+import {FocusMonitor} from '@angular/cdk/a11y';
import {Subject} from 'rxjs';
-import {takeUntil} from 'rxjs/operators';
import {
MatChipAvatar,
MatChipTrailingIcon,
@@ -52,6 +61,8 @@ import {
MAT_CHIP_TRAILING_ICON,
MAT_CHIP_REMOVE,
} from './chip-icons';
+import {emitCustomEvent} from './emit-event';
+import {MatChipAction} from './chip-action';
let uid = 0;
@@ -61,17 +72,6 @@ export interface MatChipEvent {
chip: MatChip;
}
-/**
- * Directive to add MDC CSS to non-basic chips.
- * @docs-private
- */
-@Directive({
- selector: `mat-chip, mat-chip-option, mat-chip-row, [mat-chip], [mat-chip-option],
- [mat-chip-row]`,
- host: {'class': 'mat-mdc-chip mdc-chip'},
-})
-export class MatChipCssInternalOnly {}
-
/**
* Boilerplate for applying mixins to MatChip.
* @docs-private
@@ -90,37 +90,39 @@ const _MatChipMixinBase = mixinTabIndex(mixinColor(mixinDisableRipple(MatChipBas
*/
@Component({
selector: 'mat-basic-chip, mat-chip',
- inputs: ['color', 'disableRipple'],
+ inputs: ['color', 'disableRipple', 'tabIndex'],
exportAs: 'matChip',
templateUrl: 'chip.html',
- styleUrls: ['chips.css'],
+ styleUrls: ['chip.css'],
host: {
- '[class.mat-mdc-chip-disabled]': 'disabled',
- '[class.mat-mdc-chip-highlighted]': 'highlighted',
+ 'class': 'mat-mdc-chip',
+ '[class.mdc-evolution-chip]': '!_isBasicChip',
+ '[class.mdc-evolution-chip--disabled]': 'disabled',
+ '[class.mdc-evolution-chip--with-trailing-action]': '_hasTrailingIcon()',
+ '[class.mdc-evolution-chip--with-primary-graphic]': 'leadingIcon',
+ '[class.mdc-evolution-chip--with-primary-icon]': 'leadingIcon',
+ '[class.mdc-evolution-chip--with-avatar]': 'leadingIcon',
'[class.mat-mdc-chip-with-avatar]': 'leadingIcon',
- '[class.mat-mdc-chip-with-trailing-icon]': 'trailingIcon || removeIcon',
+ '[class.mat-mdc-chip-highlighted]': 'highlighted',
+ '[class.mat-mdc-chip-disabled]': 'disabled',
'[class.mat-mdc-basic-chip]': '_isBasicChip',
'[class.mat-mdc-standard-chip]': '!_isBasicChip',
+ '[class.mat-mdc-chip-with-trailing-icon]': '_hasTrailingIcon()',
'[class._mat-animation-noopable]': '_animationsDisabled',
'[id]': 'id',
- '[attr.disabled]': 'disabled || null',
- '[attr.aria-disabled]': 'disabled.toString()',
- '(transitionend)': '_handleTransitionEnd($event)',
+ '[attr.role]': 'role',
+ '[attr.tabindex]': 'role ? tabIndex : null',
+ '[attr.aria-label]': 'ariaLabel',
},
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MatChip
extends _MatChipMixinBase
- implements
- AfterContentInit,
- AfterViewInit,
- CanColor,
- CanDisableRipple,
- CanDisable,
- HasTabIndex,
- OnDestroy
+ implements AfterViewInit, CanColor, CanDisableRipple, CanDisable, HasTabIndex, OnDestroy
{
+ protected _document: Document;
+
/** Whether the ripple is centered on the chip. */
readonly _isRippleCentered = false;
@@ -130,30 +132,30 @@ export class MatChip
/** Emits when the chip is blurred. */
readonly _onBlur = new Subject();
- readonly REMOVE_ICON_HANDLED_KEYS: ReadonlySet = new Set([SPACE, ENTER]);
-
/** Whether this chip is a basic (unstyled) chip. */
readonly _isBasicChip: boolean;
+ /** Role for the root of the chip. */
+ @Input() role: string | null = null;
+
/** Whether the chip has focus. */
protected _hasFocusInternal = false;
+ /** Whether moving focus into the chip is pending. */
+ private _pendingFocus: boolean;
+
/** Whether animations for the chip are enabled. */
_animationsDisabled: boolean;
- _handleTransitionEnd(event: TransitionEvent) {
- this._chipFoundation.handleTransitionEnd(event);
- }
-
_hasFocus() {
return this._hasFocusInternal;
}
- /** Default unique id for the chip. */
- private _uniqueId = `mat-mdc-chip-${uid++}`;
-
/** A unique id for the chip. If none is supplied, it will be auto-generated. */
- @Input() id: string = this._uniqueId;
+ @Input() id: string = `mat-mdc-chip-${uid++}`;
+
+ /** ARIA label for the content of the chip. */
+ @Input('aria-label') ariaLabel: string | null = null;
@Input()
get disabled(): boolean {
@@ -161,15 +163,21 @@ export class MatChip
}
set disabled(value: BooleanInput) {
this._disabled = coerceBooleanProperty(value);
+
if (this.removeIcon) {
this.removeIcon.disabled = this._disabled;
}
+
+ this._chipFoundation.setDisabled(this._disabled);
}
protected _disabled: boolean = false;
private _textElement!: HTMLElement;
- /** The value of the chip. Defaults to the content inside the mdc-chip__text element. */
+ /**
+ * The value of the chip. Defaults to the content inside
+ * the `mat-mdc-chip-action-label` element.
+ */
@Input()
get value(): any {
return this._value !== undefined ? this._value : this._textElement.textContent!.trim();
@@ -203,17 +211,14 @@ export class MatChip
}
protected _highlighted: boolean = false;
- /** Emitted when the user interacts with the chip. */
- @Output() readonly interaction = new EventEmitter();
+ /** Emitted when a chip is to be removed. */
+ @Output() readonly removed: EventEmitter = new EventEmitter();
/** Emitted when the chip is destroyed. */
@Output() readonly destroyed: EventEmitter = new EventEmitter();
- /** Emitted when a chip is to be removed. */
- @Output() readonly removed: EventEmitter = new EventEmitter();
-
/** The MDC foundation containing business logic for MDC chip. */
- _chipFoundation: deprecated.MDCChipFoundation;
+ _chipFoundation: MDCChipFoundation;
/** The unstyled chip selector for this component. */
protected basicChipAttrName = 'mat-basic-chip';
@@ -230,151 +235,118 @@ export class MatChip
/** Reference to the MatRipple instance of the chip. */
@ViewChild(MatRipple) ripple: MatRipple;
+ /** Action receiving the primary set of user interactions. */
+ @ViewChild(MatChipAction) primaryAction: MatChipAction;
+
/**
* Implementation of the MDC chip adapter interface.
* These methods are called by the chip foundation.
*/
- protected _chipAdapter: deprecated.MDCChipAdapter = {
+ protected _chipAdapter: MDCChipAdapter = {
addClass: className => this._setMdcClass(className, true),
removeClass: className => this._setMdcClass(className, false),
hasClass: className => this._elementRef.nativeElement.classList.contains(className),
- addClassToLeadingIcon: className => this.leadingIcon.setClass(className, true),
- removeClassFromLeadingIcon: className => this.leadingIcon.setClass(className, false),
- eventTargetHasClass: (target: EventTarget | null, className: string) => {
- // We need to null check the `classList`, because IE and Edge don't
- // support it on SVG elements and Edge seems to throw for ripple
- // elements, because they're outside the DOM.
- return target && (target as Element).classList
- ? (target as Element).classList.contains(className)
- : false;
+ emitEvent: (eventName: string, data: T) => {
+ emitCustomEvent(this._elementRef.nativeElement, this._document, eventName, data, true);
},
- notifyInteraction: () => this._notifyInteraction(),
- notifySelection: () => {
- // No-op. We call dispatchSelectionEvent ourselves in MatChipOption,
- // because we want to specify whether selection occurred via user
- // input.
+ setStyleProperty: (propertyName: string, value: string) => {
+ this._elementRef.nativeElement.style.setProperty(propertyName, value);
},
- notifyNavigation: () => this._notifyNavigation(),
- notifyTrailingIconInteraction: () => {},
- notifyRemoval: () => this.remove(),
- notifyEditStart: () => {
- this._onEditStart();
- this._changeDetectorRef.markForCheck();
+ isRTL: () => this._dir?.value === 'rtl',
+ getAttribute: attributeName => this._elementRef.nativeElement.getAttribute(attributeName),
+ getElementID: () => this._elementRef.nativeElement.id,
+ getOffsetWidth: () => this._elementRef.nativeElement.offsetWidth,
+ getActions: () => {
+ const result: MDCChipActionType[] = [];
+
+ if (this._getAction(MDCChipActionType.PRIMARY)) {
+ result.push(MDCChipActionType.PRIMARY);
+ }
+
+ if (this._getAction(MDCChipActionType.TRAILING)) {
+ result.push(MDCChipActionType.TRAILING);
+ }
+
+ return result;
},
- notifyEditFinish: () => {
- this._onEditFinish();
- this._changeDetectorRef.markForCheck();
+ isActionSelectable: (action: MDCChipActionType) => {
+ return this._getAction(action)?.isSelectable() || false;
},
- getComputedStyleValue: propertyName => {
- // This function is run when a chip is removed so it might be
- // invoked during server-side rendering. Add some extra checks just in
- // case.
- if (typeof window !== 'undefined' && window) {
- const getComputedStyle = window.getComputedStyle(this._elementRef.nativeElement);
- return getComputedStyle.getPropertyValue(propertyName);
- }
- return '';
+ isActionSelected: (action: MDCChipActionType) => {
+ return this._getAction(action)?.isSelected() || false;
},
- setStyleProperty: (propertyName: string, value: string) => {
- this._elementRef.nativeElement.style.setProperty(propertyName, value);
+ isActionDisabled: (action: MDCChipActionType) => {
+ return this._getAction(action)?.isDisabled() || false;
},
- hasLeadingIcon: () => !!this.leadingIcon,
- isTrailingActionNavigable: () => {
- if (this.trailingIcon) {
- return this.trailingIcon.isNavigable();
- }
- return false;
+ isActionFocusable: (action: MDCChipActionType) => {
+ return this._getAction(action)?.isFocusable() || false;
},
- isRTL: () => !!this._dir && this._dir.value === 'rtl',
- focusPrimaryAction: () => {
- // Angular Material MDC chips fully manage focus. TODO: Managing focus
- // and handling keyboard events was added by MDC after our
- // implementation; consider consolidating.
+ setActionSelected: (action: MDCChipActionType, isSelected: boolean) => {
+ this._getAction(action)?.setSelected(isSelected);
},
- focusTrailingAction: () => {},
- removeTrailingActionFocus: () => {},
- setPrimaryActionAttr: (name: string, value: string) => {
- // MDC is currently using this method to set aria-checked on choice
- // and filter chips, which in the MDC templates have role="checkbox"
- // and role="radio" respectively. We have role="option" on those chips
- // instead, so we do not want aria-checked. Since we also manage the
- // tabindex ourselves, we don't allow MDC to set it.
- if (name === 'aria-checked' || name === 'tabindex') {
- return;
- }
- this._elementRef.nativeElement.setAttribute(name, value);
+ setActionDisabled: (action: MDCChipActionType, isDisabled: boolean) => {
+ this._getAction(action)?.setDisabled(isDisabled);
+ },
+ setActionFocus: (action: MDCChipActionType, behavior: MDCChipActionFocusBehavior) => {
+ this._getAction(action)?.setFocus(behavior);
},
- // The 2 functions below are used by the MDC ripple, which we aren't using,
- // so they will never be called
- getRootBoundingClientRect: () => this._elementRef.nativeElement.getBoundingClientRect(),
- getCheckmarkBoundingClientRect: () => null,
- getAttribute: attr => this._elementRef.nativeElement.getAttribute(attr),
};
constructor(
public _changeDetectorRef: ChangeDetectorRef,
- elementRef: ElementRef,
+ elementRef: ElementRef,
protected _ngZone: NgZone,
+ private _focusMonitor: FocusMonitor,
+ @Inject(DOCUMENT) _document: any,
@Optional() private _dir: Directionality,
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string,
@Optional()
@Inject(MAT_RIPPLE_GLOBAL_OPTIONS)
private _globalRippleOptions?: RippleGlobalOptions,
+ @Attribute('tabindex') tabIndex?: string,
) {
super(elementRef);
- this._chipFoundation = new deprecated.MDCChipFoundation(this._chipAdapter);
+ const element = elementRef.nativeElement;
+ this._document = _document;
+ this._chipFoundation = new MDCChipFoundation(this._chipAdapter);
this._animationsDisabled = animationMode === 'NoopAnimations';
this._isBasicChip =
- elementRef.nativeElement.hasAttribute(this.basicChipAttrName) ||
- elementRef.nativeElement.tagName.toLowerCase() === this.basicChipAttrName;
- }
-
- ngAfterContentInit() {
- this._initRemoveIcon();
+ element.hasAttribute(this.basicChipAttrName) ||
+ element.tagName.toLowerCase() === this.basicChipAttrName;
+ element.addEventListener(MDCChipActionEvents.INTERACTION, this._handleActionInteraction);
+ element.addEventListener(MDCChipActionEvents.NAVIGATION, this._handleActionNavigation);
+ _focusMonitor.monitor(elementRef, true);
+
+ _ngZone.runOutsideAngular(() => {
+ element.addEventListener('transitionend', this._handleTransitionend);
+ element.addEventListener('animationend', this._handleAnimationend);
+ });
+
+ if (tabIndex != null) {
+ this.tabIndex = parseInt(tabIndex) ?? this.defaultTabIndex;
+ }
}
ngAfterViewInit() {
this._chipFoundation.init();
- this._textElement = this._elementRef.nativeElement.querySelector('.mdc-chip__text');
+ this._chipFoundation.setDisabled(this.disabled);
+ this._textElement = this._elementRef.nativeElement.querySelector('.mat-mdc-chip-action-label');
+
+ if (this._pendingFocus) {
+ this._pendingFocus = false;
+ this.focus();
+ }
}
ngOnDestroy() {
- this.destroyed.emit({chip: this});
+ const element = this._elementRef.nativeElement;
+ element.removeEventListener(MDCChipActionEvents.INTERACTION, this._handleActionInteraction);
+ element.removeEventListener(MDCChipActionEvents.NAVIGATION, this._handleActionNavigation);
+ element.removeEventListener('transitionend', this._handleTransitionend);
+ element.removeEventListener('animationend', this._handleAnimationend);
this._chipFoundation.destroy();
- }
-
- /** Sets up the remove icon chip foundation, and subscribes to remove icon events. */
- private _initRemoveIcon() {
- if (this.removeIcon) {
- this._chipFoundation.setShouldRemoveOnTrailingIconClick(true);
- this.removeIcon.disabled = this.disabled;
-
- this.removeIcon.interaction.pipe(takeUntil(this.destroyed)).subscribe(event => {
- // The MDC chip foundation calls stopPropagation() for any trailing icon interaction
- // event, even ones it doesn't handle, so we want to avoid passing it keyboard events
- // for which we have a custom handler. Note that we assert the type of the event using
- // the `type`, because `instanceof KeyboardEvent` can throw during server-side rendering.
- const isKeyboardEvent = event.type.startsWith('key');
-
- if (
- this.disabled ||
- (isKeyboardEvent && !this.REMOVE_ICON_HANDLED_KEYS.has((event as KeyboardEvent).keyCode))
- ) {
- return;
- }
-
- this.remove();
-
- if (isKeyboardEvent && !hasModifierKey(event as KeyboardEvent)) {
- const keyCode = (event as KeyboardEvent).keyCode;
-
- // Prevent default space and enter presses so we don't scroll the page or submit forms.
- if (keyCode === SPACE || keyCode === ENTER) {
- event.preventDefault();
- }
- }
- });
- }
+ this._focusMonitor.stopMonitoring(this._elementRef);
+ this.destroyed.emit({chip: this});
}
/**
@@ -395,58 +367,98 @@ export class MatChip
this._changeDetectorRef.markForCheck();
}
- /** Forwards interaction events to the MDC chip foundation. */
- _handleInteraction(event: MouseEvent | KeyboardEvent | FocusEvent) {
- if (this.disabled) {
- return;
- }
+ /** Whether or not the ripple should be disabled. */
+ _isRippleDisabled(): boolean {
+ return (
+ this.disabled ||
+ this.disableRipple ||
+ this._animationsDisabled ||
+ this._isBasicChip ||
+ !!this._globalRippleOptions?.disabled
+ );
+ }
- if (event.type === 'click') {
- this._chipFoundation.handleClick();
- return;
+ _getAction(type: MDCChipActionType): MDCChipActionFoundation | undefined {
+ switch (type) {
+ case MDCChipActionType.PRIMARY:
+ return this.primaryAction?._getFoundation();
+ case MDCChipActionType.TRAILING:
+ return (this.removeIcon || this.trailingIcon)?._getFoundation();
}
- if (event.type === 'dblclick') {
- this._chipFoundation.handleDoubleClick();
- }
+ return undefined;
+ }
+
+ _getFoundation() {
+ return this._chipFoundation;
+ }
- if (event.type === 'keydown') {
- this._chipFoundation.handleKeydown(event as KeyboardEvent);
+ _hasTrailingIcon() {
+ return !!(this.trailingIcon || this.removeIcon);
+ }
+
+ /** Allows for programmatic focusing of the chip. */
+ focus(): void {
+ if (this.disabled) {
return;
}
- if (event.type === 'focusout') {
- this._chipFoundation.handleFocusOut(event as FocusEvent);
+ // If `focus` is called before `ngAfterViewInit`, we won't have access to the primary action.
+ // This can happen if the consumer tries to focus a chip immediately after it is added.
+ // Queue the method to be called again on init.
+ if (!this.primaryAction) {
+ this._pendingFocus = true;
+ return;
}
- if (event.type === 'focusin') {
- this._chipFoundation.handleFocusIn(event as FocusEvent);
+ if (!this._hasFocus()) {
+ this._onFocus.next({chip: this});
+ this._hasFocusInternal = true;
}
- }
- /** Whether or not the ripple should be disabled. */
- _isRippleDisabled(): boolean {
- return (
- this.disabled ||
- this.disableRipple ||
- this._animationsDisabled ||
- this._isBasicChip ||
- !!this._globalRippleOptions?.disabled
- );
+ this.primaryAction.focus();
}
- _notifyInteraction() {
- this.interaction.emit(this.id);
+ /** Overridden by MatChipOption. */
+ protected _onChipInteraction(event: ActionInteractionEvent) {
+ const removeElement = this.removeIcon?._elementRef.nativeElement;
+ const trigger = event.detail.trigger;
+
+ // MDC's removal process requires an `animationend` event followed by a `transitionend`
+ // event coming from the chip, which in turn will call `remove`. While we can stub
+ // out these events in our own tests, they can be difficult to fake for consumers that are
+ // testing our components or are wrapping them. We skip the entire sequence and trigger the
+ // removal directly in order to make the component easier to deal with.
+ if (
+ removeElement &&
+ (trigger === MDCChipActionInteractionTrigger.CLICK ||
+ trigger === MDCChipActionInteractionTrigger.ENTER_KEY ||
+ trigger === MDCChipActionInteractionTrigger.SPACEBAR_KEY) &&
+ (event.target === removeElement || removeElement.contains(event.target))
+ ) {
+ this.remove();
+ } else {
+ this._chipFoundation.handleActionInteraction(event);
+ }
}
- _notifyNavigation() {
- // TODO: This is a new feature added by MDC. Consider exposing it to users
- // in the future.
- }
+ private _handleActionInteraction = (event: Event) => {
+ this._onChipInteraction(event as ActionInteractionEvent);
+ };
- /** Overridden by MatChipRow. */
- protected _onEditStart() {}
+ private _handleActionNavigation = (event: Event) => {
+ this._chipFoundation.handleActionNavigation(event as ActionNavigationEvent);
+ };
- /** Overridden by MatChipRow. */
- protected _onEditFinish() {}
+ private _handleTransitionend = (event: TransitionEvent) => {
+ if (event.target === this._elementRef.nativeElement) {
+ this._ngZone.run(() => this._chipFoundation.handleTransitionEnd());
+ }
+ };
+
+ private _handleAnimationend = (event: AnimationEvent) => {
+ if (event.target === this._elementRef.nativeElement) {
+ this._ngZone.run(() => this._chipFoundation.handleAnimationEnd(event));
+ }
+ };
}
diff --git a/src/material-experimental/mdc-chips/chips.scss b/src/material-experimental/mdc-chips/chips.scss
deleted file mode 100644
index a54134b604a3..000000000000
--- a/src/material-experimental/mdc-chips/chips.scss
+++ /dev/null
@@ -1,184 +0,0 @@
-@use '@material/chips/deprecated' as mdc-chips;
-@use '@material/ripple' as mdc-ripple;
-@use '../../material/core/style/layout-common';
-@use '../../cdk/a11y';
-@use '../mdc-helpers/mdc-helpers';
-
-@include mdc-chips.without-ripple($query: mdc-helpers.$mat-base-styles-query);
-@include mdc-chips.set-core-styles($query: mdc-helpers.$mat-base-styles-query);
-
-.mat-mdc-chip {
- // MDC uses a pointer cursor
- cursor: default;
-
- &._mat-animation-noopable {
- animation: none;
- transition: none;
-
- .mdc-chip__checkmark-svg {
- transition: none;
- }
- }
-
- @include a11y.high-contrast(active, off) {
- outline: solid 1px;
-
- &:focus {
- // Use 2px here since the dotted outline is a little thinner.
- outline: dotted 2px;
- }
- }
-}
-
-// The ripple container should match the bounds of the entire chip.
-.mat-mdc-chip-ripple {
- @include layout-common.fill;
-
- // Disable pointer events for the ripple container and state overlay because the container
- // will overlay the user content and we don't want to disable mouse events on the user content.
- // Pointer events can be safely disabled because the ripple trigger element is the host element.
- pointer-events: none;
-
- // Inherit the border radius from the parent so that state overlay and ripples don't exceed the
- // parent button boundaries.
- border-radius: inherit;
-}
-
-// The MDC chip styles related to hover and focus states are intertwined with the MDC ripple styles.
-// We currently don't use the MDC ripple due to size concerns, therefore we need to add some
-// additional styles to restore these states.
-.mdc-chip__ripple {
- @include mdc-ripple.target-common($query: structure);
-
- &::after, &::before {
- @include layout-common.fill;
- content: '';
- pointer-events: none;
- opacity: 0;
- border-radius: inherit;
-
- ._mat-animation-noopable & {
- transition: none;
- }
- }
-}
-
-// Angular Material supports disabled chips, which MDC does not.
-// Dim the disabled chips and stop MDC from changing the icon color on click.
-.mat-mdc-chip-disabled.mat-mdc-chip {
- opacity: 0.4;
-
- .mat-mdc-chip-trailing-icon, .mat-mdc-chip-row-focusable-text-content {
- pointer-events: none;
- }
-
- // Do not show state interactions for disabled chips.
- .mdc-chip__ripple::after, .mdc-chip__ripple::before {
- display: none;
- }
-}
-
-// Angular Material supports vertically-stacked chips, which MDC does not.
-.mat-mdc-chip-set-stacked {
- flex-direction: column;
- align-items: flex-start;
-
- .mat-mdc-chip {
- width: 100%;
- }
-}
-
-// Add styles for the matChipInputFor input element.
-$mat-chip-input-width: 150px;
-
-input.mat-mdc-chip-input {
- flex: 1 0 $mat-chip-input-width;
-}
-
-// The margin value is set in MDC.
-$chip-margin: 4px;
-
-// Don't let the chip margin increase the mat-form-field height.
-.mat-mdc-chip-grid {
- margin: -$chip-margin;
-
- // Keep the mat-chip-grid height the same even when there are no chips.
- input.mat-mdc-chip-input {
- margin: $chip-margin;
- }
-}
-
-.mdc-chip__checkmark-path {
- ._mat-animation-noopable & {
- transition: none;
- }
-
- @include a11y.high-contrast(black-on-white, off) {
- // SVG colors won't be changed in high contrast mode and since the checkmark is white
- // by default, it'll blend in with the background in black-on-white mode. Override the color
- // to ensure that it's visible. We need !important, because the theme styles are very specific.
- stroke: #000 !important;
- }
-}
-
-// Needed for the focus indicator.
-.mat-mdc-chip-row-focusable-text-content {
- position: relative;
-}
-
-.mat-mdc-chip-remove {
- // Reset the user agent styles in case a button is used.
- border: none;
- -webkit-appearance: none;
- -moz-appearance: none;
- padding: 0;
- background: none;
-
- .mat-icon {
- width: inherit;
- height: inherit;
- font-size: inherit;
- }
-}
-
-// Single-selection chips show their selected state using a background color which won't be visible
-// in high contrast mode. This isn't necessary in multi-selection since there's a checkmark.
-.mat-mdc-chip-selected:not(.mat-mdc-chip-multiple) {
- @include a11y.high-contrast(active, off) {
- outline-width: 3px;
- }
-}
-
-.mat-mdc-chip-row-focusable-text-content,
-.mat-mdc-chip-remove-icon {
- display: flex;
- align-items: center;
-}
-
-.mat-mdc-chip-content {
- display: inline-flex;
-}
-
-.mdc-chip--editing {
- background-color: transparent;
- display: flex;
- flex-direction: column;
-
- .mat-mdc-chip-content {
- pointer-events: none;
- height: 0;
- overflow: hidden;
- }
-}
-
-.mat-chip-edit-input {
- cursor: text;
- display: inline-block;
-}
-
-.mat-mdc-chip-edit-input-container {
- width: 100%;
- height: 100%;
- display: flex;
- align-items: center;
-}
diff --git a/src/material-experimental/mdc-chips/emit-event.ts b/src/material-experimental/mdc-chips/emit-event.ts
new file mode 100644
index 000000000000..b208b6d3f0d8
--- /dev/null
+++ b/src/material-experimental/mdc-chips/emit-event.ts
@@ -0,0 +1,33 @@
+/**
+ * @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.io/license
+ */
+
+/**
+ * Emits a custom event from an element.
+ * @param element Element from which to emit the event.
+ * @param _document Document that the element is placed in.
+ * @param eventName Name of the event.
+ * @param data Data attached to the event.
+ * @param shouldBubble Whether the event should bubble.
+ */
+export function emitCustomEvent(
+ element: HTMLElement,
+ _document: Document,
+ eventName: string,
+ data: T,
+ shouldBubble: boolean,
+): void {
+ let event: CustomEvent;
+ if (typeof CustomEvent === 'function') {
+ event = new CustomEvent(eventName, {bubbles: shouldBubble, detail: data});
+ } else {
+ event = _document.createEvent('CustomEvent');
+ event.initCustomEvent(eventName, shouldBubble, false, data);
+ }
+
+ element.dispatchEvent(event);
+}
diff --git a/src/material-experimental/mdc-chips/grid-focus-key-manager.ts b/src/material-experimental/mdc-chips/grid-focus-key-manager.ts
deleted file mode 100644
index 40aa6d2280ba..000000000000
--- a/src/material-experimental/mdc-chips/grid-focus-key-manager.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/**
- * @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.io/license
- */
-
-import {GridKeyManager} from './grid-key-manager';
-
-/**
- * A version of GridKeyManager where the cells are HTMLElements, and focus()
- * is called on a cell when it becomes active.
- */
-export class GridFocusKeyManager extends GridKeyManager {
- /**
- * Sets the active cell to the cell at the specified
- * indices and focuses the newly active cell.
- * @param cell Row and column indices of the cell to be set as active.
- */
- override setActiveCell(cell: {row: number; column: number}): void;
-
- /**
- * Sets the active cell to the cell that is specified and focuses it.
- * @param cell Cell to be set as active.
- */
- override setActiveCell(cell: HTMLElement): void;
-
- override setActiveCell(cell: any): void {
- super.setActiveCell(cell);
-
- if (this.activeCell) {
- this.activeCell.focus();
- }
- }
-}
diff --git a/src/material-experimental/mdc-chips/grid-key-manager.ts b/src/material-experimental/mdc-chips/grid-key-manager.ts
deleted file mode 100644
index d956d2f05b91..000000000000
--- a/src/material-experimental/mdc-chips/grid-key-manager.ts
+++ /dev/null
@@ -1,285 +0,0 @@
-/**
- * @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.io/license
- */
-
-import {QueryList} from '@angular/core';
-import {Subject} from 'rxjs';
-import {UP_ARROW, DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, HOME, END} from '@angular/cdk/keycodes';
-
-/** The keys handled by the GridKeyManager keydown method. */
-export const NAVIGATION_KEYS = [DOWN_ARROW, UP_ARROW, RIGHT_ARROW, LEFT_ARROW];
-
-/** This interface is for rows that can be passed to a GridKeyManager. */
-export interface GridKeyManagerRow {
- cells: T[];
-}
-
-/**
- * This class manages keyboard events for grids. If you pass it a query list
- * of GridKeyManagerRow, it will set the active cell correctly when arrow events occur.
- *
- * GridKeyManager expects that rows may change dynamically, but the cells for a given row are
- * static. It also expects that all rows have the same number of cells.
- */
-export class GridKeyManager {
- private _activeRowIndex = -1;
- private _activeColumnIndex = -1;
- private _activeRow: GridKeyManagerRow | null = null;
- private _activeCell: T | null = null;
- private _dir: 'ltr' | 'rtl' = 'ltr';
- private _homeAndEnd = false;
-
- constructor(private _rows: QueryList> | GridKeyManagerRow[]) {
- // We allow for the rows to be an array because, in some cases, the consumer may
- // not have access to a QueryList of the rows they want to manage (e.g. when the
- // rows aren't being collected via `ViewChildren` or `ContentChildren`).
- if (_rows instanceof QueryList) {
- _rows.changes.subscribe((newRows: QueryList>) => {
- if (this._activeRow) {
- const newIndex = newRows.toArray().indexOf(this._activeRow);
-
- if (newIndex > -1 && newIndex !== this._activeRowIndex) {
- this._activeRowIndex = newIndex;
- }
- }
- });
- }
- }
-
- /** Stream that emits whenever the active cell of the grid manager changes. */
- change = new Subject<{row: number; column: number}>();
-
- /**
- * Configures the directionality of the key manager's horizontal movement.
- * @param direction Direction which is considered forward movement across a row.
- *
- * If withDirectionality is not set, the default is 'ltr'.
- */
- withDirectionality(direction: 'ltr' | 'rtl'): this {
- this._dir = direction;
- return this;
- }
-
- /**
- * Sets the active cell to the cell at the indices specified.
- * @param cell The row and column containing the cell to be set as active.
- */
- setActiveCell(cell: {row: number; column: number}): void;
-
- /**
- * Sets the active cell to the cell.
- * @param cell The cell to be set as active.
- */
- setActiveCell(cell: T): void;
-
- setActiveCell(cell: any): void {
- const previousRowIndex = this._activeRowIndex;
- const previousColumnIndex = this._activeColumnIndex;
-
- this.updateActiveCell(cell);
-
- if (
- this._activeRowIndex !== previousRowIndex ||
- this._activeColumnIndex !== previousColumnIndex
- ) {
- this.change.next({row: this._activeRowIndex, column: this._activeColumnIndex});
- }
- }
-
- /**
- * Configures the key manager to activate the first and last items
- * respectively when the Home or End key is pressed.
- * @param enabled Whether pressing the Home or End key activates the first/last item.
- */
- withHomeAndEnd(enabled: boolean = true): this {
- this._homeAndEnd = enabled;
- return this;
- }
-
- /**
- * Sets the active cell depending on the key event passed in.
- * @param event Keyboard event to be used for determining which element should be active.
- */
- onKeydown(event: KeyboardEvent): void {
- const keyCode = event.keyCode;
-
- switch (keyCode) {
- case DOWN_ARROW:
- this.setNextRowActive();
- break;
-
- case UP_ARROW:
- this.setPreviousRowActive();
- break;
-
- case RIGHT_ARROW:
- this._dir === 'rtl' ? this.setPreviousColumnActive() : this.setNextColumnActive();
- break;
-
- case LEFT_ARROW:
- this._dir === 'rtl' ? this.setNextColumnActive() : this.setPreviousColumnActive();
- break;
-
- case HOME:
- if (this._homeAndEnd) {
- this.setFirstCellActive();
- break;
- } else {
- return;
- }
-
- case END:
- if (this._homeAndEnd) {
- this.setLastCellActive();
- break;
- } else {
- return;
- }
-
- default:
- // Note that we return here, in order to avoid preventing
- // the default action of non-navigational keys.
- return;
- }
-
- event.preventDefault();
- }
-
- /** Index of the currently active row. */
- get activeRowIndex(): number {
- return this._activeRowIndex;
- }
-
- /** Index of the currently active column. */
- get activeColumnIndex(): number {
- return this._activeColumnIndex;
- }
-
- /** The active cell. */
- get activeCell(): T | null {
- return this._activeCell;
- }
-
- /** Sets the active cell to the first cell in the grid. */
- setFirstCellActive(): void {
- this._setActiveCellByIndex(0, 0);
- }
-
- /** Sets the active cell to the last cell in the grid. */
- setLastCellActive(): void {
- const lastRowIndex = this._rows.length - 1;
- const lastRow = this._getRowsArray()[lastRowIndex];
- this._setActiveCellByIndex(lastRowIndex, lastRow.cells.length - 1);
- }
-
- /** Sets the active row to the next row in the grid. Active column is unchanged. */
- setNextRowActive(): void {
- this._activeRowIndex < 0 ? this.setFirstCellActive() : this._setActiveCellByDelta(1, 0);
- }
-
- /** Sets the active row to the previous row in the grid. Active column is unchanged. */
- setPreviousRowActive(): void {
- this._setActiveCellByDelta(-1, 0);
- }
-
- /**
- * Sets the active column to the next column in the grid.
- * Active row is unchanged, unless we reach the end of a row.
- */
- setNextColumnActive(): void {
- this._activeRowIndex < 0 ? this.setFirstCellActive() : this._setActiveCellByDelta(0, 1);
- }
-
- /**
- * Sets the active column to the previous column in the grid.
- * Active row is unchanged, unless we reach the end of a row.
- */
- setPreviousColumnActive(): void {
- this._setActiveCellByDelta(0, -1);
- }
-
- /**
- * Allows setting the active cell without any other effects.
- * @param cell Row and column of the cell to be set as active.
- */
- updateActiveCell(cell: {row: number; column: number}): void;
-
- /**
- * Allows setting the active cell without any other effects.
- * @param cell Cell to be set as active.
- */
- updateActiveCell(cell: T): void;
-
- updateActiveCell(cell: any): void {
- const rowArray = this._getRowsArray();
-
- if (
- typeof cell === 'object' &&
- typeof cell.row === 'number' &&
- typeof cell.column === 'number'
- ) {
- this._activeRowIndex = cell.row;
- this._activeColumnIndex = cell.column;
- this._activeRow = rowArray[cell.row] || null;
- this._activeCell = this._activeRow ? this._activeRow.cells[cell.column] || null : null;
- } else {
- rowArray.forEach((row, rowIndex) => {
- const columnIndex = row.cells.indexOf(cell);
- if (columnIndex !== -1) {
- this._activeRowIndex = rowIndex;
- this._activeColumnIndex = columnIndex;
- this._activeRow = row;
- this._activeCell = row.cells[columnIndex];
- }
- });
- }
- }
-
- /**
- * This method sets the active cell, given the row and columns deltas
- * between the currently active cell and the new active cell.
- */
- private _setActiveCellByDelta(rowDelta: -1 | 0 | 1, columnDelta: -1 | 0 | 1): void {
- // If delta puts us past the last cell in a row, move to the first cell of the next row.
- if (this._activeRow && this._activeColumnIndex + columnDelta >= this._activeRow.cells.length) {
- this._setActiveCellByIndex(this._activeRowIndex + 1, 0);
-
- // If delta puts us prior to the first cell in a row, move to the last cell of the previous row.
- } else if (this._activeColumnIndex + columnDelta < 0) {
- const previousRowIndex = this._activeRowIndex - 1;
- const previousRow = this._getRowsArray()[previousRowIndex];
- if (previousRow) {
- this._setActiveCellByIndex(previousRowIndex, previousRow.cells.length - 1);
- }
- } else {
- this._setActiveCellByIndex(
- this._activeRowIndex + rowDelta,
- this._activeColumnIndex + columnDelta,
- );
- }
- }
-
- /**
- * Sets the active cell to the cell at the indices specified, if they are valid.
- */
- private _setActiveCellByIndex(rowIndex: number, columnIndex: number): void {
- const rows = this._getRowsArray();
-
- const targetRow = rows[rowIndex];
-
- if (!targetRow || !targetRow.cells[columnIndex]) {
- return;
- }
-
- this.setActiveCell({row: rowIndex, column: columnIndex});
- }
-
- /** Returns the rows as an array. */
- private _getRowsArray(): GridKeyManagerRow[] {
- return this._rows instanceof QueryList ? this._rows.toArray() : this._rows;
- }
-}
diff --git a/src/material-experimental/mdc-chips/module.ts b/src/material-experimental/mdc-chips/module.ts
index 7ce8256d6caf..ace98686a98e 100644
--- a/src/material-experimental/mdc-chips/module.ts
+++ b/src/material-experimental/mdc-chips/module.ts
@@ -14,7 +14,7 @@ import {
MatCommonModule,
MatRippleModule,
} from '@angular/material-experimental/mdc-core';
-import {MatChip, MatChipCssInternalOnly} from './chip';
+import {MatChip} from './chip';
import {MAT_CHIPS_DEFAULT_OPTIONS, MatChipsDefaultOptions} from './chip-default-options';
import {MatChipEditInput} from './chip-edit-input';
import {MatChipGrid} from './chip-grid';
@@ -24,11 +24,11 @@ import {MatChipListbox} from './chip-listbox';
import {MatChipRow} from './chip-row';
import {MatChipOption} from './chip-option';
import {MatChipSet} from './chip-set';
+import {MatChipAction} from './chip-action';
const CHIP_DECLARATIONS = [
MatChip,
MatChipAvatar,
- MatChipCssInternalOnly,
MatChipEditInput,
MatChipGrid,
MatChipInput,
@@ -43,7 +43,7 @@ const CHIP_DECLARATIONS = [
@NgModule({
imports: [MatCommonModule, CommonModule, MatRippleModule],
exports: [MatCommonModule, CHIP_DECLARATIONS],
- declarations: CHIP_DECLARATIONS,
+ declarations: [MatChipAction, CHIP_DECLARATIONS],
providers: [
ErrorStateMatcher,
{
diff --git a/src/material-experimental/mdc-chips/testing/chip-harness.ts b/src/material-experimental/mdc-chips/testing/chip-harness.ts
index 735a5ac40aac..fa880ca782ba 100644
--- a/src/material-experimental/mdc-chips/testing/chip-harness.ts
+++ b/src/material-experimental/mdc-chips/testing/chip-harness.ts
@@ -17,6 +17,8 @@ import {MatChipRemoveHarness} from './chip-remove-harness';
/** Harness for interacting with a mat-chip in tests. */
export class MatChipHarness extends ContentContainerComponentHarness {
+ protected _primaryAction = this.locatorFor('.mdc-evolution-chip__action--primary');
+
static hostSelector = '.mat-mdc-basic-chip, .mat-mdc-chip';
/**
diff --git a/src/material-experimental/mdc-chips/testing/chip-option-harness.ts b/src/material-experimental/mdc-chips/testing/chip-option-harness.ts
index c3e73e3485bb..8005a3c7573e 100644
--- a/src/material-experimental/mdc-chips/testing/chip-option-harness.ts
+++ b/src/material-experimental/mdc-chips/testing/chip-option-harness.ts
@@ -56,6 +56,6 @@ export class MatChipOptionHarness extends MatChipHarness {
/** Toggles the selected state of the given chip. */
async toggle(): Promise {
- return (await this.host()).sendKeys(' ');
+ return (await this._primaryAction()).click();
}
}
diff --git a/src/material-experimental/mdc-chips/testing/chip-row-harness.spec.ts b/src/material-experimental/mdc-chips/testing/chip-row-harness.spec.ts
index 67ee6f528ef7..4a497932ad04 100644
--- a/src/material-experimental/mdc-chips/testing/chip-row-harness.spec.ts
+++ b/src/material-experimental/mdc-chips/testing/chip-row-harness.spec.ts
@@ -24,15 +24,25 @@ describe('MatChipRowHarness', () => {
const harnesses = await loader.getAllHarnesses(MatChipRowHarness);
expect(harnesses.length).toBe(2);
});
+
+ it('should get whether the chip is editable', async () => {
+ const harness = await loader.getHarness(MatChipRowHarness);
+ expect(await harness.isEditable()).toBe(false);
+
+ fixture.componentInstance.editable = true;
+ expect(await harness.isEditable()).toBe(true);
+ });
});
@Component({
template: `
- Basic Chip Row
- Chip Row
+ Basic Chip Row
+ Chip Row
`,
})
-class ChipRowHarnessTest {}
+class ChipRowHarnessTest {
+ editable = false;
+}
diff --git a/src/material-experimental/mdc-chips/testing/chip-row-harness.ts b/src/material-experimental/mdc-chips/testing/chip-row-harness.ts
index 216d068847c5..39ed349bd5fe 100644
--- a/src/material-experimental/mdc-chips/testing/chip-row-harness.ts
+++ b/src/material-experimental/mdc-chips/testing/chip-row-harness.ts
@@ -10,6 +10,8 @@ import {HarnessPredicate} from '@angular/cdk/testing';
import {ChipRowHarnessFilters} from './chip-harness-filters';
import {MatChipHarness} from './chip-harness';
+// TODO(crisbeto): add harness for the chip edit input inside the row.
+
/** Harness for interacting with a mat-chip-row in tests. */
export class MatChipRowHarness extends MatChipHarness {
static override hostSelector = '.mat-mdc-chip-row';
@@ -27,4 +29,14 @@ export class MatChipRowHarness extends MatChipHarness {
InstanceType
>;
}
+
+ /** Whether the chip is editable. */
+ async isEditable(): Promise {
+ return (await this.host()).hasClass('mat-mdc-chip-editable');
+ }
+
+ /** Whether the chip is currently being edited. */
+ async isEditing(): Promise {
+ return (await this.host()).hasClass('mat-mdc-chip-editing');
+ }
}
diff --git a/src/material-experimental/mdc-helpers/_focus-indicators.scss b/src/material-experimental/mdc-helpers/_focus-indicators.scss
index 4189d3486265..d15da73f9d70 100644
--- a/src/material-experimental/mdc-helpers/_focus-indicators.scss
+++ b/src/material-experimental/mdc-helpers/_focus-indicators.scss
@@ -38,7 +38,7 @@
.mat-mdc-unelevated-button .mat-mdc-focus-indicator::before,
.mat-mdc-raised-button .mat-mdc-focus-indicator::before,
.mdc-fab .mat-mdc-focus-indicator::before,
- .mat-mdc-focus-indicator.mdc-chip::before {
+ .mat-mdc-chip-action-label .mat-mdc-focus-indicator::before {
margin: -($border-width + 2px);
}
@@ -46,11 +46,16 @@
margin: -($border-width + 3px);
}
- .mat-mdc-focus-indicator.mat-mdc-chip-remove::before,
- .mat-mdc-focus-indicator.mat-mdc-chip-row-focusable-text-content::before {
+ .mat-mdc-focus-indicator.mat-mdc-chip-remove::before {
margin: -$border-width;
}
+ // MDC sets a padding a on the button which stretches out the focus indicator.
+ .mat-mdc-focus-indicator.mat-mdc-chip-remove::before {
+ left: 8px;
+ right: 8px;
+ }
+
.mat-mdc-focus-indicator.mat-mdc-tab::before,
.mat-mdc-focus-indicator.mat-mdc-tab-link::before {
margin: 5px;
@@ -74,6 +79,9 @@
.mat-mdc-slide-toggle-focused .mat-mdc-focus-indicator::before,
.mat-mdc-radio-button.cdk-focused .mat-mdc-focus-indicator::before,
+ // In the chips the individual actions have focus so we target a different element.
+ .mat-mdc-chip-action:focus .mat-mdc-focus-indicator::before,
+
// For buttons and list items, render the focus indicator when the parent
// button or list item is focused.
.mat-mdc-button-base:focus .mat-mdc-focus-indicator::before,