Skip to content

Commit

Permalink
next: Navigation Menu (#573)
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte authored Jun 16, 2024
1 parent c83f4a2 commit 0c7ec0f
Show file tree
Hide file tree
Showing 34 changed files with 2,336 additions and 147 deletions.
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

0 comments on commit 0c7ec0f

Please sign in to comment.