Skip to content

Commit

Permalink
feat: add sd-radio (#68)
Browse files Browse the repository at this point in the history
* feat: enable standalone radio buttons

* feat: add setter to Option.value, and improve getter

* feat: allow label to be derived from innerText and improve scoping

* refactor: update radio group to use dynamic slots, and remove List mixin

* perf: improve rendering of radio buttons within a radio group

* refactor: update radio group to use a slot

* refactor: switch to sd-option to allow for CheckBoxList support

* style: fix linting

---------

Co-authored-by: Richard Herman <[email protected]>
  • Loading branch information
GeekyEggo and GeekyEggo authored Nov 5, 2024
1 parent 2090d9e commit 47a784a
Show file tree
Hide file tree
Showing 7 changed files with 377 additions and 252 deletions.
15 changes: 8 additions & 7 deletions src/ui/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import "./field";
import "./label";
import "./option";
import "./radio-group";
import "./switch";
import "./text-area";
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-area";
export * from "./text-field";
50 changes: 36 additions & 14 deletions src/ui/components/option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,22 @@ export class SDOptionElement extends LitElement {
/**
* Private backing field for {@link SDOptionElement.value}.
*/
#value: boolean | number | string | undefined;
#value: boolean | number | string | null | undefined = null;

/**
* Determines whether the option is disabled; default `false`.
*/
@property({ type: Boolean })
@property({
reflect: true,
type: Boolean,
})
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.
Expand All @@ -44,23 +45,44 @@ export class SDOptionElement extends LitElement {
* @returns The value.
*/
public get value(): boolean | number | string | undefined {
if (this.#value === null) {
if (this.type === "boolean") {
this.#value = parseBoolean(this.htmlValue);
} else if (this.type === "number") {
this.#value = parseNumber(this.htmlValue);
} else {
this.#value = this.htmlValue;
}
}

return this.#value;
}

/**
* Sets the value of the option, and associated type.
* @param value New value.
*/
public set value(value: boolean | number | string | undefined) {
this.type = typeof value === "number" ? "number" : typeof value === "boolean" ? "boolean" : "string";
this.htmlValue = value?.toString();
}

/**
* @inheritdoc
*/
protected override update(changedProperties: Map<PropertyKey, unknown>): void {
super.update(changedProperties);
this.dispatchEvent(new Event("update"));
}

/**
* @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;
}
this.#value = null;
}
}
}
Expand Down
121 changes: 16 additions & 105 deletions src/ui/components/radio-group.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { css, html, LitElement, type TemplateResult } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
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.
Expand All @@ -17,92 +16,10 @@ export class SDRadioGroupElement extends List(Input<boolean | number | string>(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);
}
`,
];
Expand All @@ -114,25 +31,19 @@ export class SDRadioGroupElement extends List(Input<boolean | number | string>(L
return html`
${repeat(
this.items,
({ key }) => key,
({ disabled, label, value }) => {
return html`
<label role="radio" @mousedown=${preventDoubleClickSelection}>
<input
name="radio"
type="radio"
value=${ifDefined(value)}
tabindex=${ifDefined(disabled ? undefined : 0)}
.checked=${this.value === value}
.disabled=${disabled}
@change=${(): void => {
this.value = value;
}}
/>
<span class="indicator"></span>
${label}
</label>
`;
(opt) => opt,
(opt) => {
return html`<sd-radio
name="__radio"
.checked=${this.value === opt.value}
.disabled=${opt.disabled}
.label=${opt.label}
.value=${opt.value}
@change=${(): void => {
this.value = opt.value;
}}
>${opt.innerText}</sd-radio
>`;
},
)}
`;
Expand Down
Loading

0 comments on commit 47a784a

Please sign in to comment.