Skip to content

Commit

Permalink
next: Radio Group (WIP) (#471)
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte authored Apr 17, 2024
1 parent f036e27 commit ae9ce2b
Show file tree
Hide file tree
Showing 16 changed files with 467 additions and 204 deletions.
43 changes: 23 additions & 20 deletions packages/bits-ui/src/lib/bits/accordion/accordion.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
verifyContextDeps,
} from "$lib/internal/index.js";
import type { StyleProperties } from "$lib/shared/index.js";
import { withTick } from "$lib/internal/with-tick.js";
import { useNodeById } from "$lib/internal/elements.svelte.js";

/**
* BASE
Expand All @@ -28,6 +30,7 @@ type AccordionBaseStateProps = ReadonlyBoxedValues<{

class AccordionBaseState {
id = undefined as unknown as ReadonlyBox<string>;
node = boxedState<HTMLElement | null>(null);
disabled: ReadonlyBox<boolean>;
#attrs = $derived({
id: this.id.value,
Expand All @@ -37,6 +40,15 @@ class AccordionBaseState {
constructor(props: AccordionBaseStateProps) {
this.id = props.id;
this.disabled = props.disabled;

useNodeById(this.id, this.node);
}

getTriggerNodes() {
if (!this.node.value) return [];
return Array.from(
this.node.value.querySelectorAll<HTMLElement>("[data-accordion-trigger]")
).filter((el) => !el.dataset.disabled);
}

get props() {
Expand Down Expand Up @@ -160,6 +172,7 @@ type AccordionTriggerStateProps = ReadonlyBoxedValues<{
class AccordionTriggerState {
#disabled = undefined as unknown as ReadonlyBox<boolean>;
#id = undefined as unknown as ReadonlyBox<string>;
#node = boxedState<HTMLElement | null>(null);
#root = undefined as unknown as AccordionState;
#itemState = undefined as unknown as AccordionItemState;
#onclickProp = boxedState<AccordionTriggerStateProps["onclick"]>(readonlyBox(() => () => {}));
Expand Down Expand Up @@ -189,6 +202,8 @@ class AccordionTriggerState {
this.#onclickProp.value = props.onclick;
this.#onkeydownProp.value = props.onkeydown;
this.#id = props.id;

useNodeById(this.#id, this.#node);
}

#onclick = composeHandlers(this.#onclickProp, () => {
Expand All @@ -207,20 +222,12 @@ class AccordionTriggerState {
return;
}

if (!this.#root.id.value || !this.#id.value) return;

const rootEl = document.getElementById(this.#root.id.value);
if (!rootEl) return;
const itemEl = document.getElementById(this.#id.value);
if (!itemEl) return;
if (!this.#root.node.value || !this.#node.value) return;

const items = Array.from(rootEl.querySelectorAll<HTMLElement>("[data-accordion-trigger]"));
if (!items.length) return;

const candidateItems = items.filter((item) => !item.dataset.disabled);
const candidateItems = this.#root.getTriggerNodes();
if (!candidateItems.length) return;

const currentIndex = candidateItems.indexOf(itemEl);
const currentIndex = candidateItems.indexOf(this.#node.value);

const keyToIndex = {
[kbd.ARROW_DOWN]: (currentIndex + 1) % candidateItems.length,
Expand Down Expand Up @@ -282,11 +289,7 @@ class AccordionContentState {
this.#id = props.id;
this.#styleProp = props.style;

$effect.root(() => {
tick().then(() => {
this.node.value = document.getElementById(this.#id.value);
});
});
useNodeById(this.#id, this.node);

$effect.pre(() => {
const rAF = requestAnimationFrame(() => {
Expand All @@ -304,7 +307,7 @@ class AccordionContentState {
const node = this.node.value;
if (!node) return;

tick().then(() => {
withTick(() => {
if (!this.node) return;
// get the dimensions of the element
this.#originalStyles = this.#originalStyles || {
Expand Down Expand Up @@ -335,9 +338,9 @@ class AccordionContentState {
}
}

/**
* CONTEXT METHODS
*/
//
// CONTEXT METHODS
//

