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: 3 additions & 1 deletion src/dev-app/mdc-chips/mdc-chips-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,10 @@ <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">
<mat-chip-row *ngFor="let person of people"
<!-- Disable the third chip to test focus management with a disabled chip. -->
<mat-chip-row *ngFor="let person of people; let index = index"
[editable]="editable"
[disabled]="index === 2"
(removed)="remove(person)"
(edited)="edit(person, $event)">
{{person.name}}
Expand Down
2 changes: 0 additions & 2 deletions src/material-experimental/mdc-chips/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ ng_module(
"@npm//@angular/common",
"@npm//@angular/core",
"@npm//@angular/forms",
"@npm//@material/chips",
],
)

Expand Down Expand Up @@ -91,7 +90,6 @@ ng_test_library(
"@npm//@angular/common",
"@npm//@angular/forms",
"@npm//@angular/platform-browser",
"@npm//@material/chips",
"@npm//rxjs",
],
)
Expand Down
147 changes: 46 additions & 101 deletions src/material-experimental/mdc-chips/chip-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,147 +6,92 @@
* 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 {
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);

/**
* Interactive element within a chip.
* Section 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]': `_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: <T>(eventName: string, data: T) => {
emitCustomEvent<T>(this._elementRef.nativeElement, this._document, eventName, data, true);
},
};

export class MatChipAction extends _MatChipActionMixinBase implements 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);
}
/** Whether the action is disabled. */
@Input()
get disabled(): boolean {
return this._disabled || this._parentChip.disabled;
}

protected _createFoundation(adapter: MDCChipActionAdapter): MDCChipActionFoundation {
return new MDCChipPrimaryActionFoundation(adapter);
set disabled(value: BooleanInput) {
this._disabled = coerceBooleanProperty(value);
}
private _disabled = false;

constructor(
public _elementRef: ElementRef,
@Inject(DOCUMENT) _document: any,
private _changeDetectorRef: ChangeDetectorRef,
public _elementRef: ElementRef<HTMLElement>,
@Inject(MAT_CHIP)
protected _parentChip: {
_handlePrimaryActionInteraction(): void;
remove(): void;
disabled: boolean;
},
) {
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;
_handleClick(event: MouseEvent) {
if (!this.disabled && this.isInteractive && this._isPrimary) {
event.preventDefault();
this._parentChip._handlePrimaryActionInteraction();
}
}

_updateTabindex(value: number) {
this.tabIndex = value;
this._changeDetectorRef.markForCheck();
_handleKeydown(event: KeyboardEvent) {
if (
(event.keyCode === ENTER || event.keyCode === SPACE) &&
!this.disabled &&
this.isInteractive &&
this._isPrimary
) {
event.preventDefault();
this._parentChip._handlePrimaryActionInteraction();
}
}
}
20 changes: 0 additions & 20 deletions src/material-experimental/mdc-chips/chip-default-options.ts

This file was deleted.

51 changes: 22 additions & 29 deletions src/material-experimental/mdc-chips/chip-grid.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
dispatchFakeEvent,
dispatchKeyboardEvent,
MockNgZone,
patchElementFocus,
typeInElement,
} from '@angular/cdk/testing/private';
import {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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]);
}));
Expand All @@ -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');
});

Expand Down Expand Up @@ -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');
}));

Expand Down
Loading