Skip to content

Commit

Permalink
feat: add sd-radio-group and sd-option (#66)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
GeekyEggo and GeekyEggo authored Oct 30, 2024
1 parent ec9fd82 commit 7ddddca
Show file tree
Hide file tree
Showing 12 changed files with 558 additions and 13 deletions.
40 changes: 40 additions & 0 deletions .vscode/html.customData.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
86 changes: 85 additions & 1 deletion src/common/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -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}.
Expand Down Expand Up @@ -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}.
*/
Expand Down
27 changes: 27 additions & 0 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion src/ui/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import "./field";
import "./label";
import "./option";
import "./radio-group";
import "./switch";
import "./textfield";
import "./text-field";
10 changes: 2 additions & 8 deletions src/ui/components/label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -49,14 +50,7 @@ export class SDLabelElement extends LitElement {
* @inheritdoc
*/
public override render(): TemplateResult {
return html`<label
for=${ifDefined(this.htmlFor)}
@mousedown=${(ev: MouseEvent): void => {
// Disable text selection on double-click.
if (ev.detail > 1) {
ev.preventDefault();
}
}}
return html`<label for=${ifDefined(this.htmlFor)} @mousedown=${preventDoubleClickSelection}
><slot ${ref(this.#slotRef)}></slot>
</label>`;
}
Expand Down
75 changes: 75 additions & 0 deletions src/ui/components/option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";

import { parseBoolean, parseNumber } from "../../common/utils";

/**
* Non-visual element that provides information for an option.
*/
@customElement("sd-option")
export class SDOptionElement extends LitElement {
/**
* Private backing field for {@link SDOptionElement.value}.
*/
#value: boolean | number | string | undefined;

/**
* Determines whether the option is disabled; default `false`.
*/
@property({ type: Boolean })
public accessor disabled: boolean = false;

/**
* Label that represents the option; read from the `innerText` of the element.
* @returns The label.
*/
public get label(): string {
return this.innerText;
}

/**
* Type of the value; allows for the value to be converted to a boolean or number.
*/
@property()
public accessor type: "boolean" | "number" | "string" = "string";

/**
* Untyped value, as defined by the `value` attribute; use `value` property for the typed-value.
*/
@property({ attribute: "value" })
public accessor htmlValue: string | undefined = undefined;

/**
* Value of the option.
* @returns The value.
*/
public get value(): boolean | number | string | undefined {
return this.#value;
}

/**
* @inheritdoc
*/
protected override willUpdate(_changedProperties: Map<PropertyKey, unknown>): void {
super.willUpdate(_changedProperties);

if (_changedProperties.has("type") || _changedProperties.has("value")) {
if (this.type === "boolean") {
this.#value = parseBoolean(this.htmlValue);
} else if (this.type === "number") {
this.#value = parseNumber(this.htmlValue);
} else {
this.#value = this.htmlValue;
}
}
}
}

declare global {
interface HTMLElementTagNameMap {
/**
* Non-visual element that provides information for an option.
*/
"sd-option": SDOptionElement;
}
}
Loading

0 comments on commit 7ddddca

Please sign in to comment.