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 f02f931e3..343ebc98a 100644 --- a/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts +++ b/packages/bits-ui/src/lib/bits/menu/menu.svelte.ts @@ -826,6 +826,103 @@ class DropdownMenuTriggerState { ); } +type ContextMenuTriggerStateProps = ReadableBoxedValues<{ + id: string; + disabled: boolean; +}>; + +class ContextMenuTriggerState { + #parentMenu: MenuMenuState; + #disabled: ContextMenuTriggerStateProps["disabled"]; + #point = $state({ x: 0, y: 0 }); + #virtual = box.with(() => ({ + getBoundingClientRect: () => DOMRect.fromRect({ width: 0, height: 0, ...this.#point }), + })); + #longPressTimer = $state(null); + + constructor(props: ContextMenuTriggerStateProps, parentMenu: MenuMenuState) { + this.#parentMenu = parentMenu; + this.#disabled = props.disabled; + this.#parentMenu.triggerId = props.id; + + $effect(() => { + if (this.#disabled.value) { + this.#clearLongPressTimer(); + } + }); + + $effect(() => { + return () => { + this.#clearLongPressTimer(); + }; + }); + } + + #clearLongPressTimer() { + if (this.#longPressTimer === null) return; + window.clearTimeout(this.#longPressTimer); + } + + #handleOpen = (e: MouseEvent | PointerEvent) => { + this.#point = { x: e.clientX, y: e.clientY }; + this.#parentMenu.onOpen(); + }; + + #oncontextmenu = (e: MouseEvent) => { + if (this.#disabled.value) return; + this.#clearLongPressTimer(); + this.#handleOpen(e); + e.preventDefault(); + }; + + #onpointerdown = (e: PointerEvent) => { + if (this.#disabled.value || isMouseEvent(e)) return; + this.#clearLongPressTimer(); + this.#longPressTimer = window.setTimeout(() => this.#handleOpen(e), 700); + }; + + #onpointermove = (e: PointerEvent) => { + if (this.#disabled.value || isMouseEvent(e)) return; + this.#clearLongPressTimer(); + }; + + #onpointercancel = (e: PointerEvent) => { + if (this.#disabled.value || isMouseEvent(e)) return; + this.#clearLongPressTimer(); + }; + + #onpointerup = (e: PointerEvent) => { + if (this.#disabled.value || isMouseEvent(e)) return; + this.#clearLongPressTimer(); + }; + + #ariaControls = $derived.by(() => { + if (this.#parentMenu.open.value && this.#parentMenu.contentNode.value) + return this.#parentMenu.contentNode.value.id; + return undefined; + }); + + props = $derived.by( + () => + ({ + id: this.#parentMenu.triggerId.value, + disabled: this.#disabled.value, + "aria-haspopup": "menu", + "aria-expanded": getAriaExpanded(this.#parentMenu.open.value), + "aria-controls": this.#ariaControls, + "data-disabled": getDataDisabled(this.#disabled.value), + "data-state": getDataOpenClosed(this.#parentMenu.open.value), + [TRIGGER_ATTR]: "", + // + onpointerdown: this.#onpointerdown, + onpointermove: this.#onpointermove, + onpointercancel: this.#onpointercancel, + onpointerup: this.#onpointerup, + oncontextmenu: this.#oncontextmenu, + }) as const + ); +} + type MenuItemCombinedProps = MenuItemSharedStateProps & MenuItemStateProps; // diff --git a/packages/bits-ui/src/lib/bits/menu/utils.ts b/packages/bits-ui/src/lib/bits/menu/utils.ts index ef71ec1c6..524658396 100644 --- a/packages/bits-ui/src/lib/bits/menu/utils.ts +++ b/packages/bits-ui/src/lib/bits/menu/utils.ts @@ -1,3 +1,4 @@ +import type { EventCallback } from "$lib/internal/events.js"; import { kbd } from "$lib/internal/kbd.js"; import type { Direction } from "$lib/shared/index.js"; 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 ae9698268..620288dfc 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 @@ -21,7 +21,7 @@ import { } from "$lib/internal/index.js"; import { useSize } from "$lib/internal/useSize.svelte.js"; import { useFloating } from "$lib/internal/floating-svelte/useFloating.svelte.js"; -import type { UseFloatingReturn } from "$lib/internal/floating-svelte/types.js"; +import type { Measurable, UseFloatingReturn } from "$lib/internal/floating-svelte/types.js"; import type { Direction, StyleProperties } from "$lib/shared/index.js"; import { createContext } from "$lib/internal/createContext.js"; @@ -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 WritableBox; createAnchor(props: FloatingAnchorStateProps) { return new FloatingAnchorState(props, this); 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 918fd6348..50fbd8a94 100644 --- a/packages/bits-ui/src/lib/internal/floating-svelte/types.ts +++ b/packages/bits-ui/src/lib/internal/floating-svelte/types.ts @@ -6,12 +6,13 @@ import type { ReferenceElement, Strategy, } from "@floating-ui/dom"; -import type { VirtualElement } from "@floating-ui/core"; import type { WritableBox } from "runed"; type ValueOrGetValue = T | (() => T); -export type ReferenceType = Element | VirtualElement; +export type Measurable = { + getBoundingClientRect: () => DOMRect; +}; export type UseFloatingOptions = { /** @@ -45,7 +46,7 @@ export type UseFloatingOptions = { /** * Reference / Anchor element to position the floating element relative to */ - reference: WritableBox; + reference: WritableBox; /** * Callback to handle mounting/unmounting of the elements. @@ -60,29 +61,35 @@ export type UseFloatingOptions = { export type UseFloatingReturn = { /** - * The action used to obtain the reference element. + * The reference element to position the floating element relative to. */ - reference: WritableBox; + reference: WritableBox; + /** - * The action used to obtain the floating element. + * The floating element to position. */ floating: WritableBox; + /** * The stateful placement, which can be different from the initial `placement` passed as options. */ placement: Readonly; + /** * The type of CSS position property to use. */ strategy: Readonly; + /** * Additional data from middleware. */ middlewareData: Readonly; + /** * The boolean that let you know if the floating element has been positioned. */ isPositioned: Readonly; + /** * CSS styles to apply to the floating element to position it. */ @@ -93,6 +100,7 @@ export type UseFloatingReturn = { transform?: string; willChange?: string; }>; + /** * The function to update floating position manually. */