Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

next: Radio Group (WIP) #471

Merged
merged 3 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading