Skip to content

Commit 94ef838

Browse files
Angular Material Teamcrisbetopatrickrodee
authored andcommitted
refactor(material-experimental/mdc-chips): implement trailing icon foundation
Project import generated by Copybara. These changes are mostly based on @crisbeto's #18877 with some additional changes from @ProDee PiperOrigin-RevId: 310167103 Co-authored-by: crisbeto <[email protected]> Co-authored-by: prodee <[email protected]>
1 parent 14a51ef commit 94ef838

File tree

2 files changed

+158
-78
lines changed

2 files changed

+158
-78
lines changed

src/material-experimental/mdc-chips/chip-icons.ts

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,7 @@
77
*/
88

99
import {BooleanInput} from '@angular/cdk/coercion';
10-
import {
11-
ChangeDetectorRef,
12-
Directive,
13-
ElementRef,
14-
} from '@angular/core';
10+
import {ChangeDetectorRef, Directive, ElementRef, OnDestroy,} from '@angular/core';
1511
import {
1612
CanDisable,
1713
CanDisableCtor,
@@ -20,6 +16,7 @@ import {
2016
mixinDisabled,
2117
mixinTabIndex,
2218
} from '@angular/material/core';
19+
import {MDCChipTrailingActionAdapter, MDCChipTrailingActionFoundation} from '@material/chips';
2320
import {Subject} from 'rxjs';
2421

2522

@@ -52,13 +49,52 @@ export class MatChipAvatar {
5249
@Directive({
5350
selector: 'mat-chip-trailing-icon, [matChipTrailingIcon]',
5451
host: {
55-
'class': 'mat-mdc-chip-trailing-icon mdc-chip__icon mdc-chip__icon--trailing',
52+
'class':
53+
'mat-mdc-chip-trailing-icon mdc-chip__icon mdc-chip__icon--trailing',
5654
'tabindex': '-1',
5755
'aria-hidden': 'true',
5856
}
5957
})
60-
export class MatChipTrailingIcon {
61-
constructor(public _elementRef: ElementRef) {}
58+
export class MatChipTrailingIcon implements OnDestroy {
59+
private _foundation: MDCChipTrailingActionFoundation;
60+
private _adapter: MDCChipTrailingActionAdapter = {
61+
focus: () => this._elementRef.nativeElement.focus(),
62+
getAttribute: (name: string) =>
63+
this._elementRef.nativeElement.getAttribute(name),
64+
setAttribute:
65+
(name: string, value: string) => {
66+
this._elementRef.nativeElement.setAttribute(name, value);
67+
},
68+
// TODO(crisbeto): there's also a `trigger` parameter that the chip isn't
69+
// handling yet. Consider passing it along once MDC start using it.
70+
notifyInteraction:
71+
() => {
72+
// TODO(crisbeto): uncomment this code once we've inverted the
73+
// dependency on `MatChip`. this._chip._notifyInteraction();
74+
},
75+
76+
// TODO(crisbeto): there's also a `key` parameter that the chip isn't
77+
// handling yet. Consider passing it along once MDC start using it.
78+
notifyNavigation:
79+
() => {
80+
// TODO(crisbeto): uncomment this code once we've inverted the
81+
// dependency on `MatChip`. this._chip._notifyNavigation();
82+
}
83+
};
84+
85+
constructor(
86+
public _elementRef: ElementRef,
87+
// TODO(crisbeto): currently the chip needs a reference to the trailing
88+
// icon for the deprecated `setTrailingActionAttr` method. Until the
89+
// method is removed, we can't use the chip here, because it causes a
90+
// circular import. private _chip: MatChip
91+
) {
92+
this._foundation = new MDCChipTrailingActionFoundation(this._adapter);
93+
}
94+
95+
ngOnDestroy() {
96+
this._foundation.destroy();
97+
}
6298

6399
focus() {
64100
this._elementRef.nativeElement.focus();
@@ -68,15 +104,19 @@ export class MatChipTrailingIcon {
68104
setAttribute(name: string, value: string) {
69105
this._elementRef.nativeElement.setAttribute(name, value);
70106
}
107+
108+
isNavigable() {
109+
return this._foundation.isNavigable();
110+
}
71111
}
72112

73113
/**
74114
* Boilerplate for applying mixins to MatChipRemove.
75115
* @docs-private
76116
*/
77117
class MatChipRemoveBase extends MatChipTrailingIcon {
78-
constructor(_elementRef: ElementRef) {
79-
super(_elementRef);
118+
constructor(elementRef: ElementRef) {
119+
super(elementRef);
80120
}
81121
}
82122

src/material-experimental/mdc-chips/chip.ts

Lines changed: 108 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -245,78 +245,99 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
245245
protected _chipAdapter: MDCChipAdapter = {
246246
addClass: (className) => this._setMdcClass(className, true),
247247
removeClass: (className) => this._setMdcClass(className, false),
248-
hasClass: (className) => this._elementRef.nativeElement.classList.contains(className),
249-
addClassToLeadingIcon: (className) => this.leadingIcon.setClass(className, true),
250-
removeClassFromLeadingIcon: (className) => this.leadingIcon.setClass(className, false),
251-
eventTargetHasClass: (target: EventTarget | null, className: string) => {
252-
// We need to null check the `classList`, because IE and Edge don't support it on SVG elements
253-
// and Edge seems to throw for ripple elements, because they're outside the DOM.
254-
return (target && (target as Element).classList) ?
255-
(target as Element).classList.contains(className) : false;
256-
},
257-
notifyInteraction: () => this.interaction.emit(this.id),
258-
notifySelection: () => {
259-
// No-op. We call dispatchSelectionEvent ourselves in MatChipOption, because we want to
260-
// specify whether selection occurred via user input.
261-
},
262-
notifyNavigation: () => {
263-
// TODO: This is a new feature added by MDC; consider exposing this event to users in the
264-
// future.
265-
},
266-
notifyTrailingIconInteraction: () => this.removeIconInteraction.emit(this.id),
267-
notifyRemoval: () => {
268-
this.removed.emit({ chip: this });
269-
270-
// When MDC removes a chip it just transitions it to `width: 0px` which means that it's still
271-
// in the DOM and it's still focusable. Make it `display: none` so users can't tab into it.
272-
this._elementRef.nativeElement.style.display = 'none';
273-
},
274-
getComputedStyleValue: propertyName => {
275-
// This function is run when a chip is removed so it might be
276-
// invoked during server-side rendering. Add some extra checks just in case.
277-
if (typeof window !== 'undefined' && window) {
278-
const getComputedStyle = window.getComputedStyle(this._elementRef.nativeElement);
279-
return getComputedStyle.getPropertyValue(propertyName);
280-
}
281-
return '';
282-
},
283-
setStyleProperty: (propertyName: string, value: string) => {
284-
this._elementRef.nativeElement.style.setProperty(propertyName, value);
285-
},
248+
hasClass: (className) =>
249+
this._elementRef.nativeElement.classList.contains(className),
250+
addClassToLeadingIcon: (className) =>
251+
this.leadingIcon.setClass(className, true),
252+
removeClassFromLeadingIcon: (className) =>
253+
this.leadingIcon.setClass(className, false),
254+
eventTargetHasClass:
255+
(target: EventTarget|null, className: string) => {
256+
// We need to null check the `classList`, because IE and Edge don't
257+
// support it on SVG elements and Edge seems to throw for ripple
258+
// elements, because they're outside the DOM.
259+
return (target && (target as Element).classList) ?
260+
(target as Element).classList.contains(className) :
261+
false;
262+
},
263+
notifyInteraction: () => this._notifyInteraction(),
264+
notifySelection:
265+
() => {
266+
// No-op. We call dispatchSelectionEvent ourselves in MatChipOption,
267+
// because we want to specify whether selection occurred via user
268+
// input.
269+
},
270+
notifyNavigation: () => this._notifyNavigation(),
271+
notifyTrailingIconInteraction: () =>
272+
this.removeIconInteraction.emit(this.id),
273+
notifyRemoval:
274+
() => {
275+
this.removed.emit({chip: this});
276+
277+
// When MDC removes a chip it just transitions it to `width: 0px`
278+
// which means that it's still in the DOM and it's still focusable.
279+
// Make it `display: none` so users can't tab into it.
280+
this._elementRef.nativeElement.style.display = 'none';
281+
},
282+
getComputedStyleValue:
283+
propertyName => {
284+
// This function is run when a chip is removed so it might be
285+
// invoked during server-side rendering. Add some extra checks just in
286+
// case.
287+
if (typeof window !== 'undefined' && window) {
288+
const getComputedStyle =
289+
window.getComputedStyle(this._elementRef.nativeElement);
290+
return getComputedStyle.getPropertyValue(propertyName);
291+
}
292+
return '';
293+
},
294+
setStyleProperty:
295+
(propertyName: string, value: string) => {
296+
this._elementRef.nativeElement.style.setProperty(propertyName, value);
297+
},
286298
hasLeadingIcon: () => !!this.leadingIcon,
287-
hasTrailingAction: () => !!this.trailingIcon,
299+
isTrailingActionNavigable:
300+
() => {
301+
if (this.trailingIcon) {
302+
return this.trailingIcon.isNavigable();
303+
}
304+
return false;
305+
},
288306
isRTL: () => !!this._dir && this._dir.value === 'rtl',
289-
focusPrimaryAction: () => {
290-
// Angular Material MDC chips fully manage focus. TODO: Managing focus and handling keyboard
291-
// events was added by MDC after our implementation; consider consolidating.
292-
},
307+
focusPrimaryAction:
308+
() => {
309+
// Angular Material MDC chips fully manage focus. TODO: Managing focus
310+
// and handling keyboard events was added by MDC after our
311+
// implementation; consider consolidating.
312+
},
293313
focusTrailingAction: () => {},
294-
setTrailingActionAttr: (attr, value) =>
295-
this.trailingIcon && this.trailingIcon.setAttribute(attr, value),
296-
setPrimaryActionAttr: (name: string, value: string) => {
297-
// MDC is currently using this method to set aria-checked on choice and filter chips,
298-
// which in the MDC templates have role="checkbox" and role="radio" respectively.
299-
// We have role="option" on those chips instead, so we do not want aria-checked.
300-
// Since we also manage the tabindex ourselves, we don't allow MDC to set it.
301-
if (name === 'aria-checked' || name === 'tabindex') {
302-
return;
303-
}
304-
this._elementRef.nativeElement.setAttribute(name, value);
305-
},
314+
removeTrailingActionFocus: () => {},
315+
setPrimaryActionAttr:
316+
(name: string, value: string) => {
317+
// MDC is currently using this method to set aria-checked on choice
318+
// and filter chips, which in the MDC templates have role="checkbox"
319+
// and role="radio" respectively. We have role="option" on those chips
320+
// instead, so we do not want aria-checked. Since we also manage the
321+
// tabindex ourselves, we don't allow MDC to set it.
322+
if (name === 'aria-checked' || name === 'tabindex') {
323+
return;
324+
}
325+
this._elementRef.nativeElement.setAttribute(name, value);
326+
},
306327
// The 2 functions below are used by the MDC ripple, which we aren't using,
307328
// so they will never be called
308-
getRootBoundingClientRect: () => this._elementRef.nativeElement.getBoundingClientRect(),
329+
getRootBoundingClientRect: () =>
330+
this._elementRef.nativeElement.getBoundingClientRect(),
309331
getCheckmarkBoundingClientRect: () => null,
310332
getAttribute: (attr) => this._elementRef.nativeElement.getAttribute(attr),
311-
};
312-
313-
constructor(
314-
public _changeDetectorRef: ChangeDetectorRef,
315-
readonly _elementRef: ElementRef,
316-
protected _ngZone: NgZone,
317-
@Optional() private _dir: Directionality,
318-
// @breaking-change 8.0.0 `animationMode` parameter to become required.
319-
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
333+
};
334+
335+
constructor(
336+
public _changeDetectorRef: ChangeDetectorRef,
337+
readonly _elementRef: ElementRef, protected _ngZone: NgZone,
338+
@Optional() private _dir: Directionality,
339+
// @breaking-change 8.0.0 `animationMode` parameter to become required.
340+
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
320341
super(_elementRef);
321342
this._chipFoundation = new MDCChipFoundation(this._chipAdapter);
322343
this._animationsDisabled = animationMode === 'NoopAnimations';
@@ -365,7 +386,7 @@ export class MatChip extends _MatChipMixinBase implements AfterContentInit, Afte
365386
return;
366387
}
367388

368-
this._chipFoundation.handleTrailingIconInteraction(event);
389+
this._chipFoundation.handleTrailingActionInteraction();
369390

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

399420
/** Forwards interaction events to the MDC chip foundation. */
400421
_handleInteraction(event: MouseEvent | KeyboardEvent) {
401-
if (!this.disabled) {
402-
this._chipFoundation.handleInteraction(event);
422+
if (this.disabled) {
423+
return;
424+
}
425+
426+
if (event.type === 'click') {
427+
this._chipFoundation.handleClick();
428+
return;
429+
}
430+
431+
if (event.type === 'keydown') {
432+
this._chipFoundation.handleKeydown(event as KeyboardEvent);
433+
return;
403434
}
404435
}
405436

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

442+
_notifyInteraction() {
443+
this.interaction.emit(this.id);
444+
}
445+
446+
_notifyNavigation() {
447+
// TODO: This is a new feature added by MDC. Consider exposing it to users
448+
// in the future.
449+
}
450+
411451
static ngAcceptInputType_disabled: BooleanInput;
412452
static ngAcceptInputType_removable: BooleanInput;
413453
static ngAcceptInputType_highlighted: BooleanInput;

0 commit comments

Comments
 (0)