Skip to content

fix(webkit): [ENG-47063] flip, then shrink and scroll floating panels instead of clipping them - #973

Merged
isaque-bock-azion merged 6 commits into
mainfrom
fix/ENG-47063-size-aware-placement
Sep 18, 2026
Merged

isaque-bock-azion merged 6 commits into
mainfrom
fix/ENG-47063-size-aware-placement

Conversation

@isaque-bock-azion

@isaque-bock-azion isaque-bock-azion commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • usePlacement is now size-aware: flip first, and when no side fits, cap the panel to the free space (maxHeight/maxWidth in panelStyle) so it scrolls internally instead of overlapping its trigger or clipping at the viewport edge. The boundary is the nearest scrolling ancestor of the trigger intersected with the viewport, not the window.
  • One positioning implementation remains: select-content and multi-select-content migrate onto usePlacement (the getFixedFrame Storybook-zoom correction moved into the composable, plus a matchTriggerWidth option), and the NavigationMenu positioner becomes a thin wrapper over the exported pure computePlacement / getClippingBoundary, keeping only its arrow, rAF coalescing and settled-size handling. Its --available-height / --available-width now carry the real cap instead of a hardcoded 100vh / 100vw, and the viewport scrolls while data-constrained.
  • Popover, Dropdown, Calendar, Table filter/column selector and DocTooltip panels gain overflow-y-auto / min-h-0 so the computed cap produces scroll; new browser-mode tests cover flip, shrink and the scroll-container boundary for the composable, Select, MultiSelect, Popover and NavigationMenu.

