From 7ddddcabbf7fe53ebd76ed4df249e5cf5aae097e Mon Sep 17 00:00:00 2001 From: Richard Herman <1429781+GeekyEggo@users.noreply.github.com> Date: Wed, 30 Oct 2024 19:11:26 +0000 Subject: [PATCH] feat: add sd-radio-group and sd-option (#66) * feat: add sd-radio * fix: equality checking * feat: add support for typed options * feat: add custom data for sd-option and sd-radio-list * refactor: rename radio list to radio group * docs: update JSDocs * refactor: update element name * refactor: update file name to reflect spacing --------- Co-authored-by: Richard Herman --- .vscode/html.customData.json | 40 +++++ src/common/__tests__/utils.test.ts | 86 +++++++++- src/common/utils.ts | 27 ++++ src/ui/components/index.ts | 4 +- src/ui/components/label.ts | 10 +- src/ui/components/option.ts | 75 +++++++++ src/ui/components/radio-group.ts | 149 ++++++++++++++++++ .../{textfield.ts => text-field.ts} | 4 +- src/ui/controllers/option-observer.ts | 87 ++++++++++ src/ui/mixins/input.ts | 2 +- src/ui/mixins/list.ts | 77 +++++++++ src/ui/utils.ts | 10 ++ 12 files changed, 558 insertions(+), 13 deletions(-) create mode 100644 src/ui/components/option.ts create mode 100644 src/ui/components/radio-group.ts rename src/ui/components/{textfield.ts => text-field.ts} (98%) create mode 100644 src/ui/controllers/option-observer.ts create mode 100644 src/ui/mixins/list.ts diff --git a/.vscode/html.customData.json b/.vscode/html.customData.json index 497cd34c..b0b44e99 100644 --- a/.vscode/html.customData.json +++ b/.vscode/html.customData.json @@ -22,6 +22,46 @@ } ] }, + { + "name": "sd-option", + "description": "Non-visual element that provides information for an option.", + "attributes": [ + { + "name": "disabled", + "description": "Determines whether the option is disabled; default `false`.", + "values": [{ "name": "disabled" }] + }, + { + "name": "type", + "description": "Type of the value; allows for the value to be converted to a boolean or number.", + "values": [{ "name": "boolean" }, { "name": "number" }, { "name": "string" }] + }, + { + "name": "value", + "description": "Value of the option." + } + ] + }, + { + "name": "sd-radio-list", + "description": "Element that offers persisting a value via a list of radio options.", + "attributes": [ + { + "name": "disabled", + "description": "Determines whether the input is disabled; default `false`.", + "values": [{ "name": "disabled" }] + }, + { + "name": "global", + "description": "When `true`, the setting will be persisted in the global settings, otherwise it will be persisted in the action's settings; default `false`.", + "values": [{ "name": "global" }] + }, + { + "name": "setting", + "description": "Path to the setting where the value should be persisted, for example `name`." + } + ] + }, { "name": "sd-switch", "description": "Element that offers persisting a `boolean` via a toggle switch.", diff --git a/src/common/__tests__/utils.test.ts b/src/common/__tests__/utils.test.ts index be23e934..762e808e 100644 --- a/src/common/__tests__/utils.test.ts +++ b/src/common/__tests__/utils.test.ts @@ -1,5 +1,5 @@ import { PromiseCompletionSource } from "../promises"; -import { debounce, freeze, get, set } from "../utils"; +import { debounce, freeze, get, parseBoolean, parseNumber, set } from "../utils"; /** * Provides assertions for {@link debounce}. @@ -147,6 +147,90 @@ describe("get", () => { }); }); +/** + * Provides assertions for {@link parseBoolean}. + */ +describe("parseBoolean", () => { + /** + * Asserts {@link parseBoolean} parses truthy values that represent `true`. + */ + test.each([ + {}, + true, + 1, + "true", + "any", + ])("%s is true", (value) => { + expect(parseBoolean(value)).toBe(true); + }); + + /** + * Asserts {@link parseBoolean} parses truthy values that represent `false`. + */ + test.each([ + undefined, + null, + false, + 0, + ])("%s is false", (value) => { + expect(parseBoolean(value)).toBe(false); + }); +}); + +/** + * Provides assertions for {@link parseNumber}. + */ +describe("parseNumber", () => { + /** + * Asserts {@link parseNumber} with values that can be parsed. + */ + test.each([ + { + value: -1, + expected: -1, + }, + { + value: 0, + expected: 0, + }, + { + value: 1, + expected: 1, + }, + { + value: "13", + expected: 13, + }, + { + value: "25.0", + expected: 25, + }, + { + value: "99.9", + expected: 99.9, + }, + { + value: "100a", + expected: 100, + }, + ])("parses $value = $expected", ({ value, expected }) => { + expect(parseNumber(value)).toBe(expected); + }); + + /** + * Asserts {@link parseNumber} with values that cannot be parsed. + */ + test.each([ + undefined, + null, + "false", + "a123b", + {}, + ])("$value = undefined", (value) => { + expect(parseNumber(value)).toBeUndefined(); + }); +}); + /** * Provides assertions for {@link set}. */ diff --git a/src/common/utils.ts b/src/common/utils.ts index 0b3c4761..d0128091 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -55,6 +55,33 @@ export function get(path: string, source: unknown): unknown { return props.reduce((obj, prop) => obj && obj[prop as keyof object], source); } +/** + * Parses the specified value to a truthy boolean (using {@link https://stackoverflow.com/questions/784929/what-does-the-operator-do-in-javascript `!!` notation}). + * @param value Value to parse. + * @returns `true` when the value is truthy; otherwise `false`. + */ +export function parseBoolean(value: unknown): boolean | undefined { + return !!value; +} + +/** + * Parses the specified value to a number (using {@link parseFloat}). + * @param value Value to parse. + * @returns The parsed value; otherwise `undefined`. + */ +export function parseNumber(value: unknown): number | undefined { + if (typeof value === "number") { + return value; + } + + if (typeof value !== "string") { + return undefined; + } + + value = parseFloat(value); + return typeof value === "number" && !isNaN(value) ? value : undefined; +} + /** * Sets the specified `value` on the `target` object at the desired property `path`. * @param path The path to the property to set. diff --git a/src/ui/components/index.ts b/src/ui/components/index.ts index 52419160..b9cf3cf6 100644 --- a/src/ui/components/index.ts +++ b/src/ui/components/index.ts @@ -1,4 +1,6 @@ import "./field"; import "./label"; +import "./option"; +import "./radio-group"; import "./switch"; -import "./textfield"; +import "./text-field"; diff --git a/src/ui/components/label.ts b/src/ui/components/label.ts index 729e388a..098c80b0 100644 --- a/src/ui/components/label.ts +++ b/src/ui/components/label.ts @@ -4,6 +4,7 @@ import { ifDefined } from "lit/directives/if-defined.js"; import { createRef, ref } from "lit/directives/ref.js"; import { isSDInputElement } from "../mixins/input"; +import { preventDoubleClickSelection } from "../utils"; /** * Element that provides a label for input element. @@ -49,14 +50,7 @@ export class SDLabelElement extends LitElement { * @inheritdoc */ public override render(): TemplateResult { - return html`