From df62dd7bbc619ba542bfa8b6728c87c56c9e9d2f Mon Sep 17 00:00:00 2001 From: MikeMatusz Date: Tue, 29 Aug 2023 08:46:48 -0400 Subject: [PATCH 01/38] feat(popup): add popup ref to close event, emit from host --- src/lib/popup/popup-adapter.ts | 7 ++++++- src/lib/popup/popup-foundation.ts | 8 +++++--- src/lib/popup/popup.ts | 6 +++++- src/test/spec/popup/popup.spec.ts | 12 ++++++++++++ 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/lib/popup/popup-adapter.ts b/src/lib/popup/popup-adapter.ts index f078f3d82..9d15ed6f6 100644 --- a/src/lib/popup/popup-adapter.ts +++ b/src/lib/popup/popup-adapter.ts @@ -1,6 +1,6 @@ import { addClass, closestElement, emitEvent, getShadowElement, IPositionElementConfig, notChildEventListener, positionElementAsync, removeClass, removeElement, deepQuerySelectorAll, getActiveElement } from '@tylertech/forge-core'; import { BaseAdapter, IBaseAdapter } from '../core/base/base-adapter'; -import { IPopupComponent } from './popup'; +import { IPopupComponent, IPopupCloseEventData } from './popup'; import { IPopupPositionEventData, POPUP_CONSTANTS, PopupPlacement } from './popup-constants'; export interface IPopupAdapter extends IBaseAdapter { @@ -17,6 +17,7 @@ export interface IPopupAdapter extends IBaseAdapter { removeEventListener(type: string, listener: (evt: Event) => void): void; setBlurListener(listener: () => void): () => void; trySetInitialFocus(): void; + getCloseEventData(): IPopupCloseEventData; } export class PopupAdapter extends BaseAdapter implements IPopupAdapter { @@ -130,6 +131,10 @@ export class PopupAdapter extends BaseAdapter implements IPopup return false; } + public getCloseEventData(): IPopupCloseEventData { + return { popup: this._component }; + } + public addClass(classes: string | string[]): void { addClass(classes, this._rootElement); } diff --git a/src/lib/popup/popup-foundation.ts b/src/lib/popup/popup-foundation.ts index 95877b276..3515f7fe7 100644 --- a/src/lib/popup/popup-foundation.ts +++ b/src/lib/popup/popup-foundation.ts @@ -1,6 +1,6 @@ import { ICustomElementFoundation, isElement } from '@tylertech/forge-core'; import { IPopupAdapter } from './popup-adapter'; -import { IPopupPosition, PopupAnimationType, PopupPlacement, PopupStateCallback, POPUP_CONSTANTS as constants, POPUP_CONSTANTS } from './popup-constants'; +import { IPopupPosition, PopupAnimationType, PopupPlacement, PopupStateCallback, POPUP_CONSTANTS } from './popup-constants'; export interface IPopupFoundation extends ICustomElementFoundation { targetElement: HTMLElement; @@ -66,7 +66,7 @@ export class PopupFoundation implements IPopupFoundation { } this._adapter.manageWindowEvents(true); - this._adapter.dispatchEvent(constants.events.OPEN); + this._adapter.dispatchEvent(POPUP_CONSTANTS.events.OPEN); } private _closePopup(): void { @@ -85,7 +85,9 @@ export class PopupFoundation implements IPopupFoundation { private _destroyPopup(): void { this._adapter.manageWindowEvents(false); this._adapter.removePopup(this._manageFocus); - this._adapter.dispatchEvent(constants.events.CLOSE); + const eventData = this._adapter.getCloseEventData(); + this._adapter.dispatchEvent(POPUP_CONSTANTS.events.CLOSE, eventData); + this._adapter.emitHostEvent(POPUP_CONSTANTS.events.CLOSE, eventData); this._adapter.removeAttribute(POPUP_CONSTANTS.attributes.OPEN); } diff --git a/src/lib/popup/popup.ts b/src/lib/popup/popup.ts index 23b0c0ea3..7cedc578e 100644 --- a/src/lib/popup/popup.ts +++ b/src/lib/popup/popup.ts @@ -23,6 +23,10 @@ export interface IPopupComponent extends IBaseComponent { closeCallback: PopupStateCallback; } +export interface IPopupCloseEventData { + popup: IPopupComponent; +} + declare global { interface HTMLElementTagNameMap { 'forge-popup': IPopupComponent; @@ -30,7 +34,7 @@ declare global { interface HTMLElementEventMap { 'forge-popup-open': CustomEvent; - 'forge-popup-close': CustomEvent; + 'forge-popup-close': CustomEvent; 'forge-popup-position': CustomEvent; 'forge-popup-blur': CustomEvent; } diff --git a/src/test/spec/popup/popup.spec.ts b/src/test/spec/popup/popup.spec.ts index fcd0a8648..0c0e53c23 100644 --- a/src/test/spec/popup/popup.spec.ts +++ b/src/test/spec/popup/popup.spec.ts @@ -128,6 +128,18 @@ describe('Popup component', function(this: ITestContext) { targetElement.removeEventListener(POPUP_CONSTANTS.events.CLOSE, callback); }); + it('should also emit close event from the host element', function(this: ITestContext) { + this.context = setupTestContext(); + const { component, targetElement } = this.context; + component.targetElement = targetElement; + const callback = jasmine.createSpy('callback'); + component.addEventListener(POPUP_CONSTANTS.events.CLOSE, callback); + component.open = true; + component.open = false; + expect(callback).toHaveBeenCalledTimes(1); + component.removeEventListener(POPUP_CONSTANTS.events.CLOSE, callback); + }); + it('should accept and return focus on open and close', async function(this: ITestContext) { this.context = setupTestContext(); const { component, targetElement } = this.context; From 131ee8ae854dd0adf7e65158317c1218188dc07c Mon Sep 17 00:00:00 2001 From: MikeMatusz Date: Tue, 29 Aug 2023 09:28:17 -0400 Subject: [PATCH 02/38] chore: update docs --- src/stories/src/components/popup/popup.mdx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/stories/src/components/popup/popup.mdx b/src/stories/src/components/popup/popup.mdx index 09ad2e075..5bf98732b 100644 --- a/src/stories/src/components/popup/popup.mdx +++ b/src/stories/src/components/popup/popup.mdx @@ -185,14 +185,15 @@ Manually tells the popup to execute its positioning logic. | Name | Description | :-----------------------| :----------------- | `forge-popup-position` | Emits when the popup is re-positioned. +| `forge-popup-close` | Emits from both the host and target elements when the popup is closed. The following events are emitted from the targetElement, **NOT** the `` element. | Name | Description | :-----------------------| :----------------- | `forge-popup-open` | Emits when the popup is opened. -| `forge-popup-close` | Emits when the popup is closed. -| `forge-popup-blur` | Emits when the popup is closed. +| `forge-popup-close` | Emits from both the host and target elements when the popup is closed. +| `forge-popup-blur` | Emits when the popup is blurred. From 16c658f3e243b779fed4173b91cb7df6d8f77ca9 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 29 Aug 2023 16:32:35 +0000 Subject: [PATCH 03/38] Update CHANGELOG.md [skip ci] --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5938259e5..350c89a14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# v2.17.0 (Tue Aug 29 2023) + +#### 🚀 Enhancement + +- feat(popup): add popup ref to close event, emit from host [#365](https://github.com/tyler-technologies-oss/forge/pull/365) ([@MikeMatusz](https://github.com/MikeMatusz)) + +#### Authors: 1 + +- Mike Matuszak ([@MikeMatusz](https://github.com/MikeMatusz)) + +--- + # v2.16.6 (Mon Aug 21 2023) #### 🐛 Bug Fix From 1c45a948fe561d80edc69f7c3afe76e19e30c4b3 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 29 Aug 2023 16:32:36 +0000 Subject: [PATCH 04/38] Bump version to: 2.17.0 [skip ci] --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3a4d073cc..aff4c610a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tylertech/forge", - "version": "2.16.6", + "version": "2.17.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@tylertech/forge", - "version": "2.16.6", + "version": "2.17.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 877f68b06..bbf5267e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tylertech/forge", - "version": "2.16.6", + "version": "2.17.0", "description": "Tyler Forge™ Web Components library", "license": "Apache-2.0", "author": "Tyler Technologies, Inc.", From 31dbf288cd05a9c7a98ee5fc6221b1c72fb8660d Mon Sep 17 00:00:00 2001 From: Sam Richardson Date: Wed, 30 Aug 2023 15:29:25 -0400 Subject: [PATCH 05/38] feat(keyboard-shortcut): add activate callback (#367) --- .../keyboard-shortcut-constants.ts | 2 ++ .../keyboard-shortcut-foundation.ts | 13 ++++++++- .../keyboard-shortcut/keyboard-shortcut.ts | 7 ++++- .../keyboard-shortcut/keyboard-shortcut.mdx | 19 ++++++++++++ .../keyboard-shortcut.spec.ts | 29 ++++++++++++++++++- 5 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/lib/keyboard-shortcut/keyboard-shortcut-constants.ts b/src/lib/keyboard-shortcut/keyboard-shortcut-constants.ts index bea0f1a02..2a827cf8b 100644 --- a/src/lib/keyboard-shortcut/keyboard-shortcut-constants.ts +++ b/src/lib/keyboard-shortcut/keyboard-shortcut-constants.ts @@ -44,6 +44,8 @@ export const textInputTypes = [ 'week' ]; +export type KeyboardShortcutActivateCallback = (event: KeyboardEvent) => void; + export interface IKeyCombination { key: string; modifier?: string; diff --git a/src/lib/keyboard-shortcut/keyboard-shortcut-foundation.ts b/src/lib/keyboard-shortcut/keyboard-shortcut-foundation.ts index 0e53d4d15..9e1311e52 100644 --- a/src/lib/keyboard-shortcut/keyboard-shortcut-foundation.ts +++ b/src/lib/keyboard-shortcut/keyboard-shortcut-foundation.ts @@ -1,7 +1,7 @@ import { ICustomElementFoundation } from '@tylertech/forge-core'; import { IKeyboardShortcutAdapter } from './keyboard-shortcut-adapter'; -import { IKeyCombination, KEYBOARD_SHORTCUT_CONSTANTS } from './keyboard-shortcut-constants'; +import { IKeyCombination, KEYBOARD_SHORTCUT_CONSTANTS, KeyboardShortcutActivateCallback } from './keyboard-shortcut-constants'; import { elementAcceptsTextInput, matchKeyCombination, parseKeyCombinations } from './keyboard-shortcut-utils'; export interface IKeyboardShortcutFoundation extends ICustomElementFoundation { @@ -13,6 +13,7 @@ export interface IKeyboardShortcutFoundation extends ICustomElementFoundation { capture: boolean; useCode: boolean; disabled: boolean; + activateCallback: KeyboardShortcutActivateCallback | null | undefined; } export class KeyboardShortcutFoundation implements IKeyboardShortcutFoundation { @@ -24,6 +25,7 @@ export class KeyboardShortcutFoundation implements IKeyboardShortcutFoundation { private _capture = false; private _useCode = false; private _disabled = false; + private _activateCallback: KeyboardShortcutActivateCallback | null | undefined; private _keyCombinations: IKeyCombination[]; private _keyDownListener: (evt: KeyboardEvent) => void; @@ -82,6 +84,7 @@ export class KeyboardShortcutFoundation implements IKeyboardShortcutFoundation { evt.preventDefault(); } this._adapter.emitHostEvent(KEYBOARD_SHORTCUT_CONSTANTS.events.ACTIVATE, evt); + this._activateCallback?.call(null, evt); } } @@ -194,4 +197,12 @@ export class KeyboardShortcutFoundation implements IKeyboardShortcutFoundation { } } } + + /** Gets/sets the activation callback. */ + public get activateCallback(): KeyboardShortcutActivateCallback | null | undefined { + return this._activateCallback; + } + public set activateCallback(value: KeyboardShortcutActivateCallback | null | undefined) { + this._activateCallback = value; + } } diff --git a/src/lib/keyboard-shortcut/keyboard-shortcut.ts b/src/lib/keyboard-shortcut/keyboard-shortcut.ts index 17255523b..21798b2ee 100644 --- a/src/lib/keyboard-shortcut/keyboard-shortcut.ts +++ b/src/lib/keyboard-shortcut/keyboard-shortcut.ts @@ -2,7 +2,7 @@ import { coerceBoolean, CustomElement, FoundationProperty } from '@tylertech/for import { KeyboardShortcutAdapter } from './keyboard-shortcut-adapter'; import { KeyboardShortcutFoundation } from './keyboard-shortcut-foundation'; -import { KEYBOARD_SHORTCUT_CONSTANTS } from './keyboard-shortcut-constants'; +import { KEYBOARD_SHORTCUT_CONSTANTS, KeyboardShortcutActivateCallback } from './keyboard-shortcut-constants'; import { BaseComponent, IBaseComponent } from '../core/base/base-component'; export interface IKeyboardShortcutComponent extends IBaseComponent { @@ -15,6 +15,7 @@ export interface IKeyboardShortcutComponent extends IBaseComponent { capture: boolean; useCode: boolean; disabled: boolean; + activateCallback: KeyboardShortcutActivateCallback | null | undefined; } declare global { @@ -133,4 +134,8 @@ export class KeyboardShortcutComponent extends BaseComponent implements IKeyboar /** Gets/sets whether the callback will be called. */ @FoundationProperty() public declare disabled: boolean; + + /** Gets/sets whether the activation callback. */ + @FoundationProperty() + public declare activateCallback: KeyboardShortcutActivateCallback | null | undefined; } diff --git a/src/stories/src/components/keyboard-shortcut/keyboard-shortcut.mdx b/src/stories/src/components/keyboard-shortcut/keyboard-shortcut.mdx index a3db9e8ae..b19e90f62 100644 --- a/src/stories/src/components/keyboard-shortcut/keyboard-shortcut.mdx +++ b/src/stories/src/components/keyboard-shortcut/keyboard-shortcut.mdx @@ -127,6 +127,13 @@ target element. + + +Sets a function to call when the keyboard shortcut is activated. This provides an alternative to +binding to the `forge-keyboard-shortcut-activate` event in scenerios where that may be cumbersome. + + + --- @@ -161,4 +168,16 @@ pressed if it includes a submit button. > Even though the keyboard shortcut component exists in the DOM, the element itself doesn't affect > accessibility or layout due to having its `display` style property set to `none`. + + + + +## Types + +### KeyboardShortcutActivateCallback + +```ts +type KeyboardShortcutActivateCallback = (event: KeyboardEvent) => void; +``` + \ No newline at end of file diff --git a/src/test/spec/keyboard-shortcut/keyboard-shortcut.spec.ts b/src/test/spec/keyboard-shortcut/keyboard-shortcut.spec.ts index aa8a3ecc2..5f5a96833 100644 --- a/src/test/spec/keyboard-shortcut/keyboard-shortcut.spec.ts +++ b/src/test/spec/keyboard-shortcut/keyboard-shortcut.spec.ts @@ -1,6 +1,6 @@ import { removeElement } from '@tylertech/forge-core'; import { tick } from '@tylertech/forge-testing'; -import { IKeyboardShortcutComponent, KEYBOARD_SHORTCUT_CONSTANTS, defineKeyboardShortcutComponent } from '@tylertech/forge/keyboard-shortcut'; +import { IKeyboardShortcutComponent, KEYBOARD_SHORTCUT_CONSTANTS, KeyboardShortcutActivateCallback, defineKeyboardShortcutComponent } from '@tylertech/forge/keyboard-shortcut'; interface ITestContext { context: IKeyboardShortcutTestContext @@ -40,6 +40,19 @@ describe('KeyboardShortcutComponent', function(this: ITestContext) { expect(spy).toHaveBeenCalled(); }); + it('should invoke the callback function on a matching target keydown event', function(this: ITestContext) { + const key = 'a'; + const callback = jasmine.createSpy(); + this.context = setupTestContext(); + this.context.component.key = key; + this.context.component.activateCallback = callback; + + this.context.attach(); + + this.context.dispatchKeydownEvent({key}); + expect(callback).toHaveBeenCalled(); + }); + describe('attributes', function(this: ITestContext) { it('should set key when a key attribute is provided', function(this: ITestContext) { @@ -526,6 +539,20 @@ describe('KeyboardShortcutComponent', function(this: ITestContext) { expect(spy).not.toHaveBeenCalled(); }); + it('should not invoke the callback when disabled is set to true', function(this: ITestContext) { + const key = 'a'; + const callback = jasmine.createSpy(); + this.context = setupTestContext(); + this.context.component.key = key; + this.context.component.activateCallback = callback; + this.context.component.disabled = true; + + this.context.attach(); + + this.context.dispatchKeydownEvent({key}); + expect(callback).not.toHaveBeenCalled(); + }); + it('should connect target element when disabled is set to false', function(this: ITestContext) { const key = 'a'; this.context = setupTestContext(); From de58e1377f731192dce712027a0eeb58e079bab4 Mon Sep 17 00:00:00 2001 From: "Nichols, Kieran" Date: Tue, 5 Sep 2023 09:02:50 -0400 Subject: [PATCH 06/38] chore(deps): update Forge CLI to latest [skip ci] --- package-lock.json | 308 +++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 155 insertions(+), 155 deletions(-) diff --git a/package-lock.json b/package-lock.json index aff4c610a..200a96e38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ "@commitlint/cli": "^17.6.3", "@commitlint/config-conventional": "^17.6.3", "@tylertech-eslint/eslint-plugin": "^1.0.9", - "@tylertech/forge-cli": "^2.3.1", + "@tylertech/forge-cli": "^2.4.0", "@tylertech/forge-testing": "^2.0.0", "@tylertech/stylelint-rules": "^4.3.4", "@types/jasmine": "^3.10.3", @@ -73,6 +73,15 @@ "vite-tsconfig-paths": "^4.2.0" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -1797,9 +1806,9 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", - "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", @@ -2912,9 +2921,9 @@ } }, "node_modules/@tylertech/forge-build-tools": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tylertech/forge-build-tools/-/forge-build-tools-2.0.0.tgz", - "integrity": "sha512-YXdapWNVvB7KTzxv+C9RFqpHbkqqVhLlSeZ8ZDbPx8+Dqi6SQf4kuF/l/JD9Tn4nW0/575riQFXqFRRvvxyNCA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@tylertech/forge-build-tools/-/forge-build-tools-2.1.0.tgz", + "integrity": "sha512-7FvxR5l2M4Tpz+iNb5Rpzz2xQuJAyfH8viqxsbDqJJxZVp9MFrf/yPR1l0j1jLBAGZpEbUvn34r6WBeBroppBQ==", "dev": true, "dependencies": { "autoprefixer": "10.4.4", @@ -3074,13 +3083,13 @@ } }, "node_modules/@tylertech/forge-cli": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@tylertech/forge-cli/-/forge-cli-2.3.1.tgz", - "integrity": "sha512-+q7dV9G6r8U+2MLH2RUU76k8ajC22YifrkqiT0LCO4CXh3W81/L5JKTZ8J+OxuTRZl3T8BTMYNuk/KVi8ejKYw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@tylertech/forge-cli/-/forge-cli-2.4.0.tgz", + "integrity": "sha512-/KHSyynm0uu9jUuAFLQZDXillikgYIW7aV7gCyOzbPFpD0vlcdy9YAS7KvxF8h63qsYf5kwNZkJ/OOrdpnecKA==", "dev": true, "dependencies": { "@custom-elements-manifest/analyzer": "^0.6.4", - "@tylertech/forge-build-tools": "^2.0.0", + "@tylertech/forge-build-tools": "^2.1.0", "autoprefixer": "^10.4.4", "browser-sync": "^2.27.9", "canonical-path": "^1.0.0", @@ -3199,9 +3208,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz", - "integrity": "sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ==", + "version": "8.44.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", + "integrity": "sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==", "dev": true, "dependencies": { "@types/estree": "*", @@ -6787,9 +6796,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.0.tgz", - "integrity": "sha512-+DCows0XNwLDcUhbFJPdlQEVnT2zXlCv7hPxemTz86/O+B/hCQ+mb7ydkPKiflpVraqLPCAfu7lDy+hBXueojw==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -8619,9 +8628,9 @@ } }, "node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -9651,9 +9660,9 @@ } }, "node_modules/is-core-module": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", - "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -13277,17 +13286,17 @@ } }, "node_modules/optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" }, "engines": { "node": ">= 0.8.0" @@ -14540,12 +14549,12 @@ } }, "node_modules/purify-css/node_modules/resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", "dev": true, "dependencies": { - "is-core-module": "^2.11.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -14557,9 +14566,9 @@ } }, "node_modules/purify-css/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -15572,9 +15581,9 @@ "dev": true }, "node_modules/sass": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz", - "integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==", + "version": "1.66.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.66.1.tgz", + "integrity": "sha512-50c+zTsZOJVgFfTgwwEzkjA3/QACgdNsKueWPyAR0mRINIvLAStVQBbPg14iuqEQ74NPDbXzJARJ/O4SI1zftA==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -15623,15 +15632,15 @@ } }, "node_modules/sass/node_modules/immutable": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", - "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", + "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", "dev": true }, "node_modules/schema-utils": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", - "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.8", @@ -17292,13 +17301,13 @@ } }, "node_modules/terser": { - "version": "5.17.4", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.4.tgz", - "integrity": "sha512-jcEKZw6UPrgugz/0Tuk/PVyLAPfMBJf5clnGueo45wTweoV8yh7Q7PEkhkJ5uuUbC7zAxEcG3tqNr1bstkQ8nw==", + "version": "5.19.4", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.4.tgz", + "integrity": "sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g==", "dev": true, "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -17310,9 +17319,9 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.8.tgz", - "integrity": "sha512-WiHL3ElchZMsK27P8uIUh4604IgJyAW47LVXGbEoB21DbQcZ+OuMpGjVYnEUaqcWM6dO8uS2qUbA7LSCWqvsbg==", + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", @@ -17382,9 +17391,9 @@ } }, "node_modules/terser/node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -18417,9 +18426,9 @@ "dev": true }, "node_modules/webpack": { - "version": "5.82.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.82.1.tgz", - "integrity": "sha512-C6uiGQJ+Gt4RyHXXYt+v9f+SN1v83x68URwgxNQ98cvH8kxiuywWGP4XeNZ1paOzZ63aY3cTciCEQJNFUljlLw==", + "version": "5.88.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", + "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -18428,10 +18437,10 @@ "@webassemblyjs/wasm-edit": "^1.11.5", "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", + "acorn-import-assertions": "^1.9.0", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.14.0", + "enhanced-resolve": "^5.15.0", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -18441,7 +18450,7 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.2", + "schema-utils": "^3.2.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.7", "watchpack": "^2.4.0", @@ -18464,9 +18473,9 @@ } }, "node_modules/webpack-merge": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", - "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", + "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", "dev": true, "dependencies": { "clone-deep": "^4.0.1", @@ -18486,9 +18495,9 @@ } }, "node_modules/webpack/node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -18498,9 +18507,9 @@ } }, "node_modules/webpack/node_modules/es-module-lexer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", - "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", + "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==", "dev": true }, "node_modules/whatwg-encoding": { @@ -18686,15 +18695,6 @@ "node": ">= 8" } }, - "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -18953,6 +18953,12 @@ } }, "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, "@ampproject/remapping": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", @@ -20203,9 +20209,9 @@ "dev": true }, "@jridgewell/source-map": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", - "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", "dev": true, "requires": { "@jridgewell/gen-mapping": "^0.3.0", @@ -21208,9 +21214,9 @@ } }, "@tylertech/forge-build-tools": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tylertech/forge-build-tools/-/forge-build-tools-2.0.0.tgz", - "integrity": "sha512-YXdapWNVvB7KTzxv+C9RFqpHbkqqVhLlSeZ8ZDbPx8+Dqi6SQf4kuF/l/JD9Tn4nW0/575riQFXqFRRvvxyNCA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@tylertech/forge-build-tools/-/forge-build-tools-2.1.0.tgz", + "integrity": "sha512-7FvxR5l2M4Tpz+iNb5Rpzz2xQuJAyfH8viqxsbDqJJxZVp9MFrf/yPR1l0j1jLBAGZpEbUvn34r6WBeBroppBQ==", "dev": true, "requires": { "autoprefixer": "10.4.4", @@ -21329,13 +21335,13 @@ } }, "@tylertech/forge-cli": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@tylertech/forge-cli/-/forge-cli-2.3.1.tgz", - "integrity": "sha512-+q7dV9G6r8U+2MLH2RUU76k8ajC22YifrkqiT0LCO4CXh3W81/L5JKTZ8J+OxuTRZl3T8BTMYNuk/KVi8ejKYw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@tylertech/forge-cli/-/forge-cli-2.4.0.tgz", + "integrity": "sha512-/KHSyynm0uu9jUuAFLQZDXillikgYIW7aV7gCyOzbPFpD0vlcdy9YAS7KvxF8h63qsYf5kwNZkJ/OOrdpnecKA==", "dev": true, "requires": { "@custom-elements-manifest/analyzer": "^0.6.4", - "@tylertech/forge-build-tools": "^2.0.0", + "@tylertech/forge-build-tools": "^2.1.0", "autoprefixer": "^10.4.4", "browser-sync": "^2.27.9", "canonical-path": "^1.0.0", @@ -21439,9 +21445,9 @@ } }, "@types/eslint": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz", - "integrity": "sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ==", + "version": "8.44.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", + "integrity": "sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==", "dev": true, "requires": { "@types/estree": "*", @@ -24194,9 +24200,9 @@ "dev": true }, "enhanced-resolve": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.14.0.tgz", - "integrity": "sha512-+DCows0XNwLDcUhbFJPdlQEVnT2zXlCv7hPxemTz86/O+B/hCQ+mb7ydkPKiflpVraqLPCAfu7lDy+hBXueojw==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", "dev": true, "requires": { "graceful-fs": "^4.2.4", @@ -25502,9 +25508,9 @@ } }, "globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -26263,9 +26269,9 @@ } }, "is-core-module": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", - "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", + "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", "dev": true, "requires": { "has": "^1.0.3" @@ -29017,17 +29023,17 @@ } }, "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "type-check": "^0.4.0" } }, "ora": { @@ -29950,20 +29956,20 @@ } }, "resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "version": "1.22.4", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", + "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", "dev": true, "requires": { - "is-core-module": "^2.11.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "string-width": { @@ -30748,9 +30754,9 @@ "dev": true }, "sass": { - "version": "1.62.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz", - "integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==", + "version": "1.66.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.66.1.tgz", + "integrity": "sha512-50c+zTsZOJVgFfTgwwEzkjA3/QACgdNsKueWPyAR0mRINIvLAStVQBbPg14iuqEQ74NPDbXzJARJ/O4SI1zftA==", "dev": true, "requires": { "chokidar": ">=3.0.0 <4.0.0", @@ -30759,9 +30765,9 @@ }, "dependencies": { "immutable": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", - "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", + "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", "dev": true } } @@ -30777,9 +30783,9 @@ } }, "schema-utils": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", - "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "requires": { "@types/json-schema": "^7.0.8", @@ -32080,29 +32086,29 @@ } }, "terser": { - "version": "5.17.4", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.4.tgz", - "integrity": "sha512-jcEKZw6UPrgugz/0Tuk/PVyLAPfMBJf5clnGueo45wTweoV8yh7Q7PEkhkJ5uuUbC7zAxEcG3tqNr1bstkQ8nw==", + "version": "5.19.4", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.4.tgz", + "integrity": "sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g==", "dev": true, "requires": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "dependencies": { "acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true } } }, "terser-webpack-plugin": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.8.tgz", - "integrity": "sha512-WiHL3ElchZMsK27P8uIUh4604IgJyAW47LVXGbEoB21DbQcZ+OuMpGjVYnEUaqcWM6dO8uS2qUbA7LSCWqvsbg==", + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", "dev": true, "requires": { "@jridgewell/trace-mapping": "^0.3.17", @@ -32864,9 +32870,9 @@ "dev": true }, "webpack": { - "version": "5.82.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.82.1.tgz", - "integrity": "sha512-C6uiGQJ+Gt4RyHXXYt+v9f+SN1v83x68URwgxNQ98cvH8kxiuywWGP4XeNZ1paOzZ63aY3cTciCEQJNFUljlLw==", + "version": "5.88.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", + "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", @@ -32875,10 +32881,10 @@ "@webassemblyjs/wasm-edit": "^1.11.5", "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", + "acorn-import-assertions": "^1.9.0", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.14.0", + "enhanced-resolve": "^5.15.0", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -32888,7 +32894,7 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.2", + "schema-utils": "^3.2.0", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.7", "watchpack": "^2.4.0", @@ -32896,23 +32902,23 @@ }, "dependencies": { "acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true }, "es-module-lexer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", - "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", + "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==", "dev": true } } }, "webpack-merge": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", - "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", + "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", "dev": true, "requires": { "clone-deep": "^4.0.1", @@ -33064,12 +33070,6 @@ "fswin": "^3.18.918" } }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", diff --git a/package.json b/package.json index bbf5267e2..9b2d52aea 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "@commitlint/cli": "^17.6.3", "@commitlint/config-conventional": "^17.6.3", "@tylertech-eslint/eslint-plugin": "^1.0.9", - "@tylertech/forge-cli": "^2.3.1", + "@tylertech/forge-cli": "^2.4.0", "@tylertech/forge-testing": "^2.0.0", "@tylertech/stylelint-rules": "^4.3.4", "@types/jasmine": "^3.10.3", From 32917cae12f3ebd29b3e488f015e8b85f628f122 Mon Sep 17 00:00:00 2001 From: "Nichols, Kieran" Date: Fri, 8 Sep 2023 14:00:13 -0400 Subject: [PATCH 07/38] chore: update concurrency key [skip ci] --- .github/workflows/build-release.yml | 2 +- src/stories/src/components/select/select.mdx | 2 +- src/stories/src/components/text-field/text-field.mdx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index 9a708c99d..34d572264 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -20,7 +20,7 @@ on: - 'src/**/*' workflow_dispatch: -concurrency: build-release-${{ github.ref }} +concurrency: build-release jobs: ## Gather configuration required by other jobs diff --git a/src/stories/src/components/select/select.mdx b/src/stories/src/components/select/select.mdx index 44682b471..4fe082d3e 100644 --- a/src/stories/src/components/select/select.mdx +++ b/src/stories/src/components/select/select.mdx @@ -158,7 +158,7 @@ Gets/sets the invalid state. Setting this to `true` will style the component usi -Gets/sets whether this component is marked as required or not. Setting to `true` will display an asterisk in the label. +Gets/sets whether this component is marked as required or not. Setting to `true` will display an asterisk next to the label. diff --git a/src/stories/src/components/text-field/text-field.mdx b/src/stories/src/components/text-field/text-field.mdx index c9318880e..9dffc83f0 100644 --- a/src/stories/src/components/text-field/text-field.mdx +++ b/src/stories/src/components/text-field/text-field.mdx @@ -72,7 +72,7 @@ The `default` variant is intended to be used in desktop applications. - Controls the presence of an asterisk after the label to signify that the field is required. + Controls the presence of an asterisk next to the label to signify that the field is required. > If you are using this for form validation, be sure to also update the property on the embedded `` element. From bcb272d1e13c662fafa10d4d4c93bf130270d01b Mon Sep 17 00:00:00 2001 From: Mike Matuszak Date: Wed, 27 Sep 2023 15:14:58 -0400 Subject: [PATCH 08/38] fix(chip-field): don't propagate click if disabled (#384) --- src/lib/chip-field/chip-field-adapter.ts | 2 +- src/test/spec/chip-field/chip-field.spec.ts | 35 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/lib/chip-field/chip-field-adapter.ts b/src/lib/chip-field/chip-field-adapter.ts index 9a0efee5c..952c74883 100644 --- a/src/lib/chip-field/chip-field-adapter.ts +++ b/src/lib/chip-field/chip-field-adapter.ts @@ -74,7 +74,7 @@ export class ChipFieldAdapter extends FieldAdapter implements IChipFieldAdapter public tryPropagateClick(target: EventTarget | null): void { // We only propagate the click to the input if it originated from our internal input container - if (target instanceof HTMLElement && target.matches(CHIP_FIELD_CONSTANTS.selectors.INPUT_CONTAINER)) { + if (!this._inputElement.disabled && target instanceof HTMLElement && target.matches(CHIP_FIELD_CONSTANTS.selectors.INPUT_CONTAINER)) { this._inputElement?.dispatchEvent(new MouseEvent('click')); } } diff --git a/src/test/spec/chip-field/chip-field.spec.ts b/src/test/spec/chip-field/chip-field.spec.ts index 91bbb3312..6456ed26b 100644 --- a/src/test/spec/chip-field/chip-field.spec.ts +++ b/src/test/spec/chip-field/chip-field.spec.ts @@ -1121,6 +1121,37 @@ describe('ChipFieldComponent', function(this: ITestContext) { }); + describe('Click events', function (this: ITestContext) { + it('should emit a click event from the input if the container is clicked', function(this: ITestContext) { + this.context = setupTestContext(); + const clickSpy = jasmine.createSpy('inputClick'); + const inputEl = getNativeInput(this.context.component); + inputEl.addEventListener('click', clickSpy); + + const containerEl = getInputContainerElement(this.context.component); + containerEl.dispatchEvent(new MouseEvent('mousedown')); + + expect(clickSpy).toHaveBeenCalledTimes(1); + + inputEl.removeEventListener('click', clickSpy); + }); + + it('should not emit a click event from a disabled input if the container is clicked', function(this: ITestContext) { + this.context = setupTestContext(); + const clickSpy = jasmine.createSpy('inputClick'); + const inputEl = getNativeInput(this.context.component); + inputEl.addEventListener('click', clickSpy); + inputEl.disabled = true; + + const containerEl = getInputContainerElement(this.context.component); + containerEl.dispatchEvent(new MouseEvent('mousedown')); + + expect(clickSpy).not.toHaveBeenCalled(); + + inputEl.removeEventListener('click', clickSpy); + }); + }); + describe('Member navigation', function(this: ITestContext) { it('should focus the last element when the left arrow key is pressed while the input is focused', function(this: ITestContext) { this.context = setupTestContext(); @@ -1218,6 +1249,10 @@ describe('ChipFieldComponent', function(this: ITestContext) { return component.querySelector(CHIP_FIELD_CONSTANTS.selectors.HELPER_TEXT) as HTMLElement; } + function getInputContainerElement(component: IChipFieldComponent) { + return getShadowElement(component, CHIP_FIELD_CONSTANTS.selectors.INPUT_CONTAINER) as HTMLDivElement; + } + function dispatchKeydownEvent(ele: HTMLElement, key: string) { const keyboardEvent = new KeyboardEvent('keydown', { bubbles: true, From 95c883d283349c155ecb708cffd0bfe7713c8ad1 Mon Sep 17 00:00:00 2001 From: Mike Matuszak Date: Wed, 27 Sep 2023 15:16:43 -0400 Subject: [PATCH 09/38] fix(date-picker, time-picker, date-range-picker): select mask on focus if shown (#385) --- .../base/base-date-picker-foundation.ts | 4 +-- .../date-range-picker-foundation.ts | 4 +-- src/lib/time-picker/time-picker-foundation.ts | 8 +++--- src/test/spec/date-picker/date-picker.spec.ts | 13 ++++++++++ .../date-range-picker.spec.ts | 26 +++++++++++++++++++ src/test/spec/time-picker/time-picker.spec.ts | 14 ++++++++++ 6 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/lib/date-picker/base/base-date-picker-foundation.ts b/src/lib/date-picker/base/base-date-picker-foundation.ts index 5091f3498..562bda407 100644 --- a/src/lib/date-picker/base/base-date-picker-foundation.ts +++ b/src/lib/date-picker/base/base-date-picker-foundation.ts @@ -167,11 +167,11 @@ export abstract class BaseDatePickerFoundation Date: Wed, 27 Sep 2023 15:17:58 -0400 Subject: [PATCH 10/38] fix(chip-field): don't wrap leading/trailing icons (#386) --- src/lib/chip-field/_base.scss | 1 + src/lib/chip-field/_selector.scss | 4 ++-- src/lib/chip-field/chip-field.html | 8 ++++++-- src/stories/src/components/chip-field/chip-field.mdx | 2 ++ 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/lib/chip-field/_base.scss b/src/lib/chip-field/_base.scss index 7928de576..8ccb750c4 100644 --- a/src/lib/chip-field/_base.scss +++ b/src/lib/chip-field/_base.scss @@ -220,6 +220,7 @@ @mixin leading-core { @include _grid-leading; align-self: flex-start; + display: flex; } // Trailing diff --git a/src/lib/chip-field/_selector.scss b/src/lib/chip-field/_selector.scss index 3ef98896d..e4f207fe8 100644 --- a/src/lib/chip-field/_selector.scss +++ b/src/lib/chip-field/_selector.scss @@ -160,7 +160,7 @@ @mixin leading { // Core - ::slotted([slot='leading']) { + &--leading .forge-field__leading-container { @include base.leading-core; } // Margin @@ -171,7 +171,7 @@ @mixin trailing { // Core - ::slotted([slot='trailing']) { + &--trailing .forge-field__trailing-container { @include base.trailing-core; } // Margin diff --git a/src/lib/chip-field/chip-field.html b/src/lib/chip-field/chip-field.html index c3a84bed6..61aff6945 100644 --- a/src/lib/chip-field/chip-field.html +++ b/src/lib/chip-field/chip-field.html @@ -1,7 +1,9 @@ diff --git a/src/lib/list/list-item/list-item.scss b/src/lib/list/list-item/list-item.scss index fac7228b9..2ed4ded4f 100644 --- a/src/lib/list/list-item/list-item.scss +++ b/src/lib/list/list-item/list-item.scss @@ -1,25 +1,205 @@ -@use '@material/typography' as mdc-typography; -@use '../../theme'; -@use './mixins'; -@use '../../ripple/forge-ripple'; -@use '../../typography/mixins' as typography-mixins; -@use './variables'; - -@include mixins.core-styles; +@use './configuration'; +@use './core'; +@use '../../focus-indicator'; +@use '../../state-layer'; +@use '../../core/styles/shape'; +// Host styles :host { - @include mixins.host; + @include configuration.host; + @include core.container; } :host([hidden]) { display: none; } -// This is here until Firefox supports :host-context()... -:host([forge-drawer-context=true]) { - @include mixins.drawer-context-styles; +// Non-interactive +:host([non-interactive]) { + .forge-list-item { + @include core.non-interactive; + } +} + +// Disabled +:host([disabled]) { + .forge-list-item { + @include core.disabled; + } +} + +// Two-line +:host([two-line]) { + .forge-list-item { + @include core.two-line; + } +} + +// Three-line +:host([three-line]) { + .forge-list-item { + @include core.three-line; + } } +// Dense :host([dense]) { - @include mdc-typography.typography(body2); + .forge-list-item { + @include core.dense; + } + + .text-container { + @include core.dense-text; + } +} + +// Dense indented +:host([dense][indented]) { + @include core.dense-indented; +} + +// Dense two-line +:host([dense][two-line]) { + .forge-list-item { + @include core.dense-two-line; + } +} + +// Dense three-line +:host([dense][three-line]) { + .forge-list-item { + @include core.dense-three-line; + } +} + +// Dense leading/trailing +:host([selected]) { + .forge-list-item { + @include core.selected; + + ::slotted([slot=leading]), + ::slotted([slot=trailing]) { + @include core.leading-trailing-selected; + } + + ::slotted([slot=leading]) { + @include core.leading-selected; + } + + ::slotted([slot=trailing]) { + @include core.trailing-selected; + } + } + + .text-container { + @include core.text-container-selected; + } + + forge-state-layer { + @include state-layer.provide-theme(( + color: var(--_selected-color) + )); + } +} + +// Indented +:host([indented]) { + @include core.indented; +} + +// Base +.forge-list-item { + @include configuration.configuration; + @include core.base; +} + +// Anchor base +a.forge-list-item { + @include core.anchor-link; +} + +// Text container base +.text-container { + @include core.text-container; +} + +// Supporting text slotted elements +slot[name=secondary-text], +slot[name=subtitle], +slot[name=tertiary-text], +slot[name=tertiary-title] { + &::slotted(*) { + @include core.supporting-text; + } + + :host([selected]) &::slotted(*) { + @include core.supporting-text-selected; + } +} + +// Avatar slotted elements +::slotted([slot=avatar]) { + @include core.avatar; +} + +// Leading/trailing slotted elements +::slotted([slot=leading]), +::slotted([slot=trailing]) { + @include core.leading-trailing-base; +} + +// Leading slotted elements +::slotted([slot=leading]) { + @include core.leading; + + :host([selected]) & { + @include core.leading-selected; + } + + :host([dense]) & { + @include core.leading-dense; + } +} + +// Trailing slotted elements +::slotted([slot=trailing]) { + @include core.trailing; + + :host([selected]) & { + @include core.trailing-selected; + } + + :host([dense]) & { + @include core.trailing-dense; + } +} + +// Wrap styles +:host([wrap]) { + .forge-list-item { + @include core.wrap; + } + + .text-container { + @include core.text-container-wrap; + } + + slot[name=secondary-text], + slot[name=subtitle], + slot[name=tertiary-text], + slot[name=tertiary-title] { + &::slotted(*) { + @include core.supporting-text-wrap; + } + } +} + +forge-focus-indicator { + @include focus-indicator.provide-theme(( + shape: shape.variable(medium) + )); +} + +forge-state-layer { + border-radius: inherit; } diff --git a/src/lib/list/list-item/list-item.ts b/src/lib/list/list-item/list-item.ts index b85ffb7df..1e7b4612c 100644 --- a/src/lib/list/list-item/list-item.ts +++ b/src/lib/list/list-item/list-item.ts @@ -1,30 +1,29 @@ -import { CustomElement, attachShadowTemplate, requireParent, elementParents, coerceBoolean, FoundationProperty } from '@tylertech/forge-core'; +import { CustomElement, attachShadowTemplate, ICustomElement, FoundationProperty, coerceBoolean } from '@tylertech/forge-core'; import { ListItemAdapter } from './list-item-adapter'; import { ListItemFoundation } from './list-item-foundation'; import { IListItemSelectEventData, LIST_ITEM_CONSTANTS } from './list-item-constants'; -import { LIST_CONSTANTS } from '../list/list-constants'; -import { IListComponent } from '../list'; -import { BaseComponent, IBaseComponent } from '../../core/base/base-component'; +import { StateLayerComponent } from '../../state-layer'; +import { FocusIndicatorComponent } from '../../focus-indicator'; -import defaultTemplate from './list-item.html'; +import template from './list-item.html'; import styles from './list-item.scss'; -export interface IListItemComponent extends IBaseComponent { - static: boolean; - twoLine: boolean; - threeLine: boolean; - active: boolean; - selected: boolean; - value: any; +export interface IListItemComponent extends ICustomElement { href: string; target: string; - ripple: boolean; + /** @deprecated Use nonInteractive instead. */ + static: boolean; + nonInteractive: boolean; disabled: boolean; + selected: boolean; + active: boolean; + value: T; dense: boolean; propagateClick: boolean; indented: boolean; + twoLine: boolean; + threeLine: boolean; wrap: boolean; - focus(): void; } declare global { @@ -38,30 +37,121 @@ declare global { } /** - * The custom element class behind the `` element. - * * @tag forge-list-item + * + * @summary List items are individual rows of content inside of a list. + * + * @property {string} href - The href of the list item. This forces the list item to render as an anchor element. + * @property {string} target - The target of the list item when an `href` is set. Defaults to `'_blank'`. + * @property {boolean} nonInteractive - If true, the list item will not be interactive. + * @property {boolean} static - If true, the list item will not be interactive. Deprecated use `nonInteractive` instead. + * @property {boolean} disabled - Disables the list item. + * @property {boolean} selected - Applies the selected state to the list item. + * @property {boolean} active - Applies the active state to the list item by emulating its focused state. + * @property {unknown} value - The unique value of the list item. + * @property {boolean} dense - Applies the dense state to the list item. + * @property {boolean} propagateClick - If true, the list item will not propagate click events to itself and therefore cannot receive focus. + * @property {boolean} indented - Applies the indented state by adding margin to the start of the list item. + * @property {boolean} twoLine - Sets the list item height to support at least two lines of text. + * @property {boolean} threeLine - Sets the list item height to support at least three lines of text. + * @property {boolean} wrap - Sets the list item to wrap its text content. + * + * @attribute {string} href - The href of the list item. This forces the list item to render as an anchor element. + * @attribute {string} target - The target of the list item when an `href` is set. Defaults to `'_blank'`. + * @attribute {boolean} non-interactive - If true, the list item will not be interactive. + * @attribute {boolean} static - If true, the list item will not be interactive. Deprecated use `non-interactive` instead. + * @attribute {boolean} disabled - Disables the list item. + * @attribute {boolean} selected - Applies the selected state to the list item. + * @attribute {boolean} active - Applies the active state to the list item by emulating its focused state. + * @attribute {unknown} value - The unique value of the list item. + * @attribute {boolean} dense - Applies the dense state to the list item. + * @attribute {boolean} propagate-click - If applied, the list item will not propagate click events to itself and therefore cannot receive focus. + * @attribute {boolean} indented - Applies the indented state by adding margin to the start of the list item. + * @attribute {boolean} two-line - Sets the list item height to support at least two lines of text. + * @attribute {boolean} three-line - Sets the list item height to support at least three lines of text. + * @attribute {boolean} wrap - Sets the list item to wrap its text content. + * + * @event {CustomEvent} forge-list-item-select - Fires when the list item is selected. + * + * @slot - The primary text. + * @slot primary-text - The primary text. A named alias for the default slot. + * @slot secondary-text - The secondary text. + * @slot tertiary-text - The tertiary text. + * @slot title - The title element. An alias for the primary-text slot for backwards compatibility. + * @slot subtitle - The subtitle element. An alias for the secondary-text slot for backwards compatibility. + * @slot tertiary-title - The tertiary title element. An alias for the tertiary-text slot for backwards compatibility. + * @slot leading - The leading content. + * @slot trailing - The trailing element. + * @slot avatar - The avatar content. + * + * @csspart root - The root container element. + * @csspart text-container - The container for the text content. + * @csspart focus-indicator__indicator - The forwarded focus indicator's internal indicator element. + * @csspart state-layer__surface - The forwarded state layer's internal surface element. + * + * @cssprop --forge-list-item-background-color - The background color. + * @cssprop --forge-list-item-shape - The shape of the list item. + * @cssprop --forge-list-item-padding - The padding inside of the container element. + * @cssprop --forge-list-item-margin - The margin around the host element. + * @cssprop --forge-list-item-height - The height of the container. + * @cssprop --forge-list-item-dense-height - The height when in the dense state. + * @cssprop --forge-list-item-indent - The margin inline state when in the indented state. + * @cssprop --forge-list-item-supporting-text-color - The text color of the supporting text. + * @cssprop --forge-list-item-supporting-line-height - The line height of the supporting text. + * @cssprop --forge-list-item-selected-color - The color when in the selected state. + * @cssprop --forge-list-item-opacity - The opacity of the background color when in the disabled state. + * @cssprop --forge-list-item-selected-leading-color - The color of the leading content when in the selected state. + * @cssprop --forge-list-item-selected-trailing-color - The color of the trailing content when in the selected state. + * @cssprop --forge-list-item-selected-supporting-text-color - The color of the supporting text when in the selected state. + * @cssprop --forge-list-item-disabled-opacity - The opacity of the element when in the disabled state. + * @cssprop --forge-list-item-disabled-cursor - The cursor when in the disabled state. + * @cssprop --forge-list-item-one-line-height - The line height when in the one/single line state. + * @cssprop --forge-list-item-two-line-height - The line height when in the two line state. + * @cssprop --forge-list-item-three-line-height - The line height when in the three line state. + * @cssprop --forge-list-item-dense-one-line-height - The line height when in the dense one/single line state. + * @cssprop --forge-list-item-dense-two-line-height - The line height when in the dense two line state. + * @cssprop --forge-list-item-dense-three-line-height - The line height when in the dense three line state. + * @cssprop --forge-list-item-dense-font-size - The font size when in the dense state. + * @cssprop --forge-list-item-dense-indent - The margin inline state when in the dense indented state. + * @cssprop --forge-list-item-dense-leading-margin-end - The margin end of the leading content when in the dense state. + * @cssprop --forge-list-item-dense-trailing-margin-start - The margin start of the trailing content when in the dense state. + * @cssprop --forge-list-item-leading-margin-start - The margin start of the leading content. + * @cssprop --forge-list-item-leading-margin-end - The margin end of the leading content. + * @cssprop --forge-list-item-leading-selected-color - The color of the leading content when in the selected state. + * @cssprop --forge-list-item-trailing-margin-start - The margin start of the trailing content. + * @cssprop --forge-list-item-trailing-margin-end - The margin end of the trailing content. + * @cssprop --forge-list-item-trailing-selected-color - The color of the trailing content when in the selected state. + * @cssprop --forge-list-item-avatar-background-color - The background color of the avatar container. + * @cssprop --forge-list-item-avatar-color - The foreground color of the avatar container. + * @cssprop --forge-list-item-avatar-shape - The shape of the avatar container. + * @cssprop --forge-list-item-avatar-margin-start - The margin start of the avatar container. + * @cssprop --forge-list-item-avatar-margin-end - The margin end of the avatar container. + * @cssprop --forge-list-item-avatar-size - The height & width of the avatar container. */ @CustomElement({ - name: LIST_ITEM_CONSTANTS.elementName + name: LIST_ITEM_CONSTANTS.elementName, + dependencies: [ + StateLayerComponent, + FocusIndicatorComponent + ] }) -export class ListItemComponent extends BaseComponent implements IListItemComponent { +export class ListItemComponent extends HTMLElement implements IListItemComponent { public static get observedAttributes(): string[] { return [ - LIST_ITEM_CONSTANTS.attributes.STATIC, - LIST_ITEM_CONSTANTS.attributes.TWO_LINE, - LIST_ITEM_CONSTANTS.attributes.THREE_LINE, - LIST_ITEM_CONSTANTS.attributes.ACTIVE, - LIST_ITEM_CONSTANTS.attributes.SELECTED, - LIST_ITEM_CONSTANTS.attributes.VALUE, - LIST_ITEM_CONSTANTS.attributes.HREF, - LIST_ITEM_CONSTANTS.attributes.TARGET, - LIST_ITEM_CONSTANTS.attributes.RIPPLE, - LIST_ITEM_CONSTANTS.attributes.DISABLED, - LIST_ITEM_CONSTANTS.attributes.DENSE, - LIST_ITEM_CONSTANTS.attributes.PROPAGATE_CLICK, - LIST_ITEM_CONSTANTS.attributes.INDENTED, - LIST_ITEM_CONSTANTS.attributes.WRAP + LIST_ITEM_CONSTANTS.observedAttributes.HREF, + LIST_ITEM_CONSTANTS.observedAttributes.TARGET, + LIST_ITEM_CONSTANTS.observedAttributes.STATIC, + LIST_ITEM_CONSTANTS.observedAttributes.NON_INTERACTIVE, + LIST_ITEM_CONSTANTS.observedAttributes.DISABLED, + LIST_ITEM_CONSTANTS.observedAttributes.SELECTED, + LIST_ITEM_CONSTANTS.observedAttributes.ACTIVE, + LIST_ITEM_CONSTANTS.observedAttributes.VALUE, + LIST_ITEM_CONSTANTS.observedAttributes.DENSE, + LIST_ITEM_CONSTANTS.observedAttributes.PROPAGATE_CLICK, + LIST_ITEM_CONSTANTS.observedAttributes.INDENTED, + LIST_ITEM_CONSTANTS.observedAttributes.TWO_LINE, + LIST_ITEM_CONSTANTS.observedAttributes.THREE_LINE, + LIST_ITEM_CONSTANTS.observedAttributes.WRAP ]; } @@ -69,23 +159,11 @@ export class ListItemComponent extends BaseComponent implements IListItemCompone constructor() { super(); - attachShadowTemplate(this, defaultTemplate, styles); + attachShadowTemplate(this, template, styles); this._foundation = new ListItemFoundation(new ListItemAdapter(this)); } public connectedCallback(): void { - // To simulate the :host-context() selector for Firefox until they implement it, we need to determine if the - // list item is within a drawer for auto-styling the list item when included within a drawer. Check to see if - // any of the parents of this element are a drawer. - if (!this.hasAttribute(LIST_ITEM_CONSTANTS.attributes.DRAWER_CONTEXT) && elementParents(this).some(el => ['forge-drawer', 'forge-modal-drawer', 'forge-mini-drawer'].includes(el.tagName.toLowerCase()))) { - this.setAttribute(LIST_ITEM_CONSTANTS.attributes.DRAWER_CONTEXT, 'true'); - } - - const list = requireParent(this, LIST_CONSTANTS.elementName); - if (list) { - this._inheritParentListProps(list); - } - this._foundation.initialize(); } @@ -95,124 +173,93 @@ export class ListItemComponent extends BaseComponent implements IListItemCompone public attributeChangedCallback(name: string, oldValue: string, newValue: string): void { switch (name) { - case LIST_ITEM_CONSTANTS.attributes.STATIC: - this.static = coerceBoolean(newValue); + case LIST_ITEM_CONSTANTS.observedAttributes.HREF: + this.href = newValue; break; - case LIST_ITEM_CONSTANTS.attributes.TWO_LINE: - this.twoLine = coerceBoolean(newValue); + case LIST_ITEM_CONSTANTS.observedAttributes.TARGET: + this.target = newValue; break; - case LIST_ITEM_CONSTANTS.attributes.THREE_LINE: - this.threeLine = coerceBoolean(newValue); + case LIST_ITEM_CONSTANTS.observedAttributes.NON_INTERACTIVE: + case LIST_ITEM_CONSTANTS.observedAttributes.STATIC: + this.nonInteractive = coerceBoolean(newValue); break; - case LIST_ITEM_CONSTANTS.attributes.ACTIVE: - this.active = coerceBoolean(newValue); + case LIST_ITEM_CONSTANTS.observedAttributes.DISABLED: + this.disabled = coerceBoolean(newValue); break; - case LIST_ITEM_CONSTANTS.attributes.SELECTED: + case LIST_ITEM_CONSTANTS.observedAttributes.SELECTED: this.selected = coerceBoolean(newValue); break; - case LIST_ITEM_CONSTANTS.attributes.VALUE: - this.value = newValue; - break; - case LIST_ITEM_CONSTANTS.attributes.HREF: - this.href = newValue; - break; - case LIST_ITEM_CONSTANTS.attributes.TARGET: - this.target = newValue; - break; - case LIST_ITEM_CONSTANTS.attributes.RIPPLE: - this.ripple = coerceBoolean(newValue); + case LIST_ITEM_CONSTANTS.observedAttributes.ACTIVE: + this.active = coerceBoolean(newValue); break; - case LIST_ITEM_CONSTANTS.attributes.DISABLED: - this.disabled = coerceBoolean(newValue); + case LIST_ITEM_CONSTANTS.observedAttributes.VALUE: + this.value = newValue; break; - case LIST_ITEM_CONSTANTS.attributes.DENSE: + case LIST_ITEM_CONSTANTS.observedAttributes.DENSE: this.dense = coerceBoolean(newValue); break; - case LIST_ITEM_CONSTANTS.attributes.PROPAGATE_CLICK: + case LIST_ITEM_CONSTANTS.observedAttributes.PROPAGATE_CLICK: this.propagateClick = coerceBoolean(newValue); break; - case LIST_ITEM_CONSTANTS.attributes.INDENTED: + case LIST_ITEM_CONSTANTS.observedAttributes.INDENTED: this.indented = coerceBoolean(newValue); break; - case LIST_ITEM_CONSTANTS.attributes.WRAP: + case LIST_ITEM_CONSTANTS.observedAttributes.TWO_LINE: + this.twoLine = coerceBoolean(newValue); + break; + case LIST_ITEM_CONSTANTS.observedAttributes.THREE_LINE: + this.threeLine = coerceBoolean(newValue); + break; + case LIST_ITEM_CONSTANTS.observedAttributes.WRAP: this.wrap = coerceBoolean(newValue); break; } } - private _inheritParentListProps(list: IListComponent): void { - if (list.hasAttribute(LIST_CONSTANTS.attributes.STATIC)) { - this.static = true; - } - if (list.hasAttribute(LIST_CONSTANTS.attributes.DENSE)) { - this.dense = true; - } - if (list.getAttribute(LIST_CONSTANTS.attributes.PROPAGATE_CLICK) === 'false') { - this.propagateClick = false; - } - if (list.hasAttribute(LIST_CONSTANTS.attributes.INDENTED)) { - this.indented = true; - } + public override focus(): void { + this._foundation.setFocus(); } - /** Gets/sets whether the static state of this list item. */ @FoundationProperty() - public declare static: boolean; + public declare href: string; - /** Gets/sets whether the list item displays two lines of text. */ @FoundationProperty() - public declare twoLine: boolean; + public declare target: string; - /** Gets/sets whether the list item displays three lines of text. */ + /** @deprecated Use nonInteractive instead. */ @FoundationProperty() - public declare threeLine: boolean; + public declare static: boolean; - /** Gets/sets whether the list item is active or not. */ @FoundationProperty() - public declare active: boolean; + public declare nonInteractive: boolean; - /** Gets/sets whether the list item is selected or not. */ @FoundationProperty() - public declare selected: boolean; + public declare disabled: boolean; - /** Gets/sets list item value. */ @FoundationProperty() - public declare value: any; + public declare selected: boolean; - /** Gets/sets the href link that this list item will send the browser to when clicked. */ @FoundationProperty() - public declare href: string; + public declare active: boolean; - /** Gets/sets the href link target. Only pertains when `href` is also used. */ @FoundationProperty() - public declare target: string; + public declare value: unknown; - /** Gets/sets whether the list item has a ripple or not. */ @FoundationProperty() - public declare ripple: boolean; + public declare dense: boolean; - /** Gets/sets whether the list item is disabled or not. */ @FoundationProperty() - public declare disabled: boolean; + public declare propagateClick: boolean; - /** Gets/sets whether the list item is using dense styles or not. */ @FoundationProperty() - public declare dense: boolean; + public declare indented: boolean; - /** Gets/sets whether the list item allows mousedown events through to the underlying list item element. Default is true. */ @FoundationProperty() - public declare propagateClick: boolean; + public declare twoLine: boolean; - /** Gets/sets whether the list item is indented or not. Default is false. */ @FoundationProperty() - public declare indented: boolean; + public declare threeLine: boolean; - /** Gets/sets whether the list item content is wrapped or not. Default is true. */ @FoundationProperty() public declare wrap: boolean; - - /** Sets focus to this list item. */ - public override focus(): void { - this._foundation.setFocus(); - } } diff --git a/src/lib/list/list.test.ts b/src/lib/list/list.test.ts new file mode 100644 index 000000000..cf8a91ef2 --- /dev/null +++ b/src/lib/list/list.test.ts @@ -0,0 +1,546 @@ +import { expect } from '@esm-bundle/chai'; +import { nothing } from 'lit'; +import { elementUpdated, fixture, html } from '@open-wc/testing'; +import { getShadowElement } from '@tylertech/forge-core'; +import sinon from 'sinon'; +import { TestHarness } from '../../test/utils/test-harness'; +import { IFocusIndicatorComponent } from '../focus-indicator/focus-indicator'; +import { IStateLayerComponent } from '../state-layer/state-layer'; +import { IListItemComponent, LIST_ITEM_CONSTANTS } from './list-item'; +import './list/list'; +import { IListComponent } from './list/list'; +import { LIST_CONSTANTS } from './list/list-constants'; + +const DEFAULT_HREF = 'https://www.tylertech.com/'; + +describe('List', () => { + it('should be accessible', async () => { + const ctx = await createFixture(); + await expect(ctx.list).to.be.accessible(); + }); + + it('should have a default role of list', async () => { + const ctx = await createFixture(); + expect(ctx.list.getAttribute('role')).to.equal('list'); + expect(ctx.listItemsAttr('role', 'listitem')).to.true; + }); + + it('should set role to listbox', async () => { + const ctx = await createFixture({ role: 'listbox' }); + expect(ctx.list.getAttribute('role')).to.equal('listbox'); + expect(ctx.listItemsAttr('role', 'option')).to.true; + }); + + it('should set role to menu', async () => { + const ctx = await createFixture({ role: 'menu' }); + expect(ctx.list.getAttribute('role')).to.equal('menu'); + expect(ctx.listItemsAttr('role', 'menuitem')).to.true; + }); + + it('should update role dynamically', async () => { + const ctx = await createFixture(); + expect(ctx.listItemsAttr('role', 'listitem')).to.true; + + ctx.list.role = 'listbox'; + expect(ctx.listItemsAttr('role', 'option')).to.true; + + ctx.list.role = 'menu'; + expect(ctx.listItemsAttr('role', 'menuitem')).to.true; + + ctx.list.role = 'list'; + expect(ctx.listItemsAttr('role', 'listitem')).to.true; + }); + + it('should dispatch select event when clicked', async () => { + const ctx = await createFixture(); + const spy = sinon.spy(); + ctx.list.addEventListener('forge-list-item-select', spy); + ctx.listItems[1].click(); + expect(spy).to.have.been.calledOnceWith(sinon.match.has('detail', sinon.match.has('value', '2'))); + }); + + it('should dispatch select event when enter key is pressed', async () => { + const ctx = await createFixture(); + const spy = sinon.spy(); + ctx.list.addEventListener('forge-list-item-select', spy); + ctx.listItems[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + expect(spy).to.have.been.calledOnceWith(sinon.match.has('detail', sinon.match.has('value', '2'))); + }); + + it('should dispatch select event when space key is pressed', async () => { + const ctx = await createFixture(); + const spy = sinon.spy(); + ctx.list.addEventListener('forge-list-item-select', spy); + ctx.listItems[1].dispatchEvent(new KeyboardEvent('keydown', { key: ' ' })); + expect(spy).to.have.been.calledOnceWith(sinon.match.has('detail', sinon.match.has('value', '2'))); + }); + + it('should not dispatch select event when disabled', async () => { + const ctx = await createFixture({ disabled: true }); + const spy = sinon.spy(); + ctx.list.addEventListener('forge-list-item-select', spy); + ctx.listItems[1].click(); + ctx.listItems[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + ctx.listItems[1].dispatchEvent(new KeyboardEvent('keydown', { key: ' ' })); + expect(spy).to.not.have.been.called; + }); + + it('should not dispatch select event when noninteractive', async () => { + const ctx = await createFixture({ nonInteractive: true }); + const spy = sinon.spy(); + ctx.list.addEventListener('forge-list-item-select', spy); + ctx.listItems[1].click(); + ctx.listItems[1].dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + ctx.listItems[1].dispatchEvent(new KeyboardEvent('keydown', { key: ' ' })); + expect(spy).to.not.have.been.called; + }); + + it('should set selected via selectedValue on list', async () => { + const ctx = await createFixture({ selectedValue: '2' }); + + expect(ctx.listItems[1].selected).to.true; + expect(ctx.listItems.filter(li => li.selected).length).to.be.equal(1); + await expect(ctx.list).to.be.accessible(); + }); + + it('should set selected value via attribute', async () => { + const ctx = await createFixture(); + + ctx.list.setAttribute(LIST_CONSTANTS.attributes.SELECTED_VALUE, '2'); + expect(ctx.list.selectedValue).to.equal('2'); + expect(ctx.listItems[1].selected).to.be.true; + }); + + it('should remove selected value', async () => { + const ctx = await createFixture({ selectedValue: '2' }); + + expect(ctx.listItems[1].selected).to.true; + + ctx.list.selectedValue = null; + expect(ctx.listItems.filter(li => li.selected).length).to.be.equal(0); + }); + + it('should render anchor element when href is set', async () => { + const ctx = await createFixture({ anchor: true }); + const anchor = ctx.getListItemRootElement(0) as HTMLAnchorElement; + + expect(anchor).to.be.instanceOf(HTMLAnchorElement); + expect(ctx.listItems[0].href).to.equal(DEFAULT_HREF); + expect(ctx.listItems[0].getAttribute(LIST_ITEM_CONSTANTS.attributes.HREF)).to.equal(DEFAULT_HREF); + expect(anchor.href).to.equal(DEFAULT_HREF); + expect(anchor.target).to.equal('_blank'); + await expect(ctx.list).to.be.accessible(); + }); + + it('should set anchor target', async () => { + const ctx = await createFixture({ anchor: true, anchorTarget: '_self' }); + const anchor = ctx.getListItemRootElement(0) as HTMLAnchorElement; + + expect(anchor).to.be.instanceOf(HTMLAnchorElement); + expect(ctx.listItems[0].target).to.equal('_self'); + expect(ctx.listItems[0].getAttribute(LIST_ITEM_CONSTANTS.attributes.TARGET)).to.equal('_self'); + expect(anchor.href).to.equal(DEFAULT_HREF); + expect(anchor.target).to.equal('_self'); + }); + + it('should reset root element when href is removed', async () => { + const ctx = await createFixture({ anchor: true }); + const anchor = ctx.getListItemRootElement(0) as HTMLAnchorElement; + + expect(anchor).to.be.instanceOf(HTMLAnchorElement); + + ctx.listItems[0].href = ''; + expect(ctx.getListItemRootElement(0)).to.be.instanceOf(HTMLDivElement); + }); + + it('should update anchor target element', async () => { + const ctx = await createFixture({ anchor: true }); + const anchor = ctx.getListItemRootElement(0) as HTMLAnchorElement; + + expect(anchor.target).to.equal('_blank'); + + ctx.listItems[0].target = '_self'; + expect(anchor.target).to.equal('_self'); + }); + + it('should set disabled', async () => { + const ctx = await createFixture({ disabled: true }); + + expect(ctx.listItemsAttr('disabled', '')).to.true; + expect(ctx.listItemsTabIndex(-1)).to.true; + expect(ctx.hasStateLayer()).to.be.false; + expect(ctx.hasFocusIndicator()).to.be.false; + await expect(ctx.list).to.be.accessible(); + }); + + it('should re-enable interactivity after disabled', async () => { + const ctx = await createFixture({ disabled: true }); + + expect(ctx.hasStateLayer()).to.be.false; + expect(ctx.hasFocusIndicator()).to.be.false; + + ctx.list.disabled = false; + expect(ctx.listItemsTabIndex(0)).to.true; + expect(ctx.hasStateLayer()).to.be.true; + expect(ctx.hasFocusIndicator()).to.be.true; + }); + + it('should set nonInteractive', async () => { + const ctx = await createFixture({ nonInteractive: true }); + + expect(ctx.listItemsTabIndex(-1)).to.true; + expect(ctx.hasStateLayer()).to.be.false; + expect(ctx.hasFocusIndicator()).to.be.false; + await expect(ctx.list).to.be.accessible(); + }); + + it('should set non-interactive via static attribute for backwards compatibility', async () => { + const ctx = await createFixture(); + + ctx.list.static = true; + expect(ctx.list.static).to.be.true; + expect(ctx.list.nonInteractive).to.be.true; + expect(ctx.listItemsTabIndex(-1)).to.true; + expect(ctx.listItemsAttr(LIST_ITEM_CONSTANTS.attributes.STATIC, '')).to.true; + expect(ctx.listItemsAttr(LIST_ITEM_CONSTANTS.attributes.NON_INTERACTIVE, '')).to.true; + expect(ctx.hasStateLayer()).to.be.false; + expect(ctx.hasFocusIndicator()).to.be.false; + + ctx.list.static = false; + expect(ctx.list.static).to.be.false; + expect(ctx.list.nonInteractive).to.be.false; + expect(ctx.listItemsTabIndex(0)).to.true; + expect(ctx.listItemsAttr(LIST_ITEM_CONSTANTS.attributes.STATIC, '')).to.false; + expect(ctx.listItemsAttr(LIST_ITEM_CONSTANTS.attributes.NON_INTERACTIVE, '')).to.false; + expect(ctx.hasStateLayer()).to.be.true; + expect(ctx.hasFocusIndicator()).to.be.true; + + ctx.listItems[0].static = true; + expect(ctx.listItems[0].static).to.be.true; + expect(ctx.listItems[0].nonInteractive).to.be.true; + }); + + it('should re-enable interactivity after nonInteractive', async () => { + const ctx = await createFixture({ nonInteractive: true }); + + expect(ctx.hasStateLayer()).to.be.false; + expect(ctx.hasFocusIndicator()).to.be.false; + + ctx.list.nonInteractive = false; + expect(ctx.listItemsTabIndex(0)).to.true; + expect(ctx.hasStateLayer()).to.be.true; + expect(ctx.hasFocusIndicator()).to.be.true; + }); + + it('should set dense', async () => { + const ctx = await createFixture({ dense: true }); + + expect(ctx.list.hasAttribute(LIST_ITEM_CONSTANTS.attributes.DENSE)).to.true; + expect(ctx.listItemsAttr('dense', '')).to.true; + + ctx.list.dense = false; + + expect(ctx.list.hasAttribute(LIST_ITEM_CONSTANTS.attributes.DENSE)).to.false; + expect(ctx.listItemsAttr('dense', '')).to.false; + }); + + it('should set indented', async () => { + const ctx = await createFixture({ indented: true }); + + expect(ctx.list.hasAttribute(LIST_ITEM_CONSTANTS.attributes.INDENTED)).to.true; + expect(ctx.listItemsAttr('indented', '')).to.true; + + ctx.list.indented = false; + + expect(ctx.list.hasAttribute(LIST_ITEM_CONSTANTS.attributes.INDENTED)).to.false; + expect(ctx.listItemsAttr('indented', '')).to.false; + }); + + it('should set twoLine', async () => { + const ctx = await createFixture({ twoLine: true }); + + expect(ctx.list.hasAttribute(LIST_ITEM_CONSTANTS.attributes.TWO_LINE)).to.true; + expect(ctx.listItemsAttr('two-line', '')).to.true; + + ctx.list.twoLine = false; + + expect(ctx.list.hasAttribute(LIST_ITEM_CONSTANTS.attributes.TWO_LINE)).to.false; + expect(ctx.listItemsAttr('two-line', '')).to.false; + }); + + it('should set threeLine', async () => { + const ctx = await createFixture({ threeLine: true }); + + expect(ctx.list.hasAttribute(LIST_ITEM_CONSTANTS.attributes.THREE_LINE)).to.true; + expect(ctx.listItemsAttr('three-line', '')).to.true; + + ctx.list.threeLine = false; + + expect(ctx.list.hasAttribute(LIST_ITEM_CONSTANTS.attributes.THREE_LINE)).to.false; + expect(ctx.listItemsAttr('three-line', '')).to.false; + }); + + it('should set wrap', async () => { + const ctx = await createFixture({ wrap: true }); + + expect(ctx.list.hasAttribute(LIST_ITEM_CONSTANTS.attributes.WRAP)).to.true; + expect(ctx.listItemsAttr('wrap', '')).to.true; + + ctx.list.wrap = false; + + expect(ctx.list.hasAttribute(LIST_ITEM_CONSTANTS.attributes.WRAP)).to.false; + expect(ctx.listItemsAttr('wrap', '')).to.false; + }); + + it('should focus next item when down arrow key is pressed', async () => { + const ctx = await createFixture(); + + ctx.focusListItem(0); + ctx.listItemPressKey(0, 'ArrowDown'); + expect(ctx.isListItemFocused(1)).to.be.true; + }); + + it('should focus previous item when down up key is pressed', async () => { + const ctx = await createFixture(); + + ctx.focusListItem(1); + ctx.listItemPressKey(1, 'ArrowUp'); + expect(ctx.isListItemFocused(0)).to.be.true; + }); + + it('should focus first item when home key is pressed', async () => { + const ctx = await createFixture(); + + ctx.focusListItem(1); + ctx.listItemPressKey(1, 'Home'); + expect(ctx.isListItemFocused(0)).to.be.true; + }); + + it('should focus last item when end key is pressed', async () => { + const ctx = await createFixture(); + + ctx.focusListItem(1); + ctx.listItemPressKey(1, 'End'); + expect(ctx.isListItemFocused(2)).to.be.true; + }); + + it('should cycle focus to first item when down arrow pressed on last item', async () => { + const ctx = await createFixture(); + + ctx.focusListItem(2); + ctx.listItemPressKey(2, 'ArrowDown'); + expect(ctx.isListItemFocused(0)).to.be.true; + }); + + it('should cycle focus to last item when up arrow pressed on first item', async () => { + const ctx = await createFixture(); + + ctx.focusListItem(0); + ctx.listItemPressKey(0, 'ArrowUp'); + expect(ctx.isListItemFocused(2)).to.be.true; + }); + + it('should not move focus when modifier key is pressed', async () => { + const ctx = await createFixture(); + + ctx.focusListItem(1); + ctx.listItemPressKey(1, 'ArrowDown', { shiftKey: true }); + ctx.listItemPressKey(1, 'ArrowDown', { altKey: true }); + ctx.listItemPressKey(1, 'ArrowDown', { ctrlKey: true }); + ctx.listItemPressKey(1, 'ArrowDown', { metaKey: true }); + expect(ctx.isListItemFocused(1)).to.be.true; + }); + + it('should activate focus indicator when active set', async () => { + const ctx = await createFixture(); + + ctx.listItems[1].active = true; + expect(ctx.listItems[1].active).to.be.true; + expect(ctx.listItems[1].hasAttribute(LIST_ITEM_CONSTANTS.attributes.ACTIVE)).to.be.true; + expect(ctx.listItemActive(0)).to.be.false; + expect(ctx.listItemActive(1)).to.be.true; + expect(ctx.listItemActive(0)).to.be.false; + }); + + it('should set selected when value matches list selected value', async () => { + const ctx = await createFixture({ selectedValue: 'some-value' }); + + expect(ctx.listItems[1].selected).to.be.false; + + ctx.listItems[1].value = 'some-value'; + expect(ctx.listItems[1].selected).to.be.true; + }); + + it('should toggle slotted checkbox when list item is selected', async () => { + const ctx = await createFixture({ withCheckbox: true }); + + expect(ctx.listItems[0].selected).to.be.false; + expect((ctx.listItems[0].querySelector('input[type=checkbox]') as HTMLInputElement)?.checked).to.be.false; + + ctx.listItems[0].selected = true; + expect(ctx.listItems[0].selected).to.be.true; + expect((ctx.listItems[0].querySelector('input[type=checkbox]') as HTMLInputElement)?.checked).to.be.true; + }); + + it('should toggle slotted radio button when list item is selected', async () => { + const ctx = await createFixture({ withRadioButton: true }); + + expect(ctx.listItems[0].selected).to.be.false; + expect((ctx.listItems[0].querySelector('input[type=radio]') as HTMLInputElement)?.checked).to.be.false; + + ctx.listItems[0].selected = true; + expect(ctx.listItems[0].selected).to.be.true; + expect((ctx.listItems[0].querySelector('input[type=radio]') as HTMLInputElement)?.checked).to.be.true; + }); + + it('should inherit parent list state when adding new list item', async () => { + const ctx = await createFixture({ + role: 'listbox', + nonInteractive: true, + disabled: true, + dense: true, + propagateClick: false, + twoLine: true, + threeLine: true, + selectedValue: '4' + }); + + const listItem = document.createElement('forge-list-item'); + listItem.value = '4'; + ctx.list.appendChild(listItem); + + expect(listItem.nonInteractive).to.be.true; + expect(listItem.disabled).to.be.true; + expect(listItem.dense).to.be.true; + expect(listItem.propagateClick).to.be.false; + expect(listItem.twoLine).to.be.true; + expect(listItem.threeLine).to.be.true; + expect(listItem.selected).to.be.true; + expect(listItem.role).to.equal('option'); + }); + + it('should not dispatch select event if target element has forge-ignore attribute', async () => { + const ctx = await createFixture(); + const spy = sinon.spy(); + ctx.list.addEventListener('forge-list-item-select', spy); + + const ignoredElement = document.createElement('button'); + ignoredElement.type = 'button'; + ignoredElement.setAttribute('forge-ignore', ''); + ctx.listItems[0].appendChild(ignoredElement); + + await elementUpdated(ctx.list); + ignoredElement.click(); + + expect(spy).to.not.have.been.called; + }); +}); + +interface ListFixtureConfig { + role?: 'list' | 'listbox' | 'menu'; + nonInteractive?: boolean; + disabled?: boolean; + dense?: boolean; + propagateClick?: boolean; + indented?: boolean; + selectedValue?: any; + twoLine?: boolean; + threeLine?: boolean; + wrap?: boolean; + anchor?: boolean; + anchorTarget?: '_blank' | '_self'; + withCheckbox?: boolean; + withRadioButton?: boolean; +} + +async function createFixture({ + role, + nonInteractive, + disabled, + dense, + propagateClick, + indented, + selectedValue, + twoLine, + threeLine, + wrap, + anchor, + anchorTarget = '_blank', + withCheckbox, + withRadioButton +}: ListFixtureConfig = {}): Promise { + const el = await fixture(html` + + + One + ${withCheckbox ? html`` : null} + ${withRadioButton ? html`` : null} + + Two + Three + + `); + return new ListHarness(el); +} + +class ListHarness extends TestHarness { + public list: IListComponent; + public listItems: IListItemComponent[]; + + constructor(el: IListComponent) { + super(el); + } + + public initElementRefs(): void { + this.list = this.element; + this.listItems = Array.from(this.element.querySelectorAll('forge-list-item')); + } + + public listItemsAttr(attr: string, value: string): boolean { + return this.listItems.every(li => li.getAttribute(attr) === value); + } + + public listItemsTabIndex(tabIndex: number): boolean { + return this.listItems.every(li => (li.shadowRoot?.firstElementChild as HTMLElement)?.tabIndex === tabIndex); + } + + public getListItemRootElement(index: number): HTMLElement { + const item = this.listItems[index]; + return item.shadowRoot!.querySelector(LIST_ITEM_CONSTANTS.selectors.ROOT) as HTMLElement; + } + + public hasStateLayer(): boolean { + return this.listItems.every(li => !!(getShadowElement(li, 'forge-state-layer') as IStateLayerComponent)); + } + + public hasFocusIndicator(): boolean { + return this.listItems.every(li => !!(getShadowElement(li, 'forge-focus-indicator') as IFocusIndicatorComponent)); + } + + public listItemPressKey(index: number, key: string, modifierKeys?: { shiftKey?: true, altKey?: true, ctrlKey?: true, metaKey?: true }): void { + this.listItems[index].dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, ...modifierKeys })); + } + + public focusListItem(index: number): void { + this.listItems[index].focus(); + } + + public isListItemFocused(index: number): boolean { + return this.listItems[index].matches(':focus'); + } + + public listItemActive(index: number): boolean { + const focusIndicator = getShadowElement(this.listItems[index], 'forge-focus-indicator') as IFocusIndicatorComponent; + return focusIndicator?.active; + } +} diff --git a/src/lib/list/list/_configuration.scss b/src/lib/list/list/_configuration.scss new file mode 100644 index 000000000..267479d4f --- /dev/null +++ b/src/lib/list/list/_configuration.scss @@ -0,0 +1,7 @@ +@use '../../core/styles/tokens/list/list/tokens'; + +@mixin configuration { + --_container-color: #{tokens.get(container-color)}; + --_block-padding: #{tokens.get(block-padding)}; + --_inline-padding: #{tokens.get(inline-padding)}; +} diff --git a/src/lib/list/list/_core.scss b/src/lib/list/list/_core.scss new file mode 100644 index 000000000..ee5807632 --- /dev/null +++ b/src/lib/list/list/_core.scss @@ -0,0 +1,16 @@ +@use '../../core/styles/utils'; +@use '../../core/styles/tokens/list/list/tokens'; + +@mixin provide-theme($theme) { + @include utils.provide(tokens.$tokens, $theme, list); +} + +@mixin base { + display: block; + outline: none; + background-color: var(--_container-color); + padding: var(--_block-padding) var(--_inline-padding); + margin: 0; + border-radius: inherit; + min-width: inherit; +} diff --git a/src/lib/list/list/_mixins.scss b/src/lib/list/list/_mixins.scss deleted file mode 100644 index 7d9dcbabd..000000000 --- a/src/lib/list/list/_mixins.scss +++ /dev/null @@ -1,40 +0,0 @@ -@use 'sass:map'; -@use '@material/feature-targeting/feature-targeting' as mdc-feature-targeting; -@use '@material/typography/typography' as mdc-typography; -@use '../../theme'; - -@mixin core-styles() { - .forge-list { - @include base; - } -} - -@mixin host() { - display: block; - contain: content; -} - -@mixin base($query: mdc-feature-targeting.all()) { - $feat-color: mdc-feature-targeting.create-target($query, color); - $feat-structure: mdc-feature-targeting.create-target($query, structure); - $feat-typography: mdc-feature-targeting.create-target($query, typography); - - @include mdc-typography.typography(subtitle1, $query); - @include mdc-feature-targeting.targets($feat-typography) { - // According to the mocks and stickersheet, the line-height is - // adjusted to 24px for text content, same as for body1. - /* @alternate */ - line-height: map.get(map.get(mdc-typography.$styles, body1), line-height); - } - @include mdc-feature-targeting.targets($feat-structure) { - @include theme.css-custom-property(padding-top, --forge-list-padding, 8px); - @include theme.css-custom-property(padding-bottom, --forge-list-padding, 8px); - - margin: 0; - list-style-type: none; - - &:focus { - outline: none; - } - } -} diff --git a/src/lib/list/list/index.scss b/src/lib/list/list/index.scss new file mode 100644 index 000000000..c78abe8eb --- /dev/null +++ b/src/lib/list/list/index.scss @@ -0,0 +1,2 @@ +@forward './configuration'; +@forward './core'; diff --git a/src/lib/list/list/index.ts b/src/lib/list/list/index.ts index abd826e2f..5d0580369 100644 --- a/src/lib/list/list/index.ts +++ b/src/lib/list/list/index.ts @@ -1,5 +1,4 @@ import { defineCustomElement } from '@tylertech/forge-core'; - import { ListComponent } from './list'; export * from './list-adapter'; diff --git a/src/lib/list/list/list-adapter.ts b/src/lib/list/list/list-adapter.ts index f05c5b6d1..b63acc972 100644 --- a/src/lib/list/list/list-adapter.ts +++ b/src/lib/list/list/list-adapter.ts @@ -1,124 +1,100 @@ -import { emitEvent, deepQuerySelectorAll, getActiveElement } from '@tylertech/forge-core'; -import { BaseAdapter, IBaseAdapter } from '../../core/base/base-adapter'; -import { IListItemComponent, LIST_ITEM_CONSTANTS } from '../list-item'; +import { isDeepEqual } from '@tylertech/forge-core'; +import { BaseAdapter } from '../../core/base/base-adapter'; import { IListComponent } from './list'; -import { LIST_CONSTANTS } from './list-constants'; +import { LIST_ITEM_CONSTANTS } from '../list-item/list-item-constants'; +import { IListItemComponent } from '../list-item/list-item'; +import { ListComponentItemRole, LIST_CONSTANTS } from './list-constants'; -export interface IListAdapter extends IBaseAdapter { - initializeAccessibility(): void; - addListener(type: string, listener: (evt: Event) => void): void; - removeListener(type: string, listener: (evt: Event) => void): void; - getListItems(): IListItemComponent[]; +export interface IListAdapter extends BaseAdapter { + initialize(): void; focusNextListItem(): void; focusPreviousListItem(): void; focusFirstListItem(): void; focusLastListItem(): void; - setSelectedListItems(values: any[]): void; + setSelectedListItems(values: unknown | unknown[]): void; updateListItems(cb: (li: IListItemComponent) => void): void; + updateListItemRole(): void; } -/** - * The DOM adapter for the `` component. - */ export class ListAdapter extends BaseAdapter implements IListAdapter { constructor(component: IListComponent) { super(component); } - public initializeAccessibility(): void { + public initialize(): void { if (!this._component.hasAttribute('role')) { this._component.setAttribute('role', 'list'); } } - /** - * Adds an event listener to the `` host element. - * @param {string} type The event type. - * @param {Function} listener The event callback. - */ - public addListener(type: string, listener: (evt: Event) => void): void { - this._component.addEventListener(type, listener); - } - - /** - * Removes an event listener to the `` host element. - * @param {string} type The event type. - * @param {Function} listener The event callback. - */ - public removeListener(type: string, listener: (evt: Event) => void): void { - this._component.removeEventListener(type, listener); - } - - /** - * Returns all child `` elements. - */ - public getListItems(): IListItemComponent[] { - return Array.from(this._component.children).filter(child => child.tagName === LIST_ITEM_CONSTANTS.elementName.toUpperCase()) as IListItemComponent[]; - } - - /** - * Sets focus to the next item in the list. - */ + /** Sets focus to the next item in the list. */ public focusNextListItem(): void { - const listItems = deepQuerySelectorAll(this._component, LIST_CONSTANTS.selectors.FOCUSABLE_LIST_ITEMS, false) as HTMLElement[]; - - if (listItems && listItems.length > 0) { - const focusedListItemIndex = listItems.indexOf(getActiveElement(this._component.ownerDocument) as HTMLElement); + const listItems = this._getFocusableListItems(); + if (listItems.length) { + const focusedListItemIndex = listItems.findIndex(item => item.matches(':focus')); + console.log('focusedListItemIndex', focusedListItemIndex); const nextIndex = focusedListItemIndex < listItems.length - 1 ? focusedListItemIndex + 1 : 0; - if (nextIndex <= listItems.length - 1) { - listItems[nextIndex].focus(); + listItems[nextIndex].focus({ preventScroll: true}); } } } - /** - * Sets focus to the previous item in the list. - */ + /** Sets focus to the previous item in the list. */ public focusPreviousListItem(): void { - const listItems = deepQuerySelectorAll(this._component, LIST_CONSTANTS.selectors.FOCUSABLE_LIST_ITEMS, false) as HTMLElement[]; - - if (listItems && listItems.length > 0) { - const focusedListItemIndex = listItems.indexOf(getActiveElement(this._component.ownerDocument) as HTMLElement); + const listItems = this._getFocusableListItems(); + if (listItems.length) { + const focusedListItemIndex = listItems.findIndex(item => item.matches(':focus')); const nextIndex = focusedListItemIndex > 0 ? focusedListItemIndex - 1 : listItems.length - 1; - if (nextIndex >= 0) { - listItems[nextIndex].focus(); + listItems[nextIndex].focus({ preventScroll: true}); } } } - /** - * Sets focus to the first item in the list. - */ + /** Sets focus to the first item in the list. */ public focusFirstListItem(): void { - const listItems = deepQuerySelectorAll(this._component, LIST_CONSTANTS.selectors.FOCUSABLE_LIST_ITEMS, false) as HTMLElement[]; - - if (listItems && listItems.length > 0) { - listItems[0].focus(); + const listItems = this._getFocusableListItems(); + if (listItems.length) { + listItems[0].focus({ preventScroll: true}); } } - /** - * Sets focus to the last item in the list. - */ + /** Sets focus to the last item in the list. */ public focusLastListItem(): void { - const listItems = deepQuerySelectorAll(this._component, LIST_CONSTANTS.selectors.FOCUSABLE_LIST_ITEMS, false) as HTMLElement[]; - if (listItems && listItems.length > 0) { - listItems[listItems.length - 1].focus(); + const listItems = this._getFocusableListItems(); + if (listItems.length) { + listItems[listItems.length - 1].focus({ preventScroll: true}); } } - public setSelectedListItems(values: any[]): void { - const listItems = Array.from(this._component.querySelectorAll(LIST_ITEM_CONSTANTS.elementName)) as IListItemComponent[]; - if (listItems && listItems.length) { + /** Select all list items that match values in the provided array of values. */ + public setSelectedListItems(value: unknown | unknown[]): void { + const listItems = this._getListItems(); + if (listItems.length) { + const values = Array.isArray(value) ? value : [value]; for (const item of listItems) { - item.selected = values.includes(item.value); + item.selected = values.some(val => isDeepEqual(val, item.value)); } } } + /** Calls the provided callback on all list items to apply an updated property to each list item. */ public updateListItems(cb: (li: IListItemComponent) => void): void { - this.getListItems().forEach(li => cb(li)); + this._getListItems().forEach(cb); + } + + public updateListItemRole(): void { + const role = ListComponentItemRole[this._component.getAttribute('role') as string] ?? 'listitem'; + this.updateListItems(li => li.role = role); + } + + private _getListItems(): IListItemComponent[] { + const listItems = Array.from(this._component.querySelectorAll(LIST_ITEM_CONSTANTS.elementName)); + return listItems.filter(item => item.closest(LIST_CONSTANTS.elementName) === this._component) as IListItemComponent[]; + } + + private _getFocusableListItems(): IListItemComponent[] { + return this._getListItems().filter(li => !li.disabled && !li.nonInteractive && !li.hidden); } } diff --git a/src/lib/list/list/list-constants.ts b/src/lib/list/list/list-constants.ts index cb49dadba..59a583b66 100644 --- a/src/lib/list/list/list-constants.ts +++ b/src/lib/list/list/list-constants.ts @@ -1,21 +1,32 @@ import { COMPONENT_NAME_PREFIX } from '../../constants'; -const elementName: keyof HTMLElementTagNameMap = `${COMPONENT_NAME_PREFIX}list`; +const elementName = `${COMPONENT_NAME_PREFIX}list`; -const attributes = { +const observedAttributes = { + ROLE: 'role', STATIC: 'static', + NON_INTERACTIVE: 'non-interactive', + DISABLED: 'disabled', DENSE: 'dense', PROPAGATE_CLICK: 'propagate-click', + SELECTED_VALUE: 'selected-value', INDENTED: 'indented', - SELECTED_VALUE: 'selected-value' + TWO_LINE: 'two-line', + THREE_LINE: 'three-line', + WRAP: 'wrap' }; -const selectors = { - FOCUSABLE_LIST_ITEMS: '.forge-list-item:not(.forge-list-item--static):not(.forge-list-item--disabled)' +const attributes = { + ...observedAttributes }; export const LIST_CONSTANTS = { elementName, - attributes, - selectors + attributes +}; + +export const ListComponentItemRole = { + list: 'listitem', + listbox: 'option', + menu: 'menuitem' }; diff --git a/src/lib/list/list/list-foundation.ts b/src/lib/list/list/list-foundation.ts index 561ac7ee4..36db6182c 100644 --- a/src/lib/list/list/list-foundation.ts +++ b/src/lib/list/list/list-foundation.ts @@ -1,116 +1,106 @@ -import { ICustomElementFoundation, isDefined } from '@tylertech/forge-core'; +import { ICustomElementFoundation } from '@tylertech/forge-core'; import { IListAdapter } from './list-adapter'; import { LIST_CONSTANTS } from './list-constants'; -import { LIST_ITEM_CONSTANTS, IListItemComponent, IListItemSelectEventData } from '../list-item'; export interface IListFoundation extends ICustomElementFoundation { static: boolean; + nonInteractive: boolean; + disabled: boolean; dense: boolean; propagateClick: boolean; indented: boolean; - selectedValue: any; + selectedValue: unknown | unknown[]; + twoLine: boolean; + threeLine: boolean; + wrap: boolean; } -const ELEMENTS_KEY_ALLOWED_IN = ['input', 'button', 'textarea', 'select']; - -/** - * The foundation class behind the `` component. - */ export class ListFoundation implements IListFoundation { - private _static = false; + private _nonInteractive = false; + private _disabled = false; private _dense = false; private _propagateClick = true; private _indented = false; - private _selectedValue: any; - private _keydownListener: (evt: KeyboardEvent) => void; + private _selectedValue: unknown | unknown[]; + private _twoLine = false; + private _threeLine = false; + private _wrap = false; + private _keydownListener: EventListener; constructor(private _adapter: IListAdapter) { - this._keydownListener = (evt: KeyboardEvent) => this._onKeydown(evt); + this._keydownListener = this._onKeydown.bind(this); } public initialize(): void { - this._adapter.initializeAccessibility(); - - if (!this._static) { - this._adapter.addListener('keydown', this._keydownListener); + this._adapter.initialize(); + + if (!this._nonInteractive) { + this._adapter.addHostListener('keydown', this._keydownListener); } - if (this._selectedValue) { + if (this._selectedValue !== undefined && this._selectedValue !== null) { this._adapter.setSelectedListItems(this._selectedValue); } } private _onKeydown(evt: KeyboardEvent): void { - const isArrowDown = evt.key === 'ArrowDown' || evt.keyCode === 40; - const isArrowUp = evt.key === 'ArrowUp' || evt.keyCode === 38; - const isHome = evt.key === 'Home' || evt.keyCode === 36; - const isEnd = evt.key === 'End' || evt.keyCode === 35; - const isTab = evt.key === 'Tab' || evt.keyCode === 9; - - // We don't capture modifier keys - if (evt.altKey || evt.ctrlKey || evt.shiftKey || evt.metaKey) { - return; - } + const { key, altKey, ctrlKey, shiftKey, metaKey } = evt; - if (!isTab) { - this._preventDefaultEvent(evt); + if (altKey || ctrlKey || shiftKey || metaKey) { + return; } - if (isHome) { + if (key === 'Home') { + evt.preventDefault(); this._adapter.focusFirstListItem(); - } else if (isEnd) { + } else if (key === 'End') { + evt.preventDefault(); this._adapter.focusLastListItem(); - } else if (isArrowDown) { - this._adapter.focusNextListItem(); - } else if (isArrowUp) { + } else if (key === 'ArrowUp') { + evt.preventDefault(); this._adapter.focusPreviousListItem(); - } - } - - /** - * Ensures that preventDefault is only called if the containing element doesn't - * consume the event, and it will cause an unintended scroll. - * @param {Event} evt - */ - private _preventDefaultEvent(evt: Event): void { - const tagName = `${(evt.target as HTMLElement).tagName}`.toLowerCase(); - if (ELEMENTS_KEY_ALLOWED_IN.indexOf(tagName) === -1) { + } else if (key === 'ArrowDown') { evt.preventDefault(); + this._adapter.focusNextListItem(); } } - private _setSelectedValue(value: any): void { - let values = value instanceof Array ? value : [value]; - values = values.filter(v => isDefined(v)); - this._adapter.setSelectedListItems(values); + public updateRole(): void { + this._adapter.updateListItemRole(); } - /** Gets/sets whether the list has all static items or not. */ public get static(): boolean { - return this._static; + return this.nonInteractive; } public set static(value: boolean) { - if (this._static !== value) { - this._static = value; - - if (!this._static) { - this._adapter.addListener('keydown', this._keydownListener); - } else { - this._adapter.removeListener('keydown', this._keydownListener); - } - - this._adapter.updateListItems(li => li.static = this._static); - - if (this._static) { - this._adapter.setHostAttribute(LIST_CONSTANTS.attributes.STATIC); - } else { - this._adapter.removeHostAttribute(LIST_CONSTANTS.attributes.STATIC); - } + this.nonInteractive = value; + } + + public get nonInteractive(): boolean { + return this._nonInteractive; + } + public set nonInteractive(value: boolean) { + if (this._nonInteractive !== value) { + this._nonInteractive = value; + this._adapter.toggleHostListener('keydown', this._keydownListener, !this._nonInteractive); + this._adapter.updateListItems(li => li.nonInteractive = this._nonInteractive); + this._adapter.toggleHostAttribute(LIST_CONSTANTS.attributes.STATIC, this._nonInteractive); + this._adapter.toggleHostAttribute(LIST_CONSTANTS.attributes.NON_INTERACTIVE, this._nonInteractive); + } + } + + public get disabled(): boolean { + return this._disabled; + } + public set disabled(value: boolean) { + if (this._disabled !== value) { + this._disabled = value; + this._adapter.updateListItems(li => li.disabled = this._disabled); + this._adapter.toggleHostAttribute(LIST_CONSTANTS.attributes.DISABLED, this._disabled); } } - /** Gets/sets whether the list has all dense items or not. */ public get dense(): boolean { return this._dense; } @@ -118,11 +108,7 @@ export class ListFoundation implements IListFoundation { if (this._dense !== value) { this._dense = value; this._adapter.updateListItems(li => li.dense = this._dense); - if (this._dense) { - this._adapter.setHostAttribute(LIST_CONSTANTS.attributes.DENSE); - } else { - this._adapter.removeHostAttribute(LIST_CONSTANTS.attributes.DENSE); - } + this._adapter.toggleHostAttribute(LIST_CONSTANTS.attributes.DENSE, this._dense); } } @@ -133,7 +119,7 @@ export class ListFoundation implements IListFoundation { if (this._propagateClick !== value) { this._propagateClick = value; this._adapter.updateListItems(li => li.propagateClick = this._propagateClick); - this._adapter.setHostAttribute(LIST_CONSTANTS.attributes.PROPAGATE_CLICK, '' + !!this._propagateClick); + this._adapter.setHostAttribute(LIST_CONSTANTS.attributes.PROPAGATE_CLICK, this._propagateClick ? 'true' : 'false'); } } @@ -144,19 +130,50 @@ export class ListFoundation implements IListFoundation { if (this._indented !== value) { this._indented = value; this._adapter.updateListItems(li => li.indented = this._indented); - if (this._indented) { - this._adapter.setHostAttribute(LIST_CONSTANTS.attributes.INDENTED); - } else { - this._adapter.removeHostAttribute(LIST_CONSTANTS.attributes.INDENTED); - } + this._adapter.toggleHostAttribute(LIST_CONSTANTS.attributes.INDENTED, this._indented); } } - public get selectedValue(): any { + public get selectedValue(): unknown | unknown[] { return this._selectedValue; } - public set selectedValue(value: any) { - this._selectedValue = value; - this._setSelectedValue(value); + public set selectedValue(value: unknown | unknown[]) { + if (this._selectedValue !== value) { + this._selectedValue = value; + this._adapter.setSelectedListItems(this._selectedValue); + } + } + + public get twoLine(): boolean { + return this._twoLine; + } + public set twoLine(value: boolean) { + if (this._twoLine !== value) { + this._twoLine = value; + this._adapter.updateListItems(li => li.twoLine = this._twoLine); + this._adapter.toggleHostAttribute(LIST_CONSTANTS.attributes.TWO_LINE, this._twoLine); + } + } + + public get threeLine(): boolean { + return this._threeLine; + } + public set threeLine(value: boolean) { + if (this._threeLine !== value) { + this._threeLine = value; + this._adapter.updateListItems(li => li.threeLine = this._threeLine); + this._adapter.toggleHostAttribute(LIST_CONSTANTS.attributes.THREE_LINE, this._threeLine); + } + } + + public get wrap(): boolean { + return this._wrap; + } + public set wrap(value: boolean) { + if (this._wrap !== value) { + this._wrap = value; + this._adapter.updateListItems(li => li.wrap = this._wrap); + this._adapter.toggleHostAttribute(LIST_CONSTANTS.attributes.WRAP, this._wrap); + } } } diff --git a/src/lib/list/list/list.html b/src/lib/list/list/list.html index 9e4c361fb..574f866a1 100644 --- a/src/lib/list/list/list.html +++ b/src/lib/list/list/list.html @@ -2,4 +2,4 @@
- \ No newline at end of file + diff --git a/src/lib/list/list/list.scss b/src/lib/list/list/list.scss index e956eeb05..635e08787 100644 --- a/src/lib/list/list/list.scss +++ b/src/lib/list/list/list.scss @@ -1,11 +1,26 @@ -@use './mixins'; - -@include mixins.core-styles; +@use './configuration'; +@use './core'; +@use '../list-item'; :host { - @include mixins.host; + display: block; } :host([hidden]) { display: none; } + +:host([navlist]) { + @include list-item.provide-theme(( + height: 40px, + margin: 4px 8px, + shape: 4px, + supporting-text-font-size: 0.875rem, + supporting-text-font-weight: 500 + )); +} + +.forge-list { + @include configuration.configuration; + @include core.base; +} diff --git a/src/lib/list/list/list.ts b/src/lib/list/list/list.ts index 6580807ba..52ec4184f 100644 --- a/src/lib/list/list/list.ts +++ b/src/lib/list/list/list.ts @@ -1,19 +1,25 @@ -import { attachShadowTemplate, coerceBoolean, CustomElement, FoundationProperty } from '@tylertech/forge-core'; +import { CustomElement, attachShadowTemplate, FoundationProperty, coerceBoolean } from '@tylertech/forge-core'; import { BaseComponent, IBaseComponent } from '../../core/base/base-component'; -import { ListItemComponent } from '../list-item'; import { ListAdapter } from './list-adapter'; -import { LIST_CONSTANTS } from './list-constants'; import { ListFoundation } from './list-foundation'; +import { LIST_CONSTANTS } from './list-constants'; import template from './list.html'; import styles from './list.scss'; +import { ListItemComponent } from '../list-item'; export interface IListComponent extends IBaseComponent { + /** @deprecated Use nonInteractive instead. */ static: boolean; + nonInteractive: boolean; + disabled: boolean; dense: boolean; propagateClick: boolean; indented: boolean; selectedValue: any; + twoLine: boolean; + threeLine: boolean; + wrap: boolean; } declare global { @@ -23,22 +29,63 @@ declare global { } /** - * The custom element class behind the `` element. - * * @tag forge-list + * + * @summary Lists are vertical groupings of related content. + * + * @csspart root - The component's root container element. + * + * @slot - The default/unnamed slot for child list items. + * + * @cssproperty --forge-list-container-color - The background color of the list surface, + * @cssproperty --forge-list-block-padding - The block padding of the list before and after the list items. + * @cssproperty --forge-list-inline-padding - The inline padding of the list next to the list items. + * + * @property {string} role - The role of the list. Default is 'list'. Valid values are 'list', 'listbox', and 'menu'. + * @property {boolean} static - Whether the list has all static items or not. + * @property {boolean} nonInteractive - Whether the list has all non-interactive items or not. + * @property {boolean} disabled - Whether the list items are disabled or not. + * @property {boolean} dense - Whether the list has all dense items or not. + * @property {boolean} propagateClick - Whether the list items propagate click events or not. + * @property {boolean} indented - Whether the list items within this list are indented. Default is false. + * @property {unknown | unknown[]} selectedValue - The selected list item value(s). + * @property {boolean} twoLine - Whether the list has all two-line items or not. + * @property {boolean} threeLine - Whether the list has all three-line items or not. + * @property {boolean} wrap - Whether the list has all items that wrap their text or not. + * + * @attribute {string} role - The role of the list. Default is 'list'. Valid values are 'list', 'listbox', and 'menu'. + * @attribute {boolean} static - Whether the list has all static items or not. + * @attribute {boolean} non-interactive - Whether the list has all non-interactive items or not. + * @attribute {boolean} disabled - Whether the list items are disabled or not. + * @attribute {boolean} dense - Whether the list has all dense items or not. + * @attribute {boolean} propagate-click - Whether the list items propagate click events or not. + * @attribute {string} selected-value - The selected list item value(s). + * @attribute {boolean} indented - Whether the list items within this list are indented. Default is false. + * @attribute {boolean} two-line - Whether the list has all two-line items or not. + * @attribute {boolean} three-line - Whether the list has all three-line items or not. + * @attribute {boolean} wrap - Whether the list has all items that wrap their text or not. + * @attribute {boolean} navlist - Controls whether the list is styled a navigation list or not. */ @CustomElement({ name: LIST_CONSTANTS.elementName, - dependencies: [ListItemComponent] + dependencies: [ + ListItemComponent + ] }) export class ListComponent extends BaseComponent implements IListComponent { public static get observedAttributes(): string[] { return [ + LIST_CONSTANTS.attributes.ROLE, LIST_CONSTANTS.attributes.STATIC, + LIST_CONSTANTS.attributes.NON_INTERACTIVE, + LIST_CONSTANTS.attributes.DISABLED, LIST_CONSTANTS.attributes.DENSE, - LIST_CONSTANTS.attributes.SELECTED_VALUE, LIST_CONSTANTS.attributes.PROPAGATE_CLICK, - LIST_CONSTANTS.attributes.INDENTED + LIST_CONSTANTS.attributes.SELECTED_VALUE, + LIST_CONSTANTS.attributes.INDENTED, + LIST_CONSTANTS.attributes.TWO_LINE, + LIST_CONSTANTS.attributes.THREE_LINE, + LIST_CONSTANTS.attributes.WRAP ]; } @@ -50,14 +97,21 @@ export class ListComponent extends BaseComponent implements IListComponent { this._foundation = new ListFoundation(new ListAdapter(this)); } - public initializedCallback(): void { + public connectedCallback(): void { this._foundation.initialize(); } public attributeChangedCallback(name: string, oldValue: string, newValue: string): void { switch (name) { + case LIST_CONSTANTS.attributes.ROLE: + this._foundation.updateRole(); + break; case LIST_CONSTANTS.attributes.STATIC: - this.static = coerceBoolean(newValue); + case LIST_CONSTANTS.attributes.NON_INTERACTIVE: + this.nonInteractive = coerceBoolean(newValue); + break; + case LIST_CONSTANTS.attributes.DISABLED: + this.disabled = coerceBoolean(newValue); break; case LIST_CONSTANTS.attributes.DENSE: this.dense = coerceBoolean(newValue); @@ -71,26 +125,45 @@ export class ListComponent extends BaseComponent implements IListComponent { case LIST_CONSTANTS.attributes.SELECTED_VALUE: this.selectedValue = newValue; break; + case LIST_CONSTANTS.attributes.TWO_LINE: + this.twoLine = coerceBoolean(newValue); + break; + case LIST_CONSTANTS.attributes.THREE_LINE: + this.threeLine = coerceBoolean(newValue); + break; + case LIST_CONSTANTS.attributes.WRAP: + this.wrap = coerceBoolean(newValue); + break; } } - /** Gets/sets whether the list has all static items or not. */ @FoundationProperty() public declare static: boolean; - /** Gets/sets whether the list has all dense items or not. */ + @FoundationProperty() + public declare nonInteractive: boolean; + + @FoundationProperty() + public declare disabled: boolean; + @FoundationProperty() public declare dense: boolean; - /** Gets/sets whether the list items allow mousedown events through to their underlying list item elements. Default is true. */ @FoundationProperty() public declare propagateClick: boolean; - /** Gets/sets whether the list items within this list are indented. Default is false. */ @FoundationProperty() public declare indented: boolean; - /** Gets/sets the selected list item value(s) */ @FoundationProperty() public declare selectedValue: any; + + @FoundationProperty() + public declare twoLine: boolean; + + @FoundationProperty() + public declare threeLine: boolean; + + @FoundationProperty() + public declare wrap: boolean; } diff --git a/src/lib/slider/_core.scss b/src/lib/slider/_core.scss index a2f0b1117..d2b3bec6c 100644 --- a/src/lib/slider/_core.scss +++ b/src/lib/slider/_core.scss @@ -200,7 +200,7 @@ $_active-track-end-clip: calc($_active-track-end-offset + $_active-track-max-cli } @mixin handle-label { - @include typography.style(label, [color font-weight]); + @include typography.style(label, $exclude: [color font-weight]); position: absolute; box-sizing: border-box; diff --git a/src/lib/slider/slider.html b/src/lib/slider/slider.html index e87b06219..765abbc97 100644 --- a/src/lib/slider/slider.html +++ b/src/lib/slider/slider.html @@ -6,8 +6,8 @@
- - + +
diff --git a/src/lib/slider/slider.test.ts b/src/lib/slider/slider.test.ts index 47c1e859f..90a42f65f 100644 --- a/src/lib/slider/slider.test.ts +++ b/src/lib/slider/slider.test.ts @@ -9,7 +9,6 @@ import { SLIDER_CONSTANTS } from './slider-constants'; import type { IStateLayerComponent } from '../state-layer/state-layer'; import './slider'; -import { STATE_LAYER_CONSTANTS } from '../state-layer'; class SliderHarness extends TestHarness { public rootElement: HTMLElement; diff --git a/src/lib/tabs/tab/tab.html b/src/lib/tabs/tab/tab.html index f9587cc07..dde078b7e 100644 --- a/src/lib/tabs/tab/tab.html +++ b/src/lib/tabs/tab/tab.html @@ -8,7 +8,7 @@ - - + +
\ No newline at end of file diff --git a/src/test/spec/autocomplete/autocomplete.spec.ts b/src/test/spec/autocomplete/autocomplete.spec.ts index 077825ffa..49fc667e3 100644 --- a/src/test/spec/autocomplete/autocomplete.spec.ts +++ b/src/test/spec/autocomplete/autocomplete.spec.ts @@ -1753,11 +1753,11 @@ describe('AutocompleteComponent', function(this: ITestContext) { return options.map(({ label, value }) => ({ label, value })); } - function _getListItems(popupElement: HTMLElement | null): IListItemComponent[] { + function _getListItems(popupElement: HTMLElement | null): IListItemComponent[] { if (!popupElement) { return []; } - return Array.from(popupElement.querySelectorAll(LIST_ITEM_CONSTANTS.elementName)) as IListItemComponent[]; + return Array.from(popupElement.querySelectorAll(LIST_ITEM_CONSTANTS.elementName)) as IListItemComponent[]; } function _triggerDropdownClick(input: HTMLInputElement): void { diff --git a/src/test/spec/list-dropdown/list-dropdown.spec.ts b/src/test/spec/list-dropdown/list-dropdown.spec.ts index ad6b87626..4d0b1a2de 100644 --- a/src/test/spec/list-dropdown/list-dropdown.spec.ts +++ b/src/test/spec/list-dropdown/list-dropdown.spec.ts @@ -4,7 +4,7 @@ import { IListDropdownTestContext, createListDropdown, getListItems, getListDrop import { IListDropdownConfig, IListDropdownOption, ListDropdownAsyncStyle, ListDropdownHeaderBuilder, IListDropdownOptionGroup, LIST_DROPDOWN_CONSTANTS, ListDropdownFooterBuilder, ListDropdownType, ListDropdownOptionBuilder, ListDropdownTransformCallback } from '@tylertech/forge/list-dropdown'; import { defineOptionComponent, defineOptionGroupComponent } from '@tylertech/forge/select'; import { definePopupComponent, POPUP_CONSTANTS, IPopupComponent } from '@tylertech/forge/popup'; -import { defineListComponent, IListItemComponent } from '@tylertech/forge/list'; +import { defineListComponent, IListComponent, IListItemComponent, LIST_CONSTANTS } from '@tylertech/forge/list'; import { defineLinearProgressComponent, SKELETON_CONSTANTS, DIVIDER_CONSTANTS, IIconComponent, CIRCULAR_PROGRESS_CONSTANTS } from '@tylertech/forge'; import { tryCleanupPopups, isVisibleInScrollContainer } from '../../utils'; @@ -295,7 +295,7 @@ describe('ListDropdown', function(this: ITestContext) { expect(listItems[2].selected).toBeTrue(); }); - it('should toggle selection and active state in multiple mode of deselected option', async function(this: ITestContext) { + it('should toggle selection in multiple mode of deselected option', async function(this: ITestContext) { this.context = createListDropdown({ ...DEFAULT_CONFIG, multiple: true, selectedValues: [BASIC_OPTIONS[1].value] }); this.context.listDropdown.open(); await delayPopupAnimation(); @@ -310,7 +310,7 @@ describe('ListDropdown', function(this: ITestContext) { expect(listItems[1].selected).toBeTrue(); expect(listItems[1].active).toBeFalse(); expect(listItems[2].selected).toBeTrue(); - expect(listItems[2].active).toBeTrue(); + expect(listItems[2].active).toBeFalse(); }); it('should activate selected option', async function(this: ITestContext) { @@ -733,7 +733,8 @@ describe('ListDropdown', function(this: ITestContext) { this.context.listDropdown.open(); await delayPopupAnimation(); - expect(this.context.listDropdown.dropdownElement!.getAttribute('role')).toBe('menu'); + const listElement = this.context.listDropdown.dropdownElement!.querySelector(LIST_CONSTANTS.elementName) as IListComponent; + expect(listElement.getAttribute('role')).toBe('menu'); }); it('should set popup classes', async function(this: ITestContext) { diff --git a/src/test/spec/list/list-item/list-item.spec.ts b/src/test/spec/list/list-item/list-item.spec.ts deleted file mode 100644 index fb02935ea..000000000 --- a/src/test/spec/list/list-item/list-item.spec.ts +++ /dev/null @@ -1,675 +0,0 @@ -import { getShadowElement, removeElement, getActiveElement } from '@tylertech/forge-core'; -import { tick, timer } from '@tylertech/forge-testing'; -import { CHECKBOX_CONSTANTS, ICheckboxComponent } from '@tylertech/forge/checkbox'; -import { DRAWER_CONSTANTS, IDrawerComponent } from '@tylertech/forge/drawer'; -import { defineListComponent, IListComponent, LIST_CONSTANTS } from '@tylertech/forge/list'; -import { defineListItemComponent, IListItemComponent, LIST_ITEM_CONSTANTS } from '@tylertech/forge/list/list-item'; -import { IRadioComponent, RADIO_CONSTANTS } from '@tylertech/forge/radio'; - -interface ITestContext { - context: ITestListItemDefaultContext | ITestListItemDrawerContext | ITestListItemCheckboxContext | ITestListItemRadioContext; -} - -interface ITestListItemDefaultContext { - component: IListItemComponent; - listComponent: IListComponent; - getRootElement(): HTMLElement; - append(): void; - destroy(): void; - /** Simulates a user interaction with the button to instantiate the ripple */ - simulateInteraction(): Promise; -} - -interface ITestListItemDrawerContext { - component: IListItemComponent; - destroy(): void; -} - -interface ITestListItemCheckboxContext { - component: IListItemComponent; - listComponent: IListComponent; - getRootElement(): HTMLElement; - getInputElement(): HTMLInputElement; - append(): void; - destroy(): void; -} - -interface ITestListItemRadioContext { - component1: IListItemComponent; - component2: IListItemComponent; - listComponent: IListComponent; - getRootElement1(): HTMLElement; - getRootElement2(): HTMLElement; - getRadioInput1(): HTMLInputElement; - getRadioInput2(): HTMLInputElement; - destroy(): void; -} - -describe('ListItemComponent', function(this: ITestContext) { - beforeAll(function(this: ITestContext) { - defineListItemComponent(); - defineListComponent(); - }); - - afterEach(function(this: ITestContext) { - this.context.destroy(); - }); - - describe('individual list item', function(this: ITestContext) { - it('should have proper role', function(this: ITestContext) { - this.context = setupTestContext(true); - expect(this.context.component.getAttribute('role')).toBe(LIST_ITEM_CONSTANTS.roles.LIST_ITEM); - }); - - it('should not have drawer context attribute', function(this: ITestContext) { - this.context = setupTestContext(true); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.DRAWER_CONTEXT)).toBe(false); - }); - - it('should not be two-line by default', function(this: ITestContext) { - this.context = setupTestContext(true); - expect(this.context.component.twoLine).toBe(false); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.TWO_LINE)).toBe(false); - }); - - it('should not be three-line by default', function(this: ITestContext) { - this.context = setupTestContext(true); - expect(this.context.component.threeLine).toBe(false); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.THREE_LINE)).toBe(false); - }); - - it('should not be static by default', function(this: ITestContext) { - this.context = setupTestContext(true); - expect(this.context.component.static).toBe(false); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.STATIC)).toBe(false); - }); - - it('should not be active by default', function(this: ITestContext) { - this.context = setupTestContext(true); - expect(this.context.component.active).toBe(false); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.ACTIVE)).toBe(false); - }); - - it('should use ripple by default, but delay initialization until user interaction', async function(this: ITestContext) { - this.context = setupTestContext(true); - const context = this.context as ITestListItemDefaultContext ; - expect(context.component.ripple).withContext('ripple property').toBe(true); - expect(context.getRootElement().classList.contains('mdc-ripple-upgraded')).withContext('ripple before interaction').toBe(false); - await context.simulateInteraction(); - expect(context.getRootElement().classList.contains('mdc-ripple-upgraded')).withContext('ripple after interaction').toBe(true); - }); - - it('should not be selected by default', function(this: ITestContext) { - this.context = setupTestContext(true); - - expect(this.context.component.selected).toBe(false); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.SELECTED)).toBe(false); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.ACTIVATED)).toBe(false); - }); - - it('should not have value by default', function(this: ITestContext) { - this.context = setupTestContext(true); - expect(this.context.component.value).toBeUndefined(); - }); - - it('should not have href by default', function(this: ITestContext) { - this.context = setupTestContext(true); - expect(this.context.component.href).toBeUndefined(); - }); - - it('should not have target by default', function(this: ITestContext) { - this.context = setupTestContext(true); - expect(this.context.component.target).toBeUndefined(); - }); - - it('should not be disabled by default', function(this: ITestContext) { - this.context = setupTestContext(true); - expect(this.context.component.disabled).toBe(false); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.DISABLED)).toBe(false); - }); - - it('should not wrap by default', function(this: ITestContext) { - this.context = setupTestContext(true); - expect(this.context.component.wrap).toBeFalse(); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.WRAP)).toBeFalse(); - }); - - it('should not be dense by default', function(this: ITestContext) { - this.context = setupTestContext(true); - expect(this.context.component.dense).toBe(false); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.DENSE)).toBe(false); - }); - - it('should propagate click by default', function(this: ITestContext) { - this.context = setupTestContext(true); - expect(this.context.component.propagateClick).toBe(true); - }); - - it('should not be indented by default', function(this: ITestContext) { - this.context = setupTestContext(true); - expect(this.context.component.indented).toBe(false); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.INDENTED)).toBe(false); - }); - - it('should remove ripple', async function(this: ITestContext) { - this.context = setupTestContext(true); - await tick(); - this.context.component.ripple = false; - await tick(); - expect(this.context.component.ripple).toBe(false); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.RIPPLE)).toBe(true); - expect(this.context.component.getAttribute(LIST_ITEM_CONSTANTS.attributes.RIPPLE)).toBe('false'); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains('mdc-ripple-upgraded')).toBe(false); - }); - - it('should not set ripple if static', async function(this: ITestContext) { - this.context = setupTestContext(); - this.context.component.ripple = false; - (this.context as ITestListItemDefaultContext).append(); - await tick(); - this.context.component.static = true; - this.context.component.ripple = true; - await tick(); - expect(this.context.component.ripple).toBe(false); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.RIPPLE)).toBe(true); - expect(this.context.component.getAttribute(LIST_ITEM_CONSTANTS.attributes.RIPPLE)).toBe('false'); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains('mdc-ripple-upgraded')).toBe(false); - }); - - it('should set to static via property', function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.static = true; - expect(this.context.component.static).toBe(true); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.STATIC)).toBe(true); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.STATIC)).toBe(true); - }); - - it('should set to static via attribute', function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.setAttribute(LIST_ITEM_CONSTANTS.attributes.STATIC, 'true'); - expect(this.context.component.static).toBe(true); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.STATIC)).toBe(true); - }); - - it('should set to static via attribute by default', function(this: ITestContext) { - this.context = setupTestContext(); - this.context.component.setAttribute(LIST_ITEM_CONSTANTS.attributes.STATIC, 'true'); - (this.context as ITestListItemDefaultContext).append(); - expect(this.context.component.static).toBe(true); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.STATIC)).toBe(true); - }); - - it('should not emit selected event when static', function(this: ITestContext) { - this.context = setupTestContext(); - this.context.component.setAttribute(LIST_ITEM_CONSTANTS.attributes.STATIC, 'true'); - (this.context as ITestListItemDefaultContext).append(); - const listener = jasmine.createSpy('selected listener'); - this.context.component.addEventListener(LIST_ITEM_CONSTANTS.events.SELECT, listener); - (this.context as ITestListItemDefaultContext).getRootElement().click(); - expect(listener).not.toHaveBeenCalled(); - }); - - it('should emit selected event when not static', function(this: ITestContext) { - this.context = setupTestContext(true); - const listener = jasmine.createSpy('selected listener'); - this.context.component.addEventListener(LIST_ITEM_CONSTANTS.events.SELECT, listener); - (this.context as ITestListItemDefaultContext).getRootElement().click(); - expect(listener).toHaveBeenCalledTimes(1); - }); - - it('should emit selected event when enter key is pressed', function(this: ITestContext) { - this.context = setupTestContext(true); - const listener = jasmine.createSpy('selected listener'); - this.context.component.addEventListener(LIST_ITEM_CONSTANTS.events.SELECT, listener); - (this.context as ITestListItemDefaultContext).getRootElement().dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); - expect(listener).toHaveBeenCalledTimes(1); - }); - - it('should emit selected event when enter space is pressed', function(this: ITestContext) { - this.context = setupTestContext(true); - const listener = jasmine.createSpy('selected listener'); - this.context.component.addEventListener(LIST_ITEM_CONSTANTS.events.SELECT, listener); - (this.context as ITestListItemDefaultContext).getRootElement().dispatchEvent(new KeyboardEvent('keydown', { key: ' ' })); - expect(listener).toHaveBeenCalledTimes(1); - }); - - it('should set value via property', function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.value = 1; - expect(this.context.component.value).toBe(1); - }); - - it('should set correct role based on href value', function(this: ITestContext) { - this.context = setupTestContext(); - const url = 'https://www.google.com/'; - (this.context as ITestListItemDefaultContext).append(); - this.context.component.href = url; - expect((this.context as ITestListItemDefaultContext).getRootElement().getAttribute('role')).toBe(LIST_ITEM_CONSTANTS.roles.LINK); - }); - - it('should toggle role based on href', async function(this: ITestContext) { - this.context = setupTestContext(); - const url = 'https://www.google.com/'; - this.context.component.setAttribute(LIST_ITEM_CONSTANTS.attributes.HREF, url); - (this.context as ITestListItemDefaultContext).append(); - expect((this.context as ITestListItemDefaultContext).getRootElement().getAttribute('role')).toBe(LIST_ITEM_CONSTANTS.roles.LINK); - this.context.component.href = ''; - await tick(); - expect((this.context as ITestListItemDefaultContext).getRootElement().getAttribute('role')).toBe(LIST_ITEM_CONSTANTS.roles.LIST_ITEM); - }); - - it('should set href via property', function(this: ITestContext) { - this.context = setupTestContext(); - const url = 'https://www.google.com/'; - (this.context as ITestListItemDefaultContext).append(); - this.context.component.href = url; - expect(this.context.component.href).toBe(url); - expect(this.context.component.getAttribute(LIST_ITEM_CONSTANTS.attributes.HREF)).toBe(url); - }); - - it('should not emit selected event when href is set', function(this: ITestContext) { - this.context = setupTestContext(); - this.context.component.setAttribute(LIST_ITEM_CONSTANTS.attributes.HREF, '#SomeHref'); - (this.context as ITestListItemDefaultContext).append(); - const listener = jasmine.createSpy('selected listener'); - (this.context as ITestListItemDefaultContext).listComponent.addEventListener(LIST_ITEM_CONSTANTS.events.SELECT, listener); - (this.context as ITestListItemDefaultContext).getRootElement().click(); - expect(listener).not.toHaveBeenCalled(); - }); - - it('should set target via property', function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.target = '_blank'; - expect(this.context.component.target).toBe('_blank'); - }); - - it('should set target via attribute', function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.setAttribute(LIST_ITEM_CONSTANTS.attributes.TARGET, '_blank'); - expect(this.context.component.target).toBe('_blank'); - }); - - it('should set disabled via property', function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.disabled = true; - expect(this.context.component.disabled).toBe(true); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.DISABLED)).toBe(true); - this.context.component.disabled = false; - expect(this.context.component.disabled).toBe(false); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.DISABLED)).toBe(false); - }); - - it('should set disabled via attribute', function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.setAttribute(LIST_ITEM_CONSTANTS.attributes.DISABLED, 'true'); - expect(this.context.component.disabled).toBe(true); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.DISABLED)).toBe(true); - }); - - it('should set wrap via property', function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.wrap = true; - const rootElement = (this.context as ITestListItemDefaultContext).getRootElement(); - - expect(this.context.component.wrap).toBeTrue(); - expect(rootElement.classList.contains(LIST_ITEM_CONSTANTS.classes.WRAP)).toBeTrue(); - - this.context.component.wrap = false; - - expect(this.context.component.wrap).toBeFalse(); - expect(rootElement.classList.contains(LIST_ITEM_CONSTANTS.classes.WRAP)).toBeFalse(); - }); - - it('should set wrap via attribute', function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.setAttribute(LIST_ITEM_CONSTANTS.attributes.WRAP, ''); - expect(this.context.component.wrap).toBeTrue(); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.WRAP)).toBeTrue(); - }); - - it('should set target via attribute', function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.setAttribute(LIST_ITEM_CONSTANTS.attributes.TARGET, '_blank'); - expect(this.context.component.target).toBe('_blank'); - }); - - it('should focus list item', function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.focus(); - expect(getActiveElement()).toBe((this.context as ITestListItemDefaultContext).getRootElement()); - }); - - it('should not set focus when list item is selected', async function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.selected = true; - await tick(); - expect(getActiveElement()).not.toBe((this.context as ITestListItemDefaultContext).getRootElement()); - }); - - it('should render selected when set by default via attribute', function(this: ITestContext) { - this.context = setupTestContext(); - this.context.component.setAttribute(LIST_ITEM_CONSTANTS.attributes.SELECTED, ''); - (this.context as ITestListItemDefaultContext).append(); - - expect(this.context.component.selected).toBe(true); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.SELECTED)).toBe(true); - }); - - it('should set two-line via property', function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.twoLine = true; - expect(this.context.component.twoLine).toBe(true); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.TWO_LINE)).toBe(true); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.TWO_LINE)).toBe(true); - this.context.component.twoLine = false; - expect(this.context.component.twoLine).toBe(false); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.TWO_LINE)).toBe(false); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.TWO_LINE)).toBe(false); - }); - - it('should set two-line via attribute', function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.setAttribute(LIST_ITEM_CONSTANTS.attributes.TWO_LINE, 'true'); - expect(this.context.component.twoLine).toBe(true); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.TWO_LINE)).toBe(true); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.TWO_LINE)).toBe(true); - }); - - it('should set two-line via attribute by default', function(this: ITestContext) { - this.context = setupTestContext(); - this.context.component.setAttribute(LIST_ITEM_CONSTANTS.attributes.TWO_LINE, 'true'); - (this.context as ITestListItemDefaultContext).append(); - expect(this.context.component.twoLine).toBe(true); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.TWO_LINE)).toBe(true); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.TWO_LINE)).toBe(true); - }); - - it('should set three-line via property', function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.threeLine = true; - expect(this.context.component.threeLine).toBe(true); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.THREE_LINE)).toBe(true); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.THREE_LINE)).toBe(true); - this.context.component.threeLine = false; - expect(this.context.component.threeLine).toBe(false); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.THREE_LINE)).toBe(false); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.THREE_LINE)).toBe(false); - }); - - it('should set three-line via attribute', function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.setAttribute(LIST_ITEM_CONSTANTS.attributes.THREE_LINE, 'true'); - expect(this.context.component.threeLine).toBe(true); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.THREE_LINE)).toBe(true); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.THREE_LINE)).toBe(true); - }); - - it('should set three-line via attribute by default', function(this: ITestContext) { - this.context = setupTestContext(); - this.context.component.setAttribute(LIST_ITEM_CONSTANTS.attributes.THREE_LINE, 'true'); - (this.context as ITestListItemDefaultContext).append(); - expect(this.context.component.threeLine).toBe(true); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.THREE_LINE)).toBe(true); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.THREE_LINE)).toBe(true); - }); - - it('should set active via property', function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.active = true; - expect(this.context.component.active).toBe(true); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.ACTIVE)).toBe(true); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.ACTIVE)).toBe(true); - }); - - it('should set active via attribute', function(this: ITestContext) { - this.context = setupTestContext(true); - this.context.component.setAttribute(LIST_ITEM_CONSTANTS.attributes.ACTIVE, 'true'); - expect(this.context.component.active).toBe(true); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.ACTIVE)).toBe(true); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.ACTIVE)).toBe(true); - }); - - it('should set active via attribute by default', function(this: ITestContext) { - this.context = setupTestContext(); - this.context.component.setAttribute(LIST_ITEM_CONSTANTS.attributes.ACTIVE, 'true'); - (this.context as ITestListItemDefaultContext).append(); - expect(this.context.component.active).toBe(true); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.ACTIVE)).toBe(true); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.ACTIVE)).toBe(true); - }); - - it('should set active via property by default', function(this: ITestContext) { - this.context = setupTestContext(); - this.context.component.setAttribute(LIST_ITEM_CONSTANTS.attributes.ACTIVE, 'true'); - (this.context as ITestListItemDefaultContext).append(); - expect(this.context.component.active).toBe(true); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.ACTIVE)).toBe(true); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.ACTIVE)).toBe(true); - }); - - it('should toggle active', async function(this: ITestContext) { - this.context = setupTestContext(); - this.context.component.active = true; - (this.context as ITestListItemDefaultContext).append(); - await tick(); - this.context.component.active = false; - expect(this.context.component.active).toBe(false); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.ACTIVE)).toBe(false); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.ACTIVE)).toBe(false); - }); - - it('should toggle selected', async function(this: ITestContext) { - this.context = setupTestContext(); - this.context.component.selected = true; - (this.context as ITestListItemDefaultContext).append(); - await tick(); - this.context.component.selected = false; - expect(this.context.component.selected).toBe(false); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.SELECTED)).toBe(false); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.SELECTED)).toBe(false); - expect((this.context as ITestListItemDefaultContext).getRootElement().classList.contains(LIST_ITEM_CONSTANTS.classes.ACTIVATED)).toBe(false); - }); - }); - - describe('child of drawer component', function(this: ITestContext) { - - it('should have drawer context attribute when a child of a drawer component', function(this: ITestContext) { - this.context = setupDrawerTestContext(); - expect(this.context.component.hasAttribute(LIST_ITEM_CONSTANTS.attributes.DRAWER_CONTEXT)).toBe(true); - expect(this.context.component.getAttribute(LIST_ITEM_CONSTANTS.attributes.DRAWER_CONTEXT)).toBe('true'); - }); - }); - - describe('with checkbox', function(this: ITestContext) { - it('should check checkbox when clicked', function(this: ITestContext) { - this.context = setupCheckboxTestContext(true); - (this.context as ITestListItemCheckboxContext).getRootElement().click(); - expect((this.context as ITestListItemCheckboxContext).getInputElement().checked).toBe(true); - }); - - it('should check checkbox when parent list has selectedValue', async function(this: ITestContext) { - this.context = setupCheckboxTestContext(); - - (this.context as ITestListItemCheckboxContext).listComponent.selectedValue = [1]; - this.context.component.value = 1; - - // append to the DOM to start the test - (this.context as ITestListItemCheckboxContext).append(); - - await tick(); - - expect((this.context as ITestListItemCheckboxContext).getInputElement().checked).toBe(true); - }); - - it('should not check checkbox with the forge-ignore attribute applied when list-item is clicked', function(this: ITestContext) { - this.context = setupCheckboxTestContext(true); - const checkboxInputElement = (this.context as ITestListItemCheckboxContext).getInputElement(); - checkboxInputElement.setAttribute(LIST_ITEM_CONSTANTS.attributes.IGNORE, ''); - (this.context as ITestListItemCheckboxContext).getRootElement().click(); - expect(checkboxInputElement.checked).toBe(false); - }); - - it('should emit change event toggling checked state', function(this: ITestContext) { - this.context = setupCheckboxTestContext(true); - const changeSpy = jasmine.createSpy('change spy'); - (this.context as ITestListItemCheckboxContext).getInputElement().addEventListener('change', changeSpy); - - (this.context as ITestListItemCheckboxContext).getRootElement().click(); - - expect(changeSpy).toHaveBeenCalledTimes(1); - }); - }); - - describe('with radio', function(this: ITestContext) { - it('should check radio when clicked', function(this: ITestContext) { - this.context = setupRadioTestContext(); - this.context.getRootElement1().click(); - expect(this.context.getRadioInput1().checked).toBe(true); - }); - - it('should not check radio with the "forge-ignore" attribute applied when list-item is clicked', function(this: ITestContext) { - this.context = setupRadioTestContext(); - const radioInput1 = this.context.getRadioInput1(); - radioInput1.setAttribute(LIST_ITEM_CONSTANTS.attributes.IGNORE, ''); - - this.context.getRootElement1().click(); - expect(radioInput1.checked).toBe(false); - }); - - it('should check radio button in another list item', function(this: ITestContext) { - this.context = setupRadioTestContext(); - this.context.getRootElement1().click(); - this.context.getRootElement2().click(); - expect(this.context.getRadioInput1().checked).toBe(false); - expect(this.context.getRadioInput2().checked).toBe(true); - }); - - it('should emit change event toggling checked state', function(this: ITestContext) { - this.context = setupRadioTestContext(); - const changeSpy = jasmine.createSpy('change spy'); - this.context.getRadioInput1().addEventListener('change', changeSpy); - - this.context.getRootElement1().click(); - - expect(changeSpy).toHaveBeenCalledTimes(1); - }); - }); - - function setupTestContext(append = false): ITestListItemDefaultContext { - const fixture = document.createElement('div'); - fixture.id = 'list-item-test-fixture'; - - const listComponent = document.createElement(LIST_CONSTANTS.elementName) as IListComponent; - const component = createListItem(); - listComponent.appendChild(component); - fixture.appendChild(listComponent); - - if (append) { - document.body.appendChild(fixture); - } - - return { - listComponent, - component, - getRootElement: () => getShadowElement(component, LIST_ITEM_CONSTANTS.selectors.LIST_ITEM), - append: () => document.body.appendChild(fixture), - destroy: () => removeElement(fixture), - simulateInteraction: async () => { - const listItem = getShadowElement(component, LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - if (listItem) { - listItem.dispatchEvent(new Event('pointerenter')); - await timer(); - await tick(); - } - } - }; - } - - function setupDrawerTestContext(): ITestListItemDrawerContext { - const fixture = document.createElement('div'); - fixture.id = 'list-item-drawer-test-fixture'; - const listComponent = document.createElement(LIST_CONSTANTS.elementName) as IListComponent; - const component = createListItem(); - listComponent.appendChild(component) - const drawerComponent = document.createElement(DRAWER_CONSTANTS.elementName) as IDrawerComponent; - drawerComponent.appendChild(listComponent); - fixture.appendChild(drawerComponent); - document.body.appendChild(fixture); - return { - component, - destroy: () => removeElement(fixture) - }; - } - - function setupCheckboxTestContext(append = false): ITestListItemCheckboxContext { - const fixture = document.createElement('div'); - fixture.id = 'list-item-test-fixture'; - const listComponent = document.createElement(LIST_CONSTANTS.elementName) as IListComponent; - const component = createListItem(); - const checkboxComponent = document.createElement(CHECKBOX_CONSTANTS.elementName) as ICheckboxComponent; - checkboxComponent.slot = 'trailing'; - const checkboxInputElement = document.createElement('input'); - checkboxInputElement.type = 'checkbox'; - checkboxComponent.appendChild(checkboxInputElement); - component.appendChild(checkboxComponent); - listComponent.appendChild(component) - fixture.appendChild(listComponent); - - if(append) { - document.body.appendChild(fixture); - } - - return { - listComponent, - component, - getRootElement: () => getShadowElement(component, LIST_ITEM_CONSTANTS.selectors.LIST_ITEM), - getInputElement: () => component.querySelector('input') as HTMLInputElement, - append: () => document.body.appendChild(fixture), - destroy: () => removeElement(fixture) - }; - } - - function setupRadioTestContext(): ITestListItemRadioContext { - const fixture = document.createElement('div'); - fixture.id = 'list-item-radio-test-fixture'; - const listComponent = document.createElement(LIST_CONSTANTS.elementName) as IListComponent; - const component1 = createListItem(); - const component2 = createListItem(); - const radio1Component = createRadio(); - component1.appendChild(radio1Component); - listComponent.appendChild(component1) - const radio2Component = createRadio(); - component2.appendChild(radio2Component); - listComponent.appendChild(component2); - fixture.appendChild(listComponent); - document.body.appendChild(fixture); - return { - component1, - component2, - listComponent, - getRootElement1: () => getShadowElement(component1, LIST_ITEM_CONSTANTS.selectors.LIST_ITEM), - getRootElement2: () => getShadowElement(component2, LIST_ITEM_CONSTANTS.selectors.LIST_ITEM), - getRadioInput1: () => component1.querySelector('input') as HTMLInputElement, - getRadioInput2: () => component2.querySelector('input') as HTMLInputElement, - destroy: () => removeElement(fixture) - }; - } - - function createListItem(): IListItemComponent { - const component = document.createElement(LIST_ITEM_CONSTANTS.elementName) as IListItemComponent; - const lineOne = document.createElement('span'); - lineOne.textContent = 'Line one'; - component.appendChild(lineOne); - return component; - } - - function createRadio(): IRadioComponent { - const component = document.createElement(RADIO_CONSTANTS.elementName) as IRadioComponent; - component.slot = 'trailing'; - const inputElement = document.createElement('input'); - inputElement.type = 'radio'; - inputElement.name = 'list-radio'; - component.appendChild(inputElement); - return component; - } -}); diff --git a/src/test/spec/list/list/list.spec.ts b/src/test/spec/list/list/list.spec.ts deleted file mode 100644 index 2808bce53..000000000 --- a/src/test/spec/list/list/list.spec.ts +++ /dev/null @@ -1,425 +0,0 @@ -import { defineListComponent, IListComponent, LIST_CONSTANTS, LIST_ITEM_CONSTANTS, IListItemComponent, ListComponent } from '@tylertech/forge/list'; -import { DIVIDER_CONSTANTS, IDividerComponent } from '@tylertech/forge/divider'; -import { removeElement, getShadowElement, getActiveElement } from '@tylertech/forge-core'; -import { tick, dispatchKeyEvent } from '@tylertech/forge-testing'; - -interface ITestListItemNumber { - id: number; - text: string; - value: number; -} - -interface ITestListItemString { - id: number; - text: string; - value: string; -} - -const listNumberItems = [ - { id: 1, text: 'one', value: 1 }, - { id: 2, text: 'two', value: 2 }, - { id: 3, text: 'three', value: 3 } -]; - -const listStringItems = [ - { id: 1, text: 'one', value: '1' }, - { id: 2, text: 'two', value: '2' }, - { id: 3, text: 'three', value: '3' } -]; - -interface ITestContext { - context: ITestListContext -} - -interface ITestListContext { - component: IListComponent; - getListItemComponents(): IListItemComponent[]; - append(): void; - destroy(): void; -} - -describe('ListComponent', function(this: ITestContext) { - beforeAll(function(this: ITestContext) { - defineListComponent(); - }); - - afterEach(function(this: ITestContext) { - this.context.destroy(); - }); - - describe('list with number values', function(this: ITestContext) { - it('should not be static by default', function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - expect(this.context.component.static).toBe(false); - }); - - it('should set static via property', async function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - await tick(); - this.context.component.static = true; - expect(this.context.component.hasAttribute(LIST_CONSTANTS.attributes.STATIC)).toBe(true); - expect(this.context.getListItemComponents().every(li => li.static)).toBe(true); - }); - - it('should set static via attribute', function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - this.context.component.setAttribute(LIST_CONSTANTS.attributes.STATIC, 'true'); - expect(this.context.component.static).toBe(true); - expect(this.context.getListItemComponents().every(li => li.static)).toBe(true); - }); - - it('should render list items static when set by default via attribute', function(this: ITestContext) { - this.context = setupTestContext(false, listNumberItems); - this.context.component.setAttribute(LIST_CONSTANTS.attributes.STATIC, 'true'); - this.context.append(); - expect(this.context.component.static).toBe(true); - expect(this.context.getListItemComponents().every(li => li.static)).toBe(true); - }); - - it('should render list items static when set by default via property', function(this: ITestContext) { - this.context = setupTestContext(false, listNumberItems); - this.context.component.static = true; - this.context.append(); - expect(this.context.component.static).toBe(true); - expect(this.context.getListItemComponents().every(li => li.static)).toBe(true); - }); - - it('should set list items to not be static', async function(this: ITestContext) { - this.context = setupTestContext(false, listNumberItems); - this.context.component.static = true; - this.context.append(); - await tick(); - this.context.component.static = false; - expect(this.context.component.hasAttribute(LIST_CONSTANTS.attributes.STATIC)).toBe(false); - expect(this.context.getListItemComponents().every(li => li.static)).toBe(false); - }); - - it('should not handle keydown events when static', async function(this: ITestContext) { - this.context = setupTestContext(false, listNumberItems); - this.context.component.static = true; - this.context.append(); - const keydownSpy = spyOn(this.context.component['_foundation'], '_onKeydown').and.callThrough(); - await tick(); - dispatchKeyEvent(this.context.getListItemComponents()[0], 'keydown', 'Enter'); - expect(keydownSpy).not.toHaveBeenCalled(); - }); - - it('should handle keydown events when not static', async function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - const keydownSpy = spyOn(this.context.component['_foundation'], '_onKeydown').and.callThrough(); - await tick(); - dispatchKeyEvent(this.context.getListItemComponents()[0], 'keydown', 'Enter'); - expect(keydownSpy).toHaveBeenCalledTimes(1); - }); - - it('should handle keydown events when not static', async function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - const keydownSpy = spyOn(this.context.component['_foundation'], '_onKeydown').and.callThrough(); - await tick(); - dispatchKeyEvent(this.context.getListItemComponents()[0], 'keydown', 'Enter'); - expect(keydownSpy).toHaveBeenCalledTimes(1); - }); - - it('should not be dense by default', function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - expect(this.context.component.dense).toBe(false); - }); - - it('should set dense via property', function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - this.context.component.dense = true; - expect(this.context.component.dense).toBe(true); - expect(this.context.component.hasAttribute(LIST_CONSTANTS.attributes.DENSE)).toBe(true); - this.context.component.dense = false; - expect(this.context.component.dense).toBe(false); - expect(this.context.component.hasAttribute(LIST_CONSTANTS.attributes.DENSE)).toBe(false); - }); - - it('should set dense via attribute', function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - this.context.component.setAttribute(LIST_CONSTANTS.attributes.DENSE, 'true'); - expect(this.context.component.dense).toBe(true); - expect(this.context.getListItemComponents().every(li => li.dense)).toBe(true); - }); - - it('should render list items dense when set by default via attribute', function(this: ITestContext) { - this.context = setupTestContext(false, listNumberItems); - this.context.component.setAttribute(LIST_CONSTANTS.attributes.DENSE, 'true'); - this.context.append(); - expect(this.context.component.dense).toBe(true); - expect(this.context.getListItemComponents().every(li => li.dense)).toBe(true); - }); - - it('should render list items dense when set by default via property', function(this: ITestContext) { - this.context = setupTestContext(false, listNumberItems); - this.context.component.dense = true; - this.context.append(); - expect(this.context.component.dense).toBe(true); - expect(this.context.getListItemComponents().every(li => li.dense)).toBe(true); - }); - - it('should set list items to not be dense', async function(this: ITestContext) { - this.context = setupTestContext(false, listNumberItems); - this.context.component.dense = true; - this.context.append(); - await tick(); - this.context.component.dense = false; - expect(this.context.component.hasAttribute(LIST_CONSTANTS.attributes.DENSE)).toBe(false); - expect(this.context.getListItemComponents().every(li => li.dense)).toBe(false); - }); - - it('should set propagate click by default', function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - expect(this.context.component.propagateClick).toBe(true); - }); - - it('should set propagate click via property', function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - this.context.component.propagateClick = false; - expect(this.context.component.propagateClick).toBe(false); - this.context.component.propagateClick = true; - expect(this.context.component.propagateClick).toBe(true); - }); - - it('should set propagate click via attribute', function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - this.context.component.setAttribute(LIST_CONSTANTS.attributes.PROPAGATE_CLICK, 'false'); - expect(this.context.component.propagateClick).toBe(false); - expect(this.context.getListItemComponents().every(li => li.propagateClick)).toBe(false); - this.context.component.setAttribute(LIST_CONSTANTS.attributes.PROPAGATE_CLICK, 'true'); - expect(this.context.component.propagateClick).toBe(true); - expect(this.context.getListItemComponents().every(li => li.propagateClick)).toBe(true); - }); - - it('should set list items to not propagate click when set by default via attribute', function(this: ITestContext) { - this.context = setupTestContext(false, listNumberItems); - this.context.component.setAttribute(LIST_CONSTANTS.attributes.PROPAGATE_CLICK, 'false'); - this.context.append(); - expect(this.context.component.propagateClick).toBe(false); - expect(this.context.getListItemComponents().every(li => li.propagateClick)).toBe(false); - }); - - it('should set list items to not propagate click when set by default via property', function(this: ITestContext) { - this.context = setupTestContext(false, listNumberItems); - this.context.component.propagateClick = false; - this.context.append(); - expect(this.context.component.propagateClick).toBe(false); - expect(this.context.getListItemComponents().every(li => li.propagateClick)).toBe(false); - }); - - it('should not be indented by default', function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - expect(this.context.component.indented).toBe(false); - }); - - it('should set indented via property', function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - this.context.component.indented = true; - expect(this.context.component.indented).toBe(true); - expect(this.context.component.hasAttribute(LIST_CONSTANTS.attributes.INDENTED)).toBe(true); - this.context.component.indented = false; - expect(this.context.component.indented).toBe(false); - expect(this.context.component.hasAttribute(LIST_CONSTANTS.attributes.INDENTED)).toBe(false); - }); - - it('should set indented via attribute', function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - this.context.component.setAttribute(LIST_CONSTANTS.attributes.INDENTED, 'true'); - expect(this.context.component.indented).toBe(true); - expect(this.context.getListItemComponents().every(li => li.indented)).toBe(true); - }); - - it('should render list items indented when set by default via attribute', function(this: ITestContext) { - this.context = setupTestContext(false, listNumberItems); - this.context.component.setAttribute(LIST_CONSTANTS.attributes.INDENTED, 'true'); - this.context.append(); - expect(this.context.component.indented).toBe(true); - expect(this.context.getListItemComponents().every(li => li.indented)).toBe(true); - }); - - it('should render list items indented when set by default via property', function(this: ITestContext) { - this.context = setupTestContext(false, listNumberItems); - this.context.component.indented = true; - this.context.append(); - expect(this.context.component.indented).toBe(true); - expect(this.context.getListItemComponents().every(li => li.indented)).toBe(true); - }); - - it('should set list items to not be indented', async function(this: ITestContext) { - this.context = setupTestContext(false, listNumberItems); - this.context.component.indented = true; - this.context.append(); - await tick(); - this.context.component.indented = false; - expect(this.context.component.hasAttribute(LIST_CONSTANTS.attributes.INDENTED)).toBe(false); - expect(this.context.getListItemComponents().every(li => li.indented)).toBe(false); - }); - - it('should initialize with selected value via property', function(this: ITestContext) { - this.context = setupTestContext(false, listNumberItems); - this.context.component.selectedValue = [1, 2]; - this.context.append(); - expect(this.context.component.selectedValue.length).toBe(2); - const listItemComponents = this.context.getListItemComponents(); - expect(listItemComponents[0].selected).toBe(true); - expect(listItemComponents[1].selected).toBe(true); - }); - - it('should set selected value with via property', async function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - this.context.component.selectedValue = [1, 2]; - expect(this.context.component.selectedValue.length).toBe(2); - let listItemComponents = this.context.getListItemComponents(); - expect(listItemComponents[0].selected).toBe(true); - expect(listItemComponents[1].selected).toBe(true); - - await tick(); - - this.context.component.selectedValue = 1; - expect(this.context.component.selectedValue).toBe(1); - listItemComponents = this.context.getListItemComponents(); - expect(listItemComponents[0].selected).toBe(true); - expect(listItemComponents[1].selected).toBe(false); - }); - - it('should emit selected event when enter is pressed on focused list item', async function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - const listener = jasmine.createSpy('selected listener'); - this.context.component.addEventListener(LIST_ITEM_CONSTANTS.events.SELECT, listener); - await tick(); - - const listItemEl = getShadowElement(this.context.getListItemComponents()[0], LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - listItemEl.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); - - expect(listener).toHaveBeenCalledTimes(1); - }); - - it('should emit selected event when space is pressed on focused list item', async function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - const listener = jasmine.createSpy('selected listener'); - this.context.component.addEventListener(LIST_ITEM_CONSTANTS.events.SELECT, listener); - await tick(); - - const listItemEl = getShadowElement(this.context.getListItemComponents()[0], LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - listItemEl.dispatchEvent(new KeyboardEvent('keydown', { key: ' ' })); - - expect(listener).toHaveBeenCalledTimes(1); - }); - - it('should not ignore modifier keys when pressed on focused list item', async function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - const listener = jasmine.createSpy('selected listener'); - this.context.component.addEventListener(LIST_ITEM_CONSTANTS.events.SELECT, listener); - const listItemComponents = this.context.getListItemComponents(); - const firstItem = listItemComponents[0]; - const secondItem = listItemComponents[1]; - this.context.component.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' ,ctrlKey: true})); - this.context.component.focus(); - this.context.component.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' ,altKey: true})); - this.context.component.focus(); - this.context.component.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' ,shiftKey: true})); - this.context.component.focus(); - this.context.component.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown' ,metaKey: true})); - expect(listener).toHaveBeenCalledTimes(0); - }) - - it('should focus first list item when home is pressed', async function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - await tick(); - const listItemComponents = this.context.getListItemComponents(); - const firstListItem = getShadowElement(listItemComponents[0], LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - const secondListItem = getShadowElement(listItemComponents[1], LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - secondListItem.focus(); - dispatchKeyEvent(listItemComponents[1], 'keydown', 'Home', true); - expect(getActiveElement()).toBe(firstListItem); - }); - - it('should focus last list item when end is pressed', async function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - await tick(); - const listItemComponents = this.context.getListItemComponents(); - const lastListItem = getShadowElement(listItemComponents[2], LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - const secondListItem = getShadowElement(listItemComponents[1], LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - secondListItem.focus(); - dispatchKeyEvent(listItemComponents[1], 'keydown', 'End', true); - expect(getActiveElement()).toBe(lastListItem); - }); - - it('should focus next list item when down arrow is pressed', async function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - await tick(); - const listItemComponents = this.context.getListItemComponents(); - const firstListItem = getShadowElement(listItemComponents[0], LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - const secondListItem = getShadowElement(listItemComponents[1], LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - firstListItem.focus(); - dispatchKeyEvent(listItemComponents[0], 'keydown', 'ArrowDown', true); - expect(getActiveElement()).toBe(secondListItem); - }); - - it('should focus previous list item when up arrow is pressed', async function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - await tick(); - const listItemComponents = this.context.getListItemComponents(); - const firstListItem = getShadowElement(listItemComponents[0], LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - const secondListItem = getShadowElement(listItemComponents[1], LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - secondListItem.focus(); - dispatchKeyEvent(listItemComponents[1], 'keydown', 'ArrowUp', true); - expect(getActiveElement()).toBe(firstListItem); - }); - - it('should wrap focus when up arrow is pressed on first list item', async function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - await tick(); - const listItemComponents = this.context.getListItemComponents(); - const firstListItem = getShadowElement(listItemComponents[0], LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - const lastListItem = getShadowElement(listItemComponents[2], LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - firstListItem.focus(); - dispatchKeyEvent(listItemComponents[1], 'keydown', 'ArrowUp', true); - expect(getActiveElement()).toBe(lastListItem); - }); - - it('should wrap focus when down arrow is pressed on last list item', async function(this: ITestContext) { - this.context = setupTestContext(true, listNumberItems); - await tick(); - const listItemComponents = this.context.getListItemComponents(); - const firstListItem = getShadowElement(listItemComponents[0], LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - const lastListItem = getShadowElement(listItemComponents[2], LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - lastListItem.focus(); - dispatchKeyEvent(listItemComponents[1], 'keydown', 'ArrowDown', true); - expect(getActiveElement()).toBe(firstListItem); - }); - }); - - describe('list with string values', function(this: ITestContext) { - it('should set a single selected string value via attribute', function(this: ITestContext) { - this.context = setupTestContext(true, listStringItems); - const selectedValue = '1'; - this.context.component.setAttribute(LIST_CONSTANTS.attributes.SELECTED_VALUE, selectedValue); - const listItemComponents = this.context.getListItemComponents(); - expect(listItemComponents[0].selected).toBe(true); - expect(listItemComponents[1].selected).toBe(false); - }); - }); - - function setupTestContext(append = false, listItems: ITestListItemNumber[] | ITestListItemString[]): ITestListContext { - const fixture = document.createElement('div'); - fixture.id = 'list-test-fixture'; - const component = document.createElement(LIST_CONSTANTS.elementName) as IListComponent; - for (const item of listItems) { - const listItem = document.createElement(LIST_ITEM_CONSTANTS.elementName) as IListItemComponent; - listItem.value = item.value; - listItem.textContent = item.text; - component.appendChild(listItem); - const divider = document.createElement(DIVIDER_CONSTANTS.elementName) as IDividerComponent; - component.appendChild(divider); - } - fixture.appendChild(component); - if (append) document.body.appendChild(fixture); - return { - component, - getListItemComponents: () => Array.from(component.querySelectorAll(LIST_ITEM_CONSTANTS.elementName)), - append: () => document.body.appendChild(fixture), - destroy: () => removeElement(fixture) - }; - } -}); diff --git a/src/test/spec/menu/menu.spec.ts b/src/test/spec/menu/menu.spec.ts index 8a656e506..0551a19d6 100644 --- a/src/test/spec/menu/menu.spec.ts +++ b/src/test/spec/menu/menu.spec.ts @@ -332,8 +332,8 @@ describe('MenuComponent', function(this: ITestContext) { this.context.component.options = options; this.context.component.open = true; await timer(300); - const listItemHost = getShadowElement(getPopupListItem(5), LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - expect(listItemHost.classList.contains(LIST_ITEM_CONSTANTS.classes.DISABLED)).toBe(true); + + expect(getPopupListItem(5).hasAttribute(LIST_ITEM_CONSTANTS.attributes.DISABLED)).toBe(true); }); it(`should have selected class when option is set to selected and persistSelection is true`, async function(this: ITestContext) { @@ -344,8 +344,8 @@ describe('MenuComponent', function(this: ITestContext) { this.context.component.options = options; this.context.component.open = true; await timer(300); - const listItemHost = getShadowElement(getPopupListItem(5), LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - expect(listItemHost.classList.contains(LIST_ITEM_CONSTANTS.classes.SELECTED)).toBe(true); + + expect(getPopupListItem(5).hasAttribute(LIST_ITEM_CONSTANTS.attributes.SELECTED)).toBe(true); }); it(`should not have selected class when option is set to selected and persistSelection is false`, async function(this: ITestContext) { @@ -356,8 +356,8 @@ describe('MenuComponent', function(this: ITestContext) { this.context.component.options = options; this.context.component.open = true; await timer(300); - const listItemHost = getShadowElement(getPopupListItem(5), LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - expect(listItemHost.classList.contains(LIST_ITEM_CONSTANTS.classes.SELECTED)).toBe(false); + + expect(getPopupListItem(5).hasAttribute(LIST_ITEM_CONSTANTS.attributes.SELECTED)).toBe(false); }); it(`should not have selected class when option is set to selected and persistSelection is switched from true to false`, async function(this: ITestContext) { @@ -370,8 +370,8 @@ describe('MenuComponent', function(this: ITestContext) { this.context.component.persistSelection = false; this.context.component.open = true; await timer(300); - const listItemHost = getShadowElement(getPopupListItem(5), LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - expect(listItemHost.classList.contains(LIST_ITEM_CONSTANTS.classes.SELECTED)).toBe(false); + + expect(getPopupListItem(5).hasAttribute(LIST_ITEM_CONSTANTS.attributes.SELECTED)).toBe(false); }); it('should use option builder', async function(this: ITestContext) { @@ -447,8 +447,7 @@ describe('MenuComponent', function(this: ITestContext) { await timer(300); this.context.getToggleElement().dispatchEvent(new KeyboardEvent('keydown', { code: 'ArrowDown' })); - const listItem = getShadowElement(getPopupListItem(0), LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - expect(listItem.classList.contains(LIST_ITEM_CONSTANTS.classes.ACTIVE)).toBe(true); + expect(getPopupListItem(0).hasAttribute(LIST_ITEM_CONSTANTS.attributes.ACTIVE)).toBe(true); }); it('arrow up from the start should activate the last list element', async function(this: ITestContext) { @@ -459,8 +458,7 @@ describe('MenuComponent', function(this: ITestContext) { await timer(300); this.context.getToggleElement().dispatchEvent(new KeyboardEvent('keydown', { code: 'ArrowUp' })); - const listItem = getShadowElement(getPopupListItem(6), LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - expect(listItem.classList.contains(LIST_ITEM_CONSTANTS.classes.ACTIVE)).toBe(true); + expect(getPopupListItem(6).hasAttribute(LIST_ITEM_CONSTANTS.attributes.ACTIVE)).toBe(true); }); it('enter should select the list element when persistSelection is true', async function(this: ITestContext) { @@ -571,7 +569,7 @@ describe('MenuComponent', function(this: ITestContext) { this.context.component.open = true; await timer(300); - getShadowElement(getPopupListItem(0), LIST_ITEM_CONSTANTS.selectors.LIST_ITEM).dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + getPopupListItem(0).dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); await timer(300); @@ -684,7 +682,7 @@ describe('MenuComponent', function(this: ITestContext) { await tick(); const childMenuListItems = getChildMenuListItems(getPopupElement()); - childMenuListItems[1].shadowRoot!.querySelector(LIST_ITEM_CONSTANTS.selectors.LIST_ITEM)!.dispatchEvent(new MouseEvent('click')); + childMenuListItems[1].dispatchEvent(new MouseEvent('click')); expect(selectSpy).toHaveBeenCalledTimes(1); expect(selectSpy).toHaveBeenCalledWith(jasmine.objectContaining({ detail: { index: 1, value: EXPETED_SELECTION_VALUE, parentValue: options[1].value }})); diff --git a/src/test/spec/paginator/paginator.spec.ts b/src/test/spec/paginator/paginator.spec.ts index 6baec5be3..c3c40044f 100644 --- a/src/test/spec/paginator/paginator.spec.ts +++ b/src/test/spec/paginator/paginator.spec.ts @@ -292,8 +292,7 @@ describe('PaginatorComponent', function(this: ITestContext) { this.context.pageSizeSelect.open = true; const listItem = this.context.pageSizeSelect.popupElement?.querySelector(LIST_ITEM_CONSTANTS.elementName) as IListItemComponent; - const listItemRoot = getShadowElement(listItem, LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - listItemRoot.click(); + listItem.click(); await tick(); expect(listItem.value).toBe('5'); @@ -311,8 +310,7 @@ describe('PaginatorComponent', function(this: ITestContext) { this.context.pageSizeSelect.open = true; const listItem = this.context.pageSizeSelect.popupElement?.querySelector(LIST_ITEM_CONSTANTS.elementName) as IListItemComponent; - const listItemRoot = getShadowElement(listItem, LIST_ITEM_CONSTANTS.selectors.LIST_ITEM); - listItemRoot.click(); + listItem.click(); await tick(); expect(this.context.paginator.pageSize).toBe(Number(originalPageSize)); diff --git a/src/test/spec/select/select.spec.ts b/src/test/spec/select/select.spec.ts index 9558eb721..149423584 100644 --- a/src/test/spec/select/select.spec.ts +++ b/src/test/spec/select/select.spec.ts @@ -1487,8 +1487,8 @@ describe('SelectComponent', function(this: ITestContext) { _openDropdown(this.context.component); const listItems = Array.from(this.context.component.popupElement!.querySelectorAll(LIST_ITEM_CONSTANTS.elementName)) as IListItemComponent[]; - getShadowElement(listItems[0], LIST_ITEM_CONSTANTS.selectors.LIST_ITEM).click(); - getShadowElement(listItems[1], LIST_ITEM_CONSTANTS.selectors.LIST_ITEM).click(); + listItems[0].click(); + listItems[1].click(); expect(this.context.component.value).toEqual(['one', 'two']); expect(this.context.component.selectedIndex).toEqual([0, 1]); expect(listItems[0].selected).toBe(true); diff --git a/src/test/spec/time-picker/time-picker.spec.ts b/src/test/spec/time-picker/time-picker.spec.ts index 743267a4e..8f00e6c58 100644 --- a/src/test/spec/time-picker/time-picker.spec.ts +++ b/src/test/spec/time-picker/time-picker.spec.ts @@ -622,7 +622,7 @@ describe('TimePickerComponent', function(this: ITestContext) { await timer(POPUP_CONSTANTS.numbers.ANIMATION_DURATION); const listItems = this.context.getListItems(); - const matchingListItem = listItems.find(li => li.selected) as IListItemComponent; + const matchingListItem = listItems.find(li => li.selected) as IListItemComponent; expect(matchingListItem.value.time).toBe(timeMillis); }); @@ -667,7 +667,7 @@ describe('TimePickerComponent', function(this: ITestContext) { await timer(POPUP_CONSTANTS.numbers.ANIMATION_DURATION); const listItems = this.context.getListItems(); - const activeListItem = listItems.find(li => li.selected) as IListItemComponent; + const activeListItem = listItems.find(li => li.selected) as IListItemComponent; expect(activeListItem.value.time).toBe(timeMillis); }); @@ -701,7 +701,7 @@ describe('TimePickerComponent', function(this: ITestContext) { await timer(POPUP_CONSTANTS.numbers.ANIMATION_DURATION); const listItems = this.context.getListItems(); - const restrictedListItem = listItems.find(li => li.value.time === firstRestrictedTimeMillis) as IListItemComponent; + const restrictedListItem = listItems.find((li: IListItemComponent) => li.value.time === firstRestrictedTimeMillis) as IListItemComponent; expect(restrictedListItem.disabled).toBeTrue(); @@ -872,7 +872,7 @@ describe('TimePickerComponent', function(this: ITestContext) { this.context.inputElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'End' })); this.context.inputElement.dispatchEvent(new KeyboardEvent('keydown', { code: 'Enter' })); - const selectedListItem = listItems[listItems.length - 1]; + const selectedListItem = listItems[listItems.length - 1] as IListItemComponent; const selectedTimeString = millisToTimeString(selectedListItem.value.time, true, false); expect(changeSpy).toHaveBeenCalledOnceWith(jasmine.objectContaining({ detail: selectedTimeString })); @@ -923,10 +923,11 @@ describe('TimePickerComponent', function(this: ITestContext) { await timer(POPUP_CONSTANTS.numbers.ANIMATION_DURATION); const listItems = this.context.getListItems(); + const listItem = listItems[0] as IListItemComponent; - expect(listItems[0].value.time).toBeNull(); - expect(listItems[0].innerText).toBe('Now'); - expect(listItems[0].value.metadata).toBe('now'); + expect(listItem.value.time).toBeNull(); + expect(listItem.innerText).toBe('Now'); + expect(listItem.value.metadata).toBe('now'); }); it('should show "now" as the only option in the dropdown when showHourOptions is false and showNow is true and customOptions is empty', async function(this: ITestContext) { @@ -939,11 +940,12 @@ describe('TimePickerComponent', function(this: ITestContext) { await timer(POPUP_CONSTANTS.numbers.ANIMATION_DURATION); const listItems = this.context.getListItems(); + const firstListItem = listItems[0] as IListItemComponent; expect(listItems.length).toBe(1); - expect(listItems[0].value.time).toBeNull(); - expect(listItems[0].innerText).toBe('Now'); - expect(listItems[0].value.metadata).toBe('now'); + expect(firstListItem.value.time).toBeNull(); + expect(firstListItem.innerText).toBe('Now'); + expect(firstListItem.value.metadata).toBe('now'); }); it('should should not show dropdown when showNow is false and showHourOptions is false and customOptions is empty', async function(this: ITestContext) { @@ -973,15 +975,17 @@ describe('TimePickerComponent', function(this: ITestContext) { expect(this.context.component.customOptions).toEqual(customOptions); - expect(listItems[0].innerText).toBe(customOptions[0].label); - expect(listItems[0].value.time).toBeNull(); - expect(listItems[0].value.metadata).toBe(customOptions[0].value); - expect(listItems[0].value.isCustom).toBeTrue(); + const firstListItem = listItems[0] as IListItemComponent; + expect(firstListItem.innerText).toBe(customOptions[0].label); + expect(firstListItem.value.time).toBeNull(); + expect(firstListItem.value.metadata).toBe(customOptions[0].value); + expect(firstListItem.value.isCustom).toBeTrue(); - expect(listItems[1].innerText).toBe(customOptions[1].label); - expect(listItems[1].value.time).toBeNull(); - expect(listItems[1].value.metadata).toBe(customOptions[1].value); - expect(listItems[1].value.isCustom).toBeTrue(); + const secondListItem = listItems[1] as IListItemComponent; + expect(secondListItem.innerText).toBe(customOptions[1].label); + expect(secondListItem.value.time).toBeNull(); + expect(secondListItem.value.metadata).toBe(customOptions[1].value); + expect(secondListItem.value.isCustom).toBeTrue(); }); it('should call toMilliseconds function when selecting custom options', async function(this: ITestContext) { @@ -1014,7 +1018,7 @@ describe('TimePickerComponent', function(this: ITestContext) { const firstListItemMillis = timeStringToMillis(min, true, false); const lastListItemMillis = timeStringToMillis(max, true, false); - const listItems = this.context.getListItems(); + const listItems = this.context.getListItems() as IListItemComponent[]; expect(listItems[0].value.time).toBe(firstListItemMillis); expect(listItems[listItems.length - 1].value.time).toBe(lastListItemMillis); From 644dcd1d11df7a3718af1fdfbf3b71b744cb15eb Mon Sep 17 00:00:00 2001 From: Sam Richardson Date: Thu, 19 Oct 2023 16:19:53 -0400 Subject: [PATCH 28/38] chore(button-area): add build file and jsdoc (#412) * chore(button-area): add build file and jsdoc * chore(button-area): remove stylesheets from build file --- src/lib/button-area/build.json | 4 ++++ src/lib/button-area/button-area.ts | 5 +++++ 2 files changed, 9 insertions(+) create mode 100644 src/lib/button-area/build.json diff --git a/src/lib/button-area/build.json b/src/lib/button-area/build.json new file mode 100644 index 000000000..d8638d477 --- /dev/null +++ b/src/lib/button-area/build.json @@ -0,0 +1,4 @@ +{ + "$schema": "../../../node_modules/@tylertech/forge-cli/build-schema.json", + "extends": "../build.json" +} \ No newline at end of file diff --git a/src/lib/button-area/button-area.ts b/src/lib/button-area/button-area.ts index 4fbba24a2..dbef38258 100644 --- a/src/lib/button-area/button-area.ts +++ b/src/lib/button-area/button-area.ts @@ -17,6 +17,11 @@ declare global { } } +/** + * The custom element class behind the `` element. + * + * @tag forge-button-area + */ @CustomElement({ name: BUTTON_AREA_CONSTANTS.elementName, dependencies: [ From cddbf4a39f74c7650595b8882f945096c59fa497 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 20:23:34 +0000 Subject: [PATCH 29/38] Update CHANGELOG.md [skip ci] --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a986484a9..5ee44319a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# v2.19.1 (Thu Oct 19 2023) + +#### 🐛 Bug Fix + +- chore(button-area): add build file and jsdoc [#412](https://github.com/tyler-technologies-oss/forge/pull/412) ([@samrichardsontylertech](https://github.com/samrichardsontylertech)) + +#### Authors: 1 + +- Sam Richardson ([@samrichardsontylertech](https://github.com/samrichardsontylertech)) + +--- + # v2.19.0 (Thu Oct 12 2023) #### 🚀 Enhancement From 11d573d64eeaa0e670641c6577979d926ebe7b53 Mon Sep 17 00:00:00 2001 From: GitHub Actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 20:23:35 +0000 Subject: [PATCH 30/38] Bump version to: 2.19.1 [skip ci] --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cd87188f0..e31a59484 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tylertech/forge", - "version": "2.19.0", + "version": "2.19.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@tylertech/forge", - "version": "2.19.0", + "version": "2.19.1", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 2510b3c7f..8ae414e72 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tylertech/forge", - "version": "2.19.0", + "version": "2.19.1", "description": "Tyler Forge™ Web Components library", "license": "Apache-2.0", "author": "Tyler Technologies, Inc.", From 9b30506fa2ba66161d5a64cca6e4873a1429cc65 Mon Sep 17 00:00:00 2001 From: "Nichols, Kieran" Date: Mon, 23 Oct 2023 16:25:37 -0400 Subject: [PATCH 31/38] chore: fix dev site switches [skip ci] --- src/dev/pages/app-bar/app-bar.ts | 6 ++-- src/dev/pages/autocomplete/autocomplete.ts | 26 ++++++++--------- src/dev/pages/avatar/avatar.ts | 2 +- src/dev/pages/banner/banner.ts | 8 +++--- src/dev/pages/button-area/button-area.ts | 2 +- src/dev/pages/button-toggle/button-toggle.ts | 12 ++++---- src/dev/pages/button/button.ts | 4 +-- src/dev/pages/calendar/calendar.ts | 4 +-- src/dev/pages/card/card.ts | 2 +- src/dev/pages/chip-field/chip-field.ts | 8 +++--- src/dev/pages/chips/chips.ts | 4 +-- .../circular-progress/circular-progress.ts | 6 ++-- src/dev/pages/date-picker/date-picker.ts | 20 ++++++------- .../date-range-picker/date-range-picker.ts | 18 ++++++------ src/dev/pages/dialog/dialog.ts | 8 +++--- .../pages/expansion-panel/expansion-panel.ts | 2 +- src/dev/pages/file-picker/file-picker.ts | 4 +-- .../keyboard-shortcut/keyboard-shortcut.ts | 10 +++---- src/dev/pages/menu/menu.ts | 10 +++---- src/dev/pages/paginator/paginator.ts | 12 ++++---- src/dev/pages/select/select.ts | 22 +++++++-------- src/dev/pages/slider/slider.ts | 12 ++++---- src/dev/pages/stack/stack.ts | 6 ++-- src/dev/pages/state-layer/state-layer.ts | 6 ++-- src/dev/pages/stepper/stepper.ts | 6 ++-- src/dev/pages/table/table.ts | 26 ++++++++--------- src/dev/pages/tabs/tabs.ts | 20 ++++++------- src/dev/pages/text-field/text-field.ts | 20 ++++++------- src/dev/pages/time-picker/time-picker.ts | 28 +++++++++---------- src/dev/pages/toolbar/toolbar.ts | 8 +++--- src/dev/pages/tooltip/tooltip.ts | 2 +- src/stories/src/components/switch/switch.mdx | 2 +- 32 files changed, 163 insertions(+), 163 deletions(-) diff --git a/src/dev/pages/app-bar/app-bar.ts b/src/dev/pages/app-bar/app-bar.ts index f78c11d78..07e6542f2 100644 --- a/src/dev/pages/app-bar/app-bar.ts +++ b/src/dev/pages/app-bar/app-bar.ts @@ -78,7 +78,7 @@ appBarSearch.addEventListener('forge-app-bar-search-input', ({ detail }) => { }); const useProfileCardBuilderToggle = document.querySelector('#app-bar-profile-card-builder-toggle') as ISwitchComponent; -useProfileCardBuilderToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +useProfileCardBuilderToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { appBarProfileButton.profileCardBuilder = selected ? profileCardBuilder : undefined; }); @@ -113,7 +113,7 @@ function profileCardBuilder(): HTMLElement { } const appBarRaisedToggle = document.querySelector('#app-bar-raised-toggle') as ISwitchComponent; -appBarRaisedToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +appBarRaisedToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { appBar.raised = selected; pageAppBar.raised = selected; }); @@ -135,7 +135,7 @@ appBarThemeSelect.addEventListener('change', () => { }); const showAppBarTabsToggle = document.querySelector('#app-bar-show-tabs-toggle') as ISwitchComponent; -showAppBarTabsToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +showAppBarTabsToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { if (selected) { appBarTabs.style.removeProperty('display'); } else { diff --git a/src/dev/pages/autocomplete/autocomplete.ts b/src/dev/pages/autocomplete/autocomplete.ts index 10f8f07a4..ae523bb21 100644 --- a/src/dev/pages/autocomplete/autocomplete.ts +++ b/src/dev/pages/autocomplete/autocomplete.ts @@ -39,7 +39,7 @@ autocomplete.addEventListener('forge-autocomplete-select', ({ detail }) => { // Options const multipleToggle = document.querySelector('#autocomplete-multiple') as HTMLInputElement; -multipleToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +multipleToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { autocomplete.multiple = selected; }); @@ -59,58 +59,58 @@ optionLimitInput.addEventListener('input', () => { }); const filterOnFocusToggle = document.querySelector('#autocomplete-filter-on-focus') as HTMLInputElement; -filterOnFocusToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +filterOnFocusToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { autocomplete.filterOnFocus = selected; }); const filterFocusFirstToggle = document.querySelector('#autocomplete-filter-focus-first') as HTMLInputElement; -filterFocusFirstToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +filterFocusFirstToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { autocomplete.filterFocusFirst = selected; }); const itemBuilderToggle = document.querySelector('#autocomplete-item-builder') as HTMLInputElement; -itemBuilderToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +itemBuilderToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { autocomplete.optionBuilder = selected ? itemBuilder : undefined; }); const allowUnmatchedToggle = document.querySelector('#autocomplete-allow-unmatched') as HTMLInputElement; -allowUnmatchedToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +allowUnmatchedToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { autocomplete.allowUnmatched = selected; }); const selectedTextBuilderToggle = document.querySelector('#autocomplete-selected-text-builder') as HTMLInputElement; -selectedTextBuilderToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +selectedTextBuilderToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { autocomplete.selectedTextBuilder = selected ? selectedTextBuilder : undefined; }); const scrollObserverToggle = document.querySelector('#autocomplete-scroll-observer') as HTMLInputElement; -scrollObserverToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +scrollObserverToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { autocomplete.observeScroll = selected; }); const syncPopupWidthToggle = document.querySelector('#autocomplete-sync-popup-width') as HTMLInputElement; -syncPopupWidthToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +syncPopupWidthToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { autocomplete.syncPopupWidth = selected; }); const headerBuilderToggle = document.querySelector('#autocomplete-header-builder') as HTMLInputElement; -headerBuilderToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +headerBuilderToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { autocomplete.popupHeaderBuilder = selected ? headerBuilderCallback : undefined; }); const footerBuilderToggle = document.querySelector('#autocomplete-footer-builder') as HTMLInputElement; -footerBuilderToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +footerBuilderToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { autocomplete.popupFooterBuilder = selected ? footerBuilderCallback : undefined; }); const simulateAsyncToggle = document.querySelector('#autocomplete-simulate-async') as HTMLInputElement; -simulateAsyncToggle.addEventListener('forge-switch-select', ({ detail: selected }) => asyncFilter = selected); +simulateAsyncToggle.addEventListener('forge-switch-change', ({ detail: selected }) => asyncFilter = selected); const groupToggle = document.querySelector('#autocomplete-group') as HTMLInputElement; -groupToggle.addEventListener('forge-switch-select', ({ detail: selected }) => useGroupedData = selected); +groupToggle.addEventListener('forge-switch-change', ({ detail: selected }) => useGroupedData = selected); const groupHeaderBuilderToggle = document.querySelector('#autocomplete-group-header-builder') as HTMLInputElement; -groupHeaderBuilderToggle.addEventListener('forge-switch-select', ({ detail: selected }) => useGroupHeaderBuilder = selected); +groupHeaderBuilderToggle.addEventListener('forge-switch-change', ({ detail: selected }) => useGroupHeaderBuilder = selected); function itemBuilder(option: IListDropdownOption, filterText: string, _listItem: IListItemComponent): HTMLElement { diff --git a/src/dev/pages/avatar/avatar.ts b/src/dev/pages/avatar/avatar.ts index 9eb05b053..939fd1baa 100644 --- a/src/dev/pages/avatar/avatar.ts +++ b/src/dev/pages/avatar/avatar.ts @@ -9,7 +9,7 @@ function getAvatarElements(): NodeListOf { } const autoColorToggle = document.querySelector('#auto-color-checkbox') as ISwitchComponent; -autoColorToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +autoColorToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { const avatars = getAvatarElements(); avatars.forEach(avatar => avatar.autoColor = selected); }); diff --git a/src/dev/pages/banner/banner.ts b/src/dev/pages/banner/banner.ts index 89aeaf2f9..a9c9f7440 100644 --- a/src/dev/pages/banner/banner.ts +++ b/src/dev/pages/banner/banner.ts @@ -24,22 +24,22 @@ themeSelect.addEventListener('change', ({ detail: theme }) => { }); const dismissToggle = document.querySelector('#dismissed-toggle') as ISwitchComponent; -dismissToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +dismissToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { banner.dismissed = selected; }); const showLeadingIconToggle = document.querySelector('#show-leading-icon-toggle') as ISwitchComponent; -showLeadingIconToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +showLeadingIconToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { leadingIcon.style.display = selected ? 'block' : 'none'; }); const showDismissToggle = document.querySelector('#show-dismiss-toggle') as ISwitchComponent; -showDismissToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +showDismissToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { banner.canDismiss = selected; }); const useMoreTextToggle = document.querySelector('#use-more-text-toggle') as ISwitchComponent; -useMoreTextToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +useMoreTextToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { textEl.textContent = selected ? 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Reprehenderit explicabo labore soluta ex culpa, consectetur possimus quidem ullam voluptas est facilis quasi enim error doloribus omnis recusandae! Dolore, eaque ipsa!' : 'Lorem, ipsum dolor sit amet consectetur adipisicing elit.'; diff --git a/src/dev/pages/button-area/button-area.ts b/src/dev/pages/button-area/button-area.ts index 0b7fad6dc..b2dbaab47 100644 --- a/src/dev/pages/button-area/button-area.ts +++ b/src/dev/pages/button-area/button-area.ts @@ -22,7 +22,7 @@ expansionPanel.addEventListener('forge-expansion-panel-toggle', (event: CustomEv const disabledToggle = document.querySelector('#disabled-switch') as ISwitchComponent; -disabledToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +disabledToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { buttonArea.disabled = selected; expansionPanelButtonArea.disabled = selected; }); diff --git a/src/dev/pages/button-toggle/button-toggle.ts b/src/dev/pages/button-toggle/button-toggle.ts index 5a5e91a73..cb0604d71 100644 --- a/src/dev/pages/button-toggle/button-toggle.ts +++ b/src/dev/pages/button-toggle/button-toggle.ts @@ -30,37 +30,37 @@ buttonToggleGroupStatic.addEventListener('forge-button-toggle-select', ({ detail }); const multipleToggle = document.querySelector('#button-toggle-multiple') as ISwitchComponent; -multipleToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +multipleToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { buttonToggleGroupStatic.multiple = selected; buttonToggleGroupDynamic.multiple = selected; }); const mandatoryToggle = document.querySelector('#button-toggle-mandatory') as ISwitchComponent; -mandatoryToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +mandatoryToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { buttonToggleGroupStatic.mandatory = selected; buttonToggleGroupDynamic.mandatory = selected; }); const verticalToggle = document.querySelector('#button-toggle-vertical') as ISwitchComponent; -verticalToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +verticalToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { buttonToggleGroupStatic.vertical = selected; buttonToggleGroupDynamic.vertical = selected; }); const stretchToggle = document.querySelector('#button-toggle-stretch') as ISwitchComponent; -stretchToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +stretchToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { buttonToggleGroupStatic.stretch = selected; buttonToggleGroupDynamic.stretch = selected; }); const denseToggle = document.querySelector('#button-toggle-dense') as ISwitchComponent; -denseToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +denseToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { buttonToggleGroupStatic.dense = selected; buttonToggleGroupDynamic.dense = selected; }); const disabledToggle = document.querySelector('#button-toggle-disabled') as ISwitchComponent; -disabledToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +disabledToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { buttonToggleGroupStatic.disabled = selected; buttonToggleGroupDynamic.disabled = selected; }); diff --git a/src/dev/pages/button/button.ts b/src/dev/pages/button/button.ts index 449f17694..fdba833e8 100644 --- a/src/dev/pages/button/button.ts +++ b/src/dev/pages/button/button.ts @@ -22,7 +22,7 @@ function getButtonElements(): NodeListOf { } const disabledToggle = document.querySelector('#disabled-switch') as ISwitchComponent; -disabledToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +disabledToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { const buttons = getButtonElements(); buttons.forEach(forgeButton => { const buttonEl = forgeButton.querySelector('button') as HTMLButtonElement; @@ -31,7 +31,7 @@ disabledToggle.addEventListener('forge-switch-select', ({ detail: selected }) => }); const denseToggle = document.querySelector('#dense-switch') as ISwitchComponent; -denseToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +denseToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { const buttons = getButtonElements(); buttons.forEach(forgeButton => { const isChecked = selected; diff --git a/src/dev/pages/calendar/calendar.ts b/src/dev/pages/calendar/calendar.ts index 2fb20ab93..a8f84ebd6 100644 --- a/src/dev/pages/calendar/calendar.ts +++ b/src/dev/pages/calendar/calendar.ts @@ -7,12 +7,12 @@ import type { ISelectComponent } from '@tylertech/forge/select'; const calendar = document.querySelector('forge-calendar') as ICalendarComponent; const showTodayToggle = document.querySelector('#calendar-today') as ISwitchComponent; -showTodayToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +showTodayToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { calendar.showToday = selected; }); const readonlyToggle = document.querySelector('#calendar-readonly') as ISwitchComponent; -readonlyToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +readonlyToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { calendar.readonly = selected; }); diff --git a/src/dev/pages/card/card.ts b/src/dev/pages/card/card.ts index 0e40da1d4..afbe797d5 100644 --- a/src/dev/pages/card/card.ts +++ b/src/dev/pages/card/card.ts @@ -11,5 +11,5 @@ import type { ICardComponent, ISwitchComponent } from '@tylertech/forge'; const card = document.querySelector('.demo-card') as ICardComponent; const raisedToggle = document.querySelector('#opt-card-raised') as ISwitchComponent; -raisedToggle.addEventListener('forge-switch-select', ({ detail: selected }) => card.raised = selected); +raisedToggle.addEventListener('forge-switch-change', ({ detail: selected }) => card.raised = selected); diff --git a/src/dev/pages/chip-field/chip-field.ts b/src/dev/pages/chip-field/chip-field.ts index 6dd1ebd13..192fcd659 100644 --- a/src/dev/pages/chip-field/chip-field.ts +++ b/src/dev/pages/chip-field/chip-field.ts @@ -23,10 +23,10 @@ const denseToggle = document.querySelector('#opt-dense') as ISwitchComponent; const populateButton = document.querySelector('#opt-btn-populate') as HTMLButtonElement; const clearButton = document.querySelector('#opt-btn-clear') as HTMLButtonElement; -requiredToggle.addEventListener('forge-switch-select', updateRequiredState); -invalidToggle.addEventListener('forge-switch-select', updateInvalidState); -disabledToggle.addEventListener('forge-switch-select', updateDisabledState); -denseToggle.addEventListener('forge-switch-select', updateDenseState); +requiredToggle.addEventListener('forge-switch-change', updateRequiredState); +invalidToggle.addEventListener('forge-switch-change', updateInvalidState); +disabledToggle.addEventListener('forge-switch-change', updateDisabledState); +denseToggle.addEventListener('forge-switch-change', updateDenseState); populateButton.addEventListener('click', () => populateMembers(45)); clearButton.addEventListener('click', removeAllMembers); diff --git a/src/dev/pages/chips/chips.ts b/src/dev/pages/chips/chips.ts index c155f16e1..b0fa40c3c 100644 --- a/src/dev/pages/chips/chips.ts +++ b/src/dev/pages/chips/chips.ts @@ -44,7 +44,7 @@ refreshButton.addEventListener('click', () => { }); // Dense toggling -chipsDenseToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +chipsDenseToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { const chipSets = document.querySelectorAll('forge-chip-set') as NodeListOf; for (const chipSet of chipSets) { chipSet.dense = selected; @@ -52,7 +52,7 @@ chipsDenseToggle.addEventListener('forge-switch-select', ({ detail: selected }) }); // Disabled toggling -chipsDisabledToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +chipsDisabledToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { const chipSets = document.querySelectorAll('forge-chip-set') as NodeListOf; for (const chipSet of chipSets) { chipSet.disabled = selected; diff --git a/src/dev/pages/circular-progress/circular-progress.ts b/src/dev/pages/circular-progress/circular-progress.ts index bdc7b9419..546af10c6 100644 --- a/src/dev/pages/circular-progress/circular-progress.ts +++ b/src/dev/pages/circular-progress/circular-progress.ts @@ -7,7 +7,7 @@ const circularProgress = document.querySelector('forge-circular-progress#circula let determinateIntervalTimer: number | undefined; const showTrackToggle = document.getElementById('opt-show-track') as ISwitchComponent; -showTrackToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +showTrackToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { if (selected) { circularProgress.style.removeProperty('--forge-circular-progress-track-background'); } else { @@ -16,7 +16,7 @@ showTrackToggle.addEventListener('forge-switch-select', ({ detail: selected }) = }); const showPercentToggle = document.getElementById('opt-show-percent') as ISwitchComponent; -showPercentToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +showPercentToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { circularProgress.textContent = selected ? '0%' : ''; }); @@ -30,7 +30,7 @@ themeSelect.addEventListener('change', ({ detail }) => { }); const determinateToggle = document.getElementById('opt-determinate') as ISwitchComponent; -determinateToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +determinateToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { circularProgress.determinate = selected; showPercentToggle.disabled = !circularProgress.determinate; diff --git a/src/dev/pages/date-picker/date-picker.ts b/src/dev/pages/date-picker/date-picker.ts index 938e4e553..904b96b1f 100644 --- a/src/dev/pages/date-picker/date-picker.ts +++ b/src/dev/pages/date-picker/date-picker.ts @@ -42,47 +42,47 @@ disabledDaysSelect.addEventListener('change', () => { }); const maskedToggle = document.getElementById('opt-masked') as ISwitchComponent; -maskedToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +maskedToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { datePicker.masked = selected; }); const showMaskFormat = document.getElementById('opt-show-mask-format') as ISwitchComponent; -showMaskFormat.addEventListener('forge-switch-select', ({ detail: selected }) => { +showMaskFormat.addEventListener('forge-switch-change', ({ detail: selected }) => { datePicker.showMaskFormat = selected; }); const minDateToggle = document.getElementById('opt-min-date') as ISwitchComponent; -minDateToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +minDateToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { datePicker.min = selected ? new Date(Date.now() - 86400000) : null; }); const maxDateToggle = document.getElementById('opt-max-date') as ISwitchComponent; -maxDateToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +maxDateToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { datePicker.max = selected ? new Date(Date.now() + 86400000) : null; }); const disabledToggle = document.getElementById('opt-disabled') as ISwitchComponent; -disabledToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +disabledToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { datePicker.disabled = selected; }); const allowInvalidDateToggle = document.getElementById('opt-allow-invalid-date') as ISwitchComponent; -allowInvalidDateToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +allowInvalidDateToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { datePicker.allowInvalidDate = selected; }); const showTodayToggle = document.getElementById('opt-show-today') as ISwitchComponent; -showTodayToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +showTodayToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { datePicker.showToday = selected; }); const showClearToggle = document.getElementById('opt-show-clear') as ISwitchComponent; -showClearToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +showClearToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { datePicker.showClear = selected; }); const disableDayCallbackToggle = document.getElementById('opt-disable-day-callback') as ISwitchComponent; -disableDayCallbackToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +disableDayCallbackToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { if (selected) { datePicker.disableDayCallback = (date) => date.toLocaleDateString() === new Date().toLocaleDateString(); } else { @@ -96,7 +96,7 @@ toggleDropdownOpenButton.addEventListener('click', () => { }); const customCallbackToggle = document.getElementById('opt-custom-callbacks') as ISwitchComponent; -customCallbackToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +customCallbackToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { if (selected) { datePickerInput.placeholder = 'yyyy-mm-dd'; datePicker.maskFormat = 'YYYY-MM-DD'; diff --git a/src/dev/pages/date-range-picker/date-range-picker.ts b/src/dev/pages/date-range-picker/date-range-picker.ts index 8774bfd54..20241985f 100644 --- a/src/dev/pages/date-range-picker/date-range-picker.ts +++ b/src/dev/pages/date-range-picker/date-range-picker.ts @@ -45,47 +45,47 @@ disabledDaysSelect.addEventListener('change', () => { }); const maskedToggle = document.getElementById('opt-masked') as ISwitchComponent; -maskedToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +maskedToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { dateRangePicker.masked = selected; }); const showMaskFormat = document.getElementById('opt-show-mask-format') as ISwitchComponent; -showMaskFormat.addEventListener('forge-switch-select', ({ detail: selected }) => { +showMaskFormat.addEventListener('forge-switch-change', ({ detail: selected }) => { dateRangePicker.showMaskFormat = selected; }); const minDateToggle = document.getElementById('opt-min-date') as ISwitchComponent; -minDateToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +minDateToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { dateRangePicker.min = selected ? new Date(Date.now() - 86400000) : null; }); const maxDateToggle = document.getElementById('opt-max-date') as ISwitchComponent; -maxDateToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +maxDateToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { dateRangePicker.max = selected ? new Date(Date.now() + 86400000) : null; }); const disabledToggle = document.getElementById('opt-disabled') as ISwitchComponent; -disabledToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +disabledToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { dateRangePicker.disabled = selected; }); const allowInvalidDateToggle = document.getElementById('opt-allow-invalid-date') as ISwitchComponent; -allowInvalidDateToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +allowInvalidDateToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { dateRangePicker.allowInvalidDate = selected; }); const dateRangePickerShowTodayToggle = document.querySelector('#opt-show-today') as ISwitchComponent; -dateRangePickerShowTodayToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +dateRangePickerShowTodayToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { dateRangePicker.showToday = selected; }); const dateRangePickerShowClearToggle = document.querySelector('#opt-show-clear') as ISwitchComponent; -dateRangePickerShowClearToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +dateRangePickerShowClearToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { dateRangePicker.showClear = selected; }); const disableDayCallbackToggle = document.getElementById('opt-disable-day-callback') as ISwitchComponent; -disableDayCallbackToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +disableDayCallbackToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { if (selected) { dateRangePicker.disableDayCallback = (date) => date.toLocaleDateString() === new Date().toLocaleDateString(); } else { diff --git a/src/dev/pages/dialog/dialog.ts b/src/dev/pages/dialog/dialog.ts index 2dc7d0c40..e1c705daf 100644 --- a/src/dev/pages/dialog/dialog.ts +++ b/src/dev/pages/dialog/dialog.ts @@ -36,15 +36,15 @@ cancelButton.addEventListener('click', () => inlineDialog.hide()); const closeButton = inlineDialog.querySelector('#close-button'); closeButton.addEventListener('click', () => inlineDialog.hide()); -fullscreenToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +fullscreenToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { inlineDialog.fullscreen = selected; }); -moveableToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +moveableToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { inlineDialog.moveable = selected; }); -preventMoveToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +preventMoveToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { if (selected) { inlineDialog.addEventListener('forge-dialog-move', onPreventMove); } else { @@ -52,7 +52,7 @@ preventMoveToggle.addEventListener('forge-switch-select', ({ detail: selected }) } }); -customPositionToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +customPositionToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { inlineDialog.positionX = selected ? 100 : undefined; inlineDialog.positionY = selected ? 100 : undefined; }); diff --git a/src/dev/pages/expansion-panel/expansion-panel.ts b/src/dev/pages/expansion-panel/expansion-panel.ts index 6dab18adc..6c2bd318e 100644 --- a/src/dev/pages/expansion-panel/expansion-panel.ts +++ b/src/dev/pages/expansion-panel/expansion-panel.ts @@ -9,7 +9,7 @@ const basicExpansionPanel = document.querySelector('#expansion-panel-basic') as const cardExpansionPanel = document.querySelector('#expansion-panel-card') as IExpansionPanelComponent; const useAnimationToggle = document.getElementById('opt-use-animations') as ISwitchComponent; -useAnimationToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +useAnimationToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { basicExpansionPanel.useAnimations = selected; cardExpansionPanel.useAnimations = selected; }); diff --git a/src/dev/pages/file-picker/file-picker.ts b/src/dev/pages/file-picker/file-picker.ts index 7c9a330e6..9c8744369 100644 --- a/src/dev/pages/file-picker/file-picker.ts +++ b/src/dev/pages/file-picker/file-picker.ts @@ -60,12 +60,12 @@ maxSizeTextField.addEventListener('input', () => { }); -multipleCheckbox.addEventListener('forge-switch-select', ({ detail: selected }) => { +multipleCheckbox.addEventListener('forge-switch-change', ({ detail: selected }) => { filePicker.multiple = selected; filePickerCompact.multiple = selected; }); -disabledCheckbox.addEventListener('forge-switch-select', ({ detail: selected }) => { +disabledCheckbox.addEventListener('forge-switch-change', ({ detail: selected }) => { filePicker.disabled = selected; filePickerCompact.disabled = selected; }); diff --git a/src/dev/pages/keyboard-shortcut/keyboard-shortcut.ts b/src/dev/pages/keyboard-shortcut/keyboard-shortcut.ts index 65d748803..5e01f4989 100644 --- a/src/dev/pages/keyboard-shortcut/keyboard-shortcut.ts +++ b/src/dev/pages/keyboard-shortcut/keyboard-shortcut.ts @@ -28,31 +28,31 @@ keyTextField.addEventListener('change', () => { }); const globalToggle = document.querySelector('#opt-global') as ISwitchComponent; -globalToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +globalToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { buttonShortcut.global = selected; textFieldShortcut.global = selected; }); const allowWhileTypingToggle = document.querySelector('#opt-allow-while-typing') as ISwitchComponent; -allowWhileTypingToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +allowWhileTypingToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { buttonShortcut.allowWhileTyping = selected; textFieldShortcut.allowWhileTyping = selected; }); const preventDefaultToggle = document.querySelector('#opt-prevent-default') as ISwitchComponent; -preventDefaultToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +preventDefaultToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { buttonShortcut.preventDefault = selected; textFieldShortcut.preventDefault = selected; }); const useCodeToggle = document.querySelector('#opt-use-code') as ISwitchComponent; -useCodeToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +useCodeToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { buttonShortcut.useCode = selected; textFieldShortcut.useCode = selected; }); const disabledToggle = document.querySelector('#opt-disabled') as ISwitchComponent; -disabledToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +disabledToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { buttonShortcut.disabled = selected; textFieldShortcut.disabled = selected; }); diff --git a/src/dev/pages/menu/menu.ts b/src/dev/pages/menu/menu.ts index 96451b3f3..584cc3644 100644 --- a/src/dev/pages/menu/menu.ts +++ b/src/dev/pages/menu/menu.ts @@ -90,15 +90,15 @@ menu.addEventListener('forge-menu-select', evt => { console.log('[forge-menu-select]', evt.detail); }); -complexToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +complexToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { setOptions(selected); }); -denseToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +denseToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { menu.dense = selected; }); -asyncToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +asyncToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { if (selected) { menu.options = asyncOptions; } else { @@ -106,11 +106,11 @@ asyncToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { } }); -optionBuilderToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +optionBuilderToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { menu.optionBuilder = selected ? optionBuilderCallback : undefined; }); -allowPersistentSelectionToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +allowPersistentSelectionToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { menu.persistSelection = selected; setSelectedButton.disabled = !selected; }); diff --git a/src/dev/pages/paginator/paginator.ts b/src/dev/pages/paginator/paginator.ts index bb6f58845..f08c43795 100644 --- a/src/dev/pages/paginator/paginator.ts +++ b/src/dev/pages/paginator/paginator.ts @@ -11,26 +11,26 @@ const showFirstLastToggle = document.getElementById('opt-show-first-last') as IS const disabledToggle = document.getElementById('opt-disabled') as ISwitchComponent; const alternativeToggle = document.getElementById('opt-alternative') as ISwitchComponent; -showPageSizeOptsToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +showPageSizeOptsToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { paginator.pageSizeOptions = selected ? PAGINATOR_CONSTANTS.numbers.DEFAULT_PAGE_SIZE_OPTIONS : false; }); -showLabelToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +showLabelToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { paginator.label = selected ? PAGINATOR_CONSTANTS.strings.DEFAULT_LABEL : null; }); -showFirstToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +showFirstToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { paginator.first = selected; }); -showFirstLastToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +showFirstLastToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { paginator.firstLast = selected; }); -disabledToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +disabledToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { paginator.disabled = selected; }); -alternativeToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +alternativeToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { paginator.alternative = selected; }); diff --git a/src/dev/pages/select/select.ts b/src/dev/pages/select/select.ts index 302ea95f4..75dd38508 100644 --- a/src/dev/pages/select/select.ts +++ b/src/dev/pages/select/select.ts @@ -47,49 +47,49 @@ optLabel.addEventListener('input', () => { optPlaceholder.addEventListener('input', () => { select.placeholder = optPlaceholder.value; }); -optShowLeading.addEventListener('forge-switch-select', ({ detail: selected }) => { +optShowLeading.addEventListener('forge-switch-change', ({ detail: selected }) => { if (selected) { select.appendChild(leadingEl); } else { leadingEl.remove(); } }); -optShowAddonEnd.addEventListener('forge-switch-select', ({ detail: selected }) => { +optShowAddonEnd.addEventListener('forge-switch-change', ({ detail: selected }) => { if (selected) { select.appendChild(addonEndEl); } else { addonEndEl.remove(); } }); -optShowHelperText.addEventListener('forge-switch-select', ({ detail: selected }) => { +optShowHelperText.addEventListener('forge-switch-change', ({ detail: selected }) => { if (selected) { select.appendChild(helperTextEl); } else { helperTextEl.remove(); } }); -optMultiple.addEventListener('forge-switch-select', ({ detail: selected }) => { +optMultiple.addEventListener('forge-switch-change', ({ detail: selected }) => { select.multiple = selected; }); -optFloatLabel.addEventListener('forge-switch-select', ({ detail: selected }) => { +optFloatLabel.addEventListener('forge-switch-change', ({ detail: selected }) => { select.floatLabelType = selected ? 'always' : 'auto'; }); -optRequired.addEventListener('forge-switch-select', ({ detail: selected }) => { +optRequired.addEventListener('forge-switch-change', ({ detail: selected }) => { select.required = selected; }); -optDisabled.addEventListener('forge-switch-select', ({ detail: selected }) => { +optDisabled.addEventListener('forge-switch-change', ({ detail: selected }) => { select.disabled = selected; }); -optInvalid.addEventListener('forge-switch-select', ({ detail: selected }) => { +optInvalid.addEventListener('forge-switch-change', ({ detail: selected }) => { select.invalid = selected; }); -optRounded.addEventListener('forge-switch-select', ({ detail: selected }) => { +optRounded.addEventListener('forge-switch-change', ({ detail: selected }) => { select.shape = selected ? 'rounded' : 'default'; }); -optOptionBuilder.addEventListener('forge-switch-select', ({ detail: selected }) => { +optOptionBuilder.addEventListener('forge-switch-change', ({ detail: selected }) => { select.optionBuilder = selected ? optionBuilder : undefined; }); -optSelectedTextBuilder.addEventListener('forge-switch-select', ({ detail: selected }) => { +optSelectedTextBuilder.addEventListener('forge-switch-change', ({ detail: selected }) => { select.selectedTextBuilder = selected ? selectedTextBuilder : undefined; }); diff --git a/src/dev/pages/slider/slider.ts b/src/dev/pages/slider/slider.ts index 60e25ec14..dc30b1c54 100644 --- a/src/dev/pages/slider/slider.ts +++ b/src/dev/pages/slider/slider.ts @@ -22,21 +22,21 @@ const stepInput = document.querySelector('#slider-step') as HTMLInputElement; stepInput.addEventListener('change', ({ target }) => slider.step = (target as HTMLInputElement).valueAsNumber); const rangeToggle = document.querySelector('#slider-range') as ISwitchComponent; -rangeToggle.addEventListener('forge-switch-select', ({ detail: selected }) => slider.range = selected); +rangeToggle.addEventListener('forge-switch-change', ({ detail: selected }) => slider.range = selected); const tickmarksToggle = document.querySelector('#slider-tickmarks') as ISwitchComponent; -tickmarksToggle.addEventListener('forge-switch-select', ({ detail: selected }) => slider.tickmarks = selected); +tickmarksToggle.addEventListener('forge-switch-change', ({ detail: selected }) => slider.tickmarks = selected); const disabledToggle = document.querySelector('#slider-disabled') as ISwitchComponent; -disabledToggle.addEventListener('forge-switch-select', ({ detail: selected }) => slider.disabled = selected); +disabledToggle.addEventListener('forge-switch-change', ({ detail: selected }) => slider.disabled = selected); const readonlyToggle = document.querySelector('#slider-readonly') as ISwitchComponent; -readonlyToggle.addEventListener('forge-switch-select', ({ detail: selected }) => slider.readonly = selected); +readonlyToggle.addEventListener('forge-switch-change', ({ detail: selected }) => slider.readonly = selected); const labelsToggle = document.querySelector('#slider-labeled') as ISwitchComponent; -labelsToggle.addEventListener('forge-switch-select', ({ detail: selected }) => slider.labeled = selected); +labelsToggle.addEventListener('forge-switch-change', ({ detail: selected }) => slider.labeled = selected); const labelBuilderToggle = document.querySelector('#slider-label-builder') as ISwitchComponent; -labelBuilderToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +labelBuilderToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { slider.labelBuilder = selected ? (value: number) => `Value: ${value}` : undefined; }); diff --git a/src/dev/pages/stack/stack.ts b/src/dev/pages/stack/stack.ts index bebc4a71c..1a1f53dff 100644 --- a/src/dev/pages/stack/stack.ts +++ b/src/dev/pages/stack/stack.ts @@ -12,7 +12,7 @@ const stretchToggle = document.querySelector('#stretch-switch') as ISwitchCompon const gapInput = document.querySelector('#gap-input') as HTMLInputElement; const stackContainer = document.querySelector('#main-demo') as IStackComponent; -inlineToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +inlineToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { stackContainer.inline = selected; wrapToggle.disabled = !selected; @@ -21,11 +21,11 @@ inlineToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { } }); -wrapToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +wrapToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { stackContainer.wrap = selected; }); -stretchToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +stretchToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { stackContainer.stretch = selected; }); diff --git a/src/dev/pages/state-layer/state-layer.ts b/src/dev/pages/state-layer/state-layer.ts index eea5693a0..af089397c 100644 --- a/src/dev/pages/state-layer/state-layer.ts +++ b/src/dev/pages/state-layer/state-layer.ts @@ -9,17 +9,17 @@ const stateLayerDemoEl = document.querySelector('.state-layer-demo') as HTMLElem const stateLayer = stateLayerDemoEl.querySelector('forge-state-layer') as IStateLayerComponent; const disabledToggle = document.querySelector('#opt-disabled') as ISwitchComponent; -disabledToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +disabledToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { stateLayer.disabled = selected; }); const selectedToggle = document.querySelector('#opt-selected') as ISwitchComponent; -selectedToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +selectedToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { stateLayerDemoEl.classList.toggle('selected', selected); }); const circularToggle = document.querySelector('#opt-circular') as ISwitchComponent; -circularToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +circularToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { stateLayerDemoEl.classList.toggle('circular', selected); }); diff --git a/src/dev/pages/stepper/stepper.ts b/src/dev/pages/stepper/stepper.ts index 8224f3236..e7004cfe2 100644 --- a/src/dev/pages/stepper/stepper.ts +++ b/src/dev/pages/stepper/stepper.ts @@ -42,17 +42,17 @@ layoutAlignSelect.addEventListener('change', () => { }); const linearToggle = document.querySelector('#opt-linear') as ISwitchComponent; -linearToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +linearToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { stepper.linear = selected; }); const alternativeToggle = document.querySelector('#opt-alternative') as ISwitchComponent; -alternativeToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +alternativeToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { stepper.alternative = selected; }); const firstStepDisabledToggle = document.querySelector('#opt-first-step-disabled') as ISwitchComponent; -firstStepDisabledToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +firstStepDisabledToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { const firstStep = document.querySelector('#stepper-step-one') as IStepComponent; firstStep.disabled = selected; }); diff --git a/src/dev/pages/table/table.ts b/src/dev/pages/table/table.ts index 37c91e626..5403cd296 100644 --- a/src/dev/pages/table/table.ts +++ b/src/dev/pages/table/table.ts @@ -101,47 +101,47 @@ table.cellCreated = (cellElement, rowIndex, columnIndex) => { }; const selectToggle = document.querySelector('#opt-select') as ISwitchComponent; -selectToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +selectToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { table.select = selected; }); const multiselectToggle = document.querySelector('#opt-multiselect') as ISwitchComponent; -multiselectToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +multiselectToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { table.multiselect = selected; }); const tooltipSelectAllToggle = document.querySelector('#opt-tooltip-select-all') as ISwitchComponent; -tooltipSelectAllToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +tooltipSelectAllToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { table.tooltipSelectAll = selected ? 'Select all' : undefined; }); const tooltipSelectToggle = document.querySelector('#opt-tooltip-select') as ISwitchComponent; -tooltipSelectToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +tooltipSelectToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { table.tooltipSelect = selected ? 'Select' : undefined; }); const denseToggle = document.querySelector('#opt-dense') as ISwitchComponent; -denseToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +denseToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { table.dense = selected; }); const roomyToggle = document.querySelector('#opt-roomy') as ISwitchComponent; -roomyToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +roomyToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { table.roomy = selected; }); const filterToggle = document.querySelector('#opt-filter') as ISwitchComponent; -filterToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +filterToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { table.filter = selected; }); const wrapContentToggle = document.querySelector('#opt-wrap-content') as ISwitchComponent; -wrapContentToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +wrapContentToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { table.wrapContent = selected; }); const fixedHeadersToggle = document.querySelector('#opt-fixed-headers') as ISwitchComponent; -fixedHeadersToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +fixedHeadersToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { table.fixedHeaders = selected; if (selected) { @@ -154,22 +154,22 @@ fixedHeadersToggle.addEventListener('forge-switch-select', ({ detail: selected } }); const resizableToggle = document.querySelector('#opt-resizable') as ISwitchComponent; -resizableToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +resizableToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { table.resizable = selected; }); const allowRowClickToggle = document.querySelector('#opt-allow-row-click') as ISwitchComponent; -allowRowClickToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +allowRowClickToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { table.allowRowClick = selected; }); const multiColumnSortToggle = document.querySelector('#opt-multi-column-sort') as ISwitchComponent; -multiColumnSortToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +multiColumnSortToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { table.multiColumnSort = selected; }); const customSelectAllToggle = document.querySelector('#opt-custom-select-all') as ISwitchComponent; -customSelectAllToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +customSelectAllToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { if (selected) { table.selectAllTemplate = getSelectAllTemplate; selectAlignmentSelect.style.display = ''; diff --git a/src/dev/pages/tabs/tabs.ts b/src/dev/pages/tabs/tabs.ts index 374848bc4..2989d0f76 100644 --- a/src/dev/pages/tabs/tabs.ts +++ b/src/dev/pages/tabs/tabs.ts @@ -22,18 +22,18 @@ tabBar.addEventListener('forge-tab-bar-change', (evt) => { }); const verticalToggle = document.getElementById('opt-vertical') as ISwitchComponent; -verticalToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +verticalToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { tabBar.vertical = selected; container.classList.toggle('tabs-demo-container--vertical', selected); }); const secondaryToggle = document.getElementById('opt-secondary') as ISwitchComponent; -secondaryToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +secondaryToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { tabBar.secondary = selected; }); const clusteredToggle = document.getElementById('opt-clustered') as ISwitchComponent; -clusteredToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +clusteredToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { if (selected) { tabBar.setAttribute('clustered', clusteredAlignSelect.value); } else { @@ -43,32 +43,32 @@ clusteredToggle.addEventListener('forge-switch-select', ({ detail: selected }) = }); const stackedToggle = document.getElementById('opt-stacked') as ISwitchComponent; -stackedToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +stackedToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { tabBar.stacked = selected; }); const disabledToggle = document.getElementById('opt-disabled') as ISwitchComponent; -disabledToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +disabledToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { tabBar.disabled = selected; }); const invertedToggle = document.getElementById('opt-inverted') as ISwitchComponent; -invertedToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +invertedToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { tabBar.inverted = selected; }); const autoActivateToggle = document.getElementById('opt-auto-activate') as ISwitchComponent; -autoActivateToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +autoActivateToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { tabBar.autoActivate = selected; }); const scrollButtonsToggle = document.getElementById('opt-scroll-buttons') as ISwitchComponent; -scrollButtonsToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +scrollButtonsToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { tabBar.scrollButtons = selected; }); const showLeadingToggle = document.getElementById('opt-show-leading') as ISwitchComponent; -showLeadingToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +showLeadingToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { const tabElements = tabBar.querySelectorAll('forge-tab'); tabElements.forEach(tab => { if (selected) { @@ -80,7 +80,7 @@ showLeadingToggle.addEventListener('forge-switch-select', ({ detail: selected }) }); const showTrailingToggle = document.getElementById('opt-show-trailing') as ISwitchComponent; -showTrailingToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +showTrailingToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { const tabElements = tabBar.querySelectorAll('forge-tab'); tabElements.forEach(tab => { if (selected) { diff --git a/src/dev/pages/text-field/text-field.ts b/src/dev/pages/text-field/text-field.ts index c56764fcc..45e91eb85 100644 --- a/src/dev/pages/text-field/text-field.ts +++ b/src/dev/pages/text-field/text-field.ts @@ -61,54 +61,54 @@ optPlaceholder.addEventListener('input', () => { textarea.placeholder = optPlaceholder.value; } }); -optShowLeading.addEventListener('forge-switch-select', ({ detail: selected }) => { +optShowLeading.addEventListener('forge-switch-change', ({ detail: selected }) => { if (selected) { textField.appendChild(leadingEl); } else { leadingEl.remove(); } }); -optShowTrailing.addEventListener('forge-switch-select', ({ detail: selected }) => { +optShowTrailing.addEventListener('forge-switch-change', ({ detail: selected }) => { if (selected) { textField.appendChild(trailingEl); } else { trailingEl.remove(); } }); -optShowAddonEnd.addEventListener('forge-switch-select', ({ detail: selected }) => { +optShowAddonEnd.addEventListener('forge-switch-change', ({ detail: selected }) => { if (selected) { textField.appendChild(addonEndEl); } else { addonEndEl.remove(); } }); -optShowHelperText.addEventListener('forge-switch-select', ({ detail: selected }) => { +optShowHelperText.addEventListener('forge-switch-change', ({ detail: selected }) => { if (selected) { textField.appendChild(helperTextEl); } else { helperTextEl.remove(); } }); -optFloatLabel.addEventListener('forge-switch-select', ({ detail: selected }) => { +optFloatLabel.addEventListener('forge-switch-change', ({ detail: selected }) => { textField.floatLabelType = selected ? 'always' : 'auto'; }); -optRequired.addEventListener('forge-switch-select', ({ detail: selected }) => { +optRequired.addEventListener('forge-switch-change', ({ detail: selected }) => { textField.required = selected; }); -optDisabled.addEventListener('forge-switch-select', ({ detail: selected }) => { +optDisabled.addEventListener('forge-switch-change', ({ detail: selected }) => { if (input.isConnected) { input.disabled = selected; } else if (textarea) { textarea.disabled = selected; } }); -optInvalid.addEventListener('forge-switch-select', ({ detail: selected }) => { +optInvalid.addEventListener('forge-switch-change', ({ detail: selected }) => { textField.invalid = selected; }); -optRounded.addEventListener('forge-switch-select', ({ detail: selected }) => { +optRounded.addEventListener('forge-switch-change', ({ detail: selected }) => { textField.shape = selected ? 'rounded' : 'default'; }); -optTextarea.addEventListener('forge-switch-select', ({ detail: selected }) => { +optTextarea.addEventListener('forge-switch-change', ({ detail: selected }) => { const parent = textField.parentElement; textField.remove(); diff --git a/src/dev/pages/time-picker/time-picker.ts b/src/dev/pages/time-picker/time-picker.ts index 69b28d5c6..1f4531151 100644 --- a/src/dev/pages/time-picker/time-picker.ts +++ b/src/dev/pages/time-picker/time-picker.ts @@ -63,48 +63,48 @@ timePicker.addEventListener('forge-time-picker-input', () => { inputValueElement.textContent = timePickerInput.value || '""'; }); -opt24HourToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +opt24HourToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { timePicker.use24HourTime = selected; }); -optSecondsToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +optSecondsToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { timePicker.allowSeconds = selected; }); -optMaskedToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +optMaskedToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { timePicker.masked = selected; }); -optShowMaskFormatToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +optShowMaskFormatToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { timePicker.showMaskFormat = selected; }); -optShowNowToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +optShowNowToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { timePicker.showNow = selected; }); -optShowHourOptionsToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +optShowHourOptionsToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { timePicker.showHourOptions = selected; }); -optUseCustomOptionsToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +optUseCustomOptionsToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { timePicker.customOptions = selected ? CUSTOM_OPTIONS : []; }); -optUseCustomCallbacksToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +optUseCustomCallbacksToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { timePicker.validationCallback = selected ? validationCallback : null; timePicker.parseCallback = selected ? parseCallback : null; timePicker.formatCallback = selected ? formatCallback : null; }); -optAllowDropdownToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +optAllowDropdownToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { timePicker.allowDropdown = selected; }); -optAllowInputToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +optAllowInputToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { timePicker.allowInput = selected; }); -optAllowInvalidTimeToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +optAllowInvalidTimeToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { timePicker.allowInvalidTime = selected; }); -optUseRestrictedTimesToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +optUseRestrictedTimesToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { timePicker.restrictedTimes = selected ? RESTRICTED_TIMES : []; }); -optUseCoercionCallbackToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +optUseCoercionCallbackToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { timePicker.coercionCallback = selected ? coercionCallback : undefined; }); -optDisabledToggle.addEventListener('forge-switch-select', ({ detail: selected }) => { +optDisabledToggle.addEventListener('forge-switch-change', ({ detail: selected }) => { timePicker.disabled = selected; }); optValueTimePicker.addEventListener('forge-time-picker-change', ({ detail }) => { diff --git a/src/dev/pages/toolbar/toolbar.ts b/src/dev/pages/toolbar/toolbar.ts index 0eb503a63..b794dc3d0 100644 --- a/src/dev/pages/toolbar/toolbar.ts +++ b/src/dev/pages/toolbar/toolbar.ts @@ -7,13 +7,13 @@ import { toggleAttribute } from '@tylertech/forge-core'; const toolbar = document.querySelector('#toolbar') as IToolbarComponent; const invertedToggle = document.querySelector('#toolbar-opt-inverted') as ISwitchComponent; -invertedToggle.addEventListener('forge-switch-select', ({ detail: selected }) => toolbar.inverted = selected); +invertedToggle.addEventListener('forge-switch-change', ({ detail: selected }) => toolbar.inverted = selected); const showBorderToggle = document.querySelector('#toolbar-opt-border') as ISwitchComponent; -showBorderToggle.addEventListener('forge-switch-select', ({ detail: selected }) => toggleAttribute(toolbar, !selected, 'no-border')); +showBorderToggle.addEventListener('forge-switch-change', ({ detail: selected }) => toggleAttribute(toolbar, !selected, 'no-border')); const hasPaddingToggle = document.querySelector('#toolbar-opt-has-padding') as ISwitchComponent; -hasPaddingToggle.addEventListener('forge-switch-select', ({ detail: selected }) => toggleAttribute(toolbar, !selected, 'no-padding')); +hasPaddingToggle.addEventListener('forge-switch-change', ({ detail: selected }) => toggleAttribute(toolbar, !selected, 'no-padding')); const autoHeightToggle = document.querySelector('#toolbar-opt-auto-height') as ISwitchComponent; -autoHeightToggle.addEventListener('forge-switch-select', ({ detail: selected }) => toggleAttribute(toolbar, selected, 'auto-height')); +autoHeightToggle.addEventListener('forge-switch-change', ({ detail: selected }) => toggleAttribute(toolbar, selected, 'auto-height')); diff --git a/src/dev/pages/tooltip/tooltip.ts b/src/dev/pages/tooltip/tooltip.ts index 66904ab3d..e963dbb89 100644 --- a/src/dev/pages/tooltip/tooltip.ts +++ b/src/dev/pages/tooltip/tooltip.ts @@ -16,7 +16,7 @@ const positionSelect = document.querySelector('#tooltip-position') as ISelectCom positionSelect.addEventListener('change', () => tooltip.position = positionSelect.value); const useBuilderCheckbox = document.querySelector('#tooltip-builder') as ISwitchComponent; -useBuilderCheckbox.addEventListener('forge-switch-select', ({ detail: selected }) => tooltip.builder = selected ? tooltipBuilder : undefined); +useBuilderCheckbox.addEventListener('forge-switch-change', ({ detail: selected }) => tooltip.builder = selected ? tooltipBuilder : undefined); function tooltipBuilder(): HTMLElement { const div = document.createElement('div'); diff --git a/src/stories/src/components/switch/switch.mdx b/src/stories/src/components/switch/switch.mdx index 63acc1aa4..ffa604a84 100644 --- a/src/stories/src/components/switch/switch.mdx +++ b/src/stories/src/components/switch/switch.mdx @@ -81,7 +81,7 @@ Use switches to: | Name | Description | :---------------------------------| :----------------- -| `forge-switch-select` | Emits when the switch has changed. +| `forge-switch-change` | Emits when the switch has changed. From 7f0928d728c8959b5a8712fe2fcb2e902fe20081 Mon Sep 17 00:00:00 2001 From: Sam Richardson Date: Fri, 27 Oct 2023 09:42:58 -0400 Subject: [PATCH 32/38] refactor(checkbox): use border width token --- src/dev/pages/switch/switch.ts | 4 ++-- src/lib/core/styles/tokens/checkbox/_tokens.scss | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/dev/pages/switch/switch.ts b/src/dev/pages/switch/switch.ts index e25cfd3ce..3a936af0b 100644 --- a/src/dev/pages/switch/switch.ts +++ b/src/dev/pages/switch/switch.ts @@ -9,6 +9,6 @@ IconRegistry.define([ tylIconEmoticonSad ]); -// const preventSwitch = document.getElementById('prevent-switch') as ISwitchComponent; +const preventSwitch = document.getElementById('prevent-switch') as ISwitchComponent; -// preventSwitch.addEventListener('forge-switch-change', (evt: CustomEvent) => evt.preventDefault()); +preventSwitch.addEventListener('forge-switch-change', (evt: CustomEvent) => evt.preventDefault()); diff --git a/src/lib/core/styles/tokens/checkbox/_tokens.scss b/src/lib/core/styles/tokens/checkbox/_tokens.scss index 05efc6329..89b78e76d 100644 --- a/src/lib/core/styles/tokens/checkbox/_tokens.scss +++ b/src/lib/core/styles/tokens/checkbox/_tokens.scss @@ -1,6 +1,7 @@ @use 'sass:map'; @use '../color-palette'; @use '../../animation'; +@use '../../border'; @use '../../elevation'; @use '../../shape'; @use '../../theme'; @@ -10,7 +11,7 @@ $tokens: ( // Shared primary-color: utils.module-val(checkbox, primary-color, theme.variable(tertiary)), size: utils.module-val(checkbox, size, 16px), - border-width: utils.module-val(checkbox, border-width, 2px), + border-width: utils.module-val(checkbox, border-width, border.variable(medium)), icon-color: utils.module-val(checkbox, icon-color, theme.variable(on-tertiary)), state-layer-size: utils.module-val(checkbox, state-layer-size, 40px), state-layer-dense-size: utils.module-val(checkbox, state-layer-size, 24px), From 9ec9e11afba5868daf579b91ca95f32136ff81c4 Mon Sep 17 00:00:00 2001 From: Kieran Nichols Date: Fri, 27 Oct 2023 09:54:20 -0400 Subject: [PATCH 33/38] chore(theme): refactor theme tokens (#417) --- src/dev/pages/theme/theme.ejs | 3 + src/dev/pages/theme/theme.html | 8 + src/dev/pages/theme/theme.scss | 25 +++ src/dev/pages/theme/theme.ts | 168 ++++++++++++++++ src/dev/src/components.json | 1 + src/dev/src/styles/_header.scss | 4 +- src/dev/src/styles/_options.scss | 12 +- src/lib/core/styles/_utils.scss | 49 ++++- src/lib/core/styles/scrollbar/index.scss | 40 ++++ src/lib/core/styles/spacing/index.scss | 2 +- src/lib/core/styles/theme/_color-utils.scss | 58 ++++++ src/lib/core/styles/theme/_utils.scss | 77 ++++++++ src/lib/core/styles/theme/index.scss | 52 +++-- .../tokens/circular-progress/_tokens.scss | 2 +- .../_extended-color-palette.scss | 83 ++++++++ .../_material-color-palette.scss} | 0 .../styles/tokens/color-palette/index.scss | 2 + .../tokens/focus-indicator/_tokens.scss | 2 +- .../tokens/linear-progress/_tokens.scss | 2 +- .../styles/tokens/list/list-item/_tokens.scss | 8 +- .../core/styles/tokens/scrollbar/_tokens.scss | 21 ++ .../core/styles/tokens/slider/_tokens.scss | 20 +- .../core/styles/tokens/spacing/_tokens.scss | 20 +- .../core/styles/tokens/switch/_tokens.scss | 10 +- .../styles/tokens/tabs/tab-bar/_tokens.scss | 2 +- .../core/styles/tokens/tabs/tab/_tokens.scss | 4 +- .../styles/tokens/theme/_color-emphasis.scss | 33 ++++ .../styles/tokens/theme/_token-utils.scss | 41 ++++ .../styles/tokens/theme/_tokens.core.scss | 40 ++++ src/lib/core/styles/tokens/theme/_tokens.scss | 181 ++++++------------ .../styles/tokens/theme/_tokens.status.scss | 44 +++++ .../styles/tokens/theme/_tokens.surface.scss | 60 ++++++ .../styles/tokens/theme/_tokens.text.scss | 33 ++++ .../tokens/theme/_tokens.utilities.scss | 25 +++ .../styles/tokens/typography/_tokens.scss | 4 +- src/lib/slider/_core.scss | 8 +- src/lib/slider/slider.scss | 4 +- src/lib/tabs/tab/_configuration.scss | 5 + src/lib/tabs/tab/_core.scss | 14 +- src/lib/tabs/tab/tab.scss | 5 +- src/lib/theme/_theme.scss | 48 +---- src/lib/theme/forge-theme.scss | 24 ++- 42 files changed, 1003 insertions(+), 241 deletions(-) create mode 100644 src/dev/pages/theme/theme.ejs create mode 100644 src/dev/pages/theme/theme.html create mode 100644 src/dev/pages/theme/theme.scss create mode 100644 src/dev/pages/theme/theme.ts create mode 100644 src/lib/core/styles/scrollbar/index.scss create mode 100644 src/lib/core/styles/theme/_color-utils.scss create mode 100644 src/lib/core/styles/theme/_utils.scss create mode 100644 src/lib/core/styles/tokens/color-palette/_extended-color-palette.scss rename src/lib/core/styles/tokens/{_color-palette.scss => color-palette/_material-color-palette.scss} (100%) create mode 100644 src/lib/core/styles/tokens/color-palette/index.scss create mode 100644 src/lib/core/styles/tokens/scrollbar/_tokens.scss create mode 100644 src/lib/core/styles/tokens/theme/_color-emphasis.scss create mode 100644 src/lib/core/styles/tokens/theme/_token-utils.scss create mode 100644 src/lib/core/styles/tokens/theme/_tokens.core.scss create mode 100644 src/lib/core/styles/tokens/theme/_tokens.status.scss create mode 100644 src/lib/core/styles/tokens/theme/_tokens.surface.scss create mode 100644 src/lib/core/styles/tokens/theme/_tokens.text.scss create mode 100644 src/lib/core/styles/tokens/theme/_tokens.utilities.scss diff --git a/src/dev/pages/theme/theme.ejs b/src/dev/pages/theme/theme.ejs new file mode 100644 index 000000000..088da3ebd --- /dev/null +++ b/src/dev/pages/theme/theme.ejs @@ -0,0 +1,3 @@ +
+ + diff --git a/src/dev/pages/theme/theme.html b/src/dev/pages/theme/theme.html new file mode 100644 index 000000000..7c3b0f897 --- /dev/null +++ b/src/dev/pages/theme/theme.html @@ -0,0 +1,8 @@ +<%- +include('./src/partials/page.ejs', { + page: { + title: 'Theme', + includePath: './pages/theme/theme.ejs' + } +}) +%> diff --git a/src/dev/pages/theme/theme.scss b/src/dev/pages/theme/theme.scss new file mode 100644 index 000000000..e040a6a57 --- /dev/null +++ b/src/dev/pages/theme/theme.scss @@ -0,0 +1,25 @@ +.swatches { + display: flex; + flex-direction: column; + gap: 24px; +} + +.swatch-container { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.swatch { + border: 1px solid var(--forge-theme-outline); + box-sizing: border-box; + height: 100px; + width: 200px; + font-size: 14px; + padding: 8px; + border-radius: 4px; +} + +h2 { + margin: 0; +} diff --git a/src/dev/pages/theme/theme.ts b/src/dev/pages/theme/theme.ts new file mode 100644 index 000000000..25c9dbe65 --- /dev/null +++ b/src/dev/pages/theme/theme.ts @@ -0,0 +1,168 @@ +import '$src/shared'; +import './theme.scss'; + +interface ISwatchGroup { + header?: string; + swatches: ISwatch[]; +} + +interface ISwatch { + text?: string; + background: string; + foreground?: string; +} + +const SWATCH_GROUPS: ISwatchGroup[] = [ + { + header: 'Surface', + swatches: [ + { text: 'Surface', background: 'surface' }, + { text: 'Surface dim (background)', background: 'surface-dim' }, + { text: 'Surface bright', background: 'surface-bright' }, + { text: 'Surface inverse', background: 'surface-inverse', foreground: 'on-surface-inverse' } + ] + }, + { + swatches: [ + { text: 'Surface container', background: 'surface-container', foreground: 'on-surface-container' }, + { text: 'Surface container (low)', background: 'surface-container-low', foreground: 'on-surface' }, + { text: 'Surface container (medium)', background: 'surface-container-medium', foreground: 'on-surface' }, + { text: 'Surface container (high)', background: 'surface-container-high', foreground: 'on-surface-container-high' } + ] + }, + { + header: 'Key colors', + swatches: [ + { text: 'Primary', background: 'primary', foreground: 'on-primary' }, + { text: 'Primary container', background: 'primary-container', foreground: 'on-primary-container' } + ] + }, + { + swatches: [ + { text: 'Secondary', background: 'secondary', foreground: 'on-secondary' }, + { text: 'Secondary container', background: 'secondary-container', foreground: 'on-secondary-container' } + ] + }, + { + swatches: [ + { text: 'Tertiary', background: 'tertiary', foreground: 'on-tertiary' }, + { text: 'Tertiary container', background: 'tertiary-container', foreground: 'on-tertiary-container' } + ] + }, + { + header: 'Status', + swatches: [ + { text: 'Success', background: 'success', foreground: 'on-success' }, + { text: 'Success container', background: 'success-container', foreground: 'on-success-container' } + ] + }, + { + swatches: [ + { text: 'Error ', background: 'error', foreground: 'on-error' }, + { text: 'Error container', background: 'error-container', foreground: 'on-error-container' } + ] + }, + { + swatches: [ + { text: 'Warning', background: 'warning', foreground: 'on-warning' }, + { text: 'Warning container', background: 'warning-container', foreground: 'on-warning-container' } + ] + }, + { + swatches: [ + { text: 'Info', background: 'info', foreground: 'on-info' }, + { text: 'Info container', background: 'info-container', foreground: 'on-info-container' } + ] + }, + { + header: 'Text', + swatches: [ + { text: 'High', background: 'text-high', foreground: 'text-high-inverse' }, + { text: 'Medium', background: 'text-medium', foreground: 'text-high-inverse' }, + { text: 'Low', background: 'text-low', foreground: 'text-high' }, + { text: 'Lowest', background: 'text-lowest', foreground: 'text-high' } + ] + }, + { + header: 'Utilities', + swatches: [ + { text: 'Outline', background: 'outline', foreground: 'text-high' } + ] + } +]; + +const container = document.getElementById('container'); + +function buildSwatches(): void { + SWATCH_GROUPS.forEach(group => { + const swatchContainer = document.createElement('div'); + swatchContainer.classList.add('swatch-container'); + + if (group.header) { + const header = document.createElement('h2'); + header.classList.add('forge-typography--heading-03'); + header.textContent = group.header; + container.appendChild(header); + } + + group.swatches.forEach(config => { + swatchContainer.appendChild(createSwatch(config)); + }); + + container.appendChild(swatchContainer); + }); +} + +function createSwatch(config: ISwatch): HTMLElement { + const swatch = document.createElement('div'); + swatch.classList.add('swatch'); + if (config.text) { + swatch.textContent = config.text; + } + swatch.style.setProperty('background-color', `var(--forge-theme-${config.background})`); + + if (config.foreground) { + swatch.style.setProperty('color', `var(--forge-theme-${config.foreground})`); + } + + return swatch; +} + +buildSwatches(); + +function hexToRGB(hex: string): [number, number, number] { + let hexValue = hex.replace('#', ''); + if (hexValue.length === 3) { + hexValue = hexValue.split('').map(char => char + char).join(''); + } + const r = parseInt(hexValue.substring(0, 2), 16); + const g = parseInt(hexValue.substring(2, 4), 16); + const b = parseInt(hexValue.substring(4, 6), 16); + return [r, g, b]; +} + +function _linearChannelValue($channelValue: number): number { + const normalizedChannelValue = $channelValue / 255; + if (normalizedChannelValue < 0.03928) { + return normalizedChannelValue / 12.92; + } + return Math.pow(normalizedChannelValue + 0.055 / 1.055, 2.4); +} + +// Calculate the luminance for a color. +// See https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests +function _luminance($color): number { + const [r, g, b] = hexToRGB($color); + const red = _linearChannelValue(r); + const green = _linearChannelValue(g); + const blue = _linearChannelValue(b); + return 0.2126 * red + 0.7152 * green + 0.0722 * blue; +} + +// Calculate the contrast ratio between two colors. +// See https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests +function contrastRatio($back: string, $front: string): number { + const backLum = _luminance($back) + 0.05; + const foreLum = _luminance($front) + 0.05; + return Math.max(backLum, foreLum) / Math.min(backLum, foreLum); +} diff --git a/src/dev/src/components.json b/src/dev/src/components.json index 84190a79f..bcd9e14e6 100644 --- a/src/dev/src/components.json +++ b/src/dev/src/components.json @@ -52,6 +52,7 @@ { "label": "Table", "path": "/pages/table/table.html" }, { "label": "Tabs", "path": "/pages/tabs/tabs.html", "tags": ["view", "bar"] }, { "label": "Text field", "path": "/pages/text-field/text-field.html", "tags": ["form"] }, + { "label": "Theme", "path": "/pages/theme/theme.html", "tags": ["color"] }, { "label": "Time picker", "path": "/pages/time-picker/time-picker.html" }, { "label": "Toast", "path": "/pages/toast/toast.html" }, { "label": "Toolbar", "path": "/pages/toolbar/toolbar.html" }, diff --git a/src/dev/src/styles/_header.scss b/src/dev/src/styles/_header.scss index 2983ca043..c0a25bdb6 100644 --- a/src/dev/src/styles/_header.scss +++ b/src/dev/src/styles/_header.scss @@ -1,4 +1,4 @@ #page-app-bar { - --forge-app-bar-theme-background: var(--mdc-theme-secondary); - --forge-app-bar-theme-on-background: var(--mdc-theme-text-primary-on-light); + --forge-app-bar-theme-background: var(--forge-theme-secondary); + --forge-app-bar-theme-on-background: var(--forge-theme-on-secondary); } diff --git a/src/dev/src/styles/_options.scss b/src/dev/src/styles/_options.scss index 6990d964b..f51213299 100644 --- a/src/dev/src/styles/_options.scss +++ b/src/dev/src/styles/_options.scss @@ -2,11 +2,21 @@ padding: 16px; display: flex; flex-direction: column; - gap: 16px; forge-drawer:has(.options-container) { --forge-drawer-width: 300px; } + + forge-text-field, + forge-select { + &:not(:first-child) { + margin-block-start: 8px; + } + + &:not(:last-child) { + margin-block-end: 8px; + } + } forge-switch { &::part(label) { diff --git a/src/lib/core/styles/_utils.scss b/src/lib/core/styles/_utils.scss index b4d7c98ea..b962ccbfd 100644 --- a/src/lib/core/styles/_utils.scss +++ b/src/lib/core/styles/_utils.scss @@ -1,4 +1,5 @@ @use 'sass:map'; +@use 'sass:math'; @use './core/config'; /// @@ -19,8 +20,13 @@ /// To provide a module reference variable with a different fallback value, set the /// $type parameter to `value` and `$value` will be used verbatim. /// +/// Example: +/// ```scss +/// $my-token: module-ref(comp, color, primary-color); // => var(--forge-comp-color, var(--_primary-color)); +/// ``` +/// @function module-ref($module, $token, $value, $type: ref, $prefix: config.$prefix) { - $fallback: if($type == ref, var(--_#{$value}), $value); + $fallback: if($type == ref, module-var($value), $value); @return _create-var($module, $token, $fallback, $prefix); } @@ -35,10 +41,28 @@ /// Creates a CSS custom property declaration for a module token using the /// provided value as the variable fallback. /// +/// Example: +/// ```scss +/// $my-token: module-val(comp, color, red); // => var(--forge-comp-color, red); +/// $my-other-token: module-val(comp, color, theme.variable(primary)); // => var(--forge-comp-color, var(--forge-theme-primary)); +/// ``` +/// @function module-val($module, $token, $value, $prefix: config.$prefix) { @return _create-var($module, $token, $value, $prefix); } +/// +/// Creates a CSS custom property variable reference for a token in the provided module. +/// +/// Example: +/// ```scss +/// $my-var: variable-ref(theme, color); // => var(--forge-theme-color); +/// ``` +/// +@function variable-ref($module, $token, $value: null, $prefix: config.$prefix) { + @return _create-var($module, $token, $value, $prefix); +} + /// /// Emits CSS custom property declarations for tokens on a per-module basis, /// using the provided token map. @@ -63,7 +87,28 @@ /// /// Creates a CSS custom property declaration for a module token, using the provided fallback value. /// -@function _create-var($module, $token, $value, $prefix: config.$prefix) { +@function _create-var($module, $token, $value: null, $prefix: config.$prefix) { + @if $value == null { + @return var(--#{$prefix}-#{$module}-#{$token}); + } @return var(--#{$prefix}-#{$module}-#{$token}, $value); } +/// +/// Rounds a number to the specified number of decimal places. +/// +@function round($value, $fractionDigits: 0) { + $power: math.pow(10, $fractionDigits); + @return math.div(math.round($power * $value), $power); +} + +/// +/// Flattens multiple maps into a single map. +/// +@function flatten($maps...) { + $merged: (); + @each $map in $maps { + $merged: map.merge($merged, $map); + } + @return $merged; +} diff --git a/src/lib/core/styles/scrollbar/index.scss b/src/lib/core/styles/scrollbar/index.scss new file mode 100644 index 000000000..96432d008 --- /dev/null +++ b/src/lib/core/styles/scrollbar/index.scss @@ -0,0 +1,40 @@ +@use '../tokens/scrollbar/tokens'; + +/// +/// Provides scrollbar styles. +/// +@mixin base { + &::-webkit-scrollbar { + height: #{tokens.get(height)}; + width: #{tokens.get(width)}; + } + + &::-webkit-scrollbar-track { + background-color: #{tokens.get(track-container)}; + + &:hover { + background-color: #{tokens.get(track-container-hover)}; + } + } + + &::-webkit-scrollbar-corner { + background-color: #{tokens.get(track-container)}; + } + + &::-webkit-scrollbar-thumb { + height: #{tokens.get(thumb-min-height)}; + width: #{tokens.get(thumb-min-width)}; + + border-radius: #{tokens.get(border-radius)}; + border-width: #{tokens.get(border-width)}; + border-style: solid; + border-color: transparent; + + background-color: #{tokens.get(thumb-container)}; + background-clip: content-box; + + &:hover { + background-color: #{tokens.get(thumb-container-hover)}; + } + } +} diff --git a/src/lib/core/styles/spacing/index.scss b/src/lib/core/styles/spacing/index.scss index 6183467e3..90ca72d2b 100644 --- a/src/lib/core/styles/spacing/index.scss +++ b/src/lib/core/styles/spacing/index.scss @@ -9,7 +9,7 @@ /// Example: /// ```scss /// .my-class { -/// margin: spacing.variable('200'); // => margin: var(--forge-spacing-standard, 16px); +/// margin: spacing.variable('100'); // => margin: var(--forge-spacing-standard, 16px); /// } /// ``` /// diff --git a/src/lib/core/styles/theme/_color-utils.scss b/src/lib/core/styles/theme/_color-utils.scss new file mode 100644 index 000000000..ad0818a0b --- /dev/null +++ b/src/lib/core/styles/theme/_color-utils.scss @@ -0,0 +1,58 @@ +@use 'sass:meta'; +@use 'sass:math'; +@use 'sass:color'; + +@function _linear-channel-value($channel-value) { + $normalized-channel-value: math.div($channel-value, 255); + @if $normalized-channel-value < 0.03928 { + @return math.div($normalized-channel-value, 12.92); + } + + @return math.pow(math.div($normalized-channel-value + 0.055, 1.055), 2.4); +} + +// Calculate the luminance for a color. +// See https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests +@function luminance($color) { + $red: _linear-channel-value(color.red($color)); + $green: _linear-channel-value(color.green($color)); + $blue: _linear-channel-value(color.blue($color)); + + @return 0.2126 * $red + 0.7152 * $green + 0.0722 * $blue; +} + +// Calculate the contrast ratio between two colors. +// See https://www.w3.org/TR/WCAG20-TECHS/G17.html#G17-tests +@function contrast-ratio($back, $front) { + $backLum: luminance($back) + 0.05; + $foreLum: luminance($front) + 0.05; + @return math.div(math.max($backLum, $foreLum), math.min($backLum, $foreLum)); +} + +// Determine whether the color is 'light' or 'dark'. +@function tone($color) { + @if $color == 'dark' or $color == 'light' { + @return $color; + } + + @if meta.type-of($color) != 'color' { + @warn '#{$color} is not a color. Falling back to "dark" tone.'; + @return 'dark'; + } + + $minimumContrast: 3.1; + $lightContrast: contrast-ratio($color, white); + $darkContrast: contrast-ratio($color, rgba(black, 0.87)); + + @if ($lightContrast < $minimumContrast) and ($darkContrast > $lightContrast) { + @return 'light'; + } @else { + @return 'dark'; + } +} + +// Determine whether to use dark or light text on top of given color to meet accessibility standards for contrast. +// Returns 'dark' if the given color is light and 'light' if the given color is dark. +@function contrast-tone($color) { + @return if(tone($color) == 'dark', 'light', 'dark'); +} diff --git a/src/lib/core/styles/theme/_utils.scss b/src/lib/core/styles/theme/_utils.scss new file mode 100644 index 000000000..25340fb42 --- /dev/null +++ b/src/lib/core/styles/theme/_utils.scss @@ -0,0 +1,77 @@ +@use 'sass:meta'; +@use 'sass:string'; +@use 'sass:color'; +@use '../utils'; +@use './color-utils'; + +/// +/// Returns a new color with the same hue and saturation as the input color, but with a different lightness value. +/// +/// Example: +/// scale(#ff0000, 50%) => #ff6060 +/// +/// @param {Color} $color - The input color. +/// @param {Number} $lightness - The desired lightness percentage to scale `$color` by. Specified as a percentage number between 0 and 100. +/// @return {Color} - The new color with the same hue and saturation as the input color, but with a different lightness value. +/// +@function scale($color, $lightness) { + @return color.scale($color, $lightness: 100 - $lightness); +} + +/// +/// Creates a new color by applying the specified emphasis to the input color via the alpha channel. +/// +/// Example: +/// emphasized(#ff0000, 50%) => rgba(255, 0, 0, 0.5) +/// +/// @param {Color} $color - The input color. +/// @param {Number} $alpha - The desired opacity to apply to `$color`. +/// @return {Color} - The input color, but with an alpha channel value in `rgba()` format. +/// +@function emphasized($color, $emphasis) { + @return rgba($color, $emphasis); +} + +/// +/// Computes the contrast color for the provided color. +/// +/// Example: +/// contrast(#ff0000) => #ffffff +/// +/// @param {Color} $color - The input color. +/// @param {Color} $light - The light color to use for contrast. Default to `#ffffff`. +/// @param {Color} $dark - The dark color to use for contrast. Default to `#000000`. +/// @return {Color} - The contrast color for the provided color. +/// +@function contrast($color, $alpha: null, $light: #ffffff, $dark: #000000) { + $contrast-color: if(color-utils.contrast-tone($color) == 'dark', $dark, $light); + @if $alpha != null { + @return emphasized($contrast-color, $alpha); + } + @return $contrast-color; +} + +/// +/// Converts a color with an opacity value on top of a specific background color to an opaque hex color. +/// +@function hexify($color, $background, $alpha: 100%) { + @return if($alpha == 100%, color.mix($background, $color, $alpha), color.mix($background, $color, 100 - $alpha)); +} + +/// +/// Determines if the provided value is a `color` type or not. +/// +@function is-color($value) { + @return meta.type-of($value) == color; +} + +/// +/// Computes the hue, saturation, lightness (and optionally alpha) values for the provided color and returns a string in HSL format. +/// +@function hsl-values($color, $with-alpha: false) { + $value: utils.round(hue($color), 2) utils.round(saturation($color), 2) utils.round(lightness($color), 2); + @if ($with-alpha) { + $value: string.unquote($value + ' / ' + alpha($color)); + } + @return $value; +} diff --git a/src/lib/core/styles/theme/index.scss b/src/lib/core/styles/theme/index.scss index f458f20ae..afad3d3cc 100644 --- a/src/lib/core/styles/theme/index.scss +++ b/src/lib/core/styles/theme/index.scss @@ -1,25 +1,16 @@ @use 'sass:map'; +@use 'sass:math'; @use '../core/config'; @use '../tokens/theme/tokens'; +@use '../tokens/theme/color-emphasis'; @use '../utils'; /// /// Emits custom property declarations for all default theme tokens. /// @mixin properties { - /// Temporary tokens to map the MDC theme to Forge theme - /// TODO: Remove this once MDC dependency is removed - :root { - @each $token-name, $token-value in tokens.$mdc-remap { - --mdc-theme-#{$token-name}: #{$token-value}; - } - } - - // Forge theme tokens - :root { - @each $token-name, $token-value in tokens.$tokens { - --#{config.$prefix}-theme-#{$token-name}: #{$token-value}; - } + @each $token-name, $token-value in tokens.$tokens { + --#{config.$prefix}-theme-#{$token-name}: #{$token-value}; } } @@ -32,6 +23,31 @@ } } +/// +/// Temporary tokens to map the MDC theme to Forge theme +/// +/// TODO: Remove this once MDC dependency is removed +/// +@mixin properties-compat { + @each $token-name, $token-value in tokens.$mdc-remap { + --mdc-theme-#{$token-name}: #{$token-value}; + } +} + +/// +/// Generates utility classes for each theme token as both `color` and `background-color` styles separately. +/// +@mixin classes { + @each $token, $value in tokens.$tokens { + .#{config.$prefix}-color--#{$token} { + color: #{variable($token)}; + } + .#{config.$prefix}-background--#{$token} { + background-color: #{variable($token)}; + } + } +} + /// /// Gets a CSS custom property declaration for a specific theme token, with its token value as the fallback value /// @@ -45,3 +61,13 @@ @mixin provide($tokens, $prefix: config.$prefix) { @include utils.provide(tokens.$tokens, $tokens, theme, $prefix); } + +/// +/// Gets a theme emphasis value by semantic name. +/// +/// Valid names are `highest`, `higher`, `high`, `medium-high`, `medium`, `medium-low`, `low`, `lower`, and `lowest`. +/// +@function emphasis($emphasis) { + @return math.div(color-emphasis.value($emphasis), 100%); +} + diff --git a/src/lib/core/styles/tokens/circular-progress/_tokens.scss b/src/lib/core/styles/tokens/circular-progress/_tokens.scss index 7683316cd..7c24e713d 100644 --- a/src/lib/core/styles/tokens/circular-progress/_tokens.scss +++ b/src/lib/core/styles/tokens/circular-progress/_tokens.scss @@ -7,7 +7,7 @@ $tokens: ( padding: utils.module-val(circular-progress, padding, 0), track-width: utils.module-val(circular-progress, track-width, 12), track-color: utils.module-val(circular-progress, track-color, theme.variable(tertiary)), - track-background: utils.module-val(circular-progress, track-background, theme.variable(track-color)), + track-background: utils.module-val(circular-progress, track-background, theme.variable(surface-container)), arc-duration: utils.module-val(circular-progress, arc-duration, 1333ms) ) !default; diff --git a/src/lib/core/styles/tokens/color-palette/_extended-color-palette.scss b/src/lib/core/styles/tokens/color-palette/_extended-color-palette.scss new file mode 100644 index 000000000..504baa366 --- /dev/null +++ b/src/lib/core/styles/tokens/color-palette/_extended-color-palette.scss @@ -0,0 +1,83 @@ +$white: #ffffff; +$black: #000000; + +$neutral-50: #ffffff; +$neutral-100: #f9f9f9; +$neutral-200: #dddddd; +$neutral-300: #c1c1c1; +$neutral-400: #a6a6a6; +$neutral-500: #8b8b8b; +$neutral-600: #727272; +$neutral-700: #595959; +$neutral-800: #424242; +$neutral-900: #2c2c2c; + +$crimson-50: #fdeaee; +$crimson-100: #fbcad2; +$crimson-200: #ea959b; +$crimson-300: #de6c74; +$crimson-400: #e84853; +$crimson-500: #ed323b; +$crimson-600: #de2839; +$crimson-700: #cc1d33; +$crimson-800: #bf152c; +$crimson-900: #b00020; + +$burnt-orange-50: #fbe9e6; +$burnt-orange-100: #fdccb9; +$burnt-orange-200: #fcac8c; +$burnt-orange-300: #fa8c5d; +$burnt-orange-400: #f87439; +$burnt-orange-500: #f75d0e; +$burnt-orange-600: #ec5709; +$burnt-orange-700: #df5003; +$burnt-orange-800: #d14900; +$burnt-orange-900: #b93c00; + +$ruddy-pink-50: #fbe1e5; +$ruddy-pink-100: #f5b4c0; +$ruddy-pink-200: #ec8396; +$ruddy-pink-300: #e2526f; +$ruddy-pink-400: #d82c52; +$ruddy-pink-500: #cf0038; +$ruddy-pink-600: #c00038; +$ruddy-pink-700: #ac0035; +$ruddy-pink-800: #990033; +$ruddy-pink-900: #78002f; + +$apricot-50: #fceae3; +$apricot-100: #fbcfb0; +$apricot-200: #f8b27c; +$apricot-300: #f29645; +$apricot-400: #ed830b; +$apricot-500: #e87200; +$apricot-600: #de6c00; +$apricot-700: #d16400; +$apricot-800: #c45c00; +$apricot-900: #ad4e00; + +$dollar-bill-50: #f2f8ec; +$dollar-bill-100: #deedcf; +$dollar-bill-200: #c8e1b2; +$dollar-bill-300: #b4d494; +$dollar-bill-400: #a4cb7e; +$dollar-bill-500: #95c269; +$dollar-bill-600: #86b260; +$dollar-bill-700: #749e55; +$dollar-bill-800: #63894c; +$dollar-bill-900: #46673b; + +$maroon-50: #f0e2e4; +$maroon-100: #dcb7bb; +$maroon-200: #bf868d; +$maroon-300: #ad5661; +$maroon-400: #92313c; +$maroon-500: #7f0d1a; +$maroon-600: #6c0913; +$maroon-700: #62070f; +$maroon-800: #4f0308; +$maroon-900: #31050a; +$maroon-a100: #ff636f; +$maroon-a200: #ff3540; +$maroon-a300: #991100; +$maroon-a700: #771100; diff --git a/src/lib/core/styles/tokens/_color-palette.scss b/src/lib/core/styles/tokens/color-palette/_material-color-palette.scss similarity index 100% rename from src/lib/core/styles/tokens/_color-palette.scss rename to src/lib/core/styles/tokens/color-palette/_material-color-palette.scss diff --git a/src/lib/core/styles/tokens/color-palette/index.scss b/src/lib/core/styles/tokens/color-palette/index.scss new file mode 100644 index 000000000..a9a61222b --- /dev/null +++ b/src/lib/core/styles/tokens/color-palette/index.scss @@ -0,0 +1,2 @@ +@forward './extended-color-palette'; +@forward './material-color-palette'; diff --git a/src/lib/core/styles/tokens/focus-indicator/_tokens.scss b/src/lib/core/styles/tokens/focus-indicator/_tokens.scss index 64f5f5961..15ddd3819 100644 --- a/src/lib/core/styles/tokens/focus-indicator/_tokens.scss +++ b/src/lib/core/styles/tokens/focus-indicator/_tokens.scss @@ -11,7 +11,7 @@ $tokens: ( active-width: utils.module-val(focus-indicator, active-width, 6px), color: utils.module-val(focus-indicator, color, theme.variable(primary)), duration: utils.module-val(focus-indicator, duration, animation.variable(duration-long4)), - outward-offset: utils.module-val(focus-indicator, outward-offset, spacing.variable('050')), + outward-offset: utils.module-val(focus-indicator, outward-offset, spacing.variable('025')), inward-offset: utils.module-val(focus-indicator, inward-offset, 0px), // Requires unit shape: utils.module-val(focus-indicator, shape, shape.variable(extra-small)), width: utils.module-val(focus-indicator, width, border.variable(medium)), diff --git a/src/lib/core/styles/tokens/linear-progress/_tokens.scss b/src/lib/core/styles/tokens/linear-progress/_tokens.scss index 6373dba8d..b69ffb265 100644 --- a/src/lib/core/styles/tokens/linear-progress/_tokens.scss +++ b/src/lib/core/styles/tokens/linear-progress/_tokens.scss @@ -6,7 +6,7 @@ $tokens: ( track-height: utils.module-val(linear-progress, track-height, 4px), - track-color: utils.module-val(linear-progress, track-color, theme.variable(tertiary-track-color)), + track-color: utils.module-val(linear-progress, track-color, theme.variable(tertiary-container)), active-indicator-color: utils.module-val(linear-progress, indicator-color, theme.variable(tertiary)), track-shape: utils.module-val(linear-progress, track-shape, shape.variable(full)), active-indicator-height: utils.module-ref(linear-progress, indicator-height, track-height), diff --git a/src/lib/core/styles/tokens/list/list-item/_tokens.scss b/src/lib/core/styles/tokens/list/list-item/_tokens.scss index 0250275a0..14d7138a3 100644 --- a/src/lib/core/styles/tokens/list/list-item/_tokens.scss +++ b/src/lib/core/styles/tokens/list/list-item/_tokens.scss @@ -6,7 +6,7 @@ $tokens: ( // Base - background-color: utils.module-val(list-item, color, theme.variable(surface)), + background-color: utils.module-val(list-item, background-color, transparent), shape: utils.module-val(list-item, shape, 0), padding: utils.module-val(list-item, padding, 8px 16px), margin: utils.module-val(list-item, margin, 0), @@ -16,7 +16,7 @@ $tokens: ( cursor: utils.module-val(list-item, cursor, pointer), // Supporting text - supporting-text-color: utils.module-val(list-item, supporting-text-color, theme.variable(text-secondary)), + supporting-text-color: utils.module-val(list-item, supporting-text-color, theme.variable(text-medium)), supporting-text-font-size: utils.module-val(list-item, supporting-text-font-size, typography.variable(body, font-size)), supporting-text-font-weight: utils.module-val(list-item, supporting-text-font-weight, typography.variable(body, font-weight)), supporting-text-line-height: utils.module-val(list-item, supporting-text-line-height, typography.scale(24)), @@ -26,7 +26,7 @@ $tokens: ( selected-opacity: utils.module-val(list-item, selected-opacity, 0.08), selected-leading-color: utils.module-ref(list-item, selected-leading-color, selected-color), selected-trailing-color: utils.module-ref(list-item, selected-trailing-color, selected-color), - selected-supporting-text-color: utils.module-val(list-item, selected-supporting-text-color, theme.variable(text-secondary)), + selected-supporting-text-color: utils.module-val(list-item, selected-supporting-text-color, theme.variable(text-medium)), // Disabled disabled-opacity: utils.module-val(list-item, disabled-opacity, 0.38), @@ -59,7 +59,7 @@ $tokens: ( trailing-selected-color: utils.module-ref(list-item, trailing-selected-color, selected-color), // Avatar - avatar-background-color: utils.module-val(list-item, avatar-background-color, theme.variable(text-secondary)), + avatar-background-color: utils.module-val(list-item, avatar-background-color, theme.variable(text-medium)), avatar-color: utils.module-val(list-item, avatar-color, theme.variable(on-primary)), avatar-shape: utils.module-val(list-item, avatar-shape, shape.variable(round)), avatar-margin-start: utils.module-val(list-item, avatar-margin-start, 0), diff --git a/src/lib/core/styles/tokens/scrollbar/_tokens.scss b/src/lib/core/styles/tokens/scrollbar/_tokens.scss new file mode 100644 index 000000000..76e1ca601 --- /dev/null +++ b/src/lib/core/styles/tokens/scrollbar/_tokens.scss @@ -0,0 +1,21 @@ +@use 'sass:map'; +@use '../../theme'; +@use '../../utils'; +@use '../../shape'; + +$tokens: ( + height: utils.module-val(scrollbar, height, 16px), + width: utils.module-val(scrollbar, width, 16px), + thumb-min-height: utils.module-val(scrollbar, thumb-min-height, 32px), + thumb-min-width: utils.module-val(scrollbar, thumb-min-width, 32px), + border-radius: utils.module-val(scrollbar, border-radius, shape.variable(full)), + border-width: utils.module-val(scrollbar, border-width, 3px), + track-container: utils.module-val(scrollbar, track-container, theme.variable(surface-container-low)), + track-container-hover: utils.module-val(scrollbar, track-container-hover, theme.variable(surface-container-low)), + thumb-container: utils.module-val(scrollbar, thumb-container, theme.variable(surface-container-medium)), + thumb-container-hover: utils.module-val(scrollbar, thumb-container-hover, theme.variable(surface-container-high)) +) !default; + +@function get($key) { + @return map.get($tokens, $key); +} diff --git a/src/lib/core/styles/tokens/slider/_tokens.scss b/src/lib/core/styles/tokens/slider/_tokens.scss index 858e9cdd0..8c1d7c25a 100644 --- a/src/lib/core/styles/tokens/slider/_tokens.scss +++ b/src/lib/core/styles/tokens/slider/_tokens.scss @@ -1,7 +1,6 @@ @use 'sass:map'; @use '../../theme'; @use '../../shape'; -@use '../color-palette'; @use '../../utils'; $tokens: ( @@ -9,19 +8,18 @@ $tokens: ( active-track-color: utils.module-val(slider, active-track-color, theme.variable(primary)), active-track-height: utils.module-ref(slider, active-track-height, track-height), active-track-shape: utils.module-val(slider, active-track-shape, shape.variable(full)), - disabled-active-track-color: utils.module-val(slider, disabled-active-track-color, color-palette.$grey-500), - disabled-active-track-opacity: utils.module-val(slider, disabled-active-track-opacity, 0.38), - disabled-handle-color: utils.module-val(slider, disabled-handle-color, color-palette.$grey-500), - disabled-inactive-track-color: utils.module-val(slider, disabled-inactive-track-color, color-palette.$grey-500), - disabled-inactive-track-opacity: utils.module-val(slider, disabled-inactive-track-opacity, 0.12), + disabled-active-track-color: utils.module-val(slider, disabled-active-track-color, theme.variable(surface-container-high)), + disabled-active-track-opacity: utils.module-val(slider, disabled-active-track-opacity, theme.emphasis(medium-low)), + disabled-handle-color: utils.module-val(slider, disabled-handle-color, theme.variable(surface-container-high)), + disabled-inactive-track-color: utils.module-val(slider, disabled-inactive-track-color, theme.variable(surface-container-high)), + disabled-inactive-track-opacity: utils.module-val(slider, disabled-inactive-track-opacity, theme.emphasis(lower)), focus-handle-color: utils.module-val(slider, focus-handle-color, theme.variable(primary)), handle-color: utils.module-val(slider, handle-color, theme.variable(primary)), handle-height: utils.module-val(slider, handle-height, 20px), handle-shape: utils.module-val(slider, handle-shape, shape.variable(round)), handle-width: utils.module-val(slider, handle-width, 20px), hover-handle-color: utils.module-val(slider, hover-handle-color, theme.variable(primary)), - inactive-track-color-fallback: utils.module-val(slider, inactive-track-color, #d5d8ee), // TODO: remove this when we're confident that color-mix is more widely supported - inactive-track-color: utils.module-val(slider, inactive-track-color, color-mix(in srgb, theme.variable(primary) 24%, transparent)), + inactive-track-color: utils.module-val(slider, inactive-track-color, theme.variable(primary-container)), inactive-track-height: utils.module-ref(slider, inactive-track-height, track-height), inactive-track-shape: utils.module-val(slider, inactive-track-shape, shape.variable(full)), label-container-shape: utils.module-val(slider, label-container-shape, shape.variable(full)), @@ -32,10 +30,10 @@ $tokens: ( state-layer-size: utils.module-val(slider, state-layer-size, 40px), with-overlap-handle-outline-color: utils.module-val(slider, with-overlap-handle-outline-color, theme.variable(on-primary)), with-overlap-handle-outline-width: utils.module-val(slider, with-overlap-handle-outline-width, 1px), - with-tick-marks-active-container-color: utils.module-val(slider, with-tick-marks-active-container-color, theme.variable(primary)), + with-tick-marks-active-container-color: utils.module-val(slider, with-tick-marks-active-container-color, theme.variable(on-primary)), with-tick-marks-container-size: utils.module-val(slider, with-tick-marks-container-size, 2px), - with-tick-marks-disabled-active-container-color: utils.module-val(slider, with-tick-marks-disabled-active-container-color, theme.variable(text-primary)), - with-tick-marks-disabled-inactive-container-color: utils.module-val(slider, with-tick-marks-disabled-inactive-container-color, color-mix(in srgb, theme.variable(on-primary) 50%, transparent)), + with-tick-marks-disabled-active-container-color: utils.module-val(slider, with-tick-marks-disabled-active-container-color, theme.variable(on-surface-container-high)), + with-tick-marks-disabled-inactive-container-color: utils.module-val(slider, with-tick-marks-disabled-inactive-container-color, theme.variable(on-surface-container-high)), with-tick-marks-inactive-container-color: utils.module-val(slider, with-tick-marks-inactive-container-color, theme.variable(primary)) ) !default; diff --git a/src/lib/core/styles/tokens/spacing/_tokens.scss b/src/lib/core/styles/tokens/spacing/_tokens.scss index bb755a169..f82a0a35a 100644 --- a/src/lib/core/styles/tokens/spacing/_tokens.scss +++ b/src/lib/core/styles/tokens/spacing/_tokens.scss @@ -1,15 +1,15 @@ -$base: 8px !default; // The Forge spacing system scales in multiples of 8 +$base: 16px !default; // The Forge spacing system scales in multiples of 8 $tokens: ( - '025': $base * 0.25, // 2px - '050': $base * 0.5, // 4px - '100': $base, // 8px - '150': $base * 1.5, // 12px - '200': $base * 2, // 16px - '300': $base * 3, // 24px - '400': $base * 4, // 32px - '600': $base * 6, // 48px - '700': $base * 7, // 56px + '0125': $base * 0.125, // 2px + '025': $base * 0.25, // 4px + '050': $base * 0.5, // 8px + '075': $base * 0.75, // 12px + '100': $base, // 16px + '150': $base * 1.5, // 24px + '200': $base * 2, // 32px + '300': $base * 3, // 48px + '350': $base * 3.5, // 56px ) !default; @function get($key) { diff --git a/src/lib/core/styles/tokens/switch/_tokens.scss b/src/lib/core/styles/tokens/switch/_tokens.scss index 8fcaa548c..fc257a14c 100644 --- a/src/lib/core/styles/tokens/switch/_tokens.scss +++ b/src/lib/core/styles/tokens/switch/_tokens.scss @@ -15,15 +15,15 @@ $tokens: ( track-border-width: utils.module-val(switch, track-border-width, 0), track-border-color: utils.module-val(switch, track-border-color, transparent), - icon-color: utils.module-val(switch, icon-color, theme.variable(on-primary)), + icon-color: utils.module-val(switch, icon-color, theme.variable(on-tertiary)), icon-scale: utils.module-val(switch, icon-scale, 1), state-layer-size: utils.module-val(switch, state-layer-size, 40px), state-layer-dense-size: utils.module-val(switch, handle-dense-size, 28px), // Handle - handle-on-color: utils.module-val(switch, handle-on-color, theme.variable(primary)), - handle-off-color: utils.module-val(switch, handle-off-color, theme.variable(handle-off-color)), + handle-on-color: utils.module-val(switch, handle-on-color, theme.variable(tertiary)), + handle-off-color: utils.module-val(switch, handle-off-color, theme.variable(surface-container-high)), handle-width: utils.module-ref(switch, handle-width, handle-size), handle-height: utils.module-ref(switch, handle-height, handle-size), handle-on-scale: utils.module-ref(switch, handle-on-scale, handle-scale), @@ -33,8 +33,8 @@ $tokens: ( handle-off-elevation: utils.module-ref(switch, handle-off-elevation, handle-elevation), // Track - track-on-color: utils.module-val(switch, track-on-color, theme.variable(primary-track-color)), - track-off-color: utils.module-val(switch, track-off-color, theme.variable(track-color)), + track-on-color: utils.module-val(switch, track-on-color, theme.variable(tertiary-container)), + track-off-color: utils.module-val(switch, track-off-color, theme.variable(surface-container)), track-width: utils.module-val(switch, track-width, 36px), track-height: utils.module-val(switch, track-height, 14px), track-shape: utils.module-val(switch, track-shape, shape.variable(full)), diff --git a/src/lib/core/styles/tokens/tabs/tab-bar/_tokens.scss b/src/lib/core/styles/tokens/tabs/tab-bar/_tokens.scss index 6376d9325..310c8a43d 100644 --- a/src/lib/core/styles/tokens/tabs/tab-bar/_tokens.scss +++ b/src/lib/core/styles/tokens/tabs/tab-bar/_tokens.scss @@ -5,7 +5,7 @@ $tokens: ( container-justify: utils.module-val(tab-bar, justify, space-between), tab-flex: utils.module-val(tab-bar, stretch, 1), - divider-color: utils.module-val(tab-bar, divider-color, theme.variable(border-color)), + divider-color: utils.module-val(tab-bar, divider-color, theme.variable(outline)), divider-thickness: utils.module-val(tab-bar, divider-thickness, 1px) ); diff --git a/src/lib/core/styles/tokens/tabs/tab/_tokens.scss b/src/lib/core/styles/tokens/tabs/tab/_tokens.scss index ab20a8b4d..158d1a15a 100644 --- a/src/lib/core/styles/tokens/tabs/tab/_tokens.scss +++ b/src/lib/core/styles/tokens/tabs/tab/_tokens.scss @@ -1,11 +1,12 @@ @use 'sass:map'; @use '../../../theme'; @use '../../../utils'; +@use '../../../spacing'; $tokens: ( // Shared active-color: utils.module-val(tab, active-color, theme.variable(primary)), - inactive-color: utils.module-val(tab, inactive-color, theme.variable(text-secondary)), + inactive-color: utils.module-val(tab, inactive-color, theme.variable(text-medium)), height: utils.module-val(tab, height, 48px), stacked-height: utils.module-val(tab, stacked-height, 64px), @@ -35,6 +36,7 @@ $tokens: ( // Content content-height: utils.module-ref(tab, content-height, height), + content-padding: utils.module-val(tab, content-padding, spacing.variable('025')), // Icon active-focus-icon-color: utils.module-ref(tab, active-focus-icon-color, active-color), diff --git a/src/lib/core/styles/tokens/theme/_color-emphasis.scss b/src/lib/core/styles/tokens/theme/_color-emphasis.scss new file mode 100644 index 000000000..e9bd4dda8 --- /dev/null +++ b/src/lib/core/styles/tokens/theme/_color-emphasis.scss @@ -0,0 +1,33 @@ +@use 'sass:map'; + +$color-emphasis-scale: ( + '004': 4%, + '008': 8%, + '012': 12%, + '024': 24%, + '038': 38%, + '054': 54%, + '060': 60%, + '065': 65%, + '070': 70%, + '080': 80%, + '087': 87% +); + +$color-emphasis: ( + highest: map.get($color-emphasis-scale, '087'), + inverse: map.get($color-emphasis-scale, '080'), + higher: map.get($color-emphasis-scale, '070'), + high: map.get($color-emphasis-scale, '065'), + medium-high: map.get($color-emphasis-scale, '060'), + medium: map.get($color-emphasis-scale, '054'), + medium-low: map.get($color-emphasis-scale, '038'), + low: map.get($color-emphasis-scale, '024'), + lower: map.get($color-emphasis-scale, '012'), + lowest: map.get($color-emphasis-scale, '008'), + minimum: map.get($color-emphasis-scale, '004'), +); + +@function value($emphasis) { + @return map.get($color-emphasis, $emphasis); +} diff --git a/src/lib/core/styles/tokens/theme/_token-utils.scss b/src/lib/core/styles/tokens/theme/_token-utils.scss new file mode 100644 index 000000000..11dc021b7 --- /dev/null +++ b/src/lib/core/styles/tokens/theme/_token-utils.scss @@ -0,0 +1,41 @@ +@use '../../theme/utils' as theme-utils; +@use '../../theme/color-utils'; +@use './color-emphasis'; + +/// +/// Computes the dependent theme colors from the provided theme color value. +/// +/// @param {String} $name - The name of the theme color +/// @param {Color} $color - The theme color +/// @param {Color} $surface - The base surface color +/// @return {Map} - The computed theme map +/// +@function get-color-theme($name, $color, $surface) { + @if not theme-utils.is-color($color) { + @error 'The value for "#{$name}" must be a color type.'; + } + + $surface-tone: color-utils.tone($surface); + + // The container colors are the provided color mixed with the surface color at lower emphasis levels + $container: theme-utils.hexify($color, $surface, color-emphasis.value(if($surface-tone == 'light', low, medium-low))); + // $container-high: theme-utils.hexify($color, $surface, color-emphasis.value(if($surface-tone == 'light', medium-low, medium))); + + // The on-color is the contrast color against the provided color + $on-color: theme-utils.contrast($color); + + // The on-container colors are the contrast color for the provided color mixed with the + // container color at a lower emphasis to let the contrast color bleed through for + // increased contrast against the lower emphasis container color + $on-container: theme-utils.hexify($color, theme-utils.contrast($container), color-emphasis.value(low)); + // $on-container-high: theme-utils.contrast($container-high); + + @return ( + #{$name}: $color, + #{$name}-container: $container, + // #{$name}-container-high: $container-high, + on-#{$name}: $on-color, + on-#{$name}-container: $on-container, + // on-#{$name}-container-high: $on-container-high + ); +} diff --git a/src/lib/core/styles/tokens/theme/_tokens.core.scss b/src/lib/core/styles/tokens/theme/_tokens.core.scss new file mode 100644 index 000000000..0079710fd --- /dev/null +++ b/src/lib/core/styles/tokens/theme/_tokens.core.scss @@ -0,0 +1,40 @@ +@use 'sass:map'; +@use '../../utils'; +@use './token-utils'; +@use './tokens.surface' as surface; +@use '../color-palette'; + +/// +/// Computes the status theme colors. +/// +/// @param {Map} $colors - The status colors map. +/// @param {Map} $surface-theme - The base surface theme +/// @return {Map} - The computed status theme +/// +@function get-status-theme($colors, $surface-theme) { + $surface: map.get($surface-theme, surface); + + $primary-theme: token-utils.get-color-theme('primary', map.get($colors, primary), $surface); + $secondary-theme: token-utils.get-color-theme('secondary', map.get($colors, secondary), $surface); + $tertiary-theme: token-utils.get-color-theme('tertiary', map.get($colors, tertiary), $surface); + + @return utils.flatten( + $primary-theme, + $secondary-theme, + $tertiary-theme + ); +} + +// Light +$tokens: get-status-theme(( + primary: color-palette.$indigo-500, + secondary: color-palette.$amber-500, + tertiary: color-palette.$indigo-a400 +), surface.$tokens); + +// Dark +$tokens-dark: get-status-theme(( + primary: color-palette.$indigo-a100, + secondary: color-palette.$amber-200, + tertiary: color-palette.$amber-200 +), surface.$tokens-dark); diff --git a/src/lib/core/styles/tokens/theme/_tokens.scss b/src/lib/core/styles/tokens/theme/_tokens.scss index 1cdedfd10..0a223a916 100644 --- a/src/lib/core/styles/tokens/theme/_tokens.scss +++ b/src/lib/core/styles/tokens/theme/_tokens.scss @@ -1,130 +1,73 @@ @use 'sass:map'; -@use '../../core/config'; -@use '../color-palette' as color-palette; -@use '../../elevation'; +@use '../../utils'; +@use '../../theme/utils' as theme-utils; +@use './tokens.core' as core; +@use './tokens.surface' as surface; +@use './tokens.text' as text; +@use './tokens.status' as status; +@use './tokens.utilities' as utilities; -/// All semantic tokens for the Forge default (light) theme -$tokens: ( - // General theme colors - primary: color-palette.$indigo-500, - on-primary: #ffffff, - secondary: color-palette.$amber-500, - on-secondary: #000000, - tertiary: color-palette.$indigo-a400, - on-tertiary: #ffffff, - background: color-palette.$grey-50, - surface: #ffffff, - on-surface: #000000, - - // Status colors - success: color-palette.$green-800, - on-success: #ffffff, - error: #b00020, - on-error: #ffffff, - warning: #d14900, - on-warning: #ffffff, - - // Text colors - text-primary: rgba(0, 0, 0, 0.87), - text-secondary: rgba(0, 0, 0, 0.54), - text-disabled: rgba(0, 0, 0, 0.12), - text-hint: rgba(0, 0, 0, 0.38), - text-icon: rgba(0, 0, 0, 0.38), - text-primary-on-light: rgba(0, 0, 0, 0.87), - text-secondary-on-light: rgba(0, 0, 0, 0.54), - text-disabled-on-light: rgba(0, 0, 0, 0.38), - text-primary-on-dark: rgba(255, 255, 255, 0.87), - text-secondary-on-dark: rgba(255, 255, 255, 0.7), - - // Utility semantic colors - track-color: rgba(0, 0, 0, 0.12), - primary-track-color: #d0d4ef, - secondary-track-color: #fff0c3, - tertiary-track-color: #d0d7ff, - border-color: color-palette.$grey-300, - icon-color: color-palette.$grey-600, - form-field-label-on-background: rgba(0, 0, 0, 0.65), - form-field-icon-disabled-on-background: rgba(0, 0, 0, 0.26), - form-field-text-disabled-on-background: rgba(0, 0, 0, 0.6), - form-field-disabled-on-background: color-palette.$grey-100, - label-disabled-on-background: rgba(0, 0, 0, 0.38), - elevated-surface: #ffffff, - on-elevated-surface: rgba(0, 0, 0, 0.87), - popup-elevation: elevation.value(2), - scrollbar-thumb: color-palette.$grey-400, - scrollbar-thumb-hover: color-palette.$grey-500, - scrollbar-track: #f0f0f0, - scrollbar-track-hover: #ececec, - handle-off-color: color-palette.$grey-500 -) !default; - -/// All tokens for the Forge dark theme -$tokens-dark: ( - // General theme colors - primary: color-palette.$indigo-a100, - on-primary: #000000, - secondary: color-palette.$amber-200, - on-secondary: #000000, - tertiary: var(--forge-theme-secondary, color-palette.$amber-200), - on-tertiary: #ffffff, - background: color-palette.$grey-900, - surface: #2c2c2c, - on-surface: #ffffff, +// Forge default (light) theme +$tokens: utils.flatten( + core.$tokens, + surface.$tokens, + text.$tokens, + status.$tokens, + utilities.$tokens, + ( + // Temporary Forge-specific compatibility tokens (TODO: remove this when form field & popup are refactored) + border-color: utils.variable-ref(theme, outline), + elevated-surface: utils.variable-ref(theme, surface-bright), + on-elevated-surface: utils.variable-ref(theme, on-surface), + popup-elevation: utils.variable-ref(theme, surface-bright-shadow), + form-field-label-on-background: theme-utils.emphasized(map.get(text.$tokens, text-high), 65%), + form-field-icon-disabled-on-background: theme-utils.emphasized(map.get(text.$tokens, text-high), 26%), + form-field-text-disabled-on-background: theme-utils.emphasized(map.get(text.$tokens, text-high), 60%), + form-field-disabled-on-background: theme-utils.emphasized(map.get(text.$tokens, text-high), 4%), + label-disabled-on-background: theme-utils.emphasized(map.get(text.$tokens, text-high), 38%) + ) +); - // Status colors - text-primary: rgba(255, 255, 255, 0.87), - text-secondary: rgba(255, 255, 255, 0.54), - text-disabled: rgba(255, 255, 255, 0.12), - text-hint: rgba(255, 255, 255, 0.38), - text-icon: rgba(255, 255, 255, 0.38), - text-disabled-on-background: rgba(255, 255, 255, 0.12), - text-disabled-on-light: rgba(255, 255, 255, 0.38), - text-secondary-on-light: rgba(255, 255, 255, 0.54), - text-primary-on-dark: #000000, - // Utility semantic colors - track-color: rgba(255, 255, 255, 0.12), - primary-track-color: #e3e8ff, - secondary-track-color: #7c704c, - tertiary-track-color: var(--forge-theme-secondary-track-color, #7c704c), - border-color: #464646, - form-field-icon-disabled-on-background: rgba(78, 45, 45, 0.26), - form-field-disabled-on-background: #353535, - form-field-text-disabled-on-background: rgba(255, 255, 255, 0.6), - form-field-label-on-background: rgba(255, 255, 255, 0.65), - label-disabled-on-background: rgba(255, 255, 255, 0.38), - scrollbar-thumb: color-palette.$grey-600, - scrollbar-thumb-hover: color-palette.$grey-500, - scrollbar-track: #3a3a3a, - scrollbar-track-hover: color-palette.$grey-800, - elevated-surface: #363636, - on-elevated-surface: rgba(255, 255, 255, 0.87), - popup-elevation: elevation.value(16), - handle-off-color: color-palette.$grey-500 -) !default; +// Forge dark theme +$tokens-dark: utils.flatten( + core.$tokens-dark, + surface.$tokens-dark, + text.$tokens-dark, + status.$tokens-dark, + utilities.$tokens-dark, + ( + // Temporary Forge-specific compatibility tokens (TODO: remove this when form field is refactored) + form-field-label-on-background: theme-utils.emphasized(map.get(text.$tokens-dark, text-high), 65%), + form-field-icon-disabled-on-background: theme-utils.emphasized(map.get(text.$tokens-dark, text-high), 26%), + form-field-text-disabled-on-background: theme-utils.emphasized(map.get(text.$tokens-dark, text-high), 60%), + form-field-disabled-on-background: theme-utils.emphasized(map.get(text.$tokens-dark, text-high), 4%), + label-disabled-on-background: theme-utils.emphasized(map.get(text.$tokens-dark, text-high), 38%) + ) +); /// -/// Temporary tokens to map the MDC theme to Forge theme +/// Temporary tokens to map the MDC theme to the Forge theme. /// /// TODO: Remove this once MDC dependency is removed /// $mdc-remap: ( - primary: var(--forge-theme-primary, map.get($tokens, primary)), - on-primary: var(--forge-theme-on-primary, map.get($tokens, on-primary)), - secondary: var(--forge-theme-secondary, map.get($tokens, secondary)), - on-secondary: var(--forge-theme-on-secondary, map.get($tokens, on-secondary)), - background: var(--forge-theme-background, map.get($tokens, background)), - surface: var(--forge-theme-surface, map.get($tokens, surface)), - on-surface: var(--forge-theme-on-surface, map.get($tokens, on-surface)), - error: var(--forge-theme-error, map.get($tokens, error)), - on-error: var(--forge-theme-on-error, map.get($tokens, on-error)), - text-primary-on-background: var(--forge-theme-text-primary, map.get($tokens, text-primary)), - text-secondary-on-background: var(--forge-theme-text-secondary, map.get($tokens, text-secondary)), - text-disabled-on-background: var(--forge-theme-text-disabled, map.get($tokens, text-disabled)), - text-hint-on-background: var(--forge-theme-text-hint, map.get($tokens, text-hint)), - text-icon-on-background: var(--forge-theme-text-icon, map.get($tokens, text-icon)), - text-primary-on-light: var(--forge-theme-text-primary-on-light, map.get($tokens, text-primary-on-light)), - text-secondary-on-light: var(--forge-theme-text-secondary-on-light, map.get($tokens, text-secondary-on-light)), - text-disabled-on-light: var(--forge-theme-text-disabled-on-light, map.get($tokens, text-disabled-on-light)), - text-primary-on-dark: var(--forge-theme-text-primary-on-dark, map.get($tokens, text-primary-on-dark)), + primary: utils.variable-ref(theme, primary), + on-primary: utils.variable-ref(theme, on-primary), + secondary: utils.variable-ref(theme, secondary), + on-secondary: utils.variable-ref(theme, on-secondary), + background: utils.variable-ref(theme, surface-dim), + surface: utils.variable-ref(theme, surface), + on-surface: utils.variable-ref(theme, on-surface), + error: utils.variable-ref(theme, error), + on-error: utils.variable-ref(theme, on-error), + text-primary-on-background: utils.variable-ref(theme, text-high), + text-secondary-on-background: utils.variable-ref(theme, text-medium), + text-disabled-on-background: utils.variable-ref(theme, text-lowest), + text-hint-on-background: utils.variable-ref(theme, text-low), + text-icon-on-background: utils.variable-ref(theme, text-medium), + text-primary-on-light: utils.variable-ref(theme, text-high), + text-secondary-on-light: utils.variable-ref(theme, text-medium), + text-disabled-on-light: utils.variable-ref(theme, text-low), + text-primary-on-dark: utils.variable-ref(theme, text-high-inverse), ); diff --git a/src/lib/core/styles/tokens/theme/_tokens.status.scss b/src/lib/core/styles/tokens/theme/_tokens.status.scss new file mode 100644 index 000000000..890bc3645 --- /dev/null +++ b/src/lib/core/styles/tokens/theme/_tokens.status.scss @@ -0,0 +1,44 @@ +@use 'sass:map'; +@use '../../utils'; +@use './token-utils'; +@use './tokens.surface' as surface; +@use '../color-palette'; + +/// +/// Computes the status theme colors. +/// +/// @param {Map} $colors - The status colors map. +/// @param {Map} $surface-theme - The base surface theme +/// @return {Map} - The computed status theme +/// +@function get-status-theme($colors, $surface-theme) { + $surface: map.get($surface-theme, surface); + + $success-theme: token-utils.get-color-theme('success', map.get($colors, success), $surface); + $error-theme: token-utils.get-color-theme('error', map.get($colors, error), $surface); + $warning-theme: token-utils.get-color-theme('warning', map.get($colors, warning), $surface); + $info-theme: token-utils.get-color-theme('info', map.get($colors, info), $surface); + + @return utils.flatten( + $success-theme, + $error-theme, + $warning-theme, + $info-theme + ); +} + +// Light +$tokens: get-status-theme(( + success: color-palette.$green-800, + error: color-palette.$crimson-900, + warning: color-palette.$burnt-orange-800, + info: color-palette.$blue-800 +), surface.$tokens); + +// Dark +$tokens-dark: get-status-theme(( + success: color-palette.$dollar-bill-600, + error: color-palette.$ruddy-pink-200, + warning: color-palette.$apricot-200, + info: color-palette.$blue-800, +), surface.$tokens-dark); diff --git a/src/lib/core/styles/tokens/theme/_tokens.surface.scss b/src/lib/core/styles/tokens/theme/_tokens.surface.scss new file mode 100644 index 000000000..c25e7e94f --- /dev/null +++ b/src/lib/core/styles/tokens/theme/_tokens.surface.scss @@ -0,0 +1,60 @@ +@use 'sass:map'; +@use '../../theme/utils' as theme-utils; +@use '../../theme/color-utils'; +@use '../../elevation'; +@use './color-emphasis'; +@use '../color-palette'; + +/// +/// Computes the surface theme colors. +/// +/// @param {Map} $theme - The surface theme map. +/// @return {Map} - The computed surface theme map +/// +@function get-surface-theme($theme) { + $surface: map.get($theme, surface); + $on-surface: theme-utils.contrast($surface); + $surface-tone: color-utils.tone($surface); + $surface-inverse: theme-utils.hexify($on-surface, $surface, color-emphasis.value(inverse)); + + $surface-container: theme-utils.hexify($on-surface, $surface, color-emphasis.value(lower)); + $surface-container-low: theme-utils.hexify($on-surface, $surface, color-emphasis.value(lowest)); + $surface-container-medium: theme-utils.hexify($on-surface, $surface, color-emphasis.value(if($surface-tone == 'light', low, medium-low))); + $surface-container-high: theme-utils.hexify($on-surface, $surface, color-emphasis.value(if($surface-tone == 'light', medium-low, medium))); + + @return ( + surface: $surface, + surface-inverse: $surface-inverse, + surface-container: $surface-container, + surface-container-low: $surface-container-low, + surface-container-medium: $surface-container-medium, + surface-container-high: $surface-container-high, + surface-dim: map.get($theme, surface-dim), + surface-bright: map.get($theme, surface-bright), + surface-bright-shadow: map.get($theme, surface-bright-shadow), + on-surface: $on-surface, + on-surface-inverse: theme-utils.contrast($surface-inverse), + on-surface-container: theme-utils.contrast($surface-container), + on-surface-container-low: theme-utils.contrast($surface-container-low), + on-surface-container-medium: theme-utils.contrast($surface-container-medium), + on-surface-container-high: theme-utils.contrast($surface-container-high) + ); +} + +// Light +$tokens: get-surface-theme(( + surface: color-palette.$neutral-50, + surface-bright: color-palette.$neutral-50, + surface-bright-shadow: elevation.value(2), + surface-dim: color-palette.$grey-50 +)); + +// Dark +$surface-dark: color-palette.$neutral-900; +$on-surface-dark: theme-utils.contrast($surface-dark); +$tokens-dark: get-surface-theme(( + surface: color-palette.$neutral-900, + surface-bright: theme-utils.hexify($on-surface-dark, $surface-dark, color-emphasis.value(minimum)), + surface-bright-shadow: elevation.value(16), + surface-dim: color-palette.$grey-900 +)); diff --git a/src/lib/core/styles/tokens/theme/_tokens.text.scss b/src/lib/core/styles/tokens/theme/_tokens.text.scss new file mode 100644 index 000000000..cf166b837 --- /dev/null +++ b/src/lib/core/styles/tokens/theme/_tokens.text.scss @@ -0,0 +1,33 @@ +@use 'sass:map'; +@use '../../theme/utils' as theme-utils; +@use './color-emphasis'; +@use './tokens.surface' as surface; + +/// +/// Computes the text theme colors. +/// +/// @param {Color} $surface-theme - The base surface theme +/// @return {Map} - The computed text theme +/// +@function get-text-theme($surface-theme) { + $surface: map.get($surface-theme, surface); + $text: theme-utils.contrast($surface); + $text-inverse: theme-utils.contrast($text); + + @return ( + text-high: theme-utils.emphasized($text, color-emphasis.value(highest)), + text-high-inverse: theme-utils.emphasized($text-inverse, color-emphasis.value(highest)), + text-medium: theme-utils.emphasized($text, color-emphasis.value(medium)), + text-medium-inverse: theme-utils.emphasized($text-inverse, color-emphasis.value(medium)), + text-low: theme-utils.emphasized($text, color-emphasis.value(medium-low)), + text-low-inverse: theme-utils.emphasized($text-inverse, color-emphasis.value(medium-low)), + text-lowest: theme-utils.emphasized($text, color-emphasis.value(lowest)), + text-lowest-inverse: theme-utils.emphasized($text-inverse, color-emphasis.value(lowest)), + ); +} + +// Light +$tokens: get-text-theme(surface.$tokens); + +// Dark +$tokens-dark: get-text-theme(surface.$tokens-dark); diff --git a/src/lib/core/styles/tokens/theme/_tokens.utilities.scss b/src/lib/core/styles/tokens/theme/_tokens.utilities.scss new file mode 100644 index 000000000..d79ecc906 --- /dev/null +++ b/src/lib/core/styles/tokens/theme/_tokens.utilities.scss @@ -0,0 +1,25 @@ +@use 'sass:map'; +@use '../../theme/utils' as theme-utils; +@use './color-emphasis'; +@use './tokens.surface' as surface; + +/// +/// Computes the utilities theme colors. +/// +/// @param {Color} $surface-theme - The base surface theme +/// @return {Map} - The computed utilities theme +/// +@function get-utilities-theme($surface-theme) { + $surface: map.get($surface-theme, surface); + $on-surface: map.get($surface-theme, on-surface); + + @return ( + outline: theme-utils.hexify($on-surface, $surface, color-emphasis.value(lower)) + ); +} + +// Light +$tokens: get-utilities-theme(surface.$tokens); + +// Dark +$tokens-dark: get-utilities-theme(surface.$tokens-dark); diff --git a/src/lib/core/styles/tokens/typography/_tokens.scss b/src/lib/core/styles/tokens/typography/_tokens.scss index 6f54d5bcf..33e946bc5 100644 --- a/src/lib/core/styles/tokens/typography/_tokens.scss +++ b/src/lib/core/styles/tokens/typography/_tokens.scss @@ -46,7 +46,7 @@ $base: ( letter-spacing: normal, text-transform: inherit, text-decoration: inherit, - color: theme.variable(text-primary) + color: theme.variable(text-high) ) !default; /// @@ -147,7 +147,7 @@ $subtitle: ( letter-spacing: 0.009375em, text-transform: inherit, text-decoration: inherit, - color: theme.variable(text-secondary) + color: theme.variable(text-medium) ) !default; $body-01: utils.inherit-map($base, ( diff --git a/src/lib/slider/_core.scss b/src/lib/slider/_core.scss index d2b3bec6c..2b5f8deba 100644 --- a/src/lib/slider/_core.scss +++ b/src/lib/slider/_core.scss @@ -98,13 +98,7 @@ $_active-track-end-clip: calc($_active-track-end-offset + $_active-track-max-cli @mixin track-inactive { block-size: var(--_inactive-track-height); border-radius: var(--_inactive-track-shape); - - // TODO: remove this and the @supports check below when we're confident that color-mix is more widely supported - background-color: var(--_inactive-track-color-fallback); - - @supports (background-color: color-mix(in srgb, red 50%, transparent)) { - background-color: var(--_inactive-track-color); - } + background-color: var(--_inactive-track-color); } @mixin track-active-disabled { diff --git a/src/lib/slider/slider.scss b/src/lib/slider/slider.scss index fa13af04a..2d43fc59a 100644 --- a/src/lib/slider/slider.scss +++ b/src/lib/slider/slider.scss @@ -131,11 +131,11 @@ &.tickmarks { &::before { - @include core.tickmarks-active-disabled; + @include core.tickmarks-inactive-disabled; } &::after { - @include core.tickmarks-inactive-disabled; + @include core.tickmarks-active-disabled; } } } diff --git a/src/lib/tabs/tab/_configuration.scss b/src/lib/tabs/tab/_configuration.scss index fb98b1626..b87b1e9ba 100644 --- a/src/lib/tabs/tab/_configuration.scss +++ b/src/lib/tabs/tab/_configuration.scss @@ -5,6 +5,10 @@ --_active-color: #{tab-tokens.get(active-color)}; --_inactive-color: #{tab-tokens.get(inactive-color)}; --_height: #{tab-tokens.get(height)}; + --_stacked-height: #{tab-tokens.get(stacked-height)}; + + // Disabled + --_disabled-opacity: #{tab-tokens.get(disabled-opacity)}; // Indicator --_active-indicator-color: #{tab-tokens.get(indicator-color)}; @@ -18,6 +22,7 @@ // Content --_content-height: #{tab-tokens.get(content-height)}; + --_content-padding: #{tab-tokens.get(content-padding)}; // Icon --_active-focus-icon-color: #{tab-tokens.get(active-focus-icon-color)}; diff --git a/src/lib/tabs/tab/_core.scss b/src/lib/tabs/tab/_core.scss index 056d3e32f..f6ffc49b1 100644 --- a/src/lib/tabs/tab/_core.scss +++ b/src/lib/tabs/tab/_core.scss @@ -1,7 +1,6 @@ @use '../../core/styles/utils'; @use '../../core/styles/tokens/tabs/tab/tokens'; @use '../../core/styles/typography'; -@use '../../core/styles/spacing'; @mixin provide-theme($theme) { @include utils.provide(tokens.$tokens, $theme, tab); @@ -16,7 +15,10 @@ @mixin host-disabled { cursor: not-allowed; - opacity: #{tokens.get(disabled-opacity)}; +} + +@mixin disabled { + opacity: var(--_disabled-opacity); } @mixin tab { @@ -118,12 +120,10 @@ justify-content: center; white-space: nowrap; transition: 150ms color linear; - - $_content-padding: spacing.variable('100'); - max-height: calc(var(--_content-height) + 2 * $_content-padding); + max-height: calc(var(--_content-height) + 2 * var(--_content-padding)); min-height: var(--_content-height); - padding: $_content-padding calc(2 * $_content-padding); - gap: spacing.variable('050'); + padding: var(--_content-padding) calc(2 * var(--_content-padding)); + gap: var(--_content-padding); } @mixin label { diff --git a/src/lib/tabs/tab/tab.scss b/src/lib/tabs/tab/tab.scss index 8adce6796..25771efb9 100644 --- a/src/lib/tabs/tab/tab.scss +++ b/src/lib/tabs/tab/tab.scss @@ -1,6 +1,5 @@ @use './core'; @use './configuration'; -@use '../../core/styles/tokens/tabs/tab/tokens'; @use '../../focus-indicator'; @use '../../state-layer'; @@ -28,6 +27,8 @@ @include core.host-disabled; .forge-tab { + @include core.disabled; + pointer-events: none; } } @@ -80,7 +81,7 @@ :host([stacked]) { .forge-tab { - --_height: #{tokens.get(stacked-height)}; + --_height: var(--_stacked-height); .content { @include core.content-stacked; diff --git a/src/lib/theme/_theme.scss b/src/lib/theme/_theme.scss index 214d25d4e..24a3a86c3 100644 --- a/src/lib/theme/_theme.scss +++ b/src/lib/theme/_theme.scss @@ -9,12 +9,7 @@ @use './keys'; @use './theme-values'; @use './theme-utils'; -@use '../core/styles/theme'; - -@mixin theme-styles($prefix: forge) { - @include theme.properties; - @include theme-utils.core-styles; -} +@use '../core/styles/scrollbar'; @function text-emphasis($emphasis) { @return map.get(theme-values.$text-emphasis, $emphasis); @@ -156,45 +151,8 @@ ), $important: $important); } -@mixin scrollbar() { - &::-webkit-scrollbar { - @include css-custom-property(width, --forge-scrollbar-width, theme-values.$scrollbar-width); - @include css-custom-property(height, --forge-scrollbar-height, theme-values.$scrollbar-height); - } - - &::-webkit-scrollbar-corner { - @include property(background-color, scrollbar-track); - } - - &::-webkit-scrollbar-track { - @include property(background-color, scrollbar-track); - - &:hover { - @include property(background-color, scrollbar-track-hover); - } - } - - &::-webkit-scrollbar-thumb { - @include property(background-color, scrollbar-thumb); - @include css-custom-property(height, --forge-scrollbar-min-height, theme-values.$scrollbar-thumb-min-height); - @include css-custom-property(width, --forge-scrollbar-min-width, theme-values.$scrollbar-thumb-min-width); - @include css-custom-property(border-radius, --forge-scrollbar-border-radius, 10px); - @include css-custom-property(border-width, --forge-scrollbar-border-width, 3px); - - border-style: solid; - border-color: transparent; - background-clip: content-box; - - &:hover { - @include property(background-color, scrollbar-thumb-hover); - } - } -} - -@mixin scrollbar-theme-styles() { - * { - @include scrollbar; - } +@mixin scrollbar { + @include scrollbar.base; } @function elevation($z-value, $color: elevation-theme.$baseline-color, $opacity-boost: 0) { diff --git a/src/lib/theme/forge-theme.scss b/src/lib/theme/forge-theme.scss index f1c583395..5b111a9f4 100644 --- a/src/lib/theme/forge-theme.scss +++ b/src/lib/theme/forge-theme.scss @@ -1,4 +1,22 @@ -@use './theme'; +@use '../core/styles/theme'; +@use '../core/styles/scrollbar'; -@include theme.theme-styles; -@include theme.scrollbar-theme-styles; +// Temporary theme properties for compatibility with MDC theme tokens +// +// TODO: remove this once MDC dependency is removed +:root { + @include theme.properties-compat; +} + +// Root theme properties +:root { + @include theme.properties; +} + +// Custom scrollbar styles +* { + @include scrollbar.base; +} + +// Utility classes for all theme tokens +@include theme.classes; From 8c97129dfb5ed6ba131e887c03cc9780236ae4b4 Mon Sep 17 00:00:00 2001 From: Sam Richardson Date: Fri, 27 Oct 2023 10:42:13 -0400 Subject: [PATCH 34/38] Merge branch 'checkbox-next' of https://github.com/tyler-technologies-oss/forge into checkbox-next From 8d70fde6fd3c5687ab9bfba2bbb003faef98fc52 Mon Sep 17 00:00:00 2001 From: Kieran Nichols Date: Fri, 27 Oct 2023 13:20:16 -0400 Subject: [PATCH 35/38] [@next] refactor typography system (#418) --- src/dev/pages/avatar/avatar.ejs | 12 +- src/dev/pages/badge/badge.ejs | 4 +- src/dev/pages/button-area/button-area.ejs | 4 +- src/dev/pages/button-toggle/button-toggle.ejs | 4 +- src/dev/pages/card/card.ejs | 4 +- src/dev/pages/checkbox/checkbox.ejs | 16 +- src/dev/pages/chips/chips.ejs | 20 +- .../pages/dialog/dialog-template-basic.ejs | 8 +- src/dev/pages/divider/divider.ejs | 4 +- .../pages/drawer/drawer-dismissible-right.ejs | 4 +- src/dev/pages/drawer/drawer-dismissible.ejs | 4 +- .../pages/drawer/drawer-mini-hover-right.ejs | 4 +- src/dev/pages/drawer/drawer-mini-hover.ejs | 4 +- src/dev/pages/drawer/drawer-mini.ejs | 2 +- src/dev/pages/drawer/drawer-modal.ejs | 4 +- src/dev/pages/drawer/drawer-permanent.ejs | 4 +- .../pages/expansion-panel/expansion-panel.ejs | 8 +- src/dev/pages/file-picker/file-picker.ejs | 6 +- .../pages/focus-indicator/focus-indicator.ejs | 4 +- src/dev/pages/icon-button/icon-button.ejs | 10 +- src/dev/pages/icon/icon.ejs | 12 +- src/dev/pages/list/list.ejs | 50 ++-- src/dev/pages/popup/popup.ejs | 2 +- src/dev/pages/radio/radio.ejs | 8 +- src/dev/pages/ripple/ripple.ejs | 8 +- src/dev/pages/skeleton/skeleton.ejs | 16 +- src/dev/pages/stack/stack.ejs | 10 +- src/dev/pages/stepper/stepper.ejs | 6 +- src/dev/pages/switch/switch.ejs | 20 +- src/dev/pages/tabs/tabs.ejs | 6 +- src/dev/pages/theme/theme.ts | 2 +- src/dev/pages/typography/typography.ejs | 84 ++++--- src/dev/pages/typography/typography.scss | 3 + src/dev/pages/typography/typography.ts | 1 + src/dev/pages/view-switcher/view-switcher.ts | 4 +- src/dev/src/component-list.ts | 2 +- src/dev/src/partials/header.ejs | 2 +- src/dev/src/partials/options-drawer.ejs | 2 +- src/dev/src/partials/page.ejs | 2 +- src/lib/banner/banner.html | 2 +- src/lib/core/styles/_utils.scss | 40 ++- .../styles/tokens/list/list-item/_tokens.scss | 6 +- .../core/styles/tokens/typography/_scale.scss | 34 +++ .../tokens/typography/_tokens.body.scss | 36 +++ .../tokens/typography/_tokens.core.scss | 26 ++ .../tokens/typography/_tokens.display.scss | 62 +++++ .../tokens/typography/_tokens.heading.scss | 65 +++++ .../tokens/typography/_tokens.label.scss | 32 +++ .../styles/tokens/typography/_tokens.scss | 227 ++---------------- .../tokens/typography/_tokens.subheading.scss | 61 +++++ .../styles/tokens/typography/_type-utils.scss | 32 +++ .../styles/tokens/typography/_weight.scss | 15 ++ src/lib/core/styles/typography/index.scss | 16 +- src/lib/list/list-item/_core.scss | 4 +- src/lib/slider/_core.scss | 2 +- src/lib/typography/_mixins.scss | 20 -- src/stories/StorybookMdxProvider.tsx | 6 +- .../components/backdrop/backdrop.stories.tsx | 2 +- .../button-area/button-area.stories.tsx | 4 +- .../src/components/card/card.stories.tsx | 8 +- .../src/components/card/code/card-scaffold.ts | 4 +- .../src/components/card/code/card-styled.ts | 4 +- .../keyboard-shortcut.stories.tsx | 2 +- .../src/components/popup/popup.stories.tsx | 2 +- .../view-switcher/view-switcher.stories.tsx | 6 +- .../guides/css-custom-properties.stories.mdx | 2 +- 66 files changed, 641 insertions(+), 449 deletions(-) create mode 100644 src/dev/pages/typography/typography.scss create mode 100644 src/lib/core/styles/tokens/typography/_scale.scss create mode 100644 src/lib/core/styles/tokens/typography/_tokens.body.scss create mode 100644 src/lib/core/styles/tokens/typography/_tokens.core.scss create mode 100644 src/lib/core/styles/tokens/typography/_tokens.display.scss create mode 100644 src/lib/core/styles/tokens/typography/_tokens.heading.scss create mode 100644 src/lib/core/styles/tokens/typography/_tokens.label.scss create mode 100644 src/lib/core/styles/tokens/typography/_tokens.subheading.scss create mode 100644 src/lib/core/styles/tokens/typography/_type-utils.scss create mode 100644 src/lib/core/styles/tokens/typography/_weight.scss diff --git a/src/dev/pages/avatar/avatar.ejs b/src/dev/pages/avatar/avatar.ejs index 466dccc04..77eec2c6b 100644 --- a/src/dev/pages/avatar/avatar.ejs +++ b/src/dev/pages/avatar/avatar.ejs @@ -1,27 +1,27 @@
-

w/Image URL

+

w/Image URL

-

w/Text

+

w/Text

-

w/Custom Radius (10px)

+

w/Custom Radius (10px)

-

w/Icon

+

w/Icon

@@ -30,14 +30,14 @@
-

w/Custom Slotted Content and CSS Variable

+

w/Custom Slotted Content and CSS Variable

A
-

w/Invalid url

+

w/Invalid url

diff --git a/src/dev/pages/badge/badge.ejs b/src/dev/pages/badge/badge.ejs index 80d5f4bdd..e9f4817e3 100644 --- a/src/dev/pages/badge/badge.ejs +++ b/src/dev/pages/badge/badge.ejs @@ -31,7 +31,7 @@
-

Theme

+

Theme

Default Danger Warning @@ -40,7 +40,7 @@ Info (secondary)
-

Strong theme

+

Strong theme

Default Danger Warning diff --git a/src/dev/pages/button-area/button-area.ejs b/src/dev/pages/button-area/button-area.ejs index 1ca9b88f1..2d1b76b60 100644 --- a/src/dev/pages/button-area/button-area.ejs +++ b/src/dev/pages/button-area/button-area.ejs @@ -3,7 +3,7 @@
-
Heading
+
Heading
Content
@@ -25,7 +25,7 @@
-
Expansion panel
+
Expansion panel
Subheading
diff --git a/src/dev/pages/button-toggle/button-toggle.ejs b/src/dev/pages/button-toggle/button-toggle.ejs index cea7fd1a2..21b17a383 100644 --- a/src/dev/pages/button-toggle/button-toggle.ejs +++ b/src/dev/pages/button-toggle/button-toggle.ejs @@ -1,4 +1,4 @@ -

Static

+

Static

@@ -11,7 +11,7 @@ -

Dynamic

+

Dynamic

diff --git a/src/dev/pages/card/card.ejs b/src/dev/pages/card/card.ejs index bbe4435fb..450b50892 100644 --- a/src/dev/pages/card/card.ejs +++ b/src/dev/pages/card/card.ejs @@ -1,6 +1,6 @@ -

+

This is the card title

@@ -10,7 +10,7 @@
-

+

Lorem, ipsum dolor sit amet consectetur adipisicing elit. Molestias exercitationem doloremque dolorem ullam, nesciunt quia velit necessitatibus numquam quasi voluptates impedit earum dolores diff --git a/src/dev/pages/checkbox/checkbox.ejs b/src/dev/pages/checkbox/checkbox.ejs index 0616d3454..4053741a0 100644 --- a/src/dev/pages/checkbox/checkbox.ejs +++ b/src/dev/pages/checkbox/checkbox.ejs @@ -1,33 +1,33 @@

-

Default

+

Default

- +
-

Dense

+

Dense

- +
-

Indeterminate

+

Indeterminate

- +
-

Disabled

+

Disabled

- +
diff --git a/src/dev/pages/chips/chips.ejs b/src/dev/pages/chips/chips.ejs index f0443ab5b..51d8e50ca 100644 --- a/src/dev/pages/chips/chips.ejs +++ b/src/dev/pages/chips/chips.ejs @@ -1,6 +1,6 @@
-

Basic chips

+

Basic chips

Small Medium @@ -9,7 +9,7 @@
-

Choice chips

+

Choice chips

Small Medium @@ -18,7 +18,7 @@
-

Choice chips (vertical)

+

Choice chips (vertical)

Small Medium @@ -27,8 +27,8 @@
-

Filter chips

-
No leading icon
+

Filter chips

+

No leading icon

Tops Bottoms @@ -38,7 +38,7 @@
-
With leading icon
+

With leading icon

@@ -60,7 +60,7 @@
-

Action chips

+

Action chips

@@ -82,7 +82,7 @@
-

With leading avatar

+

With leading avatar

@@ -104,7 +104,7 @@
-

+

Input chips @@ -135,7 +135,7 @@

-

Invalid chips

+

Invalid chips

diff --git a/src/dev/pages/dialog/dialog-template-basic.ejs b/src/dev/pages/dialog/dialog-template-basic.ejs index 3fb385ef5..6ee7de002 100644 --- a/src/dev/pages/dialog/dialog-template-basic.ejs +++ b/src/dev/pages/dialog/dialog-template-basic.ejs @@ -1,7 +1,7 @@
-

Dialog title

+

Dialog title

-

Default (dense)

+

Default (dense)

-

Toggle

+

Toggle

-

Toggle (dense)

+

Toggle (dense)

-

w/Anchor link

+

w/Anchor link

diff --git a/src/dev/pages/icon/icon.ejs b/src/dev/pages/icon/icon.ejs index c2f1f90a4..af7ddcf12 100644 --- a/src/dev/pages/icon/icon.ejs +++ b/src/dev/pages/icon/icon.ejs @@ -1,19 +1,19 @@ -

External icon

+

External icon

-

External icon (lazy)

+

External icon (lazy)

-

Registry icon

+

Registry icon

-

Custom

+

Custom

-

Invalid icon

+

Invalid icon

-

Invalid icon (external)

+

Invalid icon (external)

diff --git a/src/dev/pages/list/list.ejs b/src/dev/pages/list/list.ejs index a5cb15415..c673cc770 100644 --- a/src/dev/pages/list/list.ejs +++ b/src/dev/pages/list/list.ejs @@ -1,7 +1,7 @@
-

Non-interactive (static is deprecated)

+

Non-interactive (static is deprecated)

List Item One @@ -13,7 +13,7 @@
-

Disabled

+

Disabled

List Item One @@ -25,7 +25,7 @@
-

Single-Line

+

Single-Line

List Item One @@ -37,7 +37,7 @@
-

Two-Line

+

Two-Line

@@ -58,7 +58,7 @@
-

Three-Line

+

Three-Line

@@ -82,7 +82,7 @@
-

Single-Line (long text)

+

Single-Line (long text)

Lorem ipsum dolor sit amet consectetur adipisicing elit. Dignissimos, libero nemo. Similique suscipit ipsam ab veritatis excepturi repellendus ipsa labore! Accusantium, sint ex. Illum architecto rerum, voluptates consectetur cum quaerat. @@ -94,7 +94,7 @@
-

Single-Line (long text w/wrap)

+

Single-Line (long text w/wrap)

Lorem ipsum dolor sit amet consectetur adipisicing elit. Dignissimos, libero nemo. Similique suscipit ipsam ab veritatis excepturi repellendus ipsa labore! Accusantium, sint ex. Illum architecto rerum, voluptates consectetur cum quaerat. @@ -106,7 +106,7 @@
-

Two-Line (long text w/wrap)

+

Two-Line (long text w/wrap)

@@ -127,7 +127,7 @@
-

Three-Line (long text w/wrap)

+

Three-Line (long text w/wrap)

@@ -151,7 +151,7 @@
-

Single-Line (dense)

+

Single-Line (dense)

List Item One @@ -163,7 +163,7 @@
-

Two-Line (dense)

+

Two-Line (dense)

@@ -184,7 +184,7 @@
-

Three-Line (dense)

+

Three-Line (dense)

@@ -208,7 +208,7 @@
-

List with Selected Item

+

List with Selected Item

@@ -238,7 +238,7 @@
-

Leading Icon

+

Leading Icon

@@ -259,7 +259,7 @@
-

Trailing Icon

+

Trailing Icon

@@ -280,7 +280,7 @@
-

Two-Line with Leading and Trailing Icon and Divider

+

Two-Line with Leading and Trailing Icon and Divider

@@ -314,7 +314,7 @@
-

List with Dividers and Expandable Items

+

List with Dividers and Expandable Items

@@ -350,7 +350,7 @@
-

List with Leading Checkbox

+

List with Leading Checkbox

@@ -377,7 +377,7 @@
-

List with Trailing Checkbox

+

List with Trailing Checkbox

@@ -404,7 +404,7 @@
-

List with Leading Radio Buttons

+

List with Leading Radio Buttons

@@ -431,7 +431,7 @@
-

List with Trailing Radio Buttons

+

List with Trailing Radio Buttons

@@ -459,7 +459,7 @@
-

List with Trailing Switch

+

List with Trailing Switch

@@ -480,7 +480,7 @@
-

List with multiple Checkboxes (requires use of forge-ignore attribute)

+

List with multiple Checkboxes (requires use of forge-ignore attribute)

@@ -516,7 +516,7 @@
-

List Inside of Drawer

+

List Inside of Drawer