diff --git a/module/apps/elements/StyledShadowElement.mjs b/module/apps/elements/StyledShadowElement.mjs index 4929868..24288d9 100644 --- a/module/apps/elements/StyledShadowElement.mjs +++ b/module/apps/elements/StyledShadowElement.mjs @@ -26,10 +26,13 @@ export function StyledShadowElement(Base) { /** @type {ShadowRoot} */ _shadow; - constructor() { + constructor({focusable = false} = {}) { super(); - this._shadow = this.attachShadow({ mode: `open` }); + this._shadow = this.attachShadow({ + mode: `open`, + delegatesFocus: focusable, + }); this._style = document.createElement(`style`); this._shadow.appendChild(this._style); }; diff --git a/module/apps/elements/Toggle.mjs b/module/apps/elements/Toggle.mjs new file mode 100644 index 0000000..2c106dd --- /dev/null +++ b/module/apps/elements/Toggle.mjs @@ -0,0 +1,97 @@ +import { StyledShadowElement } from "./StyledShadowElement.mjs"; + +export class TafToggle extends StyledShadowElement(HTMLElement) { + static elementName = `taf-toggle`; + + static _stylePath = `toggle.css`; + + _mounted; + _internals; + + constructor() { + super({ focusable: true }); + + this._internals = this.attachInternals(); + this._internals.role = `checkbox`; + }; + + get type() { + return `checkbox`; + }; + + get name() { + return this.getAttribute(`name`); + }; + set name(newName) { + this.setAttribute(`name`, newName); + }; + + get value() { + return this._input.value; + }; + set value(newValue) { + this._input.value = newValue; + }; + + get checked() { + return this.hasAttribute(`checked`); + }; + set checked(newValue) { + if (typeof newValue !== `boolean`) { return }; + this.toggleAttribute(`checked`, newValue); + }; + + get disabled() { + return this.matches(`:disabled`); + }; + set disabled(value) { + this.toggleAttribute(`disabled`, value); + }; + + get editable() { + return true; + }; + + connectedCallback() { + super.connectedCallback(); + if (this._mounted) { return }; + + this._internals.checked = this.checked; + + /* + This converts all of the double-dash prefixed properties on the + element to CSS variables so that they don't all need to be + provided by doing style="" + */ + for (const attrVar of this.attributes) { + if (attrVar.name?.startsWith(`var:`)) { + const prop = attrVar.name.replace(`var:`, ``); + this.style.setProperty(`--` + prop, attrVar.value); + }; + }; + + const label = document.createElement(`label`); + label.dataset.type = `round`; + + const input = document.createElement(`input`); + input.type = `checkbox`; + input.toggleAttribute(`switch`, true); + input.checked = this.checked; + + label.appendChild(input); + + const slider = document.createElement(`div`); + slider.classList = `slider`; + label.appendChild(slider); + + this._shadow.appendChild(label); + + this._mounted = true; + }; + + disconnectedCallback() { + super.disconnectedCallback(); + if (!this._mounted) { return }; + this._mounted = false; + }; +}; diff --git a/module/apps/elements/_index.mjs b/module/apps/elements/_index.mjs index dce2b24..db3ac54 100644 --- a/module/apps/elements/_index.mjs +++ b/module/apps/elements/_index.mjs @@ -1,10 +1,12 @@ import { Logger } from "../../utils/Logger.mjs"; import { TafIcon } from "./Icon.mjs"; import { TafSVGLoader } from "./svgLoader.mjs"; +import { TafToggle } from "./Toggle.mjs"; const components = [ TafSVGLoader, TafIcon, + TafToggle, ]; export function registerCustomComponents() { diff --git a/styles/components/toggle.css b/styles/components/toggle.css new file mode 100644 index 0000000..ce8fc67 --- /dev/null +++ b/styles/components/toggle.css @@ -0,0 +1,55 @@ +:host { + display: block; +} + +input { + width: 0; + height: 0; + margin: 0; + padding: 0; +} + +.slider { + width: var(--size, 16px); + height: var(--size, 16px); + background: var( + --slider-colour, + var(--toggle-slider-unchecked-colour) + ); + transition: all var(--speed, 150ms) ease-in-out; + border-radius: 9999px; +} + +label { + display: flex; + padding: var(--padding, 4px); + height: calc(var(--size, 16px) + (var(--padding, 4px) * 2)); + width: calc((var(--size, 16px) * 2) + (var(--padding, 4px) * 2)); + border-radius: 9999px; + background: var( + --toggle-background, + var(--toggle-background-colour) + ); + box-sizing: border-box; + cursor: pointer; + + /* Non-checked, clicking */ + &:active .slider { + width: calc(var(--size, 16px) * 1.5); + } + + /* checked, non-clicking */ + & > :checked + .slider { + transform: translateX(var(--size, 16px)); + background: var( + --slider-checked-colour, + var(--toggle-slider-checked-colour) + ); + } + + /* checked, clicking */ + &:active > :checked + .slider { + width: calc(var(--size, 16px) * 1.5); + transform: translateX(calc(var(--size, 16px) * 0.5)); + } +} diff --git a/styles/themes/dark.css b/styles/themes/dark.css index b08e321..9fd289b 100644 --- a/styles/themes/dark.css +++ b/styles/themes/dark.css @@ -4,6 +4,10 @@ --spinner-outer-colour: white; --spinner-inner-colour: #FF3D00; + --toggle-background-colour: #171e26; + --toggle-slider-unchecked-colour: maroon; + --toggle-slider-checked-colour: green; + --tab-button-active-border: rebeccapurple; --tab-button-hover-bg: var(--color-cool-3); @@ -19,7 +23,8 @@ --item-card-header-background: #242d38; --item-card-header-color: white; --item-card-header-input-background: #2b3642; - --item-card-header-input-color: white; + --item-card-header-input-colour: white; + --item-card-header-disabled-input-colour: gray; /* Chip Variables */ --chip-color: #fff7ed;