diff --git a/labs/gb/components/chip/_chip-tokens.scss b/labs/gb/components/chip/_chip-tokens.scss new file mode 100644 index 0000000000..3a30d901f6 --- /dev/null +++ b/labs/gb/components/chip/_chip-tokens.scss @@ -0,0 +1,167 @@ +// +// Copyright 2026 Google LLC +// SPDX-License-Identifier: Apache-2.0 +// + +// Auto-generated by cascade_tokens +// Add `// EDIT:` comments for manual adjustments. +// +// --prefix=md.comp.chip +// --states="hovered|focused|pressed|dragged,:unselected|selected,:filter|input,with-leading-icon|with-avatar,with-trailing-icon,disabled" +// --exclude=motion,state-layer,indicator + +@mixin root { + // LINT.IfChange + --avatar-opacity: unset; + // md.comp.chip.avatar.shape + --avatar-shape: var(--md-sys-shape-corner-full); + // md.comp.chip.avatar.size + --avatar-size: 24px; + --container-color: unset; + // md.comp.chip.container.elevation + --container-elevation: var(--md-sys-elevation-shadow-0); + // --container-opacity: unset; // EDIT: unused + // md.comp.chip.gap.horizontal + --gap-horizontal: var(--md-sys-space-50); + // md.comp.chip.height + --height: 32px; + // md.comp.chip.label-text + --label-text: var(--md-sys-typescale-label-lg); + // ┌┄ md.comp.chip.label-text + --label-text-axes: var(--md-sys-typescale-label-lg-axes); + // md.comp.chip.unselected.label-text.color + --label-text-color: var(--md-sys-color-on-surface-variant); + // --label-text-opacity: unset; // EDIT: unused + // ┌┄ md.comp.chip.label-text + --label-text-tracking: var(--md-sys-typescale-label-lg-tracking); + // md.comp.chip.unselected.leading-icon.color + --leading-icon-color: var(--md-sys-color-on-surface-variant); + // --leading-icon-opacity: unset; // EDIT: unused + // md.comp.chip.leading-icon.size + --leading-icon-size: 18px; + // md.comp.chip.unselected.outline.color + --outline-color: var(--md-sys-color-outline-variant); + // --outline-opacity: unset; // EDIT: unused + // md.comp.chip.unselected.outline.width + --outline-width: 1px; + // md.comp.chip.padding.bottom + --padding-bottom: var(--md-sys-space-75); + // ┌ md.comp.chip.filter.padding.leading + --padding-leading: var(--md-sys-space-200); + // md.comp.chip.padding.top + --padding-top: var(--md-sys-space-75); + // ┌ md.comp.chip.filter.padding.trailing + --padding-trailing: var(--md-sys-space-200); + // md.comp.chip.unselected.shape + --shape: var(--md-sys-shape-corner-md); + // md.comp.chip.unselected.trailing-icon.color + --trailing-icon-color: var(--md-sys-color-on-surface-variant); + // --trailing-icon-opacity: unset; // EDIT: unused + // md.comp.chip.trailing-icon.size + --trailing-icon-size: 18px; + // LINT.ThenChange(md-gb-chip.ts) sync with `@cssprop` jsdoc tags +} + +@mixin pressed { + // md.comp.chip.pressed.shape + --shape: var(--md-sys-shape-corner-sm); +} + +@mixin dragged { + // md.comp.chip.dragged.container.elevation + --container-elevation: var(--md-sys-elevation-shadow-4); +} + +@mixin selected { + // md.comp.chip.selected.container.color + --container-color: var(--md-sys-color-secondary-container); + // md.comp.chip.selected.label-text.color + --label-text-color: var(--md-sys-color-on-secondary-container); + // md.comp.chip.selected.leading-icon.color + --leading-icon-color: var(--md-sys-color-on-secondary-container); + // md.comp.chip.selected.outline.width + --outline-width: 0px; + // md.comp.chip.selected.shape + --shape: var(--md-sys-shape-corner-full); + // md.comp.chip.selected.trailing-icon.color + --trailing-icon-color: var(--md-sys-color-on-secondary-container); +} + +@mixin input { + // md.comp.chip.input.padding.leading + --padding-leading: var(--md-sys-space-150); + // md.comp.chip.input.padding.trailing + --padding-trailing: var(--md-sys-space-150); +} + +@mixin with-leading-icon { + // md.comp.chip.with-leading-icon.padding.leading + --padding-leading: var(--md-sys-space-100); +} + +@mixin with-avatar { + // md.comp.chip.with-avatar.padding.leading + --padding-leading: var(--md-sys-space-50); +} + +@mixin with-trailing-icon { + // md.comp.chip.with-trailing-icon.padding.trailing + --padding-trailing: var(--md-sys-space-100); +} + +@mixin disabled { + // md.comp.chip.disabled.avatar.opacity + --avatar-opacity: 0.38; + // ┌ md.comp.chip.disabled.label-text.opacity + // md.comp.chip.disabled.label-text.color + --label-text-color: hsl(from var(--md-sys-color-on-surface) h s l / 0.38); + // ┌ md.comp.chip.disabled.leading-icon.opacity + // md.comp.chip.disabled.leading-icon.color + --leading-icon-color: hsl(from var(--md-sys-color-on-surface) h s l / 0.38); + // ┌ md.comp.chip.unselected.disabled.outline.opacity + // md.comp.chip.unselected.disabled.outline.color + --outline-color: hsl(from var(--md-sys-color-on-surface) h s l / 0.1); + // ┌ md.comp.chip.disabled.trailing-icon.opacity + // md.comp.chip.disabled.trailing-icon.color + --trailing-icon-color: hsl(from var(--md-sys-color-on-surface) h s l / 0.38); +} + +@mixin disabled-selected { + // ┌ md.comp.chip.selected.disabled.container.opacity + // md.comp.chip.selected.disabled.container.color + --container-color: hsl(from var(--md-sys-color-on-surface) h s l / 0.12); +} + +// EDIT: Manual adjustments +@mixin filter { + // Hoisted to root, so empty here to satisfy chip.scss mapping +} + +@mixin elevated { + --container-color: var(--md-sys-color-surface-container-low); + --container-elevation: var(--md-sys-elevation-shadow-1); + --label-text-color: var(--md-sys-color-on-surface); + --outline-width: 0; +} + +@mixin filled { + --container-color: var(--md-sys-color-surface-container); + --container-elevation: var(--md-sys-elevation-shadow-0); + --label-text-color: var(--md-sys-color-on-surface); + --outline-width: 0; +} + +@mixin tonal { + --container-color: var(--md-sys-color-secondary-container); + --container-elevation: var(--md-sys-elevation-shadow-0); + --label-text-color: var(--md-sys-color-on-secondary-container); + --outline-width: 0; +} + +@mixin outlined { + --container-color: transparent; + --container-elevation: var(--md-sys-elevation-shadow-0); + --label-text-color: var(--md-sys-color-on-surface); + --outline-color: var(--md-sys-color-outline); + --outline-width: 1px; +} diff --git a/labs/gb/components/chip/chip-element.ts b/labs/gb/components/chip/chip-element.ts new file mode 100644 index 0000000000..a593797d0e --- /dev/null +++ b/labs/gb/components/chip/chip-element.ts @@ -0,0 +1,222 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {css, CSSResultOrNative, html, LitElement, nothing} from 'lit'; +import {property, state} from 'lit/decorators.js'; +import {redispatchEvent} from '../../../../internal/events/redispatch-event.js'; +import focusRingStyles from '../focus/focus-ring.cssresult.js'; +import rippleStyles from '../ripple/ripple.cssresult.js'; +import {chip, type ChipColor, type ChipType} from './chip.js'; +import {styles as chipStyles} from './chip.cssresult.js'; + +/** + * A Material Design Expressive Chip component (`md-gb-chip`). + * + * @slot - Used to display the chip label text. + * @slot icon - Used to display an optional leading icon, checkmark, or avatar. + * @slot remove-icon - Used to display the trailing remove button icon when `removable="true"`. + * @fires {Event} remove - Fired when the remove action is triggered on a removable chip. --bubbles --composed + * @fires {Event} change - Fired when a filter/toggle chip's selection state changes. --bubbles + * @fires {InputEvent} input - Fired when a filter/toggle chip's selection state changes. --bubbles --composed + * @csspart chip - The root container element. + * @cssprop --avatar-opacity + * @cssprop --avatar-shape + * @cssprop --avatar-size + * @cssprop --container-color + * @cssprop --container-elevation + * @cssprop --container-opacity + * @cssprop --gap-horizontal + * @cssprop --height + * @cssprop --label-text + * @cssprop --label-text-color + * @cssprop --label-text-opacity + * @cssprop --leading-icon-color + * @cssprop --leading-icon-opacity + * @cssprop --leading-icon-size + * @cssprop --outline-color + * @cssprop --outline-opacity + * @cssprop --outline-width + * @cssprop --padding-bottom + * @cssprop --padding-leading + * @cssprop --padding-top + * @cssprop --padding-trailing + * @cssprop --shape + * @cssprop --trailing-icon-color + * @cssprop --trailing-icon-opacity + * @cssprop --trailing-icon-size + */ +export class ChipElement extends LitElement { + static override styles: CSSResultOrNative[] = [ + focusRingStyles, + rippleStyles, + chipStyles, + css` + :host { + display: inline-flex; + vertical-align: middle; + } + .chip { + flex: 1; + } + .chip-icon { + display: inline-flex; + align-items: center; + } + `, + ]; + + /** The color emphasis of the chip. Defaults to `outlined`. */ + @property() color: ChipColor = 'outlined'; + + /** The functional behavior type of the chip (`action`, `filter`, `toggle`, `link`). */ + @property() type: ChipType = 'action'; + + /** Whether the filter or toggle chip is selected. */ + @property({type: Boolean, reflect: true}) selected = false; + + /** Whether the chip is removable. */ + @property({type: Boolean, reflect: true}) removable = false; + + /** Whether the chip is disabled. */ + @property({type: Boolean, reflect: true}) disabled = false; + + /** Whether the chip is soft-disabled (`aria-disabled="true"` while preserving focusability). */ + @property({type: Boolean, attribute: 'soft-disabled', reflect: true}) + softDisabled = false; + + /** The URL destination if `type="link"` or if `href` is set. */ + @property() href = ''; + + /** Where to display the linked `href` URL for a link chip. */ + @property() target: '_blank' | '_parent' | '_self' | '_top' | '' = ''; + + @state() private hasLeadingIcon = false; + @state() private hasAvatar = false; + @state() private hasRemoveIcon = false; + + protected override render() { + const isFilterSelected = + (this.type === 'filter' || this.type === 'toggle') && this.selected; + const withLeadingIcon = this.hasLeadingIcon || isFilterSelected; + const withAvatar = this.hasAvatar; + + const chipClasses = chip({ + color: this.color, + type: this.href ? 'link' : this.type, + selected: this.selected, + removable: this.removable, + disabled: this.softDisabled || this.disabled, + withLeadingIcon: withLeadingIcon && !withAvatar, + withAvatar, + withTrailingIcon: this.removable || this.hasRemoveIcon, + }); + + if (this.href || this.type === 'link') { + return html` + ${this.renderContent(withLeadingIcon, isFilterSelected)} + `; + } + + if (this.removable) { + return html`
+ + ${this.renderRemoveButton()} +
`; + } + + return html``; + } + + private renderContent(withLeadingIcon: boolean, isFilterSelected: boolean) { + return html` + ${this.renderLeadingIcon(withLeadingIcon, isFilterSelected)} + + `; + } + + private renderLeadingIcon( + withLeadingIcon: boolean, + isFilterSelected: boolean, + ) { + return html` + + ${isFilterSelected ? html`✓` : nothing} + + `; + } + + private renderRemoveButton() { + return html``; + } + + private handleIconSlotChange(event: Event) { + const slot = event.target as HTMLSlotElement; + const elements = slot.assignedElements({flatten: true}); + this.hasLeadingIcon = elements.length > 0; + this.hasAvatar = elements.some((el) => el.classList.contains('chip-avatar')); + } + + private handleRemoveIconSlotChange(event: Event) { + const slot = event.target as HTMLSlotElement; + this.hasRemoveIcon = slot.assignedNodes({flatten: true}).length > 0; + } + + private handleChange(event: Event) { + this.selected = (event.target as HTMLElement).ariaPressed === 'true'; + redispatchEvent(this, event); + } + + private handleRemove(event: Event) { + event.stopPropagation(); + if (this.disabled || this.softDisabled) return; + this.dispatchEvent(new Event('remove', {bubbles: true, composed: true})); + } +} diff --git a/labs/gb/components/chip/chip.scss b/labs/gb/components/chip/chip.scss new file mode 100644 index 0000000000..9ec20c9cbe --- /dev/null +++ b/labs/gb/components/chip/chip.scss @@ -0,0 +1,97 @@ +/*! + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// go/keep-sorted start by_regex='(.+) prefix_order=sass: +@use 'chip-tokens'; +// go/keep-sorted end + +@layer md.sys, md.comp.ripple, md.comp.focus-ring; +@layer md.comp.chip { + .chip { + @include chip-tokens.root; + &.chip-elevated { + @include chip-tokens.elevated; + } + &.chip-filled { + @include chip-tokens.filled; + } + &.chip-tonal { + @include chip-tokens.tonal; + } + &.chip-outlined { + @include chip-tokens.outlined; + } + &:is(.chip-selected, [aria-pressed='true']) { + @include chip-tokens.selected; + } + &.chip-filter { + @include chip-tokens.filter; + } + &.chip-input { + @include chip-tokens.input; + } + &.chip-with-leading-icon { + @include chip-tokens.with-leading-icon; + } + &.chip-with-trailing-icon { + @include chip-tokens.with-trailing-icon; + } + &.chip-with-avatar { + @include chip-tokens.with-avatar; + } + &:is(:disabled, .disabled, [aria-disabled='true']) { + @include chip-tokens.disabled; + } + + & { + display: inline-flex; + align-items: center; + justify-content: center; + height: var(--height); + gap: var(--gap-horizontal); + padding-inline: var(--padding-leading) var(--padding-trailing); + padding-block: var(--padding-top) var(--padding-bottom); + border: var(--outline-width) solid var(--outline-color); + border-radius: var(--shape); + box-shadow: var(--container-elevation); + background-color: var(--container-color); + color: var(--label-text-color); + font: var(--label-text); + box-sizing: border-box; + user-select: none; + white-space: nowrap; + --md-icon-color: var(--leading-icon-color); + --md-icon-size: var(--leading-icon-size); + } + &:not(:disabled, .disabled, [aria-disabled='true']) { + cursor: pointer; + } + + .chip-action, + .chip-remove { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--gap-horizontal); + background: transparent; + border: none; + outline: none; + color: inherit; + font: inherit; + padding: 0; + margin: 0; + height: 100%; + cursor: inherit; + } + .chip-remove { + border-radius: 50%; + aspect-ratio: 1; + margin-inline-start: 4px; + outline: var(--focus-ring-outline); + --md-icon-color: var(--trailing-icon-color); + --md-icon-size: var(--trailing-icon-size); + } + } +} diff --git a/labs/gb/components/chip/chip.ts b/labs/gb/components/chip/chip.ts new file mode 100644 index 0000000000..5b77452495 --- /dev/null +++ b/labs/gb/components/chip/chip.ts @@ -0,0 +1,157 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {type ClassInfo} from 'lit/directives/class-map.js'; +import { + afterDispatch, + setupDispatchHooks, +} from '../../../../internal/events/dispatch-hooks.js'; +import {focusRingClasses} from '../focus/focus-ring.js'; +import {rippleClasses, setupRipple} from '../ripple/ripple.js'; +import {createClassMapDirective} from '../shared/directives.js'; +import {isDisabled, PSEUDO_CLASSES} from '../shared/pseudo-classes.js'; + +/** Chip color/emphasis configuration types. */ +export type ChipColor = 'elevated' | 'filled' | 'outlined' | 'tonal'; + +/** Chip color configurations. */ +export const CHIP_COLORS = { + elevated: 'elevated', + filled: 'filled', + outlined: 'outlined', + tonal: 'tonal', +} as const; + +/** Chip functional type. */ +export type ChipType = 'action' | 'filter' | 'toggle' | 'link'; + +/** Chip classes. */ +export const CHIP_CLASSES = { + chip: 'chip', + chipElevated: 'chip-elevated', + chipFilled: 'chip-filled', + chipOutlined: 'chip-outlined', + chipTonal: 'chip-tonal', + chipSelected: 'chip-selected', + chipFilter: 'chip-filter', + chipInput: 'chip-input', + chipRemovable: 'chip-removable', + chipWithLeadingIcon: 'chip-with-leading-icon', + chipWithTrailingIcon: 'chip-with-trailing-icon', + chipWithAvatar: 'chip-with-avatar', + disabled: PSEUDO_CLASSES.disabled, +} as const; + +/** The state provided to the `chipClasses()` function. */ +export interface ChipClassesState { + /** The emphasis color of the chip. */ + color?: ChipColor; + /** The functional behavior type of the chip (`action`, `filter`, `toggle`, `link`). */ + type?: ChipType; + /** Whether the filter or toggle chip is selected. */ + selected?: boolean; + /** Whether the chip is removable (`removable="true"`). */ + removable?: boolean; + /** Whether the chip has a leading icon. */ + withLeadingIcon?: boolean; + /** Whether the chip has a trailing icon. */ + withTrailingIcon?: boolean; + /** Whether the chip has an avatar. */ + withAvatar?: boolean; + /** Emulates `:disabled`. */ + disabled?: boolean; +} + +/** + * Returns the chip classes to apply to an element based on the given state. + * + * @param state The state of the chip. + * @return An object of class names and truthy values if they apply. + */ +export function chipClasses({ + color = 'outlined', + type = 'action', + selected = false, + removable = false, + withLeadingIcon = false, + withTrailingIcon = false, + withAvatar = false, + disabled = false, +}: ChipClassesState = {}): ClassInfo { + return { + ...rippleClasses(), + ...focusRingClasses(), + [CHIP_CLASSES.chip]: true, + [CHIP_CLASSES.chipElevated]: color === CHIP_COLORS.elevated, + [CHIP_CLASSES.chipFilled]: color === CHIP_COLORS.filled, + [CHIP_CLASSES.chipOutlined]: color === CHIP_COLORS.outlined, + [CHIP_CLASSES.chipTonal]: color === CHIP_COLORS.tonal, + [CHIP_CLASSES.chipSelected]: selected, + [CHIP_CLASSES.chipFilter]: type === 'filter' || type === 'toggle', + [CHIP_CLASSES.chipInput]: removable || type === 'action', + [CHIP_CLASSES.chipRemovable]: removable, + [CHIP_CLASSES.chipWithLeadingIcon]: withLeadingIcon, + [CHIP_CLASSES.chipWithTrailingIcon]: withTrailingIcon, + [CHIP_CLASSES.chipWithAvatar]: withAvatar, + [CHIP_CLASSES.disabled]: disabled, + }; +} + +/** + * Sets up chip functionality for the given element. + * + * @param chip The element on which to set up chip functionality. + * @param opts Setup options, supports a cleanup `signal`. + */ +export function setupChip( + chip: HTMLElement, + opts?: {signal?: AbortSignal}, +): void { + setupDispatchHooks(chip, 'click'); + setupRipple(chip, opts); + chip.addEventListener( + 'click', + (event) => { + if (isDisabled(chip)) { + event.stopImmediatePropagation(); + event.preventDefault(); + return; + } + + afterDispatch(event, () => { + const isToggle = + chip.hasAttribute('aria-pressed') || + chip.matches( + `.${CHIP_CLASSES.chipFilter},.${CHIP_CLASSES.chipSelected}`, + ); + if (event.defaultPrevented || !isToggle) { + return; + } + + const isPressed = chip.ariaPressed === 'true'; + chip.ariaPressed = String(!isPressed); + chip.dispatchEvent( + new InputEvent('input', {bubbles: true, composed: true}), + ); + chip.dispatchEvent(new Event('change', {bubbles: true})); + }); + }, + opts, + ); +} + +/** + * A Lit directive that adds chip styling and functionality to its element. + * + * @example + * ```ts + * html``; + * ``` + */ +export const chip = createClassMapDirective({ + getClasses: chipClasses, + setupElement: setupChip, +}); diff --git a/labs/gb/components/chip/demo/demo.ts b/labs/gb/components/chip/demo/demo.ts new file mode 100644 index 0000000000..b79d3ef4f8 --- /dev/null +++ b/labs/gb/components/chip/demo/demo.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import './material-collection.js'; +import './index.js'; + +import { + KnobTypesToKnobs, + MaterialCollection, + materialInitsToStoryInits, + setUpDemo, +} from './material-collection.js'; +import {boolInput, Knob, selectDropdown} from './index.js'; + +import {stories, StoryKnobs} from './stories.js'; + +const collection = new MaterialCollection>( + 'Chip', + [ + new Knob('color', { + ui: selectDropdown({ + options: [ + {value: 'elevated', label: 'Elevated'}, + {value: 'filled', label: 'Filled'}, + {value: 'tonal', label: 'Tonal'}, + {value: 'outlined', label: 'Outlined'}, + ], + }), + }), + new Knob('type', { + ui: selectDropdown({ + options: [ + {value: 'action', label: 'Action'}, + {value: 'filter', label: 'Filter'}, + {value: 'toggle', label: 'Toggle'}, + ], + }), + }), + new Knob('selected', { + ui: boolInput(), + defaultValue: false, + }), + new Knob('removable', { + ui: boolInput(), + defaultValue: false, + }), + new Knob('disabled', { + ui: boolInput(), + defaultValue: false, + }), + new Knob('softDisabled', { + ui: boolInput(), + defaultValue: false, + }), + ], +); + +collection.addStories(...materialInitsToStoryInits(stories)); + +setUpDemo(collection, {fonts: 'roboto', icons: 'material-symbols'}); diff --git a/labs/gb/components/chip/demo/stories.ts b/labs/gb/components/chip/demo/stories.ts new file mode 100644 index 0000000000..c51ada5988 --- /dev/null +++ b/labs/gb/components/chip/demo/stories.ts @@ -0,0 +1,164 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import '@material/web/labs/gb/components/chip/md-gb-chip.js'; +import '@material/web/labs/gb/styles/icon/md-gb-icon.js'; + +import {MaterialStoryInit} from './material-collection.js'; +import { + type ChipColor, + type ChipType, +} from '@material/web/labs/gb/components/chip/chip.js'; +import {adoptStyles} from '@material/web/labs/gb/styles/adopt-styles.js'; +import {styles as m3Styles} from '@material/web/labs/gb/styles/m3.cssresult.js'; +import {css, html} from 'lit'; + +/** Knob types for chip stories. */ +export interface StoryKnobs { + color?: ChipColor; + type?: ChipType; + selected?: boolean; + removable?: boolean; + disabled?: boolean; + softDisabled?: boolean; +} + +adoptStyles(document, [ + m3Styles, + css` + :root { + --md-icon-font: 'Material Symbols Outlined'; + } + `, +]); + +const styles = css` + :host, + .demo-group { + font-family: var( + --md-sys-typescale-body-md-font, + var(--md-ref-typeface-plain, Roboto, sans-serif) + ); + } + .demo-group { + display: flex; + gap: 12px; + flex-wrap: wrap; + align-items: center; + margin-bottom: 24px; + padding: 16px; + border: 1px solid var(--md-sys-color-outline-variant); + border-radius: 16px; + background-color: var(--md-sys-color-surface); + } + .chip-avatar { + width: 24px; + height: 24px; + border-radius: 50%; + background-color: var(--md-sys-color-primary); + color: var(--md-sys-color-on-primary); + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 600; + } +`; + +const playground: MaterialStoryInit = { + name: 'Playground', + styles, + render(knobs) { + return html` +
+ + star + Expressive Chip + +
+ `; + }, +}; + +const variants: MaterialStoryInit = { + name: 'Expressive Action Variants', + styles, + render() { + return html` +
+ + schedule + Elevated Action + + + flight + Filled Action + + + event + Tonal Action + + + bookmark + Outlined Action + +
+ `; + }, +}; + +const functional: MaterialStoryInit = { + name: 'Filter & Removable Chip', + styles, + render() { + return html` +
+ Unselected Filter + Selected Filter + Removable Tag + Disabled Tag +
+ `; + }, +}; + +const avatarsAndIcons: MaterialStoryInit = { + name: 'Avatars & Leading Icons', + styles, + render() { + return html` +
+ + A + Avatar Chip + + + B + Removable Avatar + + + face + Leading Icon Chip + +
+ `; + }, +}; + +/** Chip stories. */ +export const stories = [playground, variants, functional, avatarsAndIcons]; diff --git a/labs/gb/components/chip/md-gb-chip.ts b/labs/gb/components/chip/md-gb-chip.ts new file mode 100644 index 0000000000..532702d90c --- /dev/null +++ b/labs/gb/components/chip/md-gb-chip.ts @@ -0,0 +1,18 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {ChipElement} from './chip-element.js'; + +export * from './chip-element.js'; + +declare global { + interface HTMLElementTagNameMap { + /** A Material Design Expressive Chip component. */ + 'md-gb-chip': ChipElement; + } +} + +customElements.define('md-gb-chip', ChipElement);