You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/soma/layers/floating/shell.ts

98 lines
3.7 KiB

/**
* Floating "shell" helpers — small abstractions over the byte-identical
* boilerplate that 7 popover-based soma providers (popover, dropdown-menu,
* context-menu, combobox, select, tooltip, link-preview) used to inline:
*
* 1. **Root-side** triad: every root provider calls `FloatingProvider.create({ dom })`
* then constructs a `contentPresence` with the open state + content ref +
* onOpenChangeComplete callback. The 5-line block was byte-identical
* across the 7 providers.
*
* 2. **Content-side** `wrapperProps`: every Content provider exposes a
* `$derived` field that re-emits the floating layer's wrapperProps
* with `pointer-events: 'auto'` forced onto the style object. The
* 7-line block was identical in 6 of 7 (tooltip parameterises the
* pointer-events value).
*
* The helpers do NOT abstract FocusScope / Dismissal / ScrollLock /
* TextSelection — those layers genuinely diverge per provider
* (different `trap` resolution, different `isValidEvent` closures,
* different close routing — see Round 3 audit analysis).
*
* Saves ~150 lines net across the 7 consumers without introducing
* configuration flags.
*/
import type { ActiveDom } from '$adom';
import type { Active, State } from '$libs/reactive';
import type { OnChangeFn } from '../../types';
import { FloatingProvider, type FloatingContent } from './floating.svelte';
import { Presence } from '../presence.svelte';
export interface FloatingShellRootOpts {
/** DOM service — every floating root piggybacks on the soma ActiveDom. */
dom: ActiveDom;
/** The provider's `open` state — drives the content presence transitions. */
open: Active<boolean>;
/** Ref to the Content element. Presence listens to its mount/unmount. */
contentRef: State<HTMLElement | null>;
/**
* Called when the open/close presence transition completes (animation
* end). Optional — when omitted, presence still runs but the callback
* is a no-op.
*/
onOpenChangeComplete?: Active<OnChangeFn<boolean> | undefined>;
}
export interface FloatingShellRoot {
floatingProvider: FloatingProvider;
contentPresence: Presence;
}
/**
* Initialise the shared `FloatingProvider + contentPresence` triad. The
* resulting handles are stored on the consumer's class (`this.floatingProvider`,
* `this.contentPresence`) — the helper just removes the boilerplate.
*/
export function createFloatingShellRoot(opts: FloatingShellRootOpts): FloatingShellRoot {
const floatingProvider = FloatingProvider.create({ dom: opts.dom });
const contentPresence = new Presence({
dom: opts.dom,
open: opts.open,
ref: opts.contentRef,
onComplete: opts.onOpenChangeComplete
? (open) => opts.onOpenChangeComplete!.current?.(open)
: undefined
});
return { floatingProvider, contentPresence };
}
/**
* Build the wrapper props the Content provider exposes on `data-*-content`'s
* wrapper. Merges the floating layer's own wrapperProps (positioning + role
* + transition attrs) with `pointer-events` forced ON so portaled content
* captures clicks.
*
* Tooltip overrides `pointerEvents` to toggle on `hoverableDisabled`; the
* rest pass `'auto'` (default).
*
* Returns the spread + an explicit `style: Record<string, unknown>` shape
* so Content components can pass it to `styleToString` directly without TS
* narrowing tripping on the upstream `string | Record<...>` union.
*/
export function buildFloatingShellWrapperProps(
floating: FloatingContent,
pointerEvents: 'auto' | 'none' = 'auto'
): FloatingContent['wrapperProps'] & { style: Record<string, unknown> } {
const baseStyle = floating.wrapperProps.style;
return {
...floating.wrapperProps,
style: {
...(typeof baseStyle === 'object' && baseStyle !== null ? baseStyle : {}),
'pointer-events': pointerEvents
}
};
}

Powered by TurnKey Linux.