export const ACCORDION_ROOT_KEY = Symbol("Accordion.Root");
export const ACCORDION_ITEM_KEY = Symbol("Accordion.Item");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
let {
checked: checkedProp = $bindable(false),
onCheckedChange,
children,
disabled: disabledProp = false,
required: requiredProp = false,
name: nameProp,
Expand Down Expand Up @@ -58,6 +59,7 @@
{:else}
<button bind:this={el} {...mergedProps}>
{@render indicator?.({ checked: checkboxState.checked.value })}
{@render children?.()}
</button>
{/if}

Expand Down
12 changes: 6 additions & 6 deletions packages/bits-ui/src/lib/bits/collapsible/collapsible.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { generateId } from "$lib/internal/id.js";
import { styleToString } from "$lib/internal/style.js";
import { type EventCallback, composeHandlers } from "$lib/internal/events.js";
import type { StyleProperties } from "$lib/shared/index.js";
import { withTick } from "$lib/internal/with-tick.js";
import { useNodeById } from "$lib/internal/elements.svelte.js";
import { verifyContextDeps } from "$lib/internal/context.js";

type CollapsibleRootStateProps = BoxedValues<{
open: boolean;
Expand Down Expand Up @@ -88,11 +91,7 @@ class CollapsibleContentState {
this.root.contentId = props.id;
this.#styleProp = props.style;

$effect.root(() => {
tick().then(() => {
this.node.value = document.getElementById(this.root.contentId.value);
});
});
useNodeById(this.root.contentId, this.node);

$effect.pre(() => {
const rAF = requestAnimationFrame(() => {
Expand All @@ -110,7 +109,7 @@ class CollapsibleContentState {
const node = this.node.value;
if (!node) return;

tick().then(() => {
withTick(() => {
if (!this.node) return;
// get the dimensions of the element
this.#originalStyles = this.#originalStyles || {
Expand Down Expand Up @@ -182,6 +181,7 @@ export function setCollapsibleRootState(props: CollapsibleRootStateProps) {
}

export function getCollapsibleRootState() {
verifyContextDeps(COLLAPSIBLE_ROOT_KEY);
return getContext<CollapsibleRootState>(COLLAPSIBLE_ROOT_KEY);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,2 @@
<script lang="ts">
import { melt } from "@melt-ui/svelte";
import type { InputProps } from "../index.js";
import { getCtx } from "../ctx.js";
type $$Props = InputProps;
export let asChild: $$Props["asChild"] = false;
export let el: $$Props["el"] = undefined;
const {
elements: { hiddenInput },
getAttrs,
} = getCtx();
const attrs = getAttrs("input");
$: builder = $hiddenInput;
$: Object.assign(builder, attrs);
</script>

{#if asChild}
<slot {builder} />
{:else}
<input bind:this={el} use:melt={builder} {...$$restProps} />
{/if}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,41 +1,43 @@
<script lang="ts">
import { melt } from "@melt-ui/svelte";
import { setItemCtx } from "../ctx.js";
import type { ItemEvents, ItemProps } from "../index.js";
import { createDispatcher } from "$lib/internal/events.js";
import type { ItemProps } from "../index.js";
import { setRadioGroupItemState } from "../radio-group.svelte.js";
import { generateId } from "$lib/internal/id.js";
import { readonlyBox } from "$lib/internal/box.svelte.js";
import { styleToString } from "$lib/internal/style.js";
type $$Props = ItemProps;
type $$Events = ItemEvents;
let {
id: idProp = generateId(),
asChild,
children,
child,
value: valueProp,
disabled: disabledProp = false,
onclick: onclickProp = () => {},
onkeydown: onkeydownProp = () => {},
el = $bindable(),
style = {},
...restProps
}: ItemProps = $props();
export let value: $$Props["value"];
export let disabled: $$Props["disabled"] = false;
export let asChild: $$Props["asChild"] = false;
export let el: $$Props["el"] = undefined;
const value = readonlyBox(() => valueProp);
const disabled = readonlyBox(() => disabledProp);
const id = readonlyBox(() => idProp);
const onclick = readonlyBox(() => onclickProp);
const onkeydown = readonlyBox(() => onkeydownProp);
const {
elements: { item },
getAttrs,
} = setItemCtx(value);
const item = setRadioGroupItemState({ value, disabled, id, onclick, onkeydown });
const dispatch = createDispatcher();
const attrs = getAttrs("item");
$: builder = $item({ value, disabled });
$: Object.assign(builder, attrs);
const mergedProps = $derived({
...restProps,
...item.props,
style: styleToString(style),
});
</script>

{#if asChild}
<slot {builder} />
{@render child?.({ props: mergedProps })}
{:else}
<button
bind:this={el}
use:melt={builder}
type="button"
{...$$restProps}
on:m-click={dispatch}
on:m-focus={dispatch}
on:m-keydown={dispatch}
>
<slot {builder} />
<button bind:this={el} {...mergedProps}>
{@render children?.()}
</button>
{/if}
Original file line number Diff line number Diff line change
@@ -1,54 +1,54 @@
<script lang="ts">
import { melt } from "@melt-ui/svelte";
import { setCtx } from "../ctx.js";
import type { Props } from "../index.js";
import type { RootProps } from "../index.js";
import { setRadioGroupRootState } from "../radio-group.svelte.js";
import { generateId } from "$lib/internal/id.js";
import { box, readonlyBox } from "$lib/internal/box.svelte.js";
import { styleToString } from "$lib/internal/style.js";
type $$Props = Props;
export let required: $$Props["required"] = undefined;
export let disabled: $$Props["disabled"] = undefined;
export let value: $$Props["value"] = undefined;
export let onValueChange: $$Props["onValueChange"] = undefined;
export let loop: $$Props["loop"] = undefined;
export let orientation: $$Props["orientation"] = undefined;
export let asChild: $$Props["asChild"] = false;
export let el: $$Props["el"] = undefined;
let {
disabled: disabledProp = false,
asChild,
children,
child,
style,
value: valueProp = $bindable(""),
el = $bindable(),
orientation: orientationProp = "vertical",
loop: loopProp = true,
name: nameProp = undefined,
required: requiredProp = false,
id: idProp = generateId(),
onValueChange,
...restProps
}: RootProps = $props();
const {
elements: { root },
states: { value: localValue },
updateOption,
getAttrs,
} = setCtx({
required,
disabled,
defaultValue: value,
loop,
orientation,
onValueChange: ({ next }) => {
if (value !== next) {
onValueChange?.(next);
value = next;
}
return next;
},
});
const attrs = getAttrs("root");
const disabled = readonlyBox(() => disabledProp);
const value = box(
() => valueProp,
(v) => {
valueProp = v;
onValueChange?.(v);
}
);
const orientation = readonlyBox(() => orientationProp);
const loop = readonlyBox(() => loopProp);
const name = readonlyBox(() => nameProp);
const required = readonlyBox(() => requiredProp);
const id = readonlyBox(() => idProp);
$: value !== undefined && localValue.set(value);
$: updateOption("required", required);
$: updateOption("disabled", disabled);
$: updateOption("loop", loop);
$: updateOption("orientation", orientation);
const root = setRadioGroupRootState({ orientation, disabled, loop, name, required, id, value });
$: builder = $root;
$: Object.assign(builder, attrs);
const mergedProps = $derived({
...restProps,
...root.props,
style: styleToString(style),
});
</script>

{#if asChild}
<slot {builder} />
{@render child?.({ props: mergedProps })}
{:else}
<div bind:this={el} use:melt={builder} {...$$restProps}>
<slot {builder} />
<div bind:this={el} {...mergedProps}>
{@render children?.()}
</div>
{/if}
Loading

0 comments on commit ae9ce2b

Please sign in to comment.