Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

next: Navigation Menu #573

Merged
merged 18 commits into from
Jun 16, 2024
13 changes: 8 additions & 5 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand All @@ -16,5 +15,9 @@ export default config({ svelte: true, ignores: [...DEFAULT_IGNORES, ...ignores]
},
],
},
}
);
})
.override("antfu/js/rules", {
rules: {
"no-unused-expressions": "off",
},
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/bits-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/bits-ui/src/lib/bits/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script lang="ts">
import { useId } from "$lib/internal/useId.svelte.js";
import { box } from "svelte-toolbelt";
import type { ContentProps } from "../index.js";
import { useNavigationMenuContent } from "../navigation-menu.svelte.js";
import { mergeProps } from "$lib/internal/mergeProps.js";
import Portal from "$lib/bits/utilities/portal/portal.svelte";
import { PresenceLayer } from "$lib/bits/utilities/presence-layer/index.js";
import DismissableLayer from "$lib/bits/utilities/dismissable-layer/dismissable-layer.svelte";
import EscapeLayer from "$lib/bits/utilities/escape-layer/escape-layer.svelte";
import Mounted from "$lib/bits/utilities/mounted.svelte";

let {
asChild,
children: contentChildren,
child,
ref = $bindable(null),
id = useId(),
forceMount = false,
...restProps
}: ContentProps = $props();

let isMounted = $state(false);

const contentState = useNavigationMenuContent({
id: box.with(() => id),
ref: box.with(
() => ref,
(v) => {
ref = v;
}
),
forceMount: box.with(() => forceMount),
isMounted: box.with(() => isMounted),
});

const mergedProps = $derived(mergeProps(restProps, contentState.props));
const portalDisabled = $derived(!Boolean(contentState.menu.viewportNode));
</script>

<Portal to={contentState.menu.viewportNode ?? undefined} disabled={portalDisabled}>
<PresenceLayer {id} present={contentState.isPresent}>
{#snippet presence({ present })}
<EscapeLayer
enabled={contentState.isPresent}
onEscapeKeydown={(e) => contentState.onEscapeKeydown(e)}
>
<DismissableLayer
enabled={contentState.isPresent}
{id}
onInteractOutside={contentState.onInteractOutside}
onFocusOutside={contentState.onFocusOutside}
>
{#snippet children({ props: dismissableProps })}
{#if asChild}
<Mounted bind:isMounted />
{@render child?.({ props: mergeProps(dismissableProps, mergedProps) })}
{:else}
<Mounted bind:isMounted />
<div {...mergeProps(dismissableProps, mergedProps)}>
{@render contentChildren?.()}
</div>
{/if}
{/snippet}
</DismissableLayer>
</EscapeLayer>
{/snippet}
</PresenceLayer>
</Portal>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script lang="ts">
import { mergeProps } from "$lib/internal/mergeProps.js";
import { useId } from "$lib/internal/useId.svelte.js";
import { box } from "svelte-toolbelt";
import type { IndicatorProps } from "../index.js";
import { useNavigationMenuIndicator } from "../navigation-menu.svelte.js";
import PresenceLayer from "$lib/bits/utilities/presence-layer/presence-layer.svelte";
import Portal from "$lib/bits/utilities/portal/portal.svelte";

let {
id = useId(),
ref = $bindable(null),
asChild,
children,
child,
forceMount = false,
...restProps
}: IndicatorProps = $props();

const indicatorState = useNavigationMenuIndicator({
id: box.with(() => id),
ref: box.with(
() => ref,
(v) => (ref = v)
),
});

const mergedProps = $derived(mergeProps(restProps, indicatorState.props));
</script>

{#if indicatorState.menu.indicatorTrackNode}
<Portal to={indicatorState.menu.indicatorTrackNode}>
<PresenceLayer {id} present={forceMount || indicatorState.isVisible}>
{#snippet presence()}
{#if asChild}
{@render child?.({ props: mergedProps })}
{:else}
<div {...mergedProps}>
{@render children?.()}
</div>
{/if}
{/snippet}
</PresenceLayer>
</Portal>
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<script lang="ts">
import { box } from "svelte-toolbelt";
import type { ItemProps } from "../index.js";
import { useNavigationMenuItem } from "../navigation-menu.svelte.js";
import { useId } from "$lib/internal/useId.svelte.js";
import { mergeProps } from "$lib/internal/mergeProps.js";

let {
id = useId(),
value = useId(),
asChild,
child,
children,
ref = $bindable(),
...restProps
}: ItemProps = $props();

const itemState = useNavigationMenuItem({
id: box.with(() => id),
value: box.with(() => value),
});

const mergedProps = $derived(mergeProps(restProps, itemState.props));
</script>

{#if asChild}
{@render child?.({ props: mergedProps })}
{:else}
<li {...mergedProps} bind:this={ref}>
{@render children?.()}
</li>
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script lang="ts">
import { noop } from "$lib/internal/callbacks.js";
import { useId } from "$lib/internal/useId.svelte.js";
import { box } from "svelte-toolbelt";
import type { LinkProps } from "../index.js";
import { useNavigationMenuLink } from "../navigation-menu.svelte.js";
import { mergeProps } from "$lib/internal/mergeProps.js";

let {
id = useId(),
ref = $bindable(),
asChild,
child,
children,
active = false,
onSelect = noop,
...restProps
}: LinkProps = $props();

const linkState = useNavigationMenuLink({
id: box.with(() => id),
active: box.with(() => active),
onSelect: box.with(() => onSelect),
});

const mergedProps = $derived(mergeProps(restProps, linkState.props));
</script>

{#if asChild}
{@render child?.({ props: mergedProps })}
{:else}
<a {...mergedProps} bind:this={ref}>
{@render children?.()}
</a>
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script lang="ts">
import { box } from "svelte-toolbelt";
import type { ListProps } from "../index.js";
import { useNavigationMenuList } from "../navigation-menu.svelte.js";
import { useId } from "$lib/internal/useId.svelte.js";
import { mergeProps } from "$lib/internal/mergeProps.js";

let {
id = useId(),
asChild,
children,
child,
ref = $bindable(null),
...restProps
}: ListProps = $props();

const listState = useNavigationMenuList({
id: box.with(() => id),
ref: box.with(
() => ref,
(v) => (ref = v)
),
indicatorTrackRef: box(null),
});

const mergedProps = $derived(mergeProps(restProps, listState.props));
const indicatorTrackProps = $derived(mergeProps(listState.indicatorTrackProps, {}));
</script>

<div {...indicatorTrackProps}>
{#if asChild}
{@render child?.({ props: mergedProps })}
{:else}
<ul {...mergedProps}>
{@render children?.()}
</ul>
{/if}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!-- <script lang="ts">
import { useId } from "$lib/internal/useId.svelte.js";
import { box } from "svelte-toolbelt";
import type { SubProps } from "../index.js";
import { useNavigationMenuSub } from "../navigation-menu.svelte.js";
import { noop } from "$lib/internal/callbacks.js";
import { mergeProps } from "$lib/internal/mergeProps.js";

let {
id = useId(),
asChild,
children,
child,
ref = $bindable(null),
value = $bindable(""),
orientation = "horizontal",
onValueChange = noop,
...restProps
}: SubProps = $props();

const subState = useNavigationMenuSub({
id: box.with(() => id),
orientation: box.with(() => orientation),
ref: box.with(
() => ref,
(v) => (ref = v)
),
value: box.with(
() => value,
(v) => {
if (v !== value) {
value = v;
onValueChange(v);
}
}
),
});

const mergedProps = $derived(mergeProps(restProps, subState.props));
</script>

{#if asChild}
{@render child?.({ props: mergedProps })}
{:else}
<div {...mergedProps}>
{@render children?.()}
</div>
{/if} -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script lang="ts">
import { box } from "svelte-toolbelt";
import type { TriggerProps } from "../index.js";
import { useNavigationMenuTrigger } from "../navigation-menu.svelte.js";
import { useId } from "$lib/internal/useId.svelte.js";
import { mergeProps } from "$lib/internal/mergeProps.js";
import VisuallyHidden from "$lib/bits/utilities/visually-hidden/visually-hidden.svelte";
import Mounted from "$lib/bits/utilities/mounted.svelte";

let {
id = useId(),
disabled = false,
asChild,
children,
child,
ref = $bindable(null),
...restProps
}: TriggerProps = $props();

let focusProxyMounted = $state(false);

const triggerState = useNavigationMenuTrigger({
id: box.with(() => id),
disabled: box.with(() => disabled),
ref: box.with(
() => ref,
(v) => (ref = v)
),
focusProxyMounted: box.with(() => focusProxyMounted),
});

const mergedProps = $derived(mergeProps(restProps, triggerState.props));
</script>

{#if asChild}
{@render child?.({ props: mergedProps })}
{:else}
<button {...mergedProps}>
{@render children?.()}
</button>
{/if}

{#if triggerState.open}
<Mounted bind:isMounted={focusProxyMounted} />
<VisuallyHidden {...triggerState.visuallyHiddenProps} />
{#if triggerState.menu.viewportNode}
<span aria-owns={triggerState.item.contentNode?.id ?? undefined}></span>
{/if}
{/if}
Loading
Loading