From ae913666011ff1f885e9c044a6e842f60b2f906b Mon Sep 17 00:00:00 2001 From: Elizabeth Mitchell Date: Fri, 14 Apr 2023 17:55:22 -0700 Subject: [PATCH] feat(chips): add filter chips PiperOrigin-RevId: 524428594 --- chips/_filter-chip.scss | 6 ++ chips/filter-chip.ts | 28 +++++++ chips/filter-chip_test.ts | 17 +++++ chips/lib/_filter-chip.scss | 141 +++++++++++++++++++++++++++++++++++ chips/lib/chip.ts | 42 ++++++----- chips/lib/filter-chip.ts | 50 +++++++++++++ chips/lib/filter-styles.scss | 10 +++ 7 files changed, 275 insertions(+), 19 deletions(-) create mode 100644 chips/_filter-chip.scss create mode 100644 chips/filter-chip.ts create mode 100644 chips/filter-chip_test.ts create mode 100644 chips/lib/_filter-chip.scss create mode 100644 chips/lib/filter-chip.ts create mode 100644 chips/lib/filter-styles.scss diff --git a/chips/_filter-chip.scss b/chips/_filter-chip.scss new file mode 100644 index 0000000000..2be8ff568e --- /dev/null +++ b/chips/_filter-chip.scss @@ -0,0 +1,6 @@ +// +// Copyright 2023 Google LLC +// SPDX-License-Identifier: Apache-2.0 +// + +@forward './lib/filter-chip' show theme; diff --git a/chips/filter-chip.ts b/chips/filter-chip.ts new file mode 100644 index 0000000000..b131577db6 --- /dev/null +++ b/chips/filter-chip.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {customElement} from 'lit/decorators.js'; + +import {FilterChip} from './lib/filter-chip.js'; +import {styles} from './lib/filter-styles.css.js'; +import {styles as sharedStyles} from './lib/shared-styles.css.js'; + +declare global { + interface HTMLElementTagNameMap { + 'md-filter-chip': MdFilterChip; + } +} + +/** + * TODO(b/243982145): add docs + * + * @final + * @suppress {visibility} + */ +@customElement('md-filter-chip') +export class MdFilterChip extends FilterChip { + static override styles = [sharedStyles, styles]; +} diff --git a/chips/filter-chip_test.ts b/chips/filter-chip_test.ts new file mode 100644 index 0000000000..369c0a731c --- /dev/null +++ b/chips/filter-chip_test.ts @@ -0,0 +1,17 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// import 'jasmine'; (google3-only) + +import {createTokenTests} from '../testing/tokens.js'; + +import {MdFilterChip} from './filter-chip.js'; + +describe('', () => { + describe('.styles', () => { + createTokenTests(MdFilterChip.styles); + }); +}); diff --git a/chips/lib/_filter-chip.scss b/chips/lib/_filter-chip.scss new file mode 100644 index 0000000000..6837e9d20f --- /dev/null +++ b/chips/lib/_filter-chip.scss @@ -0,0 +1,141 @@ +// +// Copyright 2023 Google LLC +// SPDX-License-Identifier: Apache-2.0 +// + +// go/keep-sorted start +@use '../../ripple/ripple'; +@use '../../sass/theme'; +@use '../../tokens'; +// go/keep-sorted end + +@mixin theme($tokens) { + $tokens: theme.validate-theme(tokens.md-comp-filter-chip-values(), $tokens); + + @each $token, $value in $tokens { + --md-filter-chip-#{$token}: #{$value}; + } +} + +@mixin styles() { + $tokens: tokens.md-comp-filter-chip-values(); + + :host { + @each $token, $value in $tokens { + --_#{$token}: #{$value}; + } + } + + .selected { + @include ripple.theme( + ( + focus-color: var(--_selected-focus-state-layer-color), + focus-opacity: var(--_selected-focus-state-layer-opacity), + hover-color: var(--_selected-hover-state-layer-color), + hover-opacity: var(--_selected-hover-state-layer-opacity), + pressed-color: var(--_selected-pressed-state-layer-color), + pressed-opacity: var(--_selected-pressed-state-layer-opacity), + ) + ); + } + + .selected .icon.leading { + width: var(--_icon-size); + } + + .checkmark { + inset: 0; + opacity: 0; + position: absolute; + } + + .selected .checkmark { + opacity: 1; + } + + .selected::before { + background: var(--_selected-container-color); + } + + .selected .outline { + border-width: var(--_selected-outline-width); + } + + .selected.elevated::before { + background: var(--_elevated-selected-container-color); + } + + .selected.disabled::before { + background: var(--_disabled-selected-container-color); + opacity: var(--_disabled-selected-container-opacity); + } + + .selected .label { + color: var(--_selected-label-text-color); + } + + .selected:hover .label { + color: var(--_selected-hover-label-text-color); + } + + .selected:focus .label { + color: var(--_selected-focus-label-text-color); + } + + .selected:active .label { + color: var(--_selected-pressed-label-text-color); + } + + .selected .icon.leading { + color: var(--_selected-leading-icon-color); + } + + .selected:hover .icon.leading { + color: var(--_selected-hover-leading-icon-color); + } + + .selected:focus .icon.leading { + color: var(--_selected-focus-leading-icon-color); + } + + .selected:active .icon.leading { + color: var(--_selected-pressed-leading-icon-color); + } + + .icon.trailing { + color: var(--_trailing-icon-color); + } + + :hover .icon.trailing { + color: var(--_hover-trailing-icon-color); + } + + :focus .icon.trailing { + color: var(--_focus-trailing-icon-color); + } + + :active .icon.trailing { + color: var(--_pressed-trailing-icon-color); + } + + .disabled .icon.trailing { + color: var(--_disabled-trailing-icon-color); + opacity: var(--_disabled-trailing-icon-opacity); + } + + .selected .icon.trailing { + color: var(--_selected-trailing-icon-color); + } + + .selected:hover .icon.trailing { + color: var(--_selected-hover-trailing-icon-color); + } + + .selected:focus .icon.trailing { + color: var(--_selected-focus-trailing-icon-color); + } + + .selected:active .icon.trailing { + color: var(--_selected-pressed-trailing-icon-color); + } +} diff --git a/chips/lib/chip.ts b/chips/lib/chip.ts index db09756462..d9eb53c7fa 100644 --- a/chips/lib/chip.ts +++ b/chips/lib/chip.ts @@ -11,7 +11,6 @@ import '../../ripple/ripple.js'; import {html, LitElement, nothing, TemplateResult} from 'lit'; import {property, queryAsync, state} from 'lit/decorators.js'; import {classMap} from 'lit/directives/class-map.js'; -import {when} from 'lit/directives/when.js'; import {html as staticHtml, literal} from 'lit/static-html.js'; import {pointerPress, shouldShowStrongFocus} from '../../focus/strong-focus.js'; @@ -33,15 +32,9 @@ export class Chip extends LitElement { @queryAsync('md-ripple') private readonly ripple!: Promise; override render() { - const classes = { - disabled: this.disabled, - elevated: this.elevated, - flat: !this.elevated, - }; - const button = this.href ? literal`a` : literal`button`; return staticHtml` - <${button} class="container ${classMap(classes)}" + <${button} class="container ${classMap(this.getContainerClasses())}" ?disabled=${this.disabled} href=${this.href || nothing} target=${this.href ? this.target : nothing} @@ -49,34 +42,45 @@ export class Chip extends LitElement { @focus=${this.handleFocus} @pointerdown=${this.handlePointerDown} ${ripple(this.getRipple)}> + ${!this.elevated ? html`` : nothing} ${this.elevated ? html`` : nothing} - ${when(this.showRipple, this.renderRipple)} + ${this.showRipple ? this.renderRipple() : nothing} - + ${this.renderLeadingIcon()} ${this.label} - ${this.renderTrailingIcon?.() || nothing} + ${this.renderTrailingIcon()} `; } - // Not all chip variants have a trailing icon. We still render a wrapper - // to compute the correct padding + gap of the - // button. - protected renderTrailingIcon?: () => TemplateResult; + protected getContainerClasses() { + return { + disabled: this.disabled, + elevated: this.elevated, + }; + } + + protected renderLeadingIcon(): TemplateResult { + return html``; + } + + protected renderTrailingIcon(): TemplateResult|typeof nothing { + return nothing; + } + + private renderRipple() { + return html``; + } private readonly getRipple = () => { // bind to this this.showRipple = true; return this.ripple; }; - private readonly renderRipple = () => { // bind to this - return html``; - }; - private handleBlur() { this.showFocusRing = false; } diff --git a/chips/lib/filter-chip.ts b/chips/lib/filter-chip.ts new file mode 100644 index 0000000000..e1ef028d12 --- /dev/null +++ b/chips/lib/filter-chip.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {PropertyValues, svg} from 'lit'; +import {property} from 'lit/decorators.js'; + +import {Chip} from './chip.js'; + +/** + * A filter chip component. + */ +export class FilterChip extends Chip { + @property({type: Boolean}) selected = false; + + constructor() { + super(); + this.addEventListener('click', () => { + this.selected = !this.selected; + }); + } + + protected override updated(changedProperties: PropertyValues) { + if (changedProperties.has('selected')) { + this.dispatchEvent(new Event('change', {bubbles: true})); + } + } + + protected override getContainerClasses() { + const classes = super.getContainerClasses(); + return { + ...classes, + selected: this.selected, + }; + } + + protected override renderLeadingIcon() { + if (!this.selected) { + return super.renderLeadingIcon(); + } + + return svg` + + + + `; + } +} diff --git a/chips/lib/filter-styles.scss b/chips/lib/filter-styles.scss new file mode 100644 index 0000000000..3aeeb64b13 --- /dev/null +++ b/chips/lib/filter-styles.scss @@ -0,0 +1,10 @@ +// +// Copyright 2023 Google LLC +// SPDX-License-Identifier: Apache-2.0 +// + +// go/keep-sorted start +@use './filter-chip'; +// go/keep-sorted end + +@include filter-chip.styles;