Skip to content

refactor(DropdownMenu): the finger a trigger hands to the menu runs on the shared action-group tracker - #8387

Draft
vjeux wants to merge 1008 commits into
meta:mainfrom
vjeux:uikit/c19-menu-handoff-tracker
Draft

vjeux wants to merge 1008 commits into
meta:mainfrom
vjeux:uikit/c19-menu-handoff-tracker

Conversation

@vjeux

@vjeux vjeux commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8195. Review only this PR's change: vjeux/astryx@b30de7f...d5ec70f

Maintainer impact

Under DropdownMenu's touch modes, a finger that lands in the open menu resolves its rows through the shared action-group tracker (#8195, #8346). The finger that opened the menu from its trigger (spec:AST-094 FR29) did not. It hit-tested the page with elementFromPoint on every move. Two hit-testing paths for one menu meant two answers near a row's edge, and a hit test on every move.

Change

  • The opening finger's rows now come from the shared tracker. Their rects are read once, when the menu shows under the finger, and hit-tested in memory.
  • They are read again while a menu surface still moves (its open motion, seen through its running animations) or after the menu's contents change.
  • They are read again after each edge-autoscroll step, only while the menu autoscrolls. Shifting them by the summed steps let them run ahead of the rows in iOS 26.2 Safari: the lit row cleared before the last row.
  • The machine still decides when that finger may pick (the 10 px handoff, FR29).
  • A mouse, the pointer model (presentation="popover") and an injected hitTest keep elementFromPoint.

Evidence

A press on "Move to project", a slide into the menu to its bottom edge, a rest there while it autoscrolls, the lift on "Website refresh" (it runs and the menu closes). Every row lights in turn on both.

iOS 26.2 simulator (Safari) Android Chromium 158 emulator
iOS Android

Public delta

None. The changeset is internal.

Writing rubric results

No knowledge record changes.

Testing

  • pnpm vitest run hooks, DropdownMenu, ContextMenu, MoreMenu, Selector, MultiSelector, motion, gestures and utils at this head: 2874 passed. They include "the finger a trigger hands to the menu": across a slide, the rects are read once and elementFromPoint is never called; they are read again while the surface moves; a row scrolled out of the menu's box is not under the finger. The first two fail on DropdownMenu: a finger in the open menu is tracked as UIMenu's list tracks it #8195's head.
  • pnpm test:interaction DropdownMenuTouch* and DropdownMenuSelection at this head: Chromium 31/31, WebKit (Playwright image) 26 + 5 skipped. The DropdownMenu, MoreMenu, pinch-zoom and menu-press a11y specs (Chromium) 156/156.
  • The takes above, on this PR's code: iOS 26.2 simulator (page clock: open 160 ms after the touch, autoscroll to the end, "Website refresh" clicked) and the Android emulator. Before the autoscroll re-read, the iOS take lit the rows and then cleared at scrollTop 264 of 286, so nothing ran.
  • pnpm build, pnpm typecheck and CI=true pnpm lint with no error; CLI isolation and golden reads 47/47; check:knowledge, check:changesets.

Stack (merge in this order): #8076 and #8298 → #8078 → #8508 and #8186 → #8080; #8446 → #8089; #8083 → #8084 and #8346 → #8110 → #8195; then this PR.

@vercel

vercel Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 26, 2026 9:52pm UTC

Request Review

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 25, 2026
@github-actions
github-actions Bot requested a review from cvkxx October 25, 2026 07:58
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 25, 2026
@astracat-bot

astracat-bot Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 30, 2026, 1:42 PM UTC

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 25, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval waits on a spec decision: spec:AST-094, whose FR29 handoff and FR26–FR30 govern this change, is added here through the unmerged #8378 and is not on main, so an engineering owner needs to record the decision by commenting /approve-spec 8a2a297101a5dd8485f3cd33f08528a575b95903 on this pull request (the author may do so as an engineering owner) or land #8378 first. Independently of that decision, these changes are needed:

  • In resolveHandoff (packages/core/src/hooks/useMenuPress.ts, lines 788–792), moving is read from the previous read's surfaces, so one move during the open spring keeps mid-grow rects for the rest of the slide; decide isStale after measureHandoff, and add tests for that case and for the autoscroll re-read.
  • The diff against main carries the whole unmerged stack (about 42,600 authored lines, 351 of them this commit), so please keep this pull request in draft until #8195 lands and then rebase onto it.

