Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions src/dev-app/mdc-chips/mdc-chips-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,8 @@ <h4>Input is last child of chip grid</h4>
<mat-form-field class="demo-has-chip-list">
<mat-label>New Contributor...</mat-label>
<mat-chip-grid #chipGrid1 [(ngModel)]="selectedPeople" required [disabled]="disableInputs">
<!-- Disable the third chip to test focus management with a disabled chip. -->
<mat-chip-row *ngFor="let person of people; let index = index"
<mat-chip-row *ngFor="let person of people"
[editable]="editable"
[disabled]="index === 2"
(removed)="remove(person)"
(edited)="edit(person, $event)">
{{person.name}}
Expand Down
2 changes: 2 additions & 0 deletions src/material-experimental/mdc-chips/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ ng_module(
"@npm//@angular/common",
"@npm//@angular/core",
"@npm//@angular/forms",
"@npm//@material/chips",
],
)

Expand Down Expand Up @@ -90,6 +91,7 @@ ng_test_library(
"@npm//@angular/common",
"@npm//@angular/forms",
"@npm//@angular/platform-browser",
"@npm//@material/chips",
"@npm//rxjs",
],
)
Expand Down
147 changes: 101 additions & 46 deletions src/material-experimental/mdc-chips/chip-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,92 +6,147 @@
* found in the LICENSE file at https://angular.io/license
*/

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';
import {
AfterViewInit,
ChangeDetectorRef,
Directive,
ElementRef,
Inject,
Input,
OnChanges,
OnDestroy,
SimpleChanges,
} from '@angular/core';
import {DOCUMENT} from '@angular/common';
import {
MDCChipActionAdapter,
MDCChipActionFoundation,
MDCChipActionType,
MDCChipPrimaryActionFoundation,
} from '@material/chips';
import {emitCustomEvent} from './emit-event';
import {
CanDisable,
HasTabIndex,
mixinDisabled,
mixinTabIndex,
} from '@angular/material-experimental/mdc-core';

abstract class _MatChipActionBase {
abstract disabled: boolean;
}

const _MatChipActionMixinBase = mixinTabIndex(_MatChipActionBase, -1);
const _MatChipActionMixinBase = mixinTabIndex(mixinDisabled(class {}), -1);

/**
* Section within a chip.
* Interactive element within a chip.
* @docs-private
*/
@Directive({
selector: '[matChipAction]',
inputs: ['disabled', 'tabIndex'],
host: {
'class': 'mdc-evolution-chip__action mat-mdc-chip-action',
'[class.mdc-evolution-chip__action--primary]': '_isPrimary',
'[class.mdc-evolution-chip__action--primary]': `_getFoundation().actionType() === ${MDCChipActionType.PRIMARY}`,
// Note that while our actions are interactive, we have to add the `--presentational` class,
// in order to avoid some super-specific `:hover` styles from MDC.
'[class.mdc-evolution-chip__action--presentational]': '_isPrimary',
'[class.mdc-evolution-chip__action--trailing]': '!_isPrimary',
'[class.mdc-evolution-chip__action--presentational]': `_getFoundation().actionType() === ${MDCChipActionType.PRIMARY}`,
'[class.mdc-evolution-chip__action--trailing]': `_getFoundation().actionType() === ${MDCChipActionType.TRAILING}`,
'[attr.tabindex]': '(disabled || !isInteractive) ? null : tabIndex',
'[attr.disabled]': "disabled ? '' : null",
'[attr.aria-disabled]': 'disabled',
'(click)': '_handleClick($event)',
'(keydown)': '_handleKeydown($event)',
},
})
export class MatChipAction extends _MatChipActionMixinBase implements HasTabIndex {
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: <T>(eventName: string, data: T) => {
emitCustomEvent<T>(this._elementRef.nativeElement, this._document, eventName, data, true);
},
};

/** Whether the action is interactive. */
@Input() isInteractive = true;

