From 9ad73f03c8e7ea60e482405ad47a496da1cf50e8 Mon Sep 17 00:00:00 2001 From: Richard Herman Date: Fri, 1 Nov 2024 16:12:40 +0000 Subject: [PATCH 1/8] feat: enable standalone radio buttons --- src/ui/components/index.ts | 13 +- src/ui/components/option.ts | 8 +- src/ui/components/radio-group.ts | 113 ++-------------- src/ui/components/radio.ts | 187 ++++++++++++++++++++++++++ src/ui/controllers/option-observer.ts | 22 ++- src/ui/mixins/list.ts | 5 +- 6 files changed, 229 insertions(+), 119 deletions(-) create mode 100644 src/ui/components/radio.ts diff --git a/src/ui/components/index.ts b/src/ui/components/index.ts index b9cf3cf..031e9e2 100644 --- a/src/ui/components/index.ts +++ b/src/ui/components/index.ts @@ -1,6 +1,7 @@ -import "./field"; -import "./label"; -import "./option"; -import "./radio-group"; -import "./switch"; -import "./text-field"; +export * from "./field"; +export * from "./label"; +export * from "./option"; +export * from "./radio"; +export * from "./radio-group"; +export * from "./switch"; +export * from "./text-field"; diff --git a/src/ui/components/option.ts b/src/ui/components/option.ts index 2365490..d2fdf45 100644 --- a/src/ui/components/option.ts +++ b/src/ui/components/option.ts @@ -20,12 +20,10 @@ export class SDOptionElement extends LitElement { public accessor disabled: boolean = false; /** - * Label that represents the option; read from the `innerText` of the element. - * @returns The label. + * Label that represents the option. */ - public get label(): string { - return this.innerText; - } + @property() + public accessor label: string | undefined; /** * Type of the value; allows for the value to be converted to a boolean or number. diff --git a/src/ui/components/radio-group.ts b/src/ui/components/radio-group.ts index 6d53d17..b844bdc 100644 --- a/src/ui/components/radio-group.ts +++ b/src/ui/components/radio-group.ts @@ -5,7 +5,7 @@ import { repeat } from "lit/directives/repeat.js"; import { Input } from "../mixins/input"; import { List } from "../mixins/list"; -import { preventDoubleClickSelection } from "../utils"; +import { SDRadioElement } from "./radio"; /** * Element that offers persisting a value via a list of radio options. @@ -17,92 +17,10 @@ export class SDRadioGroupElement extends List(Input(L */ public static styles = [ super.styles ?? [], + ...SDRadioElement.styles, css` - label { + sd-radio { display: flex; - align-items: center; - } - - input { - /* Hide the input, whilst still allowing focus */ - height: 0; - opacity: 0; - position: absolute; - width: 0; - } - - /** - * Radio button replacement. - */ - - .indicator { - --size: calc(var(--size-m) - calc(var(--border-width-thin) * 2)); - align-items: center; - border: var(--border-width-thin) solid var(--color-content-disabled); - border-radius: var(--rounding-full); - display: inline-flex; - height: var(--size); - justify-content: center; - margin: var(--space-xs) var(--space-xs) var(--space-xs) 0; - user-select: none; - width: var(--size); - } - - /** - * Checked. - */ - - input:checked { - & + .indicator { - background: var(--color-surface-accent); - border-color: var(--color-content-disabled); - border-radius: var(--rounding-full); - } - - & + .indicator::before { - content: ""; - background: var(--color-surface-ondark); - border-radius: var(--rounding-full); - display: block; - height: var(--size-xs); - width: var(--size-xs); - } - } - - /** - * Disabled. - */ - - label:has(input:disabled) { - color: var(--color-content-disabled); - } - - input:disabled + .indicator { - border-color: var(--color-border-subtle-disabled); - } - - /** - * Checked + disabled. - */ - - input:checked:disabled { - & + .indicator { - background-color: var(--color-surface-disabled); - } - - & + .indicator::before { - background-color: var(--color-content-disabled); - } - } - - /** - * Focus - */ - - input:focus-visible + .indicator { - box-shadow: var(--highlight-box-shadow); - outline: var(--highlight-outline--focus); - outline-offset: var(--highlight-outline-offset); } `, ]; @@ -117,21 +35,16 @@ export class SDRadioGroupElement extends List(Input(L ({ key }) => key, ({ disabled, label, value }) => { return html` - + { + this.value = value; + }} + > `; }, )} diff --git a/src/ui/components/radio.ts b/src/ui/components/radio.ts new file mode 100644 index 0000000..c2ffbbe --- /dev/null +++ b/src/ui/components/radio.ts @@ -0,0 +1,187 @@ +import { css, html, type TemplateResult } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; + +import { preventDoubleClickSelection } from "../utils"; +import { SDOptionElement } from "./option"; + +/** + * Element that offers an option in the form of a radio button. + */ +@customElement("sd-radio") +export class SDRadioElement extends SDOptionElement { + /** + * Determines whether the shared styles have already been appended to the document. + */ + static #isStyleAppended = false; + + /** + * @inheritdoc + */ + public static styles = [ + css` + label.sd-radio-container { + display: inline-flex; + align-items: center; + + & input { + /* Hide the input, whilst still allowing focus */ + height: 0; + opacity: 0; + position: absolute; + width: 0; + } + + /** + * Radio button replacement. + */ + + & span { + --size: calc(var(--size-m) - calc(var(--border-width-thin) * 2)); + align-items: center; + border: var(--border-width-thin) solid var(--color-content-disabled); + border-radius: var(--rounding-full); + display: inline-flex; + height: var(--size); + justify-content: center; + margin: var(--space-xs) var(--space-xs) var(--space-xs) 0; + user-select: none; + width: var(--size); + } + + /** + * Checked. + */ + + & input:checked { + & + span { + background: var(--color-surface-accent); + border-color: var(--color-content-disabled); + border-radius: var(--rounding-full); + } + + & + span::before { + content: ""; + background: var(--color-surface-ondark); + border-radius: var(--rounding-full); + display: block; + height: var(--size-xs); + position: absolute; + width: var(--size-xs); + } + } + + /** + * Disabled. + */ + + &:has(input:disabled) { + color: var(--color-content-disabled); + } + + & input:disabled + span { + border-color: var(--color-border-subtle-disabled); + } + + /** + * Checked + disabled. + */ + + & input:checked:disabled { + & + span { + background-color: var(--color-surface-disabled); + } + + & + span::before { + background-color: var(--color-content-disabled); + } + } + + /** + * Focus + */ + + & input:focus-visible + span { + box-shadow: var(--highlight-box-shadow); + outline: var(--highlight-outline--focus); + outline-offset: var(--highlight-outline-offset); + } + } + `, + ]; + + /** + * Name of the radio button group the element is associated with. + */ + @property() + public accessor name: string | undefined = undefined; + + /** + * Determines whether the radio button is checked; default `false`. + */ + @property({ + reflect: true, + type: Boolean, + }) + public accessor checked: boolean = false; + + /** + * @inheritdoc + */ + public override connectedCallback(): void { + super.connectedCallback(); + if (SDRadioElement.#isStyleAppended) { + return; + } + + // As the root of the element is not a shadow DOM, we can't scope styles, so instead we add + // the styles as a