-
+
+
diff --git a/src/material-experimental/mdc-chips/chip-row.ts b/src/material-experimental/mdc-chips/chip-row.ts
index b3904c24a643..73b81bb68d4c 100644
--- a/src/material-experimental/mdc-chips/chip-row.ts
+++ b/src/material-experimental/mdc-chips/chip-row.ts
@@ -6,9 +6,19 @@
* found in the LICENSE file at https://angular.io/license
*/
-import {ChangeDetectionStrategy, Component, ViewEncapsulation} from '@angular/core';
import {BACKSPACE, DELETE} from '@angular/cdk/keycodes';
+import {
+ AfterContentInit,
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ Component,
+ ElementRef,
+ ViewChild,
+ ViewEncapsulation
+} from '@angular/core';
import {MatChip} from './chip';
+import {GridKeyManagerRow, NAVIGATION_KEYS} from './grid-key-manager';
+
/**
* An extension of the MatChip component used with MatChipGrid and
@@ -19,36 +29,110 @@ import {MatChip} from './chip';
selector: 'mat-chip-row, mat-basic-chip-row',
templateUrl: 'chip-row.html',
styleUrls: ['chips.css'],
- inputs: ['color', 'disabled', 'disableRipple'],
+ inputs: ['color', 'disableRipple', 'tabIndex'],
host: {
'role': 'row',
'[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',
+ '[class.mat-mdc-chip-with-trailing-icon]': 'trailingIcon || removeIcon',
'[id]': 'id',
- '[tabIndex]': 'disabled ? null : -1',
'[attr.disabled]': 'disabled || null',
'[attr.aria-disabled]': 'disabled.toString()',
- '(click)': '_handleClick($event)',
- '(keydown)': '_handleKeydown($event)',
- '(focus)': 'focus()',
- '(blur)': '_blur()',
- '(transitionend)': '_chipFoundation.handleTransitionEnd($event)'
+ '[tabIndex]': 'tabIndex',
+ '(click)': '_click($event)',
+ '(keydown)': '_keydown($event)',
+ '(transitionend)': '_chipFoundation.handleTransitionEnd($event)',
+ '(focusin)': '_focusin()',
+ '(focusout)': '_focusout()'
},
providers: [{provide: MatChip, useExisting: MatChipRow}],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class MatChipRow extends MatChip {
+export class MatChipRow extends MatChip implements AfterContentInit, AfterViewInit,
+ GridKeyManagerRow
{
protected basicChipAttrName = 'mat-basic-chip-row';
- /** Handle custom key presses. */
- _handleKeydown(event: KeyboardEvent): void {
+ /**
+ * The focusable wrapper element in the first gridcell, which contains all
+ * chip content other than the remove icon.
+ */
+ @ViewChild('chipContent', {static: false}) chipContent: ElementRef;
+
+ /** The focusable grid cells for this row. Implemented as part of GridKeyManagerRow. */
+ cells!: HTMLElement[];
+
+ /** Key codes for which this component has a custom handler. */
+ HANDLED_KEYS = NAVIGATION_KEYS.concat([BACKSPACE, DELETE]);
+
+ ngAfterContentInit() {
+ super.ngAfterContentInit();
+
+ if (this.removeIcon) {
+ // removeIcon has tabIndex 0 for regular chips, but should only be focusable by
+ // the GridFocusKeyManager for row chips.
+ this.removeIcon.tabIndex = -1;
+ }
+ }
+
+ 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;
}
+ this.chipContent.nativeElement.focus();
+ this._hasFocusInternal = true;
+ this._onFocus.next({chip: this});
+ }
+
+ /**
+ * Emits a blur event when one of the gridcells loses focus, unless focus moved
+ * to the other gridcell.
+ */
+ _focusout() {
+ this._hasFocusInternal = false;
+ // Wait to see if focus moves to the other gridcell
+ setTimeout(() => {
+ if (this._hasFocus) {
+ return;
+ }
+ this._onBlur.next({chip: this});
+ });
+ }
+
+ /** Records that the chip has focus when one of the gridcells is focused. */
+ _focusin() {
+ this._hasFocusInternal = true;
+ }
+
+ /** Sends focus to the first gridcell when the user clicks anywhere inside the chip. */
+ _click(event: MouseEvent) {
+ if (this.disabled) {
+ event.preventDefault();
+ } else {
+ this.focus();
+ event.stopPropagation();
+ }
+ }
+
+ /** Handles custom key presses. */
+ _keydown(event: KeyboardEvent): void {
+ if (this.disabled) {
+ return;
+ }
switch (event.keyCode) {
case DELETE:
case BACKSPACE:
diff --git a/src/material-experimental/mdc-chips/chip-set.ts b/src/material-experimental/mdc-chips/chip-set.ts
index 450cfc6373d4..7cbceec71e10 100644
--- a/src/material-experimental/mdc-chips/chip-set.ts
+++ b/src/material-experimental/mdc-chips/chip-set.ts
@@ -24,9 +24,27 @@ import {MDCChipSetAdapter, MDCChipSetFoundation} from '@material/chips';
import {MatChip, MatChipEvent} from './chip';
import {merge, Observable, Subject, Subscription} from 'rxjs';
import {startWith, takeUntil} from 'rxjs/operators';
+import {
+ HasTabIndex,
+ HasTabIndexCtor,
+ mixinTabIndex,
+} from '@angular/material/core';
+
let uid = 0;
+
+/**
+ * Boilerplate for applying mixins to MatChipSet.
+ * @docs-private
+ */
+class MatChipSetBase {
+ disabled!: boolean;
+ constructor(_elementRef: ElementRef) {}
+}
+const _MatChipSetMixinBase: HasTabIndexCtor & typeof MatChipSetBase =
+ mixinTabIndex(MatChipSetBase);
+
/**
* Basic container component for the MatChip component.
*
@@ -47,7 +65,8 @@ let uid = 0;
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
-export class MatChipSet implements AfterContentInit, AfterViewInit, OnDestroy {
+export class MatChipSet extends _MatChipSetMixinBase implements AfterContentInit, AfterViewInit,
+ HasTabIndex, OnDestroy {
/** Subscription to remove changes in chips. */
private _chipRemoveSubscription: Subscription | null;
@@ -122,6 +141,7 @@ export class MatChipSet implements AfterContentInit, AfterViewInit, OnDestroy {
constructor(protected _elementRef: ElementRef,
protected _changeDetectorRef: ChangeDetectorRef) {
+ super(_elementRef);
this._chipSetFoundation = new MDCChipSetFoundation(this._chipSetAdapter);
}
diff --git a/src/material-experimental/mdc-chips/chip.ts b/src/material-experimental/mdc-chips/chip.ts
index 020c062964e3..1a82fad60d82 100644
--- a/src/material-experimental/mdc-chips/chip.ts
+++ b/src/material-experimental/mdc-chips/chip.ts
@@ -27,13 +27,13 @@ import {
import {
CanColor,
CanColorCtor,
- CanDisable,
- CanDisableCtor,
CanDisableRipple,
CanDisableRippleCtor,
+ HasTabIndex,
+ HasTabIndexCtor,
mixinColor,
- mixinDisabled,
mixinDisableRipple,
+ mixinTabIndex,
RippleConfig,
RippleRenderer,
RippleTarget,
@@ -41,6 +41,7 @@ import {
import {MDCChipAdapter, MDCChipFoundation} from '@material/chips';
import {Subject} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
+import {MatChipAvatar, MatChipTrailingIcon, MatChipRemove} from './chip-icons';
let uid = 0;
@@ -51,79 +52,6 @@ export interface MatChipEvent {
chip: MatChip;
}
-/**
- * Directive to add CSS classes to chip leading icon.
- * @docs-private
- */
-@Directive({
- selector: 'mat-chip-avatar, [matChipAvatar]',
- host: {
- 'class': 'mat-mdc-chip-avatar mdc-chip__icon mdc-chip__icon--leading',
- 'role': 'img'
- }
-})
-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) {
- const element = this._elementRef.nativeElement;
- active ? element.addClass(cssClass) : element.removeClass(cssClass);
- this._changeDetectorRef.markForCheck();
- }
-}
-
-/**
- * Directive to add CSS class to chip trailing icon and notify parent chip
- * about trailing icon interactions.
- *
- * If matChipRemove is used to add this directive, the parent chip will be
- * removed when the trailing icon is clicked.
- *
- * @docs-private
- */
-@Directive({
- selector: 'mat-chip-trailing-icon, [matChipTrailingIcon], [matChipRemove]',
- host: {
- 'class': 'mat-mdc-chip-trailing-icon mdc-chip__icon mdc-chip__icon--trailing',
- '[tabIndex]': 'tabIndex',
- '[attr.aria-hidden]': '!shouldRemove',
- '[attr.role]': 'shouldRemove ? "button" : null',
- '(click)': 'interaction.emit($event)',
- '(keydown)': 'interaction.emit($event)',
- '(blur)': 'parentChip ? parentChip._blur() : {}',
- '(focus)': 'parentChip ? parentChip._hasFocus = true : {}'
- }
-})
-export class MatChipTrailingIcon {
- /** Whether interaction with this icon should remove the parent chip. */
- shouldRemove!: boolean;
-
- /** The MatChip component associated with this icon. */
- @Input() parentChip?: MatChip;
-
- /** The tab index for this icon. */
- get tabIndex(): number|null {
- if ((this.parentChip && this.parentChip.disabled) || !this.shouldRemove) {
- return -1;
- }
- return 0;
- }
-
- /** Emits when the user interacts with the icon. */
- @Output() interaction = new EventEmitter();
-
- constructor(private _elementRef: ElementRef) {
- this.shouldRemove = this._isMatChipRemoveIcon();
- }
-
- /** Returns true if the icon was created with the matChipRemove directive. */
- _isMatChipRemoveIcon(): boolean {
- return this._elementRef.nativeElement.getAttribute('matChipRemove') !== null;
- }
-}
-
/**
* Directive to add MDC CSS to non-basic chips.
* @docs-private
@@ -140,11 +68,16 @@ export class MatChipCssInternalOnly { }
* @docs-private
*/
class MatChipBase {
+ disabled!: boolean;
constructor(public _elementRef: ElementRef) {}
}
-const _MatChipMixinBase: CanColorCtor & CanDisableRippleCtor & CanDisableCtor & typeof MatChipBase =
- mixinColor(mixinDisableRipple(mixinDisabled(MatChipBase)), 'primary');
+const _MatChipMixinBase:
+ CanColorCtor &
+ CanDisableRippleCtor &
+ HasTabIndexCtor &
+ typeof MatChipBase =
+ mixinTabIndex(mixinColor(mixinDisableRipple(MatChipBase), 'primary'), -1);
/**
* Material design styled Chip base component. Used inside the MatChipSet component.
@@ -154,7 +87,7 @@ const _MatChipMixinBase: CanColorCtor & CanDisableRippleCtor & CanDisableCtor &
@Component({
moduleId: module.id,
selector: 'mat-basic-chip, mat-chip',
- inputs: ['color', 'disabled', 'disableRipple'],
+ inputs: ['color', 'disableRipple'],
exportAs: 'matChip',
templateUrl: 'chip.html',
styleUrls: ['chips.css'],
@@ -162,7 +95,7 @@ const _MatChipMixinBase: CanColorCtor & CanDisableRippleCtor & CanDisableCtor &
'[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',
+ '[class.mat-mdc-chip-with-trailing-icon]': 'trailingIcon || removeIcon',
'[id]': 'id',
'[attr.disabled]': 'disabled || null',
'[attr.aria-disabled]': 'disabled.toString()',
@@ -172,15 +105,21 @@ const _MatChipMixinBase: CanColorCtor & CanDisableRippleCtor & CanDisableCtor &
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MatChip extends _MatChipMixinBase implements AfterContentInit, AfterViewInit,
- CanColor, CanDisable, CanDisableRipple, RippleTarget, OnDestroy {
+ CanColor, CanDisableRipple, HasTabIndex, RippleTarget, OnDestroy {
/** Emits when the chip is focused. */
readonly _onFocus = new Subject();
/** Emits when the chip is blurred. */
readonly _onBlur = new Subject();
+ readonly HANDLED_KEYS: number[] = [];
+
/** Whether the chip has focus. */
- _hasFocus: boolean = false;
+ protected _hasFocusInternal = false;
+
+ get _hasFocus() {
+ return this._hasFocusInternal;
+ }
/** Default unique id for the chip. */
private _uniqueId = `mat-mdc-chip-${uid++}`;
@@ -188,6 +127,18 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
/** A unique id for the chip. If none is supplied, it will be auto-generated. */
@Input() id: string = this._uniqueId;
+
+ @Input()
+ get disabled(): boolean { return this._disabled; }
+ set disabled(value: boolean) {
+ this._disabled = coerceBooleanProperty(value);
+ if (this.removeIcon) {
+ this.removeIcon.disabled = value;
+ }
+ }
+ protected _disabled: boolean = false;
+
+
/** The value of the chip. Defaults to the content inside `` tags. */
@Input()
get value(): any {
@@ -218,8 +169,8 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
}
protected _highlighted: boolean = false;
- /** Emitted when the user interacts with the trailing icon. */
- @Output() trailingIconInteraction = new EventEmitter();
+ /** Emitted when the user interacts with the remove icon. */
+ @Output() removeIconInteraction = new EventEmitter();
/** Emitted when the user interacts with the chip. */
@Output() interaction = new EventEmitter();
@@ -237,7 +188,7 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
protected basicChipAttrName = 'mat-basic-chip';
/** Subject that emits when the component has been destroyed. */
- private _destroyed = new Subject();
+ protected _destroyed = new Subject();
/** The ripple renderer for this chip. */
private _rippleRenderer: RippleRenderer;
@@ -267,6 +218,9 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
/** The chip's trailing icon. */
@ContentChild(MatChipTrailingIcon, {static: false}) trailingIcon: MatChipTrailingIcon;
+ /** The chip's trailing remove icon. */
+ @ContentChild(MatChipRemove, {static: false}) removeIcon: MatChipRemove;
+
/**
* Implementation of the MDC chip adapter interface.
* These methods are called by the chip foundation.
@@ -285,7 +239,7 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
// No-op. We call dispatchSelectionEvent ourselves in MatChipOption, because we want to
// specify whether selection occurred via user input.
},
- notifyTrailingIconInteraction: () => this.trailingIconInteraction.emit(this.id),
+ notifyTrailingIconInteraction: () => this.removeIconInteraction.emit(this.id),
notifyRemoval: () => this.removed.emit({chip: this}),
getComputedStyleValue: (propertyName) => {
return window.getComputedStyle(this._elementRef.nativeElement).getPropertyValue(propertyName);
@@ -310,7 +264,7 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
}
ngAfterContentInit() {
- this._initTrailingIcon();
+ this._initRemoveIcon();
}
ngAfterViewInit() {
@@ -326,58 +280,33 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
this._chipFoundation.destroy();
}
- /** 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._hasFocus = true;
- }
-
- /** Resets the state of the chip when it loses focus. */
- _blur(): void {
- this._hasFocus = false;
- this._onBlur.next({chip: this});
- }
-
- /** Handles click events on the chip. */
- _handleClick(event: MouseEvent) {
- if (this.disabled) {
- event.preventDefault();
- } else {
- this._handleInteraction(event);
- event.stopPropagation();
- }
- }
-
- /** Registers this chip with the trailing icon, and subscribes to trailing icon events. */
- _initTrailingIcon() {
- if (this.trailingIcon) {
- this.trailingIcon.parentChip = this;
- this._chipFoundation.setShouldRemoveOnTrailingIconClick(this.trailingIcon.shouldRemove);
- this._listenToTrailingIconInteraction();
+ /** Sets up the remove icon chip foundation, and subscribes to remove icon events. */
+ _initRemoveIcon() {
+ if (this.removeIcon) {
+ this._chipFoundation.setShouldRemoveOnTrailingIconClick(true);
+ this._listenToRemoveIconInteraction();
+ this.removeIcon.disabled = this.disabled;
}
}
- /** Handles interaction with the trailing icon. */
- _listenToTrailingIconInteraction() {
- this.trailingIcon.interaction
+ /** Handles interaction with the remove icon. */
+ _listenToRemoveIconInteraction() {
+ this.removeIcon.interaction
.pipe(takeUntil(this._destroyed))
.subscribe((event) => {
- if (!this.disabled) {
- this._chipFoundation.handleTrailingIconInteraction(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.
+ if (this.disabled || (event instanceof KeyboardEvent &&
+ this.HANDLED_KEYS.indexOf(event.keyCode) !== -1)) {
+ return;
}
+ this._chipFoundation.handleTrailingIconInteraction(event);
});
}
/**
- * Allows for programmatic removal of the chip. Called when the DELETE or BACKSPACE
- * keys are pressed.
+ * Allows for programmatic removal of the chip.
*
* Informs any listeners of the removal request. Does not remove the chip from the DOM.
*/
diff --git a/src/material-experimental/mdc-chips/chips.scss b/src/material-experimental/mdc-chips/chips.scss
index d3dcce094bf8..24f53f5632ec 100644
--- a/src/material-experimental/mdc-chips/chips.scss
+++ b/src/material-experimental/mdc-chips/chips.scss
@@ -9,8 +9,8 @@
// We currently don't use the MDC ripple due to size concerns, therefore we need to add some
// additional styles to restore these states.
.mat-mdc-chip:not(.mat-mdc-chip-disabled) {
- &:hover, &:focus {
- ::after {
+ &:hover, &:focus, div:focus {
+ .mdc-chip__text::after {
content: '';
position: absolute;
width: 100%;
@@ -28,7 +28,7 @@
.mat-mdc-chip-disabled.mat-mdc-chip {
opacity: 0.4;
- .mat-mdc-chip-trailing-icon {
+ .mat-mdc-chip-trailing-icon, .mat-chip-row-focusable-text-content {
pointer-events: none;
}
}
diff --git a/src/material-experimental/mdc-chips/grid-focus-key-manager.ts b/src/material-experimental/mdc-chips/grid-focus-key-manager.ts
new file mode 100644
index 000000000000..83c36c68fb64
--- /dev/null
+++ b/src/material-experimental/mdc-chips/grid-focus-key-manager.ts
@@ -0,0 +1,36 @@
+/**
+ * @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.
+ */
+ 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.
+ */
+ setActiveCell(cell: HTMLElement): void;
+
+ 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
new file mode 100644
index 000000000000..1fe9fd5ede28
--- /dev/null
+++ b/src/material-experimental/mdc-chips/grid-key-manager.ts
@@ -0,0 +1,257 @@
+/**
+ * @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,
+} 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';
+
+ 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});
+ }
+ }
+
+ /**
+ * 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;
+
+ 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 2bc2eaaa8791..c1b8b20e5b92 100644
--- a/src/material-experimental/mdc-chips/module.ts
+++ b/src/material-experimental/mdc-chips/module.ts
@@ -10,42 +10,35 @@ import {ENTER} from '@angular/cdk/keycodes';
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {ErrorStateMatcher, MatCommonModule} from '@angular/material/core';
-import {MatChip, MatChipAvatar, MatChipCssInternalOnly, MatChipTrailingIcon} from './chip';
-import {MatChipRow} from './chip-row';
-import {MatChipOption} from './chip-option';
+import {MatChip, MatChipCssInternalOnly} from './chip';
import {MAT_CHIPS_DEFAULT_OPTIONS, MatChipsDefaultOptions} from './chip-default-options';
-import {MatChipInput} from './chip-input';
-import {MatChipSet} from './chip-set';
import {MatChipGrid} from './chip-grid';
+import {MatChipAvatar, MatChipRemove, MatChipTrailingIcon} from './chip-icons';
+import {MatChipInput} from './chip-input';
import {MatChipListbox} from './chip-listbox';
+import {MatChipRow} from './chip-row';
+import {MatChipOption} from './chip-option';
+import {MatChipSet} from './chip-set';
+
+
+const CHIP_DECLARATIONS = [
+ MatChip,
+ MatChipAvatar,
+ MatChipCssInternalOnly,
+ MatChipGrid,
+ MatChipInput,
+ MatChipListbox,
+ MatChipOption,
+ MatChipRemove,
+ MatChipRow,
+ MatChipSet,
+ MatChipTrailingIcon,
+];
@NgModule({
imports: [MatCommonModule, CommonModule],
- exports: [
- MatChip,
- MatChipAvatar,
- MatChipCssInternalOnly,
- MatChipGrid,
- MatChipInput,
- MatChipListbox,
- MatChipOption,
- MatChipRow,
- MatChipSet,
- MatChipTrailingIcon,
- MatCommonModule
- ],
- declarations: [
- MatChip,
- MatChipAvatar,
- MatChipCssInternalOnly,
- MatChipGrid,
- MatChipInput,
- MatChipListbox,
- MatChipOption,
- MatChipRow,
- MatChipSet,
- MatChipTrailingIcon
- ],
+ exports: CHIP_DECLARATIONS,
+ declarations: CHIP_DECLARATIONS,
providers: [
ErrorStateMatcher,
{
diff --git a/src/material-experimental/mdc-chips/public-api.ts b/src/material-experimental/mdc-chips/public-api.ts
index dab8fb850ee8..e0a655f92639 100644
--- a/src/material-experimental/mdc-chips/public-api.ts
+++ b/src/material-experimental/mdc-chips/public-api.ts
@@ -15,3 +15,4 @@ export * from './chip-grid';
export * from './module';
export * from './chip-input';
export * from './chip-default-options';
+export * from './chip-icons';