How to test

  1. pnpm webkit:test — 109 files pass, including the new src/composables/use-placement/use-placement.test.ts and the flip/shrink cases in select.test.ts, multi-select.test.ts, popover.test.ts and navigation-menu.test.ts.
  2. pnpm storybook:dev, open Components/Inputs/Select → LongList, shrink the browser window to ~420px tall and open the select: the list is capped to the space below the trigger, ends 8px above the viewport edge and scrolls inside the panel.
  3. In the same story, move the trigger near the bottom of the preview (e.g. add style="position:fixed;bottom:8px" to #storybook-root in DevTools) and open it: the list opens above the trigger (--popup-origin: bottom left), never overlapping it.
  4. Set the Storybook zoom to 125% and open the select: the panel stays aligned to the trigger's left edge and width.
  5. Open Components/Navigation/NavigationMenu → Default, shrink the window to ~260px tall and hover "Soluções": the popup is capped 8px above the viewport edge, the positioner carries data-constrained, the panel scrolls, and the arrow stays centred on the trigger. Hover "Produtos": the box morphs to the new width and keeps the cap.
  6. pnpm --filter webkit size — every budgeted entry is within its (raised, see Notes) budget.

Notes

  • Related issue: ENG-47063 — all four work items and every acceptance criterion are in this PR.
  • Behaviour change (not an API break): an explicit placement that fits on neither side now resolves to the roomier side and is capped, instead of being pinned to the top edge over its trigger. panelStyle / resolvedPlacement are now Readonly<Ref>; all 8 call sites keep working unchanged. The NavigationMenu positioner's public props are unchanged; it gains a data-constrained attribute.
  • Bundle budgets raised by 0.5 KB each for calendar-root, chip, doc-prompt, doc-tooltip, doc-page-header: the composable grew ~0.9 KB gzip (size cap, scroll-ancestor boundary, frame correction, ResizeObserver), inherited by every overlay consumer. The old code lacked all three behaviours the ticket requires, so the growth is the feature, not slack.
  • Select's --popup-origin changes from top to top left, so the open-state visual baselines for Select/MultiSelect may need regenerating.
  • The 4 "ResizeObserver loop completed with undelivered notifications" lines in the full Vitest run are pre-existing on main (same count, verified against 444e357).
  • Spotted, out of scope: multi-select-content lacks the trigger-contains guard in onDocumentPointerDown that Select got in ENG-46739; .specs/select.md is still status: draft and both select specs describe a Popover-API implementation that never shipped; NavigationMenuPositioner's sticky prop is declared but never read.

… instead of clipping them

Every anchored panel (Select, MultiSelect, Calendar, Popover, Dropdown, Tooltip,
the Table filter and column selector) positioned itself without checking whether
it fit: usePlacement flipped but pinned an oversized panel to the top edge over
its own trigger, and select-content / multi-select-content hand-rolled a
"below, always" position with a fixed max-h-[20rem].

usePlacement (now index.ts) resolves the side flip-first and shrink-second,
exposing maxHeight / maxWidth in panelStyle so the panel scrolls internally; the
collision boundary is the viewport intersected with the trigger's scrolling
ancestors (a Drawer body, a ScrollArea), not the window; the getFixedFrame
correction for a transformed Teleport target moves into the composable; and a
matchTriggerWidth option keeps list boxes as wide as their field. The pure
geometry is exported as computePlacement / getClippingBoundary so the
NavigationMenu positioner can wrap it in a follow-up. Select and MultiSelect
drop their hand-rolled positioning; the other consumers gain overflow-y-auto so
the cap scrolls instead of clipping.

Five bundle budgets rise by 0.5 KB (calendar-root, chip, doc-prompt,
doc-tooltip, doc-page-header): the composable grew ~0.9 KB gzip for the size,
boundary and frame logic, which every overlay consumer inherits.
…e shared placement geometry

use-navigation-menu-positioner.js drops its own flip/shift maths and calls the
exported computePlacement / getClippingBoundary, so one positioning
implementation remains in the package. It keeps what is specific to the menu:
the settled popup size as the measured box, the arrow, rAF-coalesced updates
and the first-placement transition guard.

--available-width / --available-height were hardcoded to 100vw / 100vh; they
now carry the real cap when the popup fits on neither side. The caps move from
the positioner to the popup so the positioner still shrink-wraps for the
natural-size measurement, measureNatural lifts any inline max-* while it
measures so the cap cannot feed back into the size it is computed from, and the
viewport scrolls (overflow-y-auto) only while the positioner is
data-constrained, so an unconstrained morph never flashes a scrollbar.
computePlacement gains alignOffset for the positioner's align-axis offset.
…ink-and-scroll placement

The GroupsWithTopAndBottomSlots panel (355px) no longer fits under the trigger at 1280x720, so the placement caps it to the free space (328px) and lets it scroll instead of shifting it up over the trigger. Baselines regenerated by the Storybook Regenerate Baseline workflow for dark-desktop and light-desktop.
@isaque-bock-azion
isaque-bock-azion merged commit d9b5fba into main Sep 18, 2026
23 checks passed
@isaque-bock-azion
isaque-bock-azion deleted the fix/ENG-47063-size-aware-placement branch September 18, 2026 14:38
@github-actions github-actions Bot mentioned this pull request Sep 18, 2026
gabriel-lisboa-azion added a commit that referenced this pull request Sep 21, 2026
demo/vue-sample had drifted 540 ahead / 31 behind main while most of its
webkit work was reviewed and landed upstream (#941 command-menu, #942
GlobalHeader placements, #943 NavigationMenu, #952 Menu, #960 sidebar
collapse, #973 floating-panel placement, #975 paginator, #977 templates
exports, #921 amount ladder), after which main swept comments (#951) and
reworked the CLI onto a derived bundle (#968/#970).

Main is the source for everything the two share. Of the 28 conflicted
source files, 17 differed from main only in comments — their code was
byte-identical — and 6 more were genuinely newer on main, so all 23 take
main verbatim, including the use-placement TypeScript rewrite that
replaces the old index.js.

Three carve-outs keep demo work main does not have yet:

- sidebar: main's CSS-driven collapse is the base; the `side` axis is
  re-applied on top of it as data-[side=end] variants rather than the JS
  edge map it used to be, and the drag sign moves into the rail
  composable. The one test that asserted the removed inline transform now
  asserts the panel's published edge, since the direction is CSS the
  unstyled test env cannot see.
- theme: texts.data.js keeps demo's 13 extra text-wrap and 10 extra
  tracking declarations (PR #982, in flight). Both sides carry the same
  34 entries, so an entry count would not have caught the loss.
- doc-prose keeps its chrome-padding fix (PR #981, in flight).

catalog.json, authoring-baseline.json, the theme dist and the lockfile
are regenerated rather than hand-resolved; the 13 conflicted visual
baselines take main's and are regenerated from CI next.

Verified: every file main touched is byte-identical to main except the
22 accounted for above; 2364 browser tests, 179 toolkit tests, lint,
vue-tsc, 99.98% type coverage, both ratchets, the Storybook build and
the webkit-sample build all pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

4 participants