From 557a410173c582dc56823cdf75cc6f732a6b3190 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 1 Jul 2026 13:11:54 +0200 Subject: [PATCH] feat(overlays): roll out avoidCollisions:'flip' across the overlay family MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Widens `avoidCollisions` from `boolean` to `boolean | 'flip'` on the 6 overlays that expose it (combobox, context-menu, dropdown-menu, link-preview, select, tooltip) — the FloatingContent engine already decoupled flip from shift, so 'flip' (flip-only, no slide) works everywhere and lets an arrowless, non-shift overlay take the native CSS-anchor path. Additive: `boolean` is unchanged. - Public prop types + provider opts widened (types.ts + *-provider). JSDoc on the 3 descriptive ones (combobox/select/tooltip) documents the 3 modes; the terse `@default true` menu types keep their inline style (the type shows 'flip'). - Demos: the 4 that had an avoidCollisions checkbox (dropdown-menu, context-menu, link-preview, tooltip) now expose a 3-way full/flip/off control. Verified in a real foreground browser (Claude-in-Chrome): - dropdown-menu + avoidCollisions='flip' + no arrow → routes to NATIVE (data-floating-native, position-anchor link to trigger, zero JS transform, placed correctly). - context-menu correctly STAYS on JS with 'flip' — it anchors to the cursor (a Measurable), which the dispatcher's virtualAnchor gate forces to JS. The discriminator works as designed. - tooltip / link-preview 3-way controls render. 0 new check errors; 41 overlay + floating tests green. Co-Authored-By: Claude Opus 4.8 --- .../combobox/combobox-provider.svelte.ts | 2 +- src/uix/soma/components/combobox/types.ts | 5 +++-- .../context-menu-provider.svelte.ts | 4 ++-- src/uix/soma/components/context-menu/types.ts | 4 ++-- .../dropdown-menu-provider.svelte.ts | 4 ++-- src/uix/soma/components/dropdown-menu/types.ts | 4 ++-- .../link-preview-provider.svelte.ts | 2 +- src/uix/soma/components/link-preview/types.ts | 2 +- .../components/select/select-provider.svelte.ts | 2 +- src/uix/soma/components/select/types.ts | 5 +++-- .../tooltip/tooltip-provider.svelte.ts | 2 +- src/uix/soma/components/tooltip/types.ts | 5 +++-- .../uix/components/context-menu/+page.svelte | 17 ++++++++++------- .../uix/components/dropdown-menu/+page.svelte | 14 ++++++++------ .../uix/components/link-preview/+page.svelte | 14 ++++++++------ web/routes/uix/components/tooltip/+page.svelte | 17 +++++++++-------- 16 files changed, 57 insertions(+), 46 deletions(-) diff --git a/src/uix/soma/components/combobox/combobox-provider.svelte.ts b/src/uix/soma/components/combobox/combobox-provider.svelte.ts index 2d78099a3..d15ec5a57 100644 --- a/src/uix/soma/components/combobox/combobox-provider.svelte.ts +++ b/src/uix/soma/components/combobox/combobox-provider.svelte.ts @@ -532,7 +532,7 @@ interface ComboboxContentOpts align: Align; alignOffset: number; arrowPadding: number; - avoidCollisions: boolean; + avoidCollisions: boolean | 'flip'; collisionBoundary: Arrayable; collisionPadding: number | Partial>; sticky: 'partial' | 'always'; diff --git a/src/uix/soma/components/combobox/types.ts b/src/uix/soma/components/combobox/types.ts index 92b1b0244..469e7ea6b 100644 --- a/src/uix/soma/components/combobox/types.ts +++ b/src/uix/soma/components/combobox/types.ts @@ -91,8 +91,9 @@ export type ComboboxContentProps = WithChild< alignOffset?: number; /** Padding between arrow and content edges. @default 0 */ arrowPadding?: number; - /** Reposition to avoid viewport collisions. @default true */ - avoidCollisions?: boolean; + /** Collision avoidance vs the viewport — `true` = flip + shift, `'flip'` = + * flip-only (no slide; native CSS-anchor capable), `false` = none. @default true */ + avoidCollisions?: boolean | 'flip'; /** Collision boundary elements. @default [] */ collisionBoundary?: Arrayable; /** Padding from collision boundary in px. @default 0 */ diff --git a/src/uix/soma/components/context-menu/context-menu-provider.svelte.ts b/src/uix/soma/components/context-menu/context-menu-provider.svelte.ts index 705062c91..83d7f490c 100644 --- a/src/uix/soma/components/context-menu/context-menu-provider.svelte.ts +++ b/src/uix/soma/components/context-menu/context-menu-provider.svelte.ts @@ -211,7 +211,7 @@ interface ContextMenuContentOpts align: Align; alignOffset: number; arrowPadding: number; - avoidCollisions: boolean; + avoidCollisions: boolean | 'flip'; collisionBoundary: Arrayable; collisionPadding: number | Partial>; sticky: 'partial' | 'always'; @@ -958,7 +958,7 @@ interface ContextMenuSubContentOpts sideOffset: number; align: Align; alignOffset: number; - avoidCollisions: boolean; + avoidCollisions: boolean | 'flip'; collisionBoundary: Arrayable; collisionPadding: number | Partial>; loop: boolean; diff --git a/src/uix/soma/components/context-menu/types.ts b/src/uix/soma/components/context-menu/types.ts index e82e2bb6f..ca4c67303 100644 --- a/src/uix/soma/components/context-menu/types.ts +++ b/src/uix/soma/components/context-menu/types.ts @@ -51,7 +51,7 @@ export type ContextMenuContentProps = WithChild< /** @default 'start' */ align?: Align; /** @default 0 */ alignOffset?: number; /** @default 0 */ arrowPadding?: number; - /** @default true */ avoidCollisions?: boolean; + /** @default true */ avoidCollisions?: boolean | 'flip'; collisionBoundary?: Arrayable; /** @default 0 */ collisionPadding?: number | Partial>; /** @default 'partial' */ sticky?: 'partial' | 'always'; @@ -155,7 +155,7 @@ export type ContextMenuSubContentProps = WithChild< /** @default 0 */ sideOffset?: number; /** @default 'start' */ align?: Align; /** @default 0 */ alignOffset?: number; - /** @default true */ avoidCollisions?: boolean; + /** @default true */ avoidCollisions?: boolean | 'flip'; collisionBoundary?: Arrayable; /** @default 0 */ collisionPadding?: number | Partial>; /** @default false */ loop?: boolean; diff --git a/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.ts b/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.ts index 734e1858c..614e9ee1c 100644 --- a/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.ts +++ b/src/uix/soma/components/dropdown-menu/dropdown-menu-provider.svelte.ts @@ -233,7 +233,7 @@ interface MenuContentOpts align: Align; alignOffset: number; arrowPadding: number; - avoidCollisions: boolean; + avoidCollisions: boolean | 'flip'; collisionBoundary: Arrayable; collisionPadding: number | Partial>; sticky: 'partial' | 'always'; @@ -1101,7 +1101,7 @@ interface MenuSubContentOpts sideOffset: number; align: Align; alignOffset: number; - avoidCollisions: boolean; + avoidCollisions: boolean | 'flip'; collisionBoundary: Arrayable; collisionPadding: number | Partial>; loop: boolean; diff --git a/src/uix/soma/components/dropdown-menu/types.ts b/src/uix/soma/components/dropdown-menu/types.ts index 8185adf1a..98590f9ff 100644 --- a/src/uix/soma/components/dropdown-menu/types.ts +++ b/src/uix/soma/components/dropdown-menu/types.ts @@ -43,7 +43,7 @@ export type MenuContentProps = WithChild< /** @default 'start' */ align?: Align; /** @default 0 */ alignOffset?: number; /** @default 0 */ arrowPadding?: number; - /** @default true */ avoidCollisions?: boolean; + /** @default true */ avoidCollisions?: boolean | 'flip'; collisionBoundary?: Arrayable; /** @default 0 */ collisionPadding?: number | Partial>; /** @default 'partial' */ sticky?: 'partial' | 'always'; @@ -173,7 +173,7 @@ export type MenuSubContentProps = WithChild< /** @default 0 */ sideOffset?: number; /** @default 'start' */ align?: Align; /** @default 0 */ alignOffset?: number; - /** @default true */ avoidCollisions?: boolean; + /** @default true */ avoidCollisions?: boolean | 'flip'; collisionBoundary?: Arrayable; /** @default 0 */ collisionPadding?: number | Partial>; /** @default false */ loop?: boolean; diff --git a/src/uix/soma/components/link-preview/link-preview-provider.svelte.ts b/src/uix/soma/components/link-preview/link-preview-provider.svelte.ts index 58f30612c..4bd21a6a2 100644 --- a/src/uix/soma/components/link-preview/link-preview-provider.svelte.ts +++ b/src/uix/soma/components/link-preview/link-preview-provider.svelte.ts @@ -239,7 +239,7 @@ interface LinkPreviewContentOpts align: Align; alignOffset: number; arrowPadding: number; - avoidCollisions: boolean; + avoidCollisions: boolean | 'flip'; collisionBoundary: Arrayable; collisionPadding: number | Partial>; sticky: 'partial' | 'always'; diff --git a/src/uix/soma/components/link-preview/types.ts b/src/uix/soma/components/link-preview/types.ts index 5bd0d5df8..66d95d617 100644 --- a/src/uix/soma/components/link-preview/types.ts +++ b/src/uix/soma/components/link-preview/types.ts @@ -57,7 +57,7 @@ export type LinkPreviewContentProps = WithChild< /** @default 'center' */ align?: Align; /** @default 0 */ alignOffset?: number; /** @default 0 */ arrowPadding?: number; - /** @default true */ avoidCollisions?: boolean; + /** @default true */ avoidCollisions?: boolean | 'flip'; collisionBoundary?: Arrayable; /** @default 0 */ collisionPadding?: number | Partial>; /** @default 'partial' */ sticky?: 'partial' | 'always'; diff --git a/src/uix/soma/components/select/select-provider.svelte.ts b/src/uix/soma/components/select/select-provider.svelte.ts index ed5b09ae8..a72a6466e 100644 --- a/src/uix/soma/components/select/select-provider.svelte.ts +++ b/src/uix/soma/components/select/select-provider.svelte.ts @@ -445,7 +445,7 @@ interface SelectContentOpts align: Align; alignOffset: number; arrowPadding: number; - avoidCollisions: boolean; + avoidCollisions: boolean | 'flip'; collisionBoundary: Arrayable; collisionPadding: number | Partial>; sticky: 'partial' | 'always'; diff --git a/src/uix/soma/components/select/types.ts b/src/uix/soma/components/select/types.ts index 66a8c5635..ba7f9f5e8 100644 --- a/src/uix/soma/components/select/types.ts +++ b/src/uix/soma/components/select/types.ts @@ -83,8 +83,9 @@ export type SelectContentProps = WithChild< alignOffset?: number; /** Padding between arrow and content edges. @default 0 */ arrowPadding?: number; - /** Reposition to avoid viewport collisions. @default true */ - avoidCollisions?: boolean; + /** Collision avoidance vs the viewport — `true` = flip + shift, `'flip'` = + * flip-only (no slide; native CSS-anchor capable), `false` = none. @default true */ + avoidCollisions?: boolean | 'flip'; /** Collision boundary elements. @default [] */ collisionBoundary?: Arrayable; /** Padding from collision boundary in px. @default 0 */ diff --git a/src/uix/soma/components/tooltip/tooltip-provider.svelte.ts b/src/uix/soma/components/tooltip/tooltip-provider.svelte.ts index 19c9b9a18..36e1a8abc 100644 --- a/src/uix/soma/components/tooltip/tooltip-provider.svelte.ts +++ b/src/uix/soma/components/tooltip/tooltip-provider.svelte.ts @@ -384,7 +384,7 @@ interface TooltipContentOpts align: Align; alignOffset: number; arrowPadding: number; - avoidCollisions: boolean; + avoidCollisions: boolean | 'flip'; collisionBoundary: Arrayable; collisionPadding: number | Partial>; sticky: 'partial' | 'always'; diff --git a/src/uix/soma/components/tooltip/types.ts b/src/uix/soma/components/tooltip/types.ts index 6b5be22ed..4bf2f76a0 100644 --- a/src/uix/soma/components/tooltip/types.ts +++ b/src/uix/soma/components/tooltip/types.ts @@ -73,8 +73,9 @@ export type TooltipContentProps = WithChild< alignOffset?: number; /** Padding between arrow and content edges. @default 0 */ arrowPadding?: number; - /** Reposition to avoid viewport collisions. @default true */ - avoidCollisions?: boolean; + /** Collision avoidance vs the viewport — `true` = flip + shift, `'flip'` = + * flip-only (no slide; native CSS-anchor capable), `false` = none. @default true */ + avoidCollisions?: boolean | 'flip'; /** Collision boundary elements. @default [] */ collisionBoundary?: Arrayable; /** Padding from collision boundary in px. @default 8 */ diff --git a/web/routes/uix/components/context-menu/+page.svelte b/web/routes/uix/components/context-menu/+page.svelte index 2c4c0fa53..b02911b3e 100644 --- a/web/routes/uix/components/context-menu/+page.svelte +++ b/web/routes/uix/components/context-menu/+page.svelte @@ -18,7 +18,7 @@ let sideOffset = $state(undefined); let align = $state<'start' | 'center' | 'end'>('start'); let alignOffset = $state(0); - let avoidCollisions = $state(true); + let avoidCollisions: boolean | 'flip' = $state(true); let loop = $state(false); let trapFocus = $state(false); let preventScroll = $state(false); @@ -102,7 +102,8 @@ sideOffset !== undefined && ` sideOffset={${sideOffset}}`, align !== 'start' && ` align="${align}"`, alignOffset !== 0 && ` alignOffset={${alignOffset}}`, - !avoidCollisions && ' avoidCollisions={false}', + avoidCollisions !== true && + ` avoidCollisions={${avoidCollisions === 'flip' ? "'flip'" : 'false'}}`, loop && ' loop', !trapFocus && ' trapFocus={false}', preventScroll && ' preventScroll', @@ -155,7 +156,8 @@ ' ', showGroupHeading && ' ', @@ -394,10 +396,11 @@ diff --git a/web/routes/uix/components/tooltip/+page.svelte b/web/routes/uix/components/tooltip/+page.svelte index bf1414648..36a652d91 100644 --- a/web/routes/uix/components/tooltip/+page.svelte +++ b/web/routes/uix/components/tooltip/+page.svelte @@ -35,7 +35,7 @@ let sideOffset = $state(8); let align = $state('center'); let alignOffset = $state(0); - let avoidCollisions = $state(true); + let avoidCollisions: boolean | 'flip' = $state(true); let sticky = $state<'partial' | 'always'>('partial'); let hideWhenDetached = $state(true); let forceMount = $state(false); @@ -107,7 +107,8 @@ sideOffset !== 0 && ` sideOffset={${sideOffset}}`, align !== 'center' && ` align="${align}"`, alignOffset !== 0 && ` alignOffset={${alignOffset}}`, - !avoidCollisions && ' avoidCollisions={false}', + avoidCollisions !== true && + ` avoidCollisions={${avoidCollisions === 'flip' ? "'flip'" : 'false'}}`, sticky !== 'partial' && ` sticky="${sticky}"`, !hideWhenDetached && ' hideWhenDetached={false}', forceMount && ' forceMount', @@ -382,12 +383,12 @@