feat(overlays): roll out avoidCollisions:'flip' across the overlay family

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 <noreply@anthropic.com>
menubar-v4-safe
dev 3 months ago
parent 0a3e0ebf29
commit 557a410173

@ -532,7 +532,7 @@ interface ComboboxContentOpts
align: Align;
alignOffset: number;
arrowPadding: number;
avoidCollisions: boolean;
avoidCollisions: boolean | 'flip';
collisionBoundary: Arrayable<Boundary>;
collisionPadding: number | Partial<Record<Side, number>>;
sticky: 'partial' | 'always';

@ -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<Boundary>;
/** Padding from collision boundary in px. @default 0 */

@ -211,7 +211,7 @@ interface ContextMenuContentOpts
align: Align;
alignOffset: number;
arrowPadding: number;
avoidCollisions: boolean;
avoidCollisions: boolean | 'flip';
collisionBoundary: Arrayable<Boundary>;
collisionPadding: number | Partial<Record<Side, number>>;
sticky: 'partial' | 'always';
@ -958,7 +958,7 @@ interface ContextMenuSubContentOpts
sideOffset: number;
align: Align;
alignOffset: number;
avoidCollisions: boolean;
avoidCollisions: boolean | 'flip';
collisionBoundary: Arrayable<Boundary>;
collisionPadding: number | Partial<Record<Side, number>>;
loop: boolean;

@ -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<Boundary>;
/** @default 0 */ collisionPadding?: number | Partial<Record<Side, number>>;
/** @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<Boundary>;
/** @default 0 */ collisionPadding?: number | Partial<Record<Side, number>>;
/** @default false */ loop?: boolean;

@ -233,7 +233,7 @@ interface MenuContentOpts
align: Align;
alignOffset: number;
arrowPadding: number;
avoidCollisions: boolean;
avoidCollisions: boolean | 'flip';
collisionBoundary: Arrayable<Boundary>;
collisionPadding: number | Partial<Record<Side, number>>;
sticky: 'partial' | 'always';
@ -1101,7 +1101,7 @@ interface MenuSubContentOpts
sideOffset: number;
align: Align;
alignOffset: number;
avoidCollisions: boolean;
avoidCollisions: boolean | 'flip';
collisionBoundary: Arrayable<Boundary>;
collisionPadding: number | Partial<Record<Side, number>>;
loop: boolean;

@ -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<Boundary>;
/** @default 0 */ collisionPadding?: number | Partial<Record<Side, number>>;
/** @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<Boundary>;
/** @default 0 */ collisionPadding?: number | Partial<Record<Side, number>>;
/** @default false */ loop?: boolean;

@ -239,7 +239,7 @@ interface LinkPreviewContentOpts
align: Align;
alignOffset: number;
arrowPadding: number;
avoidCollisions: boolean;
avoidCollisions: boolean | 'flip';
collisionBoundary: Arrayable<Boundary>;
collisionPadding: number | Partial<Record<Side, number>>;
sticky: 'partial' | 'always';

@ -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<Boundary>;
/** @default 0 */ collisionPadding?: number | Partial<Record<Side, number>>;
/** @default 'partial' */ sticky?: 'partial' | 'always';

@ -445,7 +445,7 @@ interface SelectContentOpts
align: Align;
alignOffset: number;
arrowPadding: number;
avoidCollisions: boolean;
avoidCollisions: boolean | 'flip';
collisionBoundary: Arrayable<Boundary>;
collisionPadding: number | Partial<Record<Side, number>>;
sticky: 'partial' | 'always';

@ -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<Boundary>;
/** Padding from collision boundary in px. @default 0 */

@ -384,7 +384,7 @@ interface TooltipContentOpts
align: Align;
alignOffset: number;
arrowPadding: number;
avoidCollisions: boolean;
avoidCollisions: boolean | 'flip';
collisionBoundary: Arrayable<Boundary>;
collisionPadding: number | Partial<Record<Side, number>>;
sticky: 'partial' | 'always';

@ -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<Boundary>;
/** Padding from collision boundary in px. @default 8 */

