From 83699fdcd0d4803edc4524513145f6698c0acf11 Mon Sep 17 00:00:00 2001 From: Sam Richardson Date: Thu, 12 Oct 2023 10:42:32 -0400 Subject: [PATCH] feat(switch): refactor switch component --------- Co-authored-by: Nichols, Kieran --- package-lock.json | 163 ---- package.json | 5 - src/dev/pages/switch/switch.ejs | 33 +- src/dev/pages/switch/switch.scss | 28 + src/dev/pages/switch/switch.ts | 18 + src/lib/core/base/base-component.ts | 41 ++ .../core/styles/tokens/switch/_tokens.scss | 103 +++ src/lib/core/styles/tokens/theme/_tokens.scss | 6 +- src/lib/core/utils/a11y-utils.ts | 119 +++ src/lib/core/utils/index.ts | 1 + src/lib/core/utils/utils.ts | 1 - src/lib/switch/_configuration.scss | 90 +++ src/lib/switch/_core.scss | 250 +++++++ src/lib/switch/_switch-mixins.scss | 296 -------- src/lib/switch/_switch-theme.scss | 696 ------------------ src/lib/switch/index.scss | 2 + src/lib/switch/switch-adapter.ts | 97 +++ src/lib/switch/switch-component-delegate.ts | 17 +- src/lib/switch/switch-constants.ts | 29 +- src/lib/switch/switch-foundation.ts | 137 ++++ src/lib/switch/switch.html | 37 +- src/lib/switch/switch.scss | 186 +++-- src/lib/switch/switch.test.ts | 354 +++++++++ src/lib/switch/switch.ts | 398 ++++++---- src/lib/theme/_theme-dark.scss | 2 - .../switch/switch-attributes.fixture.html | 3 - .../spec/switch/switch-no-input.fixture.html | 3 - src/test/spec/switch/switch.fixture.html | 3 - src/test/spec/switch/switch.spec.ts | 291 -------- 29 files changed, 1700 insertions(+), 1709 deletions(-) create mode 100644 src/dev/pages/switch/switch.scss create mode 100644 src/lib/core/styles/tokens/switch/_tokens.scss create mode 100644 src/lib/core/utils/a11y-utils.ts create mode 100644 src/lib/switch/_configuration.scss create mode 100644 src/lib/switch/_core.scss delete mode 100644 src/lib/switch/_switch-mixins.scss delete mode 100644 src/lib/switch/_switch-theme.scss create mode 100644 src/lib/switch/index.scss create mode 100644 src/lib/switch/switch-adapter.ts create mode 100644 src/lib/switch/switch-foundation.ts create mode 100644 src/lib/switch/switch.test.ts delete mode 100644 src/test/spec/switch/switch-attributes.fixture.html delete mode 100644 src/test/spec/switch/switch-no-input.fixture.html delete mode 100644 src/test/spec/switch/switch.fixture.html delete mode 100644 src/test/spec/switch/switch.spec.ts diff --git a/package-lock.json b/package-lock.json index 5119a6cea..e95fe22e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,12 +27,7 @@ "@material/rtl": "^14.0.0", "@material/select": "^14.0.0", "@material/shape": "^14.0.0", - "@material/slider": "^14.0.0", - "@material/switch": "^14.0.0", - "@material/tab-indicator": "^14.0.0", - "@material/tab-scroller": "^14.0.0", "@material/theme": "^14.0.0", - "@material/tokens": "^14.0.0", "@material/top-app-bar": "^14.0.0", "@material/touch-target": "^14.0.0", "@material/typography": "^14.0.0", @@ -2278,85 +2273,6 @@ "tslib": "^2.1.0" } }, - "node_modules/@material/slider": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@material/slider/-/slider-14.0.0.tgz", - "integrity": "sha512-m5RqySIps1vhAQnGp2eg4Sh2Ss6bzrZm10TWBw2cNFHmbiI72rK2EeFnMsBXAarplY0cot/FaMuj91VP36gKFQ==", - "dependencies": { - "@material/animation": "^14.0.0", - "@material/base": "^14.0.0", - "@material/dom": "^14.0.0", - "@material/elevation": "^14.0.0", - "@material/feature-targeting": "^14.0.0", - "@material/ripple": "^14.0.0", - "@material/rtl": "^14.0.0", - "@material/theme": "^14.0.0", - "@material/typography": "^14.0.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/switch": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@material/switch/-/switch-14.0.0.tgz", - "integrity": "sha512-vHVKzbvHVKGSrkMB1lZAl8z3eJ8sPRnSR+DWn+IhqHcTsDdDyly2NNj4i2vTSrEA39CztGqkx0OnKM4vkpiZHw==", - "dependencies": { - "@material/animation": "^14.0.0", - "@material/base": "^14.0.0", - "@material/density": "^14.0.0", - "@material/dom": "^14.0.0", - "@material/elevation": "^14.0.0", - "@material/feature-targeting": "^14.0.0", - "@material/focus-ring": "^14.0.0", - "@material/ripple": "^14.0.0", - "@material/rtl": "^14.0.0", - "@material/shape": "^14.0.0", - "@material/theme": "^14.0.0", - "@material/tokens": "^14.0.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/tab": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@material/tab/-/tab-14.0.0.tgz", - "integrity": "sha512-jGSQdp6BvZOVnvGbv0DvNDJL2lHYVFtKGehV0gSZ7FrjHK6gZnKZjWOVwt1NPu9ig9zy85vPRFpvFTeje1KZpg==", - "dependencies": { - "@material/base": "^14.0.0", - "@material/elevation": "^14.0.0", - "@material/feature-targeting": "^14.0.0", - "@material/focus-ring": "^14.0.0", - "@material/ripple": "^14.0.0", - "@material/rtl": "^14.0.0", - "@material/tab-indicator": "^14.0.0", - "@material/theme": "^14.0.0", - "@material/typography": "^14.0.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/tab-indicator": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@material/tab-indicator/-/tab-indicator-14.0.0.tgz", - "integrity": "sha512-wfq136fsJGqtCIW8x1wFQHgRr7dIQ9SWqp6WG4FQGHpSzliNDA23/bdBUjh3lX2U+mfbdsFmZWEPy06jg2uc5g==", - "dependencies": { - "@material/animation": "^14.0.0", - "@material/base": "^14.0.0", - "@material/feature-targeting": "^14.0.0", - "@material/theme": "^14.0.0", - "tslib": "^2.1.0" - } - }, - "node_modules/@material/tab-scroller": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@material/tab-scroller/-/tab-scroller-14.0.0.tgz", - "integrity": "sha512-wadETsRM7vT4mRjXedaPXxI/WFSSgqHRNI//dORJ6627hoiJfLb5ixwUKTYk9zTz6gNwAlRTrKh98Dr9T7n7Kw==", - "dependencies": { - "@material/animation": "^14.0.0", - "@material/base": "^14.0.0", - "@material/dom": "^14.0.0", - "@material/feature-targeting": "^14.0.0", - "@material/tab": "^14.0.0", - "tslib": "^2.1.0" - } - }, "node_modules/@material/theme": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/@material/theme/-/theme-14.0.0.tgz", @@ -22913,85 +22829,6 @@ "tslib": "^2.1.0" } }, - "@material/slider": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@material/slider/-/slider-14.0.0.tgz", - "integrity": "sha512-m5RqySIps1vhAQnGp2eg4Sh2Ss6bzrZm10TWBw2cNFHmbiI72rK2EeFnMsBXAarplY0cot/FaMuj91VP36gKFQ==", - "requires": { - "@material/animation": "^14.0.0", - "@material/base": "^14.0.0", - "@material/dom": "^14.0.0", - "@material/elevation": "^14.0.0", - "@material/feature-targeting": "^14.0.0", - "@material/ripple": "^14.0.0", - "@material/rtl": "^14.0.0", - "@material/theme": "^14.0.0", - "@material/typography": "^14.0.0", - "tslib": "^2.1.0" - } - }, - "@material/switch": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@material/switch/-/switch-14.0.0.tgz", - "integrity": "sha512-vHVKzbvHVKGSrkMB1lZAl8z3eJ8sPRnSR+DWn+IhqHcTsDdDyly2NNj4i2vTSrEA39CztGqkx0OnKM4vkpiZHw==", - "requires": { - "@material/animation": "^14.0.0", - "@material/base": "^14.0.0", - "@material/density": "^14.0.0", - "@material/dom": "^14.0.0", - "@material/elevation": "^14.0.0", - "@material/feature-targeting": "^14.0.0", - "@material/focus-ring": "^14.0.0", - "@material/ripple": "^14.0.0", - "@material/rtl": "^14.0.0", - "@material/shape": "^14.0.0", - "@material/theme": "^14.0.0", - "@material/tokens": "^14.0.0", - "tslib": "^2.1.0" - } - }, - "@material/tab": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@material/tab/-/tab-14.0.0.tgz", - "integrity": "sha512-jGSQdp6BvZOVnvGbv0DvNDJL2lHYVFtKGehV0gSZ7FrjHK6gZnKZjWOVwt1NPu9ig9zy85vPRFpvFTeje1KZpg==", - "requires": { - "@material/base": "^14.0.0", - "@material/elevation": "^14.0.0", - "@material/feature-targeting": "^14.0.0", - "@material/focus-ring": "^14.0.0", - "@material/ripple": "^14.0.0", - "@material/rtl": "^14.0.0", - "@material/tab-indicator": "^14.0.0", - "@material/theme": "^14.0.0", - "@material/typography": "^14.0.0", - "tslib": "^2.1.0" - } - }, - "@material/tab-indicator": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@material/tab-indicator/-/tab-indicator-14.0.0.tgz", - "integrity": "sha512-wfq136fsJGqtCIW8x1wFQHgRr7dIQ9SWqp6WG4FQGHpSzliNDA23/bdBUjh3lX2U+mfbdsFmZWEPy06jg2uc5g==", - "requires": { - "@material/animation": "^14.0.0", - "@material/base": "^14.0.0", - "@material/feature-targeting": "^14.0.0", - "@material/theme": "^14.0.0", - "tslib": "^2.1.0" - } - }, - "@material/tab-scroller": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@material/tab-scroller/-/tab-scroller-14.0.0.tgz", - "integrity": "sha512-wadETsRM7vT4mRjXedaPXxI/WFSSgqHRNI//dORJ6627hoiJfLb5ixwUKTYk9zTz6gNwAlRTrKh98Dr9T7n7Kw==", - "requires": { - "@material/animation": "^14.0.0", - "@material/base": "^14.0.0", - "@material/dom": "^14.0.0", - "@material/feature-targeting": "^14.0.0", - "@material/tab": "^14.0.0", - "tslib": "^2.1.0" - } - }, "@material/theme": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/@material/theme/-/theme-14.0.0.tgz", diff --git a/package.json b/package.json index cacfb688e..4083842f4 100644 --- a/package.json +++ b/package.json @@ -54,12 +54,7 @@ "@material/rtl": "^14.0.0", "@material/select": "^14.0.0", "@material/shape": "^14.0.0", - "@material/slider": "^14.0.0", - "@material/switch": "^14.0.0", - "@material/tab-indicator": "^14.0.0", - "@material/tab-scroller": "^14.0.0", "@material/theme": "^14.0.0", - "@material/tokens": "^14.0.0", "@material/top-app-bar": "^14.0.0", "@material/touch-target": "^14.0.0", "@material/typography": "^14.0.0", diff --git a/src/dev/pages/switch/switch.ejs b/src/dev/pages/switch/switch.ejs index 2cd4a5cd9..128ac0722 100644 --- a/src/dev/pages/switch/switch.ejs +++ b/src/dev/pages/switch/switch.ejs @@ -1,7 +1,7 @@