/** Whether this is the primary action in the chip. */
_isPrimary = 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 the action is disabled. */
@Input()
get disabled(): boolean {
return this._disabled || this._parentChip.disabled;
_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);
}
}
set disabled(value: BooleanInput) {
this._disabled = coerceBooleanProperty(value);

protected _createFoundation(adapter: MDCChipActionAdapter): MDCChipActionFoundation {
return new MDCChipPrimaryActionFoundation(adapter);
}
private _disabled = false;

constructor(
public _elementRef: ElementRef<HTMLElement>,
@Inject(MAT_CHIP)
protected _parentChip: {
_handlePrimaryActionInteraction(): void;
remove(): void;
disabled: boolean;
},
public _elementRef: ElementRef,
@Inject(DOCUMENT) _document: any,
private _changeDetectorRef: ChangeDetectorRef,
) {
super();
this._foundation = this._createFoundation(this._adapter);

if (_elementRef.nativeElement.nodeName === 'BUTTON') {
_elementRef.nativeElement.setAttribute('type', 'button');
}
}

ngAfterViewInit() {
this._foundation.init();
this._foundation.setDisabled(this.disabled);
}

ngOnChanges(changes: SimpleChanges) {
if (changes['disabled']) {
this._foundation.setDisabled(this.disabled);
}
}

ngOnDestroy() {
this._foundation.destroy();
}

focus() {
this._elementRef.nativeElement.focus();
}

_handleClick(event: MouseEvent) {
if (!this.disabled && this.isInteractive && this._isPrimary) {
event.preventDefault();
this._parentChip._handlePrimaryActionInteraction();
}
_getFoundation() {
return this._foundation;
}

_handleKeydown(event: KeyboardEvent) {
if (
(event.keyCode === ENTER || event.keyCode === SPACE) &&
!this.disabled &&
this.isInteractive &&
this._isPrimary
) {
event.preventDefault();
this._parentChip._handlePrimaryActionInteraction();
}
_updateTabindex(value: number) {
this.tabIndex = value;
this._changeDetectorRef.markForCheck();
}
}
20 changes: 20 additions & 0 deletions src/material-experimental/mdc-chips/chip-default-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @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<number>;
}

/** Injection token to be used to override the default options for the chips module. */
export const MAT_CHIPS_DEFAULT_OPTIONS = new InjectionToken<MatChipsDefaultOptions>(
'mat-chips-default-options',
);
51 changes: 29 additions & 22 deletions src/material-experimental/mdc-chips/chip-grid.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
dispatchFakeEvent,
dispatchKeyboardEvent,
MockNgZone,
patchElementFocus,
typeInElement,
} from '@angular/cdk/testing/private';
import {
Expand All @@ -35,6 +34,7 @@ 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', () => {
Expand Down Expand Up @@ -199,7 +199,6 @@ 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);
Expand All @@ -209,7 +208,6 @@ describe('MDC-based MatChipGrid', () => {
testComponent.chips = [0];

spyOn(chipGridInstance, 'focus');
patchElementFocus(chips.last.primaryAction!._elementRef.nativeElement);
chips.last.focus();

testComponent.chips.pop();
Expand All @@ -218,22 +216,27 @@ 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();
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);
fixture = createComponent(StandardChipGridWithAnimations, BrowserAnimationsModule);

patchElementFocus(chips.last.primaryAction!._elementRef.nativeElement);
chips.last.focus();
fixture.detectChanges();
chips.last.focus();
fixture.detectChanges();

dispatchKeyboardEvent(chips.last._elementRef.nativeElement, 'keydown', BACKSPACE);
fixture.detectChanges();
tick(500);
expect(document.activeElement).toBe(primaryActions[primaryActions.length - 1]);

expect(document.activeElement).toBe(primaryActions[primaryActions.length - 2]);
}));
dispatchKeyboardEvent(chips.last._elementRef.nativeElement, 'keydown', BACKSPACE);
fixture.detectChanges();
tick(500);

expect(document.activeElement).toBe(primaryActions[primaryActions.length - 2]);
}),
);
});

it('should have a focus indicator', () => {
Expand Down Expand Up @@ -391,7 +394,6 @@ 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);
Expand Down Expand Up @@ -560,7 +562,14 @@ 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]);
}));
Expand All @@ -580,6 +589,7 @@ describe('MDC-based MatChipGrid', () => {
.map(chip => chip.nativeElement);

nativeChipGrid = fixture.debugElement.query(By.css('mat-chip-grid'))!.nativeElement;

nativeInput = fixture.nativeElement.querySelector('input');
});

Expand Down Expand Up @@ -720,21 +730,18 @@ 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;

patchElementFocus(firstAction);
firstAction.focus();
dispatchFakeEvent(nativeChips[0], 'focusin');
fixture.detectChanges();

expect(formField.classList).toContain('mat-focused');

firstAction.blur();
dispatchFakeEvent(nativeChips[0], 'focusout');
fixture.detectChanges();
tick();
fixture.detectChanges();
zone.simulateZoneExit();
fixture.detectChanges();
flush();

expect(formField.classList).not.toContain('mat-focused');
}));

Expand Down
Loading