-
+
+ {@render children?.()}
{/if}
diff --git a/packages/bits-ui/src/lib/bits/toolbar/components/toolbar-link.svelte b/packages/bits-ui/src/lib/bits/toolbar/components/toolbar-link.svelte
index 183bcccad..ccb98d38d 100644
--- a/packages/bits-ui/src/lib/bits/toolbar/components/toolbar-link.svelte
+++ b/packages/bits-ui/src/lib/bits/toolbar/components/toolbar-link.svelte
@@ -1,39 +1,31 @@
{#if asChild}
-
+ {@render child?.({ props: mergedProps })}
{:else}
-
-
-
-
+
+ {@render children?.()}
+
{/if}
diff --git a/packages/bits-ui/src/lib/bits/toolbar/components/toolbar.svelte b/packages/bits-ui/src/lib/bits/toolbar/components/toolbar.svelte
index de98dd1a5..112d8c010 100644
--- a/packages/bits-ui/src/lib/bits/toolbar/components/toolbar.svelte
+++ b/packages/bits-ui/src/lib/bits/toolbar/components/toolbar.svelte
@@ -1,37 +1,35 @@
{#if asChild}
-
+ {@render child?.({ props: mergedProps })}
{:else}
-
-
+
+ {@render children?.()}
-{/if}
+{/if}
\ No newline at end of file
diff --git a/packages/bits-ui/src/lib/bits/toolbar/ctx.ts b/packages/bits-ui/src/lib/bits/toolbar/ctx.ts
deleted file mode 100644
index a0d596fb3..000000000
--- a/packages/bits-ui/src/lib/bits/toolbar/ctx.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import {
- type CreateToolbarGroupProps as ToolbarGroupProps,
- type CreateToolbarProps as ToolbarProps,
- createToolbar,
-} from "@melt-ui/svelte";
-import { getContext, setContext } from "svelte";
-import { createBitAttrs, getOptionUpdater, removeUndefined } from "$lib/internal/index.js";
-
-function getToolbarData() {
- const NAME = "toolbar" as const;
- const GROUP_NAME = "toolbar-group";
- const PARTS = ["root", "button", "link", "group", "group-item"] as const;
- return {
- NAME,
- GROUP_NAME,
- PARTS,
- };
-}
-
-type GetReturn = Omit
, "updateOption">;
-type GetGroupReturn = Omit, "updateOption">;
-
-export function setCtx(props: ToolbarProps) {
- const { NAME, PARTS } = getToolbarData();
- const getAttrs = createBitAttrs(NAME, PARTS);
- const toolbar = { ...createToolbar(removeUndefined(props)), getAttrs };
- setContext(NAME, toolbar);
- return {
- ...toolbar,
- updateOption: getOptionUpdater(toolbar.options),
- };
-}
-
-export function setGroupCtx(props: ToolbarGroupProps) {
- const {
- builders: { createToolbarGroup },
- getAttrs,
- } = getCtx();
- const group = { ...createToolbarGroup(removeUndefined(props)), getAttrs };
- const { GROUP_NAME } = getToolbarData();
- setContext(GROUP_NAME, group);
- return {
- ...group,
- updateOption: getOptionUpdater(group.options),
- };
-}
-
-export function getCtx() {
- const { NAME } = getToolbarData();
- return getContext(NAME);
-}
-
-export function getGroupCtx() {
- const { GROUP_NAME } = getToolbarData();
- return getContext(GROUP_NAME);
-}
diff --git a/packages/bits-ui/src/lib/bits/toolbar/index.ts b/packages/bits-ui/src/lib/bits/toolbar/index.ts
index ac2a585ae..f4fd2b71a 100644
--- a/packages/bits-ui/src/lib/bits/toolbar/index.ts
+++ b/packages/bits-ui/src/lib/bits/toolbar/index.ts
@@ -5,12 +5,9 @@ export { default as Group } from "./components/toolbar-group.svelte";
export { default as GroupItem } from "./components/toolbar-group-item.svelte";
export type {
- ToolbarProps as Props,
+ ToolbarRootProps as RootProps,
ToolbarButtonProps as ButtonProps,
ToolbarLinkProps as LinkProps,
ToolbarGroupProps as GroupProps,
ToolbarGroupItemProps as GroupItemProps,
- ToolbarButtonEvents as ButtonEvents,
- ToolbarLinkEvents as LinkEvents,
- ToolbarGroupItemEvents as GroupItemEvents,
} from "./types.js";
diff --git a/packages/bits-ui/src/lib/bits/toolbar/toolbar.svelte.ts b/packages/bits-ui/src/lib/bits/toolbar/toolbar.svelte.ts
new file mode 100644
index 000000000..bde2041c1
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/toolbar/toolbar.svelte.ts
@@ -0,0 +1,404 @@
+import { getContext, setContext } from "svelte";
+import {
+ getAriaChecked,
+ getAriaPressed,
+ getDataDisabled,
+ getDataOrientation,
+ getDisabledAttr,
+} from "$lib/internal/attrs.js";
+import {
+ type Box,
+ type BoxedValues,
+ type ReadonlyBoxedValues,
+ boxedState,
+} from "$lib/internal/box.svelte.js";
+import { verifyContextDeps } from "$lib/internal/context.js";
+import { kbd } from "$lib/internal/kbd.js";
+import { useNodeById } from "$lib/internal/use-node-by-id.svelte.js";
+import {
+ type UseRovingFocusReturn,
+ useRovingFocus,
+} from "$lib/internal/use-roving-focus.svelte.js";
+import type { Orientation } from "$lib/shared/index.js";
+
+const ROOT_ATTR = "data-toolbar-root";
+// all links, buttons, and items must have the ITEM_ATTR for roving focus
+const ITEM_ATTR = "data-toolbar-item";
+const GROUP_ATTR = "data-toolbar-group";
+const GROUP_ITEM_ATTR = "data-toolbar-group-item";
+const LINK_ATTR = "data-toolbar-link";
+const BUTTON_ATTR = "data-toolbar-button";
+
+type ToolbarRootStateProps = ReadonlyBoxedValues<{
+ orientation: Orientation;
+ loop: boolean;
+ id: string;
+}>;
+
+class ToolbarRootState {
+ #id = undefined as unknown as ToolbarRootStateProps["id"];
+ orientation = undefined as unknown as ToolbarRootStateProps["orientation"];
+ #loop = undefined as unknown as ToolbarRootStateProps["loop"];
+ #node = boxedState(null);
+ rovingFocusGroup = undefined as unknown as UseRovingFocusReturn;
+
+ constructor(props: ToolbarRootStateProps) {
+ this.#id = props.id;
+ this.orientation = props.orientation;
+ this.#loop = props.loop;
+ this.#node = useNodeById(this.#id);
+ this.rovingFocusGroup = useRovingFocus({
+ orientation: this.orientation,
+ loop: this.#loop,
+ rootNode: this.#node,
+ candidateSelector: ITEM_ATTR,
+ });
+ }
+
+ createGroup(props: InitToolbarGroupProps) {
+ const { type, ...rest } = props;
+ const groupState =
+ type === "single"
+ ? new ToolbarGroupSingleState(rest as ToolbarGroupSingleStateProps, this)
+ : new ToolbarGroupMultipleState(rest as ToolbarGroupMultipleStateProps, this);
+ return groupState;
+ }
+
+ createLink(props: ToolbarLinkStateProps) {
+ return new ToolbarLinkState(props, this);
+ }
+
+ createButton(props: ToolbarButtonStateProps) {
+ return new ToolbarButtonState(props, this);
+ }
+
+ props = $derived({
+ id: this.#id.value,
+ role: "toolbar",
+ "data-orientation": this.orientation.value,
+ [ROOT_ATTR]: "",
+ } as const);
+}
+
+type ToolbarGroupBaseStateProps = ReadonlyBoxedValues<{
+ id: string;
+ disabled: boolean;
+}>;
+
+class ToolbarGroupBaseState {
+ id = undefined as unknown as ToolbarGroupBaseStateProps["id"];
+ node = boxedState(null);
+ disabled = undefined as unknown as ToolbarGroupBaseStateProps["disabled"];
+ root = undefined as unknown as ToolbarRootState;
+
+ constructor(props: ToolbarGroupBaseStateProps, root: ToolbarRootState) {
+ this.id = props.id;
+ this.node = useNodeById(this.id);
+ this.disabled = props.disabled;
+ this.root = root;
+ }
+
+ props = $derived({
+ id: this.id.value,
+ [GROUP_ATTR]: "",
+ role: "group",
+ "data-orientation": getDataOrientation(this.root.orientation.value),
+ "data-disabled": getDataDisabled(this.disabled.value),
+ } as const);
+}
+
+//
+// SINGLE
+//
+
+type ToolbarGroupSingleStateProps = ToolbarGroupBaseStateProps &
+ BoxedValues<{
+ value: string;
+ }>;
+
+class ToolbarGroupSingleState extends ToolbarGroupBaseState {
+ #value = undefined as unknown as ToolbarGroupSingleStateProps["value"];
+ isMulti = false;
+ anyPressed = $derived(this.#value.value !== "");
+
+ constructor(props: ToolbarGroupSingleStateProps, root: ToolbarRootState) {
+ super(props, root);
+ this.#value = props.value;
+ }
+
+ createItem(props: ToolbarGroupItemStateProps) {
+ return new ToolbarGroupItemState(props, this, this.root);
+ }
+
+ includesItem(item: string) {
+ return this.#value.value === item;
+ }
+
+ toggleItem(item: string) {
+ if (this.includesItem(item)) {
+ this.#value.value = "";
+ } else {
+ this.#value.value = item;
+ }
+ }
+}
+
+//
+// MULTIPLE
+//
+
+type ToolbarGroupMultipleStateProps = ToolbarGroupBaseStateProps &
+ BoxedValues<{
+ value: string[];
+ }>;
+
+class ToolbarGroupMultipleState extends ToolbarGroupBaseState {
+ #value = undefined as unknown as ToolbarGroupMultipleStateProps["value"];
+ isMulti = true;
+ anyPressed = $derived(this.#value.value.length > 0);
+
+ constructor(props: ToolbarGroupMultipleStateProps, root: ToolbarRootState) {
+ super(props, root);
+ this.#value = props.value;
+ }
+
+ createItem(props: ToolbarGroupItemStateProps) {
+ return new ToolbarGroupItemState(props, this, this.root);
+ }
+
+ includesItem(item: string) {
+ return this.#value.value.includes(item);
+ }
+
+ toggleItem(item: string) {
+ if (this.includesItem(item)) {
+ this.#value.value = this.#value.value.filter((v) => v !== item);
+ } else {
+ this.#value.value = [...this.#value.value, item];
+ }
+ }
+}
+
+type ToolbarGroupState = ToolbarGroupSingleState | ToolbarGroupMultipleState;
+
+//
+// ITEM
+//
+
+type ToolbarGroupItemStateProps = ReadonlyBoxedValues<{
+ id: string;
+ value: string;
+ disabled: boolean;
+}>;
+
+class ToolbarGroupItemState {
+ #id = undefined as unknown as ToolbarGroupItemStateProps["id"];
+ #group = undefined as unknown as ToolbarGroupState;
+ #root = undefined as unknown as ToolbarRootState;
+ #value = undefined as unknown as ToolbarGroupItemStateProps["value"];
+ #node = boxedState(null);
+ #disabled = undefined as unknown as ToolbarGroupItemStateProps["disabled"];
+ #isDisabled = $derived(this.#disabled.value || this.#group.disabled.value);
+
+ constructor(
+ props: ToolbarGroupItemStateProps,
+ group: ToolbarGroupState,
+ root: ToolbarRootState
+ ) {
+ this.#value = props.value;
+ this.#disabled = props.disabled;
+ this.#group = group;
+ this.#root = root;
+ this.#id = props.id;
+ this.#node = useNodeById(this.#id);
+ }
+
+ toggleItem() {
+ if (this.#isDisabled) return;
+ this.#group.toggleItem(this.#value.value);
+ }
+
+ #onclick = () => {
+ this.toggleItem();
+ };
+
+ #onkeydown = (e: KeyboardEvent) => {
+ if (this.#isDisabled) return;
+ if (e.key === kbd.ENTER || e.key === kbd.SPACE) {
+ e.preventDefault();
+ this.toggleItem();
+ return;
+ }
+
+ this.#root.rovingFocusGroup.handleKeydown(this.#node.value, e);
+ };
+
+ #isPressed = $derived(this.#group.includesItem(this.#value.value));
+
+ #ariaChecked = $derived.by(() => {
+ return this.#group.isMulti ? undefined : getAriaChecked(this.#isPressed);
+ });
+
+ #ariaPressed = $derived.by(() => {
+ return this.#group.isMulti ? undefined : getAriaPressed(this.#isPressed);
+ });
+
+ #tabIndex = $derived(this.#root.rovingFocusGroup.getTabIndex(this.#node.value).value);
+
+ props = $derived({
+ id: this.#id.value,
+ role: this.#group.isMulti ? undefined : "radio",
+ tabindex: this.#tabIndex,
+ "data-orientation": getDataOrientation(this.#root.orientation.value),
+ "data-disabled": getDataDisabled(this.#isDisabled),
+ "data-state": getToggleItemDataState(this.#isPressed),
+ "data-value": this.#value.value,
+ "aria-pressed": this.#ariaPressed,
+ "aria-checked": this.#ariaChecked,
+ [ITEM_ATTR]: "",
+ [GROUP_ITEM_ATTR]: "",
+ disabled: getDisabledAttr(this.#isDisabled),
+ //
+ onclick: this.#onclick,
+ onkeydown: this.#onkeydown,
+ });
+}
+
+type ToolbarLinkStateProps = ReadonlyBoxedValues<{
+ id: string;
+}>;
+
+class ToolbarLinkState {
+ #id = undefined as unknown as ToolbarLinkStateProps["id"];
+ #node = boxedState(null);
+ #root = undefined as unknown as ToolbarRootState;
+
+ constructor(props: ToolbarLinkStateProps, root: ToolbarRootState) {
+ this.#root = root;
+ this.#id = props.id;
+ this.#node = useNodeById(this.#id);
+ }
+
+ #onkeydown = (e: KeyboardEvent) => {
+ this.#root.rovingFocusGroup.handleKeydown(this.#node.value, e);
+ };
+
+ #role = $derived.by(() => {
+ if (!this.#node.value) return undefined;
+ const tagName = this.#node.value.tagName.toLowerCase();
+ if (tagName !== "a") return "link" as const;
+ return undefined;
+ });
+
+ #tabIndex = $derived(this.#root.rovingFocusGroup.getTabIndex(this.#node.value).value);
+
+ props = $derived({
+ id: this.#id.value,
+ [LINK_ATTR]: "",
+ [ITEM_ATTR]: "",
+ role: this.#role,
+ tabindex: this.#tabIndex,
+ "data-orientation": getDataOrientation(this.#root.orientation.value),
+ //
+ onkeydown: this.#onkeydown,
+ });
+}
+
+type ToolbarButtonStateProps = ReadonlyBoxedValues<{
+ id: string;
+ disabled: boolean;
+}>;
+
+class ToolbarButtonState {
+ #id = undefined as unknown as ToolbarButtonStateProps["id"];
+ #root = undefined as unknown as ToolbarRootState;
+ #node = boxedState(null);
+ #disabled = undefined as unknown as ToolbarButtonStateProps["disabled"];
+
+ constructor(props: ToolbarButtonStateProps, root: ToolbarRootState) {
+ this.#id = props.id;
+ this.#root = root;
+ this.#node = useNodeById(this.#id);
+ this.#disabled = props.disabled;
+ }
+
+ #onkeydown = (e: KeyboardEvent) => {
+ this.#root.rovingFocusGroup.handleKeydown(this.#node.value, e);
+ };
+
+ #tabIndex = $derived(this.#root.rovingFocusGroup.getTabIndex(this.#node.value).value);
+
+ #role = $derived.by(() => {
+ if (!this.#node.value) return undefined;
+ const tagName = this.#node.value.tagName.toLowerCase();
+ if (tagName !== "button") return "button" as const;
+ return undefined;
+ });
+
+ props = $derived({
+ id: this.#id.value,
+ [ITEM_ATTR]: "",
+ [BUTTON_ATTR]: "",
+ role: this.#role,
+ tabindex: this.#tabIndex,
+ "data-disabled": getDataDisabled(this.#disabled.value),
+ "data-orientation": getDataOrientation(this.#root.orientation.value),
+ disabled: getDisabledAttr(this.#disabled.value),
+ //
+ onkeydown: this.#onkeydown,
+ });
+}
+
+//
+// HELPERS
+//
+
+function getToggleItemDataState(condition: boolean) {
+ return condition ? "on" : "off";
+}
+
+//
+// CONTEXT METHODS
+//
+
+const TOOLBAR_ROOT_KEY = Symbol("Toolbar.Root");
+const TOOLBAR_GROUP_KEY = Symbol("Toolbar.Group");
+
+export function setToolbarRootState(props: ToolbarRootStateProps) {
+ return setContext(TOOLBAR_ROOT_KEY, new ToolbarRootState(props));
+}
+
+export function getToolbarRootState(): ToolbarRootState {
+ verifyContextDeps(TOOLBAR_ROOT_KEY);
+ return getContext(TOOLBAR_ROOT_KEY);
+}
+
+type InitToolbarGroupProps = {
+ type: "single" | "multiple";
+ value: Box | Box;
+} & ReadonlyBoxedValues<{
+ id: string;
+ disabled: boolean;
+}>;
+
+export function setToolbarGroupState(props: InitToolbarGroupProps) {
+ const groupState = getToolbarRootState().createGroup(props);
+ return setContext(TOOLBAR_GROUP_KEY, groupState);
+}
+
+export function getToolbarGroupState(): ToolbarGroupState {
+ verifyContextDeps(TOOLBAR_GROUP_KEY);
+ return getContext(TOOLBAR_GROUP_KEY);
+}
+
+export function setToolbarGroupItemState(props: ToolbarGroupItemStateProps) {
+ return getToolbarGroupState().createItem(props);
+}
+
+export function setToolbarButtonState(props: ToolbarButtonStateProps) {
+ return getToolbarRootState().createButton(props);
+}
+
+export function setToolbarLinkState(props: ToolbarLinkStateProps) {
+ return getToolbarRootState().createLink(props);
+}
diff --git a/packages/bits-ui/src/lib/bits/toolbar/types.ts b/packages/bits-ui/src/lib/bits/toolbar/types.ts
index 7ba710a09..8f360d169 100644
--- a/packages/bits-ui/src/lib/bits/toolbar/types.ts
+++ b/packages/bits-ui/src/lib/bits/toolbar/types.ts
@@ -1,100 +1,47 @@
-import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
import type {
- CreateToolbarGroupProps as MeltToolbarGroupProps,
- CreateToolbarProps as MeltToolbarProps,
-} from "@melt-ui/svelte";
+ ToggleGroupItemProps,
+ ToggleGroupItemPropsWithoutHTML,
+ ToggleGroupRootPropsWithoutHTML,
+} from "../toggle-group/types.js";
+import type { Orientation } from "$lib/shared/index.js";
import type {
- DOMElement,
- Expand,
- HTMLDivAttributes,
- OmitValue,
- OnChangeFn,
-} from "$lib/internal/index.js";
-import type { CustomEventHandler } from "$lib/index.js";
-
-export type ToolbarPropsWithoutHTML = Expand;
-
-export type ToolbarButtonPropsWithoutHTML = DOMElement;
-
-export type ToolbarLinkPropsWithoutHTML = DOMElement;
-
-export type ToolbarGroupPropsWithoutHTML = Expand<
- OmitValue> & {
- /**
- * The value of the toolbar toggle group, which is a string or an array of strings,
- * depending on the type of the toolbar toggle group.
- *
- * You can bind to this to programmatically control the value.
- */
- value?: MeltToolbarGroupProps["defaultValue"];
-
- /**
- * A callback function called when the value changes.
- */
- onValueChange?: OnChangeFn["defaultValue"]>;
-
- /**
- * The type of the toolbar toggle group.
- *
- * If the type is `"single"`, the toolbar toggle group allows only one item to be selected
- * at a time. If the type is `"multiple"`, the toolbar toggle group allows multiple items
- * to be selected at a time.
- */
- type?: T;
- } & DOMElement
+ PrimitiveAnchorAttributes,
+ PrimitiveButtonAttributes,
+ PrimitiveDivAttributes,
+ WithAsChild,
+} from "$lib/internal/types.js";
+import type { EventCallback } from "$lib/internal/events.js";
+
+export type ToolbarRootPropsWithoutHTML = WithAsChild<{
+ orientation?: Orientation;
+ loop?: boolean;
+}>;
+
+export type ToolbarRootProps = ToolbarRootPropsWithoutHTML & PrimitiveDivAttributes;
+
+export type ToolbarGroupPropsWithoutHTML = Omit<
+ ToggleGroupRootPropsWithoutHTML,
+ "orientation" | "loop" | "rovingFocus"
>;
-export type ToolbarGroupItemPropsWithoutHTML = Expand<
- {
- /**
- * The value of the toolbar toggle group item. When the toolbar toggle group item is selected,
- * the toolbar toggle group's value will be set to this value if in `"single"` mode,
- * or this value will be pushed to the toolbar toggle group's array value if in `"multiple"` mode.
- *
- * @required
- */
- value: string;
-
- /**
- * Whether the toolbar toggle group item is disabled.
- *
- * @defaultValue false
- */
- disabled?: boolean;
- } & DOMElement
->;
-
-//
-
-export type ToolbarProps = ToolbarPropsWithoutHTML & HTMLDivAttributes;
-
-export type ToolbarButtonProps = ToolbarButtonPropsWithoutHTML & HTMLButtonAttributes;
-
-export type ToolbarLinkProps = ToolbarLinkPropsWithoutHTML & HTMLAnchorAttributes;
+export type ToolbarGroupProps = ToolbarGroupPropsWithoutHTML &
+ Omit;
-export type ToolbarGroupProps = ToolbarGroupPropsWithoutHTML &
- HTMLDivAttributes;
+export type ToolbarGroupItemPropsWithoutHTML = ToggleGroupItemPropsWithoutHTML;
-export type ToolbarGroupItemProps = ToolbarGroupItemPropsWithoutHTML & HTMLButtonAttributes;
+export type ToolbarGroupItemProps = ToggleGroupItemProps;
-/**
- * Events
- */
-type HTMLEventHandler = T & {
- currentTarget: EventTarget & E;
-};
+export type ToolbarButtonPropsWithoutHTML = WithAsChild<{
+ disabled?: boolean;
+ onkeydown?: EventCallback;
+}>;
-export type ToolbarButtonEvents = {
- click: HTMLEventHandler;
- keydown: CustomEventHandler;
-};
+export type ToolbarButtonProps = ToolbarButtonPropsWithoutHTML &
+ Omit;
-export type ToolbarLinkEvents = {
- click: HTMLEventHandler;
- keydown: CustomEventHandler;
-};
+export type ToolbarLinkPropsWithoutHTML = WithAsChild<{
+ onkeydown?: EventCallback;
+}>;
-export type ToolbarGroupItemEvents = {
- click: CustomEventHandler;
- keydown: CustomEventHandler;
-};
+export type ToolbarLinkProps = ToolbarLinkPropsWithoutHTML &
+ Omit;
diff --git a/packages/bits-ui/src/lib/internal/types.ts b/packages/bits-ui/src/lib/internal/types.ts
index 171aab929..33e45ab04 100644
--- a/packages/bits-ui/src/lib/internal/types.ts
+++ b/packages/bits-ui/src/lib/internal/types.ts
@@ -1,6 +1,7 @@
import type { Snippet } from "svelte";
import type { Action } from "svelte/action";
import type {
+ HTMLAnchorAttributes,
HTMLAttributes,
HTMLButtonAttributes,
HTMLImgAttributes,
@@ -130,6 +131,7 @@ export type PrimitiveImgAttributes = Primitive;
export type PrimitiveHeadingAttributes = Primitive;
export type PrimitiveLabelAttributes = Primitive;
export type PrimitiveSVGAttributes = Primitive>;
+export type PrimitiveAnchorAttributes = Primitive;
export type AsChildProps = {
child: Snippet<[SnippetProps & { props: Record }]>;