From 8454b18ca46577a1aad8abcf5d744f278ccf563d Mon Sep 17 00:00:00 2001 From: "Nichols, Kieran" Date: Fri, 23 Feb 2024 15:51:13 -0500 Subject: [PATCH] feat(chips): refactor to use tokens --- src/dev/pages/chips/chips.ejs | 175 +++- src/dev/pages/chips/chips.html | 15 + src/dev/pages/chips/chips.ts | 22 +- src/lib/avatar/index.scss | 1 + src/lib/chip-field/chip-field-foundation.ts | 2 +- src/lib/chips/build.json | 4 - src/lib/chips/chip-set/_core.scss | 17 + src/lib/chips/chip-set/_mixins.scss | 35 - src/lib/chips/chip-set/_token-utils.scss | 25 + src/lib/chips/chip-set/_variables.scss | 1 - src/lib/chips/chip-set/chip-set-adapter.ts | 73 -- src/lib/chips/chip-set/chip-set-constants.ts | 15 +- src/lib/chips/chip-set/chip-set-foundation.ts | 101 -- src/lib/chips/chip-set/chip-set.scss | 30 +- src/lib/chips/chip-set/chip-set.ts | 141 ++- src/lib/chips/chip-set/index.scss | 1 + src/lib/chips/chip-set/index.ts | 2 - src/lib/chips/chip/_core.scss | 132 +++ src/lib/chips/chip/_mixins.scss | 301 ------ src/lib/chips/chip/_token-utils.scss | 25 + src/lib/chips/chip/_variables.scss | 3 - src/lib/chips/chip/chip-adapter.ts | 323 +++---- src/lib/chips/chip/chip-checkmark.html | 6 +- src/lib/chips/chip/chip-constants.ts | 46 +- src/lib/chips/chip/chip-foundation.ts | 395 ++++---- src/lib/chips/chip/chip.html | 16 +- src/lib/chips/chip/chip.scss | 280 +++++- src/lib/chips/chip/chip.ts | 112 ++- src/lib/chips/chip/index.scss | 1 + src/lib/chips/chips.test.ts | 914 ++++++++++++++++++ .../styles/tokens/chips/chip-set/_tokens.scss | 11 + .../styles/tokens/chips/chip/_tokens.scss | 82 ++ src/lib/theme/_theme-dark.scss | 6 + 33 files changed, 2251 insertions(+), 1062 deletions(-) create mode 100644 src/lib/avatar/index.scss delete mode 100644 src/lib/chips/build.json create mode 100644 src/lib/chips/chip-set/_core.scss delete mode 100644 src/lib/chips/chip-set/_mixins.scss create mode 100644 src/lib/chips/chip-set/_token-utils.scss delete mode 100644 src/lib/chips/chip-set/_variables.scss delete mode 100644 src/lib/chips/chip-set/chip-set-adapter.ts delete mode 100644 src/lib/chips/chip-set/chip-set-foundation.ts create mode 100644 src/lib/chips/chip-set/index.scss create mode 100644 src/lib/chips/chip/_core.scss delete mode 100644 src/lib/chips/chip/_mixins.scss create mode 100644 src/lib/chips/chip/_token-utils.scss delete mode 100644 src/lib/chips/chip/_variables.scss create mode 100644 src/lib/chips/chip/index.scss create mode 100644 src/lib/chips/chips.test.ts create mode 100644 src/lib/core/styles/tokens/chips/chip-set/_tokens.scss create mode 100644 src/lib/core/styles/tokens/chips/chip/_tokens.scss diff --git a/src/dev/pages/chips/chips.ejs b/src/dev/pages/chips/chips.ejs index c7e1a83af..2db8d07ff 100644 --- a/src/dev/pages/chips/chips.ejs +++ b/src/dev/pages/chips/chips.ejs @@ -1,158 +1,243 @@
-
+ +

Basic chips

- + Small Medium Large -
+ + + +
+

Chips w/anchor links

+ + Anchor chip + + + Alert + + Console log + + Google + + + +
-
+ +

Choice chips

Small Medium Large -
+ -
+ +

Choice chips (vertical)

Small Medium Large -
+ -
-

Filter chips

-

No leading icon

+ +
+

Filter chips w/out icons

Tops Bottoms Shoes Accessories -
+ -
-

With leading icon

+ +
+

With start icon

- + Alice - + Bob - + Charlie - + Danielle -
+ -
-

Action chips

+ +
+

With end icon

+ + + Alice + + + + Bob + + + + Charlie + + + + Danielle + + + +
+ + +
+

With start avatar

- + Add to calendar - + Bookmark - + Set alarm - + Get directions -
+ -
-

With leading avatar

+ +
+

With end avatar

- Add to calendar + - Bookmark + - Set alarm + - Get directions + -
+ -
+ +
+

Action chips

+ + + + Add to calendar + + + + Bookmark + + + + Set alarm + + + + Get directions + + +
+ + +

Input chips - +

- + Falmouth - + Yarmouth - + Plano - + Renton -
+ -
+ +

Invalid chips

- + Falmouth - + Yarmouth - + Plano - + Renton -
+ + + +
+

Field chips

+ + + + + Small + Medium + Large + + +
diff --git a/src/dev/pages/chips/chips.html b/src/dev/pages/chips/chips.html index f30bcd2df..ff46891d9 100644 --- a/src/dev/pages/chips/chips.html +++ b/src/dev/pages/chips/chips.html @@ -4,6 +4,21 @@ title: 'Chips', includePath: './pages/chips/chips.ejs', options: [ + { + type: 'select', + label: 'Theme', + id: 'opt-theme', + defaultValue: 'primary', + options: [ + { label: 'Primary (default)', value: 'primary' }, + { label: 'Secondary', value: 'secondary' }, + { label: 'Tertiary', value: 'tertiary' }, + { label: 'Success', value: 'success' }, + { label: 'Error', value: 'error' }, + { label: 'Warning', value: 'warning' }, + { label: 'Info', value: 'info' } + ] + }, { type: 'switch', label: 'Dense', id: 'opt-dense' }, { type: 'switch', label: 'Disabled', id: 'opt-disabled' } ] diff --git a/src/dev/pages/chips/chips.ts b/src/dev/pages/chips/chips.ts index f6a9af05e..29c032e6e 100644 --- a/src/dev/pages/chips/chips.ts +++ b/src/dev/pages/chips/chips.ts @@ -1,19 +1,22 @@ import '$src/shared'; import '@tylertech/forge/chips'; import '@tylertech/forge/icon-button'; -import { IChipSetComponent, IconRegistry, ISwitchComponent } from '@tylertech/forge'; +import { IChipSetComponent, IconRegistry, ISelectComponent, ISwitchComponent } from '@tylertech/forge'; import type { IChipComponent } from '@tylertech/forge'; -import { tylIconAlarm, tylIconBookmark, tylIconDirections, tylIconEvent, tylIconFace, tylIconPlace, tylIconRefresh } from '@tylertech/tyler-icons/standard'; +import { tylIconAlarm, tylIconBookmark, tylIconDirections, tylIconEvent, tylIconOpenInNew, tylIconPlace, tylIconRefresh } from '@tylertech/tyler-icons/standard'; +import { tylIconAccount, tylIconAlert } from '@tylertech/tyler-icons/extended'; import { showToast } from '$src/utils/utils'; IconRegistry.define([ tylIconRefresh, - tylIconFace, + tylIconAccount, tylIconEvent, tylIconBookmark, tylIconAlarm, tylIconDirections, - tylIconPlace + tylIconPlace, + tylIconOpenInNew, + tylIconAlert ]); const chipsDenseToggle = document.querySelector('#opt-dense') as ISwitchComponent; @@ -23,6 +26,7 @@ const chipsDisabledToggle = document.querySelector('#opt-disabled') as ISwitchCo const actionExample = document.querySelector('#chips-action'); const actionChipSet = actionExample.querySelector('forge-chip-set'); actionChipSet.addEventListener('forge-chip-select', ({ detail: { value }}) => { + console.log('[forge-chip-select]', value); showToast(`Action Chip Selected: ${value}`); }); @@ -31,6 +35,7 @@ let deletedChips = []; const inputExample = document.querySelector('#chips-input'); const inputChipSet = inputExample.querySelector('forge-chip-set[type=input]'); inputChipSet.addEventListener('forge-chip-delete', ({ target }) => { + console.log('[forge-chip-delete]', target); deletedChips.push(target); (target as IChipComponent).remove(); }); @@ -42,6 +47,15 @@ refreshButton.addEventListener('click', () => { deletedChips = []; }); +// Theme +const themeSelect = document.querySelector('#opt-theme') as ISelectComponent; +themeSelect.addEventListener('change', ({ detail }) => { + const chipSets = document.querySelectorAll('forge-chip-set') as NodeListOf; + for (const chipSet of chipSets) { + chipSet.theme = detail; + } +}); + // Dense toggling chipsDenseToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { const chipSets = document.querySelectorAll('forge-chip-set') as NodeListOf; diff --git a/src/lib/avatar/index.scss b/src/lib/avatar/index.scss new file mode 100644 index 000000000..98a38c0d9 --- /dev/null +++ b/src/lib/avatar/index.scss @@ -0,0 +1 @@ +@forward './core'; diff --git a/src/lib/chip-field/chip-field-foundation.ts b/src/lib/chip-field/chip-field-foundation.ts index 096cb3fe1..8a0e00909 100644 --- a/src/lib/chip-field/chip-field-foundation.ts +++ b/src/lib/chip-field/chip-field-foundation.ts @@ -155,7 +155,7 @@ export class ChipFieldFoundation extends FieldFoundation implements IChipFieldFo } private _memberIsActive(ele: HTMLElement): boolean { - return getActiveElement(ele.ownerDocument) === ele || ele.hasAttribute('focused'); + return ele.matches(':focus-within') || getActiveElement(ele.ownerDocument) === ele || ele.hasAttribute('focused'); } private _getActiveMember(): HTMLElement | null { diff --git a/src/lib/chips/build.json b/src/lib/chips/build.json deleted file mode 100644 index 07c79a465..000000000 --- a/src/lib/chips/build.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "../../../node_modules/@tylertech/forge-cli/config/build-schema.json", - "extends": "../build.json" -} \ No newline at end of file diff --git a/src/lib/chips/chip-set/_core.scss b/src/lib/chips/chip-set/_core.scss new file mode 100644 index 000000000..6744315cb --- /dev/null +++ b/src/lib/chips/chip-set/_core.scss @@ -0,0 +1,17 @@ +@use './token-utils' as *; + +@forward './token-utils'; + +@mixin host { + display: inline-block; +} + +@mixin base { + display: flex; + flex-wrap: wrap; + gap: #{token(spacing)}; +} + +@mixin vertical { + flex-direction: column; +} diff --git a/src/lib/chips/chip-set/_mixins.scss b/src/lib/chips/chip-set/_mixins.scss deleted file mode 100644 index 1fa07c140..000000000 --- a/src/lib/chips/chip-set/_mixins.scss +++ /dev/null @@ -1,35 +0,0 @@ -@use '../../theme'; -@use './variables'; - -@mixin core-styles() { - .forge-chip-set { - @include base; - - ::slotted(forge-chip:not(:last-child)) { - @include theme.css-custom-property(margin-right, --forge-chip-set-spacing, variables.$spacing); - } - - &--vertical { - @include vertical; - - ::slotted(forge-chip:not(:last-child)) { - @include theme.css-custom-property(margin-bottom, --forge-chip-set-spacing, variables.$spacing); - } - } - } -} - -@mixin host() { - display: inline-block; -} - -@mixin base() { - @include theme.css-custom-property(gap, --forge-chip-set-spacing, variables.$spacing); - - display: flex; - flex-wrap: wrap; -} - -@mixin vertical() { - flex-direction: column; -} diff --git a/src/lib/chips/chip-set/_token-utils.scss b/src/lib/chips/chip-set/_token-utils.scss new file mode 100644 index 000000000..ea06594b1 --- /dev/null +++ b/src/lib/chips/chip-set/_token-utils.scss @@ -0,0 +1,25 @@ +@use '../../core/styles/tokens/chips/chip-set/tokens'; +@use '../../core/styles/tokens/token-utils'; + +$_module: chip-set; +$_tokens: tokens.$tokens; + +@mixin provide-theme($theme) { + @include token-utils.provide-theme($_module, $_tokens, $theme); +} + +@function token($name, $type: token) { + @return token-utils.token($_module, $_tokens, $name, $type); +} + +@function declare($token) { + @return token-utils.declare($_module, $token); +} + +@mixin override($token, $token-or-value, $type: token) { + @include token-utils.override($_module, $_tokens, $token, $token-or-value, $type); +} + +@mixin tokens($includes: null, $excludes: null) { + @include token-utils.tokens($_module, $_tokens, $includes, $excludes); +} diff --git a/src/lib/chips/chip-set/_variables.scss b/src/lib/chips/chip-set/_variables.scss deleted file mode 100644 index 20bb0ab3f..000000000 --- a/src/lib/chips/chip-set/_variables.scss +++ /dev/null @@ -1 +0,0 @@ -$spacing: 4px !default; diff --git a/src/lib/chips/chip-set/chip-set-adapter.ts b/src/lib/chips/chip-set/chip-set-adapter.ts deleted file mode 100644 index 9265ad698..000000000 --- a/src/lib/chips/chip-set/chip-set-adapter.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { getShadowElement, toggleClass } from '@tylertech/forge-core'; -import { BaseAdapter, IBaseAdapter } from '../../core/base/base-adapter'; -import { IChipComponent } from '../chip/chip'; -import { ChipType, CHIP_CONSTANTS } from '../chip/chip-constants'; -import { IChipSetComponent } from './chip-set'; -import { CHIP_SET_CONSTANTS } from './chip-set-constants'; - -export interface IChipSetAdapter extends IBaseAdapter { - setVertical(value: boolean): void; - setType(value: ChipType): void; - setDense(value: boolean): void; - setDisabled(value: boolean): void; - tryFocusPrevious(fromChip: EventTarget | null): void; - tryFocusNext(fromChip: EventTarget | null): void; -} - -export class ChipSetAdapter extends BaseAdapter implements IChipSetAdapter { - private _rootElement: HTMLElement; - - constructor(component: IChipSetComponent) { - super(component); - this._rootElement = getShadowElement(this._component, CHIP_SET_CONSTANTS.selectors.ROOT); - } - - public setVertical(value: boolean): void { - toggleClass(this._rootElement, value, CHIP_SET_CONSTANTS.classes.VERTICAL); - } - - public setType(value: ChipType): void { - const chips = this._getChips(); - chips.forEach(c => c.type = value); - } - - public setDense(value: boolean): void { - const chips = this._getChips(); - chips.forEach(c => c.dense = value); - } - - public setDisabled(value: boolean): void { - const chips = this._getChips(); - chips.forEach(c => c.disabled = value); - } - - public tryFocusPrevious(fromChip: EventTarget | null): void { - const chips = this._getChips(); - const activeChipIndex = chips.findIndex(chip => chip === fromChip); - - if (activeChipIndex >= 0) { - if (activeChipIndex === 0) { - chips[chips.length - 1].tryFocusDelete(); - } else { - chips[activeChipIndex - 1].tryFocusDelete(); - } - } - } - - public tryFocusNext(fromChip: EventTarget | null): void { - const chips = this._getChips(); - const activeChipIndex = chips.findIndex(chip => chip === fromChip); - - if (activeChipIndex >= 0) { - if (activeChipIndex === chips.length - 1) { - chips[0].focus(); - } else { - chips[activeChipIndex + 1].focus(); - } - } - } - - private _getChips(): IChipComponent[] { - return Array.from(this._component.querySelectorAll(CHIP_CONSTANTS.elementName)); - } -} diff --git a/src/lib/chips/chip-set/chip-set-constants.ts b/src/lib/chips/chip-set/chip-set-constants.ts index b131fceae..4e5452c00 100644 --- a/src/lib/chips/chip-set/chip-set-constants.ts +++ b/src/lib/chips/chip-set/chip-set-constants.ts @@ -2,25 +2,26 @@ import { COMPONENT_NAME_PREFIX } from '../../constants'; const elementName: keyof HTMLElementTagNameMap = `${COMPONENT_NAME_PREFIX}chip-set`; -const attributes = { +const observedAttributes = { VERTICAL: 'vertical', TYPE: 'type', DENSE: 'dense', - DISABLED: 'disabled' + DISABLED: 'disabled', + INVALID: 'invalid', + THEME: 'theme' }; -const classes = { - ROOT: 'forge-chip-set', - VERTICAL: 'forge-chip-set--vertical' +const attributes = { + ...observedAttributes }; const selectors = { - ROOT: `.${classes.ROOT}` + ROOT: '.forge-chip-set' }; export const CHIP_SET_CONSTANTS = { elementName, + observedAttributes, attributes, - classes, selectors }; diff --git a/src/lib/chips/chip-set/chip-set-foundation.ts b/src/lib/chips/chip-set/chip-set-foundation.ts deleted file mode 100644 index a10c0339e..000000000 --- a/src/lib/chips/chip-set/chip-set-foundation.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { ICustomElementFoundation } from '@tylertech/forge-core'; - -import { IChipSetAdapter } from './chip-set-adapter'; -import { CHIP_CONSTANTS, ChipType } from '../chip/chip-constants'; -import { CHIP_SET_CONSTANTS } from './chip-set-constants'; - -export interface IChipSetFoundation extends ICustomElementFoundation { - vertical: boolean; - type: ChipType; - dense: boolean; - disabled: boolean; -} - -export class ChipSetFoundation implements IChipSetFoundation { - private _vertical = false; - private _type: ChipType = CHIP_CONSTANTS.defaults.TYPE as ChipType; - private _dense = false; - private _disabled = false; - private _focusPreviousListener: (evt: CustomEvent) => void; - private _focusNextListener: (evt: CustomEvent) => void; - - constructor(private _adapter: IChipSetAdapter) { - this._focusPreviousListener = (evt: CustomEvent) => this._adapter.tryFocusPrevious(evt.target); - this._focusNextListener = (evt: CustomEvent) => this._adapter.tryFocusNext(evt.target); - } - - public initialize(): void { - this._adapter.addHostListener(CHIP_CONSTANTS.events.FOCUS_PREVIOUS, this._focusPreviousListener); - this._adapter.addHostListener(CHIP_CONSTANTS.events.FOCUS_NEXT, this._focusNextListener); - - this._applyVertical(); - this._applyType(); - this._applyDense(); - this._applyDisabled(); - } - - public destroy(): void { - this._adapter.removeHostListener(CHIP_CONSTANTS.events.FOCUS_PREVIOUS, this._focusPreviousListener); - this._adapter.removeHostListener(CHIP_CONSTANTS.events.FOCUS_NEXT, this._focusNextListener); - } - - private _applyVertical(): void { - this._adapter.setVertical(this._vertical); - this._adapter.toggleHostAttribute(CHIP_SET_CONSTANTS.attributes.VERTICAL, this._vertical); - } - - private _applyType(): void { - this._adapter.setType(this._type); - this._adapter.setHostAttribute(CHIP_SET_CONSTANTS.attributes.TYPE, this._type); - } - - private _applyDense(): void { - this._adapter.setDense(this._dense); - this._adapter.toggleHostAttribute(CHIP_SET_CONSTANTS.attributes.DENSE, this._dense); - } - - private _applyDisabled(): void { - this._adapter.setDisabled(this._disabled); - this._adapter.toggleHostAttribute(CHIP_SET_CONSTANTS.attributes.DISABLED, this._disabled); - } - - public get vertical(): boolean { - return this._vertical; - } - public set vertical(value: boolean) { - if (this._vertical !== value) { - this._vertical = value; - this._applyVertical(); - } - } - - public get type(): ChipType { - return this._type; - } - public set type(value: ChipType) { - if (this._type !== value) { - this._type = value; - this._applyType(); - } - } - - public get dense(): boolean { - return this._dense; - } - public set dense(value: boolean) { - if (this._dense !== value) { - this._dense = value; - this._applyDense(); - } - } - - public get disabled(): boolean { - return this._disabled; - } - public set disabled(value: boolean) { - if (this._disabled !== value) { - this._disabled = value; - this._applyDisabled(); - } - } -} diff --git a/src/lib/chips/chip-set/chip-set.scss b/src/lib/chips/chip-set/chip-set.scss index 165c17cbf..b1bd794a2 100644 --- a/src/lib/chips/chip-set/chip-set.scss +++ b/src/lib/chips/chip-set/chip-set.scss @@ -1,11 +1,35 @@ -@use './mixins'; +@use './core' as *; -@include mixins.core-styles; +// +// Host +// :host { - @include mixins.host; + @include host; } :host([hidden]) { display: none; } + +// +// Base +// + +.forge-chip-set { + @include tokens; +} + +.forge-chip-set { + @include base; +} + +// +// Vertical +// + +:host([vertical]) { + .forge-chip-set { + @include vertical; + } +} diff --git a/src/lib/chips/chip-set/chip-set.ts b/src/lib/chips/chip-set/chip-set.ts index e887f9237..2d811bf92 100644 --- a/src/lib/chips/chip-set/chip-set.ts +++ b/src/lib/chips/chip-set/chip-set.ts @@ -1,10 +1,8 @@ -import { attachShadowTemplate, coerceBoolean, CustomElement, FoundationProperty } from '@tylertech/forge-core'; +import { attachShadowTemplate, coerceBoolean, CustomElement } from '@tylertech/forge-core'; import { BaseComponent, IBaseComponent } from '../../core/base/base-component'; -import { ChipComponent } from '../chip/chip'; -import { ChipType } from '../chip/chip-constants'; -import { ChipSetAdapter } from './chip-set-adapter'; +import { ChipComponent, IChipComponent } from '../chip/chip'; +import { ChipTheme, ChipType, CHIP_CONSTANTS, IChipNavigateEventData } from '../chip/chip-constants'; import { CHIP_SET_CONSTANTS } from './chip-set-constants'; -import { ChipSetFoundation } from './chip-set-foundation'; import template from './chip-set.html'; import styles from './chip-set.scss'; @@ -14,6 +12,8 @@ export interface IChipSetComponent extends IBaseComponent { type: ChipType; dense: boolean; disabled: boolean; + invalid: boolean; + theme: ChipTheme; } declare global { @@ -23,8 +23,6 @@ declare global { } /** - * The web component class behind the `` custom element. - * * @tag forge-chip-set */ @CustomElement({ @@ -33,56 +31,133 @@ declare global { }) export class ChipSetComponent extends BaseComponent implements IChipSetComponent { public static get observedAttributes(): string[] { - return [ - CHIP_SET_CONSTANTS.attributes.VERTICAL, - CHIP_SET_CONSTANTS.attributes.TYPE, - CHIP_SET_CONSTANTS.attributes.DENSE, - CHIP_SET_CONSTANTS.attributes.DISABLED - ]; + return Object.values(CHIP_SET_CONSTANTS.observedAttributes); } - - private _foundation: ChipSetFoundation; + + private _vertical = false; + private _type = CHIP_CONSTANTS.defaults.TYPE; + private _dense = false; + private _disabled = false; + private _invalid = false; + private _theme = CHIP_CONSTANTS.defaults.THEME; constructor() { super(); attachShadowTemplate(this, template, styles); - this._foundation = new ChipSetFoundation(new ChipSetAdapter(this)); } public connectedCallback(): void { - this._foundation.initialize(); - } - - public disconnectedCallback(): void { - this._foundation.destroy(); + this.addEventListener(CHIP_CONSTANTS.events.NAVIGATE, this._onChipNavigate.bind(this)); } public attributeChangedCallback(name: string, oldValue: string, newValue: string): void { switch (name) { - case CHIP_SET_CONSTANTS.attributes.VERTICAL: + case CHIP_SET_CONSTANTS.observedAttributes.VERTICAL: this.vertical = coerceBoolean(newValue); break; - case CHIP_SET_CONSTANTS.attributes.TYPE: + case CHIP_SET_CONSTANTS.observedAttributes.TYPE: this.type = newValue as ChipType; break; - case CHIP_SET_CONSTANTS.attributes.DENSE: + case CHIP_SET_CONSTANTS.observedAttributes.DENSE: this.dense = coerceBoolean(newValue); break; - case CHIP_SET_CONSTANTS.attributes.DISABLED: + case CHIP_SET_CONSTANTS.observedAttributes.DISABLED: this.disabled = coerceBoolean(newValue); break; + case CHIP_SET_CONSTANTS.observedAttributes.INVALID: + this.invalid = coerceBoolean(newValue); + break; + case CHIP_SET_CONSTANTS.observedAttributes.THEME: + this.theme = newValue as ChipTheme; + break; + } + } + + private _onChipNavigate(evt: CustomEvent): void { + const focusableChips = this._findChipDescendants().filter(chip => !chip.disabled); + const activeChipIndex = focusableChips.findIndex(chip => chip === evt.target); + let index = evt.detail.direction === 'previous' ? activeChipIndex - 1 : activeChipIndex + 1; + if (index > focusableChips.length - 1) { + index = 0; + } + focusableChips.at(index)?.focus(); + } + + private _findChipDescendants(): IChipComponent[] { + return Array.from(this.querySelectorAll(CHIP_CONSTANTS.elementName)); + } + + private _syncChipsProperty(property: T, value: IChipComponent[T]): void { + const chips = this._findChipDescendants(); + chips.forEach(c => c[property] = value); + } + + public get vertical(): boolean { + return this._vertical; + } + public set vertical(value: boolean) { + value = Boolean(value); + if (this._vertical !== value) { + this._vertical = value; + this.toggleAttribute(CHIP_SET_CONSTANTS.attributes.VERTICAL, this._vertical); } } - @FoundationProperty() - public declare vertical: boolean; + public get type(): ChipType { + return this._type; + } + public set type(value: ChipType) { + if (this._type !== value) { + this._type = value; + this._syncChipsProperty('type', this._type); + this.setAttribute(CHIP_SET_CONSTANTS.attributes.TYPE, this._type); + } + } + + public get dense(): boolean { + return this._dense; + } + public set dense(value: boolean) { + value = Boolean(value); + if (this._dense !== value) { + this._dense = value; + this._syncChipsProperty('dense', this._dense); + this.toggleAttribute(CHIP_SET_CONSTANTS.attributes.DENSE, this._dense); + } + } - @FoundationProperty() - public declare type: ChipType; + public get disabled(): boolean { + return this._disabled; + } + public set disabled(value: boolean) { + value = Boolean(value); + if (this._disabled !== value) { + this._disabled = value; + this._syncChipsProperty('disabled', this._disabled); + this.toggleAttribute(CHIP_SET_CONSTANTS.attributes.DISABLED, this._disabled); + } + } - @FoundationProperty() - public declare dense: boolean; + public get invalid(): boolean { + return this._invalid; + } + public set invalid(value: boolean) { + value = Boolean(value); + if (this._invalid !== value) { + this._invalid = value; + this._syncChipsProperty('invalid', this._invalid); + this.toggleAttribute(CHIP_SET_CONSTANTS.attributes.INVALID, this._invalid); + } + } - @FoundationProperty() - public declare disabled: boolean; + public get theme(): ChipTheme { + return this._theme; + } + public set theme(value: ChipTheme) { + if (this._theme !== value) { + this._theme = value; + this._syncChipsProperty('theme', this._theme); + this.setAttribute(CHIP_SET_CONSTANTS.attributes.THEME, this._theme); + } + } } diff --git a/src/lib/chips/chip-set/index.scss b/src/lib/chips/chip-set/index.scss new file mode 100644 index 000000000..98a38c0d9 --- /dev/null +++ b/src/lib/chips/chip-set/index.scss @@ -0,0 +1 @@ +@forward './core'; diff --git a/src/lib/chips/chip-set/index.ts b/src/lib/chips/chip-set/index.ts index f9f834ddc..5aa859dd4 100644 --- a/src/lib/chips/chip-set/index.ts +++ b/src/lib/chips/chip-set/index.ts @@ -2,9 +2,7 @@ import { defineCustomElement } from '@tylertech/forge-core'; import { ChipSetComponent } from './chip-set'; -export * from './chip-set-adapter'; export * from './chip-set-constants'; -export * from './chip-set-foundation'; export * from './chip-set'; export function defineChipSetComponent(): void { diff --git a/src/lib/chips/chip/_core.scss b/src/lib/chips/chip/_core.scss new file mode 100644 index 000000000..6cde115b1 --- /dev/null +++ b/src/lib/chips/chip/_core.scss @@ -0,0 +1,132 @@ +@use '../../core/styles/typography'; +@use './token-utils' as *; + +@forward './token-utils'; + +@mixin host { + display: inline-block; +} + +@mixin base { + position: relative; + + display: inline-flex; + align-items: center; + box-sizing: border-box; + height: #{token(height)}; + + border-width: #{token(border-width)}; + border-style: #{token(border-style)}; + border-color: #{token(border-color)}; + border-radius: #{token(shape)}; + + background: #{token(background)}; + color: #{token(color)}; + + transition-property: background-color, color; + transition-duration: #{token(transition-duration)}; + transition-timing-function: #{token(transition-easing)}; +} + +@mixin trigger { + @include typography.style(button); + + display: inline-flex; + align-items: center; + justify-content: center; + gap: #{token(spacing)}; + + box-sizing: border-box; + height: 100%; + padding-inline: #{token(padding-inline)}; + padding-block: #{token(padding-block)}; + + cursor: #{token(cursor)}; + + z-index: 0; + + text-decoration: none; + border: none; + + background: inherit; + color: inherit; + -webkit-tap-highlight-color: transparent; + + border-radius: #{token(shape)}; + outline: none; +} + +@mixin remove-button { + padding-inline: #{token(remove-button-spacing)}; +} + +@mixin selected { + @include override(background, selected-background); + @include override(color, selected-color); +} + +@mixin invalid { + @include override(color, invalid-color); +} + +@mixin invalid-selected { + @include override(background, invalid-selected-background); + @include override(color, invalid-selected-color); +} + +@mixin field { + @include override(background, field-background); + @include override(color, field-color); + @include override(border-color, field-border-color); + @include override(shape, field-shape); + @include override(cursor, field-cursor); +} + +@mixin dense { + @include override(height, dense-height); + @include override(padding-inline, dense-padding-inline); + @include override(spacing, dense-spacing); +} + +@mixin disabled { + opacity: #{token(disabled-opacity)}; +} + +@mixin trigger-disabled { + cursor: #{token(disabled-cursor)}; +} + +@mixin checkmark-base { + height: #{token(checkmark-size)}; + +svg { + width: 0; + height: #{token(checkmark-size)}; + transition-property: width; + transition-duration: #{token(transition-duration)}; + transition-timing-function: #{token(transition-easing)}; + + path { + stroke: #{token(checkmark-color)}; + transition-property: stroke-dashoffset; + transition-duration: #{token(transition-duration)}; + transition-timing-function: #{token(transition-easing)}; + stroke-width: 2px; + stroke-dashoffset: 29.7833385; + stroke-dasharray: 29.7833385; + } + } +} + +@mixin checkmark-selected { + padding-inline-start: #{token(checkmark-spacing)}; + + svg { + width: #{token(checkmark-size)}; + + path { + transition-delay: #{token(checkmark-transition-delay)}; + stroke-dashoffset: 0; + } + } +} diff --git a/src/lib/chips/chip/_mixins.scss b/src/lib/chips/chip/_mixins.scss deleted file mode 100644 index d35bc45b3..000000000 --- a/src/lib/chips/chip/_mixins.scss +++ /dev/null @@ -1,301 +0,0 @@ -@use '@material/chips/chip-theme' as mdc-chip-theme; -@use '@material/ripple/ripple' as mdc-ripple; -@use '@material/ripple/ripple-theme' as mdc-ripple-theme; -@use '@material/animation/animation' as mdc-animation; -@use '@material/theme/theme' as mdc-theme; -@use '@material/feature-targeting/feature-targeting' as mdc-feature-targeting; -@use '@material/button/button-base' as mdc-button-base; -@use '@material/button/button-shared-theme' as mdc-button-shared-theme; -@use '@material/button/button-outlined' as mdc-button-outlined; -@use '@material/button/button-outlined-theme' as mdc-button-outlined-theme; -@use '@material/checkbox/checkbox' as mdc-checkbox; -@use '../../theme'; -@use './variables'; - -@mixin core-styles() { - .forge-chip { - @include base; - - &.forge-chip--disabled { - @include theme.property(border-color, border-color); - @include mdc-theme.property(color, text-disabled-on-light); - cursor: default; - pointer-events: none; - - .forge-chip__delete-button { - opacity: 0.54; - } - } - - &__leading { - &--hidden { - display: none; - } - } - - &__content { - flex-grow: 1; - text-align: center; - - &:focus { - outline: none; - } - } - - ::slotted([slot='leading']) { - @include leading; - } - - ::slotted(forge-avatar[slot]) { - --forge-avatar-size: 24px; - } - - ::slotted([slot='trailing']) { - @include trailing; - } - - &__checkmark { - @include checkmark; - } - - &__delete-button { - @include delete-button; - - border-radius: 50%; - - &:focus, - &:hover { - @include mdc-theme.property(color, text-primary-on-background); - } - - &:active { - opacity: 0.75; - } - - &__touch-target { - position: absolute; - top: -6px; - left: -6px; - width: calc(100% + 12px); - height: calc(100% + 12px); - } - } - - &--filter, - &--input, - &--choice { - &.forge-chip--selected { - &:not(.forge-chip--disabled) { - @include selected; - } - - &.forge-chip--disabled { - @include selected-disabled; - } - - ::slotted([slot='leading']) { - @include mdc-theme.property(color, on-primary); - } - - ::slotted([slot='trailing']) { - @include mdc-theme.property(color, on-primary); - } - - .forge-chip__delete-button { - @include mdc-theme.property(color, on-primary); - } - - &.forge-chip--filter { - .forge-chip__checkmark { - @include checkmark-selected; - } - } - } - } - - &--field { - .forge-chip__delete-button { - @include mdc-theme.property(color, primary); - - &:focus, - &:hover { - @include mdc-theme.property(color, text-primary-on-background); - } - } - - &.forge-chip--invalid:not(.forge-chip--disabled) { - .forge-chip__delete-button { - @include mdc-theme.property(color, error); - - &:focus, - &:hover { - @include mdc-theme.property(color, text-primary-on-background); - } - } - } - - &.forge-chip--disabled { - .forge-chip__delete-button { - @include mdc-theme.property(color, text-secondary-on-background); - } - } - } - - &--dense { - @include dense; - - &.forge-chip--field { - @include theme.css-custom-property(min-height, --forge-chip-min-height, variables.$dense-field-min-height); - padding: 0; - margin: 0; - display: flex; - flex-direction: row; - - ::slotted([slot='leading']) { - margin: 0 0 0 2px; - font-size: 18px !important; - } - - .forge-chip__content { - font-size: 12px; - padding: 0 8px; - } - - ::slotted([slot='trailing']) { - font-size: 18px !important; - } - - .forge-chip__delete-button { - margin: 0 2px 0 0; - font-size: 18px; - } - } - } - - &--invalid:not(.forge-chip--disabled) { - @include invalid; - - .forge-chip__delete-button { - @include mdc-theme.property(color, error); - } - - &.forge-chip--selected { - @include invalid-selected; - - .forge-chip__delete-button { - @include mdc-theme.property(color, on-primary); - } - } - } - } -} - -@mixin host() { - display: inline-block; -} - -@mixin base() { - @include mdc-ripple.surface(); - @include mdc-ripple.radius-bounded(); - @include mdc-ripple-theme.states(primary, false); - @include mdc-button-base.base(mdc-feature-targeting.all()); - @include mdc-button-shared-theme.shape-radius(small); - @include mdc-button-shared-theme.container-fill-color(transparent); - @include mdc-button-shared-theme.ink-color(primary); - @include mdc-button-shared-theme.density(0); - @include mdc-button-outlined.outlined(mdc-feature-targeting.all()); - @include mdc-button-outlined-theme.outline-width(mdc-button-outlined-theme.$outlined-border-width); - @include mdc-button-outlined-theme.outline-color(primary); - - @include mdc-chip-theme.shape-radius(50%); - @include theme.css-custom-property(width, --forge-chip-width, auto); - @include theme.css-custom-property(height, --forge-chip-height, auto); - @include theme.css-custom-property(min-height, --forge-chip-min-height, variables.$min-height); - - box-sizing: border-box; - transition: mdc-animation.enter(background-color, 150ms); - text-transform: none; - overflow: hidden; -} - -@mixin invalid() { - @include mdc-ripple-theme.states(error, false); - @include mdc-button-outlined-theme.outline-color(error); - @include mdc-theme.property(color, error); -} - -@mixin invalid-selected() { - @include mdc-theme.property(background-color, error); -} - -@mixin selected() { - @include mdc-ripple-theme.states(on-primary, false); - @include mdc-theme.property(background-color, primary); - @include mdc-theme.property(color, text-primary-on-dark); -} - -@mixin selected-disabled() { - @include selected; - - opacity: 0.37; -} - -@mixin leading() { - margin-left: -6px; - margin-right: 4px; -} - -@mixin trailing() { - margin-left: 4px; - margin-right: -8px; -} - -@mixin delete-button() { - @include mdc-theme.property(color, text-secondary-on-background); - @include trailing; - - transition: color 150ms linear; - outline: none; - line-height: inherit; - position: relative; - - &:focus { - outline: currentColor solid 2px; - } -} - -@mixin checkmark() { - height: 24px; - - &-svg { - width: 0; - height: 24px; - transition: width 150ms mdc-animation.$standard-curve-timing-function; - } - - &-path { - @include mdc-theme.property(stroke, on-primary); - - transition: mdc-checkbox.transition-exit(stroke-dashoffset, 0, 150ms); - stroke-width: 2px; - stroke-dashoffset: mdc-checkbox.$mark-path-length_; - stroke-dasharray: mdc-checkbox.$mark-path-length_; - } -} - -@mixin checkmark-selected() { - @include leading; - - &-svg { - width: 24px; - } - - &-path { - transition: mdc-checkbox.transition-exit(stroke-dashoffset, 50ms, 150ms); - stroke-dashoffset: 0; - } -} - -@mixin dense() { - @include theme.css-custom-property(min-height, --forge-chip-min-height, variables.$dense-min-height); -} diff --git a/src/lib/chips/chip/_token-utils.scss b/src/lib/chips/chip/_token-utils.scss new file mode 100644 index 000000000..8f2659eff --- /dev/null +++ b/src/lib/chips/chip/_token-utils.scss @@ -0,0 +1,25 @@ +@use '../../core/styles/tokens/chips/chip/tokens'; +@use '../../core/styles/tokens/token-utils'; + +$_module: chip; +$_tokens: tokens.$tokens; + +@mixin provide-theme($theme) { + @include token-utils.provide-theme($_module, $_tokens, $theme); +} + +@function token($name, $type: token) { + @return token-utils.token($_module, $_tokens, $name, $type); +} + +@function declare($token) { + @return token-utils.declare($_module, $token); +} + +@mixin override($token, $token-or-value, $type: token) { + @include token-utils.override($_module, $_tokens, $token, $token-or-value, $type); +} + +@mixin tokens($includes: null, $excludes: null) { + @include token-utils.tokens($_module, $_tokens, $includes, $excludes); +} diff --git a/src/lib/chips/chip/_variables.scss b/src/lib/chips/chip/_variables.scss deleted file mode 100644 index e0dc9b8be..000000000 --- a/src/lib/chips/chip/_variables.scss +++ /dev/null @@ -1,3 +0,0 @@ -$min-height: 36px !default; -$dense-min-height: 28px !default; -$dense-field-min-height: 20px !default; diff --git a/src/lib/chips/chip/chip-adapter.ts b/src/lib/chips/chip/chip-adapter.ts index 977cad829..8dc183fc3 100644 --- a/src/lib/chips/chip/chip-adapter.ts +++ b/src/lib/chips/chip/chip-adapter.ts @@ -1,173 +1,164 @@ -import { addClass, elementFromHTML, getActiveElement, getShadowElement, removeClass, removeElement, toggleClass, walkUpUntil } from '@tylertech/forge-core'; +import { elementFromHTML, getShadowElement, walkUpUntil } from '@tylertech/forge-core'; import { BaseAdapter, IBaseAdapter } from '../../core/base/base-adapter'; -import { ICON_CONSTANTS } from '../../icon'; -import { ForgeRipple, ForgeRippleAdapter, ForgeRippleCapableSurface, ForgeRippleFoundation } from '../../ripple'; +import { FOCUS_INDICATOR_CONSTANTS, IFocusIndicatorComponent } from '../../focus-indicator'; +import { IIconButtonComponent } from '../../icon-button'; +import { IStateLayerComponent, STATE_LAYER_CONSTANTS } from '../../state-layer'; import { IChipSetComponent } from '../chip-set/chip-set'; import { CHIP_SET_CONSTANTS } from '../chip-set/chip-set-constants'; import { IChipComponent } from './chip'; -import checkmarkTemplate from './chip-checkmark.html'; import { CHIP_CONSTANTS, IChipState } from './chip-constants'; +import checkmarkTemplate from './chip-checkmark.html'; +import { replaceElement } from '../../core/utils/utils'; + export interface IChipAdapter extends IBaseAdapter { - addRootListener(type: string, listener: (evt: Event) => void): void; - removeRootListener(type: string, listener: (evt: Event) => void): void; - addButtonListener(type: string, listener: (evt: Event) => void): void; - removeButtonListener(type: string, listener: (evt: Event) => void): void; - initializeRipple(): ForgeRipple; - clearTypeClass(): void; - addRootClass(className: string): void; - removeRootClass(name: string): void; - setCheckmarkVisibility(isVisible: boolean): void; - setSelected(value: boolean): void; - setDisabled(value: boolean): void; - setDense(value: boolean): void; - setDeleteButtonVisibility(isVisible: boolean, listener: (evt: KeyboardEvent) => void): void; - setLeadingSlotVisibility(isVisible: boolean): void; + readonly removeButtonElement: IIconButtonComponent | undefined; + clickTrigger(): void; + addRootListener(type: string, listener: EventListener): void; + removeRootListener(type: string, listener: EventListener): void; + setAnchor(value: boolean): void; + setAnchorProperty(name: T, value: HTMLAnchorElement[T]): void; + setCheckmarkVisibility(value: boolean): void; + setDeleteButtonVisibility(value: boolean): void; + setStartSlotVisibility(value: boolean): void; + toggleFieldVariant(value: boolean): void; getChipSetState(): IChipState | null; - setFocus(): void; - tryFocusDelete(): void; - setEmulatedFocus(value: boolean): void; - tryMoveFocusPrevious(): void; - tryMoveFocusNext(): void; + setDisabled(value: boolean): void; + focusTrigger(options?: FocusOptions): void; + tryFocusRemoveButton(): void; + clickRemoveButton(): void; } -export class ChipAdapter extends BaseAdapter implements IChipAdapter, ForgeRippleCapableSurface { +export class ChipAdapter extends BaseAdapter implements IChipAdapter { private _rootElement: HTMLElement; - private _rippleInstance: ForgeRipple; - private _buttonElement: HTMLButtonElement; - private _deleteButton: HTMLElement; - private _deleteButtonTouchTarget: HTMLDivElement; - private _leadingSlotElement: HTMLSlotElement; + private _triggerElement: HTMLButtonElement | HTMLAnchorElement; + private _removeButtonElement: IIconButtonComponent | undefined; + private _startSlotElement: HTMLSlotElement; private _checkmarkElement: HTMLElement; + private _focusIndicatorElement: IFocusIndicatorComponent; + private _stateLayerElement: IStateLayerComponent; - constructor(component: IChipComponent) { - super(component); + constructor(protected _component: IChipComponent) { + super(_component); this._rootElement = getShadowElement(this._component, CHIP_CONSTANTS.selectors.ROOT); - this._buttonElement = getShadowElement(this._component, CHIP_CONSTANTS.selectors.BUTTON) as HTMLButtonElement; - this._leadingSlotElement = getShadowElement(this._component, 'slot[name=leading]') as HTMLSlotElement; - this._rootElement.addEventListener('click', evt => { - if (evt.target === this._deleteButton) { - return; - } - this._buttonElement.focus(); - }, { capture: true }); + this._triggerElement = getShadowElement(this._component, CHIP_CONSTANTS.selectors.TRIGGER) as HTMLButtonElement; + this._startSlotElement = getShadowElement(this._component, 'slot[name=start]') as HTMLSlotElement; + this._focusIndicatorElement = getShadowElement(this._component, FOCUS_INDICATOR_CONSTANTS.elementName) as IFocusIndicatorComponent; + this._stateLayerElement = getShadowElement(this._component, STATE_LAYER_CONSTANTS.elementName) as IStateLayerComponent; } - // ForgeRippleCapableSurface - public get root(): Element { - return this._rootElement; - } - public get unbounded(): boolean | undefined { - return false; + public get removeButtonElement(): IIconButtonComponent | undefined { + return this._removeButtonElement; } - public get disabled(): boolean | undefined { - return this._buttonElement?.disabled; + + public clickTrigger(): void { + this._triggerElement.click(); } - public addRootListener(type: string, listener: (evt: Event) => void): void { + public addRootListener(type: string, listener: EventListener): void { this._rootElement.addEventListener(type, listener); } - public removeRootListener(type: string, listener: (evt: Event) => void): void { + public removeRootListener(type: string, listener: EventListener): void { this._rootElement.removeEventListener(type, listener); } - public addButtonListener(type: string, listener: (evt: Event) => void): void { - this._buttonElement.addEventListener(type, listener); - } - - public removeButtonListener(type: string, listener: (evt: Event) => void): void { - this._buttonElement.removeEventListener(type, listener); - } - - public initializeRipple(): ForgeRipple { - const adapter: ForgeRippleAdapter = { - ...ForgeRipple.createAdapter(this), - registerInteractionHandler: (evtType, handler) => { - if (['focus', 'blur', 'keydown'].includes(evtType)) { - this._buttonElement.addEventListener(evtType, handler, { passive: true }); - } else { - this._rootElement.addEventListener(evtType, handler, { passive: true }); - } - }, - deregisterInteractionHandler: (evtType, handler) => { - if (['focus', 'blur', 'keydown'].includes(evtType)) { - this._buttonElement.removeEventListener(evtType, handler, { passive: true } as AddEventListenerOptions); - } else { - this._rootElement.removeEventListener(evtType, handler, { passive: true } as AddEventListenerOptions); - } - }, - isSurfaceActive: () => this._buttonElement.matches(':active'), - isUnbounded: () => Boolean(this.unbounded), - isSurfaceDisabled: () => this._buttonElement.disabled, - addClass: className => addClass(className, this._rootElement), - removeClass: className => removeClass(className, this._rootElement), - updateCssVariable: (varName, value) => this._rootElement.style.setProperty(varName, value) - }; - this._rippleInstance = new ForgeRipple(this._rootElement, new ForgeRippleFoundation(adapter)); - return this._rippleInstance; - } + public setAnchor(value: boolean): void { + if (value) { + if (this._triggerElement.localName === 'button') { + const anchor = this._createAnchorElement(); + this._triggerElement = replaceElement(this._triggerElement, anchor); + } + } else { + if (this._triggerElement.localName === 'a') { + const button = this._createButtonElement(); + this._triggerElement = replaceElement(this._triggerElement, button); + } + } - public clearTypeClass(): void { - removeClass([CHIP_CONSTANTS.classes.ACTION, CHIP_CONSTANTS.classes.CHOICE, CHIP_CONSTANTS.classes.FILTER, CHIP_CONSTANTS.classes.INPUT], this._buttonElement); + if (this._stateLayerElement.targetElement !== this._triggerElement) { + this._stateLayerElement.targetElement = this._triggerElement; + } + if (this._focusIndicatorElement.targetElement !== this._triggerElement) { + this._focusIndicatorElement.targetElement = this._triggerElement; + } } - public addRootClass(className: string): void { - this._rootElement.classList.add(className); - } + public setAnchorProperty(name: T, value: HTMLAnchorElement[T]): void { + if (!(this._triggerElement.localName === 'a')) { + return; + } - public removeRootClass(name: string): void { - this._rootElement.classList.remove(name); + if (this._triggerElement instanceof HTMLAnchorElement) { + this._triggerElement[name] = value; + } } - public setCheckmarkVisibility(isVisible: boolean): void { - if (isVisible) { + public setCheckmarkVisibility(value: boolean): void { + if (value) { if (!this._checkmarkElement) { this._checkmarkElement = elementFromHTML(checkmarkTemplate) as HTMLElement; } this._rootElement.insertBefore(this._checkmarkElement, this._rootElement.firstChild); } else if (this._checkmarkElement && this._checkmarkElement.isConnected) { - removeElement(this._checkmarkElement); + this._checkmarkElement.remove(); } } - public setSelected(value: boolean): void { - toggleClass(this._rootElement, value, CHIP_CONSTANTS.classes.SELECTED); - } - public setDisabled(value: boolean): void { - this._buttonElement.disabled = value; - this._buttonElement.tabIndex = value ? -1 : 0; - toggleClass(this._rootElement, value, CHIP_CONSTANTS.classes.DISABLED); - } + if (this._triggerElement instanceof HTMLAnchorElement) { + if (!this._focusIndicatorElement.isConnected) { + this._rootElement.append(this._focusIndicatorElement); + } + if (!this._stateLayerElement.isConnected) { + this._rootElement.append(this._stateLayerElement); + } + return; + } + + if (this._removeButtonElement) { + this._removeButtonElement.disabled = value; + } - public setDense(value: boolean): void { - toggleClass(this._rootElement, value, CHIP_CONSTANTS.classes.DENSE); + this._triggerElement.disabled = value; + this._triggerElement.tabIndex = value ? -1 : 0; + + if (value) { + this._focusIndicatorElement.remove(); + this._stateLayerElement.remove(); + } else { + this._rootElement.append(this._focusIndicatorElement, this._stateLayerElement); + } } - public setDeleteButtonVisibility(isVisible: boolean, listener: (evt: KeyboardEvent) => void): void { - if (isVisible) { - if (!this._deleteButton) { - this._deleteButton = this._createDeleteButton(listener); + public toggleFieldVariant(value: boolean): void { + if (value) { + if (!this._stateLayerElement.isConnected) { + this._rootElement.append(this._stateLayerElement); } - if (!this._deleteButtonTouchTarget) { - this._deleteButtonTouchTarget = this._createDeleteButtonTouchTarget(listener); + if (this._focusIndicatorElement.targetElement !== this._triggerElement) { + this._focusIndicatorElement.targetElement = this._triggerElement; } - this._deleteButton.appendChild(this._deleteButtonTouchTarget); - this._rootElement.appendChild(this._deleteButton); } else { - if (this._deleteButtonTouchTarget) { - removeElement(this._deleteButtonTouchTarget); - } - if (this._deleteButton) { - removeElement(this._deleteButton); + this._stateLayerElement.remove(); + } + + } + + public setDeleteButtonVisibility(value: boolean): void { + if (value) { + if (!this._removeButtonElement) { + this._removeButtonElement = this._createRemoveButton(); } + this._rootElement.appendChild(this._removeButtonElement); + } else { + this._removeButtonElement?.remove(); } } - public setLeadingSlotVisibility(isVisible: boolean): void { - toggleClass(this._leadingSlotElement, !isVisible, CHIP_CONSTANTS.classes.LEADING_HIDDEN); + public setStartSlotVisibility(value: boolean): void { + this._startSlotElement.style.display = value ? '' : 'none'; } public getChipSetState(): IChipState | null { @@ -183,62 +174,66 @@ export class ChipAdapter extends BaseAdapter implements IChipAda return state; } - private _createDeleteButton(listener: (evt: KeyboardEvent) => void): HTMLElement { - const el = document.createElement(ICON_CONSTANTS.elementName); - el.id = 'remove-button'; - el.name = 'cancel'; - el.tabIndex = -1; - el.setAttribute('aria-hidden', 'false'); - el.setAttribute('aria-label', `Remove ${this._component.innerText}`); - el.setAttribute('role', 'button'); - el.classList.add(CHIP_CONSTANTS.classes.DELETE_BUTTON); - el.addEventListener('keydown', listener); - return el; + public focusTrigger(options?: FocusOptions): void { + this._triggerElement.focus({ preventScroll: true, ...options }); } - private _createDeleteButtonTouchTarget(listener: (evt: KeyboardEvent) => void): HTMLDivElement { - const el = document.createElement('div'); - el.classList.add(CHIP_CONSTANTS.classes.DELETE_BUTTON_TOUCH_TARGET); - el.addEventListener('keydown', listener); - el.addEventListener('click', listener); - return el; + public tryFocusRemoveButton(): void { + if (this._removeButtonElement) { + this._removeButtonElement.focus({ preventScroll: true, focusVisible: true }); + } else { + this.focusTrigger(); + } } - public setFocus(): void { - this._buttonElement.focus(); + public clickRemoveButton(): void { + this._removeButtonElement?.click(); } - public tryFocusDelete(): void { - if (this._deleteButton) { - this._deleteButton.focus(); - } else { - this.setFocus(); - } - } + private _createRemoveButton(): IIconButtonComponent { + const buttonEl = document.createElement('forge-icon-button'); + buttonEl.density = 'small'; + buttonEl.id = 'remove-button'; + buttonEl.classList.add('remove'); + buttonEl.tabIndex = -1; + buttonEl.setAttribute('aria-label', `Remove ${this._component.innerText}`); + buttonEl.setAttribute('part', 'remove-button'); + + const iconEl = document.createElement('forge-icon'); + iconEl.name = 'close'; + buttonEl.appendChild(iconEl); - public setEmulatedFocus(value: boolean): void { - this._buttonElement.classList.toggle('mdc-ripple-upgraded--background-focused', value); + return buttonEl; } - public tryMoveFocusNext(): void { - const activeElement = getActiveElement(this._component.ownerDocument); - if (activeElement === this._buttonElement) { - if (this._deleteButton) { - this._deleteButton.focus(); - } else { - this.emitHostEvent(CHIP_CONSTANTS.events.FOCUS_NEXT); - } - } else if (activeElement === this._deleteButton) { - this.emitHostEvent(CHIP_CONSTANTS.events.FOCUS_NEXT); + private _createAnchorElement(): HTMLAnchorElement { + const anchor = document.createElement('a'); + anchor.id = 'trigger'; + anchor.setAttribute('part', 'trigger'); + anchor.classList.add('trigger'); + + if (this._component.href) { + anchor.href = this._component.href; + } + if (this._component.target) { + anchor.target = this._component.target; + } + if (this._component.download) { + anchor.download = this._component.download; } + if (this._component.rel) { + anchor.rel = this._component.rel; + } + + return anchor; } - public tryMoveFocusPrevious(): void { - const activeElement = getActiveElement(this._component.ownerDocument); - if (activeElement === this._deleteButton) { - this._buttonElement.focus(); - } else if (activeElement === this._buttonElement) { - this.emitHostEvent(CHIP_CONSTANTS.events.FOCUS_PREVIOUS); - } + private _createButtonElement(): HTMLButtonElement { + const button = document.createElement('button'); + button.type = 'button'; + button.id = 'trigger'; + button.setAttribute('part', 'trigger'); + button.classList.add('trigger'); + return button; } } diff --git a/src/lib/chips/chip/chip-checkmark.html b/src/lib/chips/chip/chip-checkmark.html index 15bc0ccc0..3225dc620 100644 --- a/src/lib/chips/chip/chip-checkmark.html +++ b/src/lib/chips/chip/chip-checkmark.html @@ -1,5 +1,5 @@ -