diff --git a/src/material-experimental/mdc-chips/chip-default-options.ts b/src/material-experimental/mdc-chips/chip-default-options.ts index 2cd5b2838922..4e48aec5cdd5 100644 --- a/src/material-experimental/mdc-chips/chip-default-options.ts +++ b/src/material-experimental/mdc-chips/chip-default-options.ts @@ -8,6 +8,7 @@ import {InjectionToken} from '@angular/core'; + /** Default options, for the chips module, that can be overridden. */ export interface MatChipsDefaultOptions { /** The list of key codes that will trigger a chipEnd event. */ diff --git a/src/material-experimental/mdc-chips/chip-grid.ts b/src/material-experimental/mdc-chips/chip-grid.ts index 018673077f2a..cf7e3f13c988 100644 --- a/src/material-experimental/mdc-chips/chip-grid.ts +++ b/src/material-experimental/mdc-chips/chip-grid.ts @@ -6,14 +6,16 @@ * found in the LICENSE file at https://angular.io/license */ +import {Directionality} from '@angular/cdk/bidi'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; -import {BACKSPACE} from '@angular/cdk/keycodes'; +import {BACKSPACE, TAB} from '@angular/cdk/keycodes'; import { AfterContentInit, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, + ContentChildren, DoCheck, ElementRef, EventEmitter, @@ -21,6 +23,7 @@ import { OnDestroy, Optional, Output, + QueryList, Self, ViewEncapsulation } from '@angular/core'; @@ -35,9 +38,10 @@ import {MatFormFieldControl} from '@angular/material/form-field'; import {MatChipTextControl} from './chip-text-control'; import {merge, Observable, Subscription} 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. */ @@ -76,10 +80,11 @@ const _MatChipGridMixinBase: CanUpdateErrorStateCtor & typeof MatChipGridBase = selector: 'mat-chip-grid', template: '', styleUrls: ['chips.css'], + inputs: ['tabIndex'], host: { 'class': 'mat-mdc-chip-set mat-mdc-chip-grid mdc-chip-set', 'role': 'grid', - '[tabIndex]': 'empty ? -1 : 0', + '[tabIndex]': 'tabIndex', // TODO: replace this binding with use of AriaDescriber '[attr.aria-describedby]': '_ariaDescribedby || null', '[attr.aria-required]': 'required.toString()', @@ -88,6 +93,9 @@ const _MatChipGridMixinBase: CanUpdateErrorStateCtor & typeof MatChipGridBase = '[class.mat-mdc-chip-list-disabled]': 'disabled', '[class.mat-mdc-chip-list-invalid]': 'errorState', '[class.mat-mdc-chip-list-required]': 'required', + '(focus)': 'focus()', + '(blur)': '_blur()', + '(keydown)': '_keydown($event)', '[id]': '_uid', }, providers: [{provide: MatFormFieldControl, useExisting: MatChipGrid}], @@ -105,6 +113,9 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn /** Subscription to blur changes in the chips. */ private _chipBlurSubscription: Subscription | null; + /** Subscription to focus changes in the chips. */ + private _chipFocusSubscription: Subscription | null; + /** The chip input to add more chips */ protected _chipInput: MatChipTextControl; @@ -120,6 +131,9 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn */ _onChange: (value: any) => void = () => {}; + /** The GridFocusKeyManager which handles focus. */ + _keyManager: GridFocusKeyManager; + /** * Implemented as part of MatFormFieldControl. * @docs-private @@ -187,6 +201,11 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn return merge(...this._chips.map(chip => chip._onBlur)); } + /** Combined stream of all of the child chips' focus events. */ + get chipFocusChanges(): Observable { + return merge(...this._chips.map(chip => chip._onFocus)); + } + /** Emits when the chip grid value has been changed by the user. */ @Output() readonly change: EventEmitter = new EventEmitter(); @@ -198,8 +217,16 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn */ @Output() readonly valueChange: EventEmitter = new EventEmitter(); + @ContentChildren(MatChipRow, { + // We need to use `descendants: true`, because Ivy will no longer match + // indirect descendants if it's left as false. + descendants: true + }) + _rowChips: QueryList; + constructor(_elementRef: ElementRef, _changeDetectorRef: ChangeDetectorRef, + @Optional() private _dir: Directionality, @Optional() _parentForm: NgForm, @Optional() _parentFormGroup: FormGroupDirective, _defaultErrorStateMatcher: ErrorStateMatcher, @@ -214,7 +241,11 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn ngAfterContentInit() { super.ngAfterContentInit(); + this._initKeyManager(); + this._chips.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => { + this._updateTabIndex(); + // Check to see if we have a destroyed chip and need to refocus this._updateFocusForDestroyedChips(); @@ -269,7 +300,7 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn } if (this._chips.length > 0) { - this._chips.toArray()[0].focus(); + this._keyManager.setFirstCellActive(); } else { this._focusInput(); } @@ -320,6 +351,7 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn // 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(); } @@ -332,7 +364,18 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn * it back to the first chip, creating a focus trap, if it user tries to tab away. */ _allowFocusEscape() { - // TODO + if (this._chipInput.focused) { + return; + } + + if (this.tabIndex !== -1) { + this.tabIndex = -1; + + setTimeout(() => { + this.tabIndex = 0; + this._changeDetectorRef.markForCheck(); + }); + } } /** Handles custom keyboard events. */ @@ -342,11 +385,15 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn // If they are on an empty input and hit backspace, focus the last chip if (event.keyCode === BACKSPACE && this._isEmptyInput(target)) { if (this._chips.length) { - this._chips.toArray()[this._chips.length - 1].focus(); + this._keyManager.setLastCellActive(); } event.preventDefault(); + } else if (event.keyCode === TAB) { + this._allowFocusEscape(); + } else { + this._keyManager.onKeydown(event); } - this.stateChanges.next(); + this.stateChanges.next(); } /** Unsubscribes from all chip events. */ @@ -356,14 +403,43 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn this._chipBlurSubscription.unsubscribe(); this._chipBlurSubscription = null; } + + if (this._chipFocusSubscription) { + this._chipFocusSubscription.unsubscribe(); + this._chipFocusSubscription = null; + } } /** Subscribes to events on the child chips. */ protected _subscribeToChipEvents() { super._subscribeToChipEvents(); + this._listenToChipsFocus(); this._listenToChipsBlur(); } + /** Initializes the key manager to manage focus. */ + private _initKeyManager() { + this._keyManager = new GridFocusKeyManager(this._rowChips) + .withDirectionality(this._dir ? this._dir.value : 'ltr'); + + if (this._dir) { + this._dir.change + .pipe(takeUntil(this._destroyed)) + .subscribe(dir => this._keyManager.withDirectionality(dir)); + } + } + + /** Subscribes to chip focus events. */ + private _listenToChipsFocus(): void { + this._chipFocusSubscription = this.chipFocusChanges.subscribe((event: MatChipEvent) => { + let chipIndex: number = this._chips.toArray().indexOf(event.chip); + + if (this._isValidIndex(chipIndex)) { + this._keyManager.updateActiveCell({row: chipIndex, column: 0}); + } + }); + } + /** Subscribes to chip blur events. */ private _listenToChipsBlur(): void { this._chipBlurSubscription = this.chipBlurChanges.subscribe(() => { @@ -409,17 +485,23 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn * If the amount of chips changed, we need to focus the next closest chip. */ private _updateFocusForDestroyedChips() { + // Wait for chips to be updated in keyManager + setTimeout(() => { // Move focus to the closest chip. If no other chips remain, focus the chip-grid itself. if (this._lastDestroyedChipIndex != null) { if (this._chips.length) { const newChipIndex = Math.min(this._lastDestroyedChipIndex, this._chips.length - 1); - this._chips.toArray()[newChipIndex].focus(); + this._keyManager.setActiveCell({ + row: newChipIndex, + column: this._keyManager.activeColumnIndex + }); } else { this.focus(); } } this._lastDestroyedChipIndex = null; + }); } /** Focus input element. */ @@ -436,4 +518,12 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn return false; } + + /** + * Check the tab index as you should not be allowed to focus an empty grid. + */ + protected _updateTabIndex(): void { + // If we have 0 chips, we should not allow keyboard focus + this.tabIndex = this._chips.length === 0 ? -1 : 0; + } } diff --git a/src/material-experimental/mdc-chips/chip-icons.ts b/src/material-experimental/mdc-chips/chip-icons.ts new file mode 100644 index 000000000000..fbec9c5c9c2d --- /dev/null +++ b/src/material-experimental/mdc-chips/chip-icons.ts @@ -0,0 +1,113 @@ +/** + * @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 { + ChangeDetectorRef, + Directive, + ElementRef, +} from '@angular/core'; +import { + CanDisable, + CanDisableCtor, + HasTabIndex, + HasTabIndexCtor, + mixinDisabled, + mixinTabIndex, +} from '@angular/material/core'; +import {Subject} from 'rxjs'; + + +/** + * 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 classes to and configure attributes for chip trailing icon. + * @docs-private + */ +@Directive({ + selector: 'mat-chip-trailing-icon, [matChipTrailingIcon]', + host: { + 'class': 'mat-mdc-chip-trailing-icon mdc-chip__icon mdc-chip__icon--trailing', + 'tabindex': '-1', + 'aria-hidden': 'true', + } +}) +export class MatChipTrailingIcon { +} + +/** + * Boilerplate for applying mixins to MatChipRemove. + * @docs-private + */ +class MatChipRemoveBase extends MatChipTrailingIcon { + constructor(public _elementRef: ElementRef) { + super(); + } +} + +const _MatChipRemoveMixinBase: + CanDisableCtor & + HasTabIndexCtor & + typeof MatChipRemoveBase = + mixinTabIndex(mixinDisabled(MatChipRemoveBase)); + +/** + * Directive to remove the parent chip when the trailing icon is clicked or + * when the ENTER key is pressed on it. + * + * Recommended for use with the Material Design "cancel" icon + * available at https://material.io/icons/#ic_cancel. + * + * Example: + * + * ` + * cancel + * ` + */ +@Directive({ + selector: '[matChipRemove]', + inputs: ['disabled', 'tabIndex'], + host: { + 'class': 'mat-mdc-chip-trailing-icon mdc-chip__icon mdc-chip__icon--trailing', + '[tabIndex]': 'tabIndex', + 'role': 'button', + '(click)': 'interaction.next($event)', + '(keydown)': 'interaction.next($event)', + } +}) +export class MatChipRemove extends _MatChipRemoveMixinBase implements CanDisable, HasTabIndex { + /** + * Emits when the user interacts with the icon. + * @docs-private + */ + interaction: Subject = new Subject(); + + constructor(_elementRef: ElementRef) { + super(_elementRef); + } +} diff --git a/src/material-experimental/mdc-chips/chip-input.ts b/src/material-experimental/mdc-chips/chip-input.ts index cbfc6b3241ea..b481364ff794 100644 --- a/src/material-experimental/mdc-chips/chip-input.ts +++ b/src/material-experimental/mdc-chips/chip-input.ts @@ -13,6 +13,7 @@ import {MAT_CHIPS_DEFAULT_OPTIONS, MatChipsDefaultOptions} from './chip-default- import {MatChipGrid} from './chip-grid'; import {MatChipTextControl} from './chip-text-control'; + /** Represents an input event on a `matChipInput`. */ export interface MatChipInputEvent { /** The native `` element that the event is being fired for. */ diff --git a/src/material-experimental/mdc-chips/chip-listbox.ts b/src/material-experimental/mdc-chips/chip-listbox.ts index 84f5a268909f..5744dc2c0b7f 100644 --- a/src/material-experimental/mdc-chips/chip-listbox.ts +++ b/src/material-experimental/mdc-chips/chip-listbox.ts @@ -7,6 +7,7 @@ */ import {FocusKeyManager} from '@angular/cdk/a11y'; +import {Directionality} from '@angular/cdk/bidi'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; import {HOME, END} from '@angular/cdk/keycodes'; import { @@ -19,6 +20,7 @@ import { EventEmitter, forwardRef, Input, + Optional, Output, QueryList, ViewEncapsulation @@ -27,7 +29,6 @@ import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; import {MDCChipSetAdapter, MDCChipSetFoundation} from '@material/chips'; import {merge, Observable, Subscription} from 'rxjs'; import {startWith, takeUntil} from 'rxjs/operators'; - import {MatChip, MatChipEvent} from './chip'; import {MatChipOption, MatChipSelectionChange} from './chip-option'; import {MatChipSet} from './chip-set'; @@ -62,15 +63,17 @@ export const MAT_CHIP_LISTBOX_CONTROL_VALUE_ACCESSOR: any = { selector: 'mat-chip-listbox', template: '', styleUrls: ['chips.css'], + inputs: ['tabIndex'], host: { 'class': 'mat-mdc-chip-set mat-mdc-chip-listbox mdc-chip-set', 'role': 'listbox', - '[tabIndex]': 'disabled ? null : _tabIndex', + '[tabIndex]': 'tabIndex', // TODO: replace this binding with use of AriaDescriber '[attr.aria-describedby]': '_ariaDescribedby || null', '[attr.aria-required]': 'required.toString()', '[attr.aria-disabled]': 'disabled.toString()', '[attr.aria-multiselectable]': 'multiple', + '[attr.aria-orientation]': 'ariaOrientation', '[class.mat-mdc-chip-list-disabled]': 'disabled', '[class.mat-mdc-chip-list-required]': 'required', '(focus)': 'focus()', @@ -109,9 +112,6 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont } }; - /** Tab index for the chip list. */ - _tabIndex = 0; - /** The FocusKeyManager which handles focus. */ _keyManager: FocusKeyManager; @@ -143,6 +143,9 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont return this.multiple ? selectedChips : selectedChips[0]; } + /** Orientation of the chip list. */ + @Input('aria-orientation') ariaOrientation: 'horizontal' | 'vertical' = 'horizontal'; + /** * Whether or not this chip listbox is selectable. * @@ -216,7 +219,8 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont _optionChips: QueryList; constructor(protected _elementRef: ElementRef, - _changeDetectorRef: ChangeDetectorRef) { + _changeDetectorRef: ChangeDetectorRef, + @Optional() private _dir: Directionality) { super(_elementRef, _changeDetectorRef); // Reinitialize the foundation with our overridden adapter this._chipSetFoundation = new MDCChipSetFoundation(this._chipSetAdapter); @@ -336,11 +340,11 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont * it back to the first chip, creating a focus trap, if it user tries to tab away. */ _allowFocusEscape() { - if (this._tabIndex !== -1) { - this._tabIndex = -1; + if (this.tabIndex !== -1) { + this.tabIndex = -1; setTimeout(() => { - this._tabIndex = 0; + this.tabIndex = 0; this._changeDetectorRef.markForCheck(); }); } @@ -442,10 +446,16 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont /** Initializes the key manager to manage focus. */ private _initKeyManager() { - this._keyManager = new FocusKeyManager(this._chips) + this._keyManager = new FocusKeyManager(this._optionChips) .withWrap() .withVerticalOrientation() - .withHorizontalOrientation('ltr'); + .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(); @@ -523,7 +533,7 @@ export class MatChipListbox extends MatChipSet implements AfterContentInit, Cont */ protected _updateTabIndex(): void { // If we have 0 chips, we should not allow keyboard focus - this._tabIndex = this._chips.length === 0 ? -1 : 0; + this.tabIndex = this._chips.length === 0 ? -1 : 0; } /** diff --git a/src/material-experimental/mdc-chips/chip-option.ts b/src/material-experimental/mdc-chips/chip-option.ts index c371544926cd..45b2e9895d35 100644 --- a/src/material-experimental/mdc-chips/chip-option.ts +++ b/src/material-experimental/mdc-chips/chip-option.ts @@ -16,7 +16,6 @@ import { Output, ViewEncapsulation } from '@angular/core'; - import {MatChip} from './chip'; @@ -40,21 +39,21 @@ export class MatChipSelectionChange { selector: 'mat-basic-chip-option, mat-chip-option', templateUrl: 'chip-option.html', styleUrls: ['chips.css'], - inputs: ['color', 'disabled', 'disableRipple'], + inputs: ['color', 'disableRipple', 'tabIndex'], host: { 'role': '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', + '[class.mat-mdc-chip-with-trailing-icon]': 'trailingIcon || removeIcon', '[class.mat-mdc-chip-selected]': 'selected', '[id]': 'id', - '[tabIndex]': 'disabled ? null : -1', + '[tabIndex]': 'tabIndex', '[attr.disabled]': 'disabled || null', '[attr.aria-disabled]': 'disabled.toString()', '[attr.aria-selected]': 'ariaSelected', - '(click)': '_handleClick($event)', - '(keydown)': '_handleKeydown($event)', + '(click)': '_click($event)', + '(keydown)': '_keydown($event)', '(focus)': 'focus()', '(blur)': '_blur()', '(transitionend)': '_chipFoundation.handleTransitionEnd($event)' @@ -168,8 +167,37 @@ export class MatChipOption extends MatChip { }); } + /** 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 { + this._hasFocusInternal = false; + this._onBlur.next({chip: this}); + } + + /** Handles click events on the chip. */ + _click(event: MouseEvent) { + if (this.disabled) { + event.preventDefault(); + } else { + this._handleInteraction(event); + event.stopPropagation(); + } + } + /** Handles custom key presses. */ - _handleKeydown(event: KeyboardEvent): void { + _keydown(event: KeyboardEvent): void { if (this.disabled) { return; } diff --git a/src/material-experimental/mdc-chips/chip-row.html b/src/material-experimental/mdc-chips/chip-row.html index 4f6bc3bf3987..1ef183ef465d 100644 --- a/src/material-experimental/mdc-chips/chip-row.html +++ b/src/material-experimental/mdc-chips/chip-row.html @@ -1,9 +1,10 @@ -
- +
+
+ + + +
-
- -
-
- +
+
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';