From af4414519dc7e02673928d7a30146076e3ac80c2 Mon Sep 17 00:00:00 2001 From: Hunter Johnston Date: Tue, 14 May 2024 19:01:49 -0400 Subject: [PATCH] context trigger --- .../components/context-menu-content.svelte | 178 ++++++++---------- .../components/context-menu-trigger.svelte | 71 +++---- .../components/context-menu.svelte | 68 ------- .../src/lib/bits/context-menu/index.ts | 5 +- .../src/lib/bits/context-menu/types.ts | 43 ++--- .../components/dropdown-menu-content.svelte | 85 +++++++++ .../src/lib/bits/dropdown-menu/index.ts | 2 +- .../menu/components/menu-checkbox-item.svelte | 1 + .../bits-ui/src/lib/bits/menu/menu.svelte.ts | 23 ++- packages/bits-ui/src/lib/bits/menu/types.ts | 2 + .../components/floating-layer-anchor.svelte | 4 +- .../bits/utilities/floating-layer/types.ts | 3 + .../floating-layer/useFloatingLayer.svelte.ts | 9 +- .../src/lib/internal/floating-svelte/types.ts | 6 +- .../components/demos/context-menu-demo.svelte | 7 +- 15 files changed, 261 insertions(+), 246 deletions(-) delete mode 100644 packages/bits-ui/src/lib/bits/context-menu/components/context-menu.svelte create mode 100644 packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content.svelte diff --git a/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content.svelte b/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content.svelte index afd9ac659..973aa1b73 100644 --- a/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content.svelte +++ b/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-content.svelte @@ -1,104 +1,92 @@ -{#if asChild && $open} - -{:else if transition && $open} -
- -
-{:else if inTransition && outTransition && $open} -
- -
-{:else if inTransition && $open} -
- -
-{:else if outTransition && $open} -
- -
-{:else if $open} -
- -
-{/if} + { + console.log("interactoutsidestart", e); + }} + onInteractOutside={(e) => { + console.log(e); + onInteractOutside(e); + if (e.defaultPrevented) return; + state.parentMenu.onClose(); + }} + onEscapeKeydown={(e) => { + // TODO: users should be able to cancel this + onEscapeKeydown(e); + state.parentMenu.onClose(); + }} + trapped + {loop} +> + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, mergedProps)} + {#if asChild} + {@render child?.({ props: finalProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
diff --git a/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-trigger.svelte b/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-trigger.svelte index 3861a966c..705fe6379 100644 --- a/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-trigger.svelte +++ b/packages/bits-ui/src/lib/bits/context-menu/components/context-menu-trigger.svelte @@ -1,46 +1,37 @@ -{#if asChild} - -{:else} -
- -
-{/if} + + {#if asChild} + {@render child?.({ props: mergedProps })} + {:else} +
+ {@render children?.()} +
+ {/if} +
diff --git a/packages/bits-ui/src/lib/bits/context-menu/components/context-menu.svelte b/packages/bits-ui/src/lib/bits/context-menu/components/context-menu.svelte deleted file mode 100644 index 856649669..000000000 --- a/packages/bits-ui/src/lib/bits/context-menu/components/context-menu.svelte +++ /dev/null @@ -1,68 +0,0 @@ - - - diff --git a/packages/bits-ui/src/lib/bits/context-menu/index.ts b/packages/bits-ui/src/lib/bits/context-menu/index.ts index 95d2bd1c0..42f4f413b 100644 --- a/packages/bits-ui/src/lib/bits/context-menu/index.ts +++ b/packages/bits-ui/src/lib/bits/context-menu/index.ts @@ -1,4 +1,4 @@ -export { default as Root } from "./components/context-menu.svelte"; +export { default as Root } from "$lib/bits/menu/components/menu.svelte"; export { default as Sub } from "$lib/bits/menu/components/menu-sub.svelte"; export { default as Item } from "$lib/bits/menu/components/menu-item.svelte"; export { default as Group } from "$lib/bits/menu/components/menu-group.svelte"; @@ -19,7 +19,7 @@ export type { ContextMenuGroupProps as GroupProps, ContextMenuItemProps as ItemProps, ContextMenuLabelProps as LabelProps, - ContextMenuProps as Props, + ContextMenuRootProps as RootProps, ContextMenuRadioGroupProps as RadioGroupProps, ContextMenuRadioItemProps as RadioItemProps, ContextMenuSeparatorProps as SeparatorProps, @@ -27,4 +27,5 @@ export type { ContextMenuSubProps as SubProps, ContextMenuSubTriggerProps as SubTriggerProps, ContextMenuContentProps as ContentProps, + ContextMenuTriggerProps as TriggerProps, } from "./types.js"; diff --git a/packages/bits-ui/src/lib/bits/context-menu/types.ts b/packages/bits-ui/src/lib/bits/context-menu/types.ts index 3219e7680..0784e0a34 100644 --- a/packages/bits-ui/src/lib/bits/context-menu/types.ts +++ b/packages/bits-ui/src/lib/bits/context-menu/types.ts @@ -1,10 +1,18 @@ -import type { - DOMElement, - HTMLDivAttributes, - Transition, - TransitionProps, -} from "$lib/internal/index.js"; -import type { FloatingProps } from "$lib/bits/floating/_types.js"; +import type { MenuContentProps, MenuContentPropsWithoutHTML } from "../menu/types.js"; +import type { PrimitiveDivAttributes, WithAsChild, Without } from "$lib/internal/types.js"; + +export type ContextMenuContentPropsWithoutHTML = MenuContentPropsWithoutHTML; + +export type ContextMenuContentProps = Omit< + MenuContentProps, + "side" | "onMountAutoFocus" | "sideOffset" | "align" +>; + +export type ContextMenuTriggerPropsWithoutHTML = WithAsChild<{ + disabled?: boolean; +}>; +export type ContextMenuTriggerProps = ContextMenuTriggerPropsWithoutHTML & + Without; export type { ArrowProps as ContextMenuArrowProps, @@ -12,7 +20,7 @@ export type { GroupProps as ContextMenuGroupProps, ItemProps as ContextMenuItemProps, LabelProps as ContextMenuLabelProps, - RootProps as ContextMenuProps, + RootProps as ContextMenuRootProps, RadioGroupProps as ContextMenuRadioGroupProps, RadioItemProps as ContextMenuRadioItemProps, SeparatorProps as ContextMenuSeparatorProps, @@ -21,32 +29,17 @@ export type { SubTriggerProps as ContextMenuSubTriggerProps, } from "$lib/bits/menu/index.js"; -type ContextFloatingProps = Omit; - -export type ContextMenuContentPropsWithoutHTML< - T extends Transition = Transition, - In extends Transition = Transition, - Out extends Transition = Transition, -> = Expand & DOMElement>; - -export type ContextMenuContentProps< - T extends Transition = Transition, - In extends Transition = Transition, - Out extends Transition = Transition, -> = ContextMenuContentPropsWithoutHTML & HTMLDivAttributes; - export type { - MenuTriggerPropsWithoutHTML as ContextMenuTriggerPropsWithoutHTML, + MenuRootPropsWithoutHTML as ContextMenuRootPropsWithoutHTML, MenuArrowPropsWithoutHTML as ContextMenuArrowPropsWithoutHTML, MenuCheckboxItemPropsWithoutHTML as ContextMenuCheckboxItemPropsWithoutHTML, MenuGroupPropsWithoutHTML as ContextMenuGroupPropsWithoutHTML, MenuItemPropsWithoutHTML as ContextMenuItemPropsWithoutHTML, MenuLabelPropsWithoutHTML as ContextMenuLabelPropsWithoutHTML, - MenuRootPropsWithoutHTML as ContextMenuPropsWithoutHTML, MenuRadioGroupPropsWithoutHTML as ContextMenuRadioGroupPropsWithoutHTML, MenuRadioItemPropsWithoutHTML as ContextMenuRadioItemPropsWithoutHTML, MenuSeparatorPropsWithoutHTML as ContextMenuSeparatorPropsWithoutHTML, - MenuSubContentPropsWithoutHTML as ContextMenuSubContentPropsWithoutHTML, MenuSubPropsWithoutHTML as ContextMenuSubPropsWithoutHTML, MenuSubTriggerPropsWithoutHTML as ContextMenuSubTriggerPropsWithoutHTML, + MenuSubContentPropsWithoutHTML as ContextMenuSubContentPropsWithoutHTML, } from "$lib/bits/menu/types.js"; diff --git a/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content.svelte b/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content.svelte new file mode 100644 index 000000000..0bc214fcd --- /dev/null +++ b/packages/bits-ui/src/lib/bits/dropdown-menu/components/dropdown-menu-content.svelte @@ -0,0 +1,85 @@ + + + { + onInteractOutside(e); + if (e.defaultPrevented) return; + state.parentMenu.onClose(); + }} + onEscapeKeydown={(e) => { + // TODO: users should be able to cancel this + onEscapeKeydown(e); + state.parentMenu.onClose(); + }} + trapped + {loop} +> + {#snippet popper({ props })} + {@const finalProps = mergeProps(props, mergedProps)} + {#if asChild} + {@render child?.({ props: finalProps })} + {:else} +
+ {@render children?.()} +
+ {/if} + {/snippet} +
diff --git a/packages/bits-ui/src/lib/bits/dropdown-menu/index.ts b/packages/bits-ui/src/lib/bits/dropdown-menu/index.ts index 6fbecf55c..faa312a76 100644 --- a/packages/bits-ui/src/lib/bits/dropdown-menu/index.ts +++ b/packages/bits-ui/src/lib/bits/dropdown-menu/index.ts @@ -4,7 +4,7 @@ export { default as Item } from "$lib/bits/menu/components/menu-item.svelte"; export { default as Group } from "$lib/bits/menu/components/menu-group.svelte"; export { default as Label } from "$lib/bits/menu/components/menu-label.svelte"; export { default as Arrow } from "$lib/bits/menu/components/menu-arrow.svelte"; -export { default as Content } from "$lib/bits/menu/components/menu-content.svelte"; +export { default as Content } from "./components/dropdown-menu-content.svelte"; export { default as Trigger } from "$lib/bits/menu/components/menu-trigger.svelte"; export { default as RadioItem } from "$lib/bits/menu/components/menu-radio-item.svelte"; export { default as Separator } from "$lib/bits/menu/components/menu-separator.svelte"; diff --git a/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-item.svelte b/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-item.svelte index c15c4799b..9780a262c 100644 --- a/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-item.svelte +++ b/packages/bits-ui/src/lib/bits/menu/components/menu-checkbox-item.svelte @@ -36,6 +36,7 @@ function handleSelect(e: Event) { onSelect(e); + if (e.defaultPrevented) return; state.toggleChecked(); } diff --git a/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts b/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts index 343ebc98a..a43bfa69d 100644 --- a/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts +++ b/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts @@ -1,5 +1,5 @@ import { box } from "runed"; -import { tick } from "svelte"; +import { tick, untrack } from "svelte"; import { focusFirst } from "../utilities/focus-scope/utils.js"; import { FIRST_LAST_KEYS, @@ -167,6 +167,10 @@ class MenuMenuState { createDropdownTrigger(props: DropdownMenuTriggerStateProps) { return new DropdownMenuTriggerState(props, this); } + + createContextTrigger(props: ContextMenuTriggerStateProps) { + return new ContextMenuTriggerState(props, this); + } } type MenuContentStateProps = ReadableBoxedValues<{ @@ -835,9 +839,11 @@ class ContextMenuTriggerState { #parentMenu: MenuMenuState; #disabled: ContextMenuTriggerStateProps["disabled"]; #point = $state({ x: 0, y: 0 }); - #virtual = box.with(() => ({ + + virtualElement = box({ getBoundingClientRect: () => DOMRect.fromRect({ width: 0, height: 0, ...this.#point }), - })); + }); + #longPressTimer = $state(null); constructor(props: ContextMenuTriggerStateProps, parentMenu: MenuMenuState) { @@ -845,6 +851,13 @@ class ContextMenuTriggerState { this.#disabled = props.disabled; this.#parentMenu.triggerId = props.id; + $effect(() => { + const point = this.#point; + this.virtualElement.value = { + getBoundingClientRect: () => DOMRect.fromRect({ width: 0, height: 0, ...point }), + }; + }); + $effect(() => { if (this.#disabled.value) { this.#clearLongPressTimer(); @@ -949,6 +962,10 @@ export function useMenuDropdownTrigger(props: DropdownMenuTriggerStateProps) { return getMenuMenuContext().createDropdownTrigger(props); } +export function useMenuContextTrigger(props: ContextMenuTriggerStateProps) { + return getMenuMenuContext().createContextTrigger(props); +} + export function useMenuContent(props: MenuContentStateProps) { return setMenuContentContext(getMenuMenuContext().createContent(props)); } diff --git a/packages/bits-ui/src/lib/bits/menu/types.ts b/packages/bits-ui/src/lib/bits/menu/types.ts index 7aeb3cd61..84c6e5041 100644 --- a/packages/bits-ui/src/lib/bits/menu/types.ts +++ b/packages/bits-ui/src/lib/bits/menu/types.ts @@ -31,6 +31,8 @@ export type MenuRootPropsWithoutHTML = { children?: Snippet; }; +export type MenuRootProps = MenuRootPropsWithoutHTML; + export type MenuContentPropsWithoutHTML = WithAsChild; export type MenuContentProps = MenuContentPropsWithoutHTML & diff --git a/packages/bits-ui/src/lib/bits/utilities/floating-layer/components/floating-layer-anchor.svelte b/packages/bits-ui/src/lib/bits/utilities/floating-layer/components/floating-layer-anchor.svelte index dbae6eed7..ca046c412 100644 --- a/packages/bits-ui/src/lib/bits/utilities/floating-layer/components/floating-layer-anchor.svelte +++ b/packages/bits-ui/src/lib/bits/utilities/floating-layer/components/floating-layer-anchor.svelte @@ -3,9 +3,9 @@ import { useFloatingAnchorState } from "../useFloatingLayer.svelte.js"; import type { AnchorProps } from "./index.js"; - let { id, children }: AnchorProps = $props(); + let { id, children, virtualEl }: AnchorProps = $props(); - useFloatingAnchorState({ id: box.with(() => id) }); + useFloatingAnchorState({ id: box.with(() => id), virtualEl: box.with(() => virtualEl) }); {@render children?.()} diff --git a/packages/bits-ui/src/lib/bits/utilities/floating-layer/types.ts b/packages/bits-ui/src/lib/bits/utilities/floating-layer/types.ts index 84c9ee3ae..e43329a3e 100644 --- a/packages/bits-ui/src/lib/bits/utilities/floating-layer/types.ts +++ b/packages/bits-ui/src/lib/bits/utilities/floating-layer/types.ts @@ -1,7 +1,9 @@ import type { Snippet } from "svelte"; +import type { WritableBox } from "runed"; import type { Align, Boundary, Side } from "./useFloatingLayer.svelte.js"; import type { Arrayable } from "$lib/internal/types.js"; import type { Direction, StyleProperties } from "$lib/shared/index.js"; +import type { Measurable } from "$lib/internal/floating-svelte/types.js"; export type FloatingLayerContentProps = { /** @@ -120,4 +122,5 @@ export type FloatingLayerContentImplProps = { export type FloatingLayerAnchorProps = { id: string; children?: Snippet; + virtualEl?: Measurable; }; diff --git a/packages/bits-ui/src/lib/bits/utilities/floating-layer/useFloatingLayer.svelte.ts b/packages/bits-ui/src/lib/bits/utilities/floating-layer/useFloatingLayer.svelte.ts index 620288dfc..c0ab43755 100644 --- a/packages/bits-ui/src/lib/bits/utilities/floating-layer/useFloatingLayer.svelte.ts +++ b/packages/bits-ui/src/lib/bits/utilities/floating-layer/useFloatingLayer.svelte.ts @@ -41,7 +41,7 @@ export type Align = (typeof ALIGN_OPTIONS)[number]; export type Boundary = Element | null; class FloatingRootState { - anchorNode = undefined as unknown as WritableBox; + anchorNode = undefined as unknown as ReadableBox; createAnchor(props: FloatingAnchorStateProps) { return new FloatingAnchorState(props, this); @@ -309,11 +309,16 @@ class FloatingArrowState { type FloatingAnchorStateProps = ReadableBoxedValues<{ id: string; + virtualEl?: Measurable; }>; class FloatingAnchorState { constructor(props: FloatingAnchorStateProps, root: FloatingRootState) { - root.anchorNode = useNodeById(props.id); + if (props.virtualEl && props.virtualEl.value) { + root.anchorNode = box.from(props.virtualEl.value); + } else { + root.anchorNode = useNodeById(props.id); + } } } diff --git a/packages/bits-ui/src/lib/internal/floating-svelte/types.ts b/packages/bits-ui/src/lib/internal/floating-svelte/types.ts index 50fbd8a94..71f2300c3 100644 --- a/packages/bits-ui/src/lib/internal/floating-svelte/types.ts +++ b/packages/bits-ui/src/lib/internal/floating-svelte/types.ts @@ -6,7 +6,7 @@ import type { ReferenceElement, Strategy, } from "@floating-ui/dom"; -import type { WritableBox } from "runed"; +import type { ReadableBox, WritableBox } from "runed"; type ValueOrGetValue = T | (() => T); @@ -46,7 +46,7 @@ export type UseFloatingOptions = { /** * Reference / Anchor element to position the floating element relative to */ - reference: WritableBox; + reference: ReadableBox; /** * Callback to handle mounting/unmounting of the elements. @@ -63,7 +63,7 @@ export type UseFloatingReturn = { /** * The reference element to position the floating element relative to. */ - reference: WritableBox; + reference: ReadableBox; /** * The floating element to position. diff --git a/sites/docs/src/lib/components/demos/context-menu-demo.svelte b/sites/docs/src/lib/components/demos/context-menu-demo.svelte index b1762d1e5..09631894a 100644 --- a/sites/docs/src/lib/components/demos/context-menu-demo.svelte +++ b/sites/docs/src/lib/components/demos/context-menu-demo.svelte @@ -1,7 +1,6 @@ - +