mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(material-experimental/chips): add grid keyboard shortcuts (#16384)
This commit is contained in:
committed by
Jeremy Elbourn
parent
aa2236820d
commit
d29df3813e
@@ -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. */
|
||||
|
||||
@@ -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: '<ng-content></ng-content>',
|
||||
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<MatChipEvent> {
|
||||
return merge(...this._chips.map(chip => chip._onFocus));
|
||||
}
|
||||
|
||||
/** Emits when the chip grid value has been changed by the user. */
|
||||
@Output() readonly change: EventEmitter<MatChipGridChange> =
|
||||
new EventEmitter<MatChipGridChange>();
|
||||
@@ -198,8 +217,16 @@ export class MatChipGrid extends _MatChipGridMixinBase implements AfterContentIn
|
||||
*/
|
||||
@Output() readonly valueChange: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
@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<MatChipRow>;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
*
|
||||
* `<mat-chip>
|
||||
* <mat-icon matChipRemove>cancel</mat-icon>
|
||||
* </mat-chip>`
|
||||
*/
|
||||
@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<MouseEvent | KeyboardEvent> = new Subject<MouseEvent | KeyboardEvent>();
|
||||
|
||||
constructor(_elementRef: ElementRef) {
|
||||
super(_elementRef);
|
||||
}
|
||||
}
|
||||
@@ -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 `<input>` element that the event is being fired for. */
|
||||
|
||||
@@ -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: '<ng-content></ng-content>',
|
||||
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<MatChip>;
|
||||
|
||||
@@ -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<MatChipOption>;
|
||||
|
||||
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<MatChip>(this._chips)
|
||||
this._keyManager = new FocusKeyManager<MatChip>(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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<div role="gridcell" *ngIf="leadingIcon">
|
||||
<ng-content select="[mat-chip-avatar], [matChipAvatar]"></ng-content>
|
||||
<div role="gridcell">
|
||||
<div #chipContent tabindex="-1" class="mat-chip-row-focusable-text-content">
|
||||
<ng-content select="[mat-chip-avatar], [matChipAvatar]"></ng-content>
|
||||
<span class="mdc-chip__text"><ng-content></ng-content></span>
|
||||
<ng-content select="[mat-chip-trailing-icon],[matChipTrailingIcon]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
<div role="gridcell" class="mdc-chip__text">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<div role="gridcell" *ngIf="trailingIcon">
|
||||
<ng-content select="[mat-chip-trailing-icon],[matChipRemove],[matChipTrailingIcon]"></ng-content>
|
||||
<div role="gridcell" *ngIf="removeIcon">
|
||||
<ng-content select="[matChipRemove]"></ng-content>
|
||||
</div>
|
||||
|
||||
@@ -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<HTMLElement> {
|
||||
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:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<MouseEvent | KeyboardEvent>();
|
||||
|
||||
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<MatChipEvent>();
|
||||
|
||||
/** Emits when the chip is blurred. */
|
||||
readonly _onBlur = new Subject<MatChipEvent>();
|
||||
|
||||
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 `<mat-chip>` 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<string>();
|
||||
/** Emitted when the user interacts with the remove icon. */
|
||||
@Output() removeIconInteraction = new EventEmitter<string>();
|
||||
|
||||
/** Emitted when the user interacts with the chip. */
|
||||
@Output() interaction = new EventEmitter<string>();
|
||||
@@ -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<void>();
|
||||
protected _destroyed = new Subject<void>();
|
||||
|
||||
/** 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();
|
||||
/** 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;
|
||||
}
|
||||
}
|
||||
|
||||
/** 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();
|
||||
}
|
||||
}
|
||||
|
||||
/** 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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<HTMLElement> {
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<T> {
|
||||
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<T> {
|
||||
private _activeRowIndex = -1;
|
||||
private _activeColumnIndex = -1;
|
||||
private _activeRow: GridKeyManagerRow<T> | null = null;
|
||||
private _activeCell: T | null = null;
|
||||
private _dir: 'ltr' | 'rtl' = 'ltr';
|
||||
|
||||
constructor(private _rows: QueryList<GridKeyManagerRow<T>> | GridKeyManagerRow<T>[]) {
|
||||
// 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<GridKeyManagerRow<T>>) => {
|
||||
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<T>[] {
|
||||
return this._rows instanceof QueryList ? this._rows.toArray() : this._rows;
|
||||
}
|
||||
}
|
||||
@@ -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 {MatChip, MatChipCssInternalOnly} from './chip';
|
||||
import {MAT_CHIPS_DEFAULT_OPTIONS, MatChipsDefaultOptions} from './chip-default-options';
|
||||
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 {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 {MatChipListbox} from './chip-listbox';
|
||||
|
||||
|
||||
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,
|
||||
{
|
||||
|
||||
@@ -15,3 +15,4 @@ export * from './chip-grid';
|
||||
export * from './module';
|
||||
export * from './chip-input';
|
||||
export * from './chip-default-options';
|
||||
export * from './chip-icons';
|
||||
|
||||
Reference in New Issue
Block a user