From 0c7ec0f009d2fe2aac19d54a0a61341e04efd2e4 Mon Sep 17 00:00:00 2001
From: Hunter Johnston <64506580+huntabyte@users.noreply.github.com>
Date: Sun, 16 Jun 2024 18:51:41 -0400
Subject: [PATCH] next: Navigation Menu (#573)
---
eslint.config.js | 13 +-
package.json | 2 +-
packages/bits-ui/package.json | 4 +-
packages/bits-ui/src/lib/bits/index.ts | 1 +
.../components/navigation-menu-content.svelte | 69 +
.../navigation-menu-indicator.svelte | 45 +
.../components/navigation-menu-item.svelte | 32 +
.../components/navigation-menu-link.svelte | 35 +
.../components/navigation-menu-list.svelte | 38 +
.../components/navigation-menu-sub.svelte | 48 +
.../components/navigation-menu-trigger.svelte | 49 +
.../navigation-menu-viewport.svelte | 40 +
.../components/navigation-menu.svelte | 54 +
.../src/lib/bits/navigation-menu/index.ts | 19 +
.../navigation-menu/navigation-menu.svelte.ts | 1125 +++++++++++++++++
.../src/lib/bits/navigation-menu/types.ts | 175 +++
.../useDismissableLayer.svelte.ts | 55 +-
.../bits-ui/src/lib/bits/utilities/index.ts | 1 +
.../src/lib/bits/utilities/mounted.svelte | 10 +
.../popper-layer/popper-layer.svelte | 14 +-
.../lib/bits/utilities/portal/portal.svelte | 24 +-
.../presence-layer/usePresence.svelte.ts | 9 +-
packages/bits-ui/src/lib/internal/types.ts | 8 +-
.../src/lib/internal/useArrowNavigation.ts | 168 +++
.../src/lib/internal/useNodeById.svelte.ts | 53 +-
packages/bits-ui/src/lib/types.ts | 1 +
pnpm-lock.yaml | 197 +--
.../content/components/navigation-menu.md | 23 +
sites/docs/package.json | 2 +-
sites/docs/src/lib/components/demos/index.ts | 1 +
.../lib/components/demos/menubar-demo.svelte | 2 +-
.../demos/navigation-menu-demo.svelte | 160 +++
sites/docs/src/lib/config/navigation.ts | 5 +
.../src/lib/content/api-reference/index.ts | 1 +
34 files changed, 2336 insertions(+), 147 deletions(-)
create mode 100644 packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-content.svelte
create mode 100644 packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-indicator.svelte
create mode 100644 packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-item.svelte
create mode 100644 packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-link.svelte
create mode 100644 packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-list.svelte
create mode 100644 packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-sub.svelte
create mode 100644 packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-trigger.svelte
create mode 100644 packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-viewport.svelte
create mode 100644 packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu.svelte
create mode 100644 packages/bits-ui/src/lib/bits/navigation-menu/index.ts
create mode 100644 packages/bits-ui/src/lib/bits/navigation-menu/navigation-menu.svelte.ts
create mode 100644 packages/bits-ui/src/lib/bits/navigation-menu/types.ts
create mode 100644 packages/bits-ui/src/lib/bits/utilities/mounted.svelte
create mode 100644 packages/bits-ui/src/lib/internal/useArrowNavigation.ts
create mode 100644 sites/docs/content/components/navigation-menu.md
create mode 100644 sites/docs/src/lib/components/demos/navigation-menu-demo.svelte
diff --git a/eslint.config.js b/eslint.config.js
index 98b16da45..de0a168fc 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -2,9 +2,8 @@ import config, { DEFAULT_IGNORES } from "@huntabyte/eslint-config";
const ignores = ["**/extended-types"];
-export default config({ svelte: true, ignores: [...DEFAULT_IGNORES, ...ignores] }).override(
- "antfu/typescript/rules",
- {
+export default config({ svelte: true, ignores: [...DEFAULT_IGNORES, ...ignores] })
+ .override("antfu/typescript/rules", {
rules: {
"ts/consistent-type-definitions": "off",
"ts/ban-types": [
@@ -16,5 +15,9 @@ export default config({ svelte: true, ignores: [...DEFAULT_IGNORES, ...ignores]
},
],
},
- }
-);
+ })
+ .override("antfu/js/rules", {
+ rules: {
+ "no-unused-expressions": "off",
+ },
+ });
diff --git a/package.json b/package.json
index 73fc05a59..0ec25dc1d 100644
--- a/package.json
+++ b/package.json
@@ -26,7 +26,7 @@
"prettier": "^3.2.5",
"prettier-plugin-svelte": "^3.2.2",
"prettier-plugin-tailwindcss": "0.5.13",
- "svelte": "5.0.0-next.143",
+ "svelte": "5.0.0-next.155",
"svelte-eslint-parser": "^0.34.1",
"wrangler": "^3.44.0"
},
diff --git a/packages/bits-ui/package.json b/packages/bits-ui/package.json
index 96313d71d..16aca21a5 100644
--- a/packages/bits-ui/package.json
+++ b/packages/bits-ui/package.json
@@ -45,7 +45,7 @@
"jsdom": "^24.0.0",
"publint": "^0.2.7",
"resize-observer-polyfill": "^1.5.1",
- "svelte": "5.0.0-next.143",
+ "svelte": "5.0.0-next.155",
"svelte-check": "^3.6.9",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
@@ -63,7 +63,7 @@
"clsx": "^2.1.0",
"esm-env": "^1.0.0",
"nanoid": "^5.0.5",
- "runed": "^0.5.0",
+ "runed": "^0.12.1",
"scule": "^1.3.0",
"style-object-to-css-string": "^1.1.3",
"style-to-object": "^1.0.6",
diff --git a/packages/bits-ui/src/lib/bits/index.ts b/packages/bits-ui/src/lib/bits/index.ts
index 17d09640a..2905b49d7 100644
--- a/packages/bits-ui/src/lib/bits/index.ts
+++ b/packages/bits-ui/src/lib/bits/index.ts
@@ -19,6 +19,7 @@ export * as DropdownMenu from "./dropdown-menu/index.js";
export * as Label from "./label/index.js";
export * as LinkPreview from "./link-preview/index.js";
export * as Menubar from "./menubar/index.js";
+export * as NavigationMenu from "./navigation-menu/index.js";
export * as Pagination from "./pagination/index.js";
export * as PinInput from "./pin-input/index.js";
export * as Popover from "./popover/index.js";
diff --git a/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-content.svelte b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-content.svelte
new file mode 100644
index 000000000..78aabada4
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-content.svelte
@@ -0,0 +1,69 @@
+
+
+
+
+ {#snippet presence({ present })}
+ contentState.onEscapeKeydown(e)}
+ >
+
+ {#snippet children({ props: dismissableProps })}
+ {#if asChild}
+
+ {@render child?.({ props: mergeProps(dismissableProps, mergedProps) })}
+ {:else}
+
+
+ {@render contentChildren?.()}
+
+ {/if}
+ {/snippet}
+
+
+ {/snippet}
+
+
diff --git a/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-indicator.svelte b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-indicator.svelte
new file mode 100644
index 000000000..2f44da5cf
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-indicator.svelte
@@ -0,0 +1,45 @@
+
+
+{#if indicatorState.menu.indicatorTrackNode}
+
+
+ {#snippet presence()}
+ {#if asChild}
+ {@render child?.({ props: mergedProps })}
+ {:else}
+
+ {@render children?.()}
+
+ {/if}
+ {/snippet}
+
+
+{/if}
diff --git a/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-item.svelte b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-item.svelte
new file mode 100644
index 000000000..7731d29ba
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-item.svelte
@@ -0,0 +1,32 @@
+
+
+{#if asChild}
+ {@render child?.({ props: mergedProps })}
+{:else}
+
+ {@render children?.()}
+
+{/if}
diff --git a/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-link.svelte b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-link.svelte
new file mode 100644
index 000000000..9aef7cf81
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-link.svelte
@@ -0,0 +1,35 @@
+
+
+{#if asChild}
+ {@render child?.({ props: mergedProps })}
+{:else}
+
+ {@render children?.()}
+
+{/if}
diff --git a/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-list.svelte b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-list.svelte
new file mode 100644
index 000000000..c002aa01d
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-list.svelte
@@ -0,0 +1,38 @@
+
+
+
+ {#if asChild}
+ {@render child?.({ props: mergedProps })}
+ {:else}
+
+ {@render children?.()}
+
+ {/if}
+
diff --git a/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-sub.svelte b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-sub.svelte
new file mode 100644
index 000000000..283374245
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-sub.svelte
@@ -0,0 +1,48 @@
+
diff --git a/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-trigger.svelte b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-trigger.svelte
new file mode 100644
index 000000000..1e195e1a6
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-trigger.svelte
@@ -0,0 +1,49 @@
+
+
+{#if asChild}
+ {@render child?.({ props: mergedProps })}
+{:else}
+
+{/if}
+
+{#if triggerState.open}
+
+
+ {#if triggerState.menu.viewportNode}
+
+ {/if}
+{/if}
diff --git a/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-viewport.svelte b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-viewport.svelte
new file mode 100644
index 000000000..68cbd9add
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu-viewport.svelte
@@ -0,0 +1,40 @@
+
+
+
+ {#snippet presence({ present })}
+ {#if asChild}
+ {@render child?.({ props: mergedProps })}
+ {:else}
+
+ {@render children?.()}
+
+ {/if}
+ {/snippet}
+
diff --git a/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu.svelte b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu.svelte
new file mode 100644
index 000000000..a468a6217
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/navigation-menu/components/navigation-menu.svelte
@@ -0,0 +1,54 @@
+
+
+{#if asChild}
+ {@render child?.({ props: mergedProps })}
+{:else}
+
+{/if}
diff --git a/packages/bits-ui/src/lib/bits/navigation-menu/index.ts b/packages/bits-ui/src/lib/bits/navigation-menu/index.ts
new file mode 100644
index 000000000..edfa842a0
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/navigation-menu/index.ts
@@ -0,0 +1,19 @@
+export { default as Root } from "./components/navigation-menu.svelte";
+export { default as Content } from "./components/navigation-menu-content.svelte";
+export { default as Indicator } from "./components/navigation-menu-indicator.svelte";
+export { default as Item } from "./components/navigation-menu-item.svelte";
+export { default as Link } from "./components/navigation-menu-link.svelte";
+export { default as List } from "./components/navigation-menu-list.svelte";
+export { default as Trigger } from "./components/navigation-menu-trigger.svelte";
+export { default as Viewport } from "./components/navigation-menu-viewport.svelte";
+
+export type {
+ NavigationMenuRootProps as RootProps,
+ NavigationMenuItemProps as ItemProps,
+ NavigationMenuListProps as ListProps,
+ NavigationMenuTriggerProps as TriggerProps,
+ NavigationMenuViewportProps as ViewportProps,
+ NavigationMenuIndicatorProps as IndicatorProps,
+ NavigationMenuContentProps as ContentProps,
+ NavigationMenuLinkProps as LinkProps,
+} from "./types.js";
diff --git a/packages/bits-ui/src/lib/bits/navigation-menu/navigation-menu.svelte.ts b/packages/bits-ui/src/lib/bits/navigation-menu/navigation-menu.svelte.ts
new file mode 100644
index 000000000..c200e72dd
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/navigation-menu/navigation-menu.svelte.ts
@@ -0,0 +1,1125 @@
+import { untrack } from "svelte";
+import { box } from "svelte-toolbelt";
+import { Previous } from "runed";
+import {
+ watch,
+ type ReadableBoxedValues,
+ type WritableBoxedValues,
+} from "$lib/internal/box.svelte.js";
+import type { Direction, Orientation } from "$lib/shared/index.js";
+import {
+ getAriaExpanded,
+ getAriaHidden,
+ getDataDisabled,
+ getDataOpenClosed,
+ getDataOrientation,
+ getDisabledAttr,
+} from "$lib/internal/attrs.js";
+import { createContext } from "$lib/internal/createContext.js";
+import { useId } from "$lib/internal/useId.svelte.js";
+import { kbd } from "$lib/internal/kbd.js";
+import { useArrowNavigation } from "$lib/internal/useArrowNavigation.js";
+import { boxAutoReset } from "$lib/internal/boxAutoReset.svelte.js";
+import { useRefById } from "$lib/internal/useNodeById.svelte.js";
+import type { ElementRef } from "$lib/internal/types.js";
+import { afterTick } from "$lib/internal/afterTick.js";
+import { getTabbableCandidates } from "../utilities/focus-scope/utils.js";
+import { noop } from "$lib/internal/callbacks.js";
+
+const [setNavigationMenuRootContext, getNavigationMenuRootContext] =
+ createContext("NavigationMenu.Root");
+
+const [setNavigationMenuMenuContext, getNavigationMenuMenuContext] = createContext<
+ NavigationMenuMenuState | NavigationMenuSubState
+>("NavigationMenu.Root or NavigationMenu.Sub");
+
+const [setNavigationMenuItemContext, getNavigationMenuItemContext] =
+ createContext("NavigationMenu.Item");
+
+const ROOT_ATTR = "data-navigation-menu-root";
+const SUB_ATTR = "data-navigation-menu-sub";
+const ITEM_ATTR = "data-navigation-menu-item";
+const INDICATOR_ATTR = "data-navigation-menu-indicator";
+const LIST_ATTR = "data-navigation-menu-list";
+const TRIGGER_ATTR = "data-navigation-menu-trigger";
+const CONTENT_ATTR = "data-navigation-menu-content";
+const LINK_ATTR = "data-navigation-menu-link";
+
+type NavigationMenuRootStateProps = ReadableBoxedValues<{
+ id: string;
+ delayDuration: number;
+ skipDelayDuration: number;
+ orientation: Orientation;
+ dir: Direction;
+}> &
+ WritableBoxedValues<{ value: string; ref: HTMLElement | null }>;
+
+class NavigationMenuRootState {
+ id: NavigationMenuRootStateProps["id"];
+ rootRef: NavigationMenuRootStateProps["ref"];
+ delayDuration: NavigationMenuRootStateProps["delayDuration"];
+ skipDelayDuration: NavigationMenuRootStateProps["skipDelayDuration"];
+ orientation: NavigationMenuRootStateProps["orientation"];
+ dir: NavigationMenuRootStateProps["dir"];
+ value: NavigationMenuRootStateProps["value"];
+ previousValue = new Previous(() => this.value.value);
+ openTimer = 0;
+ closeTimer = 0;
+ skipDelayTimer = 0;
+ isOpenDelayed = $state(false);
+
+ setValue = (v: string) => {
+ this.value.value = v;
+ };
+
+ constructor(props: NavigationMenuRootStateProps) {
+ this.id = props.id;
+ this.delayDuration = props.delayDuration;
+ this.skipDelayDuration = props.skipDelayDuration;
+ this.orientation = props.orientation;
+ this.dir = props.dir;
+ this.value = props.value;
+ this.rootRef = props.ref;
+
+ useRefById({
+ id: this.id,
+ ref: this.rootRef,
+ });
+
+ watch(this.value, (curr) => {
+ const isOpen = curr !== "";
+ const hasSkipDelayDuration = this.skipDelayDuration.value > 0;
+
+ if (isOpen) {
+ window.clearTimeout(this.skipDelayTimer);
+ if (hasSkipDelayDuration) this.isOpenDelayed = false;
+ } else {
+ window.clearTimeout(this.skipDelayTimer);
+ this.skipDelayTimer = window.setTimeout(
+ () => (this.isOpenDelayed = true),
+ this.skipDelayDuration.value
+ );
+ }
+ });
+
+ $effect(() => {
+ return () => {
+ window.clearTimeout(this.openTimer);
+ window.clearTimeout(this.closeTimer);
+ window.clearTimeout(this.skipDelayTimer);
+ };
+ });
+ }
+
+ startCloseTimer = () => {
+ window.clearTimeout(this.closeTimer);
+ this.closeTimer = window.setTimeout(() => this.setValue(""), 150);
+ };
+
+ handleOpen = (itemValue: string) => {
+ window.clearTimeout(this.closeTimer);
+ this.setValue(itemValue);
+ };
+
+ handleClose = () => {
+ this.onItemDismiss();
+ this.onContentLeave();
+ };
+
+ handleDelayedOpen = (itemValue: string) => {
+ const isOpenItem = this.value.value === itemValue;
+ if (isOpenItem) {
+ // If the item is already open (e.g. we're transitioning from the content to the trigger)
+ // then we want to clear the close timer immediately.
+ window.clearTimeout(this.closeTimer);
+ } else {
+ this.openTimer = window.setTimeout(() => {
+ window.clearTimeout(this.closeTimer);
+ this.setValue(itemValue);
+ }, this.delayDuration.value);
+ }
+ };
+
+ onTriggerEnter = (itemValue: string) => {
+ window.clearTimeout(this.openTimer);
+ if (this.isOpenDelayed) {
+ this.handleDelayedOpen(itemValue);
+ } else {
+ this.handleOpen(itemValue);
+ }
+ };
+
+ onTriggerLeave = () => {
+ window.clearTimeout(this.openTimer);
+ this.startCloseTimer();
+ };
+
+ onContentEnter = () => {
+ window.clearTimeout(this.closeTimer);
+ };
+
+ onContentLeave = () => {
+ this.startCloseTimer();
+ };
+
+ onItemSelect = (itemValue: string) => {
+ const prevValue = this.value.value;
+ this.setValue(prevValue === itemValue ? "" : itemValue);
+ };
+
+ onItemDismiss = () => {
+ this.setValue("");
+ };
+
+ props = $derived.by(() => ({
+ id: this.id.value,
+ "aria-label": "Main",
+ "data-orientation": getDataOrientation(this.orientation.value),
+ dir: this.dir.value,
+ [ROOT_ATTR]: "",
+ }));
+
+ createMenu(props: NavigationMenuMenuStateProps) {
+ return new NavigationMenuMenuState(props, this);
+ }
+}
+
+type NavigationMenuMenuStateProps = ReadableBoxedValues<{
+ rootNavigationId: string;
+ dir: Direction;
+ orientation: Orientation;
+}> &
+ WritableBoxedValues<{
+ value: string;
+ }> & {
+ isRoot: boolean;
+ onTriggerEnter: (itemValue: string) => void;
+ onTriggerLeave?: () => void;
+ onContentEnter?: () => void;
+ onContentLeave?: () => void;
+ onItemSelect: (itemValue: string) => void;
+ onItemDismiss: () => void;
+ previousValue: Previous;
+ };
+
+class NavigationMenuMenuState {
+ isRoot: NavigationMenuMenuStateProps["isRoot"] = $state(false);
+ rootNavigationId: NavigationMenuMenuStateProps["rootNavigationId"];
+ dir: NavigationMenuMenuStateProps["dir"];
+ orientation: NavigationMenuMenuStateProps["orientation"];
+ value: NavigationMenuMenuStateProps["value"];
+ previousValue: NavigationMenuMenuStateProps["previousValue"];
+ onTriggerEnter: NavigationMenuMenuStateProps["onTriggerEnter"];
+ onTriggerLeave: NavigationMenuMenuStateProps["onTriggerLeave"];
+ onContentEnter: NavigationMenuMenuStateProps["onContentEnter"];
+ onContentLeave: NavigationMenuMenuStateProps["onContentLeave"];
+ onItemSelect: NavigationMenuMenuStateProps["onItemSelect"];
+ onItemDismiss: NavigationMenuMenuStateProps["onItemDismiss"];
+ viewportNode = $state(null);
+ indicatorTrackNode = $state(null);
+ viewportContentId = box.with(() => undefined);
+ root: NavigationMenuRootState;
+ triggerRefs = new Set();
+
+ constructor(props: NavigationMenuMenuStateProps, root: NavigationMenuRootState) {
+ this.isRoot = props.isRoot;
+ this.rootNavigationId = props.rootNavigationId;
+ this.dir = props.dir;
+ this.orientation = props.orientation;
+ this.value = props.value;
+ this.onTriggerEnter = props.onTriggerEnter;
+ this.onTriggerLeave = props.onTriggerLeave;
+ this.onContentEnter = props.onContentEnter;
+ this.onContentLeave = props.onContentLeave;
+ this.onItemSelect = props.onItemSelect;
+ this.onItemDismiss = props.onItemDismiss;
+ this.root = root;
+ this.previousValue = props.previousValue;
+ }
+
+ registerTrigger = (ref: ElementRef) => {
+ this.triggerRefs.add(ref);
+ };
+
+ deRegisterTrigger = (ref: ElementRef) => {
+ this.triggerRefs.delete(ref);
+ };
+
+ getTriggerNodes = () => {
+ return Array.from(this.triggerRefs)
+ .map((ref) => ref.value)
+ .filter((node): node is HTMLElement => Boolean(node));
+ };
+
+ createList(props: NavigationMenuListStateProps) {
+ return new NavigationMenuListState(props, this);
+ }
+
+ createItem(props: NavigationMenuItemStateProps) {
+ return new NavigationMenuItemState(props, this);
+ }
+
+ createIndicator(props: NavigationMenuIndicatorStateProps) {
+ return new NavigationMenuIndicatorState(props, this);
+ }
+
+ createViewport(props: NavigationMenuViewportStateProps) {
+ return new NavigationMenuViewportState(props, this);
+ }
+
+ createSubMenu(props: NavigationMenuSubStateProps) {
+ return new NavigationMenuSubState(props, this.root);
+ }
+}
+
+type NavigationMenuSubStateProps = ReadableBoxedValues<{
+ id: string;
+ orientation: Orientation;
+}> &
+ WritableBoxedValues<{
+ value: string;
+ ref: HTMLElement | null;
+ }>;
+
+class NavigationMenuSubState {
+ id: NavigationMenuSubStateProps["id"];
+ isRoot = false;
+ rootNavigationId: NavigationMenuMenuStateProps["rootNavigationId"];
+ dir: NavigationMenuMenuStateProps["dir"];
+ orientation: NavigationMenuMenuStateProps["orientation"];
+ value: NavigationMenuMenuStateProps["value"];
+ previousValue = new Previous(() => this.value.value);
+ onTriggerLeave: NavigationMenuMenuStateProps["onTriggerLeave"];
+ onContentEnter: NavigationMenuMenuStateProps["onContentEnter"];
+ onContentLeave: NavigationMenuMenuStateProps["onContentLeave"];
+ viewportNode = $state(null);
+ indicatorTrackNode = $state(null);
+ viewportContentId = box.with(() => undefined);
+ root: NavigationMenuRootState;
+ triggerRefs = new Set();
+ ref: NavigationMenuSubStateProps["ref"];
+
+ constructor(props: NavigationMenuSubStateProps, root: NavigationMenuRootState) {
+ this.id = props.id;
+ this.rootNavigationId = root.id;
+ this.dir = root.dir;
+ this.orientation = props.orientation;
+ this.value = props.value;
+ this.root = root;
+ this.ref = props.ref;
+
+ useRefById({
+ id: this.id,
+ ref: this.ref,
+ });
+ }
+
+ onTriggerEnter = (itemValue: string) => {
+ this.value.value = itemValue;
+ };
+
+ onItemSelect = (itemValue: string) => {
+ this.value.value = itemValue;
+ };
+
+ onItemDismiss = () => {
+ this.value.value = "";
+ };
+
+ registerTrigger = (ref: ElementRef) => {
+ this.triggerRefs.add(ref);
+ };
+
+ deRegisterTrigger = (ref: ElementRef) => {
+ this.triggerRefs.delete(ref);
+ };
+
+ getTriggerNodes = () => {
+ return Array.from(this.triggerRefs)
+ .map((ref) => ref.value)
+ .filter((node): node is HTMLElement => Boolean(node));
+ };
+
+ createList(props: NavigationMenuListStateProps) {
+ return new NavigationMenuListState(props, this);
+ }
+
+ createItem(props: NavigationMenuItemStateProps) {
+ return new NavigationMenuItemState(props, this);
+ }
+
+ createIndicator(props: NavigationMenuIndicatorStateProps) {
+ return new NavigationMenuIndicatorState(props, this);
+ }
+
+ createViewport(props: NavigationMenuViewportStateProps) {
+ return new NavigationMenuViewportState(props, this);
+ }
+
+ props = $derived.by(() => ({
+ id: this.id.value,
+ "data-orientation": getDataOrientation(this.orientation.value),
+ [SUB_ATTR]: "",
+ }));
+}
+
+type NavigationMenuListStateProps = ReadableBoxedValues<{
+ id: string;
+}> &
+ WritableBoxedValues<{
+ ref: HTMLElement | null;
+ indicatorTrackRef: HTMLElement | null;
+ }>;
+
+class NavigationMenuListState {
+ id: NavigationMenuListStateProps["id"];
+ listRef: NavigationMenuListStateProps["ref"];
+ indicatorTrackRef: NavigationMenuListStateProps["indicatorTrackRef"];
+ indicatorTrackId = box(useId());
+
+ constructor(
+ props: NavigationMenuListStateProps,
+ private menu: NavigationMenuMenuState | NavigationMenuSubState
+ ) {
+ this.id = props.id;
+ this.listRef = props.ref;
+ this.indicatorTrackRef = props.indicatorTrackRef;
+
+ useRefById({
+ id: this.id,
+ ref: this.listRef,
+ });
+
+ useRefById({
+ id: this.indicatorTrackId,
+ ref: this.indicatorTrackRef,
+ onRefChange: (node) => {
+ this.menu.indicatorTrackNode = node;
+ },
+ });
+ }
+
+ indicatorTrackProps = $derived.by(
+ () =>
+ ({
+ id: this.indicatorTrackId.value,
+ style: {
+ position: "relative",
+ },
+ }) as const
+ );
+
+ props = $derived.by(
+ () =>
+ ({
+ "data-orientation": getDataOrientation(this.menu.orientation.value),
+ [LIST_ATTR]: "",
+ }) as const
+ );
+}
+
+type NavigationMenuItemStateProps = ReadableBoxedValues<{
+ id: string;
+ value: string;
+}>;
+
+class NavigationMenuItemState {
+ id: NavigationMenuItemStateProps["id"];
+ value: NavigationMenuItemStateProps["value"];
+ contentNode = $state(null);
+ triggerNode = $state(null);
+ focusProxyRef = box(null);
+ focusProxyNode = $state(null);
+ focusProxyId = box(useId());
+ restoreContentTabOrder = noop;
+ wasEscapeClose = $state(false);
+ menu: NavigationMenuMenuState | NavigationMenuSubState;
+
+ constructor(
+ props: NavigationMenuItemStateProps,
+ menu: NavigationMenuMenuState | NavigationMenuSubState
+ ) {
+ this.id = props.id;
+ this.value = props.value;
+ this.menu = menu;
+ }
+
+ #handleContentEntry = (side: "start" | "end" = "start") => {
+ if (!this.contentNode) return;
+ this.restoreContentTabOrder();
+ const candidates = getTabbableCandidates(this.contentNode);
+ if (candidates.length) {
+ if (side === "start") {
+ candidates[0]?.focus();
+ } else {
+ candidates[candidates.length - 1]?.focus();
+ }
+ }
+ };
+
+ #handleContentExit = () => {
+ if (!this.contentNode) return;
+ const candidates = getTabbableCandidates(this.contentNode);
+ if (candidates.length) {
+ this.restoreContentTabOrder = removeFromTabOrder(candidates);
+ }
+ };
+
+ onEntryKeydown = this.#handleContentEntry;
+ onFocusProxyEnter = this.#handleContentEntry;
+ onContentFocusOutside = this.#handleContentExit;
+ onRootContentClose = this.#handleContentExit;
+
+ props = $derived.by(
+ () =>
+ ({
+ id: this.id.value,
+ [ITEM_ATTR]: "",
+ }) as const
+ );
+
+ createTrigger(props: NavigationMenuTriggerStateProps) {
+ return new NavigationMenuTriggerState(props, this);
+ }
+
+ createContent(props: NavigationMenuContentStateProps) {
+ return new NavigationMenuContentState(props, this);
+ }
+
+ createLink(props: NavigationMenuLinkStateProps) {
+ return new NavigationMenuLinkState(props);
+ }
+}
+
+type NavigationMenuTriggerStateProps = ReadableBoxedValues<{
+ id: string;
+ disabled: boolean;
+ focusProxyMounted: boolean;
+}> &
+ WritableBoxedValues<{
+ ref: HTMLElement | null;
+ }>;
+
+class NavigationMenuTriggerState {
+ id: NavigationMenuTriggerStateProps["id"];
+ focusProxyMounted: NavigationMenuTriggerStateProps["focusProxyMounted"];
+ menu: NavigationMenuMenuState | NavigationMenuSubState;
+ item: NavigationMenuItemState;
+ disabled: NavigationMenuTriggerStateProps["disabled"];
+ hasPointerMoveOpened = boxAutoReset(false, 150);
+ wasClickClose = $state(false);
+ open = $derived.by(() => this.item.value.value === this.menu.value.value);
+ triggerRef: NavigationMenuTriggerStateProps["ref"];
+
+ constructor(props: NavigationMenuTriggerStateProps, item: NavigationMenuItemState) {
+ this.id = props.id;
+ this.triggerRef = props.ref;
+ this.item = item;
+ this.menu = item.menu;
+ this.disabled = props.disabled;
+ this.focusProxyMounted = props.focusProxyMounted;
+
+ useRefById({
+ id: this.id,
+ ref: this.triggerRef,
+ onRefChange: (node) => {
+ this.item.triggerNode = node;
+ },
+ });
+
+ useRefById({
+ id: this.item.focusProxyId,
+ ref: this.item.focusProxyRef,
+ onRefChange: (node) => {
+ this.item.focusProxyNode = node;
+ },
+ condition: () => this.focusProxyMounted.value,
+ });
+
+ $effect(() => {
+ this.menu.registerTrigger(this.triggerRef);
+ return () => {
+ this.menu.deRegisterTrigger(this.triggerRef);
+ };
+ });
+ }
+
+ #onpointerenter = () => {
+ this.wasClickClose = false;
+ this.item.wasEscapeClose = false;
+ };
+
+ #onpointermove = (e: PointerEvent) => {
+ if (e.pointerType !== "mouse") return;
+ if (
+ this.disabled.value ||
+ this.wasClickClose ||
+ this.item.wasEscapeClose ||
+ this.hasPointerMoveOpened.value
+ )
+ return;
+ this.menu.onTriggerEnter(this.item.value.value);
+ this.hasPointerMoveOpened.value = true;
+ };
+
+ #onpointerleave = (e: PointerEvent) => {
+ if (e.pointerType !== "mouse" || this.disabled.value) return;
+ this.menu.onTriggerLeave?.();
+ this.hasPointerMoveOpened.value = false;
+ };
+
+ #onclick = (e: PointerEvent) => {
+ // if opened via pointer move, we prevent clicke event
+ if (this.hasPointerMoveOpened.value) return;
+ if (this.open) {
+ this.menu.onItemSelect("");
+ } else {
+ this.menu.onItemSelect(this.item.value.value);
+ }
+ this.wasClickClose = this.open;
+ };
+
+ #onkeydown = (e: KeyboardEvent) => {
+ const verticalEntryKey = this.menu.dir.value === "rtl" ? kbd.ARROW_LEFT : kbd.ARROW_RIGHT;
+ const entryKey = {
+ horizontal: kbd.ARROW_DOWN,
+ vertical: verticalEntryKey,
+ }[this.menu.orientation.value];
+ if (this.open && e.key === entryKey) {
+ this.item.onEntryKeydown();
+ e.preventDefault();
+ }
+ };
+
+ props = $derived.by(
+ () =>
+ ({
+ id: this.id.value,
+ disabled: getDisabledAttr(this.disabled.value),
+ "data-disabled": getDataDisabled(this.disabled.value),
+ "data-state": getDataOpenClosed(this.open),
+ "aria-expanded": getAriaExpanded(this.open),
+ "aria-controls": this.item.contentNode ? this.item.contentNode.id : undefined,
+ "data-value": this.item.value.value,
+ onpointerenter: this.#onpointerenter,
+ onpointermove: this.#onpointermove,
+ onpointerleave: this.#onpointerleave,
+ onclick: this.#onclick,
+ onkeydown: this.#onkeydown,
+ [TRIGGER_ATTR]: "",
+ }) as const
+ );
+
+ visuallyHiddenProps = $derived.by(
+ () =>
+ ({
+ id: this.item.focusProxyId.value,
+ "aria-hidden": "true",
+ tabIndex: 0,
+ onfocus: (e: FocusEvent) => {
+ const prevFocusedElement = e.relatedTarget as HTMLElement | null;
+ const wasTriggerFocused = prevFocusedElement === this.item.triggerNode;
+ const wasFocusFromContent = this.item.contentNode?.contains(prevFocusedElement);
+
+ if (wasTriggerFocused || !wasFocusFromContent) {
+ e.preventDefault();
+ this.item.onFocusProxyEnter(wasTriggerFocused ? "start" : "end");
+ }
+ },
+ }) as const
+ );
+}
+
+type NavigationMenuLinkStateProps = ReadableBoxedValues<{
+ id: string;
+ active: boolean;
+ onSelect: (e: Event) => void;
+}>;
+
+class NavigationMenuLinkState {
+ id: NavigationMenuItemState["id"];
+ active: NavigationMenuLinkStateProps["active"];
+ onSelect: NavigationMenuLinkStateProps["onSelect"];
+
+ constructor(props: NavigationMenuLinkStateProps) {
+ this.id = props.id;
+ this.active = props.active;
+ this.onSelect = props.onSelect;
+ }
+
+ #onclick = (e: MouseEvent) => {
+ const linkSelectEvent = new CustomEvent("navigationMenu.linkSelect", {
+ bubbles: true,
+ cancelable: true,
+ });
+
+ this.onSelect.value(linkSelectEvent);
+
+ if (!linkSelectEvent.defaultPrevented && !e.metaKey) {
+ }
+ };
+
+ props = $derived.by(
+ () =>
+ ({
+ id: this.id.value,
+ "data-active": this.active.value ? "" : undefined,
+ "aria-current": this.active.value ? "page" : undefined,
+ onclick: this.#onclick,
+ onfocus: (e: FocusEvent) => {},
+ }) as const
+ );
+}
+
+type NavigationMenuIndicatorStateProps = ReadableBoxedValues<{
+ id: string;
+}> &
+ WritableBoxedValues<{
+ ref: HTMLElement | null;
+ }>;
+
+class NavigationMenuIndicatorState {
+ id: NavigationMenuIndicatorStateProps["id"];
+ menu: NavigationMenuMenuState | NavigationMenuSubState;
+ activeTrigger = $state(null);
+ position = $state<{ size: number; offset: number } | null>(null);
+ isHorizontal = $derived.by(() => this.menu.orientation.value === "horizontal");
+ isVisible = $derived.by(() => Boolean(this.menu.value.value));
+ indicatorRef: NavigationMenuIndicatorStateProps["ref"];
+
+ constructor(
+ props: NavigationMenuIndicatorStateProps,
+ menu: NavigationMenuMenuState | NavigationMenuSubState
+ ) {
+ this.id = props.id;
+ this.indicatorRef = props.ref;
+ this.menu = menu;
+
+ useRefById({
+ id: this.id,
+ ref: this.indicatorRef,
+ onRefChange: (node) => {
+ this.menu.viewportNode = node;
+ },
+ });
+
+ $effect(() => {
+ const triggerNodes = this.menu.getTriggerNodes();
+ const triggerNode = triggerNodes.find(
+ (node) => node.dataset.value === this.menu.value.value
+ );
+ if (triggerNode) {
+ untrack(() => {
+ this.activeTrigger = triggerNode;
+ });
+ }
+ });
+
+ useResizeObserver(() => this.activeTrigger, this.handlePositionChange);
+ useResizeObserver(() => this.menu.indicatorTrackNode, this.handlePositionChange);
+ }
+
+ handlePositionChange = () => {
+ if (!this.activeTrigger) return;
+ this.position = {
+ size: this.isHorizontal
+ ? this.activeTrigger.offsetWidth
+ : this.activeTrigger.offsetHeight,
+ offset: this.isHorizontal
+ ? this.activeTrigger.offsetLeft
+ : this.activeTrigger.offsetTop,
+ };
+ };
+
+ props = $derived.by(
+ () =>
+ ({
+ "aria-hidden": getAriaHidden(true),
+ "data-state": this.isVisible ? "visible" : "hidden",
+ "data-orientation": getDataOrientation(this.menu.orientation.value),
+ style: {
+ position: "absolute",
+ ...(this.isHorizontal
+ ? {
+ left: 0,
+ width: this.position ? `${this.position.size}px` : undefined,
+ transform: this.position
+ ? `translateX(${this.position.offset}px)`
+ : undefined,
+ }
+ : {
+ top: 0,
+ height: this.position ? `${this.position.size}px` : undefined,
+ transform: this.position
+ ? `translateY(${this.position.offset}px)`
+ : undefined,
+ }),
+ },
+ }) as const
+ );
+}
+
+type NavigationMenuContentStateProps = ReadableBoxedValues<{
+ id: string;
+ forceMount: boolean;
+ isMounted: boolean;
+}> &
+ WritableBoxedValues<{
+ ref: HTMLElement | null;
+ }>;
+
+type MotionAttribute = "to-start" | "to-end" | "from-start" | "from-end";
+
+class NavigationMenuContentState {
+ id: NavigationMenuContentStateProps["id"];
+ forceMount: NavigationMenuContentStateProps["forceMount"];
+ isMounted: NavigationMenuContentStateProps["isMounted"];
+ contentRef: NavigationMenuContentStateProps["ref"];
+ menu: NavigationMenuMenuState | NavigationMenuSubState;
+ item: NavigationMenuItemState;
+ prevMotionAttribute = $state(null);
+ motionAttribute = $state(null);
+ open = $derived.by(() => this.menu.value.value === this.item.value.value);
+ isPresent = $derived.by(() => this.open || this.forceMount.value);
+
+ constructor(props: NavigationMenuContentStateProps, item: NavigationMenuItemState) {
+ this.id = props.id;
+ this.forceMount = props.forceMount;
+ this.isMounted = props.isMounted;
+ this.item = item;
+ this.menu = item.menu;
+ this.contentRef = props.ref;
+
+ useRefById({
+ id: this.id,
+ ref: this.contentRef,
+ onRefChange: (node) => {
+ this.item.contentNode = node;
+ },
+ condition: () => this.isMounted.value,
+ });
+
+ $effect(() => {
+ const items = this.menu.getTriggerNodes();
+ const prev = this.menu.previousValue.current;
+ const values = items
+ .map((item) => item.dataset.value)
+ .filter((v): v is string => Boolean(v));
+ if (this.menu.dir.value === "rtl") values.reverse();
+ const index = values.indexOf(this.menu.value.value);
+ const prevIndex = values.indexOf(prev ?? "");
+ const isSelected = this.item.value.value === this.menu.value.value;
+ const wasSelected = prevIndex === values.indexOf(this.item.value.value);
+
+ // We only want to update selected and the last selected content
+ // this avoids animations being interrupted outside of that range
+ if (!isSelected && !wasSelected) {
+ this.motionAttribute = this.prevMotionAttribute;
+ }
+
+ const attribute = (() => {
+ // Don't provide a direction on the initial open
+ if (index !== prevIndex) {
+ // If we're moving to this item from another
+ if (isSelected && prevIndex !== -1) {
+ return index > prevIndex ? "from-end" : "from-start";
+ }
+ // If we're leaving this item for another
+ if (wasSelected && index !== -1) {
+ return index > prevIndex ? "to-start" : "to-end";
+ }
+ }
+ // Otherwise we're entering from closed or leaving the list
+ // entirely and should not animate in any direction
+ return null;
+ })();
+
+ this.prevMotionAttribute = attribute;
+ this.motionAttribute = attribute;
+ });
+ }
+
+ onFocusOutside = (e: Event) => {
+ this.item.onContentFocusOutside();
+ const target = e.target as HTMLElement;
+ // only dismiss content when focus moves outside the menu
+
+ if (this.menu.root.rootRef.value?.contains(target)) {
+ e.preventDefault();
+ } else {
+ this.menu.root.handleClose();
+ }
+ };
+
+ onInteractOutside = (e: Event) => {
+ if (e.defaultPrevented) return;
+ const target = e.target as HTMLElement;
+ const isTrigger = this.menu.getTriggerNodes().some((node) => node.contains(target));
+
+ const isRootViewport = this.menu.isRoot && this.menu.viewportNode?.contains(target);
+
+ if (isTrigger || isRootViewport || !this.menu.isRoot) {
+ e.preventDefault();
+ }
+ };
+
+ onEscapeKeydown = (e: KeyboardEvent) => {
+ this.menu.root.handleClose();
+ const target = e.target as HTMLElement;
+
+ if (this.contentRef.value?.contains(target)) {
+ this.item.triggerNode?.focus();
+ }
+ this.item.wasEscapeClose = true;
+ };
+
+ #onkeydown = (e: KeyboardEvent) => {
+ const isMetaKey = e.altKey || e.ctrlKey || e.metaKey;
+ const isTabKey = e.key === kbd.TAB && !isMetaKey;
+
+ const candidates = getTabbableCandidates(e.currentTarget as HTMLElement);
+ if (isTabKey) {
+ const focusedElement = document.activeElement;
+ const index = candidates.findIndex((candidate) => candidate === focusedElement);
+ const isMovingBackwards = e.shiftKey;
+ const nextCandidates = isMovingBackwards
+ ? candidates.slice(0, index).reverse()
+ : candidates.slice(index + 1, candidates.length);
+
+ if (focusFirst(nextCandidates)) {
+ // prevent browser tab keydown because we've handled focus
+ e.preventDefault();
+ return;
+ } else {
+ // If we can't focus that means we're at the edges
+ // so focus the proxy and let browser handle
+ // tab/shift+tab keypress on the proxy instead
+ this.item.focusProxyNode?.focus();
+ return;
+ }
+ }
+ const newSelectedElement = useArrowNavigation(
+ e,
+ document.activeElement as HTMLElement,
+ undefined,
+ {
+ itemsArray: candidates,
+ attributeName: `[${LINK_ATTR}]`,
+ loop: false,
+ enableIgnoredElement: true,
+ }
+ );
+
+ newSelectedElement?.focus();
+ };
+
+ props = $derived.by(
+ () =>
+ ({
+ id: this.id.value,
+ "aria-labelledby": this.item.triggerNode?.id ?? undefined,
+ "data-motion": this.motionAttribute,
+ "data-state": getDataOpenClosed(this.menu.value.value === this.item.value.value),
+ "data-orientation": getDataOrientation(this.menu.orientation.value),
+ [CONTENT_ATTR]: "",
+ style: {
+ pointerEvents: !this.open && this.menu.isRoot ? "none" : undefined,
+ },
+ onkeydown: this.#onkeydown,
+ }) as const
+ );
+}
+
+type NavigationMenuViewportStateProps = ReadableBoxedValues<{
+ id: string;
+}> &
+ WritableBoxedValues<{
+ ref: HTMLElement | null;
+ }>;
+
+class NavigationMenuViewportState {
+ id: NavigationMenuViewportStateProps["id"];
+ menu: NavigationMenuMenuState | NavigationMenuSubState;
+ size = $state<{ width: number; height: number } | null>(null);
+ open = $derived.by(() => this.menu.value.value !== "");
+ activeContentValue = $derived.by(() => this.menu.value.value);
+ viewportRef: NavigationMenuViewportStateProps["ref"];
+ contentNode = $state();
+
+ constructor(
+ props: NavigationMenuViewportStateProps,
+ menu: NavigationMenuMenuState | NavigationMenuSubState
+ ) {
+ this.id = props.id;
+ this.menu = menu;
+ this.viewportRef = props.ref;
+
+ useRefById({
+ id: this.id,
+ ref: this.viewportRef,
+ onRefChange: (node) => {
+ this.menu.viewportNode = node;
+ },
+ condition: () => this.open,
+ });
+
+ $effect(() => {
+ this.open;
+ this.activeContentValue;
+ const currentNode = untrack(() => this.viewportRef.value);
+ if (!currentNode) return;
+ afterTick(() => {
+ const contentNode = currentNode.querySelector("[data-state=open]")
+ ?.children?.[0] as HTMLElement;
+ this.contentNode = contentNode;
+ });
+ });
+
+ useResizeObserver(
+ () => this.contentNode,
+ () => {
+ if (this.contentNode) {
+ this.size = {
+ width: this.contentNode.offsetWidth,
+ height: this.contentNode.offsetHeight,
+ };
+ }
+ }
+ );
+ }
+
+ #onpointerenter = () => {
+ this.menu.onContentEnter?.();
+ };
+
+ #onpointerleave = (e: PointerEvent) => {
+ if (e.pointerType !== "mouse") return;
+ this.menu.onContentLeave?.();
+ };
+
+ props = $derived.by(
+ () =>
+ ({
+ id: this.id.value,
+ "data-state": getDataOpenClosed(this.open),
+ "data-orientation": getDataOrientation(this.menu.orientation.value),
+ style: {
+ pointerEvents: !this.open && this.menu.isRoot ? "none" : undefined,
+ "--bits-navigation-menu-viewport-width": this.size
+ ? `${this.size.width}px`
+ : undefined,
+ "--bits-navigation-menu-viewport-height": this.size
+ ? `${this.size.height}px`
+ : undefined,
+ },
+ onpointerenter: this.#onpointerenter,
+ onpointerleave: this.#onpointerleave,
+ }) as const
+ );
+}
+
+export function useNavigationMenuRoot(props: NavigationMenuRootStateProps) {
+ const rootState = new NavigationMenuRootState(props);
+ const menuState = rootState.createMenu({
+ rootNavigationId: rootState.id,
+ dir: rootState.dir,
+ orientation: rootState.orientation,
+ value: rootState.value,
+ isRoot: true,
+ onTriggerEnter: rootState.onTriggerEnter,
+ onItemSelect: rootState.onItemSelect,
+ onItemDismiss: rootState.onItemDismiss,
+ onContentEnter: rootState.onContentEnter,
+ onContentLeave: rootState.onContentLeave,
+ onTriggerLeave: rootState.onTriggerLeave,
+ previousValue: rootState.previousValue,
+ });
+
+ setNavigationMenuMenuContext(menuState);
+ return setNavigationMenuRootContext(rootState);
+}
+
+export function useNavigationMenuSub(props: NavigationMenuSubStateProps) {
+ const parentMenu = getNavigationMenuMenuContext();
+ if (parentMenu instanceof NavigationMenuMenuState) {
+ return setNavigationMenuMenuContext(
+ parentMenu.createSubMenu(props)
+ ) as NavigationMenuSubState;
+ }
+ throw new Error("useNavigationMenuSub must be used within a NavigationMenuMenu");
+}
+
+export function useNavigationMenuList(props: NavigationMenuListStateProps) {
+ const menuState = getNavigationMenuMenuContext();
+ return menuState.createList(props);
+}
+
+export function useNavigationMenuItem(props: NavigationMenuItemStateProps) {
+ const menuState = getNavigationMenuMenuContext();
+ return setNavigationMenuItemContext(menuState.createItem(props));
+}
+
+export function useNavigationMenuTrigger(props: NavigationMenuTriggerStateProps) {
+ return getNavigationMenuItemContext().createTrigger(props);
+}
+
+export function useNavigationMenuContent(props: NavigationMenuContentStateProps) {
+ return getNavigationMenuItemContext().createContent(props);
+}
+
+export function useNavigationMenuViewport(props: NavigationMenuViewportStateProps) {
+ return getNavigationMenuMenuContext().createViewport(props);
+}
+
+export function useNavigationMenuIndicator(props: NavigationMenuIndicatorStateProps) {
+ return getNavigationMenuMenuContext().createIndicator(props);
+}
+
+export function useNavigationMenuLink(props: NavigationMenuLinkStateProps) {
+ return getNavigationMenuItemContext().createLink(props);
+}
+
+/// Utils
+
+function focusFirst(candidates: HTMLElement[]) {
+ const previouslyFocusedElement = document.activeElement;
+ return candidates.some((candidate) => {
+ // if focus is already where we want to go, we don't want to keep going through the candidates
+ if (candidate === previouslyFocusedElement) return true;
+ candidate.focus();
+ return document.activeElement !== previouslyFocusedElement;
+ });
+}
+
+function removeFromTabOrder(candidates: HTMLElement[]) {
+ candidates.forEach((candidate) => {
+ candidate.dataset.tabindex = candidate.getAttribute("tabindex") || "";
+ candidate.setAttribute("tabindex", "-1");
+ });
+ return () => {
+ candidates.forEach((candidate) => {
+ const prevTabIndex = candidate.dataset.tabindex as string;
+ candidate.setAttribute("tabindex", prevTabIndex);
+ });
+ };
+}
+
+function useResizeObserver(element: () => HTMLElement | null | undefined, onResize: () => void) {
+ $effect(() => {
+ let rAF = 0;
+ const node = element();
+ if (node) {
+ const resizeObserver = new ResizeObserver(() => {
+ cancelAnimationFrame(rAF);
+ rAF = window.requestAnimationFrame(onResize);
+ });
+
+ resizeObserver.observe(node);
+
+ return () => {
+ window.cancelAnimationFrame(rAF);
+ resizeObserver.unobserve(node);
+ };
+ }
+ });
+}
diff --git a/packages/bits-ui/src/lib/bits/navigation-menu/types.ts b/packages/bits-ui/src/lib/bits/navigation-menu/types.ts
new file mode 100644
index 000000000..e1ff842de
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/navigation-menu/types.ts
@@ -0,0 +1,175 @@
+import type {
+ OnChangeFn,
+ PrimitiveAnchorAttributes,
+ PrimitiveButtonAttributes,
+ PrimitiveDivAttributes,
+ PrimitiveElementAttributes,
+ PrimitiveLiAttributes,
+ PrimitiveUListAttributes,
+ WithAsChild,
+ Without,
+} from "$lib/internal/types.js";
+import type { Direction, Orientation } from "$lib/shared/index.js";
+import type { InteractOutsideEvent } from "@melt-ui/svelte";
+
+export type NavigationMenuRootPropsWithoutHTML = WithAsChild<{
+ /**
+ * The value of the currently open menu item.
+ *
+ * @bindable
+ */
+ value?: string;
+
+ /**
+ * The callback to call when a menu item is selected.
+ */
+ onValueChange?: OnChangeFn;
+
+ /**
+ * The duration from when the mouse enters a trigger until the content opens.
+ */
+ delayDuration?: number;
+
+ /**
+ * How much time a user has to enter another trigger without incurring a delay again.
+ */
+ skipDelayDuration?: number;
+
+ /**
+ * The reading direction of the content.
+ */
+ dir?: Direction;
+
+ /**
+ * The orientation of the menu.
+ */
+ orientation?: Orientation;
+}>;
+
+export type NavigationMenuRootProps = NavigationMenuRootPropsWithoutHTML &
+ Without;
+
+export type NavigationMenuSubPropsWithoutHTML = WithAsChild<{
+ /**
+ * The value of the currently open menu item within the menu.
+ *
+ * @bindable
+ */
+ value?: string;
+
+ /**
+ * A callback fired when the active menu item changes.
+ */
+ onValueChange?: OnChangeFn;
+
+ /**
+ * The orientation of the menu.
+ */
+ orientation?: Orientation;
+}>;
+
+export type NavigationMenuSubProps = NavigationMenuSubPropsWithoutHTML &
+ Without;
+
+export type NavigationMenuListPropsWithoutHTML = WithAsChild<{}>;
+
+export type NavigationMenuListProps = NavigationMenuListPropsWithoutHTML &
+ Without;
+
+export type NavigationMenuItemPropsWithoutHTML = WithAsChild<{
+ /**
+ * The value of the menu item.
+ */
+ value?: string;
+}>;
+
+export type NavigationMenuItemProps = NavigationMenuItemPropsWithoutHTML &
+ Without;
+
+export type NavigationMenuTriggerPropsWithoutHTML = WithAsChild<{
+ /**
+ * Whether the trigger is disabled.
+ * @defaultValue false
+ */
+ disabled?: boolean;
+}>;
+
+export type NavigationMenuTriggerProps = NavigationMenuTriggerPropsWithoutHTML &
+ Without;
+
+export type NavigationMenuContentPropsWithoutHTML = WithAsChild<{
+ /**
+ * Callback fired when an interaction occurs outside the content.
+ * Default behavior can be prevented with `event.preventDefault()`
+ *
+ */
+ onInteractOutside?: (event: InteractOutsideEvent) => void;
+
+ /**
+ * Callback fired when a focus event occurs outside the content.
+ * Default behavior can be prevented with `event.preventDefault()`
+ */
+ onFocusOutside?: (event: FocusEvent) => void;
+
+ /**
+ * Callback fires when an escape keydown event occurs.
+ * Default behavior can be prevented with `event.preventDefault()`
+ */
+ onEscapeKeydown?: (event: KeyboardEvent) => void;
+
+ /**
+ * Whether to forcefully mount the content, regardless of the open state.
+ * This is useful when wanting to use more custom transition and animation
+ * libraries.
+ *
+ * @defaultValue false
+ */
+ forceMount?: boolean;
+}>;
+
+export type NavigationMenuContentProps = NavigationMenuContentPropsWithoutHTML &
+ Without;
+
+export type NavigationMenuLinkPropsWithoutHTML = WithAsChild<{
+ /**
+ * Whether the link is the current active page
+ */
+ active?: boolean;
+
+ /**
+ * A callback fired when the link is clicked.
+ * Default behavior can be prevented with `event.preventDefault()`
+ */
+ onSelect?: (e: Event) => void;
+}>;
+
+export type NavigationMenuLinkProps = NavigationMenuLinkPropsWithoutHTML &
+ Without;
+
+export type NavigationMenuIndicatorPropsWithoutHTML = WithAsChild<{
+ /**
+ * Whether to forcefully mount the content, regardless of the open state.
+ * This is useful when wanting to use more custom transition and animation
+ * libraries.
+ *
+ * @defaultValue false
+ */
+ forceMount?: boolean;
+}>;
+
+export type NavigationMenuIndicatorProps = NavigationMenuIndicatorPropsWithoutHTML &
+ Without;
+
+export type NavigationMenuViewportPropsWithoutHTML = WithAsChild<{
+ /**
+ * Whether to forcefully mount the content, regardless of the open state.
+ * This is useful when wanting to use more custom transition and animation
+ * libraries.
+ *
+ * @defaultValue false
+ */
+ forceMount?: boolean;
+}>;
+
+export type NavigationMenuViewportProps = NavigationMenuViewportPropsWithoutHTML &
+ Without;
diff --git a/packages/bits-ui/src/lib/bits/utilities/dismissable-layer/useDismissableLayer.svelte.ts b/packages/bits-ui/src/lib/bits/utilities/dismissable-layer/useDismissableLayer.svelte.ts
index 6c95a2692..3dec0630d 100644
--- a/packages/bits-ui/src/lib/bits/utilities/dismissable-layer/useDismissableLayer.svelte.ts
+++ b/packages/bits-ui/src/lib/bits/utilities/dismissable-layer/useDismissableLayer.svelte.ts
@@ -1,5 +1,5 @@
import { untrack } from "svelte";
-import type { ReadableBox } from "svelte-toolbelt";
+import { box, type ReadableBox, type WritableBox } from "svelte-toolbelt";
import type {
DismissableLayerImplProps,
InteractOutsideBehaviorType,
@@ -19,9 +19,8 @@ import {
isElement,
isOrContainsTarget,
noop,
- useNodeById,
+ useRefById,
} from "$lib/internal/index.js";
-import { eventLogs } from "$lib/bits/index.js";
const layers = new Map>();
@@ -54,40 +53,57 @@ export class DismissableLayerState {
};
#isPointerDownOutside = false;
#isResponsibleLayer = false;
- node: Box;
+ node: WritableBox = box(null);
#documentObj = undefined as unknown as Document;
#enabled: ReadableBox;
#isFocusInsideDOMTree = $state(false);
#onFocusOutside: DismissableLayerStateProps["onFocusOutside"];
+ currNode = $state(null);
constructor(props: DismissableLayerStateProps) {
- this.node = useNodeById(props.id);
+ this.#enabled = props.enabled;
+
+ useRefById({
+ id: props.id,
+ ref: this.node,
+ condition: () => this.#enabled.value,
+ onRefChange: (node) => {
+ this.currNode = node;
+ },
+ });
+
this.#behaviorType = props.interactOutsideBehavior;
this.#interactOutsideStartProp = props.onInteractOutsideStart;
this.#interactOutsideProp = props.onInteractOutside;
- this.#enabled = props.enabled;
this.#onFocusOutside = props.onFocusOutside;
$effect(() => {
- this.#documentObj = getOwnerDocument(this.node.value);
+ this.#documentObj = getOwnerDocument(this.currNode);
});
let unsubEvents = noop;
+ const cleanup = () => {
+ this.#resetState();
+ layers.delete(this);
+ this.#onInteractOutsideStart.destroy();
+ this.#onInteractOutside.destroy();
+ unsubEvents();
+ };
+
$effect(() => {
if (this.#enabled.value) {
layers.set(
this,
untrack(() => this.#behaviorType)
);
- unsubEvents = this.#addEventListeners();
+ untrack(() => {
+ unsubEvents();
+ unsubEvents = this.#addEventListeners();
+ });
}
return () => {
- this.#resetState();
- layers.delete(this);
- this.#onInteractOutsideStart.destroy();
- this.#onInteractOutside.destroy();
- unsubEvents();
+ cleanup();
};
});
@@ -106,9 +122,10 @@ export class DismissableLayerState {
}
#handleFocus = (event: FocusEvent) => {
- if (!this.node.value) return;
+ if (event.defaultPrevented) return;
+ if (!this.currNode) return;
afterTick(() => {
- if (!this.node.value || this.#isTargetWithinLayer(event.target as HTMLElement)) return;
+ if (!this.currNode || this.#isTargetWithinLayer(event.target as HTMLElement)) return;
if (event.target && !this.#isFocusInsideDOMTree) {
this.#onFocusOutside.value?.(event);
@@ -174,11 +191,11 @@ export class DismissableLayerState {
}
#onInteractOutsideStart = debounce((e: InteractOutsideEvent) => {
- if (!this.node.value) return;
+ if (!this.currNode) return;
if (
!this.#isResponsibleLayer ||
this.#isAnyEventIntercepted() ||
- !isValidEvent(e, this.node.value)
+ !isValidEvent(e, this.currNode)
)
return;
this.#interactOutsideStartProp.value(e);
@@ -187,13 +204,13 @@ export class DismissableLayerState {
}, 10);
#onInteractOutside = debounce((e: InteractOutsideEvent) => {
- if (!this.node.value) return;
+ if (!this.currNode) return;
const behaviorType = this.#behaviorType.value;
if (
!this.#isResponsibleLayer ||
this.#isAnyEventIntercepted() ||
- !isValidEvent(e, this.node.value)
+ !isValidEvent(e, this.currNode)
) {
return;
}
diff --git a/packages/bits-ui/src/lib/bits/utilities/index.ts b/packages/bits-ui/src/lib/bits/utilities/index.ts
index b2f2161c6..8db904f17 100644
--- a/packages/bits-ui/src/lib/bits/utilities/index.ts
+++ b/packages/bits-ui/src/lib/bits/utilities/index.ts
@@ -1 +1,2 @@
export { default as WithTransition } from "./with-transition.svelte";
+export { default as Mounted } from "./mounted.svelte";
diff --git a/packages/bits-ui/src/lib/bits/utilities/mounted.svelte b/packages/bits-ui/src/lib/bits/utilities/mounted.svelte
new file mode 100644
index 000000000..bee1fb215
--- /dev/null
+++ b/packages/bits-ui/src/lib/bits/utilities/mounted.svelte
@@ -0,0 +1,10 @@
+
diff --git a/packages/bits-ui/src/lib/bits/utilities/popper-layer/popper-layer.svelte b/packages/bits-ui/src/lib/bits/utilities/popper-layer/popper-layer.svelte
index a7992f7f5..33b5828d5 100644
--- a/packages/bits-ui/src/lib/bits/utilities/popper-layer/popper-layer.svelte
+++ b/packages/bits-ui/src/lib/bits/utilities/popper-layer/popper-layer.svelte
@@ -8,19 +8,19 @@
import { FocusScope } from "$lib/bits/utilities/focus-scope/index.js";
import { mergeProps } from "$lib/internal/mergeProps.js";
- let { popper, ...restProps }: PopperLayerImplProps = $props();
+ let { popper, present, ...restProps }: PopperLayerImplProps = $props();
-
- {#snippet presence({ present })}
-
+
+ {#snippet presence()}
+
{#snippet content({ props: floatingProps })}
{#snippet focusScope({ props: focusScopeProps })}
-
-
+
+
{#snippet children({ props: dismissableProps })}
-
+
{@render popper?.({
props: mergeProps(
restProps,
diff --git a/packages/bits-ui/src/lib/bits/utilities/portal/portal.svelte b/packages/bits-ui/src/lib/bits/utilities/portal/portal.svelte
index 32a2b55d0..9a277779c 100644
--- a/packages/bits-ui/src/lib/bits/utilities/portal/portal.svelte
+++ b/packages/bits-ui/src/lib/bits/utilities/portal/portal.svelte
@@ -1,5 +1,5 @@
diff --git a/packages/bits-ui/src/lib/bits/utilities/presence-layer/usePresence.svelte.ts b/packages/bits-ui/src/lib/bits/utilities/presence-layer/usePresence.svelte.ts
index 19330c8ae..f2d4cb6c1 100644
--- a/packages/bits-ui/src/lib/bits/utilities/presence-layer/usePresence.svelte.ts
+++ b/packages/bits-ui/src/lib/bits/utilities/presence-layer/usePresence.svelte.ts
@@ -7,7 +7,7 @@ export function usePresence(present: ReadableBox, id: ReadableBox(null);
- $effect.pre(() => {
+ $effect(() => {
if (!id.value) return;
if (!present.value) return;
@@ -112,10 +112,9 @@ export function usePresence(present: ReadableBox, id: ReadableBox {
- if (!node) return;
- node.removeEventListener("animationstart", handleAnimationStart);
- node.removeEventListener("animationcancel", handleAnimationEnd);
- node.removeEventListener("animationend", handleAnimationEnd);
+ node?.removeEventListener("animationstart", handleAnimationStart);
+ node?.removeEventListener("animationcancel", handleAnimationEnd);
+ node?.removeEventListener("animationend", handleAnimationEnd);
};
});
diff --git a/packages/bits-ui/src/lib/internal/types.ts b/packages/bits-ui/src/lib/internal/types.ts
index aebf3ad28..520c10e53 100644
--- a/packages/bits-ui/src/lib/internal/types.ts
+++ b/packages/bits-ui/src/lib/internal/types.ts
@@ -7,6 +7,7 @@ import type {
HTMLImgAttributes,
HTMLInputAttributes,
HTMLLabelAttributes,
+ HTMLLiAttributes,
SVGAttributes,
} from "svelte/elements";
import type { TransitionConfig } from "svelte/transition";
@@ -133,12 +134,15 @@ export type PrimitiveHeadingAttributes = Primitive;
export type PrimitiveLabelAttributes = Primitive;
export type PrimitiveSVGAttributes = Primitive>;
export type PrimitiveAnchorAttributes = Primitive;
+export type PrimitiveLiAttributes = Primitive;
+export type PrimitiveElementAttributes = Primitive>;
+export type PrimitiveUListAttributes = Primitive>;
export type AsChildProps = {
child: Snippet<[SnippetProps & { props: Record }]>;
children?: never;
asChild: true;
- ref?: Ref;
+ ref?: Ref | null;
style?: StyleProperties;
} & Omit;
@@ -150,7 +154,7 @@ export type DefaultProps = {
style?: StyleProperties;
} & Omit;
-export type ElementRef = Box;
+export type ElementRef = Box;
export type WithAsChild<
Props,
diff --git a/packages/bits-ui/src/lib/internal/useArrowNavigation.ts b/packages/bits-ui/src/lib/internal/useArrowNavigation.ts
new file mode 100644
index 000000000..1d88751eb
--- /dev/null
+++ b/packages/bits-ui/src/lib/internal/useArrowNavigation.ts
@@ -0,0 +1,168 @@
+import type { Direction } from "$lib/shared/index.js";
+
+type ArrowKeyOptions = "horizontal" | "vertical" | "both";
+
+interface ArrowNavigationOptions {
+ /**
+ * The arrow key options to allow navigation
+ *
+ * @defaultValue "both"
+ */
+ arrowKeyOptions?: ArrowKeyOptions;
+
+ /**
+ * The attribute name to find the collection items in the parent element.
+ */
+ attributeName: string;
+
+ /**
+ * The parent element where contains all the collection items, this will collect every item to be used when nav
+ * It will be ignored if attributeName is provided
+ *
+ * @defaultValue []
+ */
+ itemsArray?: HTMLElement[];
+
+ /**
+ * Allow loop navigation. If false, it will stop at the first and last element
+ *
+ * @defaultValue true
+ */
+ loop?: boolean;
+
+ /**
+ * The orientation of the collection
+ *
+ * @defaultValue "ltr"
+ */
+ dir?: Direction;
+
+ /**
+ * Prevent the scroll when navigating. This happens when the direction of the
+ * key matches the scroll direction of any ancestor scrollable elements.
+ *
+ * @defaultValue true
+ */
+ preventScroll?: boolean;
+
+ /**
+ * By default all currentElement would trigger navigation. If `true`, currentElement nodeName in the ignore list will return null
+ *
+ * @defaultValue false
+ */
+ enableIgnoredElement?: boolean;
+
+ /**
+ * Focus the element after navigation
+ *
+ * @defaultValue false
+ */
+ focus?: boolean;
+}
+
+const ignoredElement = ["INPUT", "TEXTAREA"];
+
+/**
+ * Allow arrow navigation for every html element with data-radix-vue-collection-item tag
+ *
+ * @param e Keyboard event
+ * @param currentElement Event initiator element or any element that wants to handle the navigation
+ * @param parentElement Parent element where contains all the collection items, this will collect every item to be used when nav
+ * @param options further options
+ * @returns the navigated html element or null if none
+ */
+export function useArrowNavigation(
+ e: KeyboardEvent,
+ currentElement: HTMLElement,
+ parentElement: HTMLElement | undefined,
+ options: ArrowNavigationOptions
+): HTMLElement | null {
+ if (
+ !currentElement ||
+ (options.enableIgnoredElement && ignoredElement.includes(currentElement.nodeName))
+ )
+ return null;
+
+ const {
+ arrowKeyOptions = "both",
+ attributeName,
+ itemsArray = [],
+ loop = true,
+ dir = "ltr",
+ preventScroll = true,
+ focus = false,
+ } = options;
+
+ const [right, left, up, down, home, end] = [
+ e.key === "ArrowRight",
+ e.key === "ArrowLeft",
+ e.key === "ArrowUp",
+ e.key === "ArrowDown",
+ e.key === "Home",
+ e.key === "End",
+ ];
+ const goingVertical = up || down;
+ const goingHorizontal = right || left;
+ if (
+ !home &&
+ !end &&
+ ((!goingVertical && !goingHorizontal) ||
+ (arrowKeyOptions === "vertical" && goingHorizontal) ||
+ (arrowKeyOptions === "horizontal" && goingVertical))
+ )
+ return null;
+
+ const allCollectionItems: HTMLElement[] = parentElement
+ ? Array.from(parentElement.querySelectorAll(attributeName))
+ : itemsArray;
+
+ if (!allCollectionItems.length) return null;
+
+ if (preventScroll) e.preventDefault();
+
+ let item: HTMLElement | null = null;
+
+ if (goingHorizontal || goingVertical) {
+ const goForward = goingVertical ? down : dir === "ltr" ? right : left;
+ item = findNextFocusableElement(allCollectionItems, currentElement, {
+ goForward,
+ loop,
+ });
+ } else if (home) {
+ item = allCollectionItems.at(0) || null;
+ } else if (end) {
+ item = allCollectionItems.at(-1) || null;
+ }
+
+ if (focus) item?.focus();
+
+ return item;
+}
+
+/**
+ * Recursive function to find the next focusable element to avoid disabled elements
+ */
+function findNextFocusableElement(
+ elements: HTMLElement[],
+ currentElement: HTMLElement,
+ { goForward, loop }: { goForward: boolean; loop?: boolean },
+ iterations = elements.length
+): HTMLElement | null {
+ if (--iterations === 0) return null;
+
+ const index = elements.indexOf(currentElement);
+ const newIndex = goForward ? index + 1 : index - 1;
+
+ if (!loop && (newIndex < 0 || newIndex >= elements.length)) return null;
+
+ const adjustedNewIndex = (newIndex + elements.length) % elements.length;
+ const candidate = elements[adjustedNewIndex];
+ if (!candidate) return null;
+
+ const isDisabled =
+ candidate.hasAttribute("disabled") && candidate.getAttribute("disabled") !== "false";
+ if (isDisabled) {
+ return findNextFocusableElement(elements, candidate, { goForward, loop }, iterations);
+ }
+ return candidate;
+}
diff --git a/packages/bits-ui/src/lib/internal/useNodeById.svelte.ts b/packages/bits-ui/src/lib/internal/useNodeById.svelte.ts
index 52e3a3559..45b574b69 100644
--- a/packages/bits-ui/src/lib/internal/useNodeById.svelte.ts
+++ b/packages/bits-ui/src/lib/internal/useNodeById.svelte.ts
@@ -1,5 +1,8 @@
-import { type ReadableBox, type WritableBox, box } from "svelte-toolbelt";
+import { type Getter, type ReadableBox, type WritableBox, box } from "svelte-toolbelt";
import { afterTick } from "./afterTick.js";
+import type { Box } from "./box.svelte.js";
+import { untrack } from "svelte";
+import { noop } from "./callbacks.js";
/**
* Finds the node with that ID and sets it to the boxed node.
@@ -34,3 +37,51 @@ export function useNodeById(
return node;
}
+
+type UseRefByIdProps = {
+ /**
+ * The ID of the node to find.
+ */
+ id: Box;
+
+ /**
+ * The ref to set the node to.
+ */
+ ref: WritableBox;
+
+ /**
+ * A condition that determines whether the ref should be set or not.
+ */
+ condition?: Getter;
+
+ /**
+ * A callback fired when the ref changes.
+ */
+ onRefChange?: (node: HTMLElement | null) => void;
+};
+
+/**
+ * Finds the node with that ID and sets it to the boxed node.
+ * Reactive using `$effect` to ensure when the ID or condition changes,
+ * an update is triggered and new node is found.
+ *
+ * @param id The boxed ID of the node to find.
+ */
+export function useRefById({
+ id,
+ ref,
+ condition = () => true,
+ onRefChange = noop,
+}: UseRefByIdProps) {
+ $effect(() => {
+ // re-run when the ID changes.
+ id.value;
+ condition();
+ // re-run when the condition changes.
+ untrack(() => {
+ const node = document.getElementById(id.value);
+ ref.value = node;
+ onRefChange(ref.value);
+ });
+ });
+}
diff --git a/packages/bits-ui/src/lib/types.ts b/packages/bits-ui/src/lib/types.ts
index a4654b463..7a5675f6f 100644
--- a/packages/bits-ui/src/lib/types.ts
+++ b/packages/bits-ui/src/lib/types.ts
@@ -18,6 +18,7 @@ export type * from "$lib/bits/dropdown-menu/types.js";
export type * from "$lib/bits/label/types.js";
export type * from "$lib/bits/link-preview/types.js";
export type * from "$lib/bits/menubar/types.js";
+export type * from "$lib/bits/navigation-menu/types.js";
export type * from "$lib/bits/pagination/types.js";
export type * from "$lib/bits/pin-input/types.js";
export type * from "$lib/bits/popover/types.js";
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fec03738e..c852d9515 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -13,7 +13,7 @@ importers:
version: 2.27.5
'@huntabyte/eslint-config':
specifier: ^0.3.1
- version: 0.3.1(@vue/compiler-sfc@3.4.27)(eslint-plugin-svelte@2.39.0(eslint@9.3.0)(svelte@5.0.0-next.143))(eslint@9.3.0)(svelte-eslint-parser@0.34.1(svelte@5.0.0-next.143))(svelte@5.0.0-next.143)(typescript@5.4.5)(vitest@1.6.0)
+ version: 0.3.1(@vue/compiler-sfc@3.4.27)(eslint-plugin-svelte@2.39.0(eslint@9.3.0)(svelte@5.0.0-next.155))(eslint@9.3.0)(svelte-eslint-parser@0.34.1(svelte@5.0.0-next.155))(svelte@5.0.0-next.155)(typescript@5.4.5)(vitest@1.6.0)
'@huntabyte/eslint-plugin':
specifier: ^0.1.0
version: 0.1.0(eslint@9.3.0)
@@ -25,22 +25,22 @@ importers:
version: 9.3.0
eslint-plugin-svelte:
specifier: ^2.37.0
- version: 2.39.0(eslint@9.3.0)(svelte@5.0.0-next.143)
+ version: 2.39.0(eslint@9.3.0)(svelte@5.0.0-next.155)
prettier:
specifier: ^3.2.5
version: 3.2.5
prettier-plugin-svelte:
specifier: ^3.2.2
- version: 3.2.3(prettier@3.2.5)(svelte@5.0.0-next.143)
+ version: 3.2.3(prettier@3.2.5)(svelte@5.0.0-next.155)
prettier-plugin-tailwindcss:
specifier: 0.5.13
- version: 0.5.13(prettier-plugin-svelte@3.2.3(prettier@3.2.5)(svelte@5.0.0-next.143))(prettier@3.2.5)
+ version: 0.5.13(prettier-plugin-svelte@3.2.3(prettier@3.2.5)(svelte@5.0.0-next.155))(prettier@3.2.5)
svelte:
- specifier: 5.0.0-next.143
- version: 5.0.0-next.143
+ specifier: 5.0.0-next.155
+ version: 5.0.0-next.155
svelte-eslint-parser:
specifier: ^0.34.1
- version: 0.34.1(svelte@5.0.0-next.143)
+ version: 0.34.1(svelte@5.0.0-next.155)
wrangler:
specifier: ^3.44.0
version: 3.57.2(@cloudflare/workers-types@4.20240524.0)
@@ -58,7 +58,7 @@ importers:
version: 3.5.4
'@melt-ui/svelte':
specifier: 0.76.2
- version: 0.76.2(svelte@5.0.0-next.143)
+ version: 0.76.2(svelte@5.0.0-next.155)
clsx:
specifier: ^2.1.0
version: 2.1.1
@@ -69,8 +69,8 @@ importers:
specifier: ^5.0.5
version: 5.0.7
runed:
- specifier: ^0.5.0
- version: 0.5.0(svelte@5.0.0-next.143)
+ specifier: ^0.12.1
+ version: 0.12.1(svelte@5.0.0-next.155)
scule:
specifier: ^1.3.0
version: 1.3.0
@@ -82,20 +82,20 @@ importers:
version: 1.0.6
svelte-toolbelt:
specifier: ^0.0.2
- version: 0.0.2(svelte@5.0.0-next.143)
+ version: 0.0.2(svelte@5.0.0-next.155)
devDependencies:
'@melt-ui/pp':
specifier: ^0.3.0
- version: 0.3.2(@melt-ui/svelte@0.76.2(svelte@5.0.0-next.143))(svelte@5.0.0-next.143)
+ version: 0.3.2(@melt-ui/svelte@0.76.2(svelte@5.0.0-next.155))(svelte@5.0.0-next.155)
'@sveltejs/kit':
specifier: ^2.5.0
- version: 2.5.10(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13)))(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13))
+ version: 2.5.10(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13)))(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13))
'@sveltejs/package':
specifier: ^2.2.7
- version: 2.3.1(svelte@5.0.0-next.143)(typescript@5.4.5)
+ version: 2.3.1(svelte@5.0.0-next.155)(typescript@5.4.5)
'@sveltejs/vite-plugin-svelte':
specifier: ^3.1.0
- version: 3.1.1(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13))
+ version: 3.1.1(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13))
'@testing-library/dom':
specifier: ^10.0.0
version: 10.1.0
@@ -104,7 +104,7 @@ importers:
version: 6.4.5(@types/jest@29.5.12)(vitest@1.6.0(@types/node@20.12.13)(@vitest/ui@1.6.0)(jsdom@24.1.0))
'@testing-library/svelte':
specifier: ^5.0.1
- version: 5.1.0(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13))(vitest@1.6.0(@types/node@20.12.13)(@vitest/ui@1.6.0)(jsdom@24.1.0))
+ version: 5.1.0(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13))(vitest@1.6.0(@types/node@20.12.13)(@vitest/ui@1.6.0)(jsdom@24.1.0))
'@testing-library/user-event':
specifier: ^14.5.2
version: 14.5.2(@testing-library/dom@10.1.0)
@@ -139,11 +139,11 @@ importers:
specifier: ^1.5.1
version: 1.5.1
svelte:
- specifier: 5.0.0-next.143
- version: 5.0.0-next.143
+ specifier: 5.0.0-next.155
+ version: 5.0.0-next.155
svelte-check:
specifier: ^3.6.9
- version: 3.8.0(postcss-load-config@5.1.0(jiti@1.21.0)(postcss@8.4.38))(postcss@8.4.38)(svelte@5.0.0-next.143)
+ version: 3.8.0(postcss-load-config@5.1.0(postcss@8.4.38))(postcss@8.4.38)(svelte@5.0.0-next.155)
tslib:
specifier: ^2.6.2
version: 2.6.2
@@ -164,26 +164,26 @@ importers:
version: 3.5.4
'@melt-ui/svelte':
specifier: 0.76.2
- version: 0.76.2(svelte@5.0.0-next.143)
+ version: 0.76.2(svelte@5.0.0-next.155)
bits-ui:
specifier: workspace:*
version: link:../../packages/bits-ui
devDependencies:
'@melt-ui/pp':
specifier: ^0.3.0
- version: 0.3.2(@melt-ui/svelte@0.76.2(svelte@5.0.0-next.143))(svelte@5.0.0-next.143)
+ version: 0.3.2(@melt-ui/svelte@0.76.2(svelte@5.0.0-next.155))(svelte@5.0.0-next.155)
'@prettier/sync':
specifier: 0.3.0
version: 0.3.0(prettier@3.2.5)
'@sveltejs/adapter-cloudflare':
specifier: ^4.2.0
- version: 4.4.0(@sveltejs/kit@2.5.10(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13)))(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13)))(wrangler@3.57.2(@cloudflare/workers-types@4.20240524.0))
+ version: 4.4.0(@sveltejs/kit@2.5.10(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13)))(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13)))(wrangler@3.57.2(@cloudflare/workers-types@4.20240524.0))
'@sveltejs/kit':
specifier: ^2.5.0
- version: 2.5.10(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13)))(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13))
+ version: 2.5.10(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13)))(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13))
'@sveltejs/vite-plugin-svelte':
specifier: ^3.1.0
- version: 3.1.1(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13))
+ version: 3.1.1(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13))
'@tailwindcss/typography':
specifier: ^0.5.10
version: 0.5.13(tailwindcss@3.4.3)
@@ -216,13 +216,13 @@ importers:
version: 3.0.1
mdsx:
specifier: ^0.0.5
- version: 0.0.5(svelte@5.0.0-next.143)
+ version: 0.0.5(svelte@5.0.0-next.155)
mode-watcher:
specifier: ^0.2.0
- version: 0.2.2(svelte@5.0.0-next.143)
+ version: 0.2.2(svelte@5.0.0-next.155)
phosphor-svelte:
specifier: ^1.4.2
- version: 1.4.2(svelte@5.0.0-next.143)
+ version: 1.4.2(svelte@5.0.0-next.155)
postcss:
specifier: ^8.4.33
version: 8.4.38
@@ -242,11 +242,11 @@ importers:
specifier: ^1.1.1
version: 1.6.1
svelte:
- specifier: 5.0.0-next.143
- version: 5.0.0-next.143
+ specifier: 5.0.0-next.155
+ version: 5.0.0-next.155
svelte-check:
specifier: ^3.6.9
- version: 3.8.0(postcss-load-config@5.1.0(jiti@1.21.0)(postcss@8.4.38))(postcss@8.4.38)(svelte@5.0.0-next.143)
+ version: 3.8.0(postcss-load-config@5.1.0(postcss@8.4.38))(postcss@8.4.38)(svelte@5.0.0-next.155)
tailwind-merge:
specifier: ^2.2.1
version: 2.3.0
@@ -4403,8 +4403,8 @@ packages:
run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
- runed@0.5.0:
- resolution: {integrity: sha512-nJ/36UhthXG1KNieQhxtvuoK0eHrgEesTkEwV/Tbo7HNka2QqQfoL5vq755ngisrufZl6oQVPJKPdan6msMDGw==}
+ runed@0.12.1:
+ resolution: {integrity: sha512-BlVXcGQ8+Rb7Klh2WO4y1em1Z15kYQMM0lG8bZhknAOKFUAyNHA5DgBhFNzKAUCCkgXf7GTmQng100Z5Xc7QwA==}
peerDependencies:
svelte: ^5.0.0
@@ -4762,8 +4762,8 @@ packages:
svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0
typescript: ^4.9.4 || ^5.0.0
- svelte@5.0.0-next.143:
- resolution: {integrity: sha512-hRm52FjYUfd24eUlkBS41JSmqHOx6wt0cV+wMzgwqhhxIpJoz96eiMcnvcLqXx+gTxM1m0Pt/+7xP3vlm2QvPg==}
+ svelte@5.0.0-next.155:
+ resolution: {integrity: sha512-4a4EZuiTmg4eQJuQ6LTyK+DxRAZCYm4mXgqSWcZ7TellzLfaC1Je5nxBl1aZP3xdNhvPFIstQ8c7I6d+99FdZQ==}
engines: {node: '>=18'}
symbol-tree@3.2.4:
@@ -5345,7 +5345,7 @@ snapshots:
'@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25
- '@antfu/eslint-config@2.19.1(@vue/compiler-sfc@3.4.27)(eslint-plugin-svelte@2.39.0(eslint@9.3.0)(svelte@5.0.0-next.143))(eslint@9.3.0)(svelte-eslint-parser@0.34.1(svelte@5.0.0-next.143))(svelte@5.0.0-next.143)(typescript@5.4.5)(vitest@1.6.0)':
+ '@antfu/eslint-config@2.19.1(@vue/compiler-sfc@3.4.27)(eslint-plugin-svelte@2.39.0(eslint@9.3.0)(svelte@5.0.0-next.155))(eslint@9.3.0)(svelte-eslint-parser@0.34.1(svelte@5.0.0-next.155))(svelte@5.0.0-next.155)(typescript@5.4.5)(vitest@1.6.0)':
dependencies:
'@antfu/install-pkg': 0.3.3
'@clack/prompts': 0.7.0
@@ -5365,7 +5365,7 @@ snapshots:
eslint-plugin-markdown: 5.0.0(eslint@9.3.0)
eslint-plugin-n: 17.7.0(eslint@9.3.0)
eslint-plugin-no-only-tests: 3.1.0
- eslint-plugin-perfectionist: 2.10.0(eslint@9.3.0)(svelte-eslint-parser@0.34.1(svelte@5.0.0-next.143))(svelte@5.0.0-next.143)(typescript@5.4.5)(vue-eslint-parser@9.4.2(eslint@9.3.0))
+ eslint-plugin-perfectionist: 2.10.0(eslint@9.3.0)(svelte-eslint-parser@0.34.1(svelte@5.0.0-next.155))(svelte@5.0.0-next.155)(typescript@5.4.5)(vue-eslint-parser@9.4.2(eslint@9.3.0))
eslint-plugin-regexp: 2.6.0(eslint@9.3.0)
eslint-plugin-toml: 0.11.0(eslint@9.3.0)
eslint-plugin-unicorn: 53.0.0(eslint@9.3.0)
@@ -5384,8 +5384,8 @@ snapshots:
yaml-eslint-parser: 1.2.3
yargs: 17.7.2
optionalDependencies:
- eslint-plugin-svelte: 2.39.0(eslint@9.3.0)(svelte@5.0.0-next.143)
- svelte-eslint-parser: 0.34.1(svelte@5.0.0-next.143)
+ eslint-plugin-svelte: 2.39.0(eslint@9.3.0)(svelte@5.0.0-next.155)
+ svelte-eslint-parser: 0.34.1(svelte@5.0.0-next.155)
transitivePeerDependencies:
- '@vue/compiler-sfc'
- supports-color
@@ -6050,9 +6050,9 @@ snapshots:
'@humanwhocodes/retry@0.3.0': {}
- '@huntabyte/eslint-config@0.3.1(@vue/compiler-sfc@3.4.27)(eslint-plugin-svelte@2.39.0(eslint@9.3.0)(svelte@5.0.0-next.143))(eslint@9.3.0)(svelte-eslint-parser@0.34.1(svelte@5.0.0-next.143))(svelte@5.0.0-next.143)(typescript@5.4.5)(vitest@1.6.0)':
+ '@huntabyte/eslint-config@0.3.1(@vue/compiler-sfc@3.4.27)(eslint-plugin-svelte@2.39.0(eslint@9.3.0)(svelte@5.0.0-next.155))(eslint@9.3.0)(svelte-eslint-parser@0.34.1(svelte@5.0.0-next.155))(svelte@5.0.0-next.155)(typescript@5.4.5)(vitest@1.6.0)':
dependencies:
- '@antfu/eslint-config': 2.19.1(@vue/compiler-sfc@3.4.27)(eslint-plugin-svelte@2.39.0(eslint@9.3.0)(svelte@5.0.0-next.143))(eslint@9.3.0)(svelte-eslint-parser@0.34.1(svelte@5.0.0-next.143))(svelte@5.0.0-next.143)(typescript@5.4.5)(vitest@1.6.0)
+ '@antfu/eslint-config': 2.19.1(@vue/compiler-sfc@3.4.27)(eslint-plugin-svelte@2.39.0(eslint@9.3.0)(svelte@5.0.0-next.155))(eslint@9.3.0)(svelte-eslint-parser@0.34.1(svelte@5.0.0-next.155))(svelte@5.0.0-next.155)(typescript@5.4.5)(vitest@1.6.0)
'@antfu/install-pkg': 0.3.3
'@clack/prompts': 0.7.0
'@huntabyte/eslint-plugin': 0.1.0(eslint@9.3.0)
@@ -6061,10 +6061,10 @@ snapshots:
chalk: 5.3.0
eslint: 9.3.0
eslint-flat-config-utils: 0.2.5
- eslint-plugin-svelte: 2.39.0(eslint@9.3.0)(svelte@5.0.0-next.143)
+ eslint-plugin-svelte: 2.39.0(eslint@9.3.0)(svelte@5.0.0-next.155)
local-pkg: 0.5.0
parse-gitignore: 2.0.0
- svelte-eslint-parser: 0.34.1(svelte@5.0.0-next.143)
+ svelte-eslint-parser: 0.34.1(svelte@5.0.0-next.155)
yargs: 17.7.2
transitivePeerDependencies:
- '@eslint-react/eslint-plugin'
@@ -6201,14 +6201,14 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@melt-ui/pp@0.3.2(@melt-ui/svelte@0.76.2(svelte@5.0.0-next.143))(svelte@5.0.0-next.143)':
+ '@melt-ui/pp@0.3.2(@melt-ui/svelte@0.76.2(svelte@5.0.0-next.155))(svelte@5.0.0-next.155)':
dependencies:
- '@melt-ui/svelte': 0.76.2(svelte@5.0.0-next.143)
+ '@melt-ui/svelte': 0.76.2(svelte@5.0.0-next.155)
estree-walker: 3.0.3
magic-string: 0.30.10
- svelte: 5.0.0-next.143
+ svelte: 5.0.0-next.155
- '@melt-ui/svelte@0.76.2(svelte@5.0.0-next.143)':
+ '@melt-ui/svelte@0.76.2(svelte@5.0.0-next.155)':
dependencies:
'@floating-ui/core': 1.6.2
'@floating-ui/dom': 1.6.5
@@ -6216,7 +6216,7 @@ snapshots:
dequal: 2.0.3
focus-trap: 7.5.4
nanoid: 5.0.7
- svelte: 5.0.0-next.143
+ svelte: 5.0.0-next.155
'@nodelib/fs.scandir@2.1.5':
dependencies:
@@ -6478,17 +6478,17 @@ snapshots:
- supports-color
- typescript
- '@sveltejs/adapter-cloudflare@4.4.0(@sveltejs/kit@2.5.10(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13)))(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13)))(wrangler@3.57.2(@cloudflare/workers-types@4.20240524.0))':
+ '@sveltejs/adapter-cloudflare@4.4.0(@sveltejs/kit@2.5.10(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13)))(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13)))(wrangler@3.57.2(@cloudflare/workers-types@4.20240524.0))':
dependencies:
'@cloudflare/workers-types': 4.20240524.0
- '@sveltejs/kit': 2.5.10(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13)))(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13))
+ '@sveltejs/kit': 2.5.10(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13)))(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13))
esbuild: 0.20.2
worktop: 0.8.0-next.18
wrangler: 3.57.2(@cloudflare/workers-types@4.20240524.0)
- '@sveltejs/kit@2.5.10(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13)))(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13))':
+ '@sveltejs/kit@2.5.10(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13)))(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13))':
dependencies:
- '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13))
+ '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13))
'@types/cookie': 0.6.0
cookie: 0.6.0
devalue: 5.0.0
@@ -6500,39 +6500,39 @@ snapshots:
sade: 1.8.1
set-cookie-parser: 2.6.0
sirv: 2.0.4
- svelte: 5.0.0-next.143
+ svelte: 5.0.0-next.155
tiny-glob: 0.2.9
vite: 5.2.12(@types/node@20.12.13)
- '@sveltejs/package@2.3.1(svelte@5.0.0-next.143)(typescript@5.4.5)':
+ '@sveltejs/package@2.3.1(svelte@5.0.0-next.155)(typescript@5.4.5)':
dependencies:
chokidar: 3.6.0
kleur: 4.1.5
sade: 1.8.1
semver: 7.6.2
- svelte: 5.0.0-next.143
- svelte2tsx: 0.7.9(svelte@5.0.0-next.143)(typescript@5.4.5)
+ svelte: 5.0.0-next.155
+ svelte2tsx: 0.7.9(svelte@5.0.0-next.155)(typescript@5.4.5)
transitivePeerDependencies:
- typescript
- '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13)))(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13))':
+ '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13)))(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13))':
dependencies:
- '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13))
+ '@sveltejs/vite-plugin-svelte': 3.1.1(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13))
debug: 4.3.4
- svelte: 5.0.0-next.143
+ svelte: 5.0.0-next.155
vite: 5.2.12(@types/node@20.12.13)
transitivePeerDependencies:
- supports-color
- '@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13))':
+ '@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13))':
dependencies:
- '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13)))(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13))
+ '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.1(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13)))(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13))
debug: 4.3.4
deepmerge: 4.3.1
kleur: 4.1.5
magic-string: 0.30.10
- svelte: 5.0.0-next.143
- svelte-hmr: 0.16.0(svelte@5.0.0-next.143)
+ svelte: 5.0.0-next.155
+ svelte-hmr: 0.16.0(svelte@5.0.0-next.155)
vite: 5.2.12(@types/node@20.12.13)
vitefu: 0.2.5(vite@5.2.12(@types/node@20.12.13))
transitivePeerDependencies:
@@ -6593,10 +6593,10 @@ snapshots:
'@types/jest': 29.5.12
vitest: 1.6.0(@types/node@20.12.13)(@vitest/ui@1.6.0)(jsdom@24.1.0)
- '@testing-library/svelte@5.1.0(svelte@5.0.0-next.143)(vite@5.2.12(@types/node@20.12.13))(vitest@1.6.0(@types/node@20.12.13)(@vitest/ui@1.6.0)(jsdom@24.1.0))':
+ '@testing-library/svelte@5.1.0(svelte@5.0.0-next.155)(vite@5.2.12(@types/node@20.12.13))(vitest@1.6.0(@types/node@20.12.13)(@vitest/ui@1.6.0)(jsdom@24.1.0))':
dependencies:
'@testing-library/dom': 9.3.4
- svelte: 5.0.0-next.143
+ svelte: 5.0.0-next.155
optionalDependencies:
vite: 5.2.12(@types/node@20.12.13)
vitest: 1.6.0(@types/node@20.12.13)(@vitest/ui@1.6.0)(jsdom@24.1.0)
@@ -7720,15 +7720,15 @@ snapshots:
eslint-plugin-no-only-tests@3.1.0: {}
- eslint-plugin-perfectionist@2.10.0(eslint@9.3.0)(svelte-eslint-parser@0.34.1(svelte@5.0.0-next.143))(svelte@5.0.0-next.143)(typescript@5.4.5)(vue-eslint-parser@9.4.2(eslint@9.3.0)):
+ eslint-plugin-perfectionist@2.10.0(eslint@9.3.0)(svelte-eslint-parser@0.34.1(svelte@5.0.0-next.155))(svelte@5.0.0-next.155)(typescript@5.4.5)(vue-eslint-parser@9.4.2(eslint@9.3.0)):
dependencies:
'@typescript-eslint/utils': 7.11.0(eslint@9.3.0)(typescript@5.4.5)
eslint: 9.3.0
minimatch: 9.0.4
natural-compare-lite: 1.4.0
optionalDependencies:
- svelte: 5.0.0-next.143
- svelte-eslint-parser: 0.34.1(svelte@5.0.0-next.143)
+ svelte: 5.0.0-next.155
+ svelte-eslint-parser: 0.34.1(svelte@5.0.0-next.155)
vue-eslint-parser: 9.4.2(eslint@9.3.0)
transitivePeerDependencies:
- supports-color
@@ -7745,7 +7745,7 @@ snapshots:
regexp-ast-analysis: 0.7.1
scslre: 0.3.0
- eslint-plugin-svelte@2.39.0(eslint@9.3.0)(svelte@5.0.0-next.143):
+ eslint-plugin-svelte@2.39.0(eslint@9.3.0)(svelte@5.0.0-next.155):
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@9.3.0)
'@jridgewell/sourcemap-codec': 1.4.15
@@ -7759,9 +7759,9 @@ snapshots:
postcss-safe-parser: 6.0.0(postcss@8.4.38)
postcss-selector-parser: 6.1.0
semver: 7.6.2
- svelte-eslint-parser: 0.36.0(svelte@5.0.0-next.143)
+ svelte-eslint-parser: 0.36.0(svelte@5.0.0-next.155)
optionalDependencies:
- svelte: 5.0.0-next.143
+ svelte: 5.0.0-next.155
transitivePeerDependencies:
- supports-color
- ts-node
@@ -9108,7 +9108,7 @@ snapshots:
dependencies:
'@types/mdast': 4.0.4
- mdsx@0.0.5(svelte@5.0.0-next.143):
+ mdsx@0.0.5(svelte@5.0.0-next.155):
dependencies:
esrap: 1.2.2
hast-util-to-html: 9.0.1
@@ -9117,7 +9117,7 @@ snapshots:
rehype-stringify: 10.0.0
remark-parse: 11.0.0
remark-rehype: 11.1.0
- svelte: 5.0.0-next.143
+ svelte: 5.0.0-next.155
unified: 11.0.4
unist-util-visit: 5.0.0
vfile: 6.0.1
@@ -9645,9 +9645,9 @@ snapshots:
pkg-types: 1.1.1
ufo: 1.5.3
- mode-watcher@0.2.2(svelte@5.0.0-next.143):
+ mode-watcher@0.2.2(svelte@5.0.0-next.155):
dependencies:
- svelte: 5.0.0-next.143
+ svelte: 5.0.0-next.155
mri@1.2.0: {}
@@ -9884,9 +9884,9 @@ snapshots:
estree-walker: 3.0.3
is-reference: 3.0.2
- phosphor-svelte@1.4.2(svelte@5.0.0-next.143):
+ phosphor-svelte@1.4.2(svelte@5.0.0-next.155):
dependencies:
- svelte: 5.0.0-next.143
+ svelte: 5.0.0-next.155
picocolors@1.0.1: {}
@@ -9988,16 +9988,16 @@ snapshots:
prelude-ls@1.2.1: {}
- prettier-plugin-svelte@3.2.3(prettier@3.2.5)(svelte@5.0.0-next.143):
+ prettier-plugin-svelte@3.2.3(prettier@3.2.5)(svelte@5.0.0-next.155):
dependencies:
prettier: 3.2.5
- svelte: 5.0.0-next.143
+ svelte: 5.0.0-next.155
- prettier-plugin-tailwindcss@0.5.13(prettier-plugin-svelte@3.2.3(prettier@3.2.5)(svelte@5.0.0-next.143))(prettier@3.2.5):
+ prettier-plugin-tailwindcss@0.5.13(prettier-plugin-svelte@3.2.3(prettier@3.2.5)(svelte@5.0.0-next.155))(prettier@3.2.5):
dependencies:
prettier: 3.2.5
optionalDependencies:
- prettier-plugin-svelte: 3.2.3(prettier@3.2.5)(svelte@5.0.0-next.143)
+ prettier-plugin-svelte: 3.2.3(prettier@3.2.5)(svelte@5.0.0-next.155)
prettier@2.8.8: {}
@@ -10295,10 +10295,11 @@ snapshots:
dependencies:
queue-microtask: 1.2.3
- runed@0.5.0(svelte@5.0.0-next.143):
+ runed@0.12.1(svelte@5.0.0-next.155):
dependencies:
+ esm-env: 1.0.0
nanoid: 5.0.7
- svelte: 5.0.0-next.143
+ svelte: 5.0.0-next.155
rxjs@7.8.1:
dependencies:
@@ -10595,7 +10596,7 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
- svelte-check@3.8.0(postcss-load-config@5.1.0(jiti@1.21.0)(postcss@8.4.38))(postcss@8.4.38)(svelte@5.0.0-next.143):
+ svelte-check@3.8.0(postcss-load-config@5.1.0(postcss@8.4.38))(postcss@8.4.38)(svelte@5.0.0-next.155):
dependencies:
'@jridgewell/trace-mapping': 0.3.25
chokidar: 3.6.0
@@ -10603,8 +10604,8 @@ snapshots:
import-fresh: 3.3.0
picocolors: 1.0.1
sade: 1.8.1
- svelte: 5.0.0-next.143
- svelte-preprocess: 5.1.4(postcss-load-config@5.1.0(jiti@1.21.0)(postcss@8.4.38))(postcss@8.4.38)(svelte@5.0.0-next.143)(typescript@5.4.5)
+ svelte: 5.0.0-next.155
+ svelte-preprocess: 5.1.4(postcss-load-config@5.1.0(postcss@8.4.38))(postcss@8.4.38)(svelte@5.0.0-next.155)(typescript@5.4.5)
typescript: 5.4.5
transitivePeerDependencies:
- '@babel/core'
@@ -10617,7 +10618,7 @@ snapshots:
- stylus
- sugarss
- svelte-eslint-parser@0.34.1(svelte@5.0.0-next.143):
+ svelte-eslint-parser@0.34.1(svelte@5.0.0-next.155):
dependencies:
eslint-scope: 7.2.2
eslint-visitor-keys: 3.4.3
@@ -10625,9 +10626,9 @@ snapshots:
postcss: 8.4.38
postcss-scss: 4.0.9(postcss@8.4.38)
optionalDependencies:
- svelte: 5.0.0-next.143
+ svelte: 5.0.0-next.155
- svelte-eslint-parser@0.36.0(svelte@5.0.0-next.143):
+ svelte-eslint-parser@0.36.0(svelte@5.0.0-next.155):
dependencies:
eslint-scope: 7.2.2
eslint-visitor-keys: 3.4.3
@@ -10635,37 +10636,37 @@ snapshots:
postcss: 8.4.38
postcss-scss: 4.0.9(postcss@8.4.38)
optionalDependencies:
- svelte: 5.0.0-next.143
+ svelte: 5.0.0-next.155
- svelte-hmr@0.16.0(svelte@5.0.0-next.143):
+ svelte-hmr@0.16.0(svelte@5.0.0-next.155):
dependencies:
- svelte: 5.0.0-next.143
+ svelte: 5.0.0-next.155
- svelte-preprocess@5.1.4(postcss-load-config@5.1.0(jiti@1.21.0)(postcss@8.4.38))(postcss@8.4.38)(svelte@5.0.0-next.143)(typescript@5.4.5):
+ svelte-preprocess@5.1.4(postcss-load-config@5.1.0(postcss@8.4.38))(postcss@8.4.38)(svelte@5.0.0-next.155)(typescript@5.4.5):
dependencies:
'@types/pug': 2.0.10
detect-indent: 6.1.0
magic-string: 0.30.10
sorcery: 0.11.0
strip-indent: 3.0.0
- svelte: 5.0.0-next.143
+ svelte: 5.0.0-next.155
optionalDependencies:
postcss: 8.4.38
postcss-load-config: 5.1.0(jiti@1.21.0)(postcss@8.4.38)
typescript: 5.4.5
- svelte-toolbelt@0.0.2(svelte@5.0.0-next.143):
+ svelte-toolbelt@0.0.2(svelte@5.0.0-next.155):
dependencies:
- svelte: 5.0.0-next.143
+ svelte: 5.0.0-next.155
- svelte2tsx@0.7.9(svelte@5.0.0-next.143)(typescript@5.4.5):
+ svelte2tsx@0.7.9(svelte@5.0.0-next.155)(typescript@5.4.5):
dependencies:
dedent-js: 1.0.1
pascal-case: 3.1.2
- svelte: 5.0.0-next.143
+ svelte: 5.0.0-next.155
typescript: 5.4.5
- svelte@5.0.0-next.143:
+ svelte@5.0.0-next.155:
dependencies:
'@ampproject/remapping': 2.3.0
'@jridgewell/sourcemap-codec': 1.4.15
diff --git a/sites/docs/content/components/navigation-menu.md b/sites/docs/content/components/navigation-menu.md
new file mode 100644
index 000000000..6e56a8482
--- /dev/null
+++ b/sites/docs/content/components/navigation-menu.md
@@ -0,0 +1,23 @@
+---
+title: Navigation Menu
+description: A list of links that allow users to navigate between pages of a website.
+---
+
+
+
+
+
+
+
+
+
+## Structure
+
+```svelte
+
+```
diff --git a/sites/docs/package.json b/sites/docs/package.json
index b95c165ef..b5e5c9a62 100644
--- a/sites/docs/package.json
+++ b/sites/docs/package.json
@@ -39,7 +39,7 @@
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.0",
"shiki": "^1.1.1",
- "svelte": "5.0.0-next.143",
+ "svelte": "5.0.0-next.155",
"svelte-check": "^3.6.9",
"tailwind-merge": "^2.2.1",
"tailwind-variants": "^0.1.20",
diff --git a/sites/docs/src/lib/components/demos/index.ts b/sites/docs/src/lib/components/demos/index.ts
index 77350037f..2d35e6c1e 100644
--- a/sites/docs/src/lib/components/demos/index.ts
+++ b/sites/docs/src/lib/components/demos/index.ts
@@ -17,6 +17,7 @@ export { default as DropdownMenuDemo } from "./dropdown-menu-demo.svelte";
export { default as LabelDemo } from "./label-demo.svelte";
export { default as LinkPreviewDemo } from "./link-preview-demo.svelte";
export { default as MenubarDemo } from "./menubar-demo.svelte";
+export { default as NavigationMenuDemo } from "./navigation-menu-demo.svelte";
export { default as PaginationDemo } from "./pagination-demo.svelte";
export { default as PinInputDemo } from "./pin-input-demo.svelte";
export { default as PopoverDemo } from "./popover-demo.svelte";
diff --git a/sites/docs/src/lib/components/demos/menubar-demo.svelte b/sites/docs/src/lib/components/demos/menubar-demo.svelte
index af87c0289..dde2e1420 100644
--- a/sites/docs/src/lib/components/demos/menubar-demo.svelte
+++ b/sites/docs/src/lib/components/demos/menubar-demo.svelte
@@ -96,7 +96,7 @@
{#each views as view}
{#snippet children({ checked })}
diff --git a/sites/docs/src/lib/components/demos/navigation-menu-demo.svelte b/sites/docs/src/lib/components/demos/navigation-menu-demo.svelte
new file mode 100644
index 000000000..61f73bf65
--- /dev/null
+++ b/sites/docs/src/lib/components/demos/navigation-menu-demo.svelte
@@ -0,0 +1,160 @@
+
+
+{#snippet ListItem({ className, title, content, href }: ListItemProps)}
+
+
+ {title}
+
+ {content}
+
+
+
+{/snippet}
+
+
+
+
+
+ Getting started
+
+
+
+
+ -
+
+
+
Bits UI
+
+ The headless components for Svelte.
+
+
+
+
+ {@render ListItem({
+ href: "/docs",
+ title: "Introduction",
+ content: "Headless components for Svelte and SvelteKit",
+ })}
+ {@render ListItem({
+ href: "/docs/getting-started",
+ title: "Getting Started",
+ content: "How to install and use Bits UI",
+ })}
+ {@render ListItem({
+ href: "/docs/styling",
+ title: "Styling",
+ content: "How to style Bits UI components",
+ })}
+
+
+
+
+
+ Components
+
+
+
+
+ {#each components as component (component.title)}
+ {@render ListItem({
+ href: component.href,
+ title: component.title,
+ content: component.description,
+ })}
+ {/each}
+
+
+
+
+
+ Documentation
+
+
+
+
+
+
+
+
+
+
diff --git a/sites/docs/src/lib/config/navigation.ts b/sites/docs/src/lib/config/navigation.ts
index c98094db7..4a4906e74 100644
--- a/sites/docs/src/lib/config/navigation.ts
+++ b/sites/docs/src/lib/config/navigation.ts
@@ -151,6 +151,11 @@ export const navigation: Navigation = {
href: "/docs/components/menubar",
items: [],
},
+ {
+ title: "Navigation Menu",
+ href: "/docs/components/navigation-menu",
+ items: [],
+ },
{
title: "Pagination",
href: "/docs/components/pagination",
diff --git a/sites/docs/src/lib/content/api-reference/index.ts b/sites/docs/src/lib/content/api-reference/index.ts
index a319d94c7..ee4181ab5 100644
--- a/sites/docs/src/lib/content/api-reference/index.ts
+++ b/sites/docs/src/lib/content/api-reference/index.ts
@@ -54,6 +54,7 @@ export const bits = [
"label",
"link-preview",
"menubar",
+ "navigation-menu",
"pagination",
"pin-input",
"popover",