Default

- + off/on
@@ -22,9 +22,38 @@
-

Disabled (selected)

+

Disabled (on)

off/on
+ +
+

Custom icons

+ + sad/happy + + + +
+ +
+

Form associated

+ + + +
+ +
+

Custom styling

+ off/on +
+ +
+

Prevent default

+ off/on +
diff --git a/src/dev/pages/switch/switch.scss b/src/dev/pages/switch/switch.scss new file mode 100644 index 000000000..5f45b88cb --- /dev/null +++ b/src/dev/pages/switch/switch.scss @@ -0,0 +1,28 @@ +@use '../../../lib/switch'; + +.form-switch { + display: flex; + align-items: center; +} + +.custom-switch { + --primary-color: var(--forge-theme-primary); + + @include switch.provide-theme(( + handle-on-color: var(--forge-theme-surface), + handle-size: 24px, + handle-off-scale: 0.67, + handle-active-off-scale: 0.83, + handle-on-elevation: none, + track-on-color: var(--primary-color), + track-width: 54px, + track-height: 32px, + track-border-width: 2px, + track-off-border-color: #9e9e9e, + icon-on-size: 16px, + icon-on-color: var(--primary-color), + direction: column, + gap: 4px, + state-layer-on-color: var(--primary-color) + )); +} \ No newline at end of file diff --git a/src/dev/pages/switch/switch.ts b/src/dev/pages/switch/switch.ts index 0f2b3cf28..b572567fc 100644 --- a/src/dev/pages/switch/switch.ts +++ b/src/dev/pages/switch/switch.ts @@ -1,2 +1,20 @@ import '$src/shared'; import '@tylertech/forge/switch'; +import { ISwitchComponent, IconRegistry } from '@tylertech/forge'; +import { tylIconEmoticonHappy, tylIconEmoticonSad } from '@tylertech/tyler-icons/extended'; +import './switch.scss'; + +IconRegistry.define([ + tylIconEmoticonHappy, + tylIconEmoticonSad +]); + +const preventSwitch = document.getElementById('prevent-switch') as ISwitchComponent; + +preventSwitch.addEventListener('forge-switch-change', (evt: CustomEvent) => evt.preventDefault()); + +const testForm = document.getElementById('test-form') as HTMLFormElement; +testForm.addEventListener('submit', (evt: Event) => { + evt.preventDefault(); + console.log('[submit] switch value:', new FormData(testForm).get('test-switch')); +}); diff --git a/src/lib/core/base/base-component.ts b/src/lib/core/base/base-component.ts index a66286904..de6a401ad 100644 --- a/src/lib/core/base/base-component.ts +++ b/src/lib/core/base/base-component.ts @@ -2,3 +2,44 @@ export interface IBaseComponent extends HTMLElement {} /** Any Custom HTML element. Some elements directly implement this interface, while others implement it via an interface that inherits it. */ export abstract class BaseComponent extends HTMLElement implements IBaseComponent {} + +export interface IBaseFormComponent extends IBaseComponent { + disabled: boolean; + required: boolean; + name: string; + readonly form: HTMLFormElement | null; + readonly labels: NodeList; + readonly validity: ValidityState; + readonly validationMessage: string; + readonly willValidate: boolean; + readonly internals: ElementInternals; + setFormValue(value: string | File | FormData | null, state?: string | File | FormData | null | undefined): void; + checkValidity(): boolean; + reportValidity(): boolean; + setCustomValidity(error: string): void; + formResetCallback(): void; + formStateRestoreCallback(state: T, reason: 'restore' | 'autocomplete'): void; + formDisabledCallback(isDisabled: boolean): void; +} + +/** Any form associated Custom HTML element. */ +export abstract class BaseFormComponent extends BaseComponent implements IBaseFormComponent { + public abstract disabled: boolean; + public abstract required: boolean; + public abstract name: string; + + public abstract get form(): HTMLFormElement | null; + public abstract get labels(): NodeList; + public abstract get validity(): ValidityState; + public abstract get validationMessage(): string; + public abstract get willValidate(): boolean; + public abstract get internals(): ElementInternals; + + public abstract setFormValue(value: string | File | FormData | null, state?: string | File | FormData | null | undefined): void; + public abstract checkValidity(): boolean; + public abstract reportValidity(): boolean; + public abstract setCustomValidity(error: string): void; + public abstract formResetCallback(): void; + public abstract formStateRestoreCallback(state: T, reason: 'restore' | 'autocomplete'): void; + public abstract formDisabledCallback(isDisabled: boolean): void; +} diff --git a/src/lib/core/styles/tokens/switch/_tokens.scss b/src/lib/core/styles/tokens/switch/_tokens.scss new file mode 100644 index 000000000..8fcaa548c --- /dev/null +++ b/src/lib/core/styles/tokens/switch/_tokens.scss @@ -0,0 +1,103 @@ +@use 'sass:map'; +@use '../color-palette'; +@use '../../animation'; +@use '../../elevation'; +@use '../../shape'; +@use '../../theme'; +@use '../../utils'; + +$tokens: ( + // Shared + handle-size: utils.module-val(switch, handle-size, 20px), + handle-scale: utils.module-val(switch, handle-scale, 1), + handle-elevation: utils.module-val(switch, handle-elevation, elevation.value(1)), + + 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-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-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), + handle-off-scale: utils.module-ref(switch, handle-off-scale, handle-scale), + handle-shape: utils.module-val(switch, handle-shape, shape.variable(round)), + handle-on-elevation: utils.module-ref(switch, handle-on-elevation, handle-elevation), + 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-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)), + track-on-border-width: utils.module-ref(switch, track-on-border-width, track-border-width), + track-off-border-width: utils.module-ref(switch, track-off-border-width, track-border-width), + track-on-border-color: utils.module-ref(switch, track-on-border-color, track-border-color), + track-off-border-color: utils.module-ref(switch, track-off-border-color, track-border-color), + + // Icon + icon-on-color: utils.module-ref(switch, icon-on-color, icon-color), + icon-off-color: utils.module-ref(switch, icon-off-color, icon-color), + icon-on-size: utils.module-ref(switch, icon-on-size, handle-size), + icon-off-size: utils.module-ref(switch, icon-off-size, handle-size), + icon-on-scale: utils.module-ref(switch, icon-on-scale, icon-scale), + icon-off-scale: utils.module-ref(switch, icon-off-scale, icon-scale), + + // Label + gap: utils.module-val(switch, gap, 0), + justify: utils.module-val(switch, justify, space-between), + direction: utils.module-val(switch, direction, initial), + + // State layer + state-layer-width: utils.module-ref(switch, state-layer-width, state-layer-size), + state-layer-height: utils.module-ref(switch, state-layer-height, state-layer-size), + state-layer-on-color: utils.module-ref(switch, state-layer-on-color, handle-on-color), + state-layer-off-color: utils.module-ref(state-layer, state-layer-off-color, color), + + // Dense + state-layer-dense-width: utils.module-ref(switch, state-layer-dense-width, state-layer-dense-size), + state-layer-dense-height: utils.module-ref(switch, state-layer-dense-height, state-layer-dense-size), + + // Disabled + disabled-opacity: utils.module-val(switch, disabled-opacity, 0.38), + + // Active + handle-active-on-color: utils.module-ref(switch, handle-active-on-color, handle-on-color), + handle-active-off-color: utils.module-ref(switch, handle-active-off-color, handle-off-color), + handle-active-scale: utils.module-val(switch, handle-active-scale, 1.2), + handle-active-on-scale: utils.module-ref(switch, handle-active-on-scale, handle-active-scale), + handle-active-off-scale: utils.module-ref(switch, handle-active-off-scale, handle-active-scale), + handle-active-elevation: utils.module-ref(switch, handle-active-elevation, handle-elevation), + handle-active-on-elevation: utils.module-ref(switch, handle-active-on-elevation, handle-active-elevation), + handle-active-off-elevation: utils.module-ref(switch, handle-active-on-elevation, handle-active-elevation), + track-active-on-color: utils.module-ref(switch, track-active-on-color, track-on-color), + track-active-off-color: utils.module-ref(switch, track-active-off-color, track-off-color), + track-active-on-border-width: utils.module-ref(switch, track-active-on-border-width, track-on-border-width), + track-active-off-border-width: utils.module-ref(switch, track-active-off-border-width, track-off-border-width), + track-active-on-border-color: utils.module-ref(switch, track-active-on-border-color, track-on-border-color), + track-active-off-border-color: utils.module-ref(switch, track-active-off-border-color, track-off-border-color), + icon-active-on-color: utils.module-ref(switch, icon-active-on-color, icon-on-color), + icon-active-off-color: utils.module-ref(switch, icon-active-off-color, icon-off-color), + icon-active-on-scale: utils.module-ref(switch, icon-active-on-scale, icon-on-scale), + icon-active-off-scale: utils.module-ref(switch, icon-active-off-scale, icon-off-scale), + + // Animation + animation-duration: utils.module-val(switch, animation-duration, animation.variable(duration-short2)), + animation-timing: utils.module-val(switch, animation-timing, animation.variable(easing-standard)), + active-animation-timing: utils.module-val(switch, active-animation-timing, animation.variable(easing-linear)) +) !default; + +/// +/// Gets a token from the token map. +/// +@function get($name) { + @return map.get($tokens, $name); +} diff --git a/src/lib/core/styles/tokens/theme/_tokens.scss b/src/lib/core/styles/tokens/theme/_tokens.scss index fe2bb5b9f..1cdedfd10 100644 --- a/src/lib/core/styles/tokens/theme/_tokens.scss +++ b/src/lib/core/styles/tokens/theme/_tokens.scss @@ -54,7 +54,8 @@ $tokens: ( scrollbar-thumb: color-palette.$grey-400, scrollbar-thumb-hover: color-palette.$grey-500, scrollbar-track: #f0f0f0, - scrollbar-track-hover: #ececec + scrollbar-track-hover: #ececec, + handle-off-color: color-palette.$grey-500 ) !default; /// All tokens for the Forge dark theme @@ -98,7 +99,8 @@ $tokens-dark: ( scrollbar-track-hover: color-palette.$grey-800, elevated-surface: #363636, on-elevated-surface: rgba(255, 255, 255, 0.87), - popup-elevation: elevation.value(16) + popup-elevation: elevation.value(16), + handle-off-color: color-palette.$grey-500 ) !default; /// diff --git a/src/lib/core/utils/a11y-utils.ts b/src/lib/core/utils/a11y-utils.ts new file mode 100644 index 000000000..b5c62ecce --- /dev/null +++ b/src/lib/core/utils/a11y-utils.ts @@ -0,0 +1,119 @@ +import { toggleAttribute } from '@tylertech/forge-core'; + +// Attributes referencing element IDs can't be forwarded across shadow boundaries +export const forwardingAriaAttributes = [ + 'aria-atomic', + 'aria-autocomplete', + 'aria-busy', + 'aria-checked', + 'aria-colcount', + 'aria-colindex', + 'aria-colspan', + 'aria-current', + 'aria-description', + 'aria-description', + 'aria-disabled', + 'aria-expanded', + 'aria-haspopup', + 'aria-hidden', + 'aria-invalid', + 'aria-keyshortcuts', + 'aria-label', + 'aria-level', + 'aria-live', + 'aria-modal', + 'aria-multiline', + 'aria-multiselectable', + 'aria-orientation', + 'aria-placeholder', + 'aria-posinset', + 'aria-pressed', + 'aria-readonly', + 'aria-relevant', + 'aria-required', + 'aria-roledescription', + 'aria-rowcount', + 'aria-rowindex', + 'aria-rowspan', + 'aria-selected', + 'aria-setsize', + 'aria-sort', + 'aria-valuemax', + 'aria-valuemin', + 'aria-valuenow', + 'aria-valuetext' +]; + +/** + * Gets an array of ARIA attribute names to be observed by a component. + * @param config An object indicating the part prefixes to append to the ARIA attribute names and + * whether to include unprefied names. + * @returns An array of observed ARIA attribute names. + */ +export function getObservedAriaAttributes(config: { unprefixed?: boolean; parts?: string[] }): string[] { + const observedAttributes: string[] = []; + if (config.unprefixed) { + observedAttributes.push(...forwardingAriaAttributes); + } + if (config.parts) { + config.parts.forEach(part => { + const partAttributes = forwardingAriaAttributes.map(attribute => `${part}:${attribute}`); + observedAttributes.push(...partAttributes); + }); + } + return observedAttributes; +} + +/** + * Sets ARIA attributes on one or more target elements to match those set on a source element. The + * source element is hidden from assistive technology to avoid duplicate semantics. ID dependent + * attributes are ignored. + * @param config A configuration object setting the observed attributes, the element to forward + * attributes from, an optional element to forward unprefixed attributes to and an optional object + * containing elements to forward prefixed attributes to. Attributes may target an element by + * following the naming pattern `"part:aria-*"`. + * @returns A `MutationObserver`. + */ +export function forwardAriaAttributes(config: { + observedAttributes: string[]; + sourceEl: HTMLElement; + targetEl?: HTMLElement; + parts?: Record; +}): MutationObserver { + // Set the source element's role to presentation to prevent duplicated ARIA attributes being + // seen by assistive technology + config.sourceEl.setAttribute('role', 'presentation'); + + // Sets an attribute on the targeted element + const forwardAttribute = (name: string): void => { + const nameParts = name.split(':'); + const partName = nameParts.length === 2 ? nameParts[0] : undefined; + const ariaName = nameParts.length === 2 ? nameParts[1] : nameParts[0]; + + const target = partName ? config.parts?.[partName] : config.targetEl; + if (!target) { + return; + } + + const hasAttribute = config.sourceEl.hasAttribute(name); + const value = config.sourceEl.getAttribute(name) ?? undefined; + + toggleAttribute(target, hasAttribute, ariaName, value); + }; + + // First, forward any attributes already set on the source element + config.observedAttributes.forEach(attribute => { + forwardAttribute(attribute); + }); + + // Forward new and changed attributes as needed + const observerConfig: MutationObserverInit = { attributeFilter: config.observedAttributes }; + const callback: MutationCallback = mutationList => { + mutationList.forEach(mutation => { + forwardAttribute(mutation.attributeName as string); + }); + }; + const observer = new MutationObserver(callback); + observer.observe(config.sourceEl, observerConfig); + return observer; +} diff --git a/src/lib/core/utils/index.ts b/src/lib/core/utils/index.ts index c9e8458b7..db4a1e7ee 100644 --- a/src/lib/core/utils/index.ts +++ b/src/lib/core/utils/index.ts @@ -1,3 +1,4 @@ +export * from './a11y-utils'; export * from './date-utils'; export * from './event-utils'; export * from './svg-utils'; diff --git a/src/lib/core/utils/utils.ts b/src/lib/core/utils/utils.ts index 4dabb9d52..b6a1c92b2 100644 --- a/src/lib/core/utils/utils.ts +++ b/src/lib/core/utils/utils.ts @@ -25,7 +25,6 @@ export function highlightTextHTML(label: string, highlightText: string): HTMLEle return undefined; } - /** * Awaits user interaction on an element in the form of `pointerenter` or `focusin` to let a listener know * when the user has attempted to interact with the provided element. diff --git a/src/lib/switch/_configuration.scss b/src/lib/switch/_configuration.scss new file mode 100644 index 000000000..eb266394b --- /dev/null +++ b/src/lib/switch/_configuration.scss @@ -0,0 +1,90 @@ +@use '../core/styles/tokens/switch/tokens' as switch-tokens; + +@mixin configuration { + // Shared + --_handle-size: #{switch-tokens.get(handle-size)}; + --_handle-scale: #{switch-tokens.get(handle-scale)}; + --_handle-elevation: #{switch-tokens.get(handle-elevation)}; + --_track-border-width: #{switch-tokens.get(track-border-width)}; + --_track-border-color: #{switch-tokens.get(track-border-color)}; + --_icon-color: #{switch-tokens.get(icon-color)}; + --_icon-scale: #{switch-tokens.get(icon-scale)}; + --_state-layer-size: #{switch-tokens.get(state-layer-size)}; + --_state-layer-dense-size: #{switch-tokens.get(state-layer-dense-size)}; + + // Handle + --_handle-on-color: #{switch-tokens.get(handle-on-color)}; + --_handle-off-color: #{switch-tokens.get(handle-off-color)}; + --_handle-width: #{switch-tokens.get(handle-width)}; + --_handle-height: #{switch-tokens.get(handle-height)}; + --_handle-on-scale: #{switch-tokens.get(handle-on-scale)}; + --_handle-off-scale: #{switch-tokens.get(handle-off-scale)}; + --_handle-shape: #{switch-tokens.get(handle-shape)}; + --_handle-on-elevation: #{switch-tokens.get(handle-on-elevation)}; + --_handle-off-elevation: #{switch-tokens.get(handle-off-elevation)}; + + // Track + --_track-on-color: #{switch-tokens.get(track-on-color)}; + --_track-off-color: #{switch-tokens.get(track-off-color)}; + --_track-width: #{switch-tokens.get(track-width)}; + --_track-height: #{switch-tokens.get(track-height)}; + --_track-shape: #{switch-tokens.get(track-shape)}; + --_track-on-border-width: #{switch-tokens.get(track-on-border-width)}; + --_track-off-border-width: #{switch-tokens.get(track-off-border-width)}; + --_track-on-border-color: #{switch-tokens.get(track-on-border-color)}; + --_track-off-border-color: #{switch-tokens.get(track-off-border-color)}; + + // Icon + --_icon-on-color: #{switch-tokens.get(icon-on-color)}; + --_icon-off-color: #{switch-tokens.get(icon-off-color)}; + --_icon-on-size: #{switch-tokens.get(icon-on-size)}; + --_icon-off-size: #{switch-tokens.get(icon-off-size)}; + --_icon-on-scale: #{switch-tokens.get(icon-on-scale)}; + --_icon-off-scale: #{switch-tokens.get(icon-off-scale)}; + + // Label + --_gap: #{switch-tokens.get(gap)}; + --_justify: #{switch-tokens.get(justify)}; + --_direction: #{switch-tokens.get(direction)}; + + // State layer + --_state-layer-width: #{switch-tokens.get(state-layer-width)}; + --_state-layer-height: #{switch-tokens.get(state-layer-height)}; + --_state-layer-on-color: #{switch-tokens.get(state-layer-on-color)}; + --_state-layer-off-color: #{switch-tokens.get(state-layer-off-color)}; + + // Dense + --_state-layer-dense-width: #{switch-tokens.get(state-layer-dense-width)}; + --_state-layer-dense-height: #{switch-tokens.get(state-layer-dense-height)}; + + // Disabled + --_disabled-opacity: #{switch-tokens.get(disabled-opacity)}; + + // Active + --_handle-active-on-color: #{switch-tokens.get(handle-active-on-color)}; + --_handle-active-off-color: #{switch-tokens.get(handle-active-off-color)}; + --_handle-active-scale: #{switch-tokens.get(handle-active-scale)}; + --_handle-active-on-scale: #{switch-tokens.get(handle-active-on-scale)}; + --_handle-active-off-scale: #{switch-tokens.get(handle-active-off-scale)}; + --_handle-active-elevation: #{switch-tokens.get(handle-active-elevation)}; + --_handle-active-on-elevation: #{switch-tokens.get(handle-active-on-elevation)}; + --_handle-active-off-elevation: #{switch-tokens.get(handle-active-off-elevation)}; + + --_track-active-on-color: #{switch-tokens.get(track-active-on-color)}; + --_track-active-off-color: #{switch-tokens.get(track-active-off-color)}; + --_track-active-on-border-width: #{switch-tokens.get(track-active-on-border-width)}; + --_track-active-off-border-width: #{switch-tokens.get(track-active-off-border-width)}; + --_track-active-on-border-color: #{switch-tokens.get(track-active-on-border-color)}; + --_track-active-off-border-color: #{switch-tokens.get(track-active-off-border-color)}; + + --_icon-active-on-color: #{switch-tokens.get(icon-active-on-color)}; + --_icon-active-off-color: #{switch-tokens.get(icon-active-off-color)}; + --_icon-active-scale: #{switch-tokens.get(icon-active-scale)}; + --_icon-active-on-scale: #{switch-tokens.get(icon-active-on-scale)}; + --_icon-active-off-scale: #{switch-tokens.get(icon-active-off-scale)}; + + // Animation + --_animation-duration: #{switch-tokens.get(animation-duration)}; + --_animation-timing: #{switch-tokens.get(animation-timing)}; + --_active-animation-timing: #{switch-tokens.get(active-animation-timing)}; +} diff --git a/src/lib/switch/_core.scss b/src/lib/switch/_core.scss new file mode 100644 index 000000000..17f62d921 --- /dev/null +++ b/src/lib/switch/_core.scss @@ -0,0 +1,250 @@ +@use '../core/styles/typography'; +@use '../core/styles/utils'; +@use '../core/styles/tokens/switch/tokens'; + +// The tallest element may be absolutely positioned so the container needs to +// be manually sized to match. +$_container-block-size: max(var(--_handle-height), var(--_track-height), var(--_current-state-layer-height)); + +// The effective track border radius, for use in other calculations. +$_track-border-radius: calc(var(--_track-height) / 2); + +// Returns the wider of the handle width and state layer width. +$_max-handle-inline-size: max(var(--_handle-width), var(--_current-state-layer-width)); + +// Offsets the track to place the center of its border caps at the center of +// the handle's off and on positions. +$_track-margin-inline: calc(#{$_max-handle-inline-size} / 2 - #{$_track-border-radius}); + +// The x translation of the handle in its on state. +$_handle-on-translate: calc(var(--_track-width) - #{$_track-border-radius} * 2); + +@mixin provide-theme($theme) { + @include utils.provide(tokens.$tokens, $theme, switch); +} + +@mixin host { + display: inline-block; + + cursor: pointer; +} + +@mixin host-disabled { + opacity: var(--_disabled-opacity); + + cursor: not-allowed; +} + +@mixin switch { + position: relative; + flex-direction: var(--_direction); + flex-shrink: 0; + align-items: center; + justify-content: var(--_justify); + gap: var(--_gap); + + display: flex; + + cursor: unset; +} + +@mixin input { + position: absolute; + z-index: 1; + + appearance: none; + + outline: none; + margin: 0; + inline-size: 100%; + block-size: 100%; + + cursor: unset; +} + +@mixin container { + position: relative; + align-items: center; + + display: flex; + + block-size: #{$_container-block-size}; +} + +@mixin track { + transition-property: background-color, border-color, border-width; + transition-duration: var(--_animation-duration); + transition-timing-function: var(--_animation-timing); + + box-sizing: border-box; + margin-inline: #{$_track-margin-inline}; + border-width: var(--_track-off-border-width); + border-color: var(--_track-off-border-color); + border-style: solid; + border-radius: var(--_track-shape); + inline-size: var(--_track-width); + block-size: var(--_track-height); + + background-color: var(--_track-off-color); +} + +@mixin track-on { + border-width: var(--_track-on-border-width); + border-color: var(--_track-on-border-color); + + background-color: var(--_track-on-color); +} + +@mixin track-active { + border-width: var(--_track-active-off-border-width); + border-color: var(--_track-active-off-border-color); + + background-color: var(--_track-active-off-color); +} + +@mixin track-active-on { + border-width: var(--_track-active-on-border-width); + border-color: var(--_track-active-on-border-color); + + background-color: var(--_track-active-on-color); +} + +@mixin handle { + position: absolute; + justify-content: center; + align-items: center; + + display: flex; + + transition-property: translate; + transition-duration: var(--_animation-duration); + transition-timing-function: var(--_animation-timing); + + border-radius: var(--_handle-shape); + inline-size: var(--_current-state-layer-width); + block-size: var(--_current-state-layer-height); + + &::before { + content: ''; + + position: relative; + + display: block; + scale: var(--_handle-off-scale); + + transition: + background-color var(--_animation-duration) var(--_animation-timing), + box-shadow var(--_animation-duration) var(--_animation-timing), + scale var(--_animation-duration) var(--_active-animation-timing); + + box-shadow: var(--_handle-off-elevation); + border-radius: var(--_handle-shape); + inline-size: var(--_handle-width); + block-size: var(--_handle-height); + + background-color: var(--_handle-off-color); + } +} + +@mixin handle-on { + translate: #{$_handle-on-translate}; + + &::before { + scale: var(--_handle-on-scale); + + box-shadow: var(--_handle-on-elevation); + + background-color: var(--_handle-on-color); + } +} + +@mixin handle-active { + &::before { + scale: var(--_handle-active-off-scale); + + box-shadow: var(--_handle-active-off-elevation); + + background-color: var(--_handle-active-off-color); + } +} + +@mixin handle-active-on { + &::before { + scale: var(--_handle-active-on-scale); + + box-shadow: var(--_handle-active-on-elevation); + + background-color: var(--_handle-active-on-color); + } +} + +@mixin icon { + --forge-icon-font-size: var(--_icon-off-size); + + position: absolute; + align-items: center; + justify-content: center; + + display: flex; + + transition-property: opacity, scale; + transition-duration: var(--_animation-duration); + transition-timing-function: var(--_animation-timing); + + inline-size: var(--_icon-off-size); + block-size: var(--_icon-off-size); + + color: var(--_icon-off-color); + fill: var(--_icon-off-color); + + font-size: var(--_icon-off-size); +} + +@mixin icon-on { + --forge-icon-font-size: var(--_icon-on-size); + + inline-size: var(--_icon-on-size); + block-size: var(--_icon-on-size); + + color: var(--_icon-on-color); + fill: var(--_icon-on-color); + + font-size: var(--_icon-on-size); +} + +@mixin icon-active { + scale: var(--_icon-active-off-scale); + + color: var(--_icon-active-off-color); + fill: var(--_icon-active-off-color); +} + +@mixin icon-active-on { + scale: var(--_icon-active-on-scale); + + color: var(--_icon-active-on-color); + fill: var(--_icon-active-on-color); +} + +@mixin icon-on-shown { + opacity: 1; + scale: var(--_icon-on-scale); +} + +@mixin icon-off-shown { + opacity: 1; + scale: var(--_icon-off-scale); +} + +@mixin icon-hidden { + opacity: 0; + scale: 0.4; +} + +@mixin label { + @include typography.style(label); + + &:empty { + display: none; + } +} diff --git a/src/lib/switch/_switch-mixins.scss b/src/lib/switch/_switch-mixins.scss deleted file mode 100644 index 21a08facf..000000000 --- a/src/lib/switch/_switch-mixins.scss +++ /dev/null @@ -1,296 +0,0 @@ -// -// Copyright 2021 Google Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -// stylelint-disable selector-class-pattern -- -// Selector '.mdc-*' should only be used in this project. - -@use '@material/animation/animation'; -@use '@material/elevation/elevation'; -@use '@material/ripple/ripple'; -@use '@material/rtl/rtl'; -@use '@material/theme/gss'; - -$animation-duration: 75ms; -$icon-exit-duration: 0.4 * $animation-duration; -$icon-enter-duration: $animation-duration - $icon-exit-duration; -$ripple-target: '.mdc-switch__ripple'; - -@mixin static-styles() { - @include static-styles-without-ripple(); - - .mdc-switch { - @include ripple.common; // COPYBARA_COMMENT_THIS_LINE - @include ripple.surface($ripple-target: $ripple-target); - @include ripple.radius-unbounded($ripple-target: $ripple-target); - } -} - -@mixin static-styles-without-ripple() { - @include elevation.overlay-common; // COPYBARA_COMMENT_THIS_LINE - - .mdc-switch { - @include root; - } - - .mdc-switch__track { - @include track; - @include track-off; - - .mdc-switch--selected & { - @include track-on; - } - } - - .mdc-switch__handle-track { - @include handle-track; - @include handle-track-off; - - .mdc-switch--selected & { - @include handle-track-on; - } - } - - .mdc-switch__handle { - @include handle; - } - - .mdc-switch__shadow { - @include shadow; - } - - .mdc-elevation-overlay { - @include overlay; - } - - .mdc-switch__ripple { - @include ripple; - - .mdc-switch:disabled & { - @include ripple-disabled; - } - } - - .mdc-switch__icons { - @include icons; - } - - .mdc-switch__icon { - @include icon; - @include icon-hidden; - } - - .mdc-switch--selected .mdc-switch__icon--on, - .mdc-switch--unselected .mdc-switch__icon--off { - @include icon-visible; - } -} - -@mixin root() { - align-items: center; - background: none; - border: none; - display: inline-flex; - flex-shrink: 0; // Stop from collapsing in flex containers - margin: 0; - outline: none; - overflow: visible; - padding: 0; - position: relative; -} - -@mixin disabled() { - pointer-events: none; -} - -@mixin track() { - overflow: hidden; - position: relative; - width: 100%; - - &::before, - &::after { - border: 1px solid transparent; // high contrast mode - border-radius: inherit; - box-sizing: border-box; - content: ''; - height: 100%; - @include gss.annotate($noflip: true); - left: 0; - position: absolute; - width: 100%; - } -} - -@mixin track-on() { - &::before { - transition: animation.exit-temporary(transform, $animation-duration); - transform: translateX(100%); - @include rtl.rtl { - transform: translateX(-100%); - } - } - - &::after { - transition: animation.enter(transform, $animation-duration); - transform: translateX(0); - } -} - -@mixin track-off() { - &::before { - transition: animation.enter(transform, $animation-duration); - transform: translateX(0); - } - - &::after { - transition: animation.exit-temporary(transform, $animation-duration); - transform: translateX(-100%); - @include rtl.rtl { - transform: translateX(100%); - } - } -} - -@mixin handle-track() { - height: 100%; - // The handle track is used to move the handle across the width of the switch - // and may overflow the bounds of the component. It should not be used for - // pointer events. - pointer-events: none; - position: absolute; - top: 0; // Needed for IE11 - transition: animation.standard(transform, $animation-duration); - // IE11 needs explicit left/right - @include rtl.reflexive(left, 0, right, auto); -} - -@mixin handle-track-on() { - transform: translateX(100%); - - @include rtl.rtl { - transform: translateX(-100%); - } -} - -@mixin handle-track-off() { - transform: translateX(0); -} - -@mixin handle() { - display: flex; - pointer-events: auto; - position: absolute; - top: 50%; - transform: translateY(-50%); - // IE11 needs explicit left/right - @include rtl.reflexive(left, 0, right, auto); - - &::before, - &::after { - border: 1px solid transparent; // high contrast mode - border-radius: inherit; - box-sizing: border-box; - content: ''; - width: 100%; - height: 100%; - @include gss.annotate($noflip: true); - left: 0; - position: absolute; - top: 0; // IE11 fix - transition: animation.standard(background-color, $animation-duration), - animation.standard(border-color, $animation-duration); - // Move the handle background colors beneath the shadow overlay color, - // rather than move the overlay on top of the handle with a positive - // z-index, which would require moving all other content on top of the - // overlay with an even greater z-index. - z-index: -1; - } -} - -@mixin shadow() { - border-radius: inherit; - bottom: 0; - @include gss.annotate($noflip: true); - left: 0; - position: absolute; - @include gss.annotate($noflip: true); - right: 0; - top: 0; -} - -@mixin overlay() { - bottom: 0; - @include gss.annotate($noflip: true); - left: 0; - @include gss.annotate($noflip: true); - right: 0; - top: 0; -} - -@mixin ripple() { - @include gss.annotate($noflip: true); - left: 50%; - position: absolute; - top: 50%; - transform: translate(-50%, -50%); - // Move ripple beneath shadow overlay and handle background colors (see - // handle() mixin for explanation). - z-index: -1; -} - -@mixin ripple-disabled { - display: none; -} - -@mixin icons() { - height: 100%; - position: relative; - width: 100%; - z-index: 1; -} - -@mixin icon() { - bottom: 0; - @include gss.annotate($noflip: true); - left: 0; - // IE11 needs top/right/bottom/left + margin instead of translate(-50%, -50%) - // because of SVG centering issues - margin: auto; - position: absolute; - @include gss.annotate($noflip: true); - right: 0; - top: 0; -} - -@mixin icon-hidden() { - opacity: 0; - transition: animation.exit-permanent(opacity, $icon-exit-duration); -} - -@mixin icon-visible() { - opacity: 1; - transition: animation.enter( - opacity, - $icon-enter-duration, - $delay: $icon-exit-duration - ); -} diff --git a/src/lib/switch/_switch-theme.scss b/src/lib/switch/_switch-theme.scss deleted file mode 100644 index 6e062a0ab..000000000 --- a/src/lib/switch/_switch-theme.scss +++ /dev/null @@ -1,696 +0,0 @@ -// -// Copyright 2021 Google Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -// stylelint-disable selector-class-pattern -- -// Selector '.mdc-*' should only be used in this project. - -@use 'sass:color'; -@use 'sass:map'; -@use '@material/density/density'; -@use '@material/dom/dom'; -@use '@material/elevation/elevation-theme'; -@use '@material/ripple/ripple-theme'; -@use '@material/theme/color-palette'; -@use '@material/theme/custom-properties'; -@use '@material/theme/keys'; -@use '@material/theme/shadow-dom'; -@use '@material/theme/state'; -@use '@material/theme/theme-color'; -@use '@material/theme/theme'; -@use '@material/tokens/resolvers'; -@use '@material/shape/shape'; -@use './switch-mixins' as switch; // FORGE(modify): importing from our local copy which mimics _switch.scss -@use '../theme/theme-values'; // FORGE(modify): using our tertiary theme - -$_density-config: ( - size: ( - minimum: 28px, - default: 48px, - maximum: 48px, - ), -); -$_hairline: color-palette.$grey-300; - -// FORGE(modify): use tertiary color -$_inverse-primary: color.scale( - theme-values.$tertiary, - $lightness: 75% -); - -$_on-surface: color-palette.$grey-800; -$_on-surface-dark: rgba(255, 255, 255, 0.38); // FORGE(modify): added for dark theme mixins below -$_on-surface-variant: color-palette.$grey-500; // FORGE(modify): use slighter lighter shade -$_on-surface-state-content: color-palette.$grey-900; -// FORGE(modify): use tertiary color -$_primary-state-content: color.scale( - theme-values.$tertiary, - $blackness: 50% -); - -// FORGE(modify): added for dark theme mixin below -$_inverse-secondary: color.scale(theme-values.$secondary, $lightness: 75%); -$_secondary-state-content: color.scale(theme-values.$secondary, $blackness: 25%); - -$_selectors: ( - disabled: ':disabled', - focus: ':focus', - hover: ':hover', - pressed: ':active', - selected: '.mdc-switch--selected', - unselected: '.mdc-switch--unselected', -); - -$light-theme: ( - disabled-handle-elevation: 0, - disabled-handle-opacity: 0.24, // FORGE(modify): increase default opacity - disabled-selected-handle-color: theme-values.$tertiary, // FORGE(modify): use primary color for handle when disabled - disabled-selected-icon-color: on-primary, - disabled-selected-icon-opacity: 0.54, - disabled-selected-track-color: theme-values.$tertiary, // FORGE(modify): use primary color for track when disabled - disabled-track-opacity: 0.12, - disabled-unselected-handle-color: $_on-surface, - disabled-unselected-icon-color: on-primary, - disabled-unselected-icon-opacity: 0.54, - disabled-unselected-track-color: $_on-surface, - handle-elevation: 1, - handle-height: 20px, - handle-shadow-color: elevation-theme.$baseline-color, - handle-shape: 10px, - handle-surface-color: surface, - handle-width: 20px, - selected-focus-handle-color: $_primary-state-content, - selected-focus-state-layer-color: theme-values.$tertiary, // FORGE(modify): use tertiary color - selected-focus-state-layer-opacity: 0.12, - selected-focus-track-color: $_inverse-primary, - selected-handle-color: theme-values.$tertiary, // FORGE(modify): use tertiary color - selected-hover-handle-color: $_primary-state-content, - selected-hover-state-layer-color: theme-values.$tertiary, // FORGE(modify): use tertiary color - selected-hover-state-layer-opacity: 0.04, - selected-hover-track-color: $_inverse-primary, - selected-icon-color: on-primary, - selected-icon-size: 18px, - selected-pressed-handle-color: $_primary-state-content, - selected-pressed-state-layer-color: theme-values.$tertiary, // FORGE(modify): use tertiary color - selected-pressed-state-layer-opacity: 0.1, - selected-pressed-track-color: $_inverse-primary, - selected-track-color: $_inverse-primary, - state-layer-size: 48px, - track-height: 14px, - track-shape: 7px, - track-width: 36px, - unselected-focus-handle-color: $_on-surface-state-content, - unselected-focus-state-layer-color: $_on-surface, - unselected-focus-state-layer-opacity: 0.12, - unselected-focus-track-color: $_hairline, - unselected-handle-color: $_on-surface-variant, - unselected-hover-handle-color: $_on-surface-state-content, - unselected-hover-state-layer-color: $_on-surface, - unselected-hover-state-layer-opacity: 0.04, - unselected-hover-track-color: $_hairline, - unselected-icon-color: on-primary, - unselected-icon-size: 18px, - unselected-pressed-handle-color: $_on-surface-state-content, - unselected-pressed-state-layer-color: $_on-surface, - unselected-pressed-state-layer-opacity: 0.1, - unselected-pressed-track-color: $_hairline, - unselected-track-color: $_hairline -); - -$forced-colors-theme: ( - disabled-handle-opacity: 1, - disabled-selected-icon-color: GrayText, - disabled-selected-icon-opacity: 1, - disabled-track-opacity: 1, - disabled-unselected-icon-color: GrayText, - disabled-unselected-icon-opacity: 1, -); - -@function density($density-scale) { - $size: density.prop-value( - $density-config: $_density-config, - $density-scale: $density-scale, - $property-name: size, - ); - - @return (state-layer-size: $size); -} - -@mixin theme($theme, $resolvers: resolvers.$material) { - @include theme.validate-theme($light-theme, $theme); - - // TODO(b/185172301): replace with improved feature targeting - // IE11 Fallback - @if shadow-dom.$css-selector-fallback-declarations { - @include custom-properties.configure($emit-custom-properties: false) { - @include dom.ie11-support { - @include theme-styles($theme, $resolvers: $resolvers); - } - } - } - - @include keys.declare-custom-properties($theme, switch); -} - -@mixin theme-styles($theme, $resolvers: resolvers.$material) { - @include theme.validate-theme($light-theme, $theme); - - $theme: keys.create-theme-properties($theme, switch); - - @include _selected-handle-color( - ( - default: map.get($theme, selected-handle-color), - disabled: map.get($theme, disabled-selected-handle-color), - focus: map.get($theme, selected-focus-handle-color), - hover: map.get($theme, selected-hover-handle-color), - pressed: map.get($theme, selected-pressed-handle-color), - ) - ); - - @include _unselected-handle-color( - ( - default: map.get($theme, unselected-handle-color), - disabled: map.get($theme, disabled-unselected-handle-color), - focus: map.get($theme, unselected-focus-handle-color), - hover: map.get($theme, unselected-hover-handle-color), - pressed: map.get($theme, unselected-pressed-handle-color), - ) - ); - - @include _handle-surface-color(map.get($theme, handle-surface-color)); - - @include _handle-elevation( - map.get($resolvers, elevation), - map.get($theme, handle-shadow-color), - ( - default: map.get($theme, handle-elevation), - disabled: map.get($theme, disabled-handle-elevation), - ) - ); - - @include _handle-height(map.get($theme, handle-height)); - @include _handle-opacity( - ( - disabled: map.get($theme, disabled-handle-opacity), - ) - ); - - @include _handle-shape(map.get($theme, handle-shape)); - @include _handle-width(map.get($theme, handle-width)); - - @include _selected-icon-color( - ( - default: map.get($theme, selected-icon-color), - disabled: map.get($theme, disabled-selected-icon-color), - ) - ); - - @include _unselected-icon-color( - ( - default: map.get($theme, unselected-icon-color), - disabled: map.get($theme, disabled-unselected-icon-color), - ) - ); - - @include _selected-icon-opacity( - ( - disabled: map.get($theme, disabled-selected-icon-opacity), - ) - ); - - @include _unselected-icon-opacity( - ( - disabled: map.get($theme, disabled-unselected-icon-opacity), - ) - ); - - @include _selected-icon-size(map.get($theme, selected-icon-size)); - @include _unselected-icon-size(map.get($theme, unselected-icon-size)); - - @include _selected-ripple-color( - ( - focus: map.get($theme, selected-focus-state-layer-color), - hover: map.get($theme, selected-hover-state-layer-color), - pressed: map.get($theme, selected-pressed-state-layer-color), - ) - ); - - @include _unselected-ripple-color( - ( - focus: map.get($theme, unselected-focus-state-layer-color), - hover: map.get($theme, unselected-hover-state-layer-color), - pressed: map.get($theme, unselected-pressed-state-layer-color), - ) - ); - - @include _selected-ripple-opacity( - ( - focus: map.get($theme, selected-focus-state-layer-opacity), - hover: map.get($theme, selected-hover-state-layer-opacity), - pressed: map.get($theme, selected-pressed-state-layer-opacity), - ) - ); - - @include _unselected-ripple-opacity( - ( - focus: map.get($theme, unselected-focus-state-layer-opacity), - hover: map.get($theme, unselected-hover-state-layer-opacity), - pressed: map.get($theme, unselected-pressed-state-layer-opacity), - ) - ); - - @include _state-layer-size(map.get($theme, state-layer-size)); - @include _track-height(map.get($theme, track-height)); - @include _track-opacity( - ( - disabled: map.get($theme, disabled-track-opacity), - ) - ); - - @include _track-selected-color( - ( - default: map.get($theme, selected-track-color), - disabled: map.get($theme, disabled-selected-track-color), - focus: map.get($theme, selected-focus-track-color), - hover: map.get($theme, selected-hover-track-color), - pressed: map.get($theme, selected-pressed-track-color), - ) - ); - - @include _track-unselected-color( - ( - default: map.get($theme, unselected-track-color), - disabled: map.get($theme, disabled-unselected-track-color), - focus: map.get($theme, unselected-focus-track-color), - hover: map.get($theme, unselected-hover-track-color), - pressed: map.get($theme, unselected-pressed-track-color), - ) - ); - - @include _track-shape(map.get($theme, track-shape)); - @include _track-width(map.get($theme, track-width)); -} - -@mixin _handle-color($colors) { - @include state.default($_selectors) { - @include _set-handle-color(state.get-default-state($colors)); - } - - @include state.hover($_selectors) { - @include _set-handle-color(state.get-hover-state($colors)); - } - - @include state.focus($_selectors) { - @include _set-handle-color(state.get-focus-state($colors)); - } - - @include state.pressed($_selectors) { - @include _set-handle-color(state.get-pressed-state($colors)); - } - - @include state.disabled($_selectors) { - @include _set-handle-color(state.get-disabled-state($colors)); - } -} - -@mixin _set-handle-color($color) { - .mdc-switch__handle { - &::after { - @include theme.property(background, $color); - } - } -} - -@mixin _selected-handle-color($colors) { - @include state.selected($_selectors) { - @include _handle-color($colors); - } -} - -@mixin _unselected-handle-color($colors) { - @include state.unselected($_selectors) { - @include _handle-color($colors); - } -} - -@mixin _handle-surface-color($color) { - .mdc-switch__handle { - // Sets the surface color for the handle. This is used so that when an - // opacity is applied to the "main" handle color, it will not bleed through - // and appear transparent on top of the track. - &::before { - @include theme.property(background, $color); - } - } -} - -@mixin _handle-elevation($resolver, $shadow-color, $elevations) { - @include state.default($_selectors) { - @include _set-handle-elevation( - $resolver, - $elevation: state.get-default-state($elevations), - $shadow-color: $shadow-color - ); - } - - @include state.disabled($_selectors) { - @include _set-handle-elevation( - $resolver, - $elevation: state.get-disabled-state($elevations), - $shadow-color: $shadow-color - ); - } -} - -@mixin _set-handle-elevation($resolver, $args...) { - .mdc-switch__shadow { - @include elevation-theme.with-resolver($resolver, $args...); - } -} - -@mixin _handle-height($height) { - .mdc-switch__handle { - @include theme.property(height, $height); - } -} - -@mixin _handle-opacity($opacities) { - @include state.disabled($_selectors) { - @include _set-handle-opacity(state.get-disabled-state($opacities)); - } -} - -@mixin _set-handle-opacity($opacity) { - .mdc-switch__handle { - // Only apply to the ::after pseudo element, which is the handle's "main" - // color. The ::before pseudo element is the surface color, which prevents - // the handle from bleeding through on the track. - &::after { - @include theme.property(opacity, $opacity); - } - } -} - -@mixin _handle-shape($shape) { - .mdc-switch__handle { - @include shape.radius($shape); - } -} - -@mixin _handle-width($width) { - .mdc-switch__handle { - @include theme.property(width, $width); - } - - .mdc-switch__handle-track { - @include theme.property( - width, - 'calc(100% - width)', - $replace: (width: $width) - ); - } -} - -@mixin _icon-color($colors) { - @include state.default($_selectors) { - @include _set-icon-color(state.get-default-state($colors)); - } - - @include state.disabled($_selectors) { - @include _set-icon-color(state.get-disabled-state($colors)); - } -} - -@mixin _set-icon-color($color) { - .mdc-switch__icon { - @include theme.property(fill, $color); - } -} - -@mixin _selected-icon-color($colors) { - @include state.selected($_selectors) { - @include _icon-color($colors); - } -} - -@mixin _unselected-icon-color($colors) { - @include state.unselected($_selectors) { - @include _icon-color($colors); - } -} - -@mixin _icon-opacity($opacities) { - @include state.disabled($_selectors) { - @include _set-icon-opacity(state.get-disabled-state($opacities)); - } -} - -@mixin _set-icon-opacity($opacity) { - .mdc-switch__icons { - @include theme.property(opacity, $opacity); - } -} - -@mixin _selected-icon-opacity($opacities) { - @include state.selected($_selectors) { - @include _icon-opacity($opacities); - } -} - -@mixin _unselected-icon-opacity($opacities) { - @include state.unselected($_selectors) { - @include _icon-opacity($opacities); - } -} - -@mixin _icon-size($size) { - .mdc-switch__icon { - @include theme.property(width, $size); - @include theme.property(height, $size); - } -} - -@mixin _selected-icon-size($size) { - @include state.selected($_selectors) { - @include _icon-size($size); - } -} - -@mixin _unselected-icon-size($size) { - @include state.unselected($_selectors) { - @include _icon-size($size); - } -} - -@mixin _ripple-color($colors) { - @include state.independent-elements(pressed) { - @include state.hover($_selectors) { - @include ripple-theme.states-base-color( - state.get-hover-state($colors), - $ripple-target: switch.$ripple-target - ); - } - - @include state.focus($_selectors) { - @include ripple-theme.states-base-color( - state.get-focus-state($colors), - $ripple-target: switch.$ripple-target - ); - } - - @include state.pressed($_selectors) { - @include ripple-theme.states-base-color( - state.get-pressed-state($colors), - $ripple-target: switch.$ripple-target - ); - } - } -} - -@mixin _selected-ripple-color($colors) { - @include state.selected($_selectors) { - @include _ripple-color($colors); - } -} - -@mixin _unselected-ripple-color($colors) { - @include state.unselected($_selectors) { - @include _ripple-color($colors); - } -} - -@mixin _ripple-opacity($opacities) { - @include state.independent-elements(pressed) { - @include state.hover($_selectors) { - @include ripple-theme.states-hover-opacity( - state.get-hover-state($opacities), - $ripple-target: switch.$ripple-target - ); - } - - @include state.focus($_selectors) { - @include ripple-theme.states-focus-opacity( - state.get-focus-state($opacities), - $ripple-target: switch.$ripple-target - ); - } - - @include state.pressed($_selectors) { - @include ripple-theme.states-press-opacity( - state.get-pressed-state($opacities), - $ripple-target: switch.$ripple-target - ); - } - } -} - -@mixin _selected-ripple-opacity($opacities) { - @include state.selected($_selectors) { - @include _ripple-opacity($opacities); - } -} - -@mixin _unselected-ripple-opacity($opacities) { - @include state.unselected($_selectors) { - @include _ripple-opacity($opacities); - } -} - -@mixin _state-layer-size($size) { - .mdc-switch__ripple { - @include theme.property(height, $size); - @include theme.property(width, $size); - } -} - -@mixin _track-height($height) { - .mdc-switch__track { - @include theme.property(height, $height); - } -} - -@mixin _track-opacity($opacities) { - @include state.disabled($_selectors) { - @include _set-track-opacity(state.get-disabled-state($opacities)); - } -} - -@mixin _set-track-opacity($opacity) { - .mdc-switch__track { - @include theme.property(opacity, $opacity); - } -} - -@mixin _track-selected-color($colors) { - @include state.default($_selectors) { - @include _set-track-selected-color(state.get-default-state($colors)); - } - - @include state.hover($_selectors) { - @include _set-track-selected-color(state.get-hover-state($colors)); - } - - @include state.focus($_selectors) { - @include _set-track-selected-color(state.get-focus-state($colors)); - } - - @include state.pressed($_selectors) { - @include _set-track-selected-color(state.get-pressed-state($colors)); - } - - @include state.disabled($_selectors) { - @include _set-track-selected-color(state.get-disabled-state($colors)); - } -} - -@mixin _set-track-selected-color($color) { - .mdc-switch__track::after { - @include theme.property(background, $color); - } -} - -@mixin _track-unselected-color($colors) { - @include state.default($_selectors) { - @include _set-track-unselected-color(state.get-default-state($colors)); - } - - @include state.hover($_selectors) { - @include _set-track-unselected-color(state.get-hover-state($colors)); - } - - @include state.focus($_selectors) { - @include _set-track-unselected-color(state.get-focus-state($colors)); - } - - @include state.pressed($_selectors) { - @include _set-track-unselected-color(state.get-pressed-state($colors)); - } - - @include state.disabled($_selectors) { - @include _set-track-unselected-color(state.get-disabled-state($colors)); - } -} - -@mixin _set-track-unselected-color($color) { - .mdc-switch__track::before { - @include theme.property(background, $color); - } -} - -@mixin _track-shape($shape) { - .mdc-switch__track { - @include shape.radius($shape); - } -} - -@mixin _track-width($width) { - @include theme.property(width, $width); -} - -// FORGE(new): added mixin for generating dark theme properties -@mixin dark-theme-properties() { - --mdc-switch-selected-handle-color: #{theme-values.$secondary}; - --mdc-switch-selected-hover-handle-color: #{$_secondary-state-content}; - --mdc-switch-selected-focus-handle-color: #{$_secondary-state-content}; - --mdc-switch-selected-pressed-handle-color: #{$_secondary-state-content}; - --mdc-switch-selected-focus-state-layer-color: #{theme-values.$secondary}; - --mdc-switch-selected-hover-state-layer-color: #{theme-values.$secondary}; - --mdc-switch-selected-pressed-state-layer-color: #{theme-values.$secondary}; - --mdc-switch-selected-focus-track-color: #{$_inverse-secondary}; - --mdc-switch-selected-hover-track-color: #{$_inverse-secondary}; - --mdc-switch-selected-pressed-track-color: #{$_inverse-secondary}; - --mdc-switch-selected-track-color: #{$_inverse-secondary}; - --mdc-switch-unselected-hover-handle-color: rgba(255, 255, 255, 0.54); - --mdc-switch-unselected-focus-handle-color: rgba(255, 255, 255, 0.87); - --mdc-switch-unselected-pressed-handle-color: rgba(255, 255, 255, 0.54); - --mdc-switch-unselected-focus-state-layer-color: rgba(255, 255, 255, 0.87); - --mdc-switch-unselected-hover-state-layer-color: rgba(255, 255, 255, 0.87); - --mdc-switch-unselected-pressed-state-layer-color: rgba(255, 255, 255, 0.87); - --mdc-switch-disabled-handle-opacity: 0.54; - --mdc-switch-disabled-selected-handle-color: #{$_inverse-secondary}; - --mdc-switch-disabled-selected-icon-opacity: 0.54; - --mdc-switch-disabled-selected-track-color: #{$_inverse-secondary}; - --mdc-switch-disabled-track-opacity: 0.38; - --mdc-switch-disabled-unselected-handle-color: #{$_on-surface-dark}; - --mdc-switch-disabled-unselected-icon-color: on-primary; - --mdc-switch-disabled-unselected-icon-opacity: 0.54; - --mdc-switch-disabled-unselected-track-color: #{$_on-surface-dark}; -} diff --git a/src/lib/switch/index.scss b/src/lib/switch/index.scss new file mode 100644 index 000000000..c78abe8eb --- /dev/null +++ b/src/lib/switch/index.scss @@ -0,0 +1,2 @@ +@forward './configuration'; +@forward './core'; diff --git a/src/lib/switch/switch-adapter.ts b/src/lib/switch/switch-adapter.ts new file mode 100644 index 000000000..621bcf351 --- /dev/null +++ b/src/lib/switch/switch-adapter.ts @@ -0,0 +1,97 @@ +import { getShadowElement, toggleClass } from '@tylertech/forge-core'; + +import { BaseAdapter, IBaseAdapter, forwardAriaAttributes } from '../core'; +import { ISwitchComponent } from './switch'; +import { SWITCH_CONSTANTS, SwitchIconVisibility, SwitchLabelPosition } from './switch-constants'; + +export interface ISwitchAdapter extends IBaseAdapter { + initialize(): void; + setOn(value: boolean): void; + setDisabled(value: boolean): void; + setRequired(value: boolean): void; + setIconVisibility(value: SwitchIconVisibility): void; + setLabelPosition(value: SwitchLabelPosition): void; + addInputListener(event: string, callback: EventListener): void; + syncValue(value: boolean): void; + syncValidity(hasCustomValidityError: boolean): void; + setValidity(flags?: ValidityStateFlags | undefined, message?: string | undefined): void; +} + +export class SwitchAdapter extends BaseAdapter implements ISwitchAdapter { + private readonly _rootElement: HTMLElement; + private readonly _inputElement: HTMLInputElement; + private readonly _labelElement: HTMLElement; + private readonly _iconOnElement: HTMLElement; + private readonly _iconOffElement: HTMLElement; + + constructor(component: ISwitchComponent) { + super(component); + + this._rootElement = getShadowElement(component, SWITCH_CONSTANTS.selectors.ROOT); + this._inputElement = getShadowElement(component, SWITCH_CONSTANTS.selectors.INPUT) as HTMLInputElement; + this._labelElement = getShadowElement(component, SWITCH_CONSTANTS.selectors.LABEL); + this._iconOnElement = getShadowElement(component, SWITCH_CONSTANTS.selectors.ICON_ON); + this._iconOffElement = getShadowElement(component, SWITCH_CONSTANTS.selectors.ICON_OFF); + } + + public initialize(): void { + forwardAriaAttributes({ + observedAttributes: SWITCH_CONSTANTS.ariaAttributes, + sourceEl: this._component, + targetEl: this._inputElement + }); + } + + public setOn(value: boolean): void { + this._inputElement.checked = value; + } + + public setDisabled(value: boolean): void { + this._inputElement.disabled = value; + } + + public setRequired(value: boolean): void { + this._inputElement.required = value; + } + + public setIconVisibility(value: SwitchIconVisibility): void { + const hideOn = value === 'none' || value === 'off'; + const hideOff = value === 'none' || value === 'on'; + toggleClass(this._iconOnElement, hideOn, SWITCH_CONSTANTS.classes.HIDDEN); + toggleClass(this._iconOffElement, hideOff, SWITCH_CONSTANTS.classes.HIDDEN); + } + + public setLabelPosition(value: SwitchLabelPosition): void { + this._labelElement.remove(); + + if (value === 'start') { + this._rootElement.prepend(this._labelElement); + } else { + this._rootElement.append(this._labelElement); + } + } + + public addInputListener(event: string, callback: EventListener): void { + this._inputElement.addEventListener(event, callback); + } + + public syncValue(value: boolean): void { + const data = new FormData(); + data.append(this._component.name, String(value)); + this._component.internals.setFormValue(data, value.toString()); + } + + public syncValidity(hasCustomValidityError: boolean): void { + if (hasCustomValidityError) { + this._inputElement.setCustomValidity(this._component.internals.validationMessage); + } else { + this._inputElement.setCustomValidity(''); + } + + this._component.internals.setValidity(this._inputElement.validity, this._inputElement.validationMessage, this._inputElement); + } + + public setValidity(flags?: ValidityStateFlags | undefined, message?: string | undefined): void { + this._component.internals.setValidity(flags, message, this._inputElement); + } +} diff --git a/src/lib/switch/switch-component-delegate.ts b/src/lib/switch/switch-component-delegate.ts index 958297e7c..513bbb98f 100644 --- a/src/lib/switch/switch-component-delegate.ts +++ b/src/lib/switch/switch-component-delegate.ts @@ -7,22 +7,23 @@ export type SwitchComponentDelegateProps = Partial; export interface ISwitchComponentDelegateOptions extends IFormFieldComponentDelegateOptions { id?: string; label?: string; + on?: boolean; selected?: boolean; } export interface ISwitchComponentDelegateConfig extends IBaseComponentDelegateConfig {} export class SwitchComponentDelegate extends FormFieldComponentDelegate { - private _labelElement?: HTMLLabelElement; + private _labelElement?: HTMLSpanElement; constructor(config?: ISwitchComponentDelegateConfig) { super(config); } public get value(): boolean { - return this._element.selected; + return this._element.on; } public set value(value: boolean) { - this._element.selected = value; + this._element.on = value; } public get disabled(): boolean { @@ -32,12 +33,12 @@ export class SwitchComponentDelegate extends FormFieldComponentDelegate void): void { - this._element.addEventListener('forge-switch-select', ({ detail }: CustomEvent) => listener(detail)); + this._element.addEventListener('forge-switch-change', ({ detail }: CustomEvent) => listener(detail)); } public onFocus(listener: (evt: Event) => void): void { @@ -69,8 +70,8 @@ export class SwitchComponentDelegate extends FormFieldComponentDelegate this._handleChange(evt); + } + + public initialize(): void { + this._adapter.initialize(); + this._adapter.addInputListener('change', this._changeListener); + this._adapter.setIconVisibility(this._icon); + this._adapter.syncValue(this._on); + } + + public syncValidity(hasCustomValidityError: boolean): void { + this._adapter.syncValidity(hasCustomValidityError); + } + + public setValidity(flags?: ValidityStateFlags | undefined, message?: string | undefined): void { + this._adapter.setValidity(flags, message); + } + + private _handleChange(evt: Event): void { + const target = evt.target as HTMLInputElement; + const newValue = target.checked; + const oldValue = this._on; + + this._on = newValue; + + const isCancelled = !this._adapter.emitHostEvent(SWITCH_CONSTANTS.events.CHANGE, newValue, true, true); + if (isCancelled) { + this._on = oldValue; + this._adapter.setOn(this._on); + return; + } + + this._adapter.syncValue(this._on); + this._setOnAttribute(); + } + + private _setOnAttribute(): void { + this._adapter.toggleHostAttribute(SWITCH_CONSTANTS.attributes.ON, this._on); + // Also set selected for backwards compatibility + this._adapter.toggleHostAttribute(SWITCH_CONSTANTS.attributes.SELECTED, this._on); + } + + public get on(): boolean { + return this._on; + } + public set on(value: boolean) { + if (this._on !== value) { + this._on = value; + this._adapter.setOn(this._on); + this._adapter.syncValue(this._on); + this._setOnAttribute(); + } + } + + public get disabled(): boolean { + return this._disabled; + } + public set disabled(value: boolean) { + if (this._disabled !== value) { + this._disabled = value; + this._adapter.setDisabled(this._disabled); + this._adapter.toggleHostAttribute(SWITCH_CONSTANTS.attributes.DISABLED, this._disabled); + } + } + + public get required(): boolean { + return this._required; + } + public set required(value: boolean) { + if (this._required !== value) { + this._required = value; + this._adapter.setRequired(this._required); + this._adapter.toggleHostAttribute(SWITCH_CONSTANTS.attributes.REQUIRED, this._required); + } + } + + public get dense(): boolean { + return this._dense; + } + public set dense(value: boolean) { + if (this._dense !== value) { + this._dense = value; + this._adapter.toggleHostAttribute(SWITCH_CONSTANTS.attributes.DENSE, this._dense); + } + } + + public get icon(): SwitchIconVisibility { + return this._icon; + } + public set icon(value: SwitchIconVisibility) { + if (this._icon !== value) { + this._icon = value; + this._adapter.setIconVisibility(this._icon); + this._adapter.setHostAttribute(SWITCH_CONSTANTS.attributes.ICON, this._icon); + } + } + + public get labelPosition(): SwitchLabelPosition { + return this._labelPosition; + } + public set labelPosition(value: SwitchLabelPosition) { + if (this._labelPosition !== value) { + this._labelPosition = value; + this._adapter.setLabelPosition(this._labelPosition); + this._adapter.setHostAttribute(SWITCH_CONSTANTS.attributes.LABEL_POSITION, this._labelPosition); + } + } +} diff --git a/src/lib/switch/switch.html b/src/lib/switch/switch.html index afa34a710..b9a961360 100644 --- a/src/lib/switch/switch.html +++ b/src/lib/switch/switch.html @@ -1,25 +1,28 @@