[Automated review]

Comment on lines +788 to +792
const moving = state.roots.some(root => isMoving(root.surface));
if (state.isStale || moving) {
measureHandoff(state, x, y);
// Read while it moves: read once more when it rests.
state.isStale = moving;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moving is computed from state.roots before the read, and on the first call that list is still empty, so a first read taken mid-grow stores isStale = false. If the finger then rests until the open animation retires, every later move hit-tests the mid-grow rects (line 794) and isInMenu uses the mid-grow box (line 796), so the wrong row, or none, lights and acts. Deciding after the read fixes it:

measureHandoff(state, x, y);
state.isStale = state.roots.some(root => isMoving(root.surface));

A case for it: one move while getAnimations() reports a running animation, then [], rows re-placed, one more move, expecting a second read.

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

Modified Components

AlertDialog (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 191B 191B 0B
Lines of Code 182 473 -
Complexity Medium High (25) -
ContextMenu (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 1.1KB 1.1KB 0B
Lines of Code 678 678 -
Complexity Very High Very High (86) -
MoreMenu (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Entry File (ESM) 219B 219B 0B
Lines of Code 87 87 -
Complexity Low Low (4) -

Component Size

Component Import before Import after Delta CSS before CSS after Delta
core/AlertDialog 54.6KB 80.5KB +26.0KB ⚠️ 12.4KB 12.5KB +64B
core/AppShell 55.7KB 55.9KB +266B 11.6KB 11.6KB 0B
core/AvatarGroupOverflow 39.9KB 40.2KB +303B 6.4KB 6.4KB 0B
core/Banner 47.8KB 48.1KB +289B 9.9KB 9.9KB 0B
core/BaseTypeahead 53.8KB 54.1KB +284B 9.9KB 9.9KB 0B
core/BreadcrumbItem 65.9KB 81.3KB +15.5KB ⚠️ 11.0KB 11.0KB 0B
core/Button 43.3KB 43.5KB +273B 9.2KB 9.2KB 0B
core/Calendar 54.9KB 55.2KB +296B 10.0KB 10.0KB 0B
core/Carousel 50.2KB 50.5KB +316B 10.2KB 10.2KB 0B
core/ChatComposer 60.4KB 60.7KB +265B 10.8KB 10.8KB 0B
core/ChatComposerInput 55.5KB 55.8KB +263B 10.1KB 10.1KB 0B
core/ChatComposerTokenElement 55.5KB 55.8KB +262B 10.1KB 10.1KB 0B
core/ChatDictationButton 46.5KB 46.8KB +264B 9.5KB 9.5KB 0B
core/ChatLayout 48.8KB 49.1KB +273B 9.9KB 9.9KB 0B
core/ChatLayoutScrollButton 46.4KB 46.6KB +255B 9.6KB 9.6KB 0B
core/ChatSendButton 45.8KB 46.1KB +265B 9.2KB 9.2KB 0B
core/CheckboxInput 49.4KB 49.7KB +271B 9.3KB 9.3KB 0B
core/CheckboxListItem 53.6KB 53.8KB +252B 10.5KB 10.5KB 0B
core/ClickableCard 12.8KB 13.1KB +289B 6.9KB 6.9KB 0B
core/CodeBlock 60.5KB 60.7KB +274B 10.7KB 10.7KB 0B
core/Collapsible 11.7KB 12.0KB +291B 4.2KB 4.2KB 0B
core/CommandPalette 49.2KB 49.5KB +280B 11.8KB 11.8KB 0B
core/CommandPaletteItem 8.4KB 8.7KB +280B 3.5KB 3.5KB 0B
core/ComplexSelector 55.3KB 55.6KB +336B 10.6KB 10.6KB 0B
core/ContextMenu 77.3KB 92.5KB +15.2KB ⚠️ 14.5KB 14.5KB 0B
core/ContextMenuItem 16.6KB 16.8KB +290B 5.2KB 5.2KB 0B
core/DateInput 92.5KB 92.7KB +219B 14.6KB 14.6KB 0B
core/DateRangeInput 69.0KB 69.2KB +249B 11.2KB 11.2KB 0B
core/DateTimeInput 102.6KB 102.8KB +232B 15.1KB 15.1KB 0B
core/DialogHeader 48.8KB 49.1KB +280B 9.8KB 9.8KB 0B
core/DrawerHeader 48.9KB 49.2KB +249B 11.2KB 11.2KB 0B
core/DropdownMenu 72.2KB 101.9KB +29.7KB ⚠️ 14.6KB 14.6KB 0B
core/DropdownMenuCheckboxItem 16.1KB 16.4KB +284B 5.6KB 5.6KB 0B
core/DropdownMenuDivider 6.6KB 6.6KB 0B 3.6KB 3.6KB 0B
core/DropdownMenuGroup 6.1KB 6.1KB 0B 3.3KB 3.3KB 0B
core/DropdownMenuItem 15.4KB 15.7KB +287B 5.2KB 5.2KB 0B
core/DropdownMenuRadioGroup 6.0KB 6.0KB 0B 3.2KB 3.2KB 0B
core/DropdownMenuRadioItem 16.2KB 16.5KB +275B 5.6KB 5.6KB 0B
core/DropdownMenuSubMenu 57.5KB 72.7KB +15.1KB ⚠️ 9.5KB 9.5KB 0B
core/FileInput 53.7KB 54.0KB +239B 10.3KB 10.3KB 0B
core/IconButton 43.3KB 43.6KB +273B 9.2KB 9.2KB 0B
core/Item 11.4KB 11.6KB +273B 4.7KB 4.7KB 0B
core/LayerProvider 63.2KB 63.4KB +300B 10.1KB 10.1KB 0B
core/Lightbox 49.5KB 49.8KB +276B 9.8KB 9.8KB 0B
core/Link 43.5KB 43.8KB +288B 7.6KB 7.6KB 0B
core/ListItem 53.0KB 53.3KB +263B 10.5KB 10.5KB 0B
core/Markdown 143.9KB 159.2KB +15.2KB ⚠️ 17.2KB 17.2KB 0B
core/MobileNav 48.8KB 49.1KB +272B 10.1KB 10.1KB 0B
core/MobileNavToggle 46.2KB 46.5KB +259B 9.4KB 9.4KB 0B
core/MoreMenu 72.5KB 102.2KB +29.7KB ⚠️ 14.6KB 14.6KB 0B
core/MultiSelector 75.7KB 76.0KB +265B 14.5KB 14.5KB 0B
core/NumberInput 55.6KB 55.9KB +226B 10.7KB 10.7KB 0B
core/Outline 28.6KB 28.9KB +290B 4.0KB 4.0KB 0B
core/Pagination 87.3KB 102.0KB +14.7KB ⚠️ 15.2KB 15.2KB 0B
core/Popover 49.8KB 50.1KB +239B 12.0KB 12.0KB 0B
core/PowerSearch 147.7KB 162.5KB +14.8KB ⚠️ 18.5KB 18.5KB 0B
core/RadioListItem 52.1KB 52.4KB +269B 9.7KB 9.7KB 0B
core/SegmentedControlItem 8.6KB 8.9KB +285B 3.9KB 3.9KB 0B
core/SelectableCard 12.8KB 13.1KB +294B 7.1KB 7.1KB 0B
core/Selector 78.7KB 93.6KB +14.9KB ⚠️ 14.8KB 14.8KB 0B
core/SelectorOption 14.5KB 14.8KB +251B 5.1KB 5.1KB 0B
core/SideNav 55.8KB 56.1KB +271B 10.6KB 10.6KB 0B
core/SideNavCollapseButton 46.6KB 46.9KB +242B 9.5KB 9.5KB 0B
core/SideNavHeading 54.7KB 55.0KB +241B 10.1KB 10.1KB 0B
core/SideNavItem 54.4KB 54.7KB +265B 10.1KB 10.1KB 0B
core/Step 32.4KB 32.7KB +279B 5.3KB 5.3KB 0B
core/Stepper 47.4KB 47.7KB +265B 9.6KB 9.6KB 0B
core/Switch 48.4KB 48.6KB +286B 9.3KB 9.3KB 0B
core/Tab 9.7KB 10.0KB +294B 4.1KB 4.1KB 0B
core/TabMenu 51.6KB 51.9KB +274B 10.0KB 10.0KB 0B
core/Table 83.5KB 98.7KB +15.2KB ⚠️ 15.0KB 15.0KB 0B
core/TableCell 78.4KB 93.6KB +15.2KB ⚠️ 14.6KB 14.6KB 0B
core/TableHeaderCell 78.1KB 93.4KB +15.2KB ⚠️ 14.6KB 14.6KB 0B
core/TableSelectionToolbar 52.4KB 52.6KB +278B 11.2KB 11.2KB 0B
core/TextInput 53.1KB 53.4KB +262B 10.5KB 10.5KB 0B
core/Thumbnail 50.1KB 50.4KB +275B 11.0KB 11.0KB 0B
core/TimeInput 79.3KB 79.5KB +243B 13.6KB 13.6KB 0B
core/Timestamp 27.5KB 27.5KB -1B 9.5KB 9.5KB 0B
core/Toast 59.8KB 60.1KB +258B 9.7KB 9.7KB 0B
core/ToggleButton 44.1KB 44.4KB +243B 9.3KB 9.3KB 0B
core/Token 29.6KB 29.9KB +289B 4.5KB 4.5KB 0B
core/Tokenizer 64.3KB 64.6KB +285B 11.7KB 11.7KB 0B
core/TopNav 49.7KB 49.9KB +273B 10.4KB 10.4KB 0B
core/TopNavHeading 54.2KB 54.4KB +235B 9.9KB 9.9KB 0B
core/TopNavItem 9.0KB 9.3KB +283B 3.8KB 3.8KB 0B
core/TopNavMegaMenu 54.2KB 54.4KB +264B 10.7KB 10.7KB 0B
core/TopNavMegaMenuItem 9.1KB 9.4KB +281B 4.1KB 4.1KB 0B
core/TopNavMenu 53.6KB 53.8KB +249B 10.1KB 10.1KB 0B
core/TreeList 32.5KB 32.8KB +274B 4.7KB 4.7KB 0B
core/Typeahead 61.0KB 61.2KB +262B 11.3KB 11.3KB 0B

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.

⚠️ Touch-only size budget

These components grew past the touch-only budget (2 KB gzip JavaScript or 1 KB gzip CSS per component). If the growth runs only on a coarse pointer, load its JavaScript lazily so the import does not carry it, or justify the cost in the pull request description; CSS stays in the package stylesheet, so justify it.

Component JavaScript CSS
core/AlertDialog +26.0KB ⚠️ +64B
core/BreadcrumbItem +15.5KB ⚠️ 0B
core/ContextMenu +15.2KB ⚠️ 0B
core/DropdownMenu +29.7KB ⚠️ 0B
core/DropdownMenuSubMenu +15.1KB ⚠️ 0B
core/Markdown +15.2KB ⚠️ 0B
core/MoreMenu +29.7KB ⚠️ 0B
core/Pagination +14.7KB ⚠️ 0B
core/PowerSearch +14.8KB ⚠️ 0B
core/Selector +14.9KB ⚠️ 0B
core/Table +15.2KB ⚠️ 0B
core/TableCell +15.2KB ⚠️ 0B
core/TableHeaderCell +15.2KB ⚠️ 0B

Bundle Size Summary

Gzip figures measure each package's entry file, not the code an import pulls in.

Package Entry (ESM) Entry gzip before Entry gzip after Delta
@astryxdesign/core 4.9KB 1.2KB 1.2KB 0B
Package stylesheet Gzip before Gzip after Delta
@astryxdesign/core 34.3KB 34.3KB +43B

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

cixzhang and others added 15 commits October 26, 2026 00:02
meta#8089 now carries meta#8078's restructured press model: the trigger half lives in menuTriggerPress.ts, a menu with no trigger mounts useMenuSurfacePress, and the root passes its touch model to its sub-menus on the internal surface context. meta#8110 carries the shared tracker on the current gesture line. This pull request's finger tracking moves onto that shape: the shared model attaches the selection recognizer under 'uikit', a sub-menu takes the root's touch model from the surface context (its own touchModes field goes), and the module record keeps main's table, which no longer lists the touch model.

Two coarse cases from the base now drive the finger through the gesture system, as a real finger is: the AR5 pick, and the cancelled touch that clears its lit row (the hook's and a flyout's).
…hing is attached

From CodeBunny on meta#8073 (spec:AST-094 FR24), fixed once here: the
(pointer: coarse) change listener was added when the host was created,
on a fine pointer too, and never removed. It is now added with the first
attachment and removed when the last one goes, so nothing touch-related
remains after the last instance unmounts. The test fails on the previous
head (a listener at creation, none removed).
- The detach's landing commit can promote the element again (a coast
  still moving on another axis), so attach demotes after it, as dispose()
  orders them: no will-change stays on an unmounted or replaced node.
- A new element taking over an exit by role hands the outgoing element its
  stylesheet's opacity, as for a borrowed dip (the same test stop() uses).
Lab imports no new core-private path until Lab's module-sharing decision, so
the hooks/isRtlElement entry of the internal-module resolver, its two Lab
tsconfig paths and its resolver tests lose their only users and go. A Lab
component that needs the check keeps a local helper.
…nd a finger's tap acts once

Where the primary pointer is fine (spec:AST-094 FR19, FR24), the open menu attaches nothing to the gesture system and adds no touch listener, and a finger's tap acts at its release through the press model with the browser's click that follows swallowed, so the row acts once. Where it is coarse, the menu joins the gesture system while it shows. The case fails if the touch model's gate on the selection recognizer is removed.
meta#8195 now carries meta#8078's restructured press model and main's handoff cases (module FR11). Two of those cases hand a finger to a 'uikit' menu in the hook's harness, where this pull request resolves a handed-off finger's rows from their measured rects; like this pull request's own handoff cases, they inject the row under the finger, so they still check the machine's handoff distance (the rect tracker is the DropdownMenu suite's).
…t syncs, and the stale headers and changesets match the code

- useGestureRecognizer and usePressTracker re-attach when the element behind
  the ref is replaced and detach when it goes, checked after every render at
  no render cost (architecture:react-component-runtime INV6); each Effect
  names the system it syncs (INV3).
- A control's profile releases on the deselect clock the shared press styles
  play, so a painter's `fading` never outlasts the fade it shows; UIButton's
  250 ms arrives with a control's own release layer.
- PressTracker's and GestureHost's headers describe the cell's delay inside
  scroll views only, and no delegated consumers or scroll handoff.
- One changeset for the press tracking, the one press writer and the hooks.

Co-authored-by: Christopher Chedeau <vjeux@users.noreply.github.com>
src/utils backs the public @astryxdesign/core/utils entry, where a
deliberately experimental API may carry the tag, so the guard reads only its
package-private touchPresentation.ts there. A test pins that boundary, and
the guard checks that every file it names exists.
…e, and a backward end delay is not progress

waitForAnimation's fallback finish time (read before the finish event is
delivered, right after finish()) now goes through the playback rate's speed,
and for a run going backward is its startTime; it was a whole run late for a
reverse()d run and 500 ms late at rate 2. minProgressMs no longer counts a
backward run's end delay, which plays first and presents nothing. Self-tests
for both; the finish case failed by 1,000 ms before (Chromium, 27/27 and 5x
repeat pass).
…ing is attached) into UIControl press tracking

The host has no delegated consumers at this layer, so the listener follows
the registry alone.
cixzhang and others added 15 commits October 26, 2026 11:29
# Conflicts:
#	internal/interaction-harness/README.md
#	internal/interaction-harness/package.json
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
#	internal/interaction-harness/src/story.ts
spec:AST-094 FR28: when the window loses focus the host cancels every
touch, as UIKit does, and forgets the fingers. A browser that keeps such a
touch alive past the blur then delivers its lift untracked, so the tap's
click runs whatever is under it. The unit case and the browser proof read
that the lift is refused its click, and that only that lift is: a later
finger with the same identifier, lifted or not, keeps its own. Fails on
this head (the lift clicks).
When the app loses focus the host still cancels every touch in flight, as
UIKit does, and now remembers each finger it dropped until the browser
ends it. A touchend for one is cancelled and arms the click swallower, so
a browser that keeps the touch alive past the blur cannot click whatever
is under the lift (spec:AST-094 FR28: nothing acts). A touchcancel, a new
touch with the identifier, or a touch list without the finger forgets the
record.
…ewest call's frame has passed

spec:AST-094 FR30: a menu's edge autoscroll announces each write with
ownScroll, every frame. The first call's two-frame timer cleared the
exemption the newest call had just set, so the newest write's scroll read
as the browser taking the finger and the press was lost. Fails on this
head.
…west call's frame has passed

ownScroll numbers each call and keeps the scroller's newest number; a
call's two-frame timer clears the exemption only if no newer call has
come since, so an edge autoscroll that announces a write every frame keeps
its finger (spec:AST-094 FR30).
…the host listening

spec:AST-094 FR24: nothing disabled registers anything. Detaching an
element's enabled recognizer, or its consumer, while a disabled
recognizer stays attached left the host's document listeners installed,
because the clean-up re-synced them only when the element had nothing
left. Fails on this head.
…he host listening

The clean-up after a detach now re-syncs the host's listeners whenever
the element keeps something attached, not only when it is gone, so a
disabled recognizer left behind registers nothing (spec:AST-094 FR24).
…so it stays on a short compact screen

placeOverTrigger capped only the popover surface, while the role="menu"
element inside kept its own taller cap and the surface's overflow showed it:
at 800 x 220 a menu ran 64 px past the viewport and its last rows never came
on screen. The room now caps every capped box from the surface down to the
menu (and the column it shares with a header or footer), each under its own
cap, so the scrolling element is the one held to the room.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval waits on a spec decision: this head still carries architecture:layer-runtime INV13 and its "Exit copy" section from the stacked #8186, which main does not state, so an engineering owner needs to record the decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), or INV13 can land on main through #8186 first. Independently of that decision, these changes are needed:

  • In resolveHandoff (packages/core/src/hooks/useMenuPress.ts, lines 824–828), moving is still read from the previous read's roots, so one move during the open spring leaves the opening finger on mid-grow rects; decide isStale after measureHandoff and add a test for that case.
  • The opening finger's autoscroll re-read (lines 675–680) still has no test; a case where that finger rests in the bottom edge zone and the lit row follows the scrolled rows would cover spec:AST-094 FR30.
  • The diff against main still carries the unmerged stack (34,526 authored lines, 347 of them this pull request's own), so please keep this pull request in draft until #8195 lands and then rebase onto it.

[Automated review]

cixzhang and others added 7 commits October 26, 2026 12:50
… a caller's inline cap back

placeOverTrigger removed every inline max-block-size on the menu chain at
each open, so a caller's style={{maxBlockSize}} on a desktop menu was lost at
the first open. It now keeps each box's inline value from before it capped
it, restores that when the cover clears, and leaves an uncapped menu alone.
…e it

FR21 (the menu grows out of its trigger and closes on a pan that starts
outside it) comes from the menu-motion pull request below this one, so this
pull request no longer removes it: its rows match that pull request's text
again, and the record's fate is decided there.
A finger that a window blur drops stays the host's until the browser ends it, and its touchend arms the click swallower, so the menu's FR28 blur case can run.
…n, on the gesture host's fix (spec:AST-094 FR28)

The FR28 blur case waited (test.fixme) for the gesture host to keep a finger a blur cancels until the browser ends it and to swallow its lift's click; meta#8309 1be6b6c does, so the case runs: nothing acts, the lit row clears, and the lift acts on nothing.
…, not the menu's bottom padding (spec:AST-094 FR30)

The finger rested 4 px above the menu's bottom edge, inside its 4 px bottom padding: once the list had scrolled to its end no row was under it at the lift, so nothing was picked though the press survived the scroll (the highlight followed it onto Share, then Archive). It rests 12 px up now, still in the edge zone and over the last row.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval waits on a spec decision: this head carries architecture:layer-runtime INV13 and its "Exit copy" section (from #8186) and component:DropdownMenu FR21 (from #8080), which main does not state, so an engineering owner needs to record the decision by commenting /approve-spec <full head SHA> on this pull request (the author may do so as an engineering owner), or both can land on main through their own pull requests first. Independently of that decision, these changes are needed:

  • In resolveHandoff (packages/core/src/hooks/useMenuPress.ts, lines 828–832), moving is still read from the previous read's roots, so one move during the open spring leaves the opening finger on mid-grow rects; decide isStale after measureHandoff and add a test for that case.
  • The opening finger's autoscroll re-read (lines 679–684) still has no test; a case where that finger rests in the bottom edge zone and the lit row follows the scrolled rows would cover spec:AST-094 FR30.
  • The diff against main still carries the unmerged stack (35,134 authored lines, 347 of them this pull request's own), so please keep this pull request in draft until #8195 lands and then rebase onto it.

[Automated review]

@cixzhang cixzhang closed this Oct 28, 2026
@cixzhang cixzhang reopened this Oct 28, 2026
@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf: closed and reopened only to restart this PR's CI on the current workflow after tonight's queue-wide cancel; nothing in the PR changed.

@vjeux
vjeux marked this pull request as draft October 29, 2026 22:36

This branch was successfully deployed

1 active deployment
Preview — d5ec70fc Deployed Oct 26, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants