This commit is contained in:
eewing
2026-02-17 14:10:16 -06:00
parent 2bca5834c5
commit cf73cd3b4c
11246 changed files with 1690552 additions and 0 deletions
@@ -0,0 +1,135 @@
<script lang="ts">
import { boxWith, mergeProps } from "svelte-toolbelt";
import type { ContextMenuContentStaticProps } from "../types.js";
import { CONTEXT_MENU_TRIGGER_ATTR, MenuContentState } from "../../menu/menu.svelte.js";
import { useId } from "../../../internal/use-id.js";
import { noop } from "../../../internal/noop.js";
import PopperLayer from "../../utilities/popper-layer/popper-layer.svelte";
import { getFloatingContentCSSVars } from "../../../internal/floating-svelte/floating-utils.svelte.js";
import PopperLayerForceMount from "../../utilities/popper-layer/popper-layer-force-mount.svelte";
let {
id = useId(),
child,
children,
ref = $bindable(null),
loop = true,
onInteractOutside = noop,
onCloseAutoFocus = noop,
preventScroll = true,
// we need to explicitly pass this prop to the PopperLayer to override
// the default menu behavior of handling outside interactions on the trigger
onEscapeKeydown = noop,
forceMount = false,
style,
...restProps
}: ContextMenuContentStaticProps = $props();
const contentState = MenuContentState.create({
id: boxWith(() => id),
loop: boxWith(() => loop),
ref: boxWith(
() => ref,
(v) => (ref = v)
),
onCloseAutoFocus: boxWith(() => onCloseAutoFocus),
});
const mergedProps = $derived(mergeProps(restProps, contentState.props));
function handleInteractOutside(e: PointerEvent) {
onInteractOutside(e);
if (e.defaultPrevented) return;
contentState.parentMenu.onClose();
}
function handleEscapeKeydown(e: KeyboardEvent) {
onEscapeKeydown(e);
if (e.defaultPrevented) return;
contentState.parentMenu.onClose();
}
function isValidEvent(e: PointerEvent) {
if ("button" in e && e.button === 2) {
const target = e.target as HTMLElement;
if (!target) return false;
const isAnotherContextTrigger =
target.closest(`[${CONTEXT_MENU_TRIGGER_ATTR}]`) !==
contentState.parentMenu.triggerNode;
return isAnotherContextTrigger;
}
return false;
}
</script>
{#if forceMount}
<PopperLayerForceMount
{...mergedProps}
{...contentState.popperProps}
ref={contentState.opts.ref}
isStatic
side="right"
sideOffset={2}
align="start"
enabled={contentState.parentMenu.opts.open.current}
{preventScroll}
onInteractOutside={handleInteractOutside}
onEscapeKeydown={handleEscapeKeydown}
{isValidEvent}
trapFocus
{loop}
{forceMount}
{id}
shouldRender={contentState.shouldRender}
>
{#snippet popper({ props })}
{@const finalProps = mergeProps(
props,
{ style: getFloatingContentCSSVars("context-menu") },
{ style }
)}
{#if child}
{@render child({ props: finalProps, ...contentState.snippetProps })}
{:else}
<div {...finalProps}>
{@render children?.()}
</div>
{/if}
{/snippet}
</PopperLayerForceMount>
{:else if !forceMount}
<PopperLayer
{...mergedProps}
{...contentState.popperProps}
ref={contentState.opts.ref}
isStatic
side="right"
sideOffset={2}
align="start"
open={contentState.parentMenu.opts.open.current}
{preventScroll}
onInteractOutside={handleInteractOutside}
onEscapeKeydown={handleEscapeKeydown}
{isValidEvent}
trapFocus
{loop}
forceMount={false}
{id}
shouldRender={contentState.shouldRender}
>
{#snippet popper({ props })}
{@const finalProps = mergeProps(
props,
{ style: getFloatingContentCSSVars("context-menu") },
{ style }
)}
{#if child}
{@render child({ props: finalProps, ...contentState.snippetProps })}
{:else}
<div {...finalProps}>
{@render children?.()}
</div>
{/if}
{/snippet}
</PopperLayer>
{/if}
@@ -0,0 +1,4 @@
import type { ContextMenuContentStaticProps } from "../types.js";
declare const ContextMenuContentStatic: import("svelte").Component<ContextMenuContentStaticProps, {}, "ref">;
type ContextMenuContentStatic = ReturnType<typeof ContextMenuContentStatic>;
export default ContextMenuContentStatic;
@@ -0,0 +1,138 @@
<script lang="ts">
import { boxWith, mergeProps } from "svelte-toolbelt";
import type { ContextMenuContentProps } from "../types.js";
import { CONTEXT_MENU_TRIGGER_ATTR, MenuContentState } from "../../menu/menu.svelte.js";
import { useId } from "../../../internal/use-id.js";
import { noop } from "../../../internal/noop.js";
import PopperLayer from "../../utilities/popper-layer/popper-layer.svelte";
import { getFloatingContentCSSVars } from "../../../internal/floating-svelte/floating-utils.svelte.js";
import PopperLayerForceMount from "../../utilities/popper-layer/popper-layer-force-mount.svelte";
let {
id = useId(),
child,
children,
ref = $bindable(null),
loop = true,
onInteractOutside = noop,
onCloseAutoFocus = noop,
onOpenAutoFocus = noop,
preventScroll = true,
side = "right",
sideOffset = 2,
align = "start",
// we need to explicitly pass this prop to the PopperLayer to override
// the default menu behavior of handling outside interactions on the trigger
onEscapeKeydown = noop,
forceMount = false,
trapFocus = false,
style,
...restProps
}: ContextMenuContentProps = $props();
const contentState = MenuContentState.create({
id: boxWith(() => id),
loop: boxWith(() => loop),
ref: boxWith(
() => ref,
(v) => (ref = v)
),
onCloseAutoFocus: boxWith(() => onCloseAutoFocus),
});
const mergedProps = $derived(
mergeProps(restProps, contentState.props, {
side,
sideOffset,
align,
onOpenAutoFocus,
isValidEvent,
trapFocus,
loop,
id,
ref: contentState.opts.ref,
preventScroll,
onInteractOutside: handleInteractOutside,
onEscapeKeydown: handleEscapeKeydown,
shouldRender: contentState.shouldRender,
})
);
function handleInteractOutside(e: PointerEvent) {
onInteractOutside(e);
if (e.defaultPrevented) return;
// don't close if the interaction is with a submenu content or items
if (e.target && e.target instanceof Element) {
const subContentSelector = `[${contentState.parentMenu.root.getBitsAttr("sub-content")}]`;
if (e.target.closest(subContentSelector)) return;
}
contentState.parentMenu.onClose();
}
function handleEscapeKeydown(e: KeyboardEvent) {
onEscapeKeydown(e);
if (e.defaultPrevented) return;
contentState.parentMenu.onClose();
}
function isValidEvent(e: PointerEvent) {
if ("button" in e && e.button === 2) {
const target = e.target as HTMLElement;
if (!target) return false;
const isAnotherContextTrigger =
target.closest(`[${CONTEXT_MENU_TRIGGER_ATTR}]`) !==
contentState.parentMenu.triggerNode;
return isAnotherContextTrigger;
}
return false;
}
</script>
{#if forceMount}
<PopperLayerForceMount
{...mergedProps}
{...contentState.popperProps}
enabled={contentState.parentMenu.opts.open.current}
>
{#snippet popper({ props, wrapperProps })}
{@const finalProps = mergeProps(
props,
{ style: getFloatingContentCSSVars("context-menu") },
{ style }
)}
{#if child}
{@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
{:else}
<div {...wrapperProps}>
<div {...finalProps}>
{@render children?.()}
</div>
</div>
{/if}
{/snippet}
</PopperLayerForceMount>
{:else if !forceMount}
<PopperLayer
{...mergedProps}
{...contentState.popperProps}
open={contentState.parentMenu.opts.open.current}
>
{#snippet popper({ props, wrapperProps })}
{@const finalProps = mergeProps(
props,
{ style: getFloatingContentCSSVars("context-menu") },
{ style }
)}
{#if child}
{@render child({ props: finalProps, wrapperProps, ...contentState.snippetProps })}
{:else}
<div {...wrapperProps}>
<div {...finalProps}>
{@render children?.()}
</div>
</div>
{/if}
{/snippet}
</PopperLayer>
{/if}
@@ -0,0 +1,3 @@
declare const ContextMenuContent: import("svelte").Component<import("../../menu/types.js").MenuContentProps, {}, "ref">;
type ContextMenuContent = ReturnType<typeof ContextMenuContent>;
export default ContextMenuContent;
@@ -0,0 +1,47 @@
<script lang="ts">
import { boxWith, mergeProps } from "svelte-toolbelt";
import type { ContextMenuTriggerProps } from "../types.js";
import { ContextMenuTriggerState } from "../../menu/menu.svelte.js";
import { useId } from "../../../internal/use-id.js";
import { FloatingLayer } from "../../utilities/floating-layer/index.js";
let {
id = useId(),
ref = $bindable(null),
child,
children,
disabled = false,
...restProps
}: ContextMenuTriggerProps = $props();
const triggerState = ContextMenuTriggerState.create({
id: boxWith(() => id),
disabled: boxWith(() => disabled),
ref: boxWith(
() => ref,
(v) => (ref = v)
),
});
const mergedProps = $derived(
mergeProps(
restProps,
triggerState.props,
{ style: { pointerEvents: "auto" } },
{
style: restProps.style,
tabindex: restProps.tabindex,
}
)
);
</script>
<FloatingLayer.Anchor {id} virtualEl={triggerState.virtualElement} ref={triggerState.opts.ref}>
{#if child}
{@render child({ props: mergedProps })}
{:else}
<div {...mergedProps}>
{@render children?.()}
</div>
{/if}
</FloatingLayer.Anchor>
@@ -0,0 +1,4 @@
import type { ContextMenuTriggerProps } from "../types.js";
declare const ContextMenuTrigger: import("svelte").Component<ContextMenuTriggerProps, {}, "ref">;
type ContextMenuTrigger = ReturnType<typeof ContextMenuTrigger>;
export default ContextMenuTrigger;
@@ -0,0 +1,42 @@
<script lang="ts">
import { boxWith } from "svelte-toolbelt";
import type { ContextMenuRootProps } from "../types.js";
import FloatingLayer from "../../utilities/floating-layer/components/floating-layer.svelte";
import { noop } from "../../../internal/noop.js";
import { MenuMenuState, MenuRootState } from "../../menu/menu.svelte.js";
let {
open = $bindable(false),
dir = "ltr",
onOpenChange = noop,
onOpenChangeComplete = noop,
children,
}: ContextMenuRootProps = $props();
const root = MenuRootState.create({
variant: boxWith(() => "context-menu"),
dir: boxWith(() => dir),
onClose: () => {
open = false;
onOpenChange?.(false);
},
});
MenuMenuState.create(
{
open: boxWith(
() => open,
(v) => {
open = v;
onOpenChange(v);
}
),
onOpenChangeComplete: boxWith(() => onOpenChangeComplete),
},
root
);
</script>
<FloatingLayer>
{@render children?.()}
</FloatingLayer>
@@ -0,0 +1,3 @@
declare const ContextMenu: import("svelte").Component<import("../types.js").ContextMenuRootPropsWithoutHTML, {}, "open">;
type ContextMenu = ReturnType<typeof ContextMenu>;
export default ContextMenu;