Skip to content

Commit

Permalink
next: switch (#478)
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte authored Apr 18, 2024
1 parent 4c35358 commit af6d78c
Show file tree
Hide file tree
Showing 10 changed files with 266 additions and 158 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@
...restProps
}: RootProps = $props();
const disabled = readonlyBox(() => disabledProp);
const value = box(
() => valueProp,
(v) => {
valueProp = v;
onValueChange?.(v);
}
);
const disabled = readonlyBox(() => disabledProp);
const orientation = readonlyBox(() => orientationProp);
const loop = readonlyBox(() => loopProp);
const name = readonlyBox(() => nameProp);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,8 @@
<script lang="ts">
import { melt } from "@melt-ui/svelte";
import { getCtx } from "../ctx.js";
import type { InputProps } from "../index.js";
import { getSwitchInputState } from "../switch.svelte.js";
import { srOnlyStyles, styleToString } from "$lib/internal/style.js";
type $$Props = InputProps;
export let el: $$Props["el"] = undefined;
const {
elements: { input },
options: { value, name, disabled, required },
} = getCtx();
$: inputValue = $value === undefined || $value === "" ? "on" : $value;
const inputState = getSwitchInputState();
</script>

<input
bind:this={el}
use:melt={$input}
name={$name}
disabled={$disabled}
required={$required}
value={inputValue}
{...$$restProps}
/>
<input {...inputState.props} style={styleToString(srOnlyStyles)} />
27 changes: 11 additions & 16 deletions packages/bits-ui/src/lib/bits/switch/components/switch-thumb.svelte
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
<script lang="ts">
import { getCtx } from "../ctx.js";
import type { ThumbProps } from "../index.js";
import { getSwitchThumbState } from "../switch.svelte.js";
import { styleToString } from "$lib/internal/style.js";
type $$Props = ThumbProps;
let { asChild, child, el = $bindable(), style = {}, ...restProps }: ThumbProps = $props();
export let asChild: $$Props["asChild"] = false;
export let el: $$Props["el"] = undefined;
const thumbState = getSwitchThumbState();
const {
states: { checked },
getAttrs,
} = getCtx();
$: attrs = {
...getAttrs("thumb"),
"data-state": $checked ? "checked" : "unchecked",
"data-checked": $checked ? "" : undefined,
};
const mergedProps = $derived({
...restProps,
...thumbState.props,
style: styleToString(style),
});
</script>

{#if asChild}
<slot {attrs} checked={$checked} />
{@render child?.({ props: mergedProps, checked: thumbState.root.checked.value })}
{:else}
<span bind:this={el} {...$$restProps} {...attrs} />
<span bind:this={el} {...mergedProps} />
{/if}
106 changes: 49 additions & 57 deletions packages/bits-ui/src/lib/bits/switch/components/switch.svelte
Original file line number Diff line number Diff line change
@@ -1,70 +1,62 @@
<script lang="ts">
import { melt } from "@melt-ui/svelte";
import { setCtx } from "../ctx.js";
import type { Events, Props } from "../index.js";
import SwitchInput from "./switch-input.svelte";
import { createDispatcher } from "$lib/internal/events.js";
import type { RootProps } from "../index.js";
import { setSwitchRootState } from "../switch.svelte.js";
import { box, readonlyBox } from "$lib/internal/box.svelte.js";
import { styleToString } from "$lib/internal/style.js";
type $$Props = Props;
type $$Events = Events;
export let checked: $$Props["checked"] = undefined;
export let onCheckedChange: $$Props["onCheckedChange"] = undefined;
export let disabled: $$Props["disabled"] = undefined;
export let name: $$Props["name"] = undefined;
export let value: $$Props["value"] = undefined;
export let includeInput: $$Props["includeInput"] = true;
export let required: $$Props["required"] = undefined;
export let asChild: $$Props["asChild"] = false;
export let inputAttrs: $$Props["inputAttrs"] = undefined;
export let el: $$Props["el"] = undefined;
let {
child,
asChild,
children,
el = $bindable(),
disabled: disabledProp = false,
required: requiredProp = false,
checked: checkedProp = false,
value: valueProp = "",
name: nameProp = undefined,
onclick: onclickProp = () => {},
onkeydown: onkeydownProp = () => {},
onCheckedChange,
style = {},
...restProps
}: RootProps = $props();
const {
elements: { root },
states: { checked: localChecked },
updateOption,
getAttrs,
} = setCtx({
const checked = box(
() => checkedProp,
(v) => {
checkedProp = v;
onCheckedChange?.(v);
}
);
const disabled = readonlyBox(() => disabledProp);
const required = readonlyBox(() => requiredProp);
const value = readonlyBox(() => valueProp);
const name = readonlyBox(() => nameProp);
const onclick = readonlyBox(() => onclickProp);
const onkeydown = readonlyBox(() => onkeydownProp);
const rootState = setSwitchRootState({
checked,
disabled,
name,
value,
required,
defaultChecked: checked,
onCheckedChange: ({ next }) => {
if (checked !== next) {
onCheckedChange?.(next);
checked = next;
}
return next;
},
value,
name,
onclick,
onkeydown,
});
const dispatch = createDispatcher();
$: checked !== undefined && localChecked.set(checked);
$: updateOption("disabled", disabled);
$: updateOption("name", name);
$: updateOption("value", value);
$: updateOption("required", required);
$: builder = $root;
$: attrs = { ...getAttrs("root"), "data-checked": checked ? "" : undefined };
$: Object.assign(builder, attrs);
const mergedProps = $derived({
...restProps,
...rootState.props,
style: styleToString(style),
});
</script>

{#if asChild}
<slot {builder} />
{@render child?.({ props: mergedProps, checked: rootState.checked.value })}
{:else}
<button
bind:this={el}
use:melt={builder}
type="button"
{...$$restProps}
on:m-click={dispatch}
on:m-keydown={dispatch}
>
<slot {builder} />
<button bind:this={el} {...mergedProps}>
{@render children?.()}
</button>
{/if}
{#if includeInput}
<SwitchInput {...inputAttrs} />
{/if}
30 changes: 0 additions & 30 deletions packages/bits-ui/src/lib/bits/switch/ctx.ts

This file was deleted.

8 changes: 1 addition & 7 deletions packages/bits-ui/src/lib/bits/switch/index.ts
Original file line number Diff line number Diff line change
@@ -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";
144 changes: 144 additions & 0 deletions packages/bits-ui/src/lib/bits/switch/switch.svelte.ts
Original file line number Diff line number Diff line change
@@ -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<MouseEvent>;
onkeydown: EventCallback<KeyboardEvent>;
}> &
BoxedValues<{
checked: boolean;
}>;

class SwitchRootState {
checked: SwitchRootStateProps["checked"];
disabled: SwitchRootStateProps["disabled"];
required: SwitchRootStateProps["required"];
name: SwitchRootStateProps["name"];
value: SwitchRootStateProps["value"];
#composedClick: EventCallback<MouseEvent>;
#composedKeydown: EventCallback<KeyboardEvent>;

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();
}
Loading

0 comments on commit af6d78c

Please sign in to comment.