Repository navigation
fix(HoverCard,Tooltip): wait for the trigger to have a layout box before showing - #5398
HelloOjasMutreja wants to merge 2 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR Analysis Report📚 Storybook Preview🧪 Sandbox PreviewComponent Size
Sizes are gzip. Import counts the JavaScript the import loads eagerly; CSS counts the package stylesheet rules referenced by the modules it can load from its own package, lazily loaded ones included. Bundle Size SummaryGzip figures measure each package's entry file, not the code an import pulls in.
Accessibility AuditStatus: No accessibility violations detected. Visual Regression55 of 500 shot(s) changed. A change here is a question, not a failure: check whether the after is the
and 35 more. Generated by PR Enrichment workflow | Storybook | Sandbox | View full report |
a413d10 to
39af550
Compare
39af550 to
76bdb39
Compare
cixzhang
left a comment
There was a problem hiding this comment.
Thanks—HoverCard now anchors correctly, but Tooltip still leaves the user’s hint detached at the viewport corner, and this head also makes the UI suite red.
packages/core/src/Layer/showWhenAnchored.ts:27— In Chromium, the controlled Tooltip-in-Dialog story still opens at(0, 4)while its trigger is around(206, 298). The same exact-head run centers HoverCard correctly. Please keep trigger readiness in Layer’s existing lifecycle owner rather than adding a caller-level timing abstraction, and cover both controlled paths in a real-browser regression.internal/test-utils/src/setup.ts:115— This global ResizeObserver stub changes unrelated TabList behavior: the exact-head test callsscrollBytwice; current main passes 74/74, and the head also passes when only this setup change is removed. Please keep the test double local to the affected tests.
If you'd rather talk it through with someone, we're in Discord.
[Reviewed by Robohands]
76bdb39 to
6e4d9f5
Compare
|
Moved trigger-readiness into Tracing this live in Chromium turned up more than the box-readiness gap alone. HoverCard's own controlled-in-Dialog story was passing by coincidence, not because its readiness handling differed from Tooltip's: with the trigger's box confirmed present, Past that, real-Chromium tracing surfaced a second issue: even with a real box, showing a layer while an ancestor's own CSS entry animation (Dialog's scale/translate transition) is still running anchors against a mid-transform position that also never gets revisited, since a transform changes nothing On the Added Also verified: full |
b695d9a to
9ca8b36
Compare
|
Rebased onto current main. One real conflict in playwright.config.ts: main's own Verified after rebase: typecheck clean, and the affected suites (Popover, PowerSearch, Tokenizer, Layer, Tooltip, HoverCard) pass, isolated and in a narrower batch. A full combined run of all six directories intermittently worker-crashes or shows a single unrelated PowerSearchFilterEditor failure in this environment; reproduces identically on a clean, unmodified checkout of current main with the same broad batch, so it is local Vitest worker-pool flakiness under a large parallel run here, not something this PR introduced. |
|
Checked the pr-a11y failure on this run: it's in Checkbox.a11y.chromium.spec.ts (a menu-item inventory mismatch, "Show archived" showing up as unexpectedly inert), a file this PR doesn't touch and unrelated to HoverCard/Tooltip anchoring. Not something this diff caused. |
There was a problem hiding this comment.
Thanks for moving anchor readiness into Layer and adding the real-Chromium Tooltip/HoverCard regression. A few current paths still need correction before this is ready:
show()waits for every trigger or ancestor animation to finish, so an infinitely pulsing trigger such asStatusDotcan prevent its Tooltip from opening forever. Bound the wait to finite entry motion and cover a continuous-animation trigger.- The delayed path switches from
getBoundingClientRect()toResizeObserverEntry.contentRect. Text-only Tooltip/HoverCard triggers are inline spans, whose observed content box can remain empty, so a controlled/default-open layer can stay closed. Use a readiness signal that covers inline triggers and add that browser case. contextRef(null)forgets the old trigger without removing this layer's anchor name. When an external anchor remains mounted or the ref moves, both old and new elements can keep the same association. Preserve transient same-element churn while cleaning a real detach/rebind.- The changeset still names the deleted
showWhenAnchoredhelper; update it to describe the current Layer implementation. - The current Layer contract requires real Chromium and WebKit evidence for anchor-geometry changes, but this regression is Chromium-only. Add the equivalent WebKit proof.
[Automated review]
47f85e1 to
b770b08
Compare
b770b08 to
d554a15
Compare
…izeObserver Two follow-ups from review: - contextRef(null) dropped this layer's anchor name from the previous trigger immediately and discarded the stored ref, so a real detach was never cleaned up (a rebind through two different elements left both carrying the same anchor name; an unmounted-but-still-mounted-elsewhere trigger kept a dead name forever). The removal is now deferred to a microtask: cancelled if the same element reattaches before it runs (the original meta#5398 churn case this file already handled), applied if a different element takes over (immediate, synchronous) or nothing reattaches at all (a genuine detach). Added tests for all three shapes. - The anchor-readiness wait in show() relied on ResizeObserver alone, which reliably never delivers any entry for a purely display:inline, non-replaced box -- exactly what a text-only Tooltip/HoverCard trigger's wrapper <span> is. A closed-then-open Dialog case with a text-only trigger (new story + browser spec case, passing on both Chromium and WebKit) reproduced this: the wait never resolved at all. show() now races a bounded requestAnimationFrame poll of the anchor's own getBoundingClientRect() alongside the ResizeObserver, so either signal can resolve it. The poll is bounded (ANCHOR_WAIT_MAX_FRAMES) so an environment with no real layout at all doesn't reschedule a frame forever under fake timers.
…izeObserver Two follow-ups from review: - contextRef(null) dropped this layer's anchor name from the previous trigger immediately and discarded the stored ref, so a real detach was never cleaned up (a rebind through two different elements left both carrying the same anchor name; an unmounted-but-still-mounted-elsewhere trigger kept a dead name forever). The removal is now deferred to a microtask: cancelled if the same element reattaches before it runs (the original meta#5398 churn case this file already handled), applied if a different element takes over (immediate, synchronous) or nothing reattaches at all (a genuine detach). Added tests for all three shapes. - The anchor-readiness wait in show() relied on ResizeObserver alone, which reliably never delivers any entry for a purely display:inline, non-replaced box -- exactly what a text-only Tooltip/HoverCard trigger's wrapper <span> is. A closed-then-open Dialog case with a text-only trigger (new story + browser spec case, passing on both Chromium and WebKit) reproduced this: the wait never resolved at all. show() now races a bounded requestAnimationFrame poll of the anchor's own getBoundingClientRect() alongside the ResizeObserver, so either signal can resolve it. The poll is bounded (ANCHOR_WAIT_MAX_FRAMES) so an environment with no real layout at all doesn't reschedule a frame forever under fake timers.
d554a15 to
7819e65
Compare
There was a problem hiding this comment.
Thanks — the box wait fixes the reported case: with real motion, the pinned tooltip in a Dialog now sits above its trigger instead of in the corner. Changes are still needed, because show() now defers every context-mode open and this head turns the UI test and story play-guard checks red.
waitForAncestorAnimationsalso runs when the trigger already has a box, so every open in a browser waits at least a frame, a DropdownMenu waits for its own Button's press transition (with an ordinary click it opens on release instead of on press), and a finite animation paused on any ancestor keeps a layer closed — please open synchronously when the trigger has a box and limit any settle wait to the boxless path.- Because opening is no longer synchronous, the
Core/Layerviewport-inset stories find no open layer right after the opening click, and the DateInput/DateTimeInput FR10 suites fail because their no-opResizeObserverstubs never end the boxless wait. - A pending wait keeps polling the element captured when
show()ran, so a boxless trigger that is replaced by another element never opens; please restart the wait on the new trigger and add a test. - Please drop the behavior-neutral
clearTimeouts()hoist inuseTooltipand the unused*.chromium.spec.tsglob, and refresh the PR description, which still describes the removed helper and global polyfill.
[Automated review]
| } | ||
| const rect = anchor.getBoundingClientRect(); | ||
| if (rect.width !== 0 || rect.height !== 0) { | ||
| pendingAnchorWaitRef.current = waitForAncestorAnimations(anchor, openNow); |
There was a problem hiding this comment.
This sends a trigger that already has a box through waitForAncestorAnimations, so every context-mode open now waits at least a frame and then for any finite animation on the trigger or its ancestors. In Chromium with motion on, a DropdownMenu opened with a 90 ms mouse press now opens only after release, once the trigger Button's own transform press transition is cancelled (about 100 ms after press-down, against about 10 ms before), and the Core/Layer viewport-inset stories no longer find the layer open right after the opening click. Could this branch call openNow() directly, keeping any settle wait for the boxless path?
| const running = document.getAnimations().filter(animation => { | ||
| const target = | ||
| animation.effect instanceof KeyframeEffect | ||
| ? animation.effect.target | ||
| : null; | ||
| if ( | ||
| !(target instanceof Node) || | ||
| !(target === anchor || target.contains(anchor)) | ||
| ) { | ||
| return false; | ||
| } | ||
| const timing = animation.effect?.getComputedTiming(); | ||
| return timing?.iterations !== Infinity; | ||
| }); |
There was a problem hiding this comment.
This keeps every finite animation, whatever it animates and whatever its play state: a trigger's color or press transition, a paused animation (its finished never settles while paused — in Chromium a layer under a paused ancestor animation never opened), or a scroll-driven one. If a settle wait stays, consider limiting it to running, document-timeline motion that can actually move the anchor.
| observer.observe(anchor); | ||
| if (!state.resolved) { | ||
| const poll = () => { | ||
| const box = anchor.getBoundingClientRect(); |
There was a problem hiding this comment.
The poll (and the observer at line 1039) stay bound to the element captured when show() ran. If the ref moves to another element while this wait is pending, contextRef moves the anchor name but nothing restarts the wait, so the layer never opens and this loop keeps measuring the old node every frame (a jsdom repro with mocked rects stays closed 0.5 s after the swap). Restarting the wait when a different element attaches, or reading triggerRef.current here, would fix it.
| if (isOpen === undefined) { | ||
| return; | ||
| } | ||
| clearTimeouts(); |
There was a problem hiding this comment.
This hoist doesn't change behavior, but it changes an Effect whose comment doesn't name the external system it synchronizes, which the repo's runtime rules require for any changed Effect. Dropping the hunk keeps the PR focused on the fix.
| 'packages/*/src/**/*.a11y.browser.spec.ts', | ||
| // Real-layout regressions that are not accessibility-tree bindings (e.g. | ||
| // CSS anchor positioning), which jsdom cannot exercise. | ||
| 'packages/*/src/**/*.chromium.spec.ts', |
There was a problem hiding this comment.
Every file this matches under packages/*/src is already matched by *.a11y.chromium.spec.ts, and the new spec is *.a11y.browser.spec.ts, so this glob is unused — can it go?
| observe = vi.fn(); | ||
| // Fires once synchronously on observe(), same as the real thing does | ||
| // for an already-visible element and as the jsdom-wide polyfill in | ||
| // internal/test-utils/src/setup.ts models it — useLayer's own show() |
There was a problem hiding this comment.
There is no jsdom-wide ResizeObserver polyfill in internal/test-utils/src/setup.ts at this head, and a real observer never calls back synchronously inside observe() (it delivers at the next rendering update). The same wording is in the PowerSearch, PowerSearchValueEditor and Tokenizer stubs.
…izeObserver Two follow-ups from review: - contextRef(null) dropped this layer's anchor name from the previous trigger immediately and discarded the stored ref, so a real detach was never cleaned up (a rebind through two different elements left both carrying the same anchor name; an unmounted-but-still-mounted-elsewhere trigger kept a dead name forever). The removal is now deferred to a microtask: cancelled if the same element reattaches before it runs (the original meta#5398 churn case this file already handled), applied if a different element takes over (immediate, synchronous) or nothing reattaches at all (a genuine detach). Added tests for all three shapes. - The anchor-readiness wait in show() relied on ResizeObserver alone, which reliably never delivers any entry for a purely display:inline, non-replaced box -- exactly what a text-only Tooltip/HoverCard trigger's wrapper <span> is. A closed-then-open Dialog case with a text-only trigger (new story + browser spec case, passing on both Chromium and WebKit) reproduced this: the wait never resolved at all. show() now races a bounded requestAnimationFrame poll of the anchor's own getBoundingClientRect() alongside the ResizeObserver, so either signal can resolve it. The poll is bounded (ANCHOR_WAIT_MAX_FRAMES) so an environment with no real layout at all doesn't reschedule a frame forever under fake timers.
7819e65 to
b557c8a
Compare
|
All four points addressed in b557c8a, and the description is rewritten to match the current code.
I also found and fixed one more problem on the way: a synchronously firing I checked each new test by breaking the code the way it guards against, and each one fails. One unrelated test, |
There was a problem hiding this comment.
Thanks — waiting for a boxless trigger fixes the pinned HoverCard and the text-only and late-layout cases in Chromium and WebKit, but two problems still need fixing before this is ready:
PinnedTooltipInModalstill paints "Pinned tip" in the viewport corner, and the first case ofanchorReadiness.a11y.browser.spec.tsfails in Chromium (about 430 px against its 60 px bound) and WebKit (about 301 px), becauseDialogdisplays its element during render but callsshowModal()in a later Effect, so ashow()in that commit sees a box and opens before the dialog is modal; a Tooltip whoseisOpenturns true together with its Dialog does the same.- The trigger-swap restart in
contextRefreadspendingAnchorElementRef, which is not cleared when the wait opens the layer or the browser closes it, and it ignores ahide()in the same commit, so a dismissed or hidden layer can reopen by itself; please clear that state, guard the restart, and add tests for both cases.
[Automated review]
| return; | ||
| } | ||
| const rect = anchor.getBoundingClientRect(); | ||
| if (rect.width !== 0 || rect.height !== 0) { |
There was a problem hiding this comment.
In the commit that opens a Dialog, the dialog already has display: flex (applied during render), but showModal() has not run yet — Dialog calls it in a passive Effect, after its descendants' Effects. A controlled Tooltip whose show() runs in that commit — as in PinnedTooltipInModal, whose inline onOpenChange re-runs useTooltip's controlled Effect — measures a box here and opens before the dialog is modal; the tip then paints at (16, 16) in Chromium, as on base. Could the readiness check also wait while the trigger's closest <dialog> (or popover host) is not yet open?
| // A show() is still waiting for a box on a different trigger: that wait | ||
| // polls the element it started with, so it would never end for this | ||
| // one. Restart it on the new trigger, after the commit that attached it. | ||
| const waitingOn = pendingAnchorElementRef.current; |
There was a problem hiding this comment.
pendingAnchorElementRef is cleared only by cancelPendingAnchorWait(), so it still names the old trigger after the wait has opened the layer (line 1062) or the browser has closed it (handleToggle). A later swap then calls show() and reopens a layer the user dismissed, and the microtask below also opens after a hide() in the same commit. Clearing these refs when the wait completes or the layer closes, and checking a token that show() and hide() invalidate, would fix both.
| if (triggerBox == null || tooltipBox == null) { | ||
| return; | ||
| } | ||
| expect(edgeGap(tooltipBox, triggerBox)).toBeLessThan(MAX_ANCHOR_GAP_PX); |
There was a problem hiding this comment.
Run against the CI-built Storybook for this head, this assertion fails in Chromium: Expected: < 60, Received: 429.84820176720524. Pull-request CI does not run *.a11y.browser.spec.ts, so nothing caught it.
|
Both problems are addressed in the latest push.
Not verified here: I cannot run Storybook or the browser specs on this machine ( |
There was a problem hiding this comment.
Thanks — the pinned Tooltip and HoverCard in a Dialog now anchor to their trigger in Chromium and WebKit, and the trigger-swap restart is fixed, but a few things still need changing before this can merge:
isAnchorReady()only counts a trigger as ready when its closest<dialog>isopen, which a non-modalDrawer(hasScrim={false}, shown withshowPopover()) never is, so the drawer's close-button tooltip, aDropdownMenuor aPopoverinside it no longer open; please also accept a dialog that is showing as a popover, and add a test with a trigger inside a non-modal Drawer.- The unbounded
requestAnimationFramepoll inshow()conflicts with the current Layer runtime rule that "the runtime polls nothing" (docs/architecture/layer-runtime.mdINV12, alsodocs/specs/AST-059-layer-viewport-inset/spec.mdFR8); please replace it with observer- or event-driven signals. - The branch conflicts with
mainincontextRef, wheremainnow moves an open layer's anchor observers to a replaced trigger, and CI has not run on this head; please rebase and keep both behaviors.
[Automated review]
| return false; | ||
| } | ||
| const dialog = anchor.closest('dialog'); | ||
| return dialog == null || dialog.open; |
There was a problem hiding this comment.
dialog.open stays false for a <dialog popover> shown with showPopover(), and the non-modal Drawer (hasScrim={false}) is one, so a trigger inside it never counts as ready: in Chromium and WebKit its close-button tooltip, a DropdownMenu and a Popover stay closed, and the frame poll runs until hide(). Counting a dialog that is showing as a popover as open (for example dialog.open || dialog.matches(':popover-open')), with a test that puts a trigger inside a non-modal Drawer, would fix it.
| const poll = () => { | ||
| if (isAnchorReady(anchor)) { | ||
| resolve(); | ||
| return; | ||
| } | ||
| state.rafHandle = requestAnimationFrame(poll); | ||
| }; | ||
| state.rafHandle = requestAnimationFrame(poll); |
There was a problem hiding this comment.
This loop polls every frame, which the current Layer runtime record rules out: docs/architecture/layer-runtime.md INV12 says "the runtime polls nothing" (as does docs/specs/AST-059-layer-viewport-inset/spec.md FR8). With PinnedTooltipInModal loaded and its Dialog still closed, it runs 60 callbacks a second in Chromium, against none on the base branch. An intersection observer on the trigger and a mutation observer on the dialog's open attribute could carry the inline-trigger and not-yet-modal cases instead.
|
|
||
| `Dialog` mounts its children before it opens (`isOpen` gates `showModal()`, not rendering), so a `HoverCard` or `Tooltip` inside one with a controlled `isOpen={true}` runs its show effect while the trigger sits in a still non-modal `<dialog>` and has no box. A trigger with no box is not a valid CSS anchor: the popover resolves to its fallback position in the viewport corner, and unlike a live layout change that resolution is not revisited once the trigger later gets a box. | ||
|
|
||
| `Layer`'s `show()` now opens immediately when the trigger already has a box, so a menu or popover still opens in the same task as the press that asked for it. Only a boxless trigger waits: first for it to get a layout box, then for any running, finite animation on an ancestor to finish, so the layer anchors to the trigger's settled position and not to a mid-transition one. Paused animations and infinite ones are not waited on. |
There was a problem hiding this comment.
A trigger that has a box but sits in a <dialog> that is not open also waits now, so "Only a boxless trigger waits" no longer describes the change.
|
Reworked and rebased onto current main (one commit, 2741421):
Verification: |
dfab0a9 to
2741421
Compare
|
On the failing |
There was a problem hiding this comment.
Thanks — the pinned Tooltip and HoverCard in a Dialog now anchor to their trigger in Chromium and WebKit, layers inside a non-modal Drawer open again, and the wait no longer polls, but two things still need changing before this can merge:
isAnchorReady()treats any 0×0 trigger as boxless, so a trigger rendered with an empty box never opens: a ContextMenu in a shadow root without Astryx's styles stays closed on right-click, which is whypr-a11yfails bothMenuPressShadowRootContextMenu tests on this head, and checking for no layout box at all (for exampleanchor.getClientRects().length === 0) keeps every case this PR fixes.- The
SheetWithHoverTipstory moves back to the deprecatedBottomSheetSwitcher, undoing main's migration of it, so please restore main's version of that story.
[Automated review]
|
|
||
| function isAnchorReady(anchor: HTMLElement): boolean { | ||
| const rect = anchor.getBoundingClientRect(); | ||
| if (rect.width === 0 && rect.height === 0) { |
There was a problem hiding this comment.
A trigger that is rendered but has an empty box also lands here and never becomes ready: the ContextMenu's cursor anchor inside a shadow root without Astryx's styles is an empty inline <span> (one client rect, 0×0), so no observer or dialog signal ever reports it and the menu never opens. That is what fails the two MenuPressShadowRoot ContextMenu cases in pr-a11y on this head; base opens the menu. Testing for no layout box at all, for example anchor.getClientRects().length === 0 (which a display: none trigger and one inside a closed <dialog> both return), would keep every case this PR fixes.
| </Tooltip> | ||
| </VStack> | ||
| </BottomSheet> | ||
| <BottomSheetSwitcher |
There was a problem hiding this comment.
This brings back the deprecated BottomSheetSwitcher that main's #7648 migrated this story away from, and nothing in the anchoring fix needs it; the BottomSheetSwitcher record says stories teach the standalone BottomSheet replacement. Could this hunk and the import on line 33 go back to main's version?
|
|
||
| `Dialog` mounts its children before it opens (`isOpen` gates `showModal()`, not rendering), so a `HoverCard` or `Tooltip` inside one with a controlled `isOpen={true}` runs its show effect while the trigger sits in a still non-modal `<dialog>` and has no box. A trigger with no box is not a valid CSS anchor: the popover resolves to its fallback position in the viewport corner, and unlike a live layout change that resolution is not revisited once the trigger later gets a box. | ||
|
|
||
| `Layer`'s `show()` now opens immediately when the trigger already has a box, so a menu or popover still opens in the same task as the press that asked for it. Only a boxless trigger waits: first for it to get a layout box, then for any running, finite animation on an ancestor to finish, so the layer anchors to the trigger's settled position and not to a mid-transition one. Paused animations and infinite ones are not waited on. |
There was a problem hiding this comment.
A trigger with a box inside a dialog that is not showing waits too (line 11 says so), so "Only a boxless trigger waits" undersells the change.
2741421 to
ab380fb
Compare
|
Rebased onto main and pushed ab380fb with all three points:
I ran the built Storybook in Chromium: the three |
There was a problem hiding this comment.
Thanks — at this head the pinned Tooltip and HoverCard in a Dialog anchor to their trigger in Chromium and WebKit, a ContextMenu whose cursor anchor has an empty box opens again, and the SheetWithHoverTip story is back to main's version, so this review found nothing left for you to change. Approval still waits on cixzhang's earlier change request, which stays open until cixzhang re-reviews this head or dismisses it; both of its points are now addressed. CI has not run on this head yet because it needs a maintainer's ci:run label.
[Automated review]
Fixes #5374.
Problem
Dialogmounts its children before it opens:isOpengates the<dialog>'s ownshowModal(), not whether{children}renders. So aHoverCardorTooltipinside one with a controlledisOpen={true}(orisDefaultOpen) runs its show effect while the trigger sits in a still non-modal<dialog>and has no layout box (0x0).A trigger with no box is not a valid CSS anchor.
position-anchorresolves to its fallback position in the viewport corner, and unlike a live layout change that resolution is not revisited once the trigger later gets a box. I measured the card staying at(0, 4)for more than 3 seconds after the dialog visibly opened, so this is a permanently invalid anchor, not a one-frame race. ArequestAnimationFramedefer and asetTimeout(50)defer both failed to fix it, which points at the missing box itself being the precondition.Fix
All in
useLayer'sshow(), the one place every context-mode open goes through:ResizeObserveragainst an unboundedrequestAnimationFramepoll of the trigger's owngetBoundingClientRect(). AResizeObserverdelivers no entry at all for a purelydisplay: inlinebox (a text-only trigger's wrapper<span>), so it cannot be the only signal. There is no time limit: the wait ends when the trigger gets a box or the open is cancelled.ResizeObserver, norequestAnimationFrame, or no Web Animations API, as in jsdom) the layer opens immediately.Separately, a transient
nullthen same-element call on the trigger ref (what a combined ref utility makes when a handler changes identity) no longer removes and re-adds the element'sanchor-name. That churn could also strand an open layer in the corner.What changed after review
The first version waited at least a frame on every open, including for a trigger that already had a box. That moved a DropdownMenu's open from press to release, failed the
Core/Layerviewport-inset stories (no open layer right after the click) and the DateInput and DateTimeInput FR10 suites (their no-opResizeObserverstubs never ended the wait). It also had these gaps, all fixed here:ResizeObserverleft the settle wait uncancellable byhide(), because the final cancel function overwrote its handle;ResizeObservermocks I had edited in Popover, PowerSearch and Tokenizer tests, theclearTimeoutshoist inuseTooltip, and the Playwright glob are all reverted, since they only existed to accommodate the always-wait version. This PR no longer touches any of those files.Verification
Reproduced against the exact story from the issue (
Core/Layer Dismissal,Pinned Hover Card In Modal), where the Dialog opens later via a click.Before, card stuck in the corner, disconnected from its trigger:

After, anchored above "Card trigger", matching

placement="above":Those two images are from the earlier version of this fix; the visible outcome for a boxless trigger is unchanged.
Tests (jsdom, with layout stubbed): eight new
show()readiness tests inuseLayer.test.tsx. I broke the code four ways to confirm each is caught: waiting on the box path again fails the same-task open test, dropping the running-only filter fails the paused-animation test, removing the trigger restart fails the replacement test, and a cancel that skips the settle wait fails thehide()test. The wholeuseLayerfile passes (78 tests), as do the Layer, Popover, Tooltip, HoverCard, DropdownMenu, DateInput, DateTimeInput, DateRangeInput, PowerSearch, Tokenizer, Selector and MultiSelector suites, apart from one failure that is also onmain. That one islayerViewportInset.test.ts, which compares Windows backslash paths to forward-slash names. Core typecheck, eslint and prettier are clean.Limits
I could not run the browser parts here: the Storybook build and dev server crash on my machine with a native out-of-memory error. So the
Core/Layerstory play-guards, theanchorReadinessPlaywright spec and the WebKit run are not verified by me, and the "after" image above is from the earlier version. TheCore/Layerand DateInput FR10 failures from the last review should be gone by construction, because a trigger with a box now opens synchronously and jsdom without the Web Animations API no longer waits, but that is for CI to confirm.