Skip to content

Commit

Permalink
refactor(material-experimental/mdc-chips): implement trailing icon fo…
Browse files Browse the repository at this point in the history
…undation (#19318)
  • Loading branch information
mmalerba committed May 15, 2020
1 parent e43f4ac commit de155e2
Show file tree
Hide file tree
Showing 4 changed files with 689 additions and 608 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"@types/youtube": "^0.0.38",
"@webcomponents/custom-elements": "^1.1.0",
"core-js": "^2.6.9",
"material-components-web": "7.0.0-canary.047e6b337.0",
"material-components-web": "7.0.0-canary.058cfd23c.0",
"rxjs": "^6.5.3",
"systemjs": "0.19.43",
"tslib": "^1.10.0",
Expand Down
60 changes: 50 additions & 10 deletions src/material-experimental/mdc-chips/chip-icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@
*/

import {BooleanInput} from '@angular/cdk/coercion';
import {
ChangeDetectorRef,
Directive,
ElementRef,
} from '@angular/core';
import {ChangeDetectorRef, Directive, ElementRef, OnDestroy} from '@angular/core';
import {
CanDisable,
CanDisableCtor,
Expand All @@ -20,6 +16,7 @@ import {
mixinDisabled,
mixinTabIndex,
} from '@angular/material/core';
import {MDCChipTrailingActionAdapter, MDCChipTrailingActionFoundation} from '@material/chips';
import {Subject} from 'rxjs';


Expand Down Expand Up @@ -52,13 +49,52 @@ export class MatChipAvatar {
@Directive({
selector: 'mat-chip-trailing-icon, [matChipTrailingIcon]',
host: {
'class': 'mat-mdc-chip-trailing-icon mdc-chip__icon mdc-chip__icon--trailing',
'class':
'mat-mdc-chip-trailing-icon mdc-chip__icon mdc-chip__icon--trailing',
'tabindex': '-1',
'aria-hidden': 'true',
}
})
export class MatChipTrailingIcon {
constructor(public _elementRef: ElementRef) {}
export class MatChipTrailingIcon implements OnDestroy {
private _foundation: MDCChipTrailingActionFoundation;
private _adapter: MDCChipTrailingActionAdapter = {
focus: () => this._elementRef.nativeElement.focus(),
getAttribute: (name: string) =>
this._elementRef.nativeElement.getAttribute(name),
setAttribute:
(name: string, value: string) => {
this._elementRef.nativeElement.setAttribute(name, value);
},
// TODO(crisbeto): there's also a `trigger` parameter that the chip isn't
// handling yet. Consider passing it along once MDC start using it.
notifyInteraction:
() => {
// TODO(crisbeto): uncomment this code once we've inverted the
// dependency on `MatChip`. this._chip._notifyInteraction();
},

// TODO(crisbeto): there's also a `key` parameter that the chip isn't
// handling yet. Consider passing it along once MDC start using it.
notifyNavigation:
() => {
// TODO(crisbeto): uncomment this code once we've inverted the
// dependency on `MatChip`. this._chip._notifyNavigation();
}
};

constructor(
public _elementRef: ElementRef,
// TODO(crisbeto): currently the chip needs a reference to the trailing
// icon for the deprecated `setTrailingActionAttr` method. Until the
// method is removed, we can't use the chip here, because it causes a
// circular import. private _chip: MatChip
) {
this._foundation = new MDCChipTrailingActionFoundation(this._adapter);
}

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

focus() {
this._elementRef.nativeElement.focus();
Expand All @@ -68,15 +104,19 @@ export class MatChipTrailingIcon {
setAttribute(name: string, value: string) {
this._elementRef.nativeElement.setAttribute(name, value);
}

isNavigable() {
return this._foundation.isNavigable();
}
}

/**
* Boilerplate for applying mixins to MatChipRemove.
* @docs-private
*/
class MatChipRemoveBase extends MatChipTrailingIcon {
constructor(_elementRef: ElementRef) {
super(_elementRef);
constructor(elementRef: ElementRef) {
super(elementRef);
}
}

Expand Down
176 changes: 108 additions & 68 deletions src/material-experimental/mdc-chips/chip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,78 +245,99 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
protected _chipAdapter: MDCChipAdapter = {
addClass: (className) => this._setMdcClass(className, true),
removeClass: (className) => this._setMdcClass(className, false),
hasClass: (className) => this._elementRef.nativeElement.classList.contains(className),
addClassToLeadingIcon: (className) => this.leadingIcon.setClass(className, true),
removeClassFromLeadingIcon: (className) => this.leadingIcon.setClass(className, false),
eventTargetHasClass: (target: EventTarget | null, className: string) => {
// We need to null check the `classList`, because IE and Edge don't support it on SVG elements
// and Edge seems to throw for ripple elements, because they're outside the DOM.
return (target && (target as Element).classList) ?
(target as Element).classList.contains(className) : false;
},
notifyInteraction: () => this.interaction.emit(this.id),
notifySelection: () => {
// No-op. We call dispatchSelectionEvent ourselves in MatChipOption, because we want to
// specify whether selection occurred via user input.
},
notifyNavigation: () => {
// TODO: This is a new feature added by MDC; consider exposing this event to users in the
// future.
},
notifyTrailingIconInteraction: () => this.removeIconInteraction.emit(this.id),
notifyRemoval: () => {
this.removed.emit({ chip: this });

// When MDC removes a chip it just transitions it to `width: 0px` which means that it's still
// in the DOM and it's still focusable. Make it `display: none` so users can't tab into it.
this._elementRef.nativeElement.style.display = 'none';
},
getComputedStyleValue: propertyName => {
// This function is run when a chip is removed so it might be
// invoked during server-side rendering. Add some extra checks just in case.
if (typeof window !== 'undefined' && window) {
const getComputedStyle = window.getComputedStyle(this._elementRef.nativeElement);
return getComputedStyle.getPropertyValue(propertyName);
}
return '';
},
setStyleProperty: (propertyName: string, value: string) => {
this._elementRef.nativeElement.style.setProperty(propertyName, value);
},
hasClass: (className) =>
this._elementRef.nativeElement.classList.contains(className),
addClassToLeadingIcon: (className) =>
this.leadingIcon.setClass(className, true),
removeClassFromLeadingIcon: (className) =>
this.leadingIcon.setClass(className, false),
eventTargetHasClass:
(target: EventTarget|null, className: string) => {
// We need to null check the `classList`, because IE and Edge don't
// support it on SVG elements and Edge seems to throw for ripple
// elements, because they're outside the DOM.
return (target && (target as Element).classList) ?
(target as Element).classList.contains(className) :
false;
},
notifyInteraction: () => this._notifyInteraction(),
notifySelection:
() => {
// No-op. We call dispatchSelectionEvent ourselves in MatChipOption,
// because we want to specify whether selection occurred via user
// input.
},
notifyNavigation: () => this._notifyNavigation(),
notifyTrailingIconInteraction: () =>
this.removeIconInteraction.emit(this.id),
notifyRemoval:
() => {
this.removed.emit({chip: this});

// When MDC removes a chip it just transitions it to `width: 0px`
// which means that it's still in the DOM and it's still focusable.
// Make it `display: none` so users can't tab into it.
this._elementRef.nativeElement.style.display = 'none';
},
getComputedStyleValue:
propertyName => {
// This function is run when a chip is removed so it might be
// invoked during server-side rendering. Add some extra checks just in
// case.
if (typeof window !== 'undefined' && window) {
const getComputedStyle =
window.getComputedStyle(this._elementRef.nativeElement);
return getComputedStyle.getPropertyValue(propertyName);
}
return '';
},
setStyleProperty:
(propertyName: string, value: string) => {
this._elementRef.nativeElement.style.setProperty(propertyName, value);
},
hasLeadingIcon: () => !!this.leadingIcon,
hasTrailingAction: () => !!this.trailingIcon,
isTrailingActionNavigable:
() => {
if (this.trailingIcon) {
return this.trailingIcon.isNavigable();
}
return false;
},
isRTL: () => !!this._dir && this._dir.value === 'rtl',
focusPrimaryAction: () => {
// Angular Material MDC chips fully manage focus. TODO: Managing focus and handling keyboard
// events was added by MDC after our implementation; consider consolidating.
},
focusPrimaryAction:
() => {
// Angular Material MDC chips fully manage focus. TODO: Managing focus
// and handling keyboard events was added by MDC after our
// implementation; consider consolidating.
},
focusTrailingAction: () => {},
setTrailingActionAttr: (attr, value) =>
this.trailingIcon && this.trailingIcon.setAttribute(attr, value),
setPrimaryActionAttr: (name: string, value: string) => {
// MDC is currently using this method to set aria-checked on choice and filter chips,
// which in the MDC templates have role="checkbox" and role="radio" respectively.
// We have role="option" on those chips instead, so we do not want aria-checked.
// Since we also manage the tabindex ourselves, we don't allow MDC to set it.
if (name === 'aria-checked' || name === 'tabindex') {
return;
}
this._elementRef.nativeElement.setAttribute(name, value);
},
removeTrailingActionFocus: () => {},
setPrimaryActionAttr:
(name: string, value: string) => {
// MDC is currently using this method to set aria-checked on choice
// and filter chips, which in the MDC templates have role="checkbox"
// and role="radio" respectively. We have role="option" on those chips
// instead, so we do not want aria-checked. Since we also manage the
// tabindex ourselves, we don't allow MDC to set it.
if (name === 'aria-checked' || name === 'tabindex') {
return;
}
this._elementRef.nativeElement.setAttribute(name, value);
},
// The 2 functions below are used by the MDC ripple, which we aren't using,
// so they will never be called
getRootBoundingClientRect: () => this._elementRef.nativeElement.getBoundingClientRect(),
getRootBoundingClientRect: () =>
this._elementRef.nativeElement.getBoundingClientRect(),
getCheckmarkBoundingClientRect: () => null,
getAttribute: (attr) => this._elementRef.nativeElement.getAttribute(attr),
};

constructor(
public _changeDetectorRef: ChangeDetectorRef,
readonly _elementRef: ElementRef,
protected _ngZone: NgZone,
@Optional() private _dir: Directionality,
// @breaking-change 8.0.0 `animationMode` parameter to become required.
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
};

constructor(
public _changeDetectorRef: ChangeDetectorRef,
readonly _elementRef: ElementRef, protected _ngZone: NgZone,
@Optional() private _dir: Directionality,
// @breaking-change 8.0.0 `animationMode` parameter to become required.
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
super(_elementRef);
this._chipFoundation = new MDCChipFoundation(this._chipAdapter);
this._animationsDisabled = animationMode === 'NoopAnimations';
Expand Down Expand Up @@ -365,7 +386,7 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
return;
}

this._chipFoundation.handleTrailingIconInteraction(event);
this._chipFoundation.handleTrailingActionInteraction();

if (isKeyboardEvent && !hasModifierKey(event as KeyboardEvent)) {
const keyCode = (event as KeyboardEvent).keyCode;
Expand Down Expand Up @@ -398,8 +419,18 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte

/** Forwards interaction events to the MDC chip foundation. */
_handleInteraction(event: MouseEvent | KeyboardEvent) {
if (!this.disabled) {
this._chipFoundation.handleInteraction(event);
if (this.disabled) {
return;
}

if (event.type === 'click') {
this._chipFoundation.handleClick();
return;
}

if (event.type === 'keydown') {
this._chipFoundation.handleKeydown(event as KeyboardEvent);
return;
}
}

Expand All @@ -408,6 +439,15 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
return this.disabled || this.disableRipple || this._animationsDisabled || this._isBasicChip;
}

_notifyInteraction() {
this.interaction.emit(this.id);
}

_notifyNavigation() {
// TODO: This is a new feature added by MDC. Consider exposing it to users
// in the future.
}

static ngAcceptInputType_disabled: BooleanInput;
static ngAcceptInputType_removable: BooleanInput;
static ngAcceptInputType_highlighted: BooleanInput;
Expand Down
Loading

0 comments on commit de155e2

Please sign in to comment.