@ -18,7 +18,7 @@
let sideOffset = $state<number | undefined>(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 @@
' <ContextMenu.Content',
side !== 'bottom' && ` side="${side}"`,
align !== 'start' && ` align="${align}"`,
!avoidCollisions && ' avoidCollisions={false}',
avoidCollisions !== true &&
` avoidCollisions={${avoidCollisions === 'flip' ? "'flip'" : 'false'}}`,
loop && ' loop',
' >',
showGroupHeading && ' <ContextMenu.Group>',
@ -394,10 +396,11 @@
<input type="range" min="-24" max="24" step="1" bind:value={alignOffset} />
</label>
<label data-uix-control>
<span data-uix-control-label>avoidCollisions</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={avoidCollisions} />
<span data-uix-switch-label>{avoidCollisions ? 'on' : 'off'}</span>
<span data-uix-control-label>avoidCollisions <span data-uix-control-hint>flip → native CSS</span></span>
<span data-uix-chips role="radiogroup">
<button data-uix-chip data-active={avoidCollisions === true} onclick={() => (avoidCollisions = true)}>full</button>
<button data-uix-chip data-active={avoidCollisions === 'flip'} onclick={() => (avoidCollisions = 'flip')}>flip</button>
<button data-uix-chip data-active={avoidCollisions === false} onclick={() => (avoidCollisions = false)}>off</button>
</span>
</label>
<label data-uix-control>

@ -19,7 +19,7 @@
let sideOffset = $state<number | undefined>(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);
@ -116,7 +116,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={false}',
@ -448,10 +449,11 @@
<input type="range" min="-24" max="24" step="1" bind:value={alignOffset} />
</label>
<label data-uix-control>
<span data-uix-control-label>avoidCollisions</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={avoidCollisions} />
<span data-uix-switch-label>{avoidCollisions ? 'on' : 'off'}</span>
<span data-uix-control-label>avoidCollisions <span data-uix-control-hint>flip → native CSS</span></span>
<span data-uix-chips role="radiogroup">
<button data-uix-chip data-active={avoidCollisions === true} onclick={() => (avoidCollisions = true)}>full</button>
<button data-uix-chip data-active={avoidCollisions === 'flip'} onclick={() => (avoidCollisions = 'flip')}>flip</button>
<button data-uix-chip data-active={avoidCollisions === false} onclick={() => (avoidCollisions = false)}>off</button>
</span>
</label>
<label data-uix-control>

@ -26,7 +26,7 @@
let sideOffset = $state(8);
let align = $state<Align>('center');
let alignOffset = $state(0);
let avoidCollisions = $state(true);
let avoidCollisions: boolean | 'flip' = $state(true);
// Content (Eidos) props
let size = $state<LinkPreviewSize>('md');
// Composition
@ -96,7 +96,8 @@
side !== 'top' && ` side="${side}"`,
align !== 'center' && ` align="${align}"`,
sideOffset !== 8 && ` sideOffset={${sideOffset}}`,
!avoidCollisions && ' avoidCollisions={false}',
avoidCollisions !== true &&
` avoidCollisions={${avoidCollisions === 'flip' ? "'flip'" : 'false'}}`,
' >',
showArrow && ' <LinkPreview.Arrow />',
' <h3>...</h3>',
@ -290,10 +291,11 @@
<input type="range" min="-32" max="32" step="1" bind:value={alignOffset} />
</label>
<label data-uix-control>
<span data-uix-control-label>avoidCollisions</span>
<span data-uix-switch>
<input type="checkbox" bind:checked={avoidCollisions} />
<span data-uix-switch-label>{avoidCollisions ? 'on' : 'off'}</span>
<span data-uix-control-label>avoidCollisions <span data-uix-control-hint>flip → native CSS</span></span>
<span data-uix-chips role="radiogroup">
<button data-uix-chip data-active={avoidCollisions === true} onclick={() => (avoidCollisions = true)}>full</button>
<button data-uix-chip data-active={avoidCollisions === 'flip'} onclick={() => (avoidCollisions = 'flip')}>flip</button>
<button data-uix-chip data-active={avoidCollisions === false} onclick={() => (avoidCollisions = false)}>off</button>
</span>
</label>
</div>

@ -35,7 +35,7 @@
let sideOffset = $state(8);
let align = $state<Align>('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 @@
<input type="range" min="-40" max="40" step="1" bind:value={alignOffset} />
</label>
<label data-uix-control>
<span data-uix-control-label>Content · avoidCollisions</span>
<span data-uix-switch
><input type="checkbox" bind:checked={avoidCollisions} /><span data-uix-switch-label
>{avoidCollisions ? 'on' : 'off'}</span
></span
>
<span data-uix-control-label>Content · avoidCollisions <span data-uix-control-hint>flip → native CSS</span></span>
<span data-uix-chips role="radiogroup">
<button data-uix-chip data-active={avoidCollisions === true} onclick={() => (avoidCollisions = true)}>full</button>
<button data-uix-chip data-active={avoidCollisions === 'flip'} onclick={() => (avoidCollisions = 'flip')}>flip</button>
<button data-uix-chip data-active={avoidCollisions === false} onclick={() => (avoidCollisions = false)}>off</button>
</span>
</label>
<label data-uix-control>
<span data-uix-control-label>Content · sticky</span>

Loading…
Cancel
Save

Powered by TurnKey Linux.