Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1fda019
chore(core): the layer runtime's exit copy, for a touch presentation'…
vjeux Nov 4, 2026
515dd46
Merge branch 'main' into uikit/c26-layer-exit-copy
cixzhang Nov 5, 2026
415707b
feat(Popover): under a finger, it grows out of its trigger and shrink…
vjeux Nov 5, 2026
1cc771f
Merge branch 'main' into uikit/c26-layer-exit-copy
vjeux Nov 5, 2026
940af75
Merge the updated lower stack branch into uikit/c26-popover-present (…
cixzhang Nov 5, 2026
456d9ab
feat(Popover): a touch during the growth is ignored
vjeux Nov 5, 2026
544ddee
fix(core): the exit copy keeps no closed layer
vjeux Nov 5, 2026
0e69c33
docs(core): the exit copy's JSDoc states the empty-canvas rule once
vjeux Nov 5, 2026
04268f2
Merge the updated lower stack branch into uikit/c26-popover-present (…
cixzhang Nov 5, 2026
223875a
Merge main into the layer exit copy branch (layer-runtime.md verifica…
cixzhang Nov 5, 2026
a46586c
Merge the updated lower stack branch into uikit/c26-popover-present (…
cixzhang Nov 5, 2026
6ecb859
Merge main into the layer exit copy branch (#8147 landed)
cixzhang Nov 6, 2026
e8cd6f6
Merge the updated lower stack branch into the Popover branch (#8147 l…
cixzhang Nov 6, 2026
f642349
Merge the updated lower stack branch into the Popover branch
cixzhang Nov 6, 2026
d7905ea
Merge main into the layer exit copy branch (placeOver's beforeNode be…
cixzhang Nov 6, 2026
0981880
Merge the updated lower stack branch into the Popover branch (#10224 …
cixzhang Nov 6, 2026
c2dc518
Merge main into the layer exit copy branch (#10224 landed)
cixzhang Nov 6, 2026
9c96e82
feat(Popover): the touch presentation starts in the next frame
vjeux Nov 6, 2026
d99ab9c
docs(Popover): record the next-frame window with the code
vjeux Nov 6, 2026
30e42ae
fix(core): the exit copy reads its host's style before the insertion
vjeux Nov 6, 2026
60a6e14
Merge the updated lower stack branch into the Popover branch
vjeux Nov 6, 2026
51f8457
perf(Popover): the growth reads its boxes and timing before it writes
vjeux Nov 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/layer-exit-copy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[chore] Internal: the layer runtime gains one exit copy for touch presentations that animate a popover out: a static, inert copy of the closing popover, shown only in the top layer where the popover was drawn, for the close to play on. Core's other inert copies now join no form, carry no anchor name and keep their color scheme, as their documentation says.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/popover-uikit-presentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[feature] Under a finger (a coarse primary pointer, on any touch device), Popover grows out of its trigger, wide before tall, with its edge held on the trigger, and shrinks back into the trigger when it closes. The growth plays the theme's `--duration-medium-min` (in the Storybook theme, the step nearest iOS's 206 ms growth) with its entrance easing, and the shrink plays its exit duration and easing, on the compositor. A close during the growth, or an open during the shrink, turns back from where the popover is. With reduced motion it cross-fades in and out, opacity only, in 200 ms or less. A fine primary pointer (a mouse) keeps its entrance. A tap that lands while it grows is ignored, and a pan started then scrolls nothing. The shrink plays on the layer runtime's exit copy, of which Popover is the first user. The growth starts in the next animation frame, before that frame paints, so the opening tap does no extra layout.

@vjeux
49 changes: 44 additions & 5 deletions docs/architecture/layer-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ verified_by:
packages/core/src/Layer/useLayerDismissal.test.tsx,
packages/core/src/Layer/layerClickBoundary.test.tsx,
packages/core/src/Layer/layerDismissalInvariants.test.tsx,
packages/core/src/Layer/layerExitCopy.test.ts,
packages/core/src/Layer/__tests__/layerExitCopy.interaction.browser.spec.ts,
packages/core/src/Popover/Popover.test.tsx,
packages/core/src/DropdownMenu/DropdownMenu.test.tsx,
packages/core/src/ContextMenu/ContextMenu.test.tsx,
Expand Down Expand Up @@ -68,6 +70,7 @@ deciding_specs: [spec:AST-038, spec:AST-059]
"INV7",
"INV11",
"INV12",
"INV13",
"INV14",
"INV15",
"INV17",
Expand Down Expand Up @@ -315,6 +318,26 @@ else closes the layer, a scroll or pinch that starts outside does not, and
Escape stays on the shared stack. Focus leaving the field stays each owner's to
handle (`family:overlay-dismissal` AV4).

### Exit copy

The browser hides a popover the moment it closes, whether by a pick, Escape, or
its own light dismiss. A touch presentation whose close stays on screen plays it
on an exit copy: a static copy of the layer that `placeLayerExitCopy` takes as
the layer closes. The copy is laid out on the layer's own box and carries the
transform and opacity the layer is drawn with, so a copy taken while the layer
is still entering holds where it was drawn. It carries no ids, form names, or
form owner; it is `inert`, hidden from assistive technology, and out of pointer
reach; it keeps the layer's scroll positions. It is shown only in the top layer,
as a manual popover, so a clipping, transformed, or sticky ancestor neither cuts
nor moves it; a fixed box or a larger `z-index` is no substitute
(`spec:AST-027` FR8). Where the browser refuses that show during the closing
popover's own toggle, the copy waits, hidden, and enters the top layer once the
toggle is over: after the script that closed the layer, or in the next frame
before it paints. Where there is no top layer (no popover API), or the browser
still refuses, there is no copy and the close is immediate. The presentation
that placed the copy plays its exit on it (`spec:AST-094` FR7) and removes it
when its close settles.

### Current dismissal plumbing

`family:overlay-dismissal` owns the shipped Escape/platform-close membership
Expand Down Expand Up @@ -546,6 +569,12 @@ layers use Layer rendering without joining Escape/platform dismissal.
correct, and the runtime polls nothing: an anchor that a scrolled
container hides is followed again when an intersection observer reports it
back in view.
- **INV13 — One exit copy per closing layer.** A layer's close plays on at most
one exit copy, made by `placeLayerExitCopy`: drawn where the layer was drawn,
inert, hidden from assistive technology, without ids, form names, or form
owner, shown only in the top layer, and removed by its owner when the close
settles. Placing a second copy for the same layer removes the first; another
layer's copy stays.
- **INV14 — The runtime owns every layer's shared mechanism.** Internally, the layer runtime owns the shared
mechanism layers use: top-layer mounting, close-request delivery, the
presentation hold (keeping a layer's chosen presentation from the open until
Expand Down Expand Up @@ -613,11 +642,12 @@ Current gaps are observable facts, not current target behavior:
migration, verification, and completion criteria for these gaps. This record must
be updated only as that work ships.

INV14 and INV15 are not met yet, and `spec:AST-003` does not cover them: no
shared presentation hold, exit copy, runtime backdrop tint, or lift module
exists, and Dialog, Drawer, BottomSheet, Lightbox, MobileNav, ToastViewport,
and Selector each build their own top-layer mounting, exit, or backdrop, and
Popover builds its own presentation hold for its sheet presentation.
INV14 and INV15 are not met yet, and `spec:AST-003` does not cover them. The
runtime owns the shared exit copy (INV13, `placeLayerExitCopy`); no shared
presentation hold, runtime backdrop tint or lift module exists, Dialog, Drawer,
BottomSheet, Lightbox, MobileNav, ToastViewport, and Selector each build their
own top-layer mounting, exit, or backdrop, and Popover builds its own
presentation hold for its sheet presentation.
BottomSheet's own includes its sheet stack's mounting — the re-show that
keeps a document's sheets in open order, a step dialog's modal present, and
the non-modal sheet's popover host — alongside its scrim; the ordering rules
Expand Down Expand Up @@ -677,6 +707,12 @@ is staged as `[breaking]` (`spec:AST-017` FR32) and `component:SelectableCard`
entry/return, and controlled close behavior.
- A browser fallback change updates public `useLayer` and Popover docs and tests
for both the native path and actual fallback.
- An exit-copy change verifies, where each can be observed: the copy's reach,
accessibility and form exclusion in jsdom; its top-layer show (from script
and by light dismiss), its place mid-entrance, and that a layer keeps one
copy, in jsdom and in Chromium and WebKit; and the refused show's wait in
jsdom and on Chromium 150 and later, the engines that refuse a show during
another popover's toggle (Chromium 149 accepts it).

## Owning code

Expand Down Expand Up @@ -711,6 +747,8 @@ is staged as `[breaking]` (`spec:AST-017` FR32) and `component:SelectableCard`
`stopPropagation()` also stops the native event in React 19, which would
break ContextMenu's outside `mousedown`, Typeahead's pending click, and the
layer's keep-open `pointerup`, so the seam never stops the native event.
- `Layer/layerExitCopy.ts` owns the exit copy a touch presentation's close plays
on.
- `Layer/layerStack.ts`, `Layer/useLayerDismissal.ts`, and
`Layer/LayerDepthContext.tsx` own current registration, presence, ordering, and
Escape/platform routing.
Expand Down Expand Up @@ -767,6 +805,7 @@ this current architecture record.
| INV11 | `layerViewportInset.test.ts`, `useLayer.test.tsx`, and the `Core/Layer` viewport-inset stories under the story play guard at a desktop and a phone viewport; `BaseTypeahead.incremental.test.tsx` and the Typeahead incremental-results stories for the `spec:AST-092` FR6 exception | A second gutter definition, a `100%`-of-span cap, a layer narrower than its explicit size, a layer box wider than the viewport, a layer inside the gutter, or a layer that slides toward an off-screen anchor; for an open Typeahead popup, a later list for the same query that flips it to the other side or grows it past the room on its side, or a first list for a query that is capped to the span beside the trigger |
| INV12 | The `Core/Layer` viewport-inset stories' first-frame comparison, and `useLayer.test.tsx`'s synchronous anchor read | A layer whose first painted rectangle differs from its settled one |
| INV11, INV12 | The software keyboard's cap: `useLayerKeyboardClearance.test.tsx` for its rule, its timing, and its read before first paint; `PopoverKeyboard.interaction.browser.spec.ts` and `keyboardCappedLists.interaction.browser.spec.ts` for its rendered geometry in Chromium and WebKit | A layer that runs under the keyboard, or is capped though the keyboard does not reach it; a cap that changes more than once per keyboard movement or under a press; a layer opened with the keyboard up whose first frame is uncapped or paints its content past its box |
| INV13 | `layerExitCopy.test.ts` and `layerExitCopy.interaction.browser.spec.ts` | A copy with an id, a form name, a form owner, or pointer reach; a copy off the layer's drawn box; a copy outside the top layer; a second copy for one layer |
| INV6, INV7 | `useLayer.test.tsx`, `Popover.test.tsx`, `DropdownMenu.test.tsx`, and `useMenuHover.test.tsx` | Duplicate close callback or the same press/re-hover reopens a surface |
| INV8 | `useLayerDismissal.test.tsx`, `layerDismissalInvariants.test.tsx`, and `useFocusTrap.test.tsx` | Current top registered layer is skipped, two layers close, or a blocker leaks through |
| INV9 | Representative Dialog, ContextMenu, Tooltip/HoverCard, and BottomSheet source/tests; `useBackdropPress.test.tsx`; `outsidePress.test.ts` | A current local channel silently changes ownership or policy |
Expand Down
4 changes: 3 additions & 1 deletion docs/specs/AST-094-touch-presentations/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -920,7 +920,9 @@ under a finger, and a mouse follows `module:DropdownMenu/useMenuPress` FR3 and
touch target reaches 44 px only through an invisible hit area clamped by
its neighbors, unless a component or module record sets its own (FR42).
- `component:DropdownMenu`, `component:ContextMenu` and `component:Popover`:
under a finger they follow FR26–FR35 as each applies.
under a finger they follow FR26–FR35 as each applies. Popover's input hold
(FR35) starts on the first frame after the open, so a touch that lands
within that frame reaches the page.
- `component:AlertDialog`, `component:Dialog`, `component:BottomSheet`,
`component:BottomSheetSwitcher`, `component:Drawer`, `component:MobileNav`
and `component:Lightbox`: under a finger, a tap or a drag during the
Expand Down
Loading
Loading