Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
8fc6d73 to
7f17a8b
Compare
PR Analysis Report
Generated by PR Enrichment workflow | View current CI run |
There was a problem hiding this comment.
Approval is on hold for an engineering-owner decision: this pull request adds direction that no current spec states, namely draft spec:AST-094 (FR1–FR23, DEC-1, DEC-2), the ContextMenu lift for touch-opened block triggers, and the new --color-background-backdrop dim that replaces the blurred scrim on Dialog, Drawer, MobileNav and BottomSheet, and its FR11 and FR14 would change the current design:user-states DR1 and spec:AST-025 FR14, which those records would then need to say. Any engineering owner, including the author as one, can record approval by commenting /approve-spec <full head SHA>, or reject or split any of these. Whatever is decided, these need fixing before approval:
- A block trigger without its own background lifts as a see-through copy over the still-visible trigger, so the ContextMenu Default story shows its label and border twice.
lintruns out of memory on this head (as on #8074 and #8125) while it passes onmain.- The range carries 26,730 authored lines because the stacked predecessors are unmerged; landing them first, or basing this pull request on its predecessor, leaves this commit's own change to review.
Smaller fixes (the close-path Effect, the unused safe-area clamp, right-to-left menu alignment, the zh guidance and a citation of the unmerged AST-090) are detailed in the review.
[Automated review]
7f17a8b to
1d09473
Compare
There was a problem hiding this comment.
Approval waits on an engineering owner's decision on the touch-presentation direction this pull request introduces: draft spec:AST-094, the iOS ContextMenu lift, the unblurred modal dim token and the new 744 / 500 px compact rule. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so if they are an engineering owner. Whatever the decision, these changes are needed:
- Rename
--color-background-backdrop, which breaks the colour token grammar (theme-tokens INV10) and failstest-ui, and regenerate the native Swift theme after rebasing onmain. - Clear the two strict-lint
flushSyncerrors atContextMenu.tsx:779and:790. - Split the 28,130-line range so that this pull request reviews the ContextMenu press and lift alone.
- Hide the source trigger under the lifted copy, move the close reset out of the Effect, feed the safe-area insets to the lift geometry, and fix right-to-left menu alignment.
- Correct the Chinese guidance in
ContextMenu.doc.mjsand the AST-090 citation in the spec.
[Automated review]
|
Posted by Cindy's agent on her behalf. Parts of this PR re-implement behavior that already exists, or is being specified, as a shared part of core. Converging keeps one implementation per behavior:
This is about where the behavior lives, not about the PR's intent. |
|
Agreed: one implementation.
|
1d09473 to
e01d233
Compare
|
Converged in e01d233:
|
|
Addressed in e01d233:
|
…nd a blur-on frame
It brings meta#8125's final head: the press reads the page's time from the gesture arena's injected clock, which is performance.now() in a browser, so nothing changes there.
The menu's entrance plays as a transform on its layer. A slow first frame, such as WebKit's on a cold shader cache, can hold that transform at its start when the case first reads the menu. The case now reads the menu's laid-out position, which an anchor that moved the menu still changes and the entrance does not.
The menu's entrance plays as a transform on its layer. A slow first frame, such as WebKit's on a cold shader cache, can hold that transform at its start when the inline trigger's case first reads the menu. The case now reads the menu's laid-out position, which an anchor that moved the menu still changes and the entrance does not.
There was a problem hiding this comment.
Changes requested for this stack. A pull request in it lands only when it and every pull request below it are approved.
#8125 — changes requested
- Miss: under a finger, a ContextMenu rendered into a same-origin iframe (or a Picture-in-Picture window) opens its menu on any tap, because the gesture host times every document's touches on the page's clock (
GestureHost.ts:913) while the hold counts from the touch's own timestamp (contextMenuPress.ts:148-149); give each document's gesture host that document's clock (or keep the released long press for a trigger in another document) and add a touch case to the second-document tests. - The Android Chrome-with-Gboard emulator run that
spec:AST-094DEC-27 requires at the exact head (normal and removed animations, plus a slow-device profile) is not reported for this head. - @humbertovirtudes's changes-requested review stays in force until they re-review or dismiss it.
#9816 — changes requested; approval also waits on an engineering owner
- The new
component:ContextMenuFR12 (growth onsprings.contextMenuLiftfrom 150 ms, the 400 ms commit, the 735 ms display) and the newspec:AST-094"FR16 gap" line have no engineering owner's approval at that head; an engineering owner records it by commenting/approve-spec <full head SHA>on #9816, and the author may do so as an engineering owner. spec:AST-094DEC-27 needs the Chrome-with-Gboard Android emulator takes (normal animations, animations removed, a slow-device profile) at the head under review.- Its stack-line item is met: #8133's head now contains it.
#8133 — changes requested; approval also waits on an engineering owner
- The new
spec:AST-094FR16 gap (the menu waits 50 ms after a late menu timer),component:ContextMenuFR12's growth toward 1.0735 from a 150 ms onset and PR1's commit-time mount of the closed menu have no engineering owner's approval at that head; an engineering owner records it by commenting/approve-spec <full head SHA>on #8133. spec:AST-094DEC-27 asks for the Android emulator run (Chrome with Gboard: normal animations, animations removed, a slow-device profile) at the exact head under review, and the description's takes name an earlier head.
#8201 — changes requested; approval also waits on an engineering owner
- Miss:
ContextMenuLift.ts:38–:48parses a colour's alpha with its own regexes whileutils/color.tsis Core's shared colour parser, so extend that parser with the slash-alpha form of any colour function, call it fromfills, and deletealphaOf. - Miss: the menu's entrance reads its box (
liftFromTrigger.ts:905) after the lift's writes and before theflushSynccommit whose layout effect reads the menu again (ContextMenu.tsx:857), so the frame that shows the menu lays the page out twice more after the lift's writes (architecture:react-component-runtimeINV18); start the entrance from that layout effect with its one read.
The new component:ContextMenu DEC-3 (no shadow, three lift elements, no exit copy) replaces current DEC-2's element count, and the new DD2 plate rule has no governing spec yet; an engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, the author may do so as an engineering owner, and a design owner may approve DD2.
Whole stack — fails: the top head still carries #8125's iframe clock defect, the missing emulator takes of #8125, #9816 and #8133, and #8201's two defects above.
| function alphaOf(color: string): number { | ||
| if (color === '' || color === 'transparent') { | ||
| return 0; | ||
| } | ||
| const slash = /\/\s*([\d.]+)(%?)\s*\)$/.exec(color); | ||
| if (slash != null) { | ||
| return Number(slash[1]) / (slash[2] === '%' ? 100 : 1); | ||
| } | ||
| const comma = /rgba?\([^,]+,[^,]+,[^,]+,\s*([\d.]+)\)/.exec(color); | ||
| return comma == null ? 1 : Number(comma[1]); | ||
| } |
There was a problem hiding this comment.
Miss: alphaOf parses a computed colour's alpha with its own regexes, while utils/color.ts is Core's one colour parser ("A single home for the color parsers that were previously duplicated") and parseRgb/parseColor already read rgb()/rgba() alpha, comma or slash. Two parsers drift apart, so a fix in one silently misses the plate rule. Give utils/color.ts the slash-alpha form of any colour function (the oklch() and color() values a browser computes), call it from fills, and delete alphaOf.
| const element = | ||
| surfaceElement.closest<HTMLElement>('[popover]') ?? surfaceElement; | ||
| endSurface(current); | ||
| const rect = element.getBoundingClientRect(); |
There was a problem hiding this comment.
Miss: this read runs inside the touch layer's flushSync (ContextMenuTouchLayer.tsx:185), after the lift has written the trigger's pins, placeholder and popover, and the same task then writes the morph and shield, commits the placement and reads menu.offsetHeight in ContextMenu's layout effect (ContextMenu.tsx:857). The frame that shows the menu lays the page out twice more after the lift's writes (architecture:react-component-runtime INV18). Starting the entrance from that layout effect, with the box it already reads, keeps every read after the lift's writes to one.
| exists, and Dialog, Drawer, BottomSheet, Lightbox, MobileNav, ToastViewport, | ||
| sheet and anchored presentations, and the one lift-from-trigger module (INV15, | ||
| `packages/core/src/liftFromTrigger/`), which ContextMenu's long press uses | ||
| and Lightbox does not yet; no runtime backdrop tint exists, and Dialog, Drawer, BottomSheet, Lightbox, MobileNav, ToastViewport, |
There was a problem hiding this comment.
Non-blocking: "which ContextMenu's long press uses and Lightbox does not yet" is adoption order; state the INV15 mismatch that remains (Lightbox builds its own) and drop the sequencing.
| `spec:AST-094` FR5) once Core has that morph; until then, the module plays | ||
| its own compositor translate. It never animates height. Its walk visits only | ||
| its own compositor translate. (Implementation gap: Core has the shared | ||
| rectangle morph, but the size-change glide still plays its own compositor | ||
| translate.) It never animates height. Its walk visits only |
There was a problem hiding this comment.
Non-blocking: this now says both "until then, the module plays its own compositor translate" and "Core has the shared rectangle morph". Drop the "once Core has that morph; until then" clause and keep the gap sentence.
FR11's row names the member of useMenuPress's return through which an opener hands its held pointer to the menu.
The press feedback kept every mounted trigger in a module-level set to find the trigger a press mark belongs to. Each trigger now carries the private context-menu-trigger attribute while its press feedback is attached, and closest() finds it. The attribute's owner module is privateAttributes/contextMenuTrigger.stylex.ts.
The Ownership section names the private attribute's owner module, its writer and its closest reader.
The Chinese summary gains the slide-to-pick sentence the English one has. A comment that only restated continueGesture goes.
ContextMenuTouch.ts only re-exported the press recognizer for the touch layer, its one importer. The layer now imports contextMenuPress.ts.
A finger crosses 11.5 px at 700 ms while the page is blocked from 600 ms to 850 ms, so its move reaches the page after the menu's moment. The hold must still end, with no menu. The case checks that the move reached the page behind the block and within the press's 50 ms wait.
AST-094's current state says why the press keeps a 50 ms wait beside the arena's one-task yield, and names the arena's page time and the touch fields the press reads. The FR16 gap line stays as it was. Layer runtime's outsidePress owner line names ContextMenu's watch and its pressTypes option.
The record now links motion/uikit/contextMenu.ts, the context menu's measured UIKit table, by path. Its public-contract line says that an omitted presentation means the touch model under a coarse pointer, with presentation="popover" as the way back, instead of no change.
A ContextMenu rendered into a same-origin iframe gets a 120 ms tap stamped on that window's clock. It must open no menu and keep its click. The gesture host reads each document's own clock since the second-document clock fix.
component:ContextMenu's Ownership names contextMenuPress.ts and ContextMenuTouchLayer.tsx: the touch hold's recognizer, and the layer that attaches it and opens the menu. Records only, no code.
The press feedback's head carries main, where useDropdownMenuPress folds into the one useMenuPress. The continuation moves with it: continueGesture and MenuPressContinuation sit on useMenuPress's return, marked internal, and ContextMenu calls useMenuPress. The test that asserted no continuation on the public return goes, and the harness hands its tests the one return.
The touch layer's header read as if any second finger ended the hold. The recognizer fails on a lift, a second finger or a scroll only before the commit. The timeline's header, the spec's header and one spec comment named a press feedback, its growing copy and its scales, which this code does not have. Comments only.
The press feedback ships here. So the timeline's header names it, its growing copy and its scales, and the long-press spec's header and its scroll helper name it again. Comments only.
A ContextMenu in a same-origin iframe or a Picture-in-Picture window gets its touches on that window's clock. Its growth must start in the first frame at or after that clock's onset, 150 ms after the landing.
The gesture host times each document's touches on its own window's clock, so a trigger in a same-origin iframe or a Picture-in-Picture window reports its landing on that clock. The press feedback still watched the page's frames and clock, so the growth started in the first frame, dated on the page's clock. It now reads the trigger's window for its frames and clock. For another window, its animator runs on that window's clock and frames; the page's own document keeps the defaults. A comment that only restated a name goes.
The touch grow and the trigger lift are a gap that Current-state impact states against FR5. So the End-user impact row drops its "does not yet" sentence. Records only.
The block repeated what the hook's and the touch layer's JSDoc say. One line stays: only the recognizer's failure or cancel returns the growth. Comment only.
There was a problem hiding this comment.
Changes requested for this stack. A pull request in it lands only when it and every pull request below it are approved.
#8125 — changes requested; approval also waits on an engineering owner
- The Android Chrome-with-Gboard emulator run that
spec:AST-094DEC-27 requires at the exact head (normal and removed animations, plus a slow-device profile) is not reported for its head. - Its
component:ContextMenu,spec:AST-094andarchitecture:layer-runtimerecord edits wait on an engineering owner's/approve-spec <full head SHA>on #8125, and @humbertovirtudes's changes-requested review stays in force until they re-review or dismiss it.
#9816 — changes requested; approval also waits on an engineering owner
privateAttributes/contextMenuTrigger.stylex.tsis a new internal shared module that no current record names, so it needs an engineering owner's API decision.motion/uikit/contextMenu.tsis a new internal shared module that no current record names, so it needs the same owner decision.spec:AST-094DEC-27 needs the Chrome-with-Gboard Android emulator takes (normal animations, animations removed, a slow-device profile) at the head under review.component:ContextMenuFR12 with its Ownership and path lines andspec:AST-094's "FR16 gap" and "Two waits" lines wait on an engineering owner's/approve-spec <full head SHA>on #9816; its stack-line item is now met.
#8133 — changes requested; approval also waits on an engineering owner
spec:AST-094DEC-27 asks for the Android emulator run (Chrome with Gboard: normal animations, animations removed, a slow-device profile) at the exact head under review.motion/uikit/contextMenu.tsis a new internal shared module, imported by the ContextMenu folder, that no current record names; the exact-head approval of the record that names it settles this.spec:AST-094's FR16 gap,component:ContextMenuFR12, FR13, PR1 and its path entry, anduseMenuPressFR11'scontinueGesturesentence wait on an engineering owner's/approve-spec <full head SHA>on #8133.
#8201 — changes requested; approval also waits on an engineering owner
- Miss:
ContextMenuLift.ts:38–:48parses a colour's alpha with its own regexes whileutils/color.tsis Core's shared colour parser, so extend that parser with the slash-alpha form of any colour function, call it fromfills, and deletealphaOf. - Miss: the menu's entrance reads its box (
liftFromTrigger.ts:905) after the lift's writes and before theflushSynccommit whose layout effect reads the menu again (ContextMenu.tsx:857), so the frame that shows the menu lays the page out twice more (architecture:react-component-runtimeINV18); start the entrance from that layout effect with its one read. - Newly live check:
privateAttributes/contextMenuLift.stylex.tsis a new internal shared module, imported byContextMenu/ContextMenuLift.ts, that no current record names, so it needs an owner API decision that says why the existing trigger mark's module is not enough. - Miss: the plate paints the trigger's fill and an outline at
outline-offset: 0px(ContextMenuLift.ts:174–:179), and a one-pixel ring of the tint shows where they meet on every plated trigger, so overlap the outline onto the fill by a pixel.
The new component:ContextMenu DEC-3 (no shadow, three lift elements, no exit copy) replaces current DEC-2's element count, and the new DD2 plate rule has no governing spec yet; an engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, the author may do so as an engineering owner, and a design owner may approve DD2. Non-blocking notes on two temporary-status record lines, a stale runtime clause, the DD2 verification row and two touch-layer comments are inline or in the review.
Whole stack — fails: the top head carries the members' blocking findings above and #8201's four. Required CI has not run on this draft head.
Claims and files checked
- Verified: the lift into the top layer, up to 15% and 26 px on the longer side (
liftFromTriggerGeometry.ts:64–:87); the placeholder and the overlay tint (liftFromTrigger.ts:704,:756–:813); the menu 16 px below when it fits, else above (ContextMenu.tsx:849–:867); a tap on the tint closes and the trigger returns (story frame after the tap matches rest). - Verified: no shadow and three lift elements (126 elements lifted against 110 at rest); the plate for a trigger with no surface and none for a card (story frames); an inline trigger, the sheet,
presentation="popover", mouse and keyboard keep today's menu (ContextMenu.tsx:880–:883); no new prop or token. - Verified: the changesets
contextmenu-touch-lift,contextmenu-touch-timeline,lift-module,motion-morph-pathandmotion-rect-morphmatch the code. - Not verified here: the description's local browser-spec tallies, bundle-size numbers and red-first runs.
- Files: all 51 files of #8201's increment visited (source and records by full diff, tests by case, unit suites run: 890 pass); member files are judged in their own reviews.
| 'outline-style': 'solid', | ||
| 'outline-width': PLATE_INSET, | ||
| 'outline-color': SURFACE, | ||
| 'outline-offset': '0px', |
There was a problem hiding this comment.
Miss: the plate is two paints, the trigger's fill and this outline at outline-offset: 0px, and where they meet the tint shows through. On the lift plate story's table cell, held in Chromium at 3×, a one-device-pixel ring of RGB 223–230 runs inside the white plate around "Budget.xlsx", following the inner radius. Overlap the outline onto the fill, for example outline-offset: -1px with the outline one pixel wider.
| LiftFromTriggerResult, | ||
| } from '../liftFromTrigger/liftFromTrigger'; | ||
|
|
||
| const CONTEXT_MENU_LIFT = 'data-astryx-context-menu-lift'; |
There was a problem hiding this comment.
Newly live check: this is a new internal shared module (imported by ContextMenu/ContextMenuLift.ts) that no current record names: it needs an owner API decision that says why the existing privateAttributes/contextMenuTrigger.stylex.ts is not enough. Naming it in component:ContextMenu's ownership lines, approved at the head, settles it.
| | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| | Public contract | One default moves: under a coarse primary pointer, an omitted `presentation` means the touch model (`spec:AST-094` FR25), and `presentation="popover"` keeps the released surface. ContextMenu takes caller `children` as the Trigger area, data-driven `items` or compound `menuContent`, `label`, `presentation`, `isDisabled`, and `triggerAs`. | | ||
| | Behavior | Right-click, Shift+F10 or the Menu key, and a touch long press open a menu at the cursor (or the trigger's bottom-left from the keyboard), or a bottom sheet; rows follow DropdownMenu's rows and press model. | | ||
| | End-user impact | People reach a region's actions by mouse, keyboard, or a held finger; a sub-menu drills in on a phone. A held finger lifts a block trigger above a tinted page, with the menu beside it; the menu does not yet grow from the finger. Mouse and keyboard opens keep today's look. | |
There was a problem hiding this comment.
Non-blocking: "the menu does not yet grow from the finger" is temporary status. State the FR31 mismatch instead, or leave it to the Current-state impact entry that already names it.
| | FR3 | `ContextMenu.test.tsx` destructive-variant, section, divider, selectable, and alias cases | Shared rows through ContextMenu | A row that loses DropdownMenu's semantics or variant fails. Evidence gap: no test asserts the BottomSheet and List delegation beyond rendering. | `audit:ContextMenu/theming` | | ||
| | FR5 | `ContextMenu.test.tsx` "acts on the row under a finger release, once, and swallows the stray click" and outside-dismissal cases; `outsidePress.test.ts` pan cases; `ContextMenuLongPress.interaction.browser.spec.ts` "a new finger that pans outside the open menu closes it, and the next pan scrolls the page"; `ContextMenuLift.interaction.browser.spec.ts` lift cases; `ContextMenuLongPress.a11y.chromium.spec.ts` | Finger release; long press lift; tap, mouse, pen, and pan outside | A row acting on the press row, a stray click that acts, a long press that closes its own menu, a tap or mouse press outside that leaves it open, or a pan outside that leaves it open fails. Evidence gap: no test asserts the FR31 grow. | `audit:ContextMenu/behavior` | | ||
| | FR5, DD1 touch scope | `ContextMenuLift.interaction.browser.spec.ts` "a held card lifts itself to UIKit’s size about its center over the theme’s dim with no shadow, its spot in the page held, its menu 16 px below and centered" and "the menu fades in on springs.contextMenuMorph, opacity only: no blur, travel or growth (AST-094 FR5)"; the long-press spec's fine-pointer cases | Touch-opened, mouse-opened and keyboard-opened menus on a block trigger | A mouse- or keyboard-opened menu that lifts its trigger, tints the page or grows, or a touch-opened menu that blurs the page, grows a second surface, or animates anything but transform and opacity, fails. Evidence gap: the grow is unbuilt, so no test asserts it. | `audit:ContextMenu/behavior` | | ||
| | DD2, DEC-3 | `ContextMenuLift.interaction.browser.spec.ts` "a trigger that paints no box of its own lifts on the theme’s surface, rounded, and leaves no style behind once it is back", "a picture that paints its own surface lifts as it is, with no plate, though its trigger is taller than the picture", "a table cell lifts on a plate that reaches past its text on every side, with the container radius outside" and the shape cases; `ContextMenu.test.tsx` plate case | |
There was a problem hiding this comment.
Non-blocking: this verification row fills two of the table's five cells; add the representative states, the failure expectation and the audit section.
| `spec:AST-094` FR31, FR32, and FR33. A menu opened by mouse or keyboard | ||
| already keeps its released appearance and motion. | ||
| - **Touch grow (FR5).** A touch-opened menu fades in beside the lifted trigger | ||
| instead of growing from the finger, and it closes on the layer's own exit; a |
There was a problem hiding this comment.
Non-blocking: under a lift the menu renders without layerAnimations.below, so it has no layer exit; DEC-3 says the lifted menu closes with no exit copy. Say that here for the lifted case.
| // places the menu's anchor under the finger, outside the growth. | ||
| feedbackRef.current.hold(); |
There was a problem hiding this comment.
Non-blocking: on a block trigger the lift now lets go of the growth (letGo) and anchors the menu to its own box, so this comment, and the file comment's "it holds while that menu is up", describe only the inline trigger and the sheet.
Stack: #8125 → #9816 → #8133 → #8201
Stack: #9764 → #9769 → #8485 → #8201
The branch merges #8133's head (4b1f2a7) and carries #8485 at a0eec83. Own commits: vjeux/astryx@cb0cc0b...382edd5, and seven since (Testing).
User need
On iOS, a held finger lifts the view out of the page above a tint, with the menu beside it. ContextMenu under a finger lifts nothing.
Problem and solution fit
When a held finger opens a block trigger's menu (
spec:AST-094FR33):The lifted trigger casts no shadow (owner direction, 2026-11-09). A trigger with no surface of its own, such as a text row or cell, lifts on a plate of the theme's surface, rounded. A card or picture lifts as it is. An inline trigger, the sheet,
presentation="popover", mouse and keyboard keep today's look.Current specification
component:ContextMenuFR5 and DEC-2,spec:AST-094FR31–FR33 and FR35. The lift is #8485's module (architecture:layer-runtimeINV15). Record edits, for the owners' stamp:Public delta and caller burden
No new prop or token.
Design and alternatives
The menu anchors to an empty box at the lifted rect, and shows as the lift's layer shows.
A trigger has a surface when a near-opaque fill or picture covers all it shows. UIKit lifts a cell on its own background, inside its margins. A web cell's trigger sits inside the padding, so the plate reaches past it. Rejected: plating every preview, since a trigger's box is often taller than its picture.
A new finger's pan on the tint closes the menu, scrolling nothing.
Scope
Not here: the grow from the press and back (FR31), after this look's approval.
Testing
On a phone, open the Sandbox page
/pages/context-menu-lift/in this head's preview:This head, 6f2621d, merges #8133's head 4b1f2a7 with no code change. It brings this round's #8125, #9816 and #8133 heads, on main b8835b8. The conflicts are in five ContextMenu files and SegmentedControl's motion imports. Each resolution keeps both sides. This pull request's own diff is unchanged: the same 24 files and the same changed lines. Two files differ only around those lines. The record's table needs no wider separator now. The Chinese summary adds the same text after a new sentence. SegmentedControl keeps #9769's move of
easedProgressand main's newshadowVarsimport.#8485 stays at a0eec83 here. Its newer heads drop the hook and the geometry module this lift imports, so they come with this pull request's next round.
Local checks at 6f2621d, one worker:
test:ui --changed): 541 files, 15,630 passed;Lines, this pull request's own diff over its bases: product +530/−16 (
ContextMenuLift.ts+264,ContextMenu.tsx+127/−10,ContextMenuTouchLayer.tsx+47/−2,motion/uikit/contextMenu.ts+35/−1,privateAttributes/contextMenuLift.stylex.ts+33,contextMenuPressFeedback.ts+24/−3), tests +1,748/−168, story and Sandbox +466/−1, records +74/−49, changesets +11/−4, docs +4/−3.App-level size, eager JS gzip of the
example-vitepage in a Vite production build, #8133's head 4b1f2a7 to 6f2621d: no change as shipped; +11.2 KB with one ContextMenu; +11.8 KB with one ContextMenu and one Switch.The previous head, d096b78, sits on 61f0365, which merges #8133's head 17cab2a. That brings #8125's final head cae3644 through the stack. There a late menu timer waits up to 50 ms for moves queued behind a block. The press reads the page's time from the arena's injected clock. The merge's one conflict keeps this pull request's removal of #8133's block-trigger anchor case, whose inline twin it keeps.
d096b78 makes the inline twin read the menu as laid out, not as painted, as #8133's case now does. The menu's entrance plays as a transform on its layer. On a cold shader cache, WebKit can show that entrance's first frame late, and the case's first read caught it. With a fresh shader cache, the old read failed 2 of 3 runs, and the new one passed 3 of 3.
The earlier merge, 548ccb5, merged #8133's 967b5f6. That carries #9816's inner visual box, which the press grows. With the box, seven lift cases and one slide case failed in Chromium at the first merge, 8b33ba1. dbb295a fixes them: the lift grows from the box's pressed rect, and the plate reads through it. 8b33ba1 also changes two tests in the merge. The INV22 press case now checks the box the lifted menu is placed against. #8133's block-trigger finger-anchor case gives way to the inline one, since a block trigger lifts. The released outside-press changeset is gone through the stack's merges of
main, and this pull request edits no released changeset.Own commits since 382edd5: condition waits for the seven fixed waits (5d52587), DEC-3 (cc5afbd), the box fix (dbb295a), FR5's gap (4c88c64). 180083f makes the long-press a11y case's outside tap wait for the lift to settle, since a growing lift's tint swallows a tap (FR35). ba4a81e says what the placement effect syncs.
Red first, at 15de4d7: the new plate cases fail in Chromium and WebKit, 7 of 8. The thumbnail, the pseudo-element fill and the inline image got a plate. The cell, the icon row and the outlined box got one with no reach. The translucent fill got none. The card passed.
Local checks at d096b78, one worker:
At e792e3f, the same Sandbox page: it type-checks against the built core, and its route and bundle tests pass (79).
At dd5acaa: the two finger-down-at-735 ms cases, twelve times each in WebKit under the CI profile (24 passed). The menu used to show past its bound there, 2 of 16 runs, because the lift's writes ran before its show.
Public and internal API delta
Public: no prop, type or export changes. One default changes under a finger: a held block trigger lifts above a tinted page, with the menu beside it. An inline trigger, the sheet,
presentation="popover", mouse and keyboard keep today's look.Internal, new:
ContextMenu/ContextMenuLift.ts:useContextMenuLift(triggerRef, visualRef, onBackdropTap)returnsContextMenuLift, withlift(from, release, onLayerShown),enterMenu(menu)anddrop().lift()at the menu's moment,enterMenu(menu)once the lift holds the trigger, anddrop()when the menu closes.liftFromTrigger/) is component-neutral and takes these choices from its caller.ContextMenu.tsxowns the open state and the placement, andContextMenuTouchLayer.tsxowns the recognizer. Folding the lift into either would mix the presentation into those owners.privateAttributes/contextMenuLift.stylex.ts:liftContextMenuTrigger(lift, trigger, options)lifts the trigger with the privatedata-astryx-context-menu-liftmark, on the layer, its anchor and its placeholder.ContextMenuLift.tscalls it in place oflift.lift().privateAttributes/contextMenuTrigger.stylex.tsmarks the trigger itself at rest, for the press feedback. The lift's layer, anchor and placeholder are elements the lift module creates, so they take no StyleX class.Internal, changed:
motion/uikit/contextMenu.tsgainscontextMenu.lift:maxScale(1.15),scaleUpPoints(26) andmenuGap(16). No package entry exportsmotion/uikit.maxScaleandscaleUpPoints; the menu's placement readsmenuGap.contextMenu.pressholds the hold's timeline, not the lift's geometry. The lift module takes its numbers from its caller and holds none of UIKit's.Writing rubric results
R1 narration: none. DEC-2 is unchanged. DEC-3 states today's count and the plate rule in the present tense; its verbatim quote is the owners' requested basis. DD2 states the decision and its reason. FR5's gap and the layer runtime's gaps paragraph state observable mismatches. R2 research: DD2's reason cites UIKit's behavior as a fact. R3 evidence: the verification rows name tests. R4 rejected alternatives: DEC-2 keeps its one; DEC-3 has one, in a sentence. R5 change references: none. R6 open questions: none touched. R7 project status: the End-user row's "the menu does not yet grow from the finger" is status; it is kept here for the restack proof and goes in the next round.