INIT
This commit is contained in:
Generated
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import { untrack, type Snippet } from "svelte";
|
||||
import type { NavigationMenuContentProps } from "../types.js";
|
||||
import {
|
||||
NavigationMenuItemContext,
|
||||
NavigationMenuItemState,
|
||||
NavigationMenuContentImplState,
|
||||
} from "../navigation-menu.svelte.js";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import DismissibleLayer from "../../utilities/dismissible-layer/dismissible-layer.svelte";
|
||||
import EscapeLayer from "../../utilities/escape-layer/escape-layer.svelte";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
id = createId(uid),
|
||||
child: childProp,
|
||||
children: childrenProp,
|
||||
onInteractOutside = noop,
|
||||
onFocusOutside = noop,
|
||||
onEscapeKeydown = noop,
|
||||
escapeKeydownBehavior = "close",
|
||||
interactOutsideBehavior = "close",
|
||||
itemState,
|
||||
onRefChange,
|
||||
...restProps
|
||||
}: Omit<NavigationMenuContentProps, "child"> & {
|
||||
itemState?: NavigationMenuItemState;
|
||||
onRefChange?: (ref: HTMLElement | null) => void;
|
||||
child?: Snippet<[{ props: Record<string, unknown> }]>;
|
||||
} = $props();
|
||||
|
||||
const contentImplState = NavigationMenuContentImplState.create(
|
||||
{
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => {
|
||||
ref = v;
|
||||
untrack(() => onRefChange?.(v));
|
||||
}
|
||||
),
|
||||
},
|
||||
itemState
|
||||
);
|
||||
|
||||
if (itemState) {
|
||||
NavigationMenuItemContext.set(itemState);
|
||||
}
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, contentImplState.props));
|
||||
</script>
|
||||
|
||||
<DismissibleLayer
|
||||
{id}
|
||||
ref={contentImplState.opts.ref}
|
||||
enabled={true}
|
||||
onInteractOutside={(e) => {
|
||||
onInteractOutside(e);
|
||||
if (e.defaultPrevented) return;
|
||||
contentImplState.onInteractOutside(e);
|
||||
}}
|
||||
onFocusOutside={(e) => {
|
||||
onFocusOutside(e);
|
||||
if (e.defaultPrevented) return;
|
||||
contentImplState.onFocusOutside(e);
|
||||
}}
|
||||
{interactOutsideBehavior}
|
||||
>
|
||||
{#snippet children({ props: dismissibleProps })}
|
||||
<EscapeLayer
|
||||
enabled={true}
|
||||
ref={contentImplState.opts.ref}
|
||||
onEscapeKeydown={(e) => {
|
||||
onEscapeKeydown(e);
|
||||
if (e.defaultPrevented) return;
|
||||
contentImplState.onEscapeKeydown(e);
|
||||
}}
|
||||
{escapeKeydownBehavior}
|
||||
>
|
||||
{@const finalProps = mergeProps(mergedProps, dismissibleProps)}
|
||||
{#if childProp}
|
||||
{@render childProp({ props: finalProps })}
|
||||
{:else}
|
||||
<div {...finalProps}>
|
||||
{@render childrenProp?.()}
|
||||
</div>
|
||||
{/if}
|
||||
</EscapeLayer>
|
||||
{/snippet}
|
||||
</DismissibleLayer>
|
||||
Generated
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
import { type Snippet } from "svelte";
|
||||
import type { NavigationMenuContentProps } from "../types.js";
|
||||
import { NavigationMenuItemState } from "../navigation-menu.svelte.js";
|
||||
type $$ComponentProps = Omit<NavigationMenuContentProps, "child"> & {
|
||||
itemState?: NavigationMenuItemState;
|
||||
onRefChange?: (ref: HTMLElement | null) => void;
|
||||
child?: Snippet<[{
|
||||
props: Record<string, unknown>;
|
||||
}]>;
|
||||
};
|
||||
declare const NavigationMenuContentImpl: import("svelte").Component<$$ComponentProps, {}, "ref">;
|
||||
type NavigationMenuContentImpl = ReturnType<typeof NavigationMenuContentImpl>;
|
||||
export default NavigationMenuContentImpl;
|
||||
Generated
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import { NavigationMenuContentState } from "../navigation-menu.svelte.js";
|
||||
import NavigationMenuContentImpl from "./navigation-menu-content-impl.svelte";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import type { NavigationMenuContentProps } from "../../../types.js";
|
||||
import Portal from "../../utilities/portal/portal.svelte";
|
||||
import PresenceLayer from "../../utilities/presence-layer/presence-layer.svelte";
|
||||
import Mounted from "../../utilities/mounted.svelte";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
id = createId(uid),
|
||||
children,
|
||||
child,
|
||||
forceMount = false,
|
||||
...restProps
|
||||
}: NavigationMenuContentProps = $props();
|
||||
|
||||
const contentState = NavigationMenuContentState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, contentState.props));
|
||||
</script>
|
||||
|
||||
<Portal
|
||||
to={contentState.context.viewportRef.current || undefined}
|
||||
disabled={!contentState.context.viewportRef.current}
|
||||
>
|
||||
<PresenceLayer
|
||||
open={forceMount || contentState.open || contentState.isLastActiveValue}
|
||||
ref={contentState.opts.ref}
|
||||
>
|
||||
{#snippet presence()}
|
||||
<NavigationMenuContentImpl {...mergedProps} {children} {child} />
|
||||
<Mounted bind:mounted={contentState.mounted} />
|
||||
{/snippet}
|
||||
</PresenceLayer>
|
||||
</Portal>
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { NavigationMenuContentProps } from "../../../types.js";
|
||||
declare const NavigationMenuContent: import("svelte").Component<NavigationMenuContentProps, {}, "ref">;
|
||||
type NavigationMenuContent = ReturnType<typeof NavigationMenuContent>;
|
||||
export default NavigationMenuContent;
|
||||
Generated
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { NavigationMenuIndicatorProps } from "../types.js";
|
||||
import { NavigationMenuIndicatorImplState } from "../navigation-menu.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
child,
|
||||
...restProps
|
||||
}: NavigationMenuIndicatorProps = $props();
|
||||
|
||||
const indicatorState = NavigationMenuIndicatorImplState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, indicatorState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
node_modules/bits-ui/dist/bits/navigation-menu/components/navigation-menu-indicator-impl.svelte.d.ts
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { NavigationMenuIndicatorProps } from "../types.js";
|
||||
declare const NavigationMenuIndicatorImpl: import("svelte").Component<NavigationMenuIndicatorProps, {}, "ref">;
|
||||
type NavigationMenuIndicatorImpl = ReturnType<typeof NavigationMenuIndicatorImpl>;
|
||||
export default NavigationMenuIndicatorImpl;
|
||||
Generated
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { NavigationMenuIndicatorProps } from "../types.js";
|
||||
import { NavigationMenuIndicatorState } from "../navigation-menu.svelte.js";
|
||||
import NavigationMenuIndicatorImpl from "./navigation-menu-indicator-impl.svelte";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import PresenceLayer from "../../utilities/presence-layer/presence-layer.svelte";
|
||||
import Portal from "../../utilities/portal/portal.svelte";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
children,
|
||||
child,
|
||||
forceMount = false,
|
||||
...restProps
|
||||
}: NavigationMenuIndicatorProps = $props();
|
||||
|
||||
const indicatorState = NavigationMenuIndicatorState.create();
|
||||
const mergedProps = $derived(mergeProps(restProps));
|
||||
</script>
|
||||
|
||||
{#if indicatorState.context.indicatorTrackRef.current}
|
||||
<Portal to={indicatorState.context.indicatorTrackRef.current}>
|
||||
<PresenceLayer open={forceMount || indicatorState.isVisible} ref={boxWith(() => ref)}>
|
||||
{#snippet presence()}
|
||||
<NavigationMenuIndicatorImpl {...mergedProps} {children} {child} {id} bind:ref />
|
||||
{/snippet}
|
||||
</PresenceLayer>
|
||||
</Portal>
|
||||
{/if}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { NavigationMenuIndicatorProps } from "../types.js";
|
||||
declare const NavigationMenuIndicator: import("svelte").Component<NavigationMenuIndicatorProps, {}, "ref">;
|
||||
type NavigationMenuIndicator = ReturnType<typeof NavigationMenuIndicator>;
|
||||
export default NavigationMenuIndicator;
|
||||
Generated
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { NavigationMenuItemProps } from "../types.js";
|
||||
import { NavigationMenuItemState } from "../navigation-menu.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
|
||||
const uid = $props.id();
|
||||
const defaultId = createId(uid);
|
||||
|
||||
let {
|
||||
id = defaultId,
|
||||
value = defaultId,
|
||||
ref = $bindable(null),
|
||||
child,
|
||||
children,
|
||||
openOnHover = true,
|
||||
...restProps
|
||||
}: NavigationMenuItemProps = $props();
|
||||
|
||||
const itemState = NavigationMenuItemState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
value: boxWith(() => value),
|
||||
openOnHover: boxWith(() => openOnHover),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, itemState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<li {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</li>
|
||||
{/if}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { NavigationMenuItemProps } from "../types.js";
|
||||
declare const NavigationMenuItem: import("svelte").Component<NavigationMenuItemProps, {}, "ref">;
|
||||
type NavigationMenuItem = ReturnType<typeof NavigationMenuItem>;
|
||||
export default NavigationMenuItem;
|
||||
Generated
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { NavigationMenuLinkProps } from "../types.js";
|
||||
import { NavigationMenuLinkState } from "../navigation-menu.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
child,
|
||||
children,
|
||||
active = false,
|
||||
onSelect = noop,
|
||||
tabindex = 0,
|
||||
...restProps
|
||||
}: NavigationMenuLinkProps = $props();
|
||||
|
||||
const linkState = NavigationMenuLinkState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
active: boxWith(() => active),
|
||||
onSelect: boxWith(() => onSelect),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, linkState.props, { tabindex }));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<a {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</a>
|
||||
{/if}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { NavigationMenuLinkProps } from "../types.js";
|
||||
declare const NavigationMenuLink: import("svelte").Component<NavigationMenuLinkProps, {}, "ref">;
|
||||
type NavigationMenuLink = ReturnType<typeof NavigationMenuLink>;
|
||||
export default NavigationMenuLink;
|
||||
Generated
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { NavigationMenuListProps } from "../types.js";
|
||||
import { NavigationMenuListState } from "../navigation-menu.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import Mounted from "../../utilities/mounted.svelte";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
children,
|
||||
child,
|
||||
ref = $bindable(null),
|
||||
...restProps
|
||||
}: NavigationMenuListProps = $props();
|
||||
|
||||
const listState = NavigationMenuListState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, listState.props));
|
||||
const wrapperProps = $derived(mergeProps(listState.wrapperProps));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps, wrapperProps })}
|
||||
<Mounted bind:mounted={listState.wrapperMounted} />
|
||||
{:else}
|
||||
<div {...wrapperProps}>
|
||||
<ul {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</ul>
|
||||
</div>
|
||||
<Mounted bind:mounted={listState.wrapperMounted} />
|
||||
{/if}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { NavigationMenuListProps } from "../types.js";
|
||||
declare const NavigationMenuList: import("svelte").Component<NavigationMenuListProps, {}, "ref">;
|
||||
type NavigationMenuList = ReturnType<typeof NavigationMenuList>;
|
||||
export default NavigationMenuList;
|
||||
Generated
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { NavigationMenuSubProps } from "../types.js";
|
||||
import { NavigationMenuSubState } from "../navigation-menu.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
child,
|
||||
children,
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
onValueChange = noop,
|
||||
orientation = "horizontal",
|
||||
...restProps
|
||||
}: NavigationMenuSubProps = $props();
|
||||
|
||||
const rootState = NavigationMenuSubState.create({
|
||||
id: boxWith(() => id),
|
||||
value: boxWith(
|
||||
() => value,
|
||||
(v) => {
|
||||
value = v;
|
||||
onValueChange(v);
|
||||
}
|
||||
),
|
||||
orientation: boxWith(() => orientation),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, rootState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { NavigationMenuSubProps } from "../types.js";
|
||||
declare const NavigationMenuSub: import("svelte").Component<NavigationMenuSubProps, {}, "value" | "ref">;
|
||||
type NavigationMenuSub = ReturnType<typeof NavigationMenuSub>;
|
||||
export default NavigationMenuSub;
|
||||
Generated
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { NavigationMenuTriggerProps } from "../types.js";
|
||||
import { NavigationMenuTriggerState } from "../navigation-menu.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import VisuallyHidden from "../../utilities/visually-hidden/visually-hidden.svelte";
|
||||
import Mounted from "../../utilities/mounted.svelte";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
disabled = false,
|
||||
children,
|
||||
child,
|
||||
ref = $bindable(null),
|
||||
tabindex = 0,
|
||||
...restProps
|
||||
}: NavigationMenuTriggerProps = $props();
|
||||
|
||||
const triggerState = NavigationMenuTriggerState.create({
|
||||
id: boxWith(() => id),
|
||||
disabled: boxWith(() => disabled ?? false),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, triggerState.props, { tabindex }));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<button {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if triggerState.open}
|
||||
<VisuallyHidden {...triggerState.focusProxyProps} />
|
||||
<Mounted bind:mounted={triggerState.focusProxyMounted} />
|
||||
{#if triggerState.context.viewportRef.current}
|
||||
<span aria-owns={triggerState.itemContext.contentId ?? undefined}></span>
|
||||
{/if}
|
||||
{/if}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { NavigationMenuTriggerProps } from "../types.js";
|
||||
declare const NavigationMenuTrigger: import("svelte").Component<NavigationMenuTriggerProps, {}, "ref">;
|
||||
type NavigationMenuTrigger = ReturnType<typeof NavigationMenuTrigger>;
|
||||
export default NavigationMenuTrigger;
|
||||
Generated
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import type { NavigationMenuViewportProps } from "../types.js";
|
||||
import { NavigationMenuViewportState } from "../navigation-menu.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import PresenceLayer from "../../utilities/presence-layer/presence-layer.svelte";
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import { Mounted } from "../../utilities/index.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
forceMount = false,
|
||||
child,
|
||||
children,
|
||||
...restProps
|
||||
}: NavigationMenuViewportProps = $props();
|
||||
|
||||
const viewportState = NavigationMenuViewportState.create({
|
||||
id: boxWith(() => id),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps(restProps, viewportState.props));
|
||||
</script>
|
||||
|
||||
<PresenceLayer open={forceMount || viewportState.open} ref={viewportState.opts.ref}>
|
||||
{#snippet presence()}
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<div {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
<Mounted bind:mounted={viewportState.mounted} />
|
||||
{/snippet}
|
||||
</PresenceLayer>
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { NavigationMenuViewportProps } from "../types.js";
|
||||
declare const NavigationMenuViewport: import("svelte").Component<NavigationMenuViewportProps, {}, "ref">;
|
||||
type NavigationMenuViewport = ReturnType<typeof NavigationMenuViewport>;
|
||||
export default NavigationMenuViewport;
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { boxWith, mergeProps } from "svelte-toolbelt";
|
||||
import type { NavigationMenuRootProps } from "../types.js";
|
||||
import { NavigationMenuRootState } from "../navigation-menu.svelte.js";
|
||||
import { createId } from "../../../internal/create-id.js";
|
||||
import { noop } from "../../../internal/noop.js";
|
||||
|
||||
const uid = $props.id();
|
||||
|
||||
let {
|
||||
child,
|
||||
children,
|
||||
id = createId(uid),
|
||||
ref = $bindable(null),
|
||||
value = $bindable(""),
|
||||
onValueChange = noop,
|
||||
delayDuration = 200,
|
||||
skipDelayDuration = 300,
|
||||
dir = "ltr",
|
||||
orientation = "horizontal",
|
||||
...restProps
|
||||
}: NavigationMenuRootProps = $props();
|
||||
|
||||
const rootState = NavigationMenuRootState.create({
|
||||
id: boxWith(() => id),
|
||||
value: boxWith(
|
||||
() => value,
|
||||
(v) => {
|
||||
value = v;
|
||||
onValueChange(v);
|
||||
}
|
||||
),
|
||||
delayDuration: boxWith(() => delayDuration),
|
||||
skipDelayDuration: boxWith(() => skipDelayDuration),
|
||||
dir: boxWith(() => dir),
|
||||
orientation: boxWith(() => orientation),
|
||||
ref: boxWith(
|
||||
() => ref,
|
||||
(v) => (ref = v)
|
||||
),
|
||||
});
|
||||
|
||||
const mergedProps = $derived(mergeProps({ "aria-label": "main" }, restProps, rootState.props));
|
||||
</script>
|
||||
|
||||
{#if child}
|
||||
{@render child({ props: mergedProps })}
|
||||
{:else}
|
||||
<nav {...mergedProps}>
|
||||
{@render children?.()}
|
||||
</nav>
|
||||
{/if}
|
||||
Generated
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
import type { NavigationMenuRootProps } from "../types.js";
|
||||
declare const NavigationMenu: import("svelte").Component<NavigationMenuRootProps, {}, "value" | "ref">;
|
||||
type NavigationMenu = ReturnType<typeof NavigationMenu>;
|
||||
export default NavigationMenu;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
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 { default as Sub } from "./components/navigation-menu-sub.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, NavigationMenuSubProps as SubProps, } from "./types.js";
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
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 { default as Sub } from "./components/navigation-menu-sub.svelte";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * as NavigationMenu from "./exports.js";
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * as NavigationMenu from "./exports.js";
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
/**
|
||||
* Based on Radix UI's Navigation Menu
|
||||
* https://www.radix-ui.com/docs/primitives/components/navigation-menu
|
||||
*/
|
||||
import { type AnyFn, type ReadableBox, type ReadableBoxedValues, type WithRefProps, type WritableBox, type WritableBoxedValues, DOMContext } from "svelte-toolbelt";
|
||||
import { Context } from "runed";
|
||||
import { type Snippet } from "svelte";
|
||||
import { SvelteMap } from "svelte/reactivity";
|
||||
import { type Direction, type Orientation } from "../../shared/index.js";
|
||||
import type { BitsFocusEvent, BitsKeyboardEvent, BitsMouseEvent, BitsPointerEvent, RefAttachment } from "../../internal/types.js";
|
||||
import type { FocusEventHandler, KeyboardEventHandler, MouseEventHandler, PointerEventHandler } from "svelte/elements";
|
||||
import { RovingFocusGroup } from "../../internal/roving-focus-group.js";
|
||||
export declare const NavigationMenuItemContext: Context<NavigationMenuItemState>;
|
||||
interface NavigationMenuProviderStateOpts extends ReadableBoxedValues<{
|
||||
dir: Direction;
|
||||
orientation: Orientation;
|
||||
}>, WritableBoxedValues<{
|
||||
rootNavigationMenuRef: HTMLElement | null;
|
||||
value: string;
|
||||
previousValue: string;
|
||||
}> {
|
||||
isRootMenu: boolean;
|
||||
onTriggerEnter: (itemValue: string, itemState: NavigationMenuItemState | null) => void;
|
||||
onTriggerLeave?: () => void;
|
||||
onContentEnter?: () => void;
|
||||
onContentLeave?: () => void;
|
||||
onItemSelect: (itemValue: string, itemState: NavigationMenuItemState | null) => void;
|
||||
onItemDismiss: () => void;
|
||||
}
|
||||
declare class NavigationMenuProviderState {
|
||||
static create(opts: NavigationMenuProviderStateOpts): NavigationMenuProviderState;
|
||||
readonly opts: NavigationMenuProviderStateOpts;
|
||||
indicatorTrackRef: WritableBox<HTMLElement | null>;
|
||||
viewportRef: WritableBox<HTMLElement | null>;
|
||||
viewportContent: SvelteMap<string, NavigationMenuItemState>;
|
||||
onTriggerEnter: NavigationMenuProviderStateOpts["onTriggerEnter"];
|
||||
onTriggerLeave: () => void;
|
||||
onContentEnter: () => void;
|
||||
onContentLeave: () => void;
|
||||
onItemSelect: NavigationMenuProviderStateOpts["onItemSelect"];
|
||||
onItemDismiss: NavigationMenuProviderStateOpts["onItemDismiss"];
|
||||
activeItem: NavigationMenuItemState | null;
|
||||
prevActiveItem: NavigationMenuItemState | null;
|
||||
constructor(opts: NavigationMenuProviderStateOpts);
|
||||
setActiveItem: (item: NavigationMenuItemState | null) => void;
|
||||
}
|
||||
interface NavigationMenuRootStateOpts extends WithRefProps, WritableBoxedValues<{
|
||||
value: string;
|
||||
}>, ReadableBoxedValues<{
|
||||
dir: Direction;
|
||||
orientation: Orientation;
|
||||
delayDuration: number;
|
||||
skipDelayDuration: number;
|
||||
}> {
|
||||
}
|
||||
export declare class NavigationMenuRootState {
|
||||
#private;
|
||||
static create(opts: NavigationMenuRootStateOpts): NavigationMenuRootState;
|
||||
readonly opts: NavigationMenuRootStateOpts;
|
||||
readonly attachment: RefAttachment;
|
||||
provider: NavigationMenuProviderState;
|
||||
previousValue: WritableBox<string>;
|
||||
isDelaySkipped: WritableBox<boolean>;
|
||||
constructor(opts: NavigationMenuRootStateOpts);
|
||||
setValue: (newValue: string, itemState: NavigationMenuItemState | null) => void;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly "data-orientation": Orientation;
|
||||
readonly dir: Direction;
|
||||
};
|
||||
}
|
||||
interface NavigationMenuSubStateOpts extends WithRefProps, WritableBoxedValues<{
|
||||
value: string;
|
||||
}>, ReadableBoxedValues<{
|
||||
orientation: Orientation;
|
||||
}> {
|
||||
}
|
||||
export declare class NavigationMenuSubState {
|
||||
static create(opts: NavigationMenuSubStateOpts): NavigationMenuSubState;
|
||||
readonly opts: NavigationMenuSubStateOpts;
|
||||
readonly context: NavigationMenuProviderState;
|
||||
previousValue: WritableBox<string>;
|
||||
readonly subProvider: NavigationMenuProviderState;
|
||||
readonly attachment: RefAttachment;
|
||||
constructor(opts: NavigationMenuSubStateOpts, context: NavigationMenuProviderState);
|
||||
setValue: (newValue: string, itemState: NavigationMenuItemState | null) => void;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly "data-orientation": Orientation;
|
||||
};
|
||||
}
|
||||
interface NavigationMenuListStateOpts extends WithRefProps {
|
||||
}
|
||||
export declare class NavigationMenuListState {
|
||||
static create(opts: NavigationMenuListStateOpts): NavigationMenuListState;
|
||||
wrapperId: WritableBox<string>;
|
||||
wrapperRef: WritableBox<HTMLElement | null>;
|
||||
readonly opts: NavigationMenuListStateOpts;
|
||||
readonly context: NavigationMenuProviderState;
|
||||
readonly attachment: RefAttachment;
|
||||
readonly wrapperAttachment: RefAttachment;
|
||||
listTriggers: HTMLElement[];
|
||||
readonly rovingFocusGroup: RovingFocusGroup;
|
||||
wrapperMounted: boolean;
|
||||
constructor(opts: NavigationMenuListStateOpts, context: NavigationMenuProviderState);
|
||||
registerTrigger(trigger: HTMLElement | null): () => void;
|
||||
readonly wrapperProps: {
|
||||
readonly id: string;
|
||||
};
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly "data-orientation": Orientation;
|
||||
};
|
||||
}
|
||||
interface NavigationMenuItemStateOpts extends WithRefProps, ReadableBoxedValues<{
|
||||
value: string;
|
||||
openOnHover: boolean;
|
||||
}> {
|
||||
}
|
||||
export declare class NavigationMenuItemState {
|
||||
#private;
|
||||
static create(opts: NavigationMenuItemStateOpts): NavigationMenuItemState;
|
||||
readonly opts: NavigationMenuItemStateOpts;
|
||||
readonly attachment: RefAttachment;
|
||||
readonly listContext: NavigationMenuListState;
|
||||
contentNode: HTMLElement | null;
|
||||
triggerNode: HTMLElement | null;
|
||||
focusProxyNode: HTMLElement | null;
|
||||
restoreContentTabOrder: AnyFn;
|
||||
wasEscapeClose: boolean;
|
||||
readonly contentId: string | undefined;
|
||||
readonly triggerId: string | undefined;
|
||||
contentChildren: ReadableBox<Snippet | undefined>;
|
||||
contentChild: ReadableBox<Snippet<[{
|
||||
props: Record<string, unknown>;
|
||||
}]> | undefined>;
|
||||
contentProps: ReadableBox<Record<string, unknown>>;
|
||||
domContext: DOMContext;
|
||||
constructor(opts: NavigationMenuItemStateOpts, listContext: NavigationMenuListState);
|
||||
onEntryKeydown: (side?: "start" | "end") => void;
|
||||
onFocusProxyEnter: (side?: "start" | "end") => void;
|
||||
onRootContentClose: () => void;
|
||||
onContentFocusOutside: () => void;
|
||||
props: {
|
||||
readonly id: string;
|
||||
};
|
||||
}
|
||||
interface NavigationMenuTriggerStateOpts extends WithRefProps, ReadableBoxedValues<{
|
||||
disabled: boolean | null | undefined;
|
||||
}> {
|
||||
}
|
||||
export declare class NavigationMenuTriggerState {
|
||||
static create(opts: NavigationMenuTriggerStateOpts): NavigationMenuTriggerState;
|
||||
readonly opts: NavigationMenuTriggerStateOpts;
|
||||
readonly attachment: RefAttachment;
|
||||
focusProxyId: WritableBox<string>;
|
||||
focusProxyRef: WritableBox<HTMLElement | null>;
|
||||
readonly focusProxyAttachment: RefAttachment;
|
||||
context: NavigationMenuProviderState;
|
||||
itemContext: NavigationMenuItemState;
|
||||
listContext: NavigationMenuListState;
|
||||
hasPointerMoveOpened: WritableBox<boolean>;
|
||||
wasClickClose: boolean;
|
||||
focusProxyMounted: boolean;
|
||||
readonly open: boolean;
|
||||
constructor(opts: NavigationMenuTriggerStateOpts, context: {
|
||||
provider: NavigationMenuProviderState;
|
||||
item: NavigationMenuItemState;
|
||||
list: NavigationMenuListState;
|
||||
sub: NavigationMenuSubState | null;
|
||||
});
|
||||
onpointerenter: (_: BitsPointerEvent<HTMLButtonElement>) => void;
|
||||
onpointermove: PointerEventHandler<HTMLElement>;
|
||||
onpointerleave: PointerEventHandler<HTMLElement>;
|
||||
onclick: MouseEventHandler<HTMLButtonElement>;
|
||||
onkeydown: KeyboardEventHandler<HTMLButtonElement>;
|
||||
focusProxyOnFocus: FocusEventHandler<HTMLElement>;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly disabled: boolean | null | undefined;
|
||||
readonly "data-disabled": "" | undefined;
|
||||
readonly "data-state": "open" | "closed";
|
||||
readonly "data-value": string;
|
||||
readonly "aria-expanded": "true" | "false";
|
||||
readonly "aria-controls": string | undefined;
|
||||
readonly onpointermove: PointerEventHandler<HTMLElement>;
|
||||
readonly onpointerleave: PointerEventHandler<HTMLElement>;
|
||||
readonly onpointerenter: (_: BitsPointerEvent<HTMLButtonElement>) => void;
|
||||
readonly onclick: MouseEventHandler<HTMLButtonElement>;
|
||||
readonly onkeydown: KeyboardEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
readonly focusProxyProps: {
|
||||
readonly id: string;
|
||||
readonly tabindex: 0;
|
||||
readonly onfocus: FocusEventHandler<HTMLElement>;
|
||||
};
|
||||
}
|
||||
interface NavigationMenuLinkStateOpts extends WithRefProps, ReadableBoxedValues<{
|
||||
active: boolean;
|
||||
onSelect: (e: Event) => void;
|
||||
}> {
|
||||
}
|
||||
export declare class NavigationMenuLinkState {
|
||||
#private;
|
||||
static create(opts: NavigationMenuLinkStateOpts): NavigationMenuLinkState;
|
||||
readonly opts: NavigationMenuLinkStateOpts;
|
||||
readonly context: {
|
||||
provider: NavigationMenuProviderState;
|
||||
item: NavigationMenuItemState;
|
||||
};
|
||||
readonly attachment: RefAttachment;
|
||||
isFocused: boolean;
|
||||
constructor(opts: NavigationMenuLinkStateOpts, context: {
|
||||
provider: NavigationMenuProviderState;
|
||||
item: NavigationMenuItemState;
|
||||
});
|
||||
onclick: (e: BitsMouseEvent<HTMLAnchorElement>) => void;
|
||||
onkeydown: (e: BitsKeyboardEvent) => void;
|
||||
onfocus: (_: BitsFocusEvent) => void;
|
||||
onblur: (_: BitsFocusEvent) => void;
|
||||
onpointerenter: PointerEventHandler<HTMLAnchorElement>;
|
||||
onpointermove: PointerEventHandler<HTMLElement>;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly "data-active": "" | undefined;
|
||||
readonly "aria-current": "page" | undefined;
|
||||
readonly "data-focused": "" | undefined;
|
||||
readonly onclick: (e: BitsMouseEvent<HTMLAnchorElement>) => void;
|
||||
readonly onkeydown: (e: BitsKeyboardEvent) => void;
|
||||
readonly onfocus: (_: BitsFocusEvent) => void;
|
||||
readonly onblur: (_: BitsFocusEvent) => void;
|
||||
readonly onpointerenter: PointerEventHandler<HTMLAnchorElement>;
|
||||
readonly onpointermove: PointerEventHandler<HTMLElement>;
|
||||
};
|
||||
}
|
||||
interface NavigationMenuIndicatorStateOpts extends WithRefProps {
|
||||
}
|
||||
export declare class NavigationMenuIndicatorState {
|
||||
static create(): NavigationMenuIndicatorState;
|
||||
readonly context: NavigationMenuProviderState;
|
||||
readonly isVisible: boolean;
|
||||
constructor(context: NavigationMenuProviderState);
|
||||
}
|
||||
export declare class NavigationMenuIndicatorImplState {
|
||||
static create(opts: NavigationMenuIndicatorStateOpts): NavigationMenuIndicatorImplState;
|
||||
readonly opts: NavigationMenuIndicatorStateOpts;
|
||||
readonly attachment: RefAttachment;
|
||||
context: NavigationMenuProviderState;
|
||||
listContext: NavigationMenuListState;
|
||||
position: {
|
||||
size: number;
|
||||
offset: number;
|
||||
} | null;
|
||||
readonly isHorizontal: boolean;
|
||||
readonly isVisible: boolean;
|
||||
readonly activeTrigger: HTMLElement | null;
|
||||
readonly shouldRender: boolean;
|
||||
constructor(opts: NavigationMenuIndicatorStateOpts, context: {
|
||||
provider: NavigationMenuProviderState;
|
||||
list: NavigationMenuListState;
|
||||
});
|
||||
handlePositionChange: () => void;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly "data-state": "hidden" | "visible";
|
||||
readonly "data-orientation": Orientation;
|
||||
readonly style: {
|
||||
readonly left: number;
|
||||
readonly width: string;
|
||||
readonly transform: string;
|
||||
readonly position: "absolute";
|
||||
} | {
|
||||
readonly top: number;
|
||||
readonly height: string;
|
||||
readonly transform: string;
|
||||
readonly position: "absolute";
|
||||
};
|
||||
};
|
||||
}
|
||||
interface NavigationMenuContentStateOpts extends WithRefProps {
|
||||
}
|
||||
export declare class NavigationMenuContentState {
|
||||
static create(opts: NavigationMenuContentStateOpts): NavigationMenuContentState;
|
||||
readonly opts: NavigationMenuContentStateOpts;
|
||||
readonly context: NavigationMenuProviderState;
|
||||
readonly itemContext: NavigationMenuItemState;
|
||||
readonly listContext: NavigationMenuListState;
|
||||
readonly attachment: RefAttachment;
|
||||
mounted: boolean;
|
||||
readonly open: boolean;
|
||||
readonly value: string;
|
||||
readonly isLastActiveValue: boolean;
|
||||
constructor(opts: NavigationMenuContentStateOpts, context: {
|
||||
provider: NavigationMenuProviderState;
|
||||
item: NavigationMenuItemState;
|
||||
list: NavigationMenuListState;
|
||||
});
|
||||
onpointerenter: (_: BitsPointerEvent) => void;
|
||||
onpointerleave: PointerEventHandler<HTMLElement>;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly onpointerenter: (_: BitsPointerEvent) => void;
|
||||
readonly onpointerleave: PointerEventHandler<HTMLElement>;
|
||||
};
|
||||
}
|
||||
type MotionAttribute = "to-start" | "to-end" | "from-start" | "from-end";
|
||||
interface NavigationMenuContentImplStateOpts extends WithRefProps {
|
||||
}
|
||||
export declare class NavigationMenuContentImplState {
|
||||
static create(opts: NavigationMenuContentImplStateOpts, itemState?: NavigationMenuItemState): NavigationMenuContentImplState;
|
||||
readonly opts: NavigationMenuContentImplStateOpts;
|
||||
readonly itemContext: NavigationMenuItemState;
|
||||
readonly context: NavigationMenuProviderState;
|
||||
readonly listContext: NavigationMenuListState;
|
||||
readonly attachment: RefAttachment;
|
||||
prevMotionAttribute: MotionAttribute | null;
|
||||
readonly motionAttribute: MotionAttribute | null;
|
||||
domContext: DOMContext;
|
||||
constructor(opts: NavigationMenuContentImplStateOpts, itemContext: NavigationMenuItemState);
|
||||
onFocusOutside: (e: Event) => void;
|
||||
onInteractOutside: (e: PointerEvent) => void;
|
||||
onkeydown: (e: BitsKeyboardEvent) => void;
|
||||
onEscapeKeydown: (_: KeyboardEvent) => void;
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly "aria-labelledby": string | undefined;
|
||||
readonly "data-motion": MotionAttribute | undefined;
|
||||
readonly "data-orientation": Orientation;
|
||||
readonly "data-state": "open" | "closed";
|
||||
readonly onkeydown: (e: BitsKeyboardEvent) => void;
|
||||
};
|
||||
}
|
||||
interface NavigationMenuViewportStateOpts extends WithRefProps {
|
||||
}
|
||||
export declare class NavigationMenuViewportState {
|
||||
static create(opts: NavigationMenuViewportStateOpts): NavigationMenuViewportState;
|
||||
readonly opts: NavigationMenuViewportStateOpts;
|
||||
readonly context: NavigationMenuProviderState;
|
||||
readonly attachment: RefAttachment;
|
||||
readonly open: boolean;
|
||||
readonly viewportWidth: string | undefined;
|
||||
readonly viewportHeight: string | undefined;
|
||||
readonly activeContentValue: string;
|
||||
size: {
|
||||
width: number;
|
||||
height: number;
|
||||
} | null;
|
||||
contentNode: HTMLElement | null;
|
||||
mounted: boolean;
|
||||
constructor(opts: NavigationMenuViewportStateOpts, context: NavigationMenuProviderState);
|
||||
readonly props: {
|
||||
readonly id: string;
|
||||
readonly "data-state": "open" | "closed";
|
||||
readonly "data-orientation": Orientation;
|
||||
readonly style: {
|
||||
readonly pointerEvents: "none" | undefined;
|
||||
readonly "--bits-navigation-menu-viewport-width": string | undefined;
|
||||
readonly "--bits-navigation-menu-viewport-height": string | undefined;
|
||||
};
|
||||
readonly onpointerenter: () => void;
|
||||
readonly onpointerleave: () => void;
|
||||
};
|
||||
}
|
||||
export {};
|
||||
+888
@@ -0,0 +1,888 @@
|
||||
/**
|
||||
* Based on Radix UI's Navigation Menu
|
||||
* https://www.radix-ui.com/docs/primitives/components/navigation-menu
|
||||
*/
|
||||
import { afterSleep, afterTick, attachRef, DOMContext, getWindow, simpleBox, boxWith, } from "svelte-toolbelt";
|
||||
import { Context, useDebounce, watch } from "runed";
|
||||
import { untrack } from "svelte";
|
||||
import { SvelteMap } from "svelte/reactivity";
|
||||
import { useId } from "../../shared/index.js";
|
||||
import { createBitsAttrs, boolToStr, boolToEmptyStrOrUndef, getDataOpenClosed, } from "../../internal/attrs.js";
|
||||
import { noop } from "../../internal/noop.js";
|
||||
import { getTabbableCandidates } from "../../internal/focus.js";
|
||||
import { kbd } from "../../internal/kbd.js";
|
||||
import { CustomEventDispatcher } from "../../internal/events.js";
|
||||
import { useArrowNavigation } from "../../internal/use-arrow-navigation.js";
|
||||
import { boxAutoReset } from "../../internal/box-auto-reset.svelte.js";
|
||||
import { isElement } from "../../internal/is.js";
|
||||
import { RovingFocusGroup } from "../../internal/roving-focus-group.js";
|
||||
import { SvelteResizeObserver } from "../../internal/svelte-resize-observer.svelte.js";
|
||||
const navigationMenuAttrs = createBitsAttrs({
|
||||
component: "navigation-menu",
|
||||
parts: [
|
||||
"root",
|
||||
"sub",
|
||||
"item",
|
||||
"list",
|
||||
"trigger",
|
||||
"content",
|
||||
"link",
|
||||
"viewport",
|
||||
"menu",
|
||||
"indicator",
|
||||
],
|
||||
});
|
||||
const NavigationMenuProviderContext = new Context("NavigationMenu.Root");
|
||||
export const NavigationMenuItemContext = new Context("NavigationMenu.Item");
|
||||
const NavigationMenuListContext = new Context("NavigationMenu.List");
|
||||
const NavigationMenuContentContext = new Context("NavigationMenu.Content");
|
||||
const NavigationMenuSubContext = new Context("NavigationMenu.Sub");
|
||||
class NavigationMenuProviderState {
|
||||
static create(opts) {
|
||||
return NavigationMenuProviderContext.set(new NavigationMenuProviderState(opts));
|
||||
}
|
||||
opts;
|
||||
indicatorTrackRef = simpleBox(null);
|
||||
viewportRef = simpleBox(null);
|
||||
viewportContent = new SvelteMap();
|
||||
onTriggerEnter;
|
||||
onTriggerLeave = noop;
|
||||
onContentEnter = noop;
|
||||
onContentLeave = noop;
|
||||
onItemSelect;
|
||||
onItemDismiss;
|
||||
activeItem = null;
|
||||
prevActiveItem = null;
|
||||
constructor(opts) {
|
||||
this.opts = opts;
|
||||
this.onTriggerEnter = opts.onTriggerEnter;
|
||||
this.onTriggerLeave = opts.onTriggerLeave ?? noop;
|
||||
this.onContentEnter = opts.onContentEnter ?? noop;
|
||||
this.onContentLeave = opts.onContentLeave ?? noop;
|
||||
this.onItemDismiss = opts.onItemDismiss;
|
||||
this.onItemSelect = opts.onItemSelect;
|
||||
}
|
||||
setActiveItem = (item) => {
|
||||
this.prevActiveItem = this.activeItem;
|
||||
this.activeItem = item;
|
||||
};
|
||||
}
|
||||
export class NavigationMenuRootState {
|
||||
static create(opts) {
|
||||
return new NavigationMenuRootState(opts);
|
||||
}
|
||||
opts;
|
||||
attachment;
|
||||
provider;
|
||||
previousValue = simpleBox("");
|
||||
isDelaySkipped;
|
||||
#derivedDelay = $derived.by(() => {
|
||||
const isOpen = this.opts?.value?.current !== "";
|
||||
if (isOpen || this.isDelaySkipped.current) {
|
||||
// 150 for user to switch trigger or move into content view
|
||||
return 100;
|
||||
}
|
||||
else {
|
||||
return this.opts.delayDuration.current;
|
||||
}
|
||||
});
|
||||
constructor(opts) {
|
||||
this.opts = opts;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
this.isDelaySkipped = boxAutoReset(false, {
|
||||
afterMs: this.opts.skipDelayDuration.current,
|
||||
getWindow: () => getWindow(opts.ref.current),
|
||||
});
|
||||
this.provider = NavigationMenuProviderState.create({
|
||||
value: this.opts.value,
|
||||
previousValue: this.previousValue,
|
||||
dir: this.opts.dir,
|
||||
orientation: this.opts.orientation,
|
||||
rootNavigationMenuRef: this.opts.ref,
|
||||
isRootMenu: true,
|
||||
onTriggerEnter: (itemValue, itemState) => {
|
||||
this.#onTriggerEnter(itemValue, itemState);
|
||||
},
|
||||
onTriggerLeave: this.#onTriggerLeave,
|
||||
onContentEnter: this.#onContentEnter,
|
||||
onContentLeave: this.#onContentLeave,
|
||||
onItemSelect: this.#onItemSelect,
|
||||
onItemDismiss: this.#onItemDismiss,
|
||||
});
|
||||
}
|
||||
#debouncedFn = useDebounce((val, itemState) => {
|
||||
// passing `undefined` meant to reset the debounce timer
|
||||
if (typeof val === "string") {
|
||||
this.setValue(val, itemState);
|
||||
}
|
||||
}, () => this.#derivedDelay);
|
||||
#onTriggerEnter = (itemValue, itemState) => {
|
||||
this.#debouncedFn(itemValue, itemState);
|
||||
};
|
||||
#onTriggerLeave = () => {
|
||||
this.isDelaySkipped.current = false;
|
||||
this.#debouncedFn("", null);
|
||||
};
|
||||
#onContentEnter = () => {
|
||||
this.#debouncedFn(undefined, null);
|
||||
};
|
||||
#onContentLeave = () => {
|
||||
if (this.provider.activeItem &&
|
||||
this.provider.activeItem.opts.openOnHover.current === false) {
|
||||
return;
|
||||
}
|
||||
this.#debouncedFn("", null);
|
||||
};
|
||||
#onItemSelect = (itemValue, itemState) => {
|
||||
this.setValue(itemValue, itemState);
|
||||
};
|
||||
#onItemDismiss = () => {
|
||||
this.setValue("", null);
|
||||
};
|
||||
setValue = (newValue, itemState) => {
|
||||
this.previousValue.current = this.opts.value.current;
|
||||
this.opts.value.current = newValue;
|
||||
this.provider.setActiveItem(itemState);
|
||||
// When all menus are closed, we want to reset previousValue to prevent
|
||||
// weird transitions from old positions when opening fresh
|
||||
if (newValue === "") {
|
||||
this.previousValue.current = "";
|
||||
}
|
||||
};
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"data-orientation": this.opts.orientation.current,
|
||||
dir: this.opts.dir.current,
|
||||
[navigationMenuAttrs.root]: "",
|
||||
[navigationMenuAttrs.menu]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class NavigationMenuSubState {
|
||||
static create(opts) {
|
||||
return new NavigationMenuSubState(opts, NavigationMenuProviderContext.get());
|
||||
}
|
||||
opts;
|
||||
context;
|
||||
previousValue = simpleBox("");
|
||||
subProvider;
|
||||
attachment;
|
||||
constructor(opts, context) {
|
||||
this.opts = opts;
|
||||
this.context = context;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
this.subProvider = NavigationMenuProviderState.create({
|
||||
isRootMenu: false,
|
||||
value: this.opts.value,
|
||||
dir: this.context.opts.dir,
|
||||
orientation: this.opts.orientation,
|
||||
rootNavigationMenuRef: this.opts.ref,
|
||||
onTriggerEnter: this.setValue,
|
||||
onItemSelect: this.setValue,
|
||||
onItemDismiss: () => this.setValue("", null),
|
||||
previousValue: this.previousValue,
|
||||
});
|
||||
}
|
||||
setValue = (newValue, itemState) => {
|
||||
this.previousValue.current = this.opts.value.current;
|
||||
this.opts.value.current = newValue;
|
||||
this.subProvider.setActiveItem(itemState);
|
||||
// When all menus are closed, we want to reset previousValue to prevent
|
||||
// weird transitions from old positions when opening fresh
|
||||
if (newValue === "") {
|
||||
this.previousValue.current = "";
|
||||
}
|
||||
};
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"data-orientation": this.opts.orientation.current,
|
||||
[navigationMenuAttrs.sub]: "",
|
||||
[navigationMenuAttrs.menu]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class NavigationMenuListState {
|
||||
static create(opts) {
|
||||
return NavigationMenuListContext.set(new NavigationMenuListState(opts, NavigationMenuProviderContext.get()));
|
||||
}
|
||||
wrapperId = simpleBox(useId());
|
||||
wrapperRef = simpleBox(null);
|
||||
opts;
|
||||
context;
|
||||
attachment;
|
||||
wrapperAttachment = attachRef(this.wrapperRef, (v) => (this.context.indicatorTrackRef.current = v));
|
||||
listTriggers = $state.raw([]);
|
||||
rovingFocusGroup;
|
||||
wrapperMounted = $state(false);
|
||||
constructor(opts, context) {
|
||||
this.opts = opts;
|
||||
this.context = context;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
this.rovingFocusGroup = new RovingFocusGroup({
|
||||
rootNode: opts.ref,
|
||||
candidateSelector: `${navigationMenuAttrs.selector("trigger")}:not([data-disabled]), ${navigationMenuAttrs.selector("link")}:not([data-disabled])`,
|
||||
loop: boxWith(() => false),
|
||||
orientation: this.context.opts.orientation,
|
||||
});
|
||||
}
|
||||
registerTrigger(trigger) {
|
||||
if (trigger)
|
||||
this.listTriggers.push(trigger);
|
||||
return () => {
|
||||
this.listTriggers = this.listTriggers.filter((t) => t.id !== trigger.id);
|
||||
};
|
||||
}
|
||||
wrapperProps = $derived.by(() => ({
|
||||
id: this.wrapperId.current,
|
||||
...this.wrapperAttachment,
|
||||
}));
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"data-orientation": this.context.opts.orientation.current,
|
||||
[navigationMenuAttrs.list]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class NavigationMenuItemState {
|
||||
static create(opts) {
|
||||
return NavigationMenuItemContext.set(new NavigationMenuItemState(opts, NavigationMenuListContext.get()));
|
||||
}
|
||||
opts;
|
||||
attachment;
|
||||
listContext;
|
||||
contentNode = $state(null);
|
||||
triggerNode = $state(null);
|
||||
focusProxyNode = $state(null);
|
||||
restoreContentTabOrder = noop;
|
||||
wasEscapeClose = false;
|
||||
contentId = $derived.by(() => this.contentNode?.id);
|
||||
triggerId = $derived.by(() => this.triggerNode?.id);
|
||||
contentChildren = simpleBox(undefined);
|
||||
contentChild = simpleBox(undefined);
|
||||
contentProps = simpleBox({});
|
||||
domContext;
|
||||
constructor(opts, listContext) {
|
||||
this.opts = opts;
|
||||
this.listContext = listContext;
|
||||
this.domContext = new DOMContext(opts.ref);
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
#handleContentEntry = (side = "start") => {
|
||||
if (!this.contentNode)
|
||||
return;
|
||||
this.restoreContentTabOrder();
|
||||
const candidates = getTabbableCandidates(this.contentNode);
|
||||
if (candidates.length)
|
||||
focusFirst(side === "start" ? candidates : candidates.reverse(), () => this.domContext.getActiveElement());
|
||||
};
|
||||
#handleContentExit = () => {
|
||||
if (!this.contentNode)
|
||||
return;
|
||||
const candidates = getTabbableCandidates(this.contentNode);
|
||||
if (candidates.length)
|
||||
this.restoreContentTabOrder = removeFromTabOrder(candidates);
|
||||
};
|
||||
onEntryKeydown = this.#handleContentEntry;
|
||||
onFocusProxyEnter = this.#handleContentEntry;
|
||||
onRootContentClose = this.#handleContentExit;
|
||||
onContentFocusOutside = this.#handleContentExit;
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
[navigationMenuAttrs.item]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class NavigationMenuTriggerState {
|
||||
static create(opts) {
|
||||
return new NavigationMenuTriggerState(opts, {
|
||||
provider: NavigationMenuProviderContext.get(),
|
||||
item: NavigationMenuItemContext.get(),
|
||||
list: NavigationMenuListContext.get(),
|
||||
sub: NavigationMenuSubContext.getOr(null),
|
||||
});
|
||||
}
|
||||
opts;
|
||||
attachment;
|
||||
focusProxyId = simpleBox(useId());
|
||||
focusProxyRef = simpleBox(null);
|
||||
focusProxyAttachment = attachRef(this.focusProxyRef, (v) => (this.itemContext.focusProxyNode = v));
|
||||
context;
|
||||
itemContext;
|
||||
listContext;
|
||||
hasPointerMoveOpened = simpleBox(false);
|
||||
wasClickClose = false;
|
||||
focusProxyMounted = $state(false);
|
||||
open = $derived.by(() => this.itemContext.opts.value.current === this.context.opts.value.current);
|
||||
constructor(opts, context) {
|
||||
this.opts = opts;
|
||||
this.attachment = attachRef(this.opts.ref, (v) => (this.itemContext.triggerNode = v));
|
||||
this.hasPointerMoveOpened = boxAutoReset(false, {
|
||||
afterMs: 300,
|
||||
getWindow: () => getWindow(opts.ref.current),
|
||||
});
|
||||
this.context = context.provider;
|
||||
this.itemContext = context.item;
|
||||
this.listContext = context.list;
|
||||
watch(() => this.opts.ref.current, () => {
|
||||
const node = this.opts.ref.current;
|
||||
if (!node)
|
||||
return;
|
||||
return this.listContext.registerTrigger(node);
|
||||
});
|
||||
}
|
||||
onpointerenter = (_) => {
|
||||
this.wasClickClose = false;
|
||||
this.itemContext.wasEscapeClose = false;
|
||||
};
|
||||
onpointermove = whenMouse(() => {
|
||||
if (this.opts.disabled.current ||
|
||||
this.wasClickClose ||
|
||||
this.itemContext.wasEscapeClose ||
|
||||
this.hasPointerMoveOpened.current ||
|
||||
!this.itemContext.opts.openOnHover.current) {
|
||||
return;
|
||||
}
|
||||
this.context.onTriggerEnter(this.itemContext.opts.value.current, this.itemContext);
|
||||
this.hasPointerMoveOpened.current = true;
|
||||
});
|
||||
onpointerleave = whenMouse(() => {
|
||||
if (this.opts.disabled.current || !this.itemContext.opts.openOnHover.current)
|
||||
return;
|
||||
this.context.onTriggerLeave();
|
||||
this.hasPointerMoveOpened.current = false;
|
||||
});
|
||||
onclick = () => {
|
||||
// if opened via pointer move, we prevent the click event
|
||||
if (this.hasPointerMoveOpened.current)
|
||||
return;
|
||||
const shouldClose = this.open &&
|
||||
(!this.itemContext.opts.openOnHover.current || this.context.opts.isRootMenu);
|
||||
if (shouldClose) {
|
||||
this.context.onItemSelect("", null);
|
||||
}
|
||||
else if (!this.open) {
|
||||
this.context.onItemSelect(this.itemContext.opts.value.current, this.itemContext);
|
||||
}
|
||||
this.wasClickClose = shouldClose;
|
||||
};
|
||||
onkeydown = (e) => {
|
||||
const verticalEntryKey = this.context.opts.dir.current === "rtl" ? kbd.ARROW_LEFT : kbd.ARROW_RIGHT;
|
||||
const entryKey = { horizontal: kbd.ARROW_DOWN, vertical: verticalEntryKey }[this.context.opts.orientation.current];
|
||||
if (this.open && e.key === entryKey) {
|
||||
this.itemContext.onEntryKeydown();
|
||||
// prevent focus group from handling the event
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
this.itemContext.listContext.rovingFocusGroup.handleKeydown(this.opts.ref.current, e);
|
||||
};
|
||||
focusProxyOnFocus = (e) => {
|
||||
const content = this.itemContext.contentNode;
|
||||
const prevFocusedElement = e.relatedTarget;
|
||||
const wasTriggerFocused = this.opts.ref.current && prevFocusedElement === this.opts.ref.current;
|
||||
const wasFocusFromContent = content?.contains(prevFocusedElement);
|
||||
if (wasTriggerFocused || !wasFocusFromContent) {
|
||||
this.itemContext.onFocusProxyEnter(wasTriggerFocused ? "start" : "end");
|
||||
}
|
||||
};
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
disabled: this.opts.disabled.current,
|
||||
"data-disabled": boolToEmptyStrOrUndef(Boolean(this.opts.disabled.current)),
|
||||
"data-state": getDataOpenClosed(this.open),
|
||||
"data-value": this.itemContext.opts.value.current,
|
||||
"aria-expanded": boolToStr(this.open),
|
||||
"aria-controls": this.itemContext.contentId,
|
||||
[navigationMenuAttrs.trigger]: "",
|
||||
onpointermove: this.onpointermove,
|
||||
onpointerleave: this.onpointerleave,
|
||||
onpointerenter: this.onpointerenter,
|
||||
onclick: this.onclick,
|
||||
onkeydown: this.onkeydown,
|
||||
...this.attachment,
|
||||
}));
|
||||
focusProxyProps = $derived.by(() => ({
|
||||
id: this.focusProxyId.current,
|
||||
tabindex: 0,
|
||||
onfocus: this.focusProxyOnFocus,
|
||||
...this.focusProxyAttachment,
|
||||
}));
|
||||
}
|
||||
const LINK_SELECT_EVENT = new CustomEventDispatcher("bitsLinkSelect", {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
const ROOT_CONTENT_DISMISS_EVENT = new CustomEventDispatcher("bitsRootContentDismiss", {
|
||||
cancelable: true,
|
||||
bubbles: true,
|
||||
});
|
||||
export class NavigationMenuLinkState {
|
||||
static create(opts) {
|
||||
return new NavigationMenuLinkState(opts, {
|
||||
provider: NavigationMenuProviderContext.get(),
|
||||
item: NavigationMenuItemContext.get(),
|
||||
});
|
||||
}
|
||||
opts;
|
||||
context;
|
||||
attachment;
|
||||
isFocused = $state(false);
|
||||
constructor(opts, context) {
|
||||
this.opts = opts;
|
||||
this.context = context;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
}
|
||||
onclick = (e) => {
|
||||
const currTarget = e.currentTarget;
|
||||
LINK_SELECT_EVENT.listen(currTarget, (e) => this.opts.onSelect.current(e), { once: true });
|
||||
const linkSelectEvent = LINK_SELECT_EVENT.dispatch(currTarget);
|
||||
if (!linkSelectEvent.defaultPrevented && !e.metaKey) {
|
||||
ROOT_CONTENT_DISMISS_EVENT.dispatch(currTarget);
|
||||
}
|
||||
};
|
||||
onkeydown = (e) => {
|
||||
if (this.context.item.contentNode)
|
||||
return;
|
||||
this.context.item.listContext.rovingFocusGroup.handleKeydown(this.opts.ref.current, e);
|
||||
};
|
||||
onfocus = (_) => {
|
||||
this.isFocused = true;
|
||||
};
|
||||
onblur = (_) => {
|
||||
this.isFocused = false;
|
||||
};
|
||||
#handlePointerDismiss = () => {
|
||||
// only close submenu if this link is not inside the currently open submenu content
|
||||
const currentlyOpenValue = this.context.provider.opts.value.current;
|
||||
const isInsideOpenSubmenu = this.context.item.opts.value.current === currentlyOpenValue;
|
||||
const activeItem = this.context.item.listContext.context.activeItem;
|
||||
if (activeItem && !activeItem.opts.openOnHover.current)
|
||||
return;
|
||||
if (currentlyOpenValue && !isInsideOpenSubmenu) {
|
||||
this.context.provider.onItemDismiss();
|
||||
}
|
||||
};
|
||||
onpointerenter = () => {
|
||||
this.#handlePointerDismiss();
|
||||
};
|
||||
onpointermove = whenMouse(() => {
|
||||
this.#handlePointerDismiss();
|
||||
});
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"data-active": this.opts.active.current ? "" : undefined,
|
||||
"aria-current": this.opts.active.current ? "page" : undefined,
|
||||
"data-focused": this.isFocused ? "" : undefined,
|
||||
onclick: this.onclick,
|
||||
onkeydown: this.onkeydown,
|
||||
onfocus: this.onfocus,
|
||||
onblur: this.onblur,
|
||||
onpointerenter: this.onpointerenter,
|
||||
onpointermove: this.onpointermove,
|
||||
[navigationMenuAttrs.link]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class NavigationMenuIndicatorState {
|
||||
static create() {
|
||||
return new NavigationMenuIndicatorState(NavigationMenuProviderContext.get());
|
||||
}
|
||||
context;
|
||||
isVisible = $derived.by(() => Boolean(this.context.opts.value.current));
|
||||
constructor(context) {
|
||||
this.context = context;
|
||||
}
|
||||
}
|
||||
export class NavigationMenuIndicatorImplState {
|
||||
static create(opts) {
|
||||
return new NavigationMenuIndicatorImplState(opts, {
|
||||
provider: NavigationMenuProviderContext.get(),
|
||||
list: NavigationMenuListContext.get(),
|
||||
});
|
||||
}
|
||||
opts;
|
||||
attachment;
|
||||
context;
|
||||
listContext;
|
||||
position = $state.raw(null);
|
||||
isHorizontal = $derived.by(() => this.context.opts.orientation.current === "horizontal");
|
||||
isVisible = $derived.by(() => !!this.context.opts.value.current);
|
||||
activeTrigger = $derived.by(() => {
|
||||
const items = this.listContext.listTriggers;
|
||||
const triggerNode = items.find((item) => item.getAttribute("data-value") === this.context.opts.value.current);
|
||||
return triggerNode ?? null;
|
||||
});
|
||||
shouldRender = $derived.by(() => this.position !== null);
|
||||
constructor(opts, context) {
|
||||
this.opts = opts;
|
||||
this.context = context.provider;
|
||||
this.listContext = context.list;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
new SvelteResizeObserver(() => this.activeTrigger, this.handlePositionChange);
|
||||
new SvelteResizeObserver(() => this.context.indicatorTrackRef.current, 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(() => ({
|
||||
id: this.opts.id.current,
|
||||
"data-state": this.isVisible ? "visible" : "hidden",
|
||||
"data-orientation": this.context.opts.orientation.current,
|
||||
style: {
|
||||
position: "absolute",
|
||||
...(this.isHorizontal
|
||||
? {
|
||||
left: 0,
|
||||
width: `${this.position?.size}px`,
|
||||
transform: `translateX(${this.position?.offset}px)`,
|
||||
}
|
||||
: {
|
||||
top: 0,
|
||||
height: `${this.position?.size}px`,
|
||||
transform: `translateY(${this.position?.offset}px)`,
|
||||
}),
|
||||
},
|
||||
[navigationMenuAttrs.indicator]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class NavigationMenuContentState {
|
||||
static create(opts) {
|
||||
return NavigationMenuContentContext.set(new NavigationMenuContentState(opts, {
|
||||
provider: NavigationMenuProviderContext.get(),
|
||||
item: NavigationMenuItemContext.get(),
|
||||
list: NavigationMenuListContext.get(),
|
||||
}));
|
||||
}
|
||||
opts;
|
||||
context;
|
||||
itemContext;
|
||||
listContext;
|
||||
attachment;
|
||||
mounted = $state(false);
|
||||
open = $derived.by(() => this.itemContext.opts.value.current === this.context.opts.value.current);
|
||||
value = $derived.by(() => this.itemContext.opts.value.current);
|
||||
// We persist the last active content value as the viewport may be animating out
|
||||
// and we want the content to remain mounted for the lifecycle of the viewport.
|
||||
isLastActiveValue = $derived.by(() => {
|
||||
if (this.context.viewportRef.current) {
|
||||
if (!this.context.opts.value.current && this.context.opts.previousValue.current) {
|
||||
return (this.context.opts.previousValue.current === this.itemContext.opts.value.current);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
constructor(opts, context) {
|
||||
this.opts = opts;
|
||||
this.context = context.provider;
|
||||
this.itemContext = context.item;
|
||||
this.listContext = context.list;
|
||||
this.attachment = attachRef(this.opts.ref, (v) => (this.itemContext.contentNode = v));
|
||||
}
|
||||
onpointerenter = (_) => {
|
||||
this.context.onContentEnter();
|
||||
};
|
||||
onpointerleave = whenMouse(() => {
|
||||
if (!this.itemContext.opts.openOnHover.current)
|
||||
return;
|
||||
this.context.onContentLeave();
|
||||
});
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
onpointerenter: this.onpointerenter,
|
||||
onpointerleave: this.onpointerleave,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class NavigationMenuContentImplState {
|
||||
static create(opts, itemState) {
|
||||
return new NavigationMenuContentImplState(opts, itemState ?? NavigationMenuItemContext.get());
|
||||
}
|
||||
opts;
|
||||
itemContext;
|
||||
context;
|
||||
listContext;
|
||||
attachment;
|
||||
prevMotionAttribute = $state(null);
|
||||
motionAttribute = $derived.by(() => {
|
||||
const items = this.listContext.listTriggers;
|
||||
const values = items.map((item) => item.getAttribute("data-value")).filter(Boolean);
|
||||
if (this.context.opts.dir.current === "rtl")
|
||||
values.reverse();
|
||||
const index = values.indexOf(this.context.opts.value.current);
|
||||
const prevIndex = values.indexOf(this.context.opts.previousValue.current);
|
||||
const isSelected = this.itemContext.opts.value.current === this.context.opts.value.current;
|
||||
const wasSelected = prevIndex === values.indexOf(this.itemContext.opts.value.current);
|
||||
// When all menus are closed, we want to reset motion state to prevent residual animations
|
||||
if (!this.context.opts.value.current && !this.context.opts.previousValue.current) {
|
||||
untrack(() => (this.prevMotionAttribute = null));
|
||||
return null;
|
||||
}
|
||||
// We only want to update selected and the last selected content
|
||||
// this avoids animations being interrupted outside of that range
|
||||
if (!isSelected && !wasSelected)
|
||||
return untrack(() => 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;
|
||||
})();
|
||||
untrack(() => (this.prevMotionAttribute = attribute));
|
||||
return attribute;
|
||||
});
|
||||
domContext;
|
||||
constructor(opts, itemContext) {
|
||||
this.opts = opts;
|
||||
this.attachment = attachRef(this.opts.ref);
|
||||
this.itemContext = itemContext;
|
||||
this.listContext = itemContext.listContext;
|
||||
this.context = itemContext.listContext.context;
|
||||
this.domContext = new DOMContext(opts.ref);
|
||||
watch([
|
||||
() => this.itemContext.opts.value.current,
|
||||
() => this.itemContext.triggerNode,
|
||||
() => this.opts.ref.current,
|
||||
], () => {
|
||||
const content = this.opts.ref.current;
|
||||
if (!(content && this.context.opts.isRootMenu))
|
||||
return;
|
||||
const handleClose = () => {
|
||||
this.context.onItemDismiss();
|
||||
this.itemContext.onRootContentClose();
|
||||
if (content.contains(this.domContext.getActiveElement())) {
|
||||
this.itemContext.triggerNode?.focus();
|
||||
}
|
||||
};
|
||||
const removeListener = ROOT_CONTENT_DISMISS_EVENT.listen(content, handleClose);
|
||||
return () => {
|
||||
removeListener();
|
||||
};
|
||||
});
|
||||
}
|
||||
onFocusOutside = (e) => {
|
||||
this.itemContext.onContentFocusOutside();
|
||||
const target = e.target;
|
||||
// only dismiss content when focus moves outside of the menu
|
||||
if (this.context.opts.rootNavigationMenuRef.current?.contains(target)) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
this.context.onItemDismiss();
|
||||
};
|
||||
onInteractOutside = (e) => {
|
||||
const target = e.target;
|
||||
const isTrigger = this.listContext.listTriggers.some((trigger) => trigger.contains(target));
|
||||
const isRootViewport = this.context.opts.isRootMenu && this.context.viewportRef.current?.contains(target);
|
||||
if (!this.context.opts.isRootMenu && !isTrigger) {
|
||||
this.context.onItemDismiss();
|
||||
return;
|
||||
}
|
||||
if (isTrigger || isRootViewport) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (!this.itemContext.opts.openOnHover.current) {
|
||||
this.context.onItemSelect("", null);
|
||||
}
|
||||
};
|
||||
onkeydown = (e) => {
|
||||
// prevent parent menus handling sub-menu keydown events
|
||||
const target = e.target;
|
||||
if (!isElement(target))
|
||||
return;
|
||||
if (target.closest(navigationMenuAttrs.selector("menu")) !==
|
||||
this.context.opts.rootNavigationMenuRef.current)
|
||||
return;
|
||||
const isMetaKey = e.altKey || e.ctrlKey || e.metaKey;
|
||||
const isTabKey = e.key === kbd.TAB && !isMetaKey;
|
||||
const candidates = getTabbableCandidates(e.currentTarget);
|
||||
if (isTabKey) {
|
||||
const focusedElement = this.domContext.getActiveElement();
|
||||
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, () => this.domContext.getActiveElement())) {
|
||||
// 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
|
||||
handleProxyFocus(this.itemContext.focusProxyNode);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let activeEl = this.domContext.getActiveElement();
|
||||
if (this.itemContext.contentNode) {
|
||||
const focusedNode = this.itemContext.contentNode.querySelector("[data-focused]");
|
||||
if (focusedNode) {
|
||||
activeEl = focusedNode;
|
||||
}
|
||||
}
|
||||
if (activeEl === this.itemContext.triggerNode)
|
||||
return;
|
||||
const newSelectedElement = useArrowNavigation(e, activeEl, undefined, {
|
||||
itemsArray: candidates,
|
||||
candidateSelector: navigationMenuAttrs.selector("link"),
|
||||
loop: false,
|
||||
enableIgnoredElement: true,
|
||||
});
|
||||
newSelectedElement?.focus();
|
||||
};
|
||||
onEscapeKeydown = (_) => {
|
||||
this.context.onItemDismiss();
|
||||
this.itemContext.triggerNode?.focus();
|
||||
// prevent the dropdown from reopening after the escape key has been pressed
|
||||
this.itemContext.wasEscapeClose = true;
|
||||
};
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"aria-labelledby": this.itemContext.triggerId,
|
||||
"data-motion": this.motionAttribute ?? undefined,
|
||||
"data-orientation": this.context.opts.orientation.current,
|
||||
"data-state": getDataOpenClosed(this.context.opts.value.current === this.itemContext.opts.value.current),
|
||||
onkeydown: this.onkeydown,
|
||||
[navigationMenuAttrs.content]: "",
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
export class NavigationMenuViewportState {
|
||||
static create(opts) {
|
||||
return new NavigationMenuViewportState(opts, NavigationMenuProviderContext.get());
|
||||
}
|
||||
opts;
|
||||
context;
|
||||
attachment;
|
||||
open = $derived.by(() => !!this.context.opts.value.current);
|
||||
viewportWidth = $derived.by(() => (this.size ? `${this.size.width}px` : undefined));
|
||||
viewportHeight = $derived.by(() => (this.size ? `${this.size.height}px` : undefined));
|
||||
activeContentValue = $derived.by(() => this.context.opts.value.current);
|
||||
size = $state(null);
|
||||
contentNode = $state(null);
|
||||
mounted = $state(false);
|
||||
constructor(opts, context) {
|
||||
this.opts = opts;
|
||||
this.context = context;
|
||||
this.attachment = attachRef(this.opts.ref, (v) => (this.context.viewportRef.current = v));
|
||||
watch([() => this.activeContentValue, () => this.open], () => {
|
||||
afterTick(() => {
|
||||
const currNode = this.context.viewportRef.current;
|
||||
if (!currNode)
|
||||
return;
|
||||
const el = currNode.querySelector("[data-state=open]")
|
||||
?.children?.[0] ?? null;
|
||||
this.contentNode = el;
|
||||
});
|
||||
});
|
||||
/**
|
||||
* Update viewport size to match the active content node.
|
||||
* We prefer offset dimensions over `getBoundingClientRect` as the latter respects CSS transform.
|
||||
* For example, if content animates in from `scale(0.5)` the dimensions would be anything
|
||||
* from `0.5` to `1` of the intended size.
|
||||
*/
|
||||
new SvelteResizeObserver(() => this.contentNode, () => {
|
||||
if (this.contentNode) {
|
||||
this.size = {
|
||||
width: this.contentNode.offsetWidth,
|
||||
height: this.contentNode.offsetHeight,
|
||||
};
|
||||
}
|
||||
});
|
||||
// reset size when viewport closes to prevent residual size animations
|
||||
watch(() => this.mounted, () => {
|
||||
if (!this.mounted && this.size) {
|
||||
this.size = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
props = $derived.by(() => ({
|
||||
id: this.opts.id.current,
|
||||
"data-state": getDataOpenClosed(this.open),
|
||||
"data-orientation": this.context.opts.orientation.current,
|
||||
style: {
|
||||
pointerEvents: !this.open && this.context.opts.isRootMenu ? "none" : undefined,
|
||||
"--bits-navigation-menu-viewport-width": this.viewportWidth,
|
||||
"--bits-navigation-menu-viewport-height": this.viewportHeight,
|
||||
},
|
||||
[navigationMenuAttrs.viewport]: "",
|
||||
onpointerenter: this.context.onContentEnter,
|
||||
onpointerleave: this.context.onContentLeave,
|
||||
...this.attachment,
|
||||
}));
|
||||
}
|
||||
//
|
||||
function focusFirst(candidates, getActiveElement) {
|
||||
const previouslyFocusedElement = getActiveElement();
|
||||
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 getActiveElement() !== previouslyFocusedElement;
|
||||
});
|
||||
}
|
||||
function removeFromTabOrder(candidates) {
|
||||
candidates.forEach((candidate) => {
|
||||
candidate.dataset.tabindex = candidate.getAttribute("tabindex") || "";
|
||||
candidate.setAttribute("tabindex", "-1");
|
||||
});
|
||||
return () => {
|
||||
candidates.forEach((candidate) => {
|
||||
const prevTabIndex = candidate.dataset.tabindex;
|
||||
candidate.setAttribute("tabindex", prevTabIndex);
|
||||
});
|
||||
};
|
||||
}
|
||||
function whenMouse(handler) {
|
||||
return (e) => (e.pointerType === "mouse" ? handler(e) : undefined);
|
||||
}
|
||||
/**
|
||||
*
|
||||
* We apply the `aria-hidden` attribute to elements that should not be visible to screen readers
|
||||
* under specific circumstances, mostly when in a "modal" context or when they are strictly for
|
||||
* utility purposes, like the focus guards.
|
||||
*
|
||||
* When these elements receive focus before we can remove the aria-hidden attribute, we need to
|
||||
* handle the focus in a way that does not cause an error to be logged.
|
||||
*
|
||||
* This function handles the focus of the guard element first by momentary removing the
|
||||
* `aria-hidden` attribute, focusing the guard (which will cause something else to focus), and then
|
||||
* restoring the attribute.
|
||||
*/
|
||||
function handleProxyFocus(guard, focusOptions) {
|
||||
if (!guard)
|
||||
return;
|
||||
const ariaHidden = guard.getAttribute("aria-hidden");
|
||||
guard.removeAttribute("aria-hidden");
|
||||
guard.focus(focusOptions);
|
||||
afterSleep(0, () => {
|
||||
if (ariaHidden === null) {
|
||||
guard.setAttribute("aria-hidden", "");
|
||||
}
|
||||
else {
|
||||
guard.setAttribute("aria-hidden", ariaHidden);
|
||||
}
|
||||
});
|
||||
}
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
import type { EscapeBehaviorType } from "../utilities/escape-layer/types.js";
|
||||
import type { InteractOutsideBehaviorType } from "../utilities/dismissible-layer/types.js";
|
||||
import type { OnChangeFn, WithChild, WithChildNoChildrenSnippetProps, Without } from "../../internal/types.js";
|
||||
import type { BitsPrimitiveAnchorAttributes, BitsPrimitiveButtonAttributes, BitsPrimitiveDivAttributes, BitsPrimitiveElementAttributes, BitsPrimitiveLiAttributes, BitsPrimitiveUListAttributes } from "../../shared/attributes.js";
|
||||
import type { Direction, Orientation } from "../../shared/index.js";
|
||||
export type NavigationMenuRootPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* The value of the currently open menu item.
|
||||
*
|
||||
* @bindable
|
||||
*/
|
||||
value?: string;
|
||||
/**
|
||||
* The callback to call when a menu item is selected.
|
||||
*/
|
||||
onValueChange?: OnChangeFn<string>;
|
||||
/**
|
||||
* The amount of time in ms from when the mouse enters a trigger until the content opens.
|
||||
*
|
||||
* @default 200
|
||||
*/
|
||||
delayDuration?: number;
|
||||
/**
|
||||
* The amount of time in ms that a user has to enter another trigger without
|
||||
* incurring a delay again.
|
||||
*
|
||||
* @default 300
|
||||
*/
|
||||
skipDelayDuration?: number;
|
||||
/**
|
||||
* The reading direction of the content.
|
||||
*
|
||||
* @default "ltr"
|
||||
*/
|
||||
dir?: Direction;
|
||||
/**
|
||||
* The orientation of the menu.
|
||||
*/
|
||||
orientation?: Orientation;
|
||||
}>;
|
||||
export type NavigationMenuRootProps = NavigationMenuRootPropsWithoutHTML & Without<BitsPrimitiveElementAttributes, NavigationMenuRootPropsWithoutHTML>;
|
||||
export type NavigationMenuSubPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* 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<string>;
|
||||
/**
|
||||
* The orientation of the menu.
|
||||
*/
|
||||
orientation?: Orientation;
|
||||
}>;
|
||||
export type NavigationMenuSubProps = NavigationMenuSubPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, NavigationMenuSubPropsWithoutHTML>;
|
||||
export type NavigationMenuListPropsWithoutHTML = WithChildNoChildrenSnippetProps<{}, {
|
||||
/**
|
||||
* Attributes to spread onto a wrapper element around the content.
|
||||
* Do not style the wrapper element, its styles are computed by Floating UI.
|
||||
*/
|
||||
wrapperProps: Record<string, unknown>;
|
||||
}>;
|
||||
export type NavigationMenuListProps = NavigationMenuListPropsWithoutHTML & Without<BitsPrimitiveUListAttributes, NavigationMenuListPropsWithoutHTML>;
|
||||
export type NavigationMenuItemPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* The value of the menu item.
|
||||
*/
|
||||
value?: string;
|
||||
/**
|
||||
* Whether to open the menu associated with the item when the item's trigger
|
||||
* is hovered.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
openOnHover?: boolean;
|
||||
}>;
|
||||
export type NavigationMenuItemProps = NavigationMenuItemPropsWithoutHTML & Without<BitsPrimitiveLiAttributes, NavigationMenuItemPropsWithoutHTML>;
|
||||
export type NavigationMenuTriggerPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* Whether the trigger is disabled.
|
||||
* @defaultValue false
|
||||
*/
|
||||
disabled?: boolean | null | undefined;
|
||||
}>;
|
||||
export type NavigationMenuTriggerProps = NavigationMenuTriggerPropsWithoutHTML & Without<BitsPrimitiveButtonAttributes, NavigationMenuTriggerPropsWithoutHTML>;
|
||||
export type NavigationMenuContentPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* Callback fired when an interaction occurs outside the content.
|
||||
* Default behavior can be prevented with `event.preventDefault()`
|
||||
*/
|
||||
onInteractOutside?: (event: PointerEvent) => 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;
|
||||
/**
|
||||
* Behavior when the escape key is pressed while the menu content is open.
|
||||
*/
|
||||
escapeKeydownBehavior?: EscapeBehaviorType;
|
||||
/**
|
||||
* Behavior when an interaction occurs outside the content.
|
||||
*/
|
||||
interactOutsideBehavior?: InteractOutsideBehaviorType;
|
||||
/**
|
||||
* Whether to forcefully mount the content, regardless of the open state.
|
||||
* This is useful when wanting to use more custom transition and animation
|
||||
* libraries.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
forceMount?: boolean;
|
||||
}>;
|
||||
export type NavigationMenuContentProps = NavigationMenuContentPropsWithoutHTML & Without<BitsPrimitiveDivAttributes, NavigationMenuContentPropsWithoutHTML>;
|
||||
export type NavigationMenuLinkPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* 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<BitsPrimitiveAnchorAttributes, NavigationMenuLinkPropsWithoutHTML>;
|
||||
export type NavigationMenuIndicatorPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* 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<BitsPrimitiveDivAttributes, NavigationMenuIndicatorPropsWithoutHTML>;
|
||||
export type NavigationMenuViewportPropsWithoutHTML = WithChild<{
|
||||
/**
|
||||
* 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<BitsPrimitiveDivAttributes, NavigationMenuViewportPropsWithoutHTML>;
|
||||
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Reference in New Issue
Block a user