From 09c9d09b58062b8267156abbd1b7ab8062ab4772 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 24 May 2022 15:05:41 +0200 Subject: [PATCH 1/2] refactor(material-experimental/mdc-chips): remove usage of MDC adapter Reworks the MDC-based chips not to use the MDC adapters. --- src/dev-app/mdc-chips/mdc-chips-demo.html | 4 +- .../mdc-chips/BUILD.bazel | 2 - .../mdc-chips/chip-action.ts | 114 +------- .../mdc-chips/chip-grid.spec.ts | 51 ++-- .../mdc-chips/chip-grid.ts | 113 +++----- .../mdc-chips/chip-icons.ts | 21 +- .../mdc-chips/chip-input.ts | 4 - .../mdc-chips/chip-listbox.spec.ts | 15 +- .../mdc-chips/chip-listbox.ts | 73 +---- .../mdc-chips/chip-option.html | 4 +- .../mdc-chips/chip-option.spec.ts | 3 +- .../mdc-chips/chip-option.ts | 94 +----- .../mdc-chips/chip-remove.spec.ts | 44 +-- .../mdc-chips/chip-row.spec.ts | 12 +- .../mdc-chips/chip-row.ts | 121 +++----- .../mdc-chips/chip-set.ts | 250 ++++++++-------- .../mdc-chips/chip.spec.ts | 3 +- src/material-experimental/mdc-chips/chip.ts | 273 +++++++----------- .../mdc-chips/emit-event.ts | 33 --- 19 files changed, 400 insertions(+), 834 deletions(-) delete mode 100644 src/material-experimental/mdc-chips/emit-event.ts diff --git a/src/dev-app/mdc-chips/mdc-chips-demo.html b/src/dev-app/mdc-chips/mdc-chips-demo.html index 228e8ea2f88f..e669128b9fc1 100644 --- a/src/dev-app/mdc-chips/mdc-chips-demo.html +++ b/src/dev-app/mdc-chips/mdc-chips-demo.html @@ -147,8 +147,10 @@

Input is last child of chip grid

