Skip to content

Commit

Permalink
feat(material-experimental/mdc-chips): switch to evolution API (#23931)
Browse files Browse the repository at this point in the history
* feat(material-experimental/mdc-chips): switch to evolution API

Reworks the MDC-based chips to use the new `evolution` API instead of the deprecated one we're currently using. The new API comes with a lot of markup changes and some behavior differences.

The new API also allows to remove the `GridKeyManager`, because the keyboard navigation is handled correctly by MDC.

* refactor(material-experimental/mdc-chips): reduce specificity and bundle size

Reworks the theme of the MDC-based chips to produce less specific and more compact CSS.
  • Loading branch information
crisbeto committed Jan 11, 2022
1 parent 5fcc634 commit c5482c9
Show file tree
Hide file tree
Showing 38 changed files with 1,659 additions and 2,199 deletions.
6 changes: 6 additions & 0 deletions scripts/check-mdc-tests-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export const config = {

// This test checks something that isn't supported in the MDC form field.
'should propagate the dynamic `placeholder` value to the form field',

// Disabled, because the MDC-based chip input doesn't deal with focus escaping anymore.
'should not allow focus to escape when tabbing backwards',

// Disabled, because preventing the default action isn't required.
'should prevent the default click action when the chip is disabled',
],
'mdc-dialog': [
// These tests are verifying implementation details that are not relevant for MDC.
Expand Down
4 changes: 2 additions & 2 deletions src/dev-app/chips/chips-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,12 @@ <h4>With avatar and icons</h4>
</mat-chip>

<mat-chip>
<img src="https://material.angularjs.org/material2_assets/ngconf/Mal.png" matChipAvatar>
<img src="https://material.angular.io/assets/img/examples/shiba2.jpg" matChipAvatar>
Mal
</mat-chip>

<mat-chip selected="true" color="warn">
<img src="https://material.angularjs.org/material2_assets/ngconf/Husi.png" matChipAvatar>
<img src="https://material.angular.io/assets/img/examples/shiba2.jpg" matChipAvatar>
Husi
<button matChipRemove>
<mat-icon>cancel</mat-icon>
Expand Down
4 changes: 2 additions & 2 deletions src/dev-app/mdc-chips/mdc-chips-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ <h4>With avatar, icons, and color</h4>
</mat-chip>

<mat-chip>
<img src="https://material.angularjs.org/material2_assets/ngconf/Mal.png" matChipAvatar>
<img src="https://material.angular.io/assets/img/examples/shiba2.jpg" matChipAvatar>
Mal
</mat-chip>

<mat-chip highlighted="true" color="warn">
<img src="https://material.angularjs.org/material2_assets/ngconf/Husi.png" matChipAvatar>
<img src="https://material.angular.io/assets/img/examples/shiba2.jpg" matChipAvatar>
Husi
<button matChipRemove>
<mat-icon>cancel</mat-icon>
Expand Down
22 changes: 19 additions & 3 deletions src/material-experimental/mdc-chips/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ ng_module(
"**/*.spec.ts",
],
),
assets = [":chips_scss"] + glob(["**/*.html"]),
assets = [
":chip_scss",
":chip_set_scss",
] + glob(["**/*.html"]),
deps = [
"//src:dev_mode_types",
"//src/material-experimental/mdc-core",
Expand All @@ -40,8 +43,21 @@ sass_library(
)

sass_binary(
name = "chips_scss",
src = "chips.scss",
name = "chip_scss",
src = "chip.scss",
include_paths = [
"external/npm/node_modules",
],
deps = [
"//src/material-experimental/mdc-helpers:mdc_helpers_scss_lib",
"//src/material-experimental/mdc-helpers:mdc_scss_deps_lib",
"//src/material/core:core_scss_lib",
],
)

sass_binary(
name = "chip_set_scss",
src = "chip-set.scss",
include_paths = [
"external/npm/node_modules",
],
Expand Down
106 changes: 53 additions & 53 deletions src/material-experimental/mdc-chips/_chips-theme.scss
Original file line number Diff line number Diff line change
@@ -1,93 +1,93 @@
@use '@material/chips/deprecated' as mdc-chips;
@use '@material/chips/chip' as mdc-chip;
@use '@material/chips/chip-theme' as mdc-chip-theme;
@use '@material/chips/chip-set' as mdc-chip-set;
@use '@material/theme/theme-color' as mdc-theme-color;
@use '@material/theme/color-palette' as mdc-color-palette;
@use 'sass:color';
@use 'sass:map';
@use '../mdc-helpers/mdc-helpers';
@use '../../material/core/typography/typography';
@use '../../material/core/theming/theming';

@mixin _selected-color($color) {
@include mdc-chips.fill-color($color, $query: mdc-helpers.$mat-theme-styles-query);
@include mdc-chips.ink-color(text-primary-on-dark, $query: mdc-helpers.$mat-theme-styles-query);
@include mdc-chips.selected-ink-color-without-ripple_(
text-primary-on-dark,
$query: mdc-helpers.$mat-theme-styles-query
);
@include mdc-chips.leading-icon-color(text-primary-on-dark,
$query: mdc-helpers.$mat-theme-styles-query);
@include mdc-chips.trailing-icon-color(text-primary-on-dark,
$query: mdc-helpers.$mat-theme-styles-query);
// Customizes the appearance of a chip. Note that ideally we would be doing this using the
// `theme-styles` mixin, however it has the following problems:
// 1. Some of MDC's base styles have **very** high specificity. E.g. setting the background of a
// non-selected, enabled chip uses a selector like `.chip:not(.selected):not(.disabled)` instead of
// just `.chip`. This specificity increase has a ripple effect over all other components that are
// built on top of ours, making overrides extremely difficult and brittle.
// 2. Including the individual mixins allows us to avoid a lot of unnecessary CSS (~35kb in the
// dev app theme).
@mixin _chip-variant($background, $foreground) {
@include mdc-chip-theme.container-color($background);
@include mdc-chip-theme.icon-color($foreground);
@include mdc-chip-theme.trailing-action-color($foreground);
@include mdc-chip-theme.checkmark-color($foreground);
@include mdc-chip-theme.text-label-color($foreground);

// Technically the avatar is only supposed to have an image, but we also allow for icons.
// Set the color so the icons inherit the correct color.
.mat-mdc-chip-avatar {
color: $foreground;
}
}

@mixin _colored-chip($palette) {
$background: theming.get-color-from-palette($palette);
$foreground: theming.get-color-from-palette($palette, default-contrast);

&.mat-mdc-chip-selected,
&.mat-mdc-chip-highlighted {
@include _chip-variant($background, $foreground);
}
}

@mixin color($config-or-theme) {
$config: theming.get-color-config($config-or-theme);
$primary: theming.get-color-from-palette(map.get($config, primary));
$accent: theming.get-color-from-palette(map.get($config, accent));
$warn: theming.get-color-from-palette(map.get($config, warn));
$background: map.get($config, background);
$unselected-background: theming.get-color-from-palette($background, unselected-chip);

// Save original values of MDC global variables. We need to save these so we can restore the
// variables to their original values and prevent unintended side effects from using this mixin.
$orig-mdc-chips-fill-color-default: mdc-chips.$fill-color-default;
$orig-mdc-chips-ink-color-default: mdc-chips.$ink-color-default;
$orig-mdc-chips-icon-color: mdc-chips.$icon-color;
$primary: map.get($config, primary);
$accent: map.get($config, accent);
$warn: map.get($config, warn);
$foreground: map.get($config, foreground);
$is-dark: map.get($config, is-dark);

@include mdc-helpers.mat-using-mdc-theme($config) {
mdc-chips.$fill-color-default:
color.mix(mdc-theme-color.prop-value(on-surface), mdc-theme-color.prop-value(surface), 12%);
mdc-chips.$ink-color-default: rgba(mdc-theme-color.prop-value(on-surface), 0.87);
mdc-chips.$icon-color: mdc-theme-color.prop-value(on-surface);

@include mdc-chips.set-core-styles($query: mdc-helpers.$mat-theme-styles-query);
@include mdc-chips.without-ripple($query: mdc-helpers.$mat-theme-styles-query);
.mat-mdc-standard-chip {
@include _chip-variant(
color.mix(mdc-theme-color.prop-value(on-surface), mdc-theme-color.prop-value(surface), 12%),
if($is-dark, mdc-color-palette.$grey-50, mdc-color-palette.$grey-900)
);

.mat-mdc-chip {
@include mdc-chips.fill-color-accessible($unselected-background,
$query: mdc-helpers.$mat-theme-styles-query);

// mdc-chip-fill-color-accessible includes mdc-chip-selected-ink-color which overrides the
// opacity so selected chips always show a ripple.
// Include the same mixins but use mdc-chip-selected-ink-color-without-ripple
&.mat-primary {
&.mdc-chip--selected, &.mat-mdc-chip-highlighted {
@include _selected-color($primary);
}
@include _colored-chip($primary);
}

&.mat-accent {
&.mdc-chip--selected, &.mat-mdc-chip-highlighted {
@include _selected-color($accent);
}
@include _colored-chip($accent);
}

&.mat-warn {
&.mdc-chip--selected, &.mat-mdc-chip-highlighted {
@include _selected-color($warn);
}
@include _colored-chip($warn);
}
}
}

// Restore original values of MDC global variables.
mdc-chips.$fill-color-default: $orig-mdc-chips-fill-color-default;
mdc-chips.$ink-color-default: $orig-mdc-chips-ink-color-default;
mdc-chips.$icon-color: $orig-mdc-chips-icon-color;
.mat-mdc-chip-focus-overlay {
background: map.get($foreground, base);
}
}

@mixin typography($config-or-theme) {
$config: typography.private-typography-to-2018-config(
theming.get-typography-config($config-or-theme));
@include mdc-chips.set-core-styles($query: mdc-helpers.$mat-typography-styles-query);
@include mdc-chip-set.core-styles($query: mdc-helpers.$mat-typography-styles-query);
@include mdc-helpers.mat-using-mdc-typography($config) {
@include mdc-chips.without-ripple($query: mdc-helpers.$mat-typography-styles-query);
@include mdc-chip.without-ripple-styles($query: mdc-helpers.$mat-typography-styles-query);
}
}

@mixin density($config-or-theme) {
$density-scale: theming.get-density-config($config-or-theme);
.mat-mdc-chip {
@include mdc-chips.density($density-scale, $query: mdc-helpers.$mat-base-styles-query);
@include mdc-chip-theme.density($density-scale, $query: mdc-helpers.$mat-base-styles-query);
}
}

Expand Down
151 changes: 151 additions & 0 deletions src/material-experimental/mdc-chips/chip-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* @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 {
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';

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

/**
* 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]': `_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]': `_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 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;

_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();
}
}

_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,
) {
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,6 @@ describe('MDC-based MatChipEditInput', () => {
});

@Component({
template: `<span matChipEditInput></span>`,
template: `<mat-chip><span matChipEditInput></span></mat-chip>`,
})
class ChipEditInputContainer {}
2 changes: 1 addition & 1 deletion src/material-experimental/mdc-chips/chip-edit-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {DOCUMENT} from '@angular/common';
@Directive({
selector: 'span[matChipEditInput]',
host: {
'class': 'mdc-chip__primary-action mat-chip-edit-input',
'class': 'mat-chip-edit-input',
'role': 'textbox',
'tabindex': '-1',
'contenteditable': 'true',
Expand Down
Loading

0 comments on commit c5482c9

Please sign in to comment.