feat(floating): P1 complete — detectOverflow + middleware + compute + autoUpdate (flag-gated)

Finishes the in-house positioning engine that replaces @floating-ui. The DOM-read
core (rects + clipping ancestor-walk) was already committed; this adds the rest:

- engine/overflow.ts — detectOverflow, PURE over the pre-read clipping +
  viewportDelta/offsetScale (no DOM read in the middleware phase).
- engine/middleware/{offset,shift,flip,arrow,size,hide,limit-shift}.ts — each a
  factory with @floating-ui's call signatures + index barrel.
- engine/compute.ts — one dom.measure read phase → the pure, exported
  runMiddleware loop (base coords → chain → flip/arrow reset, capped at 50).
- engine/auto-update.ts — ancestor scroll/resize via $adom listen, element
  resize via ResizeObserver, optional rAF loop; no raw window/getBoundingClientRect.
- MiddlewareState gains viewportDelta/offsetScale/rtl (precomputed upfront so the
  middleware stay pure); MiddlewareReturn.reset widened to boolean | {placement}.
- geometry.ts gains getOppositeAxis/getAlignmentSides/getExpandedPlacements/
  getOppositeAxisPlacements; clipping.ts exports getOverflowAncestors.
- Middleware de-vendored from @floating-ui: engine/types owns it, ../types
  re-exports it as the public surface.
- engine/flag.ts — USE_OWN_ENGINE A/B switch, default OFF (runtime still
  @floating-ui). use-floating + floating.svelte branch on it and thread `dom`.

Verified: engine/engine.test.ts — 13-case parity guard proving runMiddleware is
pixel-identical to @floating-ui/core on synthetic rects (offset, flip with/without
overflow, shift, limitShift, arrow centerOffset + alignmentOffset, size, hide,
full chain with viewportDelta/scroll, rtl). npm run check at baseline (0 new).

Note: the engine/ dir keeps its P1 no-semicolon style (pre-existing), so it does
not match the repo's prettier semi:true; left as-is to avoid reformatting the
committed P1 files. P2/P3 (broad foreground-browser A/B) + P4 (@floating-ui
removal) remain — see engine CONTINUE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
menubar-v4-safe
dev 3 months ago
parent 9fecb80fac
commit 4438012198