New Contributor... - + {{person.name}} diff --git a/src/material-experimental/mdc-chips/BUILD.bazel b/src/material-experimental/mdc-chips/BUILD.bazel index 906bb0a0fe8b..239641aede5c 100644 --- a/src/material-experimental/mdc-chips/BUILD.bazel +++ b/src/material-experimental/mdc-chips/BUILD.bazel @@ -29,7 +29,6 @@ ng_module( "@npm//@angular/common", "@npm//@angular/core", "@npm//@angular/forms", - "@npm//@material/chips", ], ) @@ -91,7 +90,6 @@ ng_test_library( "@npm//@angular/common", "@npm//@angular/forms", "@npm//@angular/platform-browser", - "@npm//@material/chips", "@npm//rxjs", ], ) diff --git a/src/material-experimental/mdc-chips/chip-action.ts b/src/material-experimental/mdc-chips/chip-action.ts index a7c42ffcdfa4..63c3bc4df228 100644 --- a/src/material-experimental/mdc-chips/chip-action.ts +++ b/src/material-experimental/mdc-chips/chip-action.ts @@ -6,25 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import { - AfterViewInit, - ChangeDetectorRef, - Directive, - ElementRef, - Inject, - Input, - OnChanges, - OnDestroy, - SimpleChanges, -} from '@angular/core'; -import {DOCUMENT} from '@angular/common'; -import { - MDCChipActionAdapter, - MDCChipActionFoundation, - MDCChipActionType, - MDCChipPrimaryActionFoundation, -} from '@material/chips'; -import {emitCustomEvent} from './emit-event'; +import {Directive, ElementRef, Input} from '@angular/core'; import { CanDisable, HasTabIndex, @@ -35,7 +17,7 @@ import { const _MatChipActionMixinBase = mixinTabIndex(mixinDisabled(class {}), -1); /** - * Interactive element within a chip. + * Section within a chip. * @docs-private */ @Directive({ @@ -43,110 +25,32 @@ const _MatChipActionMixinBase = mixinTabIndex(mixinDisabled(class {}), -1); inputs: ['disabled', 'tabIndex'], host: { 'class': 'mdc-evolution-chip__action mat-mdc-chip-action', - '[class.mdc-evolution-chip__action--primary]': `_getFoundation().actionType() === ${MDCChipActionType.PRIMARY}`, + '[class.mdc-evolution-chip__action--primary]': '_isPrimary', // Note that while our actions are interactive, we have to add the `--presentational` class, // in order to avoid some super-specific `:hover` styles from MDC. - '[class.mdc-evolution-chip__action--presentational]': `_getFoundation().actionType() === ${MDCChipActionType.PRIMARY}`, - '[class.mdc-evolution-chip__action--trailing]': `_getFoundation().actionType() === ${MDCChipActionType.TRAILING}`, + '[class.mdc-evolution-chip__action--presentational]': '_isPrimary', + '[class.mdc-evolution-chip__action--trailing]': '!_isPrimary', '[attr.tabindex]': '(disabled || !isInteractive) ? null : tabIndex', '[attr.disabled]': "disabled ? '' : null", '[attr.aria-disabled]': 'disabled', - '(click)': '_handleClick($event)', - '(keydown)': '_handleKeydown($event)', }, }) -export class MatChipAction - extends _MatChipActionMixinBase - implements AfterViewInit, OnDestroy, CanDisable, HasTabIndex, OnChanges -{ - private _document: Document; - private _foundation: MDCChipActionFoundation; - private _adapter: MDCChipActionAdapter = { - focus: () => this.focus(), - getAttribute: (name: string) => this._elementRef.nativeElement.getAttribute(name), - setAttribute: (name: string, value: string) => { - // MDC tries to update the tabindex directly in the DOM when navigating using the keyboard - // which overrides our own handling. If we detect such a case, assign it to the same property - // as the Angular binding in order to maintain consistency. - if (name === 'tabindex') { - this._updateTabindex(parseInt(value)); - } else { - this._elementRef.nativeElement.setAttribute(name, value); - } - }, - removeAttribute: (name: string) => { - if (name !== 'tabindex') { - this._elementRef.nativeElement.removeAttribute(name); - } - }, - getElementID: () => this._elementRef.nativeElement.id, - emitEvent: (eventName: string, data: T) => { - emitCustomEvent(this._elementRef.nativeElement, this._document, eventName, data, true); - }, - }; - +export class MatChipAction extends _MatChipActionMixinBase implements CanDisable, HasTabIndex { /** Whether the action is interactive. */ @Input() isInteractive = true; - _handleClick(event: MouseEvent) { - // Usually these events can't happen while the chip is disabled since the browser won't - // allow them which is what MDC seems to rely on, however the event can be faked in tests. - if (!this.disabled && this.isInteractive) { - this._foundation.handleClick(); - event.preventDefault(); - } - } + /** Whether this is the primary action in the chip. */ + _isPrimary = true; - _handleKeydown(event: KeyboardEvent) { - // Usually these events can't happen while the chip is disabled since the browser won't - // allow them which is what MDC seems to rely on, however the event can be faked in tests. - if (!this.disabled && this.isInteractive) { - this._foundation.handleKeydown(event); - } - } - - protected _createFoundation(adapter: MDCChipActionAdapter): MDCChipActionFoundation { - return new MDCChipPrimaryActionFoundation(adapter); - } - - constructor( - public _elementRef: ElementRef, - @Inject(DOCUMENT) _document: any, - private _changeDetectorRef: ChangeDetectorRef, - ) { + constructor(public _elementRef: ElementRef) { super(); - this._foundation = this._createFoundation(this._adapter); if (_elementRef.nativeElement.nodeName === 'BUTTON') { _elementRef.nativeElement.setAttribute('type', 'button'); } } - ngAfterViewInit() { - this._foundation.init(); - this._foundation.setDisabled(this.disabled); - } - - ngOnChanges(changes: SimpleChanges) { - if (changes['disabled']) { - this._foundation.setDisabled(this.disabled); - } - } - - ngOnDestroy() { - this._foundation.destroy(); - } - focus() { this._elementRef.nativeElement.focus(); } - - _getFoundation() { - return this._foundation; - } - - _updateTabindex(value: number) { - this.tabIndex = value; - this._changeDetectorRef.markForCheck(); - } } diff --git a/src/material-experimental/mdc-chips/chip-grid.spec.ts b/src/material-experimental/mdc-chips/chip-grid.spec.ts index ef69491bfb5a..f0b34e39c298 100644 --- a/src/material-experimental/mdc-chips/chip-grid.spec.ts +++ b/src/material-experimental/mdc-chips/chip-grid.spec.ts @@ -16,6 +16,7 @@ import { dispatchFakeEvent, dispatchKeyboardEvent, MockNgZone, + patchElementFocus, typeInElement, } from '@angular/cdk/testing/private'; import { @@ -34,7 +35,6 @@ import {MatFormFieldModule} from '@angular/material-experimental/mdc-form-field' import {MatInputModule} from '@angular/material-experimental/mdc-input'; import {By} from '@angular/platform-browser'; import {BrowserAnimationsModule, NoopAnimationsModule} from '@angular/platform-browser/animations'; -import {MDCChipAnimation} from '@material/chips'; import {MatChipEvent, MatChipGrid, MatChipInputEvent, MatChipRow, MatChipsModule} from './index'; describe('MDC-based MatChipGrid', () => { @@ -199,6 +199,7 @@ describe('MDC-based MatChipGrid', () => { // Destroy the middle item testComponent.chips.splice(2, 1); fixture.detectChanges(); + flush(); // Should not have focus expect(chipGridNativeElement.contains(document.activeElement)).toBe(false); @@ -208,6 +209,7 @@ describe('MDC-based MatChipGrid', () => { testComponent.chips = [0]; spyOn(chipGridInstance, 'focus'); + patchElementFocus(chips.last.primaryAction!._elementRef.nativeElement); chips.last.focus(); testComponent.chips.pop(); @@ -216,27 +218,22 @@ describe('MDC-based MatChipGrid', () => { expect(chipGridInstance.focus).toHaveBeenCalled(); }); - it( - 'should move focus to the last chip when the focused chip was deleted inside a ' + - 'component with animations', - fakeAsync(() => { - fixture.destroy(); - TestBed.resetTestingModule(); - - fixture = createComponent(StandardChipGridWithAnimations, BrowserAnimationsModule); + it('should move focus to the last chip when the focused chip was deleted inside a component with animations', fakeAsync(() => { + fixture.destroy(); + TestBed.resetTestingModule(); - chips.last.focus(); - fixture.detectChanges(); + fixture = createComponent(StandardChipGridWithAnimations, BrowserAnimationsModule); - expect(document.activeElement).toBe(primaryActions[primaryActions.length - 1]); + patchElementFocus(chips.last.primaryAction!._elementRef.nativeElement); + chips.last.focus(); + fixture.detectChanges(); - dispatchKeyboardEvent(chips.last._elementRef.nativeElement, 'keydown', BACKSPACE); - fixture.detectChanges(); - tick(500); + dispatchKeyboardEvent(chips.last._elementRef.nativeElement, 'keydown', BACKSPACE); + fixture.detectChanges(); + tick(500); - expect(document.activeElement).toBe(primaryActions[primaryActions.length - 2]); - }), - ); + expect(document.activeElement).toBe(primaryActions[primaryActions.length - 2]); + })); }); it('should have a focus indicator', () => { @@ -394,6 +391,7 @@ describe('MDC-based MatChipGrid', () => { expect(document.activeElement).toBe(primaryActions[1]); directionality.value = 'rtl'; + directionality.change.next('rtl'); fixture.detectChanges(); dispatchKeyboardEvent(primaryActions[1], 'keydown', RIGHT_ARROW); @@ -562,14 +560,7 @@ describe('MDC-based MatChipGrid', () => { // associated chip remove element. trailingActions[2].click(); fixture.detectChanges(); - (chip as any)._handleAnimationend({ - animationName: MDCChipAnimation.EXIT, - target: chip._elementRef.nativeElement, - }); flush(); - (chip as any)._handleTransitionend({target: chip._elementRef.nativeElement}); - flush(); - fixture.detectChanges(); expect(document.activeElement).toBe(primaryActions[3]); })); @@ -589,7 +580,6 @@ describe('MDC-based MatChipGrid', () => { .map(chip => chip.nativeElement); nativeChipGrid = fixture.debugElement.query(By.css('mat-chip-grid'))!.nativeElement; - nativeInput = fixture.nativeElement.querySelector('input'); }); @@ -730,18 +720,21 @@ describe('MDC-based MatChipGrid', () => { it('should blur the form field when the active chip is blurred', fakeAsync(() => { const formField: HTMLElement = fixture.nativeElement.querySelector('.mat-mdc-form-field'); + const firstAction = nativeChips[0].querySelector('.mat-mdc-chip-action') as HTMLElement; - dispatchFakeEvent(nativeChips[0], 'focusin'); + patchElementFocus(firstAction); + firstAction.focus(); fixture.detectChanges(); expect(formField.classList).toContain('mat-focused'); - dispatchFakeEvent(nativeChips[0], 'focusout'); + firstAction.blur(); fixture.detectChanges(); - tick(); fixture.detectChanges(); zone.simulateZoneExit(); fixture.detectChanges(); + flush(); + expect(formField.classList).not.toContain('mat-focused'); })); diff --git a/src/material-experimental/mdc-chips/chip-grid.ts b/src/material-experimental/mdc-chips/chip-grid.ts index e8ed7e87d9e5..d5cf726efded 100644 --- a/src/material-experimental/mdc-chips/chip-grid.ts +++ b/src/material-experimental/mdc-chips/chip-grid.ts @@ -7,7 +7,7 @@ */ import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; -import {TAB} from '@angular/cdk/keycodes'; +import {hasModifierKey, TAB} from '@angular/cdk/keycodes'; import { AfterContentInit, AfterViewInit, @@ -18,7 +18,6 @@ import { DoCheck, ElementRef, EventEmitter, - Inject, Input, OnDestroy, Optional, @@ -34,20 +33,19 @@ import { NgForm, Validators, } from '@angular/forms'; -import {DOCUMENT} from '@angular/common'; import { CanUpdateErrorState, ErrorStateMatcher, mixinErrorState, } from '@angular/material-experimental/mdc-core'; import {MatFormFieldControl} from '@angular/material-experimental/mdc-form-field'; -import {LiveAnnouncer} from '@angular/cdk/a11y'; import {MatChipTextControl} from './chip-text-control'; import {Observable, Subject} from 'rxjs'; -import {startWith, takeUntil} from 'rxjs/operators'; +import {takeUntil} from 'rxjs/operators'; import {MatChipEvent} from './chip'; import {MatChipRow} from './chip-row'; import {MatChipSet} from './chip-set'; +import {Directionality} from '@angular/cdk/bidi'; /** Change event object that is emitted when the chip grid value has changed. */ export class MatChipGridChange { @@ -72,10 +70,9 @@ class MatChipGridBase extends MatChipSet { readonly stateChanges = new Subject(); constructor( - liveAnnouncer: LiveAnnouncer, - document: any, elementRef: ElementRef, changeDetectorRef: ChangeDetectorRef, + dir: Directionality, public _defaultErrorStateMatcher: ErrorStateMatcher, public _parentForm: NgForm, public _parentFormGroup: FormGroupDirective, @@ -86,7 +83,7 @@ class MatChipGridBase extends MatChipSet { */ public ngControl: NgControl, ) { - super(liveAnnouncer, document, elementRef, changeDetectorRef); + super(elementRef, changeDetectorRef, dir); } } const _MatChipGridMixinBase = mixinErrorState(MatChipGridBase); @@ -115,7 +112,6 @@ const _MatChipGridMixinBase = mixinErrorState(MatChipGridBase); '[class.mat-mdc-chip-list-required]': 'required', '(focus)': 'focus()', '(blur)': '_blur()', - '(keydown)': '_keydown($event)', }, providers: [{provide: MatFormFieldControl, useExisting: MatChipGrid}], encapsulation: ViewEncapsulation.None, @@ -253,11 +249,6 @@ export class MatChipGrid return this._getChipStream(chip => chip._onBlur); } - /** Combined stream of all of the child chips' focus events. */ - get chipFocusChanges(): Observable { - return this._getChipStream(chip => chip._onFocus); - } - /** Emits when the chip grid value has been changed by the user. */ @Output() readonly change: EventEmitter = new EventEmitter(); @@ -277,20 +268,18 @@ export class MatChipGrid override _chips: QueryList; constructor( - liveAnnouncer: LiveAnnouncer, - @Inject(DOCUMENT) document: any, elementRef: ElementRef, changeDetectorRef: ChangeDetectorRef, + @Optional() dir: Directionality, @Optional() parentForm: NgForm, @Optional() parentFormGroup: FormGroupDirective, defaultErrorStateMatcher: ErrorStateMatcher, @Optional() @Self() ngControl: NgControl, ) { super( - liveAnnouncer, - document, elementRef, changeDetectorRef, + dir, defaultErrorStateMatcher, parentForm, parentFormGroup, @@ -301,23 +290,20 @@ export class MatChipGrid } } - override ngAfterContentInit() { - super.ngAfterContentInit(); - - this._chips.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => { - // Check to see if we have a destroyed chip and need to refocus - this._updateFocusForDestroyedChips(); - this.stateChanges.next(); - }); - + ngAfterContentInit() { this.chipBlurChanges.pipe(takeUntil(this._destroyed)).subscribe(() => { this._blur(); this.stateChanges.next(); }); + + this.chipFocusChanges + .pipe(takeUntil(this._destroyed)) + .subscribe(() => this.stateChanges.next()); } override ngAfterViewInit() { super.ngAfterViewInit(); + if (!this._chipInput && (typeof ngDevMode === 'undefined' || ngDevMode)) { throw Error('mat-chip-grid must be used in combination with matChipInputFor.'); } @@ -362,16 +348,12 @@ export class MatChipGrid return; } - if (this._chips.length > 0) { - // MDC sets the tabindex directly on the DOM node when the user is navigating which means - // that we may end up with a `0` value from a previous interaction. We reset it manually - // here to ensure that the state is correct. - this._chips.forEach(chip => chip.primaryAction._updateTabindex(-1)); - this._chips.first.focus(); - } else { + if (!this._chips.length || this._chips.first.disabled) { // Delay until the next tick, because this can cause a "changed after checked" // error if the input does something on focus (e.g. opens an autocomplete). Promise.resolve().then(() => this._chipInput.focus()); + } else if (this._chips.length) { + this._keyManager.setFirstItemActive(); } this.stateChanges.next(); @@ -432,20 +414,18 @@ export class MatChipGrid /** When blurred, mark the field as touched when focus moved outside the chip grid. */ _blur() { - if (this.disabled) { - return; + if (!this.disabled) { + // Check whether the focus moved to chip input. + // If the focus is not moved to chip input, mark the field as touched. If the focus moved + // to chip input, do nothing. + // Timeout is needed to wait for the focus() event trigger on chip input. + setTimeout(() => { + if (!this.focused) { + this._propagateChanges(); + this._markAsTouched(); + } + }); } - - // Check whether the focus moved to chip input. - // If the focus is not moved to chip input, mark the field as touched. If the focus moved - // to chip input, do nothing. - // Timeout is needed to wait for the focus() event trigger on chip input. - setTimeout(() => { - if (!this.focused) { - this._propagateChanges(); - this._markAsTouched(); - } - }); } /** @@ -460,9 +440,23 @@ export class MatChipGrid } /** Handles custom keyboard events. */ - _keydown(event: KeyboardEvent) { - if (event.keyCode === TAB && (event.target as HTMLElement).id !== this._chipInput.id) { - this._allowFocusEscape(); + override _handleKeydown(event: KeyboardEvent) { + if (event.keyCode === TAB) { + if ( + this._chipInput.focused && + hasModifierKey(event, 'shiftKey') && + this._chips.length && + !this._chips.last.disabled + ) { + event.preventDefault(); + this._focusLastChip(); + } else { + // Use the super method here since it doesn't check for the input focused state. + // This allows focus to escape if there's only one disabled chip left in the list. + super._allowFocusEscape(); + } + } else if (!this._chipInput.focused) { + super._handleKeydown(event); } this.stateChanges.next(); @@ -470,7 +464,7 @@ export class MatChipGrid _focusLastChip() { if (this._chips.length) { - this._chips.last.primaryAction.focus(); + this._chips.last.focus(); } } @@ -490,21 +484,4 @@ export class MatChipGrid this._changeDetectorRef.markForCheck(); this.stateChanges.next(); } - - /** - * If the amount of chips changed, we need to focus the next closest chip. - */ - private _updateFocusForDestroyedChips() { - // 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(); - } else { - this.focus(); - } - } - - this._lastDestroyedChipIndex = null; - } } diff --git a/src/material-experimental/mdc-chips/chip-icons.ts b/src/material-experimental/mdc-chips/chip-icons.ts index 8e7b79c3c9e7..49cbe6fb38d3 100644 --- a/src/material-experimental/mdc-chips/chip-icons.ts +++ b/src/material-experimental/mdc-chips/chip-icons.ts @@ -7,7 +7,6 @@ */ import {Directive, InjectionToken} from '@angular/core'; -import {MDCChipActionAdapter, MDCChipTrailingActionFoundation} from '@material/chips'; import {MatChipAction} from './chip-action'; /** @@ -60,9 +59,7 @@ export class MatChipTrailingIcon extends MatChipAction { */ override isInteractive = false; - protected override _createFoundation(adapter: MDCChipActionAdapter) { - return new MDCChipTrailingActionFoundation(adapter); - } + override _isPrimary = false; } /** @@ -100,19 +97,5 @@ export const MAT_CHIP_REMOVE = new InjectionToken('MatChipRemove' providers: [{provide: MAT_CHIP_REMOVE, useExisting: MatChipRemove}], }) export class MatChipRemove extends MatChipAction { - protected override _createFoundation(adapter: MDCChipActionAdapter) { - return new MDCChipTrailingActionFoundation(adapter); - } - - override _handleClick(event: MouseEvent) { - // Some consumers bind `click` events directly on the chip - // which will also pick up clicks on the remove button. - event.stopPropagation(); - super._handleClick(event); - } - - override _handleKeydown(event: KeyboardEvent) { - event.stopPropagation(); - super._handleKeydown(event); - } + override _isPrimary = false; } diff --git a/src/material-experimental/mdc-chips/chip-input.ts b/src/material-experimental/mdc-chips/chip-input.ts index deb2a519fc16..2588632e26dd 100644 --- a/src/material-experimental/mdc-chips/chip-input.ts +++ b/src/material-experimental/mdc-chips/chip-input.ts @@ -213,10 +213,6 @@ export class MatChipInput implements MatChipTextControl, AfterContentInit, OnCha /** Checks to see if the (chipEnd) event needs to be emitted. */ _emitChipEnd(event?: KeyboardEvent) { - if (!this.inputElement.value && !!event) { - this._chipGrid._keydown(event); - } - if (!event || this._isSeparatorKey(event)) { this.chipEnd.emit({ input: this.inputElement, diff --git a/src/material-experimental/mdc-chips/chip-listbox.spec.ts b/src/material-experimental/mdc-chips/chip-listbox.spec.ts index 5bce81eb16db..2161a0d43cb3 100644 --- a/src/material-experimental/mdc-chips/chip-listbox.spec.ts +++ b/src/material-experimental/mdc-chips/chip-listbox.spec.ts @@ -1,6 +1,11 @@ import {Direction, Directionality} from '@angular/cdk/bidi'; import {END, HOME, LEFT_ARROW, RIGHT_ARROW, SPACE, TAB} from '@angular/cdk/keycodes'; -import {dispatchFakeEvent, dispatchKeyboardEvent, MockNgZone} from '../../cdk/testing/private'; +import { + dispatchFakeEvent, + dispatchKeyboardEvent, + MockNgZone, + patchElementFocus, +} from '../../cdk/testing/private'; import { Component, DebugElement, @@ -187,6 +192,7 @@ describe('MDC-based MatChipListbox', () => { const midItem = chips.get(2)!; // Focus the middle item + patchElementFocus(midItem.primaryAction!._elementRef.nativeElement); midItem.focus(); // Destroy the middle item @@ -199,6 +205,7 @@ describe('MDC-based MatChipListbox', () => { it('should focus the previous item', () => { // Focus the last item + patchElementFocus(chips.last.primaryAction!._elementRef.nativeElement); chips.last.focus(); // Destroy the last item @@ -227,18 +234,19 @@ describe('MDC-based MatChipListbox', () => { expect(chipListboxNativeElement.contains(document.activeElement)).toBe(false); })); - it('should focus the listbox if the last focused item is removed', () => { + it('should focus the listbox if the last focused item is removed', fakeAsync(() => { testComponent.chips = [0]; fixture.detectChanges(); spyOn(chipListboxInstance, 'focus'); + patchElementFocus(chips.last.primaryAction!._elementRef.nativeElement); chips.last.focus(); testComponent.chips.pop(); fixture.detectChanges(); expect(chipListboxInstance.focus).toHaveBeenCalled(); - }); + })); }); }); @@ -393,6 +401,7 @@ describe('MDC-based MatChipListbox', () => { expect(document.activeElement).toBe(primaryActions[1]); directionality.value = 'rtl'; + directionality.change.next('rtl'); fixture.detectChanges(); dispatchKeyboardEvent(primaryActions[1], 'keydown', RIGHT_ARROW); diff --git a/src/material-experimental/mdc-chips/chip-listbox.ts b/src/material-experimental/mdc-chips/chip-listbox.ts index 35fdea92b1bb..de98ff405554 100644 --- a/src/material-experimental/mdc-chips/chip-listbox.ts +++ b/src/material-experimental/mdc-chips/chip-listbox.ts @@ -22,7 +22,6 @@ import { ViewEncapsulation, } from '@angular/core'; import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; -import {MDCChipActionType} from '@material/chips'; import {Observable} from 'rxjs'; import {startWith, takeUntil} from 'rxjs/operators'; import {MatChip, MatChipEvent} from './chip'; @@ -143,15 +142,7 @@ export class MatChipListbox * is a value from an option. The second is a value from the selection. A boolean * should be returned. */ - @Input() - get compareWith(): (o1: any, o2: any) => boolean { - return this._compareWith; - } - set compareWith(fn: (o1: any, o2: any) => boolean) { - this._compareWith = fn; - this._initializeSelection(); - } - private _compareWith = (o1: any, o2: any) => o1 === o2; + @Input() compareWith: (o1: any, o2: any) => boolean = (o1: any, o2: any) => o1 === o2; /** Whether this chip listbox is required. */ @Input() @@ -168,11 +159,6 @@ export class MatChipListbox return this._getChipStream(chip => chip.selectionChange); } - /** Combined stream of all of the child chips' focus events. */ - get chipFocusChanges(): Observable { - return this._getChipStream(chip => chip._onFocus); - } - /** Combined stream of all of the child chips' blur events. */ get chipBlurChanges(): Observable { return this._getChipStream(chip => chip._onBlur); @@ -200,18 +186,10 @@ export class MatChipListbox }) override _chips: QueryList; - override ngAfterContentInit() { - super.ngAfterContentInit(); - + ngAfterContentInit() { this._chips.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => { // Update listbox selectable/multiple properties on chips this._syncListboxProperties(); - - // Reset chips selected/deselected status - this._initializeSelection(); - - // Check to see if we have a destroyed chip and need to refocus - this._updateFocusForDestroyedChips(); }); this.chipBlurChanges.pipe(takeUntil(this._destroyed)).subscribe(() => this._blur()); @@ -233,14 +211,12 @@ export class MatChipListbox const firstSelectedChip = this._getFirstSelectedChip(); - if (firstSelectedChip) { + if (firstSelectedChip && !firstSelectedChip.disabled) { firstSelectedChip.focus(); } else if (this._chips.length > 0) { - // MDC sets the tabindex directly on the DOM node when the user is navigating which means - // that we may end up with a `0` value from a previous interaction. We reset it manually - // here to ensure that the state is correct. - this._chips.forEach(chip => chip.primaryAction._updateTabindex(-1)); - this._chips.first.primaryAction.focus(); + this._keyManager.setFirstItemActive(); + } else { + this._elementRef.nativeElement.focus(); } } @@ -329,21 +305,6 @@ export class MatChipListbox this._changeDetectorRef.markForCheck(); } - /** - * Initializes the chip listbox selection state to reflect any chips that were preselected. - */ - private _initializeSelection() { - setTimeout(() => { - // Defer setting the value in order to avoid the "Expression - // has changed after it was checked" errors from Angular. - this._chips.forEach((chip, index) => { - if (chip.selected) { - this._chipSetFoundation.setChipSelected(index, MDCChipActionType.PRIMARY, true); - } - }); - }); - } - /** * Deselects every chip in the listbox. * @param skip Chip that should not be deselected. @@ -360,9 +321,9 @@ export class MatChipListbox * Finds and selects the chip based on its value. * @returns Chip that has the corresponding value. */ - private _selectValue(value: any, isUserInput: boolean = true): MatChip | undefined { + private _selectValue(value: any, isUserInput: boolean): MatChip | undefined { const correspondingChip = this._chips.find(chip => { - return chip.value != null && this._compareWith(chip.value, value); + return chip.value != null && this.compareWith(chip.value, value); }); if (correspondingChip) { @@ -395,22 +356,4 @@ export class MatChipListbox return this.selected; } } - - /** - * If the amount of chips changed, we need to update the - * key manager state and focus the next closest chip. - */ - private _updateFocusForDestroyedChips() { - // Move focus to the closest chip. If no other chips remain, focus the chip-listbox itself. - if (this._lastDestroyedChipIndex != null) { - if (this._chips.length) { - const newChipIndex = Math.min(this._lastDestroyedChipIndex, this._chips.length - 1); - this._chips.toArray()[newChipIndex].focus(); - } else { - this.focus(); - } - } - - this._lastDestroyedChipIndex = null; - } } diff --git a/src/material-experimental/mdc-chips/chip-option.html b/src/material-experimental/mdc-chips/chip-option.html index 262203c58f69..4b04272ce88c 100644 --- a/src/material-experimental/mdc-chips/chip-option.html +++ b/src/material-experimental/mdc-chips/chip-option.html @@ -11,9 +11,7 @@ [disabled]="disabled" [attr.aria-selected]="ariaSelected" [attr.aria-label]="ariaLabel" - role="option" - (blur)="_blur()" - (focus)="focus()"> + role="option"> diff --git a/src/material-experimental/mdc-chips/chip-option.spec.ts b/src/material-experimental/mdc-chips/chip-option.spec.ts index f6e95dc74020..0bf8561e1410 100644 --- a/src/material-experimental/mdc-chips/chip-option.spec.ts +++ b/src/material-experimental/mdc-chips/chip-option.spec.ts @@ -312,7 +312,7 @@ describe('MDC-based Option Chips', () => {
{{name}} @@ -329,7 +329,6 @@ class SingleChip { selectable: boolean = true; shouldShow: boolean = true; - chipFocus: (event?: MatChipEvent) => void = () => {}; chipDestroy: (event?: MatChipEvent) => void = () => {}; chipSelectionChange: (event?: MatChipSelectionChange) => void = () => {}; } diff --git a/src/material-experimental/mdc-chips/chip-option.ts b/src/material-experimental/mdc-chips/chip-option.ts index dfca8a135d6c..000be24bbcec 100644 --- a/src/material-experimental/mdc-chips/chip-option.ts +++ b/src/material-experimental/mdc-chips/chip-option.ts @@ -14,16 +14,8 @@ import { Input, Output, ViewEncapsulation, - AfterViewInit, OnInit, } from '@angular/core'; -import { - ActionInteractionEvent, - MDCChipActionInteractionTrigger, - MDCChipActionType, - MDCChipCssClasses, -} from '@material/chips'; -import {take} from 'rxjs/operators'; import {MatChip} from './chip'; /** Event object emitted by MatChipOption when selected or deselected. */ @@ -55,6 +47,12 @@ export class MatChipSelectionChange { '[class.mat-mdc-chip-with-avatar]': 'leadingIcon', '[class.mdc-evolution-chip--selectable]': 'selectable', '[class.mdc-evolution-chip--disabled]': 'disabled', + '[class.mdc-evolution-chip--selected]': 'selected', + // This class enables the transition on the checkmark. Usually MDC adds it when selection + // starts and removes it once the animation is finished. We don't need to go through all + // the trouble, because we only care about the selection animation. MDC needs to do it, + // because they also have an exit animation that we don't care about. + '[class.mdc-evolution-chip--selecting]': '!_animationsDisabled', '[class.mdc-evolution-chip--with-trailing-action]': '_hasTrailingIcon()', '[class.mdc-evolution-chip--with-primary-graphic]': '_hasLeadingGraphic()', '[class.mdc-evolution-chip--with-primary-icon]': 'leadingIcon', @@ -70,16 +68,7 @@ export class MatChipSelectionChange { encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) -export class MatChipOption extends MatChip implements OnInit, AfterViewInit { - /** Whether the component is done initializing. */ - private _isInitialized: boolean; - - /** - * Selected state that was assigned before the component was initializing - * and which needs to be synced back up with the foundation. - */ - private _pendingSelectedState: boolean | undefined; - +export class MatChipOption extends MatChip implements OnInit { /** Whether the chip list is selectable. */ chipListSelectable: boolean = true; @@ -105,21 +94,15 @@ export class MatChipOption extends MatChip implements OnInit, AfterViewInit { /** Whether the chip is selected. */ @Input() get selected(): boolean { - return ( - this._pendingSelectedState ?? this._chipFoundation.isActionSelected(MDCChipActionType.PRIMARY) - ); + return this._selected; } set selected(value: BooleanInput) { if (this.selectable) { const coercedValue = coerceBooleanProperty(value); - - if (this._isInitialized) { - this._setSelectedState(coercedValue, false); - } else { - this._pendingSelectedState = coercedValue; - } + this._setSelectedState(coercedValue, false); } } + private _selected = false; /** The ARIA selected applied to the chip. */ get ariaSelected(): string | null { @@ -141,19 +124,6 @@ export class MatChipOption extends MatChip implements OnInit, AfterViewInit { this.role = 'presentation'; } - override ngAfterViewInit() { - super.ngAfterViewInit(); - this._isInitialized = true; - - if (this._pendingSelectedState != null) { - // Note that we want to clear the pending state before calling `_setSelectedState`, because - // we want it to read the actual selected state instead falling back to the pending one. - const selectedState = this._pendingSelectedState; - this._pendingSelectedState = undefined; - this._setSelectedState(selectedState, false); - } - } - /** Selects the chip. */ select(): void { if (this.selectable) { @@ -184,39 +154,9 @@ export class MatChipOption extends MatChip implements OnInit, AfterViewInit { return this.selected; } - /** Resets the state of the chip when it loses focus. */ - _blur(): void { - // When animations are enabled, Angular may end up removing the chip from the DOM a little - // earlier than usual, causing it to be blurred and throwing off the logic in the chip list - // that moves focus not the next item. To work around the issue, we defer marking the chip - // as not focused until the next time the zone stabilizes. - this._ngZone.onStable.pipe(take(1)).subscribe(() => { - this._ngZone.run(() => { - this._hasFocusInternal = false; - this._onBlur.next({chip: this}); - }); - }); - } - - protected override _onChipInteraction(event: ActionInteractionEvent) { - const {trigger, source} = event.detail; - - // Non-selection interactions should work the same as other chips. - if ( - source !== MDCChipActionType.PRIMARY || - (trigger !== MDCChipActionInteractionTrigger.CLICK && - trigger !== MDCChipActionInteractionTrigger.ENTER_KEY && - trigger !== MDCChipActionInteractionTrigger.SPACEBAR_KEY) - ) { - super._onChipInteraction(event); - } else if (this.selectable && !this.disabled) { - // Otherwise only let the event through if the chip is enabled and selectable. - this._chipFoundation.handleActionInteraction(event); - this.selectionChange.emit({ - source: this, - isUserInput: true, - selected: this.selected, - }); + protected override _handlePrimaryActionInteraction() { + if (this.selectable && !this.disabled) { + this._setSelectedState(!this.selected, true); } } @@ -227,18 +167,12 @@ export class MatChipOption extends MatChip implements OnInit, AfterViewInit { private _setSelectedState(isSelected: boolean, isUserInput: boolean) { if (isSelected !== this.selected) { - this._chipFoundation.setActionSelected(MDCChipActionType.PRIMARY, isSelected); + this._selected = isSelected; this.selectionChange.emit({ source: this, isUserInput, selected: this.selected, }); } - - // MDC won't assign the selected class until the animation finishes, but that may not - // happen if animations are disabled. If we detect such a case, assign the class manually. - if (this._animationsDisabled) { - this._elementRef.nativeElement.classList.toggle(MDCChipCssClasses.SELECTED, isSelected); - } } } diff --git a/src/material-experimental/mdc-chips/chip-remove.spec.ts b/src/material-experimental/mdc-chips/chip-remove.spec.ts index 427ffb9fe3f3..b747e7d0c879 100644 --- a/src/material-experimental/mdc-chips/chip-remove.spec.ts +++ b/src/material-experimental/mdc-chips/chip-remove.spec.ts @@ -3,13 +3,11 @@ import {waitForAsync, ComponentFixture, TestBed, fakeAsync, flush} from '@angula import {dispatchKeyboardEvent, dispatchMouseEvent} from '@angular/cdk/testing/private'; import {By} from '@angular/platform-browser'; import {SPACE, ENTER} from '@angular/cdk/keycodes'; -import {MDCChipAnimation, MDCChipCssClasses} from '@material/chips/chip'; import {MatChip, MatChipsModule} from './index'; describe('MDC-based Chip Remove', () => { let fixture: ComponentFixture; let testChip: TestChip; - let chipInstance: MatChip; let chipNativeElement: HTMLElement; beforeEach(waitForAsync(() => { @@ -28,28 +26,8 @@ describe('MDC-based Chip Remove', () => { const chipDebugElement = fixture.debugElement.query(By.directive(MatChip))!; chipNativeElement = chipDebugElement.nativeElement; - chipInstance = chipDebugElement.componentInstance; })); - function triggerRemoveSequence() { - // At the time of writing, MDC's removal sequence requires the following to happen: - // 1. Button is clicked, triggering the animation. - // 2. Before the animation has finished, the `--hidden` class is added. - // 3. Animation callback fires at some point. It doesn't really matter for the test, - // but it does queue up some `requestAnimationFrame` calls that we need to flush. - // 4. `transitionend` callback fires and finishes the removal sequence if the - // `--hidden` class exists. - fixture.detectChanges(); - (chipInstance as any)._handleAnimationend({ - animationName: MDCChipAnimation.EXIT, - target: chipNativeElement, - }); - flush(); - (chipInstance as any)._handleTransitionend({target: chipNativeElement}); - flush(); - fixture.detectChanges(); - } - describe('basic behavior', () => { it('should apply a CSS class to the remove icon', fakeAsync(() => { const buttonElement = chipNativeElement.querySelector('.mdc-evolution-chip__icon--trailing')!; @@ -71,19 +49,10 @@ describe('MDC-based Chip Remove', () => { fixture.detectChanges(); chipNativeElement.querySelector('button')!.click(); - triggerRemoveSequence(); - - expect(testChip.didRemove).toHaveBeenCalled(); - })); - - it('should not start MDC exit animation if parent chip is disabled', fakeAsync(() => { - testChip.removable = true; - testChip.disabled = true; fixture.detectChanges(); + flush(); - chipNativeElement.querySelector('button')!.click(); - - expect(chipNativeElement.classList.contains(MDCChipCssClasses.HIDDEN)).toBe(false); + expect(testChip.didRemove).toHaveBeenCalled(); })); it('should not make the element aria-hidden when it is focusable', fakeAsync(() => { @@ -100,7 +69,8 @@ describe('MDC-based Chip Remove', () => { fixture.detectChanges(); const event = dispatchKeyboardEvent(buttonElement, 'keydown', SPACE); - triggerRemoveSequence(); + fixture.detectChanges(); + flush(); expect(event.defaultPrevented).toBe(true); })); @@ -112,7 +82,8 @@ describe('MDC-based Chip Remove', () => { fixture.detectChanges(); const event = dispatchKeyboardEvent(buttonElement, 'keydown', ENTER); - triggerRemoveSequence(); + fixture.detectChanges(); + flush(); expect(event.defaultPrevented).toBe(true); })); @@ -125,7 +96,8 @@ describe('MDC-based Chip Remove', () => { it('should prevent the default click action', fakeAsync(() => { const buttonElement = chipNativeElement.querySelector('button')!; const event = dispatchMouseEvent(buttonElement, 'click'); - triggerRemoveSequence(); + fixture.detectChanges(); + flush(); expect(event.defaultPrevented).toBe(true); })); diff --git a/src/material-experimental/mdc-chips/chip-row.spec.ts b/src/material-experimental/mdc-chips/chip-row.spec.ts index cb85899e480a..00432532ba29 100644 --- a/src/material-experimental/mdc-chips/chip-row.spec.ts +++ b/src/material-experimental/mdc-chips/chip-row.spec.ts @@ -127,7 +127,7 @@ describe('MDC-based Row Chips', () => { spyOn(testComponent, 'chipRemove'); - chipInstance._keydown(DELETE_EVENT); + chipInstance._handleKeydown(DELETE_EVENT); fixture.detectChanges(); expect(testComponent.chipRemove).toHaveBeenCalled(); @@ -138,7 +138,7 @@ describe('MDC-based Row Chips', () => { spyOn(testComponent, 'chipRemove'); - chipInstance._keydown(BACKSPACE_EVENT); + chipInstance._handleKeydown(BACKSPACE_EVENT); fixture.detectChanges(); expect(testComponent.chipRemove).toHaveBeenCalled(); @@ -156,7 +156,7 @@ describe('MDC-based Row Chips', () => { spyOn(testComponent, 'chipRemove'); - chipInstance._keydown(DELETE_EVENT); + chipInstance._handleKeydown(DELETE_EVENT); fixture.detectChanges(); expect(testComponent.chipRemove).not.toHaveBeenCalled(); @@ -168,7 +168,7 @@ describe('MDC-based Row Chips', () => { spyOn(testComponent, 'chipRemove'); // Use the delete to remove the chip - chipInstance._keydown(BACKSPACE_EVENT); + chipInstance._handleKeydown(BACKSPACE_EVENT); fixture.detectChanges(); expect(testComponent.chipRemove).not.toHaveBeenCalled(); @@ -266,8 +266,8 @@ describe('MDC-based Row Chips', () => { expect(testComponent.chipDestroy).not.toHaveBeenCalled(); }); - it('should stop editing on focusout', fakeAsync(() => { - dispatchFakeEvent(primaryAction, 'focusout', true); + it('should stop editing on blur', fakeAsync(() => { + chipInstance._onBlur.next(); flush(); expect(testComponent.chipEdit).toHaveBeenCalled(); })); diff --git a/src/material-experimental/mdc-chips/chip-row.ts b/src/material-experimental/mdc-chips/chip-row.ts index 46a5308c7948..3865395f89b2 100644 --- a/src/material-experimental/mdc-chips/chip-row.ts +++ b/src/material-experimental/mdc-chips/chip-row.ts @@ -6,8 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directionality} from '@angular/cdk/bidi'; -import {BACKSPACE, DELETE, ENTER} from '@angular/cdk/keycodes'; +import {ENTER} from '@angular/cdk/keycodes'; import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; import { AfterViewInit, @@ -34,6 +33,7 @@ import { import {FocusMonitor} from '@angular/cdk/a11y'; import {MatChip, MatChipEvent} from './chip'; import {MatChipEditInput} from './chip-edit-input'; +import {takeUntil} from 'rxjs/operators'; /** Represents an event fired on an individual `mat-chip` when it is edited. */ export interface MatChipEditedEvent extends MatChipEvent { @@ -68,10 +68,7 @@ export interface MatChipEditedEvent extends MatChipEvent { '[attr.aria-label]': 'null', '[attr.role]': 'role', '(mousedown)': '_mousedown($event)', - '(keydown)': '_keydown($event)', - '(dblclick)': '_doubleclick()', - '(focusin)': '_focusin($event)', - '(focusout)': '_focusout($event)', + '(dblclick)': '_doubleclick($event)', }, providers: [{provide: MatChip, useExisting: MatChipRow}], encapsulation: ViewEncapsulation.None, @@ -94,19 +91,12 @@ export class MatChipRow extends MatChip implements AfterViewInit { _isEditing = false; - /** - * Timeout used to give some time between `focusin` and `focusout` - * in order to determine whether focus has left the chip. - */ - private _focusoutTimeout: number | null; - constructor( changeDetectorRef: ChangeDetectorRef, elementRef: ElementRef, ngZone: NgZone, focusMonitor: FocusMonitor, @Inject(DOCUMENT) _document: any, - @Optional() dir: Directionality, @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string, @Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) @@ -119,48 +109,22 @@ export class MatChipRow extends MatChip implements AfterViewInit { ngZone, focusMonitor, _document, - dir, animationMode, globalRippleOptions, tabIndex, ); this.role = 'row'; - } - - override _hasTrailingIcon() { - // The trailing icon is hidden while editing. - return !this._isEditing && super._hasTrailingIcon(); - } - - /** - * Emits a blur event when one of the gridcells loses focus, unless focus moved - * to the other gridcell. - */ - _focusout() { - if (this._focusoutTimeout) { - clearTimeout(this._focusoutTimeout); - } - - // Wait to see if focus moves to the other gridcell - this._focusoutTimeout = window.setTimeout(() => { + this._onBlur.pipe(takeUntil(this.destroyed)).subscribe(() => { if (this._isEditing) { this._onEditFinish(); } - - this._hasFocusInternal = false; - this._onBlur.next({chip: this}); }); } - /** Records that the chip has focus when one of the gridcells is focused. */ - _focusin() { - if (this._focusoutTimeout) { - clearTimeout(this._focusoutTimeout); - this._focusoutTimeout = null; - } - - this._hasFocusInternal = true; + override _hasTrailingIcon() { + // The trailing icon is hidden while editing. + return !this._isEditing && super._hasTrailingIcon(); } /** Sends focus to the first gridcell when the user clicks anywhere inside the chip. */ @@ -174,41 +138,36 @@ export class MatChipRow extends MatChip implements AfterViewInit { } } - /** Handles custom key presses. */ - _keydown(event: KeyboardEvent): void { - if (this.disabled) { - return; - } - - switch (event.keyCode) { - case ENTER: - if (this._isEditing) { - event.preventDefault(); - // Wrap in a timeout so the timing is consistent as when it is emitted in `focusout`. - setTimeout(() => this._onEditFinish()); - } else if (this.editable) { - this._startEditing(); - } - break; - case DELETE: - case BACKSPACE: - if (!this._isEditing) { - // Remove the focused chip - this.remove(); - // Always prevent so page navigation does not occur - event.preventDefault(); - } - break; + override _handleKeydown(event: KeyboardEvent): void { + if (event.keyCode === ENTER && !this.disabled) { + if (this._isEditing) { + event.preventDefault(); + this._onEditFinish(); + } else if (this.editable) { + this._startEditing(event); + } + } else if (this._isEditing) { + // Stop the event from reaching the chip set in order to avoid navigating. + event.stopPropagation(); + } else { + super._handleKeydown(event); } } - _doubleclick() { + _doubleclick(event: MouseEvent) { if (!this.disabled && this.editable) { - this._startEditing(); + this._startEditing(event); } } - private _startEditing() { + private _startEditing(event: Event) { + if ( + !this.primaryAction || + (this.removeIcon && this._getSourceAction(event.target as Node) === this.removeIcon) + ) { + return; + } + // The value depends on the DOM so we need to extract it before we flip the flag. const value = this.value; @@ -222,16 +181,20 @@ export class MatChipRow extends MatChip implements AfterViewInit { } private _onEditFinish() { - // If the edit input is still focused or focus was returned to the body after it was destroyed, - // return focus to the chip contents. - if ( - this._document.activeElement === this._getEditInput().getNativeElement() || - this._document.activeElement === this._document.body - ) { - this.primaryAction.focus(); + if (this.primaryAction) { + // If the edit input is still focused or focus was returned to the body after it was destroyed, + // return focus to the chip contents. + if ( + this._document.activeElement === this._getEditInput().getNativeElement() || + this._document.activeElement === this._document.body + ) { + this.primaryAction.focus(); + } + + this.primaryAction.isInteractive = true; } + this.edited.emit({chip: this, value: this._getEditInput().getValue()}); - this.primaryAction.isInteractive = true; this._isEditing = false; } diff --git a/src/material-experimental/mdc-chips/chip-set.ts b/src/material-experimental/mdc-chips/chip-set.ts index 8552ea0e5c55..ede85c8e3ed2 100644 --- a/src/material-experimental/mdc-chips/chip-set.ts +++ b/src/material-experimental/mdc-chips/chip-set.ts @@ -6,38 +6,27 @@ * found in the LICENSE file at https://angular.io/license */ -import {LiveAnnouncer} from '@angular/cdk/a11y'; +import {FocusKeyManager} from '@angular/cdk/a11y'; +import {Directionality} from '@angular/cdk/bidi'; import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; -import {DOCUMENT} from '@angular/common'; import { - AfterContentInit, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChildren, ElementRef, - Inject, Input, OnDestroy, + Optional, QueryList, ViewEncapsulation, } from '@angular/core'; import {HasTabIndex, mixinTabIndex} from '@angular/material-experimental/mdc-core'; -import { - MDCChipSetFoundation, - MDCChipSetAdapter, - MDCChipFoundation, - MDCChipEvents, - ChipAnimationEvent, - ChipInteractionEvent, - ChipNavigationEvent, - MDCChipActionType, -} from '@material/chips'; import {merge, Observable, Subject} from 'rxjs'; import {startWith, switchMap, takeUntil} from 'rxjs/operators'; import {MatChip, MatChipEvent} from './chip'; -import {emitCustomEvent} from './emit-event'; +import {MatChipAction} from './chip-action'; /** * Boilerplate for applying mixins to MatChipSet. @@ -64,6 +53,7 @@ const _MatChipSetMixinBase = mixinTabIndex(MatChipSetBase); styleUrls: ['chip-set.css'], host: { 'class': 'mat-mdc-chip-set mdc-evolution-chip-set', + '(keydown)': '_handleKeydown($event)', '[attr.role]': 'role', }, encapsulation: ViewEncapsulation.None, @@ -71,17 +61,13 @@ const _MatChipSetMixinBase = mixinTabIndex(MatChipSetBase); }) export class MatChipSet extends _MatChipSetMixinBase - implements AfterContentInit, AfterViewInit, HasTabIndex, OnDestroy + implements AfterViewInit, HasTabIndex, OnDestroy { - /** - * When a chip is destroyed, we store the index of the destroyed chip until the chips - * query list notifies about the update. This is necessary because we cannot determine an - * appropriate chip that should receive focus until the array of chips updated completely. - */ - protected _lastDestroyedChipIndex: number | null = null; + /** Index of the last destroyed chip that had focus. */ + private _lastDestroyedFocusedChipIndex: number | null = null; - /** The MDC foundation containing business logic for MDC chip-set. */ - protected _chipSetFoundation: MDCChipSetFoundation; + /** Used to manage focus within the chip list. */ + protected _keyManager: FocusKeyManager; /** Subject that emits when the component has been destroyed. */ protected _destroyed = new Subject(); @@ -89,59 +75,16 @@ export class MatChipSet /** Role to use if it hasn't been overwritten by the user. */ protected _defaultRole = 'presentation'; + /** Combined stream of all of the child chips' focus events. */ + get chipFocusChanges(): Observable { + return this._getChipStream(chip => chip._onFocus); + } + /** Combined stream of all of the child chips' remove events. */ get chipDestroyedChanges(): Observable { return this._getChipStream(chip => chip.destroyed); } - /** - * Implementation of the MDC chip-set adapter interface. - * These methods are called by the chip set foundation. - */ - protected _chipSetAdapter: MDCChipSetAdapter = { - announceMessage: message => this._liveAnnouncer.announce(message), - emitEvent: (eventName, eventDetail) => { - emitCustomEvent(this._elementRef.nativeElement, this._document, eventName, eventDetail, true); - }, - getAttribute: name => this._elementRef.nativeElement.getAttribute(name), - getChipActionsAtIndex: index => this._chipFoundation(index)?.getActions() || [], - getChipCount: () => this._chips.length, - getChipIdAtIndex: index => this._chipFoundation(index)?.getElementID() || '', - getChipIndexById: id => { - return this._chips.toArray().findIndex(chip => chip._getFoundation().getElementID() === id); - }, - isChipFocusableAtIndex: (index, actionType) => { - return this._chipFoundation(index)?.isActionFocusable(actionType) || false; - }, - isChipSelectableAtIndex: (index, actionType) => { - return this._chipFoundation(index)?.isActionSelectable(actionType) || false; - }, - isChipSelectedAtIndex: (index, actionType) => { - return this._chipFoundation(index)?.isActionSelected(actionType) || false; - }, - removeChipAtIndex: index => this._chips.toArray()[index]?.remove(), - setChipFocusAtIndex: (index, action, behavior) => { - this._chipFoundation(index)?.setActionFocus(action, behavior); - }, - setChipSelectedAtIndex: (index, actionType, isSelected) => { - // Setting the trailing action as deselected ends up deselecting the entire chip. - // This is working as expected, but it's not something we want so we only apply the - // selected state to the primary chip. - if (actionType === MDCChipActionType.PRIMARY) { - this._chipFoundation(index)?.setActionSelected(actionType, isSelected); - } - }, - startChipAnimationAtIndex: (index, animation) => { - this._chipFoundation(index)?.startAnimation(animation); - }, - }; - - /** - * Map from class to whether the class is enabled. - * Enabled classes are set on the MDC chip-set div. - */ - _mdcClasses: {[key: string]: boolean} = {}; - /** Whether the chip set is disabled. */ @Input() get disabled(): boolean { @@ -186,56 +129,27 @@ export class MatChipSet }) _chips: QueryList; + /** Flat list of all the actions contained within the chips. */ + _chipActions = new QueryList(); + constructor( - private _liveAnnouncer: LiveAnnouncer, - @Inject(DOCUMENT) private _document: any, protected _elementRef: ElementRef, protected _changeDetectorRef: ChangeDetectorRef, + @Optional() private _dir: Directionality, ) { super(_elementRef); - const element = _elementRef.nativeElement; - this._chipSetFoundation = new MDCChipSetFoundation(this._chipSetAdapter); - element.addEventListener(MDCChipEvents.ANIMATION, this._handleChipAnimation); - element.addEventListener(MDCChipEvents.INTERACTION, this._handleChipInteraction); - element.addEventListener(MDCChipEvents.NAVIGATION, this._handleChipNavigation); } ngAfterViewInit() { - this._chipSetFoundation.init(); - } - - ngAfterContentInit() { - this._chips.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => { - if (this.disabled) { - // Since this happens after the content has been - // checked, we need to defer it to the next tick. - Promise.resolve().then(() => { - this._syncChipsState(); - }); - } - }); - - this.chipDestroyedChanges.pipe(takeUntil(this._destroyed)).subscribe((event: MatChipEvent) => { - const chip = event.chip; - const chipIndex = this._chips.toArray().indexOf(event.chip); - - // In case the chip that will be removed is currently focused, we temporarily store - // the index in order to be able to determine an appropriate sibling chip that will - // receive focus. - if (this._isValidIndex(chipIndex) && chip._hasFocus()) { - this._lastDestroyedChipIndex = chipIndex; - } - }); + this._setUpFocusManagement(); + this._trackChipSetChanges(); + this._trackDestroyedFocusedChip(); } ngOnDestroy() { - const element = this._elementRef.nativeElement; - element.removeEventListener(MDCChipEvents.ANIMATION, this._handleChipAnimation); - element.removeEventListener(MDCChipEvents.INTERACTION, this._handleChipInteraction); - element.removeEventListener(MDCChipEvents.NAVIGATION, this._handleChipNavigation); + this._chipActions.destroy(); this._destroyed.next(); this._destroyed.complete(); - this._chipSetFoundation.destroy(); } /** Checks whether any of the chips is focused. */ @@ -256,6 +170,13 @@ export class MatChipSet /** Dummy method for subclasses to override. Base chip set cannot be focused. */ focus() {} + /** Handles keyboard events on the chip set. */ + _handleKeydown(event: KeyboardEvent) { + if (this._originatesFromChip(event)) { + this._keyManager.onKeydown(event); + } + } + /** * Utility to ensure all indexes are valid. * @@ -266,11 +187,6 @@ export class MatChipSet return index >= 0 && index < this._chips.length; } - /** Checks whether an event comes from inside a chip element. */ - protected _originatesFromChip(event: Event): boolean { - return this._checkForClassInHierarchy(event, 'mdc-evolution-chip'); - } - /** * Removes the `tabindex` from the chip grid and resets it back afterwards, allowing the * user to tab out of it. This prevents the grid from capturing focus and redirecting @@ -302,34 +218,110 @@ export class MatChipSet ); } - protected _checkForClassInHierarchy(event: Event, className: string) { + /** Checks whether an event comes from inside a chip element. */ + protected _originatesFromChip(event: Event): boolean { let currentElement = event.target as HTMLElement | null; while (currentElement && currentElement !== this._elementRef.nativeElement) { // Null check the classList, because IE and Edge don't support it on all elements. - if (currentElement.classList && currentElement.classList.contains(className)) { + if (currentElement.classList && currentElement.classList.contains('mdc-evolution-chip')) { return true; } - currentElement = currentElement.parentElement; } - return false; } - private _chipFoundation(index: number): MDCChipFoundation | undefined { - return this._chips.toArray()[index]?._getFoundation(); + /** Sets up the chip set's focus management logic. */ + private _setUpFocusManagement() { + // Create a flat `QueryList` containing the actions of all of the chips. + // This allows us to navigate both within the chip and move to the next/previous + // one using the existing `ListKeyManager`. + this._chips.changes.pipe(startWith(this._chips)).subscribe((chips: QueryList) => { + const actions: MatChipAction[] = []; + chips.forEach(chip => chip._getActions().forEach(action => actions.push(action))); + this._chipActions.reset(actions); + this._chipActions.notifyOnChanges(); + }); + + this._keyManager = new FocusKeyManager(this._chipActions) + .withVerticalOrientation() + .withHorizontalOrientation(this._dir ? this._dir.value : 'ltr') + .withHomeAndEnd() + // Skip non-interactive and disabled actions since the user can't do anything with them. + .skipPredicate(action => !action.isInteractive || action.disabled); + + // Keep the manager active index in sync so that navigation picks + // up from the current chip if the user clicks into the list directly. + this.chipFocusChanges.pipe(takeUntil(this._destroyed)).subscribe(({chip}) => { + const action = chip._getSourceAction(document.activeElement as Element); + + if (action) { + this._keyManager.updateActiveItem(action); + } + }); + + this._dir?.change + .pipe(takeUntil(this._destroyed)) + .subscribe(direction => this._keyManager.withHorizontalOrientation(direction)); } - private _handleChipAnimation = (event: Event) => { - this._chipSetFoundation.handleChipAnimation(event as ChipAnimationEvent); - }; + /** Listens to changes in the chip set and syncs up the state of the individual chips. */ + private _trackChipSetChanges() { + this._chips.changes.pipe(startWith(null), takeUntil(this._destroyed)).subscribe(() => { + if (this.disabled) { + // Since this happens after the content has been + // checked, we need to defer it to the next tick. + Promise.resolve().then(() => this._syncChipsState()); + } - private _handleChipInteraction = (event: Event) => { - this._chipSetFoundation.handleChipInteraction(event as ChipInteractionEvent); - }; + this._redirectDestroyedChipFocus(); + }); + } + + /** Starts tracking the destroyed chips in order to capture the focused one. */ + private _trackDestroyedFocusedChip() { + this.chipDestroyedChanges.pipe(takeUntil(this._destroyed)).subscribe((event: MatChipEvent) => { + const chipArray = this._chips.toArray(); + const chipIndex = chipArray.indexOf(event.chip); + + // If the focused chip is destroyed, save its index so that we can move focus to the next + // chip. We only save the index here, rather than move the focus immediately, because we want + // to wait until the chip is removed from the chip list before focusing the next one. This + // allows us to keep focus on the same index if the chip gets swapped out. + if (this._isValidIndex(chipIndex) && event.chip._hasFocus()) { + this._lastDestroyedFocusedChipIndex = chipIndex; + } + }); + } - private _handleChipNavigation = (event: Event) => { - this._chipSetFoundation.handleChipNavigation(event as ChipNavigationEvent); - }; + /** + * Finds the next appropriate chip to move focus to, + * if the currently-focused chip is destroyed. + */ + private _redirectDestroyedChipFocus() { + if (this._lastDestroyedFocusedChipIndex == null) { + return; + } + + if (this._chips.length) { + const newIndex = Math.min(this._lastDestroyedFocusedChipIndex, this._chips.length - 1); + const chipToFocus = this._chips.toArray()[newIndex]; + + if (chipToFocus.disabled) { + // If we're down to one disabled chip, move focus back to the set. + if (this._chips.length === 1) { + this.focus(); + } else { + this._keyManager.setPreviousItemActive(); + } + } else { + chipToFocus.focus(); + } + } else { + this.focus(); + } + + this._lastDestroyedFocusedChipIndex = null; + } } diff --git a/src/material-experimental/mdc-chips/chip.spec.ts b/src/material-experimental/mdc-chips/chip.spec.ts index 026bbabc697b..a68e7811615c 100644 --- a/src/material-experimental/mdc-chips/chip.spec.ts +++ b/src/material-experimental/mdc-chips/chip.spec.ts @@ -194,7 +194,7 @@ describe('MDC-based MatChip', () => {
{{name}} @@ -211,7 +211,6 @@ class SingleChip { value: any; rippleDisabled: boolean = false; - chipFocus: (event?: MatChipEvent) => void = () => {}; chipDestroy: (event?: MatChipEvent) => void = () => {}; chipRemove: (event?: MatChipEvent) => void = () => {}; } diff --git a/src/material-experimental/mdc-chips/chip.ts b/src/material-experimental/mdc-chips/chip.ts index 87af59368d6c..866ebea0e3de 100644 --- a/src/material-experimental/mdc-chips/chip.ts +++ b/src/material-experimental/mdc-chips/chip.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directionality} from '@angular/cdk/bidi'; import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations'; import { @@ -40,19 +39,9 @@ import { mixinTabIndex, RippleGlobalOptions, } from '@angular/material-experimental/mdc-core'; -import { - MDCChipFoundation, - MDCChipAdapter, - MDCChipActionType, - MDCChipActionFocusBehavior, - MDCChipActionFoundation, - MDCChipActionEvents, - ActionInteractionEvent, - ActionNavigationEvent, - MDCChipActionInteractionTrigger, -} from '@material/chips'; import {FocusMonitor} from '@angular/cdk/a11y'; import {Subject} from 'rxjs'; +import {take} from 'rxjs/operators'; import { MatChipAvatar, MatChipTrailingIcon, @@ -61,8 +50,8 @@ import { MAT_CHIP_TRAILING_ICON, MAT_CHIP_REMOVE, } from './chip-icons'; -import {emitCustomEvent} from './emit-event'; import {MatChipAction} from './chip-action'; +import {BACKSPACE, DELETE, ENTER, SPACE} from '@angular/cdk/keycodes'; let uid = 0; @@ -78,7 +67,7 @@ export interface MatChipEvent { */ abstract class MatChipBase { abstract disabled: boolean; - constructor(public _elementRef: ElementRef) {} + constructor(public _elementRef: ElementRef) {} } const _MatChipMixinBase = mixinTabIndex(mixinColor(mixinDisableRipple(MatChipBase), 'primary'), -1); @@ -113,6 +102,8 @@ const _MatChipMixinBase = mixinTabIndex(mixinColor(mixinDisableRipple(MatChipBas '[attr.role]': 'role', '[attr.tabindex]': 'role ? tabIndex : null', '[attr.aria-label]': 'ariaLabel', + '(click)': '_handleClick($event)', + '(keydown)': '_handleKeydown($event)', }, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, @@ -139,7 +130,7 @@ export class MatChip @Input() role: string | null = null; /** Whether the chip has focus. */ - protected _hasFocusInternal = false; + private _hasFocusInternal = false; /** Whether moving focus into the chip is pending. */ private _pendingFocus: boolean; @@ -163,12 +154,7 @@ export class MatChip } set disabled(value: BooleanInput) { this._disabled = coerceBooleanProperty(value); - - if (this.removeIcon) { - this.removeIcon.disabled = this._disabled; - } - - this._chipFoundation.setDisabled(this._disabled); + this._syncActionDisabledStates(); } protected _disabled: boolean = false; @@ -217,9 +203,6 @@ export class MatChip /** Emitted when the chip is destroyed. */ @Output() readonly destroyed: EventEmitter = new EventEmitter(); - /** The MDC foundation containing business logic for MDC chip. */ - _chipFoundation: MDCChipFoundation; - /** The unstyled chip selector for this component. */ protected basicChipAttrName = 'mat-basic-chip'; @@ -238,67 +221,12 @@ export class MatChip /** Action receiving the primary set of user interactions. */ @ViewChild(MatChipAction) primaryAction: MatChipAction; - /** - * Implementation of the MDC chip adapter interface. - * These methods are called by the chip foundation. - */ - protected _chipAdapter: MDCChipAdapter = { - addClass: className => this._setMdcClass(className, true), - removeClass: className => this._setMdcClass(className, false), - hasClass: className => this._elementRef.nativeElement.classList.contains(className), - emitEvent: (eventName: string, data: T) => { - emitCustomEvent(this._elementRef.nativeElement, this._document, eventName, data, true); - }, - setStyleProperty: (propertyName: string, value: string) => { - this._elementRef.nativeElement.style.setProperty(propertyName, value); - }, - isRTL: () => this._dir?.value === 'rtl', - getAttribute: attributeName => this._elementRef.nativeElement.getAttribute(attributeName), - getElementID: () => this._elementRef.nativeElement.id, - getOffsetWidth: () => this._elementRef.nativeElement.offsetWidth, - getActions: () => { - const result: MDCChipActionType[] = []; - - if (this._getAction(MDCChipActionType.PRIMARY)) { - result.push(MDCChipActionType.PRIMARY); - } - - if (this._getAction(MDCChipActionType.TRAILING)) { - result.push(MDCChipActionType.TRAILING); - } - - return result; - }, - isActionSelectable: (action: MDCChipActionType) => { - return this._getAction(action)?.isSelectable() || false; - }, - isActionSelected: (action: MDCChipActionType) => { - return this._getAction(action)?.isSelected() || false; - }, - isActionDisabled: (action: MDCChipActionType) => { - return this._getAction(action)?.isDisabled() || false; - }, - isActionFocusable: (action: MDCChipActionType) => { - return this._getAction(action)?.isFocusable() || false; - }, - setActionSelected: (action: MDCChipActionType, isSelected: boolean) => { - this._getAction(action)?.setSelected(isSelected); - }, - setActionDisabled: (action: MDCChipActionType, isDisabled: boolean) => { - this._getAction(action)?.setDisabled(isDisabled); - }, - setActionFocus: (action: MDCChipActionType, behavior: MDCChipActionFocusBehavior) => { - this._getAction(action)?.setFocus(behavior); - }, - }; - constructor( public _changeDetectorRef: ChangeDetectorRef, elementRef: ElementRef, protected _ngZone: NgZone, private _focusMonitor: FocusMonitor, @Inject(DOCUMENT) _document: any, - @Optional() private _dir: Directionality, @Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string, @Optional() @Inject(MAT_RIPPLE_GLOBAL_OPTIONS) @@ -308,29 +236,19 @@ export class MatChip super(elementRef); const element = elementRef.nativeElement; this._document = _document; - this._chipFoundation = new MDCChipFoundation(this._chipAdapter); this._animationsDisabled = animationMode === 'NoopAnimations'; this._isBasicChip = element.hasAttribute(this.basicChipAttrName) || element.tagName.toLowerCase() === this.basicChipAttrName; - element.addEventListener(MDCChipActionEvents.INTERACTION, this._handleActionInteraction); - element.addEventListener(MDCChipActionEvents.NAVIGATION, this._handleActionNavigation); - _focusMonitor.monitor(elementRef, true); - - _ngZone.runOutsideAngular(() => { - element.addEventListener('transitionend', this._handleTransitionend); - element.addEventListener('animationend', this._handleAnimationend); - }); - if (tabIndex != null) { this.tabIndex = parseInt(tabIndex) ?? this.defaultTabIndex; } + this._monitorFocus(); } ngAfterViewInit() { - this._chipFoundation.init(); - this._chipFoundation.setDisabled(this.disabled); - this._textElement = this._elementRef.nativeElement.querySelector('.mat-mdc-chip-action-label'); + this._textElement = this._elementRef.nativeElement.querySelector('.mat-mdc-chip-action-label')!; + Promise.resolve().then(() => this._syncActionDisabledStates()); if (this._pendingFocus) { this._pendingFocus = false; @@ -339,14 +257,9 @@ export class MatChip } ngOnDestroy() { - const element = this._elementRef.nativeElement; - element.removeEventListener(MDCChipActionEvents.INTERACTION, this._handleActionInteraction); - element.removeEventListener(MDCChipActionEvents.NAVIGATION, this._handleActionNavigation); - element.removeEventListener('transitionend', this._handleTransitionend); - element.removeEventListener('animationend', this._handleAnimationend); - this._chipFoundation.destroy(); this._focusMonitor.stopMonitoring(this._elementRef); this.destroyed.emit({chip: this}); + this.destroyed.complete(); } /** @@ -360,13 +273,6 @@ export class MatChip } } - /** Sets whether the given CSS class should be applied to the MDC chip. */ - private _setMdcClass(cssClass: string, active: boolean) { - const classes = this._elementRef.nativeElement.classList; - active ? classes.add(cssClass) : classes.remove(cssClass); - this._changeDetectorRef.markForCheck(); - } - /** Whether or not the ripple should be disabled. */ _isRippleDisabled(): boolean { return ( @@ -378,87 +284,118 @@ export class MatChip ); } - _getAction(type: MDCChipActionType): MDCChipActionFoundation | undefined { - switch (type) { - case MDCChipActionType.PRIMARY: - return this.primaryAction?._getFoundation(); - case MDCChipActionType.TRAILING: - return (this.removeIcon || this.trailingIcon)?._getFoundation(); - } - - return undefined; + /** Returns whether the chip has a trailing icon. */ + _hasTrailingIcon() { + return !!(this.trailingIcon || this.removeIcon); } - _getFoundation() { - return this._chipFoundation; + /** Handles click events on the chip. */ + _handleClick(event: MouseEvent) { + const action = this._getSourceAction(event.target as Node); + + if (action) { + this._handleClickLikeInteraction(event, action); + } } - _hasTrailingIcon() { - return !!(this.trailingIcon || this.removeIcon); + /** Handles keyboard events on the chip. */ + _handleKeydown(event: KeyboardEvent) { + const action = this._getSourceAction(event.target as Node); + const keyCode = event.keyCode; + + if (action && (keyCode === ENTER || keyCode === SPACE)) { + this._handleClickLikeInteraction(event, action); + } else if (keyCode === BACKSPACE || keyCode === DELETE) { + event.preventDefault(); + this.remove(); + } } /** Allows for programmatic focusing of the chip. */ focus(): void { - if (this.disabled) { - return; + if (!this.disabled) { + // If `focus` is called before `ngAfterViewInit`, we won't have access to the primary action. + // This can happen if the consumer tries to focus a chip immediately after it is added. + // Queue the method to be called again on init. + if (this.primaryAction) { + this.primaryAction.focus(); + } else { + this._pendingFocus = true; + } } + } - // If `focus` is called before `ngAfterViewInit`, we won't have access to the primary action. - // This can happen if the consumer tries to focus a chip immediately after it is added. - // Queue the method to be called again on init. - if (!this.primaryAction) { - this._pendingFocus = true; - return; - } + /** Gets the action that contains a specific target node. */ + _getSourceAction(target: Node): MatChipAction | undefined { + return this._getActions().find(action => { + const element = action._elementRef.nativeElement; + return element === target || element.contains(target); + }); + } + + /** Gets all of the actions within the chip. */ + _getActions(): MatChipAction[] { + const result: MatChipAction[] = []; - if (!this._hasFocus()) { - this._onFocus.next({chip: this}); - this._hasFocusInternal = true; + if (this.primaryAction) { + result.push(this.primaryAction); } - this.primaryAction.focus(); - } + if (this.removeIcon) { + result.push(this.removeIcon); + } - /** Overridden by MatChipOption. */ - protected _onChipInteraction(event: ActionInteractionEvent) { - const removeElement = this.removeIcon?._elementRef.nativeElement; - const trigger = event.detail.trigger; - - // MDC's removal process requires an `animationend` event followed by a `transitionend` - // event coming from the chip, which in turn will call `remove`. While we can stub - // out these events in our own tests, they can be difficult to fake for consumers that are - // testing our components or are wrapping them. We skip the entire sequence and trigger the - // removal directly in order to make the component easier to deal with. - if ( - removeElement && - (trigger === MDCChipActionInteractionTrigger.CLICK || - trigger === MDCChipActionInteractionTrigger.ENTER_KEY || - trigger === MDCChipActionInteractionTrigger.SPACEBAR_KEY) && - (event.target === removeElement || removeElement.contains(event.target)) - ) { - this.remove(); - } else { - this._chipFoundation.handleActionInteraction(event); + if (this.trailingIcon) { + result.push(this.trailingIcon); } - } - private _handleActionInteraction = (event: Event) => { - this._onChipInteraction(event as ActionInteractionEvent); - }; + return result; + } - private _handleActionNavigation = (event: Event) => { - this._chipFoundation.handleActionNavigation(event as ActionNavigationEvent); - }; + /** Handles interactions with the primary action of the chip. */ + protected _handlePrimaryActionInteraction() { + // Empty here, but is overwritten in child classes. + } - private _handleTransitionend = (event: TransitionEvent) => { - if (event.target === this._elementRef.nativeElement) { - this._ngZone.run(() => this._chipFoundation.handleTransitionEnd()); + /** Handles clicks or enter/space presses on a specific action. */ + private _handleClickLikeInteraction(event: Event, action: MatChipAction) { + if (action.isInteractive && !action.disabled) { + if (action === this.removeIcon) { + event.preventDefault(); + event.stopPropagation(); + this.remove(); + } else if (action === this.primaryAction) { + event.preventDefault(); + this._handlePrimaryActionInteraction(); + } } - }; + } - private _handleAnimationend = (event: AnimationEvent) => { - if (event.target === this._elementRef.nativeElement) { - this._ngZone.run(() => this._chipFoundation.handleAnimationEnd(event)); - } - }; + /** Starts the focus monitoring process on the chip. */ + private _monitorFocus() { + this._focusMonitor.monitor(this._elementRef, true).subscribe(origin => { + const hasFocus = origin !== null; + + if (hasFocus !== this._hasFocusInternal) { + this._hasFocusInternal = hasFocus; + + if (hasFocus) { + this._onFocus.next({chip: this}); + } else { + // When animations are enabled, Angular may end up removing the chip from the DOM a little + // earlier than usual, causing it to be blurred and throwing off the logic in the chip list + // that moves focus not the next item. To work around the issue, we defer marking the chip + // as not focused until the next time the zone stabilizes. + this._ngZone.onStable + .pipe(take(1)) + .subscribe(() => this._ngZone.run(() => this._onBlur.next({chip: this}))); + } + } + }); + } + + /** Syncs up the disabled state of the actions with the one of the chip. */ + private _syncActionDisabledStates() { + this._getActions().forEach(action => (action.disabled = this._disabled)); + } } diff --git a/src/material-experimental/mdc-chips/emit-event.ts b/src/material-experimental/mdc-chips/emit-event.ts deleted file mode 100644 index b208b6d3f0d8..000000000000 --- a/src/material-experimental/mdc-chips/emit-event.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -/** - * Emits a custom event from an element. - * @param element Element from which to emit the event. - * @param _document Document that the element is placed in. - * @param eventName Name of the event. - * @param data Data attached to the event. - * @param shouldBubble Whether the event should bubble. - */ -export function emitCustomEvent( - element: HTMLElement, - _document: Document, - eventName: string, - data: T, - shouldBubble: boolean, -): void { - let event: CustomEvent; - if (typeof CustomEvent === 'function') { - event = new CustomEvent(eventName, {bubbles: shouldBubble, detail: data}); - } else { - event = _document.createEvent('CustomEvent'); - event.initCustomEvent(eventName, shouldBubble, false, data); - } - - element.dispatchEvent(event); -} From 1dbac2d1a80f199d7aeb796628c7df7d32451b49 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Fri, 27 May 2022 13:12:30 +0200 Subject: [PATCH 2/2] fixup! refactor(material-experimental/mdc-chips): remove usage of MDC adapter --- .../mdc-chips/chip-action.ts | 61 +++++++++++--- .../mdc-chips/chip-default-options.ts | 20 ----- .../mdc-chips/chip-grid.ts | 16 ++-- .../mdc-chips/chip-icons.ts | 43 +++++----- .../mdc-chips/chip-input.ts | 2 +- .../mdc-chips/chip-listbox.ts | 8 ++ .../mdc-chips/chip-option.ts | 34 +++----- .../mdc-chips/chip-row.spec.ts | 10 --- .../mdc-chips/chip-row.ts | 31 +++---- src/material-experimental/mdc-chips/chip.ts | 84 +++++-------------- src/material-experimental/mdc-chips/module.ts | 2 +- .../mdc-chips/public-api.ts | 2 +- src/material-experimental/mdc-chips/tokens.ts | 46 ++++++++++ 13 files changed, 188 insertions(+), 171 deletions(-) delete mode 100644 src/material-experimental/mdc-chips/chip-default-options.ts create mode 100644 src/material-experimental/mdc-chips/tokens.ts diff --git a/src/material-experimental/mdc-chips/chip-action.ts b/src/material-experimental/mdc-chips/chip-action.ts index 63c3bc4df228..153c71a7efff 100644 --- a/src/material-experimental/mdc-chips/chip-action.ts +++ b/src/material-experimental/mdc-chips/chip-action.ts @@ -6,15 +6,17 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, ElementRef, Input} from '@angular/core'; -import { - CanDisable, - HasTabIndex, - mixinDisabled, - mixinTabIndex, -} from '@angular/material-experimental/mdc-core'; +import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion'; +import {ENTER, SPACE} from '@angular/cdk/keycodes'; +import {Directive, ElementRef, Inject, Input} from '@angular/core'; +import {HasTabIndex, mixinTabIndex} from '@angular/material-experimental/mdc-core'; +import {MAT_CHIP} from './tokens'; -const _MatChipActionMixinBase = mixinTabIndex(mixinDisabled(class {}), -1); +abstract class _MatChipActionBase { + abstract disabled: boolean; +} + +const _MatChipActionMixinBase = mixinTabIndex(_MatChipActionBase, -1); /** * Section within a chip. @@ -33,16 +35,36 @@ const _MatChipActionMixinBase = mixinTabIndex(mixinDisabled(class {}), -1); '[attr.tabindex]': '(disabled || !isInteractive) ? null : tabIndex', '[attr.disabled]': "disabled ? '' : null", '[attr.aria-disabled]': 'disabled', + '(click)': '_handleClick($event)', + '(keydown)': '_handleKeydown($event)', }, }) -export class MatChipAction extends _MatChipActionMixinBase implements CanDisable, HasTabIndex { +export class MatChipAction extends _MatChipActionMixinBase implements HasTabIndex { /** Whether the action is interactive. */ @Input() isInteractive = true; /** Whether this is the primary action in the chip. */ _isPrimary = true; - constructor(public _elementRef: ElementRef) { + /** Whether the action is disabled. */ + @Input() + get disabled(): boolean { + return this._disabled || this._parentChip.disabled; + } + set disabled(value: BooleanInput) { + this._disabled = coerceBooleanProperty(value); + } + private _disabled = false; + + constructor( + public _elementRef: ElementRef, + @Inject(MAT_CHIP) + protected _parentChip: { + _handlePrimaryActionInteraction(): void; + remove(): void; + disabled: boolean; + }, + ) { super(); if (_elementRef.nativeElement.nodeName === 'BUTTON') { @@ -53,4 +75,23 @@ export class MatChipAction extends _MatChipActionMixinBase implements CanDisable focus() { this._elementRef.nativeElement.focus(); } + + _handleClick(event: MouseEvent) { + if (!this.disabled && this.isInteractive && this._isPrimary) { + event.preventDefault(); + this._parentChip._handlePrimaryActionInteraction(); + } + } + + _handleKeydown(event: KeyboardEvent) { + if ( + (event.keyCode === ENTER || event.keyCode === SPACE) && + !this.disabled && + this.isInteractive && + this._isPrimary + ) { + event.preventDefault(); + this._parentChip._handlePrimaryActionInteraction(); + } + } } diff --git a/src/material-experimental/mdc-chips/chip-default-options.ts b/src/material-experimental/mdc-chips/chip-default-options.ts deleted file mode 100644 index 3f9f392cf6aa..000000000000 --- a/src/material-experimental/mdc-chips/chip-default-options.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {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. */ - separatorKeyCodes: readonly number[] | ReadonlySet; -} - -/** Injection token to be used to override the default options for the chips module. */ -export const MAT_CHIPS_DEFAULT_OPTIONS = new InjectionToken( - 'mat-chips-default-options', -); diff --git a/src/material-experimental/mdc-chips/chip-grid.ts b/src/material-experimental/mdc-chips/chip-grid.ts index d5cf726efded..cdf0c486b859 100644 --- a/src/material-experimental/mdc-chips/chip-grid.ts +++ b/src/material-experimental/mdc-chips/chip-grid.ts @@ -40,7 +40,7 @@ import { } from '@angular/material-experimental/mdc-core'; import {MatFormFieldControl} from '@angular/material-experimental/mdc-form-field'; import {MatChipTextControl} from './chip-text-control'; -import {Observable, Subject} from 'rxjs'; +import {Observable, Subject, merge} from 'rxjs'; import {takeUntil} from 'rxjs/operators'; import {MatChipEvent} from './chip'; import {MatChipRow} from './chip-row'; @@ -296,7 +296,7 @@ export class MatChipGrid this.stateChanges.next(); }); - this.chipFocusChanges + merge(this.chipFocusChanges, this._chips.changes) .pipe(takeUntil(this._destroyed)) .subscribe(() => this.stateChanges.next()); } @@ -449,10 +449,16 @@ export class MatChipGrid !this._chips.last.disabled ) { event.preventDefault(); - this._focusLastChip(); + + if (this._keyManager.activeItem) { + this._keyManager.setActiveItem(this._keyManager.activeItem); + } else { + this._focusLastChip(); + } } else { - // Use the super method here since it doesn't check for the input focused state. - // This allows focus to escape if there's only one disabled chip left in the list. + // Use the super method here since it doesn't check for the input + // focused state. This allows focus to escape if there's only one + // disabled chip left in the list. super._allowFocusEscape(); } } else if (!this._chipInput.focused) { diff --git a/src/material-experimental/mdc-chips/chip-icons.ts b/src/material-experimental/mdc-chips/chip-icons.ts index 49cbe6fb38d3..5db06291c672 100644 --- a/src/material-experimental/mdc-chips/chip-icons.ts +++ b/src/material-experimental/mdc-chips/chip-icons.ts @@ -6,15 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {Directive, InjectionToken} from '@angular/core'; +import {ENTER, SPACE} from '@angular/cdk/keycodes'; +import {Directive} from '@angular/core'; import {MatChipAction} from './chip-action'; - -/** - * Injection token that can be used to reference instances of `MatChipAvatar`. It serves as - * alternative token to the actual `MatChipAvatar` class which could cause unnecessary - * retention of the class and its directive metadata. - */ -export const MAT_CHIP_AVATAR = new InjectionToken('MatChipAvatar'); +import {MAT_CHIP_AVATAR, MAT_CHIP_REMOVE, MAT_CHIP_TRAILING_ICON} from './tokens'; /** * Directive to add CSS classes to chip leading icon. @@ -30,15 +25,6 @@ export const MAT_CHIP_AVATAR = new InjectionToken('MatChipAvatar' }) export class MatChipAvatar {} -/** - * Injection token that can be used to reference instances of `MatChipTrailingIcon`. It serves as - * alternative token to the actual `MatChipTrailingIcon` class which could cause unnecessary - * retention of the class and its directive metadata. - */ -export const MAT_CHIP_TRAILING_ICON = new InjectionToken( - 'MatChipTrailingIcon', -); - /** * Directive to add CSS classes to and configure attributes for chip trailing icon. * @docs-private @@ -62,13 +48,6 @@ export class MatChipTrailingIcon extends MatChipAction { override _isPrimary = false; } -/** - * Injection token that can be used to reference instances of `MatChipRemove`. It serves as - * alternative token to the actual `MatChipRemove` class which could cause unnecessary - * retention of the class and its directive metadata. - */ -export const MAT_CHIP_REMOVE = new InjectionToken('MatChipRemove'); - /** * Directive to remove the parent chip when the trailing icon is clicked or * when the ENTER key is pressed on it. @@ -98,4 +77,20 @@ export const MAT_CHIP_REMOVE = new InjectionToken('MatChipRemove' }) export class MatChipRemove extends MatChipAction { override _isPrimary = false; + + override _handleClick(event: MouseEvent): void { + if (!this.disabled) { + event.stopPropagation(); + event.preventDefault(); + this._parentChip.remove(); + } + } + + override _handleKeydown(event: KeyboardEvent) { + if ((event.keyCode === ENTER || event.keyCode === SPACE) && !this.disabled) { + event.stopPropagation(); + event.preventDefault(); + this._parentChip.remove(); + } + } } diff --git a/src/material-experimental/mdc-chips/chip-input.ts b/src/material-experimental/mdc-chips/chip-input.ts index 2588632e26dd..5894cbdaea64 100644 --- a/src/material-experimental/mdc-chips/chip-input.ts +++ b/src/material-experimental/mdc-chips/chip-input.ts @@ -21,7 +21,7 @@ import { Output, } from '@angular/core'; import {MatFormField, MAT_FORM_FIELD} from '@angular/material-experimental/mdc-form-field'; -import {MatChipsDefaultOptions, MAT_CHIPS_DEFAULT_OPTIONS} from './chip-default-options'; +import {MatChipsDefaultOptions, MAT_CHIPS_DEFAULT_OPTIONS} from './tokens'; import {MatChipGrid} from './chip-grid'; import {MatChipTextControl} from './chip-text-control'; diff --git a/src/material-experimental/mdc-chips/chip-listbox.ts b/src/material-experimental/mdc-chips/chip-listbox.ts index de98ff405554..6d97647944fd 100644 --- a/src/material-experimental/mdc-chips/chip-listbox.ts +++ b/src/material-experimental/mdc-chips/chip-listbox.ts @@ -195,6 +195,14 @@ export class MatChipListbox this.chipBlurChanges.pipe(takeUntil(this._destroyed)).subscribe(() => this._blur()); this.chipSelectionChanges.pipe(takeUntil(this._destroyed)).subscribe(event => { if (event.isUserInput) { + if (!this.multiple && event.selected) { + this._chips.forEach(chip => { + if (chip !== event.source) { + chip.deselect(); + } + }); + } + this._propagateChanges(); } }); diff --git a/src/material-experimental/mdc-chips/chip-option.ts b/src/material-experimental/mdc-chips/chip-option.ts index 000be24bbcec..94995f46db65 100644 --- a/src/material-experimental/mdc-chips/chip-option.ts +++ b/src/material-experimental/mdc-chips/chip-option.ts @@ -17,6 +17,7 @@ import { OnInit, } from '@angular/core'; import {MatChip} from './chip'; +import {MAT_CHIP} from './tokens'; /** Event object emitted by MatChipOption when selected or deselected. */ export class MatChipSelectionChange { @@ -38,7 +39,7 @@ export class MatChipSelectionChange { selector: 'mat-basic-chip-option, mat-chip-option', templateUrl: 'chip-option.html', styleUrls: ['chip.css'], - inputs: ['color', 'disableRipple', 'tabIndex'], + inputs: ['color', 'disabled', 'disableRipple', 'tabIndex'], host: { 'class': 'mat-mdc-chip mat-mdc-chip-option mdc-evolution-chip mdc-evolution-chip--filter', '[class.mat-mdc-chip-selected]': 'selected', @@ -64,7 +65,10 @@ export class MatChipSelectionChange { '[attr.role]': 'role', '[id]': 'id', }, - providers: [{provide: MatChip, useExisting: MatChipOption}], + providers: [ + {provide: MatChip, useExisting: MatChipOption}, + {provide: MAT_CHIP, useExisting: MatChipOption}, + ], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -97,10 +101,7 @@ export class MatChipOption extends MatChip implements OnInit { return this._selected; } set selected(value: BooleanInput) { - if (this.selectable) { - const coercedValue = coerceBooleanProperty(value); - this._setSelectedState(coercedValue, false); - } + this._setSelectedState(coerceBooleanProperty(value), false); } private _selected = false; @@ -126,37 +127,28 @@ export class MatChipOption extends MatChip implements OnInit { /** Selects the chip. */ select(): void { - if (this.selectable) { - this._setSelectedState(true, false); - } + this._setSelectedState(true, false); } /** Deselects the chip. */ deselect(): void { - if (this.selectable) { - this._setSelectedState(false, false); - } + this._setSelectedState(false, false); } /** Selects this chip and emits userInputSelection event */ selectViaInteraction(): void { - if (this.selectable) { - this._setSelectedState(true, true); - } + this._setSelectedState(true, true); } /** Toggles the current selected state of this chip. */ toggleSelected(isUserInput: boolean = false): boolean { - if (this.selectable) { - this._setSelectedState(!this.selected, isUserInput); - } - + this._setSelectedState(!this.selected, isUserInput); return this.selected; } - protected override _handlePrimaryActionInteraction() { + override _handlePrimaryActionInteraction() { if (this.selectable && !this.disabled) { - this._setSelectedState(!this.selected, true); + this.toggleSelected(true); } } diff --git a/src/material-experimental/mdc-chips/chip-row.spec.ts b/src/material-experimental/mdc-chips/chip-row.spec.ts index 00432532ba29..88c85e91c705 100644 --- a/src/material-experimental/mdc-chips/chip-row.spec.ts +++ b/src/material-experimental/mdc-chips/chip-row.spec.ts @@ -313,16 +313,6 @@ describe('MDC-based Row Chips', () => { expect(document.activeElement).toBe(primaryAction); })); - it('should focus the chip content if the body has focus on completion', fakeAsync(() => { - const chipValue = 'chip value'; - editInputInstance.setValue(chipValue); - (document.activeElement as HTMLElement).blur(); - dispatchKeyboardEvent(getEditInput(), 'keydown', ENTER); - fixture.detectChanges(); - flush(); - expect(document.activeElement).toBe(primaryAction); - })); - it('should not change focus if another element has focus on completion', fakeAsync(() => { const chipValue = 'chip value'; editInputInstance.setValue(chipValue); diff --git a/src/material-experimental/mdc-chips/chip-row.ts b/src/material-experimental/mdc-chips/chip-row.ts index 3865395f89b2..c5f3bb96af3a 100644 --- a/src/material-experimental/mdc-chips/chip-row.ts +++ b/src/material-experimental/mdc-chips/chip-row.ts @@ -34,6 +34,7 @@ import {FocusMonitor} from '@angular/cdk/a11y'; import {MatChip, MatChipEvent} from './chip'; import {MatChipEditInput} from './chip-edit-input'; import {takeUntil} from 'rxjs/operators'; +import {MAT_CHIP} from './tokens'; /** Represents an event fired on an individual `mat-chip` when it is edited. */ export interface MatChipEditedEvent extends MatChipEvent { @@ -49,7 +50,7 @@ export interface MatChipEditedEvent extends MatChipEvent { selector: 'mat-chip-row, mat-basic-chip-row', templateUrl: 'chip-row.html', styleUrls: ['chip.css'], - inputs: ['color', 'disableRipple', 'tabIndex'], + inputs: ['color', 'disabled', 'disableRipple', 'tabIndex'], host: { 'class': 'mat-mdc-chip mat-mdc-chip-row mdc-evolution-chip', '[class.mat-mdc-chip-with-avatar]': 'leadingIcon', @@ -70,7 +71,10 @@ export interface MatChipEditedEvent extends MatChipEvent { '(mousedown)': '_mousedown($event)', '(dblclick)': '_doubleclick($event)', }, - providers: [{provide: MatChip, useExisting: MatChipRow}], + providers: [ + {provide: MatChip, useExisting: MatChipRow}, + {provide: MAT_CHIP, useExisting: MatChipRow}, + ], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -181,21 +185,18 @@ export class MatChipRow extends MatChip implements AfterViewInit { } private _onEditFinish() { - if (this.primaryAction) { - // If the edit input is still focused or focus was returned to the body after it was destroyed, - // return focus to the chip contents. - if ( - this._document.activeElement === this._getEditInput().getNativeElement() || - this._document.activeElement === this._document.body - ) { - this.primaryAction.focus(); - } + this._isEditing = false; + this.primaryAction.isInteractive = true; + this.edited.emit({chip: this, value: this._getEditInput().getValue()}); - this.primaryAction.isInteractive = true; + // If the edit input is still focused or focus was returned to the body after it was destroyed, + // return focus to the chip contents. + if ( + this._document.activeElement === this._getEditInput().getNativeElement() || + this._document.activeElement === this._document.body + ) { + this.primaryAction.focus(); } - - this.edited.emit({chip: this, value: this._getEditInput().getValue()}); - this._isEditing = false; } /** diff --git a/src/material-experimental/mdc-chips/chip.ts b/src/material-experimental/mdc-chips/chip.ts index 866ebea0e3de..465473b20570 100644 --- a/src/material-experimental/mdc-chips/chip.ts +++ b/src/material-experimental/mdc-chips/chip.ts @@ -37,21 +37,16 @@ import { mixinColor, mixinDisableRipple, mixinTabIndex, + mixinDisabled, RippleGlobalOptions, } from '@angular/material-experimental/mdc-core'; import {FocusMonitor} from '@angular/cdk/a11y'; import {Subject} from 'rxjs'; import {take} from 'rxjs/operators'; -import { - MatChipAvatar, - MatChipTrailingIcon, - MatChipRemove, - MAT_CHIP_AVATAR, - MAT_CHIP_TRAILING_ICON, - MAT_CHIP_REMOVE, -} from './chip-icons'; +import {MatChipAvatar, MatChipTrailingIcon, MatChipRemove} from './chip-icons'; import {MatChipAction} from './chip-action'; -import {BACKSPACE, DELETE, ENTER, SPACE} from '@angular/cdk/keycodes'; +import {BACKSPACE, DELETE} from '@angular/cdk/keycodes'; +import {MAT_CHIP, MAT_CHIP_AVATAR, MAT_CHIP_REMOVE, MAT_CHIP_TRAILING_ICON} from './tokens'; let uid = 0; @@ -65,12 +60,19 @@ export interface MatChipEvent { * Boilerplate for applying mixins to MatChip. * @docs-private */ -abstract class MatChipBase { - abstract disabled: boolean; - constructor(public _elementRef: ElementRef) {} -} - -const _MatChipMixinBase = mixinTabIndex(mixinColor(mixinDisableRipple(MatChipBase), 'primary'), -1); +const _MatChipMixinBase = mixinTabIndex( + mixinColor( + mixinDisableRipple( + mixinDisabled( + class { + constructor(public _elementRef: ElementRef) {} + }, + ), + ), + 'primary', + ), + -1, +); /** * Material design styled Chip base component. Used inside the MatChipSet component. @@ -79,7 +81,7 @@ const _MatChipMixinBase = mixinTabIndex(mixinColor(mixinDisableRipple(MatChipBas */ @Component({ selector: 'mat-basic-chip, mat-chip', - inputs: ['color', 'disableRipple', 'tabIndex'], + inputs: ['color', 'disabled', 'disableRipple', 'tabIndex'], exportAs: 'matChip', templateUrl: 'chip.html', styleUrls: ['chip.css'], @@ -102,11 +104,11 @@ const _MatChipMixinBase = mixinTabIndex(mixinColor(mixinDisableRipple(MatChipBas '[attr.role]': 'role', '[attr.tabindex]': 'role ? tabIndex : null', '[attr.aria-label]': 'ariaLabel', - '(click)': '_handleClick($event)', '(keydown)': '_handleKeydown($event)', }, encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, + providers: [{provide: MAT_CHIP, useExisting: MatChip}], }) export class MatChip extends _MatChipMixinBase @@ -148,16 +150,6 @@ export class MatChip /** ARIA label for the content of the chip. */ @Input('aria-label') ariaLabel: string | null = null; - @Input() - get disabled(): boolean { - return this._disabled; - } - set disabled(value: BooleanInput) { - this._disabled = coerceBooleanProperty(value); - this._syncActionDisabledStates(); - } - protected _disabled: boolean = false; - private _textElement!: HTMLElement; /** @@ -248,7 +240,6 @@ export class MatChip ngAfterViewInit() { this._textElement = this._elementRef.nativeElement.querySelector('.mat-mdc-chip-action-label')!; - Promise.resolve().then(() => this._syncActionDisabledStates()); if (this._pendingFocus) { this._pendingFocus = false; @@ -289,23 +280,9 @@ export class MatChip return !!(this.trailingIcon || this.removeIcon); } - /** Handles click events on the chip. */ - _handleClick(event: MouseEvent) { - const action = this._getSourceAction(event.target as Node); - - if (action) { - this._handleClickLikeInteraction(event, action); - } - } - /** Handles keyboard events on the chip. */ _handleKeydown(event: KeyboardEvent) { - const action = this._getSourceAction(event.target as Node); - const keyCode = event.keyCode; - - if (action && (keyCode === ENTER || keyCode === SPACE)) { - this._handleClickLikeInteraction(event, action); - } else if (keyCode === BACKSPACE || keyCode === DELETE) { + if (event.keyCode === BACKSPACE || event.keyCode === DELETE) { event.preventDefault(); this.remove(); } @@ -353,24 +330,10 @@ export class MatChip } /** Handles interactions with the primary action of the chip. */ - protected _handlePrimaryActionInteraction() { + _handlePrimaryActionInteraction() { // Empty here, but is overwritten in child classes. } - /** Handles clicks or enter/space presses on a specific action. */ - private _handleClickLikeInteraction(event: Event, action: MatChipAction) { - if (action.isInteractive && !action.disabled) { - if (action === this.removeIcon) { - event.preventDefault(); - event.stopPropagation(); - this.remove(); - } else if (action === this.primaryAction) { - event.preventDefault(); - this._handlePrimaryActionInteraction(); - } - } - } - /** Starts the focus monitoring process on the chip. */ private _monitorFocus() { this._focusMonitor.monitor(this._elementRef, true).subscribe(origin => { @@ -393,9 +356,4 @@ export class MatChip } }); } - - /** Syncs up the disabled state of the actions with the one of the chip. */ - private _syncActionDisabledStates() { - this._getActions().forEach(action => (action.disabled = this._disabled)); - } } diff --git a/src/material-experimental/mdc-chips/module.ts b/src/material-experimental/mdc-chips/module.ts index ace98686a98e..90b564c19a41 100644 --- a/src/material-experimental/mdc-chips/module.ts +++ b/src/material-experimental/mdc-chips/module.ts @@ -15,7 +15,7 @@ import { MatRippleModule, } from '@angular/material-experimental/mdc-core'; import {MatChip} from './chip'; -import {MAT_CHIPS_DEFAULT_OPTIONS, MatChipsDefaultOptions} from './chip-default-options'; +import {MAT_CHIPS_DEFAULT_OPTIONS, MatChipsDefaultOptions} from './tokens'; import {MatChipEditInput} from './chip-edit-input'; import {MatChipGrid} from './chip-grid'; import {MatChipAvatar, MatChipRemove, MatChipTrailingIcon} from './chip-icons'; diff --git a/src/material-experimental/mdc-chips/public-api.ts b/src/material-experimental/mdc-chips/public-api.ts index dc6a425f3914..4971595984dc 100644 --- a/src/material-experimental/mdc-chips/public-api.ts +++ b/src/material-experimental/mdc-chips/public-api.ts @@ -14,7 +14,7 @@ export * from './chip-listbox'; export * from './chip-grid'; export * from './module'; export * from './chip-input'; -export * from './chip-default-options'; +export * from './tokens'; export * from './chip-icons'; export * from './chip-text-control'; export * from './chip-edit-input'; diff --git a/src/material-experimental/mdc-chips/tokens.ts b/src/material-experimental/mdc-chips/tokens.ts new file mode 100644 index 000000000000..ea5cd59883e6 --- /dev/null +++ b/src/material-experimental/mdc-chips/tokens.ts @@ -0,0 +1,46 @@ +/** + * @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 {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. */ + separatorKeyCodes: readonly number[] | ReadonlySet; +} + +/** Injection token to be used to override the default options for the chips module. */ +export const MAT_CHIPS_DEFAULT_OPTIONS = new InjectionToken( + 'mat-chips-default-options', +); + +/** + * Injection token that can be used to reference instances of `MatChipAvatar`. It serves as + * alternative token to the actual `MatChipAvatar` class which could cause unnecessary + * retention of the class and its directive metadata. + */ +export const MAT_CHIP_AVATAR = new InjectionToken('MatChipAvatar'); + +/** + * Injection token that can be used to reference instances of `MatChipTrailingIcon`. It serves as + * alternative token to the actual `MatChipTrailingIcon` class which could cause unnecessary + * retention of the class and its directive metadata. + */ +export const MAT_CHIP_TRAILING_ICON = new InjectionToken('MatChipTrailingIcon'); + +/** + * Injection token that can be used to reference instances of `MatChipRemove`. It serves as + * alternative token to the actual `MatChipRemove` class which could cause unnecessary + * retention of the class and its directive metadata. + */ +export const MAT_CHIP_REMOVE = new InjectionToken('MatChipRemove'); + +/** + * Injection token used to avoid a circular dependency between the `MatChip` and `MatChipAction`. + */ +export const MAT_CHIP = new InjectionToken('MatChip');