diff --git a/packages/bits-ui/src/lib/bits/radio-group/components/radio-group.svelte b/packages/bits-ui/src/lib/bits/radio-group/components/radio-group.svelte index ed79f4681..a9a1bd143 100644 --- a/packages/bits-ui/src/lib/bits/radio-group/components/radio-group.svelte +++ b/packages/bits-ui/src/lib/bits/radio-group/components/radio-group.svelte @@ -22,7 +22,6 @@ ...restProps }: RootProps = $props(); - const disabled = readonlyBox(() => disabledProp); const value = box( () => valueProp, (v) => { @@ -30,6 +29,7 @@ onValueChange?.(v); } ); + const disabled = readonlyBox(() => disabledProp); const orientation = readonlyBox(() => orientationProp); const loop = readonlyBox(() => loopProp); const name = readonlyBox(() => nameProp); diff --git a/packages/bits-ui/src/lib/bits/switch/components/switch-input.svelte b/packages/bits-ui/src/lib/bits/switch/components/switch-input.svelte index 7dfe4ac46..c68d90157 100644 --- a/packages/bits-ui/src/lib/bits/switch/components/switch-input.svelte +++ b/packages/bits-ui/src/lib/bits/switch/components/switch-input.svelte @@ -1,25 +1,8 @@ - + diff --git a/packages/bits-ui/src/lib/bits/switch/components/switch-thumb.svelte b/packages/bits-ui/src/lib/bits/switch/components/switch-thumb.svelte index dc100bed1..f6d6a7267 100644 --- a/packages/bits-ui/src/lib/bits/switch/components/switch-thumb.svelte +++ b/packages/bits-ui/src/lib/bits/switch/components/switch-thumb.svelte @@ -1,26 +1,21 @@ {#if asChild} - + {@render child?.({ props: mergedProps, checked: thumbState.root.checked.value })} {:else} - + {/if} diff --git a/packages/bits-ui/src/lib/bits/switch/components/switch.svelte b/packages/bits-ui/src/lib/bits/switch/components/switch.svelte index dc0cb1cf0..4d650bdda 100644 --- a/packages/bits-ui/src/lib/bits/switch/components/switch.svelte +++ b/packages/bits-ui/src/lib/bits/switch/components/switch.svelte @@ -1,70 +1,62 @@ {#if asChild} - + {@render child?.({ props: mergedProps, checked: rootState.checked.value })} {:else} - {/if} -{#if includeInput} - -{/if} diff --git a/packages/bits-ui/src/lib/bits/switch/ctx.ts b/packages/bits-ui/src/lib/bits/switch/ctx.ts deleted file mode 100644 index ad98dfa94..000000000 --- a/packages/bits-ui/src/lib/bits/switch/ctx.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { type CreateSwitchProps, createSwitch } from "@melt-ui/svelte"; -import { getContext, setContext } from "svelte"; -import { createBitAttrs, getOptionUpdater, removeUndefined } from "$lib/internal/index.js"; - -function getSwitchData() { - const NAME = "switch" as const; - const PARTS = ["root", "input", "thumb"] as const; - return { - NAME, - PARTS, - }; -} - -type GetReturn = Omit, "updateOption">; - -export function setCtx(props: CreateSwitchProps) { - const { NAME, PARTS } = getSwitchData(); - const getAttrs = createBitAttrs(NAME, PARTS); - const Switch = { ...createSwitch(removeUndefined(props)), getAttrs }; - setContext(NAME, Switch); - return { - ...Switch, - updateOption: getOptionUpdater(Switch.options), - }; -} - -export function getCtx() { - const { NAME } = getSwitchData(); - return getContext(NAME); -} diff --git a/packages/bits-ui/src/lib/bits/switch/index.ts b/packages/bits-ui/src/lib/bits/switch/index.ts index 4c948247c..4f4f8b7d7 100644 --- a/packages/bits-ui/src/lib/bits/switch/index.ts +++ b/packages/bits-ui/src/lib/bits/switch/index.ts @@ -1,10 +1,4 @@ export { default as Root } from "./components/switch.svelte"; export { default as Thumb } from "./components/switch-thumb.svelte"; -export { default as Input } from "./components/switch-input.svelte"; -export type { - SwitchProps as Props, - SwitchThumbProps as ThumbProps, - SwitchInputProps as InputProps, - SwitchEvents as Events, -} from "./types.js"; +export type { SwitchRootProps as RootProps, SwitchThumbProps as ThumbProps } from "./types.js"; diff --git a/packages/bits-ui/src/lib/bits/switch/switch.svelte.ts b/packages/bits-ui/src/lib/bits/switch/switch.svelte.ts new file mode 100644 index 000000000..f903de46a --- /dev/null +++ b/packages/bits-ui/src/lib/bits/switch/switch.svelte.ts @@ -0,0 +1,144 @@ +import { getContext, setContext } from "svelte"; +import { + getAriaChecked, + getAriaRequired, + getDataChecked, + getDataDisabled, + getDataRequired, +} from "$lib/internal/attrs.js"; +import type { BoxedValues, ReadonlyBoxedValues } from "$lib/internal/box.svelte.js"; +import { type EventCallback, composeHandlers } from "$lib/internal/events.js"; +import { kbd } from "$lib/internal/kbd.js"; + +type SwitchRootStateProps = ReadonlyBoxedValues<{ + disabled: boolean; + required: boolean; + name: string | undefined; + value: string; + onclick: EventCallback; + onkeydown: EventCallback; +}> & + BoxedValues<{ + checked: boolean; + }>; + +class SwitchRootState { + checked: SwitchRootStateProps["checked"]; + disabled: SwitchRootStateProps["disabled"]; + required: SwitchRootStateProps["required"]; + name: SwitchRootStateProps["name"]; + value: SwitchRootStateProps["value"]; + #composedClick: EventCallback; + #composedKeydown: EventCallback; + + constructor(props: SwitchRootStateProps) { + this.checked = props.checked; + this.disabled = props.disabled; + this.required = props.required; + this.name = props.name; + this.value = props.value; + this.#composedClick = composeHandlers(props.onclick, this.#onclick); + this.#composedKeydown = composeHandlers(props.onkeydown, this.#onkeydown); + } + + #toggle() { + this.checked.value = !this.checked.value; + } + + #onkeydown = (e: KeyboardEvent) => { + if (!(e.key === kbd.ENTER || e.key === kbd.SPACE) || this.disabled.value) return; + e.preventDefault(); + this.#toggle(); + }; + + #onclick = () => { + if (this.disabled.value) return; + this.#toggle(); + }; + + get props() { + return { + "data-disabled": getDataDisabled(this.disabled.value), + "data-state": getDataChecked(this.checked.value), + "data-required": getDataRequired(this.required.value), + type: "button", + role: "switch", + "aria-checked": getAriaChecked(this.checked.value), + "aria-required": getAriaRequired(this.required.value), + "data-switch-root": "", + // + onclick: this.#composedClick, + onkeydown: this.#composedKeydown, + } as const; + } + + createInput() { + return new SwitchInputState(this); + } + + createThumb() { + return new SwitchThumbState(this); + } +} + +class SwitchInputState { + #root: SwitchRootState; + + constructor(root: SwitchRootState) { + this.#root = root; + } + + get shouldRender() { + return this.#root.name.value !== undefined; + } + + get props() { + return { + type: "checkbox", + name: this.#root.name.value, + value: this.#root.value.value, + checked: this.#root.checked.value, + disabled: this.#root.disabled.value, + required: this.#root.required.value, + } as const; + } +} + +class SwitchThumbState { + root: SwitchRootState; + + constructor(root: SwitchRootState) { + this.root = root; + } + + get props() { + return { + "data-disabled": getDataDisabled(this.root.disabled.value), + "data-state": getDataChecked(this.root.checked.value), + "data-required": getDataRequired(this.root.required.value), + "data-switch-thumb": "", + } as const; + } +} + +// +// CONTEXT METHODS +// + +const SWITCH_ROOT_KEY = Symbol("Switch.Root"); + +export function setSwitchRootState(props: SwitchRootStateProps) { + return setContext(SWITCH_ROOT_KEY, new SwitchRootState(props)); +} + +export function getSwitchRootState(): SwitchRootState { + return getContext(SWITCH_ROOT_KEY); +} + +export function getSwitchInputState(): SwitchInputState { + return getSwitchRootState().createInput(); +} + +export function getSwitchThumbState(): SwitchThumbState { + return getSwitchRootState().createThumb(); +} diff --git a/packages/bits-ui/src/lib/bits/switch/types.ts b/packages/bits-ui/src/lib/bits/switch/types.ts index 174487155..eaf62b8b8 100644 --- a/packages/bits-ui/src/lib/bits/switch/types.ts +++ b/packages/bits-ui/src/lib/bits/switch/types.ts @@ -3,53 +3,75 @@ import type { CreateSwitchProps as MeltSwitchProps } from "@melt-ui/svelte"; import type { DOMEl, DOMElement, + EventCallback, Expand, HTMLSpanAttributes, OmitChecked, OnChangeFn, + PrimitiveButtonAttributes, + PrimitiveSpanAttributes, + WithAsChild, } from "$lib/internal/index.js"; import type { CustomEventHandler } from "$lib/index.js"; -export type SwitchPropsWithoutHTML = Expand< - OmitChecked & { +export type SwitchRootPropsWithoutHTML = WithAsChild< + { /** - * The checked state of the switch. - * You can bind this to a boolean value to programmatically control the checked state. + * Whether the switch is disabled. * * @defaultValue false */ - checked?: boolean; + disabled?: boolean; /** - * A callback function called when the checked state changes. + * Whether the switch is required (for form validation). + * + * @defaultValue false */ - onCheckedChange?: OnChangeFn; + required?: boolean; /** - * Whether to include the hidden input element in the DOM. + * The name of the switch used in form submission. + * If not provided, the hidden input will not be rendered. + * + * @defaultValue undefined */ - includeInput?: boolean; + name?: string; /** - * Additional input attributes to pass to the hidden input element. - * Note, the value, name, type, and checked attributes are derived from the - * Switch props and cannot be overridden. + * The value of the switch used in form submission. + * + * @defaultValue undefined */ - inputAttrs?: Partial>; - } & DOMElement ->; + value?: string; + + /** + * The checked state of the switch. + * + * @defaultValue false + */ + checked?: boolean; -export type SwitchThumbPropsWithoutHTML = DOMElement; + /** + * A callback function called when the checked state changes. + */ + onCheckedChange?: OnChangeFn; -// + onclick?: EventCallback; -export type SwitchProps = SwitchPropsWithoutHTML & HTMLButtonAttributes; + onkeydown?: EventCallback; + }, + { + /** + * The checked state of the switch. + */ + checked: boolean; + } +>; -export type SwitchThumbProps = SwitchThumbPropsWithoutHTML & HTMLSpanAttributes; +export type SwitchRootProps = SwitchRootPropsWithoutHTML & + Omit; -export type SwitchInputProps = HTMLInputAttributes & DOMEl; +export type SwitchThumbPropsWithoutHTML = WithAsChild; -export type SwitchEvents = { - click: CustomEventHandler; - keydown: CustomEventHandler; -}; +export type SwitchThumbProps = SwitchThumbPropsWithoutHTML & PrimitiveSpanAttributes; diff --git a/packages/bits-ui/src/lib/internal/attrs.ts b/packages/bits-ui/src/lib/internal/attrs.ts index 091bedf02..5bb6c8c64 100644 --- a/packages/bits-ui/src/lib/internal/attrs.ts +++ b/packages/bits-ui/src/lib/internal/attrs.ts @@ -60,6 +60,10 @@ export function getDataOpenClosed(condition: boolean): "open" | "closed" { return condition ? "open" : "closed"; } +export function getDataChecked(condition: boolean): "checked" | "unchecked" { + return condition ? "checked" : "unchecked"; +} + export function dataDisabledAttrs(condition: boolean): "" | undefined { return condition ? "" : undefined; } @@ -102,3 +106,7 @@ export function getDataOrientation( ): "horizontal" | "vertical" { return orientation; } + +export function getDataRequired(condition: boolean): "" | undefined { + return condition ? "" : undefined; +} diff --git a/sites/docs/src/lib/components/demos/switch-demo.svelte b/sites/docs/src/lib/components/demos/switch-demo.svelte index dab33f822..b2fb9c625 100644 --- a/sites/docs/src/lib/components/demos/switch-demo.svelte +++ b/sites/docs/src/lib/components/demos/switch-demo.svelte @@ -4,12 +4,12 @@
- Do not disturb + Do not disturb