@ -1,8 +1,14 @@
# CONTINUE — Removing `@floating-ui`, building our own positioning layer
> **Status: P1 in progress (~half).** The hard part (DOM-read + clipping ancestor-walk)
> is built, committed and compiling. The middleware + orchestration + wiring remain.
> Branch: `alpha-0.1-sec-dom`. Read this cold to resume.
> **Status: P1 COMPLETE — flag-gated, math-verified.** The whole engine is built
> and compiling: DOM-read + clipping ancestor-walk + `detectOverflow` + all 7
> middleware + `compute` (read phase + pure `runMiddleware` loop) + `autoUpdate` +
> the `Middleware` de-vendor + the `USE_OWN_ENGINE` flag wiring. A 13-case parity
> test (`engine/engine.test.ts`) proves the engine is **pixel-identical to
> `@floating-ui`** on synthetic rects. The flag is **OFF by default** (runtime
> still `@floating-ui`); flip `engine/flag.ts` to A/B in a browser. What remains
> is broad foreground-browser A/B across every overlay (P2/P3) then the actual
> `@floating-ui` deletion (P4). Branch: `alpha-0.1-sec-dom`. Read this cold to resume.
## Why
@ -82,73 +88,98 @@ eidos `@supports` block — both must agree.
**Module map** (`engine/`):
```
types.ts ✅ internal types (Rect/Coords/ElementRects/ClippingContext/
MiddlewareState/MiddlewareReturn/Middleware/ComputePosition*)
geometry.ts ✅ pure placement/coords math
MiddlewareState[+viewportDelta/offsetScale/rtl]/MiddlewareReturn
[reset: boolean | {placement}]/Middleware/ComputePosition*)
geometry.ts ✅ pure placement/coords math (+getOppositeAxis/getAlignmentSides/
getExpandedPlacements/getOppositeAxisPlacements)
rects.ts ✅ offsetParent + scale-aware rects + viewport conversion
clipping.ts ✅ getClippingRect ancestor-walk (option 2)
clipping.ts ✅ getClippingRect ancestor-walk (option 2) [+exports getOverflowAncestors]
supports.ts ✅ supportsCssAnchor (dispatcher gate)
overflow.ts ⬜ detectOverflow — PURE, over the pre-read clipping + viewportDelta
middleware/ ⬜ offset · shift · flip · arrow · size · hide · limit-shift
compute.ts ⬜ the read phase + the middleware loop (with flip's `reset`)
auto-update.ts⬜ scroll/resize/rAF via $adom
overflow.ts ✅ detectOverflow — PURE, over pre-read clipping + viewportDelta + offsetScale
middleware/ ✅ offset · shift · flip · arrow · size · hide · limit-shift (+index barrel)
compute.ts ✅ read phase (computePosition) → pure loop (runMiddleware, exported for tests)
auto-update.ts✅ scroll/resize ($adom listen) + ResizeObserver + rAF loop, no raw window
flag.ts ✅ USE_OWN_ENGINE (A/B switch; OFF = @floating-ui)
engine.test.ts✅ 13-case parity guard: runMiddleware ↔ @floating-ui/core, identical
```
## Remaining (ordered — do these next)
1. **Adjust `MiddlewareState`** (`engine/types.ts`): add `viewportDelta: Coords`
(precomputed in the read phase so `detectOverflow` converts purely); the middleware
are pure so they don't need `dom`. The read phase computes `viewportDelta` via
`convertOffsetParentRelativeRectToViewportRelativeRect({x:0,y:0,width:0,height:0},
offsetParent, strategy, win)` → `{x,y}`.
2. **`engine/overflow.ts`** — `detectOverflow(state, { padding?, elementContext? })`:
PURE. `elementRect` (floating at `state.x/y` + `rects.floating` dims, or
`rects.reference`) → convert to viewport via `viewportDelta` → compare to
`state.clipping[elementContext]` (already viewport coords) → `SideObject`. Use
`getPaddingObject` + `rectToClientRect`.
3. **`engine/middleware/*.ts`** — each is a factory returning `Middleware`, matching
floating-ui's call signatures (so `floating.svelte.ts`'s array works with our
factories under the flag):
- `offset({ mainAxis?, alignmentAxis? } | number)` — pure (shift coords along axes).
- `shift({ mainAxis?, crossAxis?, limiter?, boundary?, padding? })` — detectOverflow,
slide within; `limiter` from `limitShift()`.
- `flip({ boundary?, padding?, ... })` — detectOverflow on side; if overflows, return
`{ reset: { placement: getOppositePlacement(...) } }`.
- `arrow({ element, padding? })` — pure; center on reference, clamp by padding, emit
`{ data: { x?, y?, centerOffset } }`. Uses `state.arrowDimensions`.
- `size({ boundary?, padding?, apply })` — detectOverflow → availableWidth/Height →
call `apply({ availableWidth, availableHeight, rects, ... })`.
- `hide({ strategy: 'referenceHidden', boundary?, padding? })` — detectOverflow on the
REFERENCE (`elementContext:'reference'`) → `{ data: { referenceHidden } }`.
- `limitShift(options?)` — the shift limiter for `sticky:'partial'`.
- **`transformOrigin` is ALREADY ours** (floating.svelte.ts:391-429) — keep verbatim.
4. **`engine/compute.ts`** — `computePosition(reference, floating, config): Promise<...>`:
- Extract `boundary`/`padding` from the middleware options (uniform in our usage —
`floating.svelte`'s `this.detectOverflowOptions`; take the first middleware that
declares a `boundary`, default `'clippingAncestors'`). Detect if `hide` is present
(→ also read the reference clipping).
- `await` ONE read phase: `new Promise(res => dom.measure(() => res(readAll()), floating))`
reading `getElementRects` + `getClippingRect(floating, boundary)` +
(hide ? `getClippingRect(reference, boundary)`) + arrow dims + `viewportDelta`.
- Run the middleware loop: base coords from `computeCoordsFromPlacement`, then each
middleware in order; on `reset: { placement }` recompute base coords for the new
placement and restart the loop (cap iterations ~50, like floating-ui). Merge
`data` into `middlewareData[name]`.
- Return `{ x, y, placement, strategy, middlewareData }`.
5. **`engine/auto-update.ts`** — `autoUpdate(reference, floating, update, { animationFrame?, dom })`:
scroll + resize listeners via `dom.listen`; element resize via `$adom` ResizeObserver
(or runed `ElementSize`); `animationFrame` loop via `dom.requestFrame`. Returns cleanup.
**No raw `window`/`getBoundingClientRect`.**
6. **De-vendor `Middleware`** — now that `engine/types.ts` defines `MiddlewareState`/
`Return`/`Middleware`, move `Middleware` out of `@floating-ui` into `../types`
(re-export from `engine/types`). Update the `type Middleware` imports in `types.ts`,
`use-floating.svelte.ts`, `floating.svelte.ts`.
7. **Wire the `USE_OWN_ENGINE` flag** — a module const. When true:
- `use-floating.svelte.ts:59` — call `computePosition` from `./engine/compute` (thread
`dom` from `FloatingProvider.opts.dom`). Keep the `.then(...)` shape (compute is async
via the read-phase rAF — matches today).
- `floating.svelte.ts:143-177` — build the middleware array from `./engine/middleware`;
swap `autoUpdate` (line 294) for `./engine/auto-update`.
Keep `@floating-ui` as the default (flag off) for A/B until verified.
## Done this pass (P1 finished — all 7 items below landed)
1. ✅ **`MiddlewareState`** gained `viewportDelta` + `offsetScale` + `rtl` (precomputed
in the read phase so the middleware are PURE; `dom` dropped from the state).
`MiddlewareReturn.reset` widened to `boolean | { placement? }` (arrow's bare reset).
2. ✅ **`engine/overflow.ts`** — `detectOverflow(state, { padding?, elementContext?, altBoundary? })`:
PURE. element rect → `+ viewportDelta` → `rectToClientRect` → compare to
`state.clipping[context]` → `SideObject` / `offsetScale`. `context = altBoundary ?
swap(elementContext) : elementContext`.
3. ✅ **`engine/middleware/*.ts`** — offset · shift · flip · arrow · size · hide ·
limit-shift, each a factory returning `Middleware` with floating-ui's call signatures.
`transformOrigin` stays in `floating.svelte.ts` (ours already), now typed to our
`Middleware`.
4. ✅ **`engine/compute.ts`** — `computePosition` does ONE `dom.measure` read phase
(`getElementRects` + `getClippingRect(floating)` + optional reference clip + arrow
dims + `viewportDelta`/`offsetScale`/`rtl`), then hands a `ReadSnapshot` to the pure,
exported **`runMiddleware`** loop (base coords → chain → `reset` restart, cap 50).
`boundary` extracted from the first middleware that declares one (`[]` = explicit
viewport; default `'clippingAncestors'`). Reference clip read only when `hide` /
`altBoundary` present.
5. ✅ **`engine/auto-update.ts`** — ancestor scroll/resize via `dom.listen`
(reusing `clipping.getOverflowAncestors`), element resize via `dom.observeResize`,
`animationFrame` loop via `dom.requestFrame`. No raw `window`/`getBoundingClientRect`
(the per-frame ref read runs at frame-start = post-layout). `observeMove`
IntersectionObserver path intentionally omitted (out of CONTINUE scope).
6. ✅ **De-vendored `Middleware`** — `engine/types.ts` owns it; `../types.ts` re-exports
it (public surface). `types.ts` / `use-floating.svelte.ts` / `floating.svelte.ts`
repointed off `@floating-ui/dom`.
7. ✅ **Wired `USE_OWN_ENGINE`** (`engine/flag.ts`, default **false**):
- `use-floating.svelte.ts` branches `computePosition` (own vs fui) + threads `dom`
(new `UseFloatingOptions.dom`, fed from `FloatingProvider.opts.dom`).
- `floating.svelte.ts` selects own/fui factories via a loosely-typed factory set +
branches `autoUpdate`. `@floating-ui` stays the default for A/B (P4 deletes it).
## Verification done this pass
- **Math parity guard — `engine/engine.test.ts` (13 cases, all green).** `runMiddleware`
(the real loop) vs `@floating-ui/core`'s `computePosition`, both fed identical
synthetic rects (a synthetic floating-ui `platform` mirrors `snapshotOf`). Covers
offset (+alignmentAxis), flip (overflow→flip AND fits→no-flip), shift (+limitShift),
arrow (centerOffset + alignmentOffset path), size (available W/H), hide (true/false),
the full chain with `viewportDelta`/scroll, and RTL. **Pixel-identical.** This is the
authoritative proof the engine math is correct — keep it as the regression guard.
- **Browser end-to-end (flag flipped to true) — engine runs + positions correctly.**
Popover demo, 4 sides: own engine executed (instrumented count) and centered exactly
on the trigger. `npm run check` at baseline (61, 0 new); soma overlay providers
(popover/dropdown/tooltip/combobox/link-preview/context-menu) all green with flag off.
### ⚠️ Preview-tool caveat for the broad browser A/B (read before P2/P3)
The own engine's read phase uses `dom.measure` → `requestAnimationFrame`. The
**preview tool's tab is backgrounded** (`document.hidden === true`), so rAF is paused
and the engine never positions there. floating-ui's `computePosition` is microtask-
based, so the flag-off baseline works in the hidden tab but flag-on does not. Two ways
through it next time: (a) drive a foreground browser, or (b) inject a setTimeout-based
rAF shim from the page (`window.requestAnimationFrame = cb => setTimeout(() => cb(performance.now()), 0)`)
BEFORE opening the overlay — that's how this pass confirmed the engine end-to-end.
Also: editing `flag.ts` only HMR-updates leaf `.svelte` components, leaving the floating
layer modules stale/inconsistent — do a **full cache-busted reload** (`location.href =
url + '?cb=' + Date.now()`), not `location.reload()`, after flipping the flag. Finally,
the popover demo's trigger shifts ~8px between position-time and measure-time, so
**absolute** A/B numbers are noisy — compare wrapper-relative-to-trigger (gap +
centering) measured in the same frame, or trust the unit parity guard.
## Remaining (next sessions)
- **P2/P3 — broad foreground-browser A/B** (flag on): walk every overlay demo (popover,
tooltip, dropdown/context/sub menus, menubar, nav-menu, select, combobox, the 5
pickers, dialog, drawer, link-preview) across all 12 placements + flip-at-edges +
shift + arrow + matchAnchorWidth + sticky-scroll + virtual anchor (context-menu) +
safe-polygon (link-preview) + **nested-scroll/transform clipping** (the option-2 win).
Use the rAF-shim or a foreground browser (see caveat). Zero `[Violation] Forced reflow`.
- **P4 — delete `@floating-ui`** (after P2/P3): see the removal surface below. Flip the
flag default to true (or inline the own path + drop the flag), strip the `fui*` imports
+ the loose factory shim + the `as unknown as FuiMiddleware[]` cast, then
`npm uninstall @floating-ui/core @floating-ui/dom`. Verify `grep -r "@floating-ui"
src/ web/` → ZERO.
## API-compat checklist (the consumer × feature matrix to preserve)

@ -0,0 +1,93 @@
// autoUpdate — re-runs `update` whenever the reference or floating element could
// have moved: ancestor scroll / resize, element resize (ResizeObserver), and an
// optional per-frame loop. Everything goes through `$adom` (iframe / popup safe,
// no raw `window`). Reimplemented from floating-ui's `autoUpdate` (spec); the
// `observeMove` IntersectionObserver layout-shift path is intentionally omitted
// (CONTINUE.md scope: scroll + resize + rAF).
import { isElement } from '$adom'
import type { ActiveDom } from '$adom'
import type { Measurable } from '../types'
import { getOverflowAncestors } from './clipping'
export type AutoUpdateOptions = {
ancestorScroll?: boolean
ancestorResize?: boolean
elementResize?: boolean
animationFrame?: boolean
dom: ActiveDom
}
function rectsEqual(a: DOMRect, b: DOMRect): boolean {
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height
}
export function autoUpdate(
reference: Measurable | HTMLElement,
floating: HTMLElement,
update: () => void,
options: AutoUpdateOptions
): () => void {
const {
ancestorScroll = true,
ancestorResize = true,
elementResize = true,
animationFrame = false,
dom
} = options
const win = dom.getWindow(floating)
const referenceEl = isElement(reference) ? (reference as Element) : null
const cleanups: Array<() => void> = []
const ancestors =
ancestorScroll || ancestorResize
? [
...(referenceEl ? getOverflowAncestors(referenceEl, win) : []),
...getOverflowAncestors(floating, win)
]
: []
for (const ancestor of ancestors) {
if (ancestorScroll) {
cleanups.push(dom.listen(ancestor as EventTarget, 'scroll', update, { passive: true }))
}
if (ancestorResize) {
cleanups.push(dom.listen(ancestor as EventTarget, 'resize', update))
}
}
// Element resize. In `animationFrame` mode the reference is tracked by the
// frame loop instead (its movement, not just its size, matters). The soma
// `size` middleware never resizes the floating element synchronously, so the
// floating-ui unobserve/reobserve loop-guard isn't needed here.
if (elementResize) {
if (referenceEl && !animationFrame) cleanups.push(dom.observeResize(referenceEl, update))
cleanups.push(dom.observeResize(floating, update))
}
// Per-frame movement tracking. Reading the reference rect at the START of a
// rAF callback is post-layout (frame-start, before any write) — compliant
// with the no-sync-after-write rule.
let frameId = 0
let stopped = false
if (animationFrame) {
let prevRect = reference.getBoundingClientRect()
const loop = () => {
if (stopped) return
const nextRect = reference.getBoundingClientRect()
if (!rectsEqual(prevRect, nextRect)) update()
prevRect = nextRect
frameId = dom.requestFrame(loop, floating)
}
frameId = dom.requestFrame(loop, floating)
}
update()
return () => {
stopped = true
if (frameId) dom.cancelFrame(frameId, floating)
for (const cleanup of cleanups) cleanup()
}
}

@ -38,8 +38,9 @@ function getNearestOverflowAncestor(node: Node, win: Window): HTMLElement {
return getNearestOverflowAncestor(parent, win)
}
/** The element's overflow/clip ancestors + the window (single document). */
function getOverflowAncestors(
/** The element's overflow/clip ancestors + the window (single document).
* Exported for `auto-update.ts` (it attaches scroll/resize listeners to each). */
export function getOverflowAncestors(
node: Node,
win: Window,
list: (Element | Window)[] = []

@ -0,0 +1,205 @@
// computePosition — the orchestrator. One `dom.measure` read phase reads
// EVERYTHING upfront (reference rect, floating dims, offsetParent, the clipping
// ancestor-walk, arrow dims, and the offsetParent→viewport conversion
// constants); the middleware chain then runs PURELY over that snapshot. This is
// the read-phase model (CONTINUE.md): one coalesced rAF, no forced reflow.
// Reimplemented from floating-ui's `computePosition` (spec), our idioms.
import { getDocumentElement, isElement } from '$adom'
import type { Placement, Strategy } from '../placement'
import type { Measurable, MiddlewareData } from '../types'
import { getClippingRect } from './clipping'
import { computeCoordsFromPlacement } from './geometry'
import {
convertOffsetParentRelativeRectToViewportRelativeRect,
getDimensions,
getElementRects,
getScale
} from './rects'
import type {
ClippingContext,
ComputePositionConfig,
ComputePositionReturn,
Coords,
Dimensions,
ElementRects,
FloatingElements,
Middleware,
MiddlewareState
} from './types'
// Bound on flip / arrow resets, matching floating-ui — guards against a chain
// that oscillates between two placements forever.
const MAX_RESET_COUNT = 50
type Boundary = 'clippingAncestors' | Element[]
/** The boundary the read phase walks — the first middleware that declares one
* (uniform across shift / flip / size / hide in our usage). `[]` means an
* explicit viewport-only clip; absent means the full ancestor walk. */
function extractBoundary(middleware: readonly Middleware[]): Boundary {
for (const m of middleware) {
const opts = m.options as { boundary?: unknown } | undefined
if (opts && opts.boundary !== undefined) {
return opts.boundary as Boundary
}
}
return 'clippingAncestors'
}
function extractArrowElement(middleware: readonly Middleware[]): Element | undefined {
for (const m of middleware) {
if (m.name === 'arrow') {
const opts = m.options as { element?: Element } | undefined
if (opts?.element) return opts.element
}
}
return undefined
}
function needsReferenceClip(middleware: readonly Middleware[]): boolean {
return middleware.some(
(m) => m.name === 'hide' || (m.options as { altBoundary?: boolean } | undefined)?.altBoundary
)
}
export function computePosition(
reference: Measurable | HTMLElement,
floating: HTMLElement,
config: ComputePositionConfig
): Promise<ComputePositionReturn> {
const { placement = 'bottom', strategy = 'absolute', middleware = [], dom } = config
const win = dom.getWindow(floating)
return new Promise<ComputePositionReturn>((resolve) => {
// Single coalesced post-layout read — every DOM read the chain needs.
dom.measure(() => {
resolve(runChain(reference, floating, placement, strategy, middleware, win))
}, floating)
})
}
function runChain(
reference: Measurable | HTMLElement,
floating: HTMLElement,
placement: Placement,
strategy: Strategy,
middleware: readonly Middleware[],
win: Window
): ComputePositionReturn {
const elements = { reference, floating }
const { rects, offsetParent } = getElementRects(elements, strategy, win)
const boundary = extractBoundary(middleware)
const clippingFloating = getClippingRect(floating, boundary, strategy, win)
const referenceClipElement = isElement(reference)
? (reference as Element)
: getDocumentElement(floating)
const clipping: ClippingContext = {
floating: clippingFloating,
reference: needsReferenceClip(middleware)
? getClippingRect(referenceClipElement, boundary, strategy, win)
: undefined
}
const arrowElement = extractArrowElement(middleware)
const arrowDimensions = arrowElement ? getDimensions(arrowElement, win) : undefined
const delta = convertOffsetParentRelativeRectToViewportRelativeRect(
{ x: 0, y: 0, width: 0, height: 0 },
offsetParent,
strategy,
win
)
const viewportDelta: Coords = { x: delta.x, y: delta.y }
const offsetScale: Coords = isElement(offsetParent)
? getScale(offsetParent, win)
: { x: 1, y: 1 }
const rtl = win.getComputedStyle(floating).direction === 'rtl'
return runMiddleware(placement, strategy, middleware, {
rects,
elements,
clipping,
arrowDimensions,
viewportDelta,
offsetScale,
rtl
})
}
/**
* The fully-read snapshot the middleware chain runs over — everything
* `runMiddleware` needs, with NO DOM access. `runChain` populates it from the
* read phase; tests populate it with synthetic rects to exercise the pure math.
*/
export type ReadSnapshot = {
readonly rects: ElementRects
readonly elements: FloatingElements
readonly clipping: ClippingContext
readonly arrowDimensions?: Dimensions
readonly viewportDelta: Coords
readonly offsetScale: Coords
readonly rtl: boolean
}
/**
* The pure middleware loop — base coords from the placement, then each
* middleware in order; `reset` restarts the chain (flip recomputes base coords
* for the new placement, capped at {@link MAX_RESET_COUNT}). No DOM: operates
* solely on the read snapshot. Exported so the engine's math can be verified
* against `@floating-ui` with synthetic inputs.
*/
export function runMiddleware(
placement: Placement,
strategy: Strategy,
middleware: readonly Middleware[],
snapshot: ReadSnapshot
): ComputePositionReturn {
const { rects, elements, clipping, arrowDimensions, viewportDelta, offsetScale, rtl } = snapshot
let { x, y } = computeCoordsFromPlacement(rects, placement, rtl)
let statefulPlacement = placement
let resetCount = 0
const middlewareData: MiddlewareData = {}
for (let i = 0; i < middleware.length; i++) {
const current = middleware[i]
if (!current) continue
const state: MiddlewareState = {
x,
y,
initialPlacement: placement,
placement: statefulPlacement,
strategy,
middlewareData,
rects,
elements,
clipping,
arrowDimensions,
viewportDelta,
offsetScale,
rtl
}
const { x: nextX, y: nextY, data, reset } = current.fn(state)
x = nextX ?? x
y = nextY ?? y
middlewareData[current.name] = {
...(middlewareData[current.name] as Record<string, unknown> | undefined),
...data
}
if (reset && resetCount < MAX_RESET_COUNT) {
resetCount++
if (typeof reset === 'object') {
if (reset.placement) statefulPlacement = reset.placement
;({ x, y } = computeCoordsFromPlacement(rects, statefulPlacement, rtl))
}
i = -1
}
}
return { x, y, placement: statefulPlacement, strategy, middlewareData }
}

@ -0,0 +1,376 @@
// Parity guard: the in-house positioning engine must match `@floating-ui` to the
// pixel. We feed BOTH the same synthetic rects (no DOM, no layout, no rAF) and
// assert identical coords / placement / middleware data. Our `runMiddleware`
// (the real loop, extracted from `computePosition`) runs over a hand-built read
// snapshot; floating-ui's `computePosition` runs over a synthetic `platform`
// that returns the SAME rects. Any drift in offset / shift / flip / arrow / size
// / hide / detectOverflow / the reset loop fails here.
import { describe, expect, it } from 'vitest'
import {
arrow as fuiArrow,
computePosition as fuiComputePosition,
flip as fuiFlip,
hide as fuiHide,
limitShift as fuiLimitShift,
offset as fuiOffset,
shift as fuiShift,
size as fuiSize
} from '@floating-ui/core'
import { runMiddleware, type ReadSnapshot } from './compute'
import {
arrow as ownArrow,
flip as ownFlip,
hide as ownHide,
limitShift as ownLimitShift,
offset as ownOffset,
shift as ownShift,
size as ownSize
} from './middleware'
import type { Coords, Dimensions, Middleware, Rect } from './types'
import type { Placement } from '../placement'
type Scenario = {
reference: Rect
floatingDims: Dimensions
clip: Rect
viewportDelta?: Coords
offsetScale?: Coords
rtl?: boolean
arrowDims?: Dimensions
}
const VIEWPORT: Rect = { x: 0, y: 0, width: 1000, height: 800 }
function snapshotOf(s: Scenario): ReadSnapshot {
return {
rects: {
reference: s.reference,
floating: { x: 0, y: 0, width: s.floatingDims.width, height: s.floatingDims.height }
},
elements: { reference: {} as never, floating: {} as never },
clipping: { floating: s.clip, reference: s.clip },
arrowDimensions: s.arrowDims,
viewportDelta: s.viewportDelta ?? { x: 0, y: 0 },
offsetScale: s.offsetScale ?? { x: 1, y: 1 },
rtl: s.rtl ?? false
}
}
// A floating-ui `platform` that returns the scenario's synthetic rects — the
// twin of `snapshotOf` so both engines see identical geometry. The arrow element
// and offsetParent are sentinel objects routed by identity.
function platformOf(s: Scenario, arrowEl: object, offsetParent: object) {
return {
getElementRects: async () => ({
reference: s.reference,
floating: { x: 0, y: 0, width: s.floatingDims.width, height: s.floatingDims.height }
}),
getClippingRect: async () => ({ ...s.clip }),
getDimensions: async (el: unknown) =>
el === arrowEl ? (s.arrowDims ?? { width: 0, height: 0 }) : { ...s.floatingDims },
getOffsetParent: async () => offsetParent,
getScale: async () => s.offsetScale ?? { x: 1, y: 1 },
isElement: async () => true,
convertOffsetParentRelativeRectToViewportRelativeRect: async ({
rect
}: {
rect: Rect
}) => ({
x: rect.x + (s.viewportDelta?.x ?? 0),
y: rect.y + (s.viewportDelta?.y ?? 0),
width: rect.width,
height: rect.height
}),
isRTL: async () => s.rtl ?? false,
getDocumentElement: async () => ({})
}
}
const EPS = 0.01
async function compare(
scenario: Scenario,
placement: Placement,
build: (kind: 'own' | 'fui', ctx: { arrowEl: object; captured: Record<string, number> }) => unknown[]
) {
const arrowEl = {}
const offsetParent = {}
const ownCaptured: Record<string, number> = {}
const ownMw = build('own', { arrowEl, captured: ownCaptured }) as Middleware[]
const own = runMiddleware(placement, 'absolute', ownMw, snapshotOf(scenario))
const fuiCaptured: Record<string, number> = {}
const fuiMw = build('fui', { arrowEl, captured: fuiCaptured })
const fui = await fuiComputePosition({} as never, {} as never, {
placement: placement as never,
strategy: 'absolute',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
middleware: fuiMw as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
platform: platformOf(scenario, arrowEl, offsetParent) as any
})
return { own, fui, ownCaptured, fuiCaptured }
}
describe('positioning engine ↔ @floating-ui parity', () => {
it('offset only — base placement, no collision', async () => {
const scenario: Scenario = {
reference: { x: 100, y: 100, width: 80, height: 30 },
floatingDims: { width: 200, height: 100 },
clip: VIEWPORT
}
const { own, fui } = await compare(scenario, 'bottom', (kind) =>
[kind === 'own' ? ownOffset({ mainAxis: 8 }) : fuiOffset({ mainAxis: 8 })].filter(Boolean)
)
expect(own.x).toBeCloseTo(fui.x, 5)
expect(own.y).toBeCloseTo(fui.y, 5)
expect(own.placement).toBe(fui.placement)
})
it('offset with alignmentAxis on an aligned placement', async () => {
const scenario: Scenario = {
reference: { x: 100, y: 100, width: 80, height: 30 },
floatingDims: { width: 200, height: 100 },
clip: VIEWPORT
}
for (const placement of ['bottom-start', 'bottom-end', 'right-start'] as Placement[]) {
const { own, fui } = await compare(scenario, placement, (kind) =>
[
kind === 'own'
? ownOffset({ mainAxis: 8, alignmentAxis: 12 })
: fuiOffset({ mainAxis: 8, alignmentAxis: 12 })
].filter(Boolean)
)
expect(own.x).toBeCloseTo(fui.x, 5)
expect(own.y).toBeCloseTo(fui.y, 5)
}
})
it('flip — bottom overflows, flips to top', async () => {
const scenario: Scenario = {
reference: { x: 100, y: 700, width: 80, height: 30 },
floatingDims: { width: 200, height: 120 },
clip: { x: 0, y: 0, width: 1000, height: 760 }
}
const { own, fui } = await compare(scenario, 'bottom', (kind) =>
[
kind === 'own' ? ownOffset({ mainAxis: 8 }) : fuiOffset({ mainAxis: 8 }),
kind === 'own' ? ownFlip({ padding: 0 }) : fuiFlip({ padding: 0 })
].filter(Boolean)
)
expect(own.placement).toBe('top')
expect(own.placement).toBe(fui.placement)
expect(own.x).toBeCloseTo(fui.x, 5)
expect(own.y).toBeCloseTo(fui.y, 5)
})
it('flip — fits, no flip', async () => {
const scenario: Scenario = {
reference: { x: 100, y: 100, width: 80, height: 30 },
floatingDims: { width: 200, height: 120 },
clip: VIEWPORT
}
const { own, fui } = await compare(scenario, 'bottom', (kind) =>
[
kind === 'own' ? ownOffset({ mainAxis: 8 }) : fuiOffset({ mainAxis: 8 }),
kind === 'own' ? ownFlip({ padding: 0 }) : fuiFlip({ padding: 0 })
].filter(Boolean)
)
expect(own.placement).toBe('bottom')
expect(own.placement).toBe(fui.placement)
expect(own.y).toBeCloseTo(fui.y, 5)
})
it('shift — reference near the right edge, slides into view', async () => {
const scenario: Scenario = {
reference: { x: 900, y: 100, width: 80, height: 30 },
floatingDims: { width: 200, height: 100 },
clip: VIEWPORT
}
const { own, fui } = await compare(scenario, 'bottom', (kind) =>
[
kind === 'own' ? ownOffset({ mainAxis: 8 }) : fuiOffset({ mainAxis: 8 }),
kind === 'own'
? ownShift({ mainAxis: true, crossAxis: false, padding: 0 })
: fuiShift({ mainAxis: true, crossAxis: false, padding: 0 })
].filter(Boolean)
)
expect(own.x).toBeCloseTo(fui.x, 5)
expect(own.y).toBeCloseTo(fui.y, 5)
// Right edge of the floating element clamped inside the viewport.
expect(own.x + scenario.floatingDims.width).toBeLessThanOrEqual(1000 + EPS)
})
it('shift with limitShift (sticky: partial)', async () => {
const scenario: Scenario = {
reference: { x: 900, y: 100, width: 80, height: 30 },
floatingDims: { width: 200, height: 100 },
clip: VIEWPORT
}
const { own, fui } = await compare(scenario, 'bottom', (kind) =>
[
kind === 'own' ? ownOffset({ mainAxis: 8 }) : fuiOffset({ mainAxis: 8 }),
kind === 'own'
? ownShift({ mainAxis: true, crossAxis: false, limiter: ownLimitShift(), padding: 0 })
: fuiShift({ mainAxis: true, crossAxis: false, limiter: fuiLimitShift(), padding: 0 })
].filter(Boolean)
)
expect(own.x).toBeCloseTo(fui.x, 5)
expect(own.y).toBeCloseTo(fui.y, 5)
})
it('arrow — centers + reports centerOffset', async () => {
const scenario: Scenario = {
reference: { x: 100, y: 100, width: 80, height: 30 },
floatingDims: { width: 200, height: 100 },
clip: VIEWPORT,
arrowDims: { width: 12, height: 12 }
}
const { own, fui } = await compare(scenario, 'bottom', (kind, ctx) =>
[
kind === 'own' ? ownOffset({ mainAxis: 8 }) : fuiOffset({ mainAxis: 8 }),
kind === 'own'
? ownArrow({ element: ctx.arrowEl as Element, padding: 4 })
: fuiArrow({ element: ctx.arrowEl as never, padding: 4 })
].filter(Boolean)
)
const oa = own.middlewareData.arrow as { x?: number; centerOffset?: number }
const fa = fui.middlewareData.arrow as { x?: number; centerOffset?: number }
expect(oa.x).toBeCloseTo(fa.x as number, 5)
expect(oa.centerOffset).toBeCloseTo(fa.centerOffset as number, 5)
})
it('arrow — aligned placement (alignmentOffset path)', async () => {
const scenario: Scenario = {
reference: { x: 100, y: 100, width: 20, height: 20 },
floatingDims: { width: 200, height: 100 },
clip: VIEWPORT,
arrowDims: { width: 14, height: 14 }
}
const { own, fui } = await compare(scenario, 'bottom-start', (kind, ctx) =>
[
kind === 'own' ? ownOffset({ mainAxis: 8 }) : fuiOffset({ mainAxis: 8 }),
kind === 'own'
? ownArrow({ element: ctx.arrowEl as Element, padding: 10 })
: fuiArrow({ element: ctx.arrowEl as never, padding: 10 })
].filter(Boolean)
)
expect(own.x).toBeCloseTo(fui.x, 5)
expect(own.y).toBeCloseTo(fui.y, 5)
const oa = own.middlewareData.arrow as { centerOffset?: number; alignmentOffset?: number }
const fa = fui.middlewareData.arrow as { centerOffset?: number; alignmentOffset?: number }
expect(oa.centerOffset).toBeCloseTo(fa.centerOffset as number, 5)
expect(oa.alignmentOffset ?? 0).toBeCloseTo(fa.alignmentOffset ?? 0, 5)
})
it('size — available width/height handed to apply', async () => {
const scenario: Scenario = {
reference: { x: 100, y: 600, width: 80, height: 30 },
floatingDims: { width: 200, height: 400 },
clip: { x: 0, y: 0, width: 1000, height: 800 }
}
const { ownCaptured, fuiCaptured } = await compare(scenario, 'bottom', (kind, ctx) => {
const apply = (args: { availableWidth: number; availableHeight: number }) => {
ctx.captured.availableWidth = args.availableWidth
ctx.captured.availableHeight = args.availableHeight
}
return [
kind === 'own' ? ownOffset({ mainAxis: 8 }) : fuiOffset({ mainAxis: 8 }),
kind === 'own' ? ownSize({ padding: 0, apply }) : fuiSize({ padding: 0, apply })
].filter(Boolean)
})
expect(ownCaptured.availableWidth).toBeCloseTo(fuiCaptured.availableWidth, 5)
expect(ownCaptured.availableHeight).toBeCloseTo(fuiCaptured.availableHeight, 5)
})
it('hide — reference fully outside the clip → referenceHidden', async () => {
const scenario: Scenario = {
reference: { x: 100, y: 1200, width: 80, height: 30 },
floatingDims: { width: 200, height: 100 },
clip: { x: 0, y: 0, width: 1000, height: 800 }
}
const { own, fui } = await compare(scenario, 'bottom', (kind) =>
[
kind === 'own'
? ownHide({ strategy: 'referenceHidden', padding: 0 })
: fuiHide({ strategy: 'referenceHidden', padding: 0 })
].filter(Boolean)
)
const oh = own.middlewareData.hide as { referenceHidden?: boolean }
const fh = fui.middlewareData.hide as { referenceHidden?: boolean }
expect(oh.referenceHidden).toBe(true)
expect(oh.referenceHidden).toBe(fh.referenceHidden)
})
it('hide — reference visible → not hidden', async () => {
const scenario: Scenario = {
reference: { x: 100, y: 100, width: 80, height: 30 },
floatingDims: { width: 200, height: 100 },
clip: VIEWPORT
}
const { own, fui } = await compare(scenario, 'bottom', (kind) =>
[
kind === 'own'
? ownHide({ strategy: 'referenceHidden', padding: 0 })
: fuiHide({ strategy: 'referenceHidden', padding: 0 })
].filter(Boolean)
)
const oh = own.middlewareData.hide as { referenceHidden?: boolean }
const fh = fui.middlewareData.hide as { referenceHidden?: boolean }
expect(oh.referenceHidden).toBe(false)
expect(oh.referenceHidden).toBe(fh.referenceHidden)
})
it('full chain — offset + shift + flip + size + arrow, with viewportDelta + scroll', async () => {
const scenario: Scenario = {
reference: { x: 850, y: 700, width: 120, height: 36 },
floatingDims: { width: 240, height: 160 },
clip: { x: 0, y: 0, width: 1000, height: 760 },
viewportDelta: { x: -40, y: -120 },
arrowDims: { width: 12, height: 12 }
}
const { own, fui } = await compare(scenario, 'bottom', (kind, ctx) => {
const detect = { padding: 8 }
const apply = () => {}
return [
kind === 'own' ? ownOffset({ mainAxis: 8 }) : fuiOffset({ mainAxis: 8 }),
kind === 'own'
? ownShift({ mainAxis: true, crossAxis: false, ...detect })
: fuiShift({ mainAxis: true, crossAxis: false, ...detect }),
kind === 'own' ? ownFlip({ ...detect }) : fuiFlip({ ...detect }),
kind === 'own' ? ownSize({ ...detect, apply }) : fuiSize({ ...detect, apply }),
kind === 'own'
? ownArrow({ element: ctx.arrowEl as Element, padding: 4 })
: fuiArrow({ element: ctx.arrowEl as never, padding: 4 })
].filter(Boolean)
})
expect(own.placement).toBe(fui.placement)
expect(own.x).toBeCloseTo(fui.x, 5)
expect(own.y).toBeCloseTo(fui.y, 5)
const oa = own.middlewareData.arrow as { x?: number; y?: number; centerOffset?: number }
const fa = fui.middlewareData.arrow as { x?: number; y?: number; centerOffset?: number }
expect(oa.x ?? oa.y).toBeCloseTo((fa.x ?? fa.y) as number, 5)
})
it('rtl — aligned offset mirrors', async () => {
const scenario: Scenario = {
reference: { x: 400, y: 100, width: 80, height: 30 },
floatingDims: { width: 200, height: 100 },
clip: VIEWPORT,
rtl: true
}
const { own, fui } = await compare(scenario, 'bottom-start', (kind) =>
[
kind === 'own'
? ownOffset({ mainAxis: 8, alignmentAxis: 12 })
: fuiOffset({ mainAxis: 8, alignmentAxis: 12 })
].filter(Boolean)
)
expect(own.x).toBeCloseTo(fui.x, 5)
expect(own.y).toBeCloseTo(fui.y, 5)
})
})

@ -0,0 +1,6 @@
// A/B switch for the in-house positioning engine. While `false`, the floating
// layer keeps using `@floating-ui` (the default during the migration's
// verification window). Flip to `true` to drive positioning with `engine/`
// (computePosition + middleware + autoUpdate). P4 deletes the flag, the
// `@floating-ui` imports, and the dependency once the engine is verified.
export const USE_OWN_ENGINE = false

@ -7,6 +7,11 @@ import type { Axis, Coords, ElementRects, Length, Padding, SideObject } from './
const OPPOSITE_ALIGN: Record<'start' | 'end', 'start' | 'end'> = { start: 'end', end: 'start' }
/** The axis perpendicular to `axis`. */
export function getOppositeAxis(axis: Axis): Axis {
return axis === 'x' ? 'y' : 'x'
}
export function getSide(placement: Placement): Side {
return placement.split('-')[0] as Side
}
@ -43,6 +48,78 @@ export function getOppositeAlignmentPlacement(placement: Placement): Placement {
return `${getSide(placement)}-${OPPOSITE_ALIGN[align]}` as Placement
}
/**
* The two sides on the alignment axis that flip's cross-axis check compares —
* the side the floating element aligns toward and its opposite. Mirrors
* floating-ui's `getAlignmentSides`.
*/
export function getAlignmentSides(placement: Placement, rects: ElementRects, rtl = false): [Side, Side] {
const align = getAlignment(placement)
const alignmentAxis = getAlignmentAxis(placement)
const length = getAxisLength(alignmentAxis)
let mainAlignmentSide: Side =
alignmentAxis === 'x'
? align === (rtl ? 'end' : 'start')
? 'right'
: 'left'
: align === 'start'
? 'bottom'
: 'top'
if (rects.reference[length] > rects.floating[length]) {
mainAlignmentSide = OPPOSITE_SIDE[mainAlignmentSide]
}
return [mainAlignmentSide, OPPOSITE_SIDE[mainAlignmentSide]]
}
/** The opposite-side placement plus the two alignment variants — flip's default
* fallback list for an aligned initial placement. */
export function getExpandedPlacements(placement: Placement): Placement[] {
const opposite = getOppositePlacement(placement)
return [
getOppositeAlignmentPlacement(placement),
opposite,
getOppositeAlignmentPlacement(opposite)
]
}
const LR: Side[] = ['left', 'right']
const RL: Side[] = ['right', 'left']
const TB: Side[] = ['top', 'bottom']
const BT: Side[] = ['bottom', 'top']
function getSideList(side: Side, isStart: boolean, rtl: boolean): Side[] {
switch (side) {
case 'top':
case 'bottom':
if (rtl) return isStart ? RL : LR
return isStart ? LR : RL
case 'left':
case 'right':
return isStart ? TB : BT
default:
return []
}
}
/** The perpendicular-axis fallback placements — flip's `fallbackAxisSideDirection`
* path (unused by the soma layer today, kept for engine parity). */
export function getOppositeAxisPlacements(
placement: Placement,
flipAlignment: boolean,
direction: 'start' | 'end',
rtl: boolean
): Placement[] {
const align = getAlignment(placement)
let list: string[] = getSideList(getSide(placement), direction === 'start', rtl)
if (align) {
list = list.map((side) => `${side}-${align}`)
if (flipAlignment) {
list = list.concat(list.map((p) => getOppositeAlignmentPlacement(p as Placement)))
}
}
return list as Placement[]
}
export function getPaddingObject(padding: Padding): SideObject {
return typeof padding === 'number'
? { top: padding, right: padding, bottom: padding, left: padding }

@ -0,0 +1,78 @@
// arrow — positions an inner arrow element so it points at the reference center,
// clamped within the floating element by `padding`. PURE: the arrow's dimensions
// are pre-read in the read phase (`state.arrowDimensions`); the container size is
// taken from `rects.floating` (the borderless floating wrapper — a faithful
// stand-in for floating-ui's `offsetParent.clientWidth/Height`). Reimplemented
// from floating-ui's `arrow` (spec).
import { getAlignment, getAlignmentAxis, getAxisLength, getPaddingObject } from '../geometry'
import type { Middleware, Padding } from '../types'
export type ArrowOptions = { element: Element; padding?: Padding }
const { min } = Math
function clamp(start: number, value: number, end: number): number {
return Math.max(start, Math.min(value, end))
}
export function arrow(options: ArrowOptions): Middleware {
return {
name: 'arrow',
options,
fn(state) {
const { x, y, placement, rects, middlewareData, arrowDimensions } = state
const { padding = 0 } = options
if (!arrowDimensions) return {}
const paddingObject = getPaddingObject(padding)
const coords = { x, y }
const axis = getAlignmentAxis(placement)
const length = getAxisLength(axis)
const isYAxis = axis === 'y'
const minProp = isYAxis ? 'top' : 'left'
const maxProp = isYAxis ? 'bottom' : 'right'
const clientSize = rects.floating[length]
const endDiff =
rects.reference[length] + rects.reference[axis] - coords[axis] - rects.floating[length]
const startDiff = coords[axis] - rects.reference[axis]
const centerToReference = endDiff / 2 - startDiff / 2
// Clamp the padding so the arrow stays centered if it's large.
const largestPossiblePadding = clientSize / 2 - arrowDimensions[length] / 2 - 1
const minPadding = min(paddingObject[minProp], largestPossiblePadding)
const maxPadding = min(paddingObject[maxProp], largestPossiblePadding)
const minValue = minPadding
const maxValue = clientSize - arrowDimensions[length] - maxPadding
const center = clientSize / 2 - arrowDimensions[length] / 2 + centerToReference
const offset = clamp(minValue, center, maxValue)
// If the reference is so small the arrow points at nothing for an aligned
// placement, nudge the floating element itself (one reset).
const shouldAddOffset =
!middlewareData.arrow &&
getAlignment(placement) != null &&
center !== offset &&
rects.reference[length] / 2 -
(center < minValue ? minPadding : maxPadding) -
arrowDimensions[length] / 2 <
0
const alignmentOffset = shouldAddOffset
? center < minValue
? center - minValue
: center - maxValue
: 0
return {
[axis]: coords[axis] + alignmentOffset,
data: {
[axis]: offset,
centerOffset: center - offset - alignmentOffset,
...(shouldAddOffset && { alignmentOffset })
},
reset: shouldAddOffset
}
}
}
}

@ -0,0 +1,148 @@
// flip — when the preferred placement overflows, swap to a fallback placement
// that fits (the side / alignment opposite). PURE (detectOverflow reads
// pre-walked clipping). Returns `{ reset: { placement } }` to restart the chain.
// Reimplemented from floating-ui's `flip` (spec).
import type { Placement } from '../../placement'
import {
getAlignmentSides,
getExpandedPlacements,
getOppositeAxisPlacements,
getOppositePlacement,
getSide,
getSideAxis
} from '../geometry'
import { detectOverflow, type DetectOverflowOptions } from '../overflow'
import type { Middleware } from '../types'
export type FlipOptions = DetectOverflowOptions & {
mainAxis?: boolean
crossAxis?: boolean | 'alignment'
fallbackPlacements?: Placement[]
fallbackStrategy?: 'bestFit' | 'initialPlacement'
fallbackAxisSideDirection?: 'none' | 'start' | 'end'
flipAlignment?: boolean
}
type OverflowRecord = { placement: Placement; overflows: number[] }
export function flip(options: FlipOptions = {}): Middleware {
const {
mainAxis: checkMainAxis = true,
crossAxis: checkCrossAxis = true,
fallbackPlacements: specifiedFallbackPlacements,
fallbackStrategy = 'bestFit',
fallbackAxisSideDirection = 'none',
flipAlignment = true,
...detectOptions
} = options
return {
name: 'flip',
options,
fn(state) {
const { placement, middlewareData, rects, initialPlacement, rtl } = state
// If the arrow added an alignment offset, flip already settled; skip.
const arrowData = middlewareData.arrow as { alignmentOffset?: number } | undefined
if (arrowData?.alignmentOffset) return {}
const side = getSide(placement)
const initialSideAxis = getSideAxis(initialPlacement)
const isBasePlacement = getSide(initialPlacement) === initialPlacement
const fallbackPlacements =
specifiedFallbackPlacements ||
(isBasePlacement || !flipAlignment
? [getOppositePlacement(initialPlacement)]
: getExpandedPlacements(initialPlacement))
const hasFallbackAxisSideDirection = fallbackAxisSideDirection !== 'none'
if (!specifiedFallbackPlacements && hasFallbackAxisSideDirection) {
fallbackPlacements.push(
...getOppositeAxisPlacements(
initialPlacement,
flipAlignment,
fallbackAxisSideDirection,
rtl
)
)
}
const placements = [initialPlacement, ...fallbackPlacements]
const overflow = detectOverflow(state, detectOptions)
const flipData = middlewareData.flip as
| { index?: number; overflows?: OverflowRecord[] }
| undefined
const overflows: number[] = []
let overflowsData = flipData?.overflows || []
if (checkMainAxis) overflows.push(overflow[side])
if (checkCrossAxis) {
const sides = getAlignmentSides(placement, rects, rtl)
overflows.push(overflow[sides[0]], overflow[sides[1]])
}
overflowsData = [...overflowsData, { placement, overflows }]
// One or more sides overflows — try the next placement.
if (!overflows.every((s) => s <= 0)) {
const nextIndex = (flipData?.index || 0) + 1
const nextPlacement = placements[nextIndex]
if (nextPlacement) {
const ignoreCrossAxisOverflow =
checkCrossAxis === 'alignment'
? initialSideAxis !== getSideAxis(nextPlacement)
: false
if (
!ignoreCrossAxisOverflow ||
overflowsData.every((d) =>
getSideAxis(d.placement) === initialSideAxis ? d.overflows[0] > 0 : true
)
) {
return {
data: { index: nextIndex, overflows: overflowsData },
reset: { placement: nextPlacement }
}
}
}
// Exhausted the list — pick the best fit on the main axis, then cross.
let resetPlacement = overflowsData
.filter((d) => d.overflows[0] <= 0)
.sort((a, b) => a.overflows[1] - b.overflows[1])[0]?.placement
if (!resetPlacement) {
switch (fallbackStrategy) {
case 'bestFit': {
const best = overflowsData
.filter((d) => {
if (hasFallbackAxisSideDirection) {
const currentSideAxis = getSideAxis(d.placement)
return currentSideAxis === initialSideAxis || currentSideAxis === 'y'
}
return true
})
.map(
(d) =>
[
d.placement,
d.overflows.filter((o) => o > 0).reduce((acc, o) => acc + o, 0)
] as const
)
.sort((a, b) => a[1] - b[1])[0]?.[0]
if (best) resetPlacement = best
break
}
case 'initialPlacement':
resetPlacement = initialPlacement
break
}
}
if (placement !== resetPlacement) {
return { reset: { placement: resetPlacement } }
}
}
return {}
}
}
}

@ -0,0 +1,58 @@
// hide — detects when the floating element should be hidden because the
// reference left the shared clipping context (`referenceHidden`) or the floating
// element escaped its boundary (`escaped`). PURE. Reimplemented from
// floating-ui's `hide` (spec). The soma layer uses `referenceHidden` only.
import { detectOverflow, type DetectOverflowOptions } from '../overflow'
import type { Dimensions, Middleware, SideObject } from '../types'
export type HideOptions = DetectOverflowOptions & {
strategy?: 'referenceHidden' | 'escaped'
}
const SIDES = ['top', 'right', 'bottom', 'left'] as const
function getSideOffsets(overflow: SideObject, rect: Dimensions): SideObject {
return {
top: overflow.top - rect.height,
right: overflow.right - rect.width,
bottom: overflow.bottom - rect.height,
left: overflow.left - rect.width
}
}
function isAnySideFullyClipped(overflow: SideObject): boolean {
return SIDES.some((side) => overflow[side] >= 0)
}
export function hide(options: HideOptions = {}): Middleware {
const { strategy = 'referenceHidden', ...detectOptions } = options
return {
name: 'hide',
options,
fn(state) {
const { rects } = state
switch (strategy) {
case 'referenceHidden': {
const overflow = detectOverflow(state, { ...detectOptions, elementContext: 'reference' })
const offsets = getSideOffsets(overflow, rects.reference)
return {
data: {
referenceHiddenOffsets: offsets,
referenceHidden: isAnySideFullyClipped(offsets)
}
}
}
case 'escaped': {
const overflow = detectOverflow(state, { ...detectOptions, altBoundary: true })
const offsets = getSideOffsets(overflow, rects.floating)
return {
data: { escapedOffsets: offsets, escaped: isAnySideFullyClipped(offsets) }
}
}
default:
return {}
}
}
}
}

@ -0,0 +1,11 @@
// The middleware factories — each returns a pure `Middleware` matching
// floating-ui's call signatures, so `floating.svelte`'s array works with either
// implementation under the `USE_OWN_ENGINE` flag.
export { offset, type OffsetOptions } from './offset'
export { shift, type ShiftOptions } from './shift'
export { flip, type FlipOptions } from './flip'
export { arrow, type ArrowOptions } from './arrow'
export { size, type SizeOptions, type SizeApplyArgs } from './size'
export { hide, type HideOptions } from './hide'
export { limitShift, type LimitShiftOptions, type ShiftLimiter } from './limit-shift'

@ -0,0 +1,69 @@
// limitShift — the shift limiter used for `sticky: 'partial'`: stops the shift
// once the reference edge would pass the floating element. PURE. Reimplemented
// from floating-ui's `limitShift` (spec). Returns a limiter (no `name`), passed
// to `shift({ limiter })`.
import { getOppositeAxis, getSide, getSideAxis } from '../geometry'
import type { Coords, MiddlewareState } from '../types'
export type LimitShiftOptions = {
offset?: number | { mainAxis?: number; crossAxis?: number }
mainAxis?: boolean
crossAxis?: boolean
}
export type ShiftLimiter = {
readonly options?: unknown
readonly fn: (state: MiddlewareState) => Coords
}
const ORIGIN_SIDES = new Set<string>(['left', 'top'])
export function limitShift(options: LimitShiftOptions = {}): ShiftLimiter {
return {
options,
fn(state) {
const { x, y, placement, rects, middlewareData } = state
const { offset = 0, mainAxis: checkMainAxis = true, crossAxis: checkCrossAxis = true } =
options
const coords: Coords = { x, y }
const crossAxis = getSideAxis(placement)
const mainAxis = getOppositeAxis(crossAxis)
let mainAxisCoord = coords[mainAxis]
let crossAxisCoord = coords[crossAxis]
const computedOffset =
typeof offset === 'number'
? { mainAxis: offset, crossAxis: 0 }
: { mainAxis: 0, crossAxis: 0, ...offset }
if (checkMainAxis) {
const len = mainAxis === 'y' ? 'height' : 'width'
const limitMin = rects.reference[mainAxis] - rects.floating[len] + computedOffset.mainAxis
const limitMax = rects.reference[mainAxis] + rects.reference[len] - computedOffset.mainAxis
if (mainAxisCoord < limitMin) mainAxisCoord = limitMin
else if (mainAxisCoord > limitMax) mainAxisCoord = limitMax
}
if (checkCrossAxis) {
const len = mainAxis === 'y' ? 'width' : 'height'
const isOriginSide = ORIGIN_SIDES.has(getSide(placement))
const offsetData = middlewareData.offset as Coords | undefined
const limitMin =
rects.reference[crossAxis] -
rects.floating[len] +
(isOriginSide ? offsetData?.[crossAxis] || 0 : 0) +
(isOriginSide ? 0 : computedOffset.crossAxis)
const limitMax =
rects.reference[crossAxis] +
rects.reference[len] +
(isOriginSide ? 0 : offsetData?.[crossAxis] || 0) -
(isOriginSide ? computedOffset.crossAxis : 0)
if (crossAxisCoord < limitMin) crossAxisCoord = limitMin
else if (crossAxisCoord > limitMax) crossAxisCoord = limitMax
}
return { [mainAxis]: mainAxisCoord, [crossAxis]: crossAxisCoord } as Coords
}
}
}

@ -0,0 +1,63 @@
// offset — shifts the floating element along the main / cross / alignment axes.
// PURE. Reimplemented from floating-ui's `offset` (spec).
import type { Placement } from '../../placement'
import { getAlignment, getSide, getSideAxis } from '../geometry'
import type { Coords, Middleware, MiddlewareState } from '../types'
export type OffsetOptions =
| number
| { mainAxis?: number; crossAxis?: number; alignmentAxis?: number | null }
const ORIGIN_SIDES = new Set<string>(['left', 'top'])
function convertValueToCoords(state: MiddlewareState, options: OffsetOptions): Coords {
const { placement, rtl } = state
const side = getSide(placement)
const alignment = getAlignment(placement)
const isVertical = getSideAxis(placement) === 'y'
const mainAxisMulti = ORIGIN_SIDES.has(side) ? -1 : 1
const crossAxisMulti = rtl && isVertical ? -1 : 1
let mainAxis: number
let crossAxis: number
let alignmentAxis: number | null
if (typeof options === 'number') {
mainAxis = options
crossAxis = 0
alignmentAxis = null
} else {
mainAxis = options.mainAxis || 0
crossAxis = options.crossAxis || 0
alignmentAxis = options.alignmentAxis ?? null
}
if (alignment && typeof alignmentAxis === 'number') {
crossAxis = alignment === 'end' ? alignmentAxis * -1 : alignmentAxis
}
return isVertical
? { x: crossAxis * crossAxisMulti, y: mainAxis * mainAxisMulti }
: { x: mainAxis * mainAxisMulti, y: crossAxis * crossAxisMulti }
}
export function offset(options: OffsetOptions = 0): Middleware {
return {
name: 'offset',
options,
fn(state) {
const { x, y, placement, middlewareData } = state
const diff = convertValueToCoords(state, options)
// If the placement is unchanged and the arrow applied an alignment
// offset, leave the coords alone (offset already ran for this placement).
const offsetData = middlewareData.offset as { placement?: Placement } | undefined
const arrowData = middlewareData.arrow as { alignmentOffset?: number } | undefined
if (placement === offsetData?.placement && arrowData?.alignmentOffset) {
return {}
}
return { x: x + diff.x, y: y + diff.y, data: { ...diff, placement } }
}
}
}

@ -0,0 +1,70 @@
// shift — slides the floating element along an axis to keep it within the
// clipping boundary. PURE (detectOverflow reads pre-walked clipping).
// Reimplemented from floating-ui's `shift` (spec).
import { getOppositeAxis, getSide, getSideAxis } from '../geometry'
import { detectOverflow, type DetectOverflowOptions } from '../overflow'
import type { Coords, Middleware } from '../types'
import type { ShiftLimiter } from './limit-shift'
export type ShiftOptions = DetectOverflowOptions & {
mainAxis?: boolean
crossAxis?: boolean
limiter?: ShiftLimiter
}
function clamp(start: number, value: number, end: number): number {
return Math.max(start, Math.min(value, end))
}
export function shift(options: ShiftOptions = {}): Middleware {
const {
mainAxis: checkMainAxis = true,
crossAxis: checkCrossAxis = false,
limiter,
...detectOptions
} = options
return {
name: 'shift',
options,
fn(state) {
const { x, y, placement } = state
const coords: Coords = { x, y }
const overflow = detectOverflow(state, detectOptions)
const crossAxis = getSideAxis(getSide(placement))
const mainAxis = getOppositeAxis(crossAxis)
let mainAxisCoord = coords[mainAxis]
let crossAxisCoord = coords[crossAxis]
if (checkMainAxis) {
const minSide = mainAxis === 'y' ? 'top' : 'left'
const maxSide = mainAxis === 'y' ? 'bottom' : 'right'
const min = mainAxisCoord + overflow[minSide]
const max = mainAxisCoord - overflow[maxSide]
mainAxisCoord = clamp(min, mainAxisCoord, max)
}
if (checkCrossAxis) {
const minSide = crossAxis === 'y' ? 'top' : 'left'
const maxSide = crossAxis === 'y' ? 'bottom' : 'right'
const min = crossAxisCoord + overflow[minSide]
const max = crossAxisCoord - overflow[maxSide]
crossAxisCoord = clamp(min, crossAxisCoord, max)
}
const nextCoords: Coords = { x: coords.x, y: coords.y }
nextCoords[mainAxis] = mainAxisCoord
nextCoords[crossAxis] = crossAxisCoord
const limited = limiter ? limiter.fn({ ...state, ...nextCoords }) : nextCoords
return {
x: limited.x,
y: limited.y,
data: {
x: limited.x - x,
y: limited.y - y,
enabled: { [mainAxis]: checkMainAxis, [crossAxis]: checkCrossAxis }
}
}
}
}
}

@ -0,0 +1,78 @@
// size — computes the width/height available to the floating element within the
// clipping boundary and hands it to `apply` (the consumer writes CSS vars).
// PURE. Our engine never re-reads DOM mid-loop: `apply` doesn't resize the
// floating element synchronously in the soma layer, so the floating-ui
// `reset: { rects: true }` round-trip is unnecessary (a real resize re-triggers
// via the autoUpdate ResizeObserver). Reimplemented from floating-ui's `size`.
import { getAlignment, getSide, getSideAxis } from '../geometry'
import { detectOverflow, type DetectOverflowOptions } from '../overflow'
import type { Middleware, MiddlewareState } from '../types'
export type SizeApplyArgs = MiddlewareState & {
availableWidth: number
availableHeight: number
}
export type SizeOptions = DetectOverflowOptions & {
apply?: (args: SizeApplyArgs) => void
}
const { min, max } = Math
export function size(options: SizeOptions = {}): Middleware {
const { apply = () => {}, ...detectOptions } = options
return {
name: 'size',
options,
fn(state) {
const { placement, rects, rtl, middlewareData } = state
const overflow = detectOverflow(state, detectOptions)
const side = getSide(placement)
const alignment = getAlignment(placement)
const isYAxis = getSideAxis(placement) === 'y'
const { width, height } = rects.floating
let heightSide: 'top' | 'bottom'
let widthSide: 'left' | 'right'
if (side === 'top' || side === 'bottom') {
heightSide = side
widthSide = alignment === (rtl ? 'start' : 'end') ? 'left' : 'right'
} else {
widthSide = side as 'left' | 'right'
heightSide = alignment === 'end' ? 'top' : 'bottom'
}
const maximumClippingHeight = height - overflow.top - overflow.bottom
const maximumClippingWidth = width - overflow.left - overflow.right
const overflowAvailableHeight = min(height - overflow[heightSide], maximumClippingHeight)
const overflowAvailableWidth = min(width - overflow[widthSide], maximumClippingWidth)
const shiftData = middlewareData.shift as
| { enabled?: { x?: boolean; y?: boolean } }
| undefined
const noShift = !shiftData
let availableHeight = overflowAvailableHeight
let availableWidth = overflowAvailableWidth
if (shiftData?.enabled?.x) availableWidth = maximumClippingWidth
if (shiftData?.enabled?.y) availableHeight = maximumClippingHeight
if (noShift && !alignment) {
const xMin = max(overflow.left, 0)
const xMax = max(overflow.right, 0)
const yMin = max(overflow.top, 0)
const yMax = max(overflow.bottom, 0)
if (isYAxis) {
availableWidth =
width - 2 * (xMin !== 0 || xMax !== 0 ? xMin + xMax : max(overflow.left, overflow.right))
} else {
availableHeight =
height - 2 * (yMin !== 0 || yMax !== 0 ? yMin + yMax : max(overflow.top, overflow.bottom))
}
}
apply({ ...state, availableWidth, availableHeight })
return {}
}
}
}

@ -0,0 +1,63 @@
// detectOverflow — PURE. Compares the proposed element rect against the clipping
// region that the read phase already walked (`state.clipping`, viewport coords).
// No DOM read: the offsetParent→viewport conversion is folded into
// `state.viewportDelta` + `state.offsetScale` upfront. Reimplemented from
// floating-ui's `detectOverflow` (spec), our idioms.
import { getPaddingObject } from './geometry'
import { rectToClientRect } from './rects'
import type { MiddlewareState, Padding, SideObject } from './types'
export type DetectOverflowOptions = {
padding?: Padding
/** Which element's rect to test — `'floating'` at `state.x/y`, or the
* reference rect. @default 'floating' */
elementContext?: 'floating' | 'reference'
/** Test against the OTHER element's clipping region (swaps the boundary
* source). @default false */
altBoundary?: boolean
}
/**
* Per-side overflow of the tested element past its clipping boundary, in CSS px:
* positive = overflowing by that many pixels, negative = pixels of slack before
* it would overflow, 0 = flush.
*/
export function detectOverflow(
state: MiddlewareState,
options: DetectOverflowOptions = {}
): SideObject {
const { padding = 0, elementContext = 'floating', altBoundary = false } = options
const { x, y, rects, clipping, viewportDelta, offsetScale } = state
const paddingObject = getPaddingObject(padding)
const altContext = elementContext === 'floating' ? 'reference' : 'floating'
const context = altBoundary ? altContext : elementContext
// `clipping.reference` is only read when needed; fall back to the floating
// clip (identical when the boundary is explicit — the soma layer's only case).
const clip = clipping[context] ?? clipping.floating
const clippingClientRect = rectToClientRect(clip)
// The tested element's rect in offsetParent-relative coords…
const elementRect =
elementContext === 'floating'
? { x, y, width: rects.floating.width, height: rects.floating.height }
: rects.reference
// …converted to viewport coords purely via the precomputed delta.
const elementClientRect = rectToClientRect({
x: elementRect.x + viewportDelta.x,
y: elementRect.y + viewportDelta.y,
width: elementRect.width,
height: elementRect.height
})
return {
top: (clippingClientRect.top - elementClientRect.top + paddingObject.top) / offsetScale.y,
bottom:
(elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom) / offsetScale.y,
left: (clippingClientRect.left - elementClientRect.left + paddingObject.left) / offsetScale.x,
right:
(elementClientRect.right - clippingClientRect.right + paddingObject.right) / offsetScale.x
}
}

@ -36,7 +36,8 @@ export type ClippingContext = {
}
/** State handed to each middleware. All layout already read into `rects` +
* `clipping` + `arrowDimensions`; middleware are pure (no DOM read). */
* `clipping` + `arrowDimensions` + the conversion constants; middleware are
* PURE (no DOM read — that's why `dom` is absent here, unlike the config). */
export type MiddlewareState = {
x: number
y: number
@ -48,15 +49,31 @@ export type MiddlewareState = {
readonly elements: FloatingElements
readonly clipping: ClippingContext
readonly arrowDimensions?: Dimensions
readonly dom: ActiveDom
/**
* Offset to convert an offsetParent-relative rect → viewport coordinates,
* precomputed once in the read phase: `convert({0,0,0,0}) → {x,y}`. The
* conversion is `viewport = offsetParentRelative + viewportDelta` (the
* scroll/offset terms are independent of the rect being converted), so
* `detectOverflow` converts purely. See `compute.ts`.
*/
readonly viewportDelta: Coords
/** The offsetParent's CSS scale (`{1,1}` when it isn't a scaled element).
* `detectOverflow` divides its result by this — matching floating-ui. */
readonly offsetScale: Coords
/** Floating element's resolved `direction === 'rtl'` (read once upfront). */
readonly rtl: boolean
}
export type MiddlewareReturn = {
x?: number
y?: number
data?: Record<string, unknown>
/** Restart the chain (flip): recompute base coords for the new placement. */
reset?: { placement?: Placement }
/**
* Restart the chain. `{ placement }` recomputes base coords for the new
* placement (flip); a bare `true` restarts WITHOUT recomputing coords —
* the arrow uses it after applying an alignment offset.
*/
reset?: boolean | { placement?: Placement }
}
export type Middleware = {

@ -1,14 +1,25 @@
import {
type Middleware,
arrow,
autoUpdate,
flip,
hide,
limitShift,
offset,
shift,
size
arrow as fuiArrow,
autoUpdate as fuiAutoUpdate,
flip as fuiFlip,
hide as fuiHide,
limitShift as fuiLimitShift,
offset as fuiOffset,
shift as fuiShift,
size as fuiSize
} from '@floating-ui/dom';
import {
arrow as ownArrow,
flip as ownFlip,
hide as ownHide,
limitShift as ownLimitShift,
offset as ownOffset,
shift as ownShift,
size as ownSize
} from './engine/middleware';
import { autoUpdate as ownAutoUpdate } from './engine/auto-update';
import { USE_OWN_ENGINE } from './engine/flag';
import type { Middleware } from './types';
import { attachRef, type RefAttachment } from '$libs/reactive';
import { cssToStyleObj, styleToString } from '../../css';
import type { ActiveDom } from '$adom';
@ -24,6 +35,21 @@ import { useFloating } from './use-floating.svelte';
import type { Measurable, UseFloatingReturn } from './types';
import { OPPOSITE_SIDE, type Align, type Boundary, type Placement, type Side } from './placement';
// A/B middleware factory set — own engine or @floating-ui, selected once via the
// flag. Typed loosely so the single `middleware` array expression below serves
// both implementations (the option shapes are structurally equivalent for our
// usage). Removed in P4 together with the @floating-ui imports + the flag.
type MiddlewareFactory = (options?: unknown) => Middleware;
type LimiterFactory = (options?: unknown) => unknown;
const offset = (USE_OWN_ENGINE ? ownOffset : fuiOffset) as unknown as MiddlewareFactory;
const shift = (USE_OWN_ENGINE ? ownShift : fuiShift) as unknown as MiddlewareFactory;
const flip = (USE_OWN_ENGINE ? ownFlip : fuiFlip) as unknown as MiddlewareFactory;
const size = (USE_OWN_ENGINE ? ownSize : fuiSize) as unknown as MiddlewareFactory;
const arrow = (USE_OWN_ENGINE ? ownArrow : fuiArrow) as unknown as MiddlewareFactory;
const hide = (USE_OWN_ENGINE ? ownHide : fuiHide) as unknown as MiddlewareFactory;
const limitShift = (USE_OWN_ENGINE ? ownLimitShift : fuiLimitShift) as unknown as LimiterFactory;
// ── FloatingProvider ─────────────────────────────────────────────────────────
export interface FloatingProviderOpts {
@ -156,7 +182,15 @@ export class FloatingContent {
this.opts.avoidCollisions.current && flip({ ...this.detectOverflowOptions }),
size({
...this.detectOverflowOptions,
apply: ({ rects, availableWidth, availableHeight }) => {
apply: ({
rects,
availableWidth,
availableHeight
}: {
rects: { reference: { width: number; height: number } };
availableWidth: number;
availableHeight: number;
}) => {
const { width: anchorWidth, height: anchorHeight } = rects.reference;
this.availableWidth = availableWidth;
this.availableHeight = availableHeight;
@ -289,11 +323,15 @@ export class FloatingContent {
placement: () => this.desiredPlacement,
middleware: () => this.middleware,
reference: this.provider.anchorNode,
whileElementsMounted: (...args) => {
const cleanup = autoUpdate(...args, {
animationFrame: this.updatePositionStrategy?.current === 'always'
});
return cleanup;
dom: this.provider.opts.dom,
whileElementsMounted: (reference, floating, update) => {
const animationFrame = this.updatePositionStrategy?.current === 'always';
return USE_OWN_ENGINE
? ownAutoUpdate(reference, floating, update, {
animationFrame,
dom: this.provider.opts.dom
})
: fuiAutoUpdate(reference, floating, update, { animationFrame });
},
open: () => this.opts.enabled.current,
sideOffset: () => this.opts.sideOffset.current,

@ -1,4 +1,5 @@
import type { Middleware } from '@floating-ui/dom';
import type { Middleware } from './engine/types';
import type { ActiveDom } from '$adom';
import type {
Active,
State
@ -9,6 +10,10 @@ import type { Align, Boundary, Placement, Side, Strategy } from './placement';
// ─── Shared ───────────────────────────────────────────────────────────────────
// The middleware contract now lives in `engine/types` (de-vendored from
// `@floating-ui/dom`). Re-exported here so `./types` stays the public surface.
export type { Middleware } from './engine/types';
export type Measurable = {
getBoundingClientRect: () => DOMRect;
};
@ -56,6 +61,9 @@ export type UseFloatingOptions = {
sideOffset?: () => number;
/** Align offset — used for bad-coordinate detection during transitions. @default 0 */
alignOffset?: () => number;
/** DOM runtime — threaded to the in-house engine's read phase / autoUpdate
* when `USE_OWN_ENGINE` is on. Absent → the `@floating-ui` path. */
dom?: ActiveDom;
};
// ─── useFloating Return ──────────────────────────────────────────────────────

@ -1,5 +1,5 @@
import { computePosition } from '@floating-ui/dom';
import type { Middleware } from '@floating-ui/dom';
import { computePosition as fuiComputePosition } from '@floating-ui/dom';
import type { Middleware as FuiMiddleware } from '@floating-ui/dom';
import {
readableActive,
state
@ -7,6 +7,8 @@ import {
import { getDPR, roundByDPR, isReferenceHidden } from './utils';
import type { MiddlewareData, UseFloatingOptions, UseFloatingReturn } from './types';
import type { Placement, Strategy } from './placement';
import { computePosition as ownComputePosition } from './engine/compute';
import { USE_OWN_ENGINE } from './engine/flag';
export function useFloating(options: UseFloatingOptions): UseFloatingReturn {
const whileElementsMountedOption = options.whileElementsMounted;
@ -18,6 +20,7 @@ export function useFloating(options: UseFloatingOptions): UseFloatingReturn {
const sideOffsetOption = $derived(options.sideOffset?.() ?? 0);
const alignOffsetOption = $derived(options.alignOffset?.() ?? 0);
const reference = options.reference;
const domOption = options.dom;
let x = $state(0);
let y = $state(0);
@ -57,11 +60,25 @@ export function useFloating(options: UseFloatingOptions): UseFloatingReturn {
function update() {
if (reference.current === null || floating.current === null) return;
computePosition(reference.current, floating.current, {
middleware: middlewareOption,
placement: placementOption,
strategy: strategyOption
}).then((position) => {
// A/B: own engine when the flag is on AND a `dom` runtime was threaded
// through; otherwise the `@floating-ui` path. The `as unknown as` cast
// crosses the two structurally-similar middleware shapes — removed in P4
// when `@floating-ui` (and the flag) go away.
const positioned =
USE_OWN_ENGINE && domOption
? ownComputePosition(reference.current, floating.current, {
middleware: middlewareOption,
placement: placementOption,
strategy: strategyOption,
dom: domOption
})
: fuiComputePosition(reference.current, floating.current, {
middleware: middlewareOption as unknown as FuiMiddleware[],
placement: placementOption,
strategy: strategyOption
});
positioned.then((position) => {
const referenceNode = reference.current;
const referenceHidden = isReferenceHidden(referenceNode);
if (referenceHidden) {

Loading…
Cancel
Save

Powered by TurnKey Linux.