Skip to content

feat(ContextMenu): a held finger lifts a block trigger above a tint - #8201

Draft
vjeux wants to merge 163 commits into
meta:mainfrom
vjeux:uikit/c20-contextmenu-lift
Draft

vjeux wants to merge 163 commits into
meta:mainfrom
vjeux:uikit/c20-contextmenu-lift

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

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-094 FR33):

  • the trigger lifts into the top layer, 26 px larger on its longer side, at most 15%;
  • an empty placeholder holds its spot, and the theme's overlay tint fades in behind it;
  • the menu fades in 16 px below it when it fits, else above;
  • a tap on the tint closes the menu and reaches nothing, and the trigger returns.

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:ContextMenu FR5 and DEC-2, spec:AST-094 FR31–FR33 and FR35. The lift is #8485's module (architecture:layer-runtime INV15). Record edits, for the owners' stamp:

  • DEC-2 stays as decided. DEC-3 replaces its element count: three elements, no shadow, and the plate as a trigger style (DD2). It quotes the owner verbatim.
  • DD2 matches UIKit's cell background, and DD2 and the count row cite DEC-3.
  • FR5's gap keeps only the layer's own entrance and exit, and the pen press.
  • The layer runtime's gaps paragraph says ContextMenu's long press uses the lift module.

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

  • One capability is added.
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Consumer docs, focused tests, and migration evidence ship with the feature.
  • Public text and artifacts contain no internal Meta context.

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:

  1. Hold the "Budget.xlsx" table cell until its menu shows. It lifts on a plate, and its first letter clears the rounded edge.
  2. Hold the fourth thumbnail: it lifts as it is, with no plate below it.
  3. Under "Other shapes", hold each box. The icon row, the outlined box and the translucent fill lift on a plate. The filled box and the image lift as they are.
  4. Turn on Dark mode and hold a row: the plate takes the dark surface.

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 easedProgress and main's new shadowVars import.

#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:

  • the tests this change reaches on main b8835b8 (test:ui --changed): 541 files, 15,630 passed;
  • the lift spec: Chromium 26 passed; WebKit under the CI profile 25 passed, 1 skipped;
  • the long-press spec, every case three times: Chromium 96 passed, 3 skipped; WebKit 75 passed, 24 skipped;
  • feat(lift): one opt-in lift-from-trigger presentation #8485's lift spec: Chromium 16 passed; WebKit 14 passed, 2 skipped;
  • DropdownMenu's interaction specs: Chromium 26 passed; WebKit 23 passed, 3 skipped;
  • the keyboard-capped lists spec: Chromium 7 passed; WebKit 7 passed;
  • the gesture specs: Chromium 77 passed, 4 skipped; WebKit 69 passed, 12 skipped; presented hit testing 6 passed in each;
  • in Chromium: the ContextMenu audit (18 passed), long-press a11y (3 passed), cursor-anchor (3 passed) and menu hover-focus (7 passed) specs; in WebKit: the cursor-anchor spec (3 passed);
  • the core typecheck, strict eslint and prettier on the changed files, changeset coverage, check:changesets and check:knowledge.

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-vite page 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:

  • the lift spec: Chromium 26 passed; WebKit under the CI profile 25 passed, 1 skipped;
  • the long-press spec: Chromium 31 passed, 1 skipped; WebKit 25 passed, 7 skipped;
  • feat(lift): one opt-in lift-from-trigger presentation #8485's lift spec: Chromium 16 passed; WebKit 14 passed, 2 skipped;
  • the keyboard-capped lists spec: Chromium 7 passed; WebKit 7 passed;
  • the gesture specs: Chromium 76 passed, 4 skipped; WebKit 68 passed, 12 skipped; presented hit testing 6 passed in each;
  • the ContextMenu audit (18 passed), long-press a11y (3 passed) and cursor-anchor specs in Chromium (3 passed), and the cursor-anchor spec in WebKit (3 passed);
  • the ContextMenu, lift, Layer, motion, DropdownMenu, hooks, gestures and utils units (5,469 passed), the core typecheck, eslint, check:changesets and check:knowledge.

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) returns ContextMenuLift, with lift(from, release, onLayerShown), enterMenu(menu) and drop().
    • Usage: the touch layer calls lift() at the menu's moment, enterMenu(menu) once the lift holds the trigger, and drop() when the menu closes.
    • Why: ContextMenu's lift adds UIKit's springs and scale, the app's layer inset, a plate for a trigger with no surface, and the menu's entrance.
    • Existing modules considered, and why they don't fit: the lift module (liftFromTrigger/) is component-neutral and takes these choices from its caller. ContextMenu.tsx owns the open state and the placement, and ContextMenuTouchLayer.tsx owns 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 private data-astryx-context-menu-lift mark, on the layer, its anchor and its placeholder.
    • Usage: ContextMenuLift.ts calls it in place of lift.lift().
    • Why: the lift module requires a private attribute that its caller owns. Under INV23, one owner module is the only file that writes the name. Tests find the lift's layer and placeholder by it.
    • Existing modules considered, and why they don't fit: privateAttributes/contextMenuTrigger.stylex.ts marks 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.ts gains contextMenu.lift: maxScale (1.15), scaleUpPoints (26) and menuGap (16). No package entry exports motion/uikit.
    • Usage: the lift's scale rule reads maxScale and scaleUpPoints; the menu's placement reads menuGap.
    • Why: they are UIKit's measured lift on iOS 26.2. A 60 pt icon lifts to 69 pt, and a larger view grows 26 pt on its longer side. The menu sits 16 pt from it.
    • Existing modules considered, and why they don't fit: contextMenu.press holds 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.

@vercel

vercel Bot commented Oct 24, 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 Nov 11, 2026 6:20pm UTC

Request Review

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

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟡 Reviewed (for maintainers only) Nov 11, 2026, 6:35 PM UTC

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

Current run: The current CI run concluded skipped. Current analysis is unavailable.


Generated by PR Enrichment workflow | View current CI run

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.
  • lint runs out of memory on this head (as on #8074 and #8125) while it passes on main.
  • 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]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 fails test-ui, and regenerate the native Swift theme after rebasing on main.
  • Clear the two strict-lint flushSync errors at ContextMenu.tsx:779 and :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.mjs and the AST-090 citation in the spec.

[Automated review]

@cixzhang

Copy link
Copy Markdown
Contributor

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.

@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Agreed: one implementation.

  • Reduced motion: the lift asks the animator; the local check, the local fade and every reducedMotion: 'keep' are gone, so a finger gets the animator's 200 ms opacity fade in place.
  • Spring player: the copy, its shadow and the dim play on the shared animator. The menu entrance's local spring sampler moves to the single spring player once it is specified (the animator does not drive filter today).
  • Exit copy: the copy moves to placeLayerExitCopy (Layer/layerExitCopy.ts) at the restack onto chore(core): the layer runtime's exit copy, for a touch presentation's close #8186.

@vjeux
vjeux force-pushed the uikit/c20-contextmenu-lift branch from 1d09473 to e01d233 Compare October 25, 2026 01:37
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 25, 2026
@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Converged in e01d233:

@vjeux

vjeux commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

Addressed in e01d233:

  • flushSync: gone. The menu renders below first, and a layout effect measures it and moves it above before the frame paints.
  • Close reset: in the close handlers (the lifted copy's return, the placement and the anchor size included); the Effect is gone.
  • The source trigger now hides under its lifted copy, as UIKit hides the source view, and shows again once the copy is back. Under Reduce Motion it hides after the copy's fade-in and shows as the fade-out starts. New browser test; it fails without the fix.
  • Safe-area insets: liftViewportOf reads env(safe-area-inset-top/bottom) through a probe and feeds the clamp and the placement. New unit test.
  • Right-to-left: the alignment is logical, so a right-to-left page swaps the physical sides. New geometry and browser tests; both fail without the fix.
  • zh guidance: corrected to match the English.
  • The tint is now its own token, --color-background-backdrop-muted (UIKit's #16152A at 21 %, the same in dark mode), within the INV10 amendment proposed in spec(theme-tokens): the modal dim and the bar glass are surface roles #8283.
  • The lift's margins against the layer runtime's gutter wait on an owner decision; the touch direction is spec(AST-094): touch presentations feel native by default #8316's one touch model.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 25, 2026
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.
It brings meta#8125's final head through the stack, with its late-timer
wait and the injected clock. The one conflict keeps this pull
request's removal of meta#8133's block-trigger anchor case, whose inline
twin it keeps.
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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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-094 DEC-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:ContextMenu FR12 (growth on springs.contextMenuLift from 150 ms, the 400 ms commit, the 735 ms display) and the new spec: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-094 DEC-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-094 FR16 gap (the menu waits 50 ms after a late menu timer), component:ContextMenu FR12'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-094 DEC-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–:48 parses a colour's alpha with its own regexes while utils/color.ts is Core's shared colour parser, so extend that parser with the slash-alpha form of any colour function, call it from fills, and delete alphaOf.
  • Miss: the menu's entrance reads its box (liftFromTrigger.ts:905) after the lift's writes and before the flushSync commit 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-runtime INV18); 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.

Automated review (maintainer link)

Comment on lines +38 to +48
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]);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines 228 to +231
`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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

vjeux added 23 commits November 11, 2026 01:56
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.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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-094 DEC-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-094 and architecture:layer-runtime record 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.ts is a new internal shared module that no current record names, so it needs an engineering owner's API decision.
  • motion/uikit/contextMenu.ts is a new internal shared module that no current record names, so it needs the same owner decision.
  • spec:AST-094 DEC-27 needs the Chrome-with-Gboard Android emulator takes (normal animations, animations removed, a slow-device profile) at the head under review.
  • component:ContextMenu FR12 with its Ownership and path lines and spec: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-094 DEC-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.ts is 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:ContextMenu FR12, FR13, PR1 and its path entry, and useMenuPress FR11's continueGesture sentence 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–:48 parses a colour's alpha with its own regexes while utils/color.ts is Core's shared colour parser, so extend that parser with the slash-alpha form of any colour function, call it from fills, and delete alphaOf.
  • Miss: the menu's entrance reads its box (liftFromTrigger.ts:905) after the lift's writes and before the flushSync commit 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-runtime INV18); start the entrance from that layout effect with its one read.
  • Newly live check: privateAttributes/contextMenuLift.stylex.ts is a new internal shared module, imported by ContextMenu/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-path and motion-rect-morph match 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.

Automated review (maintainer link)

'outline-style': 'solid',
'outline-width': PLATE_INSET,
'outline-color': SURFACE,
'outline-offset': '0px',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +180 to +181
// places the menu's anchor under the finger, outside the growth.
feedbackRef.current.hold();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

This branch was successfully deployed

1 active deployment
Preview — 6f2621d0 Deployed Nov 11, 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