contextmenu_helpers.ts

view source

Declarations
#

22 declarations

contextmenu_calculate_constrained_x
#

contextmenu_helpers.ts view source

(menu_x: number, menu_width: number, layout_width: number): number import {contextmenu_calculate_constrained_x} from '@fuzdev/fuz_ui/contextmenu_helpers.js';

Constrains the menu's x coordinate to the layout, shifting left to fit the right edge. Clamps at 0 so a menu wider than the layout pins its left edge - the start of each item stays visible.

menu_x

type number

menu_width

type number

layout_width

type number

returns

number

contextmenu_calculate_constrained_y
#

contextmenu_helpers.ts view source

(menu_y: number, menu_height: number, layout_height: number): number import {contextmenu_calculate_constrained_y} from '@fuzdev/fuz_ui/contextmenu_helpers.js';

Constrains the menu's y coordinate to the layout, shifting up to fit the bottom edge. Clamps at 0 so a menu taller than the layout pins its top edge - the first items stay visible.

menu_y

type number

menu_height

type number

layout_height

type number

returns

number

contextmenu_calculate_submenu_translate
#

contextmenu_helpers.ts view source

(options: ContextmenuSubmenuTranslateOptions): { x: number; y: number; } import {contextmenu_calculate_submenu_translate} from '@fuzdev/fuz_ui/contextmenu_helpers.js';

Calculates a submenu flyout's translation so it fits the layout.

Preference order on the x axis: fly out to the right of the parent menu, else flip fully to the left of it, else shift to pin whichever side overflows less. The y axis shifts up only as far as needed to fit the bottom edge.

Used by ContextmenuSubmenu.svelte.

options

returns

{ x: number; y: number; }

contextmenu_create_keyboard_handlers
#

contextmenu_helpers.ts view source

(contextmenu: ContextmenuState): Map<string, () => void> import {contextmenu_create_keyboard_handlers} from '@fuzdev/fuz_ui/contextmenu_helpers.js';

contextmenu

returns

Map<string, () => void>

contextmenu_create_keydown_handler
#

contextmenu_helpers.ts view source

(keyboard_handlers: Map<string, () => void>): (e: KeyboardEvent) => void import {contextmenu_create_keydown_handler} from '@fuzdev/fuz_ui/contextmenu_helpers.js';

keyboard_handlers

type Map<string, () => void>

returns

(e: KeyboardEvent) => void

contextmenu_create_mousedown_handler
#

contextmenu_helpers.ts view source

(contextmenu: ContextmenuState, get_menu_el: () => HTMLElement | undefined, open_guard?: ContextmenuOpenGuard | undefined): (e: MouseEvent) => void import {contextmenu_create_mousedown_handler} from '@fuzdev/fuz_ui/contextmenu_helpers.js';

Creates a mousedown handler that closes the contextmenu when pressing outside of the menu element, deferring to open_guard for presses that belong to the gesture that opened the menu: gesture presses outside don't close, and gesture presses on the menu arm the click blocker instead of activating the entry under the pointer.

Secondary-button presses outside the menu never close here - their own contextmenu event resolves the menu in the roots' handlers (reopening it elsewhere, or closing it when there's nothing to open) - with one exception: shift+rightclick, an explicit native-menu request whose contextmenu event Firefox suppresses entirely, closes on the press itself.

Registered on the window during the bubble phase deliberately - consumers keep the menu open through a press by swallowing the event (e.g. menu controller buttons that use onmousedowncapture + swallow).

contextmenu

the contextmenu state

get_menu_el

getter for the open menu element, if any

type () => HTMLElement | undefined

open_guard?

guard that identifies presses belonging to the gesture that opened the menu

optional

returns

(e: MouseEvent) => void

CONTEXTMENU_DEFAULT_BYPASS_MOVE_TOLERANCE
#

CONTEXTMENU_DEFAULT_BYPASS_WINDOW
#

CONTEXTMENU_DEFAULT_LONGPRESS_DURATION
#

CONTEXTMENU_DEFAULT_LONGPRESS_MOVE_TOLERANCE
#

CONTEXTMENU_DEFAULT_OPEN_OFFSET_X
#

CONTEXTMENU_DEFAULT_OPEN_OFFSET_Y
#

contextmenu_is_valid_target
#

contextmenu_helpers.ts view source

(target: EventTarget | null, shiftKey: boolean): target is HTMLElement | SVGElement import {contextmenu_is_valid_target} from '@fuzdev/fuz_ui/contextmenu_helpers.js';

Returns true if valid and narrows the type to HTMLElement | SVGElement.

target

type EventTarget | null

shiftKey

type boolean

returns

boolean

contextmenu_open_from_event
#

contextmenu_helpers.ts view source

(e: MouseEvent, contextmenu: ContextmenuState, menu_el: HTMLElement | undefined, options?: ContextmenuOpenFromEventOptions | undefined): boolean import {contextmenu_open_from_event} from '@fuzdev/fuz_ui/contextmenu_helpers.js';

Handles a contextmenu event by opening the contextmenu from the event target, unless the target is invalid or inside the open menu element. Swallows the event when the menu opens.

e

the contextmenu event

type MouseEvent

contextmenu

the contextmenu state

menu_el

the open menu element, if any, so events inside it are ignored

type HTMLElement | undefined

options?

offsets and entry filtering forwarded to contextmenu_open

optional

returns

boolean

whether the contextmenu was opened

contextmenu_popover_attachment
#

contextmenu_helpers.ts view source

(contextmenu: ContextmenuState): Attachment<HTMLElement> import {contextmenu_popover_attachment} from '@fuzdev/fuz_ui/contextmenu_helpers.js';

Creates an attachment that shows the contextmenu element as a manual popover, promoting it into the top layer so it paints above modal <dialog> elements (e.g. Dialog.svelte) - top-layer elements paint in insertion order.

Painting is only half of it: a modal dialog also makes everything outside its subtree inert. When the menu opens from inside a modal dialog, the menu element is reparented into that dialog (see contextmenu_resolve_popover_host) so it escapes the inert-ness and stays interactive - top-layer positioning is relative to the viewport regardless of ancestors, so the menu's fixed coordinates are unaffected. The host dialog's close also closes the menu, since the menu's DOM node vanishes with the dialog.

No-ops where the Popover API is unavailable (older browsers, jsdom), falling back to --contextmenu_z_index stacking - the menu then renders beneath and inert to any open modal dialog.

Used with the popover="manual" attribute - "manual" rather than "auto" so opening, closing, and keyboard handling stay fully owned by ContextmenuState.

contextmenu

returns

Attachment<HTMLElement>

contextmenu_resolve_contextmenu_event
#

contextmenu_helpers.ts view source

(e: MouseEvent, contextmenu: ContextmenuState, menu_el: HTMLElement | undefined, open_guard: ContextmenuOpenGuard, options?: ContextmenuOpenFromEventOptions | undefined): boolean import {contextmenu_resolve_contextmenu_event} from '@fuzdev/fuz_ui/contextmenu_helpers.js';

Resolves a contextmenu event: opens the menu from the event target when possible (arming open_guard against the gesture's residual events), otherwise lets the native contextmenu show - closing ours unless the press was on the menu itself.

Shared by ContextmenuRoot.svelte and ContextmenuRootForSafariCompatibility.svelte, after their root-specific bypass and longpress handling.

e

the contextmenu event

type MouseEvent

contextmenu

the contextmenu state

menu_el

the open menu element, if any

type HTMLElement | undefined

open_guard

guard that identifies presses belonging to the gesture that opened the menu

options?

offsets and entry filtering forwarded to contextmenu_open

optional

returns

boolean

whether the contextmenu was opened

contextmenu_resolve_popover_host
#

contextmenu_helpers.ts view source

(target: HTMLElement | SVGElement | undefined): HTMLDialogElement | null import {contextmenu_resolve_popover_host} from '@fuzdev/fuz_ui/contextmenu_helpers.js';

Resolves the element that must host the contextmenu popover for it to stay interactive: the modal <dialog> containing target, if any.

A modal dialog makes every element outside its subtree inert, and the top layer fixes painting order, not inertness - so a popover shown from outside the dialog paints above it but receives no pointer or focus events (hit-testing skips inert elements). Only the topmost modal dialog's subtree is interactive, so the dialog containing the menu's open target is by construction the one the menu must join.

target

the element the menu was opened from

type HTMLElement | SVGElement | undefined

returns

HTMLDialogElement | null

the modal dialog to reparent the menu into, or null when the menu doesn't need a host

ContextmenuBypassTracker
#

contextmenu_helpers.ts view source

import {ContextmenuBypassTracker} from '@fuzdev/fuz_ui/contextmenu_helpers.js';

Tracks the tap-then-longpress gesture that bypasses the Fuz contextmenu, letting users reach the system contextmenu by tapping once then longpressing/rightclicking within a time window.

Plain DOM-event bookkeeping with no reactive state - used only inside event handlers by ContextmenuRoot.svelte and ContextmenuRootForSafariCompatibility.svelte.

bypassed

Set when a tap-then-longpress is detected, telling the next contextmenu event handler to let the system contextmenu through.

type boolean

track

Records a single-touch touchstart at the given coordinates. Detects a tap-then-longpress when the previous tap happened within bypass_window milliseconds and moved less than bypass_move_tolerance pixels, setting bypassed and returning true. Otherwise records the tap for future detection and returns false.

type (x: number, y: number, bypass_window: number, bypass_move_tolerance: number): boolean

x

type number

y

type number

bypass_window

type number

bypass_move_tolerance

type number
returns boolean

consume

Consumes a pending bypass, returning true and resetting all state if bypassed was set, otherwise returning false with no effect so tap tracking is preserved.

type (): boolean

returns boolean

reset

Clears all tracking state including any pending bypass.

type (): void

returns void

ContextmenuOpenFromEventOptions
#

contextmenu_helpers.ts view source

ContextmenuOpenFromEventOptions import type {ContextmenuOpenFromEventOptions} from '@fuzdev/fuz_ui/contextmenu_helpers.js';

inheritance

open_offset_x?

The number of pixels to offset from the pointer X position when opened.

type number

open_offset_y?

The number of pixels to offset from the pointer Y position when opened.

type number

ContextmenuOpenGuard
#

contextmenu_helpers.ts view source

import {ContextmenuOpenGuard} from '@fuzdev/fuz_ui/contextmenu_helpers.js';

Guards the menu from the residual events of the gesture that opened it, using exact gesture causality - no timing heuristics or tunable windows.

Touch: when the menu opens during a touch (the native longpress contextmenu event, or the Safari-compat custom longpress), the release of that same gesture must not interact with the menu - an unprevented touchend lets the browser synthesize compatibility mouse events (mousedown/mouseup/click) at the touch point, and the open offsets place the first menu item exactly there, activating it immediately. touchend swallows the release to stop the synthesis, and consume_blocked_click blocks the next click on the menu as belt and braces (iOS can synthesize the click regardless).

Mouse: tap-style input devices (e.g. some touchpads) can register an overlapping primary-button press during the right-click gesture that opened the menu - the compositor serializes the event stream, but the press's hardware timeStamp falls inside the gesture, and its delivery can lag long after the menu opened. Because the open offsets place the first menu item under the pointer, that press's click would activate the entry. press_belongs_to_open_gesture identifies overlap presses exactly: the press's own buttons bitmask shows the secondary button still down at generation time, or its timeStamp predates the release tracked by track_mouseup. No pressed-state is tracked across events deliberately: the native contextmenu's pointer grab swallows the secondary mouseup whenever it opens (right-click on our menu, shift+rightclick - which in Firefox doesn't even fire the contextmenu event), so a tracked flag wedges stuck, while the judged press's own buttons self-corrects. A deliberate right-then-left click - however fast - is sequential in hardware time and is never blocked.

Plain DOM-event bookkeeping with no reactive state - used only inside event handlers by ContextmenuRoot.svelte and ContextmenuRootForSafariCompatibility.svelte.

touchstart

Begins a new gesture on touchstart, clearing stale flags from the previous one.

type (): void

returns void

opened

Arms the touch release guard for the gesture that just opened the menu. Call after a successful open.

type (): void

returns void

track_mouseup

Tracks the physical secondary button's release - call from an always-on window mouseup capture listener. The release may never be delivered (the native contextmenu's pointer grab eats it), which is why press_belongs_to_open_gesture never depends on it alone.

type (e: MouseEvent): void

e

type MouseEvent
returns void

press_belongs_to_open_gesture

Returns true when a press belongs to the gesture that opened the menu rather than being a deliberate response to it: secondary-button presses (their own contextmenu event resolves them), presses during or predating an active touch (synthesized compatibility events), and presses overlapping the secondary button in hardware time - the press's own buttons bitmask shows the secondary button still down at generation time, or its timeStamp predates the release tracked by track_mouseup.

type (e: MouseEvent): boolean

e

type MouseEvent
returns boolean

mousedown_on_menu

Handles a mousedown that landed on the menu element: when a primary-button press belongs to the opening gesture, its click is blocked from activating the entry that the open offsets placed under the pointer. Non-primary presses never arm the blocker - they produce no click, so an armed blocker would linger and eat the next deliberate click on the menu.

type (e: MouseEvent): void

e

type MouseEvent
returns void

touchend

Handles touchend: when the release belongs to the gesture that opened the menu, swallows it (stopping mouse event synthesis) and arms the click blocker, returning true.

type (e: TouchEvent): boolean

e

type TouchEvent
returns boolean

reset

Clears gesture state, including any armed click blocker. Call on touchcancel. The secondary release timestamp persists - it mirrors hardware history, not gesture state.

type (): void

returns void

consume_blocked_click

Consumes an armed click blocker, returning true if the click should be swallowed. Call from the menu element's click capture handler.

type (): boolean

returns boolean

ContextmenuRootBaseProps
#

contextmenu_helpers.ts view source

ContextmenuRootBaseProps import type {ContextmenuRootBaseProps} from '@fuzdev/fuz_ui/contextmenu_helpers.js';

Props shared by ContextmenuRoot.svelte and ContextmenuRootForSafariCompatibility.svelte. Defaults are applied identically by both roots.

contextmenu?

The contextmenu state. Each root defaults to its own new instance - pass one to control or observe the menu externally.

type ContextmenuState

open_offset_x?

The number of pixels to offset from the pointer X position when opened. Useful to ensure the first menu item is immediately under the pointer.

type number

open_offset_y?

The number of pixels to offset from the pointer Y position when opened. Useful to ensure the first menu item is immediately under the pointer.

type number

bypass_with_tap_then_longpress?

Whether to detect tap-then-longpress to bypass the Fuz contextmenu. This allows access to the system contextmenu by tapping once then rightclicking/long-pressing. Setting to false disables the gesture.

type boolean

bypass_window?

The number of milliseconds between taps to detect a gesture that bypasses the Fuz contextmenu. Used only when bypass_with_tap_then_longpress is true. If the duration is too long, it'll detect more false positives and interrupt normal usage, but too short and some people will have difficulty performing the gesture.

type number

bypass_move_tolerance?

The number of pixels the pointer can be moved between taps to detect a tap-then-longpress. Used only when bypass_with_tap_then_longpress is true.

type number

scoped?

If true, wraps children with a div and listens to events on it instead of the window.

type boolean

link_entry?

Snippet for rendering link entries. Set to null to disable automatic link detection. Defaults to link_entry_default which renders ContextmenuLinkEntry.

type Snippet<[any]> | null

text_entry?

Snippet for rendering copy text entries. Set to null to disable automatic copy text detection. Defaults to text_entry_default which renders ContextmenuTextEntry.

type Snippet<[any]> | null

separator_entry?

Snippet for rendering separator entries. Set to null to disable automatic separator rendering. Defaults to separator_entry_default which renders ContextmenuSeparator.

type Snippet<[any]> | null

children

The content the root listens over for contextmenu gestures.

type Snippet<[]>

ContextmenuSubmenuTranslateOptions
#

contextmenu_helpers.ts view source

ContextmenuSubmenuTranslateOptions import type {ContextmenuSubmenuTranslateOptions} from '@fuzdev/fuz_ui/contextmenu_helpers.js';

base_x

The submenu's untranslated viewport x - the parent menu's left edge.

type number

base_y

The submenu's untranslated viewport y.

type number

width

The submenu's width.

type number

height

The submenu's height.

type number

parent_width

The parent menu's width.

type number

layout_width

The layout's width.

type number

layout_height

The layout's height.

type number

Depends on
#

Imported by
#