Skip to content

feat(core): createMenuPress runs the menu press model without React; useMenuPress takes the menu's ref - #9504

Draft
vjeux wants to merge 6 commits into
meta:mainfrom
vjeux:feat/create-menu-press
Draft

vjeux wants to merge 6 commits into
meta:mainfrom
vjeux:feat/create-menu-press

Conversation

@vjeux

@vjeux vjeux commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #9642 and #9455; this change is the last two commits (+2,257 −808, 976 of them tests; the factory mostly moves out of useMenuPress).

User need

A menu built without React can't use the menu press model.

Problem and solution fit

INV22 asks every public interaction hook to delegate to a public create…(element, options) factory with destroy(); useMenuPress gains a ref-taking form that does. INV22 also deprecates the old signature (INV9), but spec:AST-017 FR29c migrates every maintained caller in that patch, and the item-only menus first need item-only starts in the ref form.

Current specification

  • architecture:public-component-api INV22 decides the factory and the ref-taking signature (the new API report rows; useMenuPress was already public, and its row gains an overload); spec:AST-107 FR6.
  • For a spec owner's /approve-spec: proposed module:DropdownMenu/useMenuPress DEC-9 (with OQ2) and proposed component:DropdownMenu DEC-11: both deprecate only with their callers' migration.

Public delta and caller burden

  • createMenuPress(menu, {trigger?, itemSelector, …}) returns {isTriggerClickFromPress, cancel, destroy}. It keeps its options; to change them, destroy it and create another.
  • useMenuPress(menuRef, {triggerRef?, …}) returns {isTriggerClickFromPress, cancel}. It calls the latest callbacks and re-creates the factory when the menu, its document, the trigger, a value option or the given callbacks change.
  • useMenuPress({menuRef, …}) is unchanged. No caller has to move.

Design and alternatives

  • One core runs the unchanged model for both forms, reading only committed options, with refactor(core): useLongPress runs its long press through a detector with no React #9642's long-press detector; useLongPressDetector folds into useLongPress.
  • Five core menus use the ref form. ContextMenu's press and marker stay on its menu element, as on main, never on the items sheet's container.
  • The private-attribute baseline's data-astryx-menu-press-surface entry moves with the code, count unchanged.
  • Declined: update(options) on the handle (no record decides it).

Evidence

  • createMenuPress.test.ts runs the factory with no React; useMenuPress.factory.test.tsx checks the ref form only delegates. Both fail at their first commit.
  • Four mutations each turn a case red (StrictMode's destroyed factory left attached, render-time options, a key blind to a value option or the callbacks). Each sheet case fails on one of this pull request's earlier ContextMenus.

Writing rubric results

R1 narration: none (present-tense rules). R2 research: none. R3 evidence: none changed. R4 rejected alternatives: none. R5 change references: none. R6 open questions: OQ2, unanswered, names its decision.

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.

Testing

Local gates at fc210bc: all pass: static checks, pnpm test:ui, pnpm test:node, 334 menu accessibility specs, and pinned and WebKit press specs.

@vercel

vercel Bot commented Nov 2, 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 3, 2026 3:47am UTC

Request Review

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

astracat-bot Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Nov 3, 2026, 4:04 AM UTC

@github-actions

github-actions Bot commented Nov 2, 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

Choose a reason for hiding this comment

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

This needs an engineering owner's decision before approval, and it also needs changes. Two public additions have no governing spec yet: update(options) on the handle createMenuPress returns, and deprecating DropdownMenuTriggerProps.onPointerDown/onContextMenu in favour of a press delivered through ref. An engineering owner should identify or provide the spec for them and record the decision by commenting /approve-spec <full head SHA>; the author can do this as an engineering owner. Independently:

  • spec:AST-017 FR29c asks maintained packages to migrate in the deprecation patch, but DropdownMenu and the five item-only menus (BreadcrumbItem, NavHeadingMenu, TopNavMenu, SideNavHeading, TopNavHeading) stay on useMenuPress({menuRef, …}), and the ref form has no replacement for their pressOnItemsOnly filter.
  • ContextMenu's listRef also sits on the items bottom-sheet container, so after a drill-in the whole sheet becomes a press root and the Back button loses its pressed paint under a coarse pointer.
  • At 3,145 authored lines, landing the long-press detector extraction separately would keep this PR to the INV22 move, and the StrictMode factory test should observe every destroy it claims.

[Automated review]

Comment thread .changeset/create-menu-press.md Outdated
'@astryxdesign/core': patch
---

[feat] `createMenuPress(menu, options)` runs the menu press model on any element without React: the row under a release acts, the highlight follows a held pointer, a mouse press on `trigger` opens the menu, and a finger held on it opens it after the long-press delay. `destroy()` removes every listener it added (`spec:AST-107` FR6). `useMenuPress(menuRef, options)` is its React form: `triggerRef` names the trigger, and there is nothing to spread. The options form, `useMenuPress({menuRef, …})` with its `menuProps` and `triggerProps`, still works and is deprecated: migrate to `useMenuPress(menuRef, {triggerRef, …})` and drop the spreads. It is removed at the next minor. Core's own users of the options form, which move at that minor: DropdownMenu, for its trigger's props, and the menus that track only presses on their items (BreadcrumbItem, NavHeadingMenu, TopNavMenu, and the SideNav and TopNav heading menus).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

spec:AST-017 FR29c: "Maintained Astryx packages and authoring surfaces migrate in the deprecation patch." This Changeset defers DropdownMenu and the five item-only menus to the next minor, and the ref form has no replacement for their pressOnItemsOnly filter, so a consumer menu that filters the spread handler has no working migration either.

// menu open.
const menuPress = useMenuPress({
menuRef: listRef,
useMenuPress(listRef, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

listRef is also the ref on the items bottom-sheet container (line 815), which never spread menuProps before. After a drill-in re-render the factory marks that container with data-astryx-menu-press, so pressFeedback.ts:375 stops painting a mouse or Space press on the sheet's Back button under a coarse pointer. A ref that only renderedMenu carries would keep the base behavior.

// Every factory the hook created is destroyed: StrictMode's replayed
// effect destroyed the first before creating the second.
expect(destroys.every(d => d.mock.calls.length <= 1)).toBe(true);
expect(handles.length - 1 + 1).toBe(handles.length);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This assertion is always true, and the destroy spies are attached after render, so the factory StrictMode's replay destroys is never observed. Spying each handle as it is created (wrapping the mocked createMenuPress) and asserting that created equals destroyed after unmount would prove the test's name.

/** End the gesture in flight with nothing acting (the menu closed). */
cancel: () => void;
/** Take new options: callbacks, the trigger, the row selector and the rest. */
update: (options: CreateMenuPressOptions) => void;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

architecture:public-component-api INV22 and spec:AST-107 FR6 name only destroy() on a factory's handle, and no current record decides an update(options) member. An engineering owner should confirm this public shape, or the hook could re-create the factory when its options change.

vjeux added 2 commits November 2, 2026 03:43
…, on the touch events' clock

useLongPress timed a hold with a page timer set at touchstart and cleared
by touchend. When the main thread was busy through the hold, the page ran
the queued touchend before the overdue timer, and the long press was lost:
a Table cell's ContextMenu opened nothing after an 800 ms hold.
spec:AST-094 FR16 asks a gesture to keep the event clock: a blocked main
thread delays the report and never the moment reported.

- The hold is a deadline on the touch events' own timestamps
  (touchstart.timeStamp + delay). A move, a second finger, or a lift
  stamped at or past it reports the pending press first, at its start
  point, then acts.
- The page timer waits only for what is left of the hold, so a touch the
  page handled late does not lengthen it. A timer that fires late yields
  once to queued input, so a lift stamped before the deadline still
  cancels the press: the gesture arena's rule (gestures/GestureArena.ts).
- The clock is the touch's own window's, (e.view ?? window).performance,
  captured at touchstart: a same-origin iframe or a Picture-in-Picture
  window has its own time origin. Epoch stamps (jsdom) come onto it through
  its time origin, and the timer never waits past the delay.
- onTouchEnd and onTouchCancel take the touch event, optionally;
  ContextMenu passes it.

Tests: useLongPress's event-clock cases (six fail on main), among them a
touch on a window clock whose origin is later than the page's, and a
TableCellMenus browser case whose first long press is held through a
1,200 ms busy main thread (fails on main). A [fix] changeset.
jsdom stamps events with Date.now(). The hold converted such a stamp by
subtracting the window's real time origin, so in a test that freezes the
system time at an earlier date the touch landed months in the past, its
timer fired at once, and a 100 ms tap opened ContextMenu. An epoch stamp's
age is now read on Date.now(), the clock that made it, and placed on the
window's clock: clock.now() - (Date.now() - timeStamp).

Tests: an epoch stamp handled 300 ms late is due in 200 ms, and a tap
under a frozen earlier system time opens nothing; both fail on the
previous version.
@vjeux
vjeux force-pushed the feat/create-menu-press branch from f0907e7 to 492c328 Compare November 2, 2026 13:58
…one by distance

A stamp counted as a Date.now() stamp only above 1e12, so in a jsdom test
that froze the date at 0 and left performance real, a small Date.now()
stamp read as a window-clock time seconds in the past and a 100 ms tap
opened ContextMenu. A stamp nearer Date.now() than the window's clock is
now read as a Date.now() stamp, by its age. A stamp of 0 carries no time,
so the hold falls back to the full delay on the page's timer.

Tests: a tap under a date frozen at 0 with a real window clock, and a
touch stamped 0; each fails on the previous version.

@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 an engineering owner's decision, and the pull request also needs changes. No current spec decides two public additions: update(options) on the handle createMenuPress returns, and deprecating DropdownMenuTriggerProps.onPointerDown/onContextMenu so the press reaches a renderTrigger control through ref; an engineering owner (the author is one) should identify or provide the governing spec and record the decision by commenting /approve-spec <full head SHA>. Independently:

  • spec:AST-017 FR29c asks maintained packages to migrate in the deprecation patch, but DropdownMenu and the five item-only menus stay on useMenuPress({menuRef, …}), and the ref form has no replacement for their pressOnItemsOnly filter.
  • ContextMenu's listRef also sits on the items bottom-sheet container, so after a drill-in the whole sheet becomes a press root and Back loses its pressed paint under a coarse pointer.
  • At 3,050 authored lines, landing the long-press detector extraction separately would keep this to the INV22 move, and the StrictMode factory test should observe every destroy it claims.

[Automated review]

Comment thread .changeset/create-menu-press.md Outdated
'@astryxdesign/core': patch
---

[feat] `createMenuPress(menu, options)` runs the menu press model on any element without React: the row under a release acts, the highlight follows a held pointer, a mouse press on `trigger` opens the menu, and a finger held on it opens it after the long-press delay. `destroy()` removes every listener it added (`spec:AST-107` FR6). `useMenuPress(menuRef, options)` is its React form: `triggerRef` names the trigger, and there is nothing to spread. The options form, `useMenuPress({menuRef, …})` with its `menuProps` and `triggerProps`, still works and is deprecated: migrate to `useMenuPress(menuRef, {triggerRef, …})` and drop the spreads. It is removed at the next minor. Core's own users of the options form, which move at that minor: DropdownMenu, for its trigger's props, and the menus that track only presses on their items (BreadcrumbItem, NavHeadingMenu, TopNavMenu, and the SideNav and TopNav heading menus).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

spec:AST-017 FR29c: "Maintained Astryx packages and authoring surfaces migrate in the deprecation patch." This defers DropdownMenu and the five item-only menus to the next minor, and the ref form has no replacement for their pressOnItemsOnly filter, so a consumer menu that filters the same way has no working migration either.

// menu open.
const menuPress = useMenuPress({
menuRef: listRef,
useMenuPress(listRef, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

listRef is also the ref on the items bottom-sheet container (line 815), which never carried the menu marker before. After a drill-in the factory marks that container with data-astryx-menu-press, so pressFeedback.ts:335 stops painting a mouse or Space press on the sheet's Back button under a coarse pointer. A ref that only renderedMenu carries would keep the base behavior.

// Every factory the hook created is destroyed: StrictMode's replayed
// effect destroyed the first before creating the second.
expect(destroys.every(d => d.mock.calls.length <= 1)).toBe(true);
expect(handles.length - 1 + 1).toBe(handles.length);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This assertion is always true, and the destroy spies are attached after render, so the factory StrictMode's replay destroys is never observed. Spying each handle as it is created (wrapping the mocked createMenuPress) and asserting created equals destroyed after unmount would prove the test's name.

Comment thread packages/core/src/hooks/useMenuPress.ts Outdated
const options: UseMenuPressRefOptions = isRefForm
? second
: (first as UseMenuPressOptions);
// The latest options, read by the press at event time, as they always were.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-blocking: "as they always were" narrates history. Suggest: // The press reads the latest options at event time.

…ith no React

The long-press detection moves out of useLongPressDetector's refs and
callbacks into an internal factory, longPressDetector: a hand start from a
point, the touch handlers on the events' clock, the movement tolerance, and
the options it reads at each start, move and fire. useLongPressDetector
creates one detector for the component's life, hands it the committed
render's options after each commit, cancels it on unmount, and returns its
methods as the handlers. A press model that runs without React can then
share the one detector.

No behavior change: the existing useLongPress cases pass unchanged. A new
case keeps a suspended transition's options away from the press. The
detector's own cases run with no React. A [chore] changeset.

The core API report also gains the row for the optional event that
useLongPress's onTouchEnd and onTouchCancel take, which the change below
this one adds.
@vjeux
vjeux force-pushed the feat/create-menu-press branch from 492c328 to 4935463 Compare November 2, 2026 22:47
@vjeux

vjeux commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

At 4935463, rebuilt on #9642 (the long-press detector, split out as suggested):

  • update(options) is gone: the handle is {isTriggerClickFromPress, cancel, destroy}. The hook calls the latest callbacks and creates a new factory when the menu, its document, the trigger, a value option or the given callbacks change.
  • Nothing is deprecated. Proposed module:DropdownMenu/useMenuPress DEC-9 (with OQ2) and component:DropdownMenu DEC-11 record that the options form and DropdownMenuTriggerProps.onPointerDown/onContextMenu are deprecated only together with their callers' migration (spec:AST-017 FR29c). Both wait on a spec owner's /approve-spec.
  • ContextMenu's press and marker stay on its menu element, never on the items sheet's container. Two cases cover the sheets, each failing on one of this pull request's earlier ContextMenus.
  • The StrictMode case counts every factory's destroy from its creation.
  • The options are synced after each commit, and the "as they always were" comment is gone.

@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 2, 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 an engineering owner's decision, and two lines need fixing either way.

No current spec settles two record changes this pull request proposes: module:DropdownMenu/useMenuPress DEC-9 with OQ2, which keeps useMenuPress({menuRef, …}) undeprecated until its callers can migrate, although architecture:public-component-api INV22 says the old signature is deprecated; and component:DropdownMenu DEC-11, which keeps DropdownMenuTriggerProps.onPointerDown and onContextMenu until a ref path exists. An engineering owner records the decision by commenting /approve-spec <full head SHA>; the author may do so as an engineering owner.

Independently:

  • useMenuPress.doc.mjs still tells builders that createMenuPress returns update (lines 104 and 175), which the handle no longer has, and it drops the options form's menuProps and triggerProps return rows while that form stays supported.
  • DEC-11 names cixzhang as its decider on 2026-10-31, but no record or comment shows that ruling, so it should read as proposed and pending owner review, as DEC-9 does.

Otherwise the factory, the ref form and the five migrated menus behave as on main.

[Automated review]

type: '() => void',
description:
'End the gesture in flight with nothing acting, for when the menu closes under it.',
'End the gesture in flight with nothing acting, for when the menu closes under it. Nothing is spread: the press listens on the menu and its trigger itself and marks the menu root with data-astryx-menu-press. Without React, createMenuPress(menu, options) takes the same options with the trigger as an element (trigger) and returns {isTriggerClickFromPress, cancel, update, destroy}; destroy removes every listener it added.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The handle has no update any more (createMenuPress.ts:145-159). This should read {isTriggerClickFromPress, cancel, destroy}; a builder following it gets a type error, or a TypeError in plain JavaScript.

'true = the trigger click belongs to the press that already opened / closed; ignore it.',
cancel: 'end the gesture w/o acting.',
cancel:
'end the gesture w/o acting. Nothing to spread. No React: createMenuPress(menu, {trigger, …}) → {isTriggerClickFromPress, cancel, update, destroy}.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Same here: createMenuPress(menu, {trigger, …}) → {isTriggerClickFromPress, cancel, destroy}.


**Reference:** `component:DropdownMenu/DEC-11`

**Decider:** `cixzhang`, 2026-10-31

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No record or comment shows a 2026-10-31 ruling on this, and the description says DEC-11 waits on /approve-spec. Suggest ### DEC-11 (proposed) and **Decider:** proposed by vjeux, 2026-11-02; pending owner review, as DEC-9 is written, until an owner rules.

Comment thread packages/core/src/hooks/useMenuPress.ts Outdated
Comment on lines +284 to +287
// The ref form: createMenuPress on the element behind the ref. The factory
// keeps the options it is created with, so its callbacks call the latest
// ones, and a new factory replaces it when its menu, the menu's document, the
// trigger, a value option, or the set of given callbacks changes.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-blocking (INV29): one 40-word sentence on several topics. Suggest:

// The factory keeps its creation options. Its callbacks call the latest ones.
// A new menu, document, trigger, value option or callback set creates a new factory.

Comment thread packages/core/src/hooks/useLongPress.ts Outdated
// the only touch way to open a surface at the press point.
//
// A caller can spread the touch handlers, or call `start`, `moveTo` and
// `cancel` from its own pointer events, as `useMenuPress` does. Both ways use

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-blocking: useMenuPress no longer drives these handlers; the press uses longPressDetector directly, and useLongPressDetector now has no caller but useLongPress. Suggest updating this comment and the @position line above, and folding useLongPressDetector into useLongPress.

@vjeux
vjeux force-pushed the feat/create-menu-press branch from 4935463 to 499c662 Compare November 3, 2026 01:57
@vjeux

vjeux commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

At 499c662:

  • The hook doc no longer lists update: the handle is {isTriggerClickFromPress, cancel, destroy}, and the options form's menuProps and triggerProps return rows are back.
  • DEC-11 now reads as proposed, pending owner review, like DEC-9.
  • The ref-form comment is split as suggested.
  • useLongPressDetector folds into useLongPress, whose header no longer names useMenuPress.

The owner decision on DEC-9 with OQ2 and on DEC-11 waits on the spec owners.

@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 an engineering owner's decision, and one fix is needed either way.

No current spec settles two record changes this pull request proposes: module:DropdownMenu/useMenuPress DEC-9 with OQ2, which keeps useMenuPress({menuRef, …}) undeprecated until its callers can migrate although architecture:public-component-api INV22 says the old signature is deprecated, and component:DropdownMenu DEC-11, which keeps DropdownMenuTriggerProps.onPointerDown and onContextMenu until a ref path exists. An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so if they are an engineering owner.

Required either way:

  • The ref form creates no press while its menu element is unmounted (useMenuPress.ts:303-305), so a menu rendered only while open never opens on a trigger press-down, yet useMenuPress.doc.mjs recommends that form for any new menu, its triggerRef row and the changeset state no such condition, and line 27 says the hook hands the press the latest options although the factory keeps its creation options.

Otherwise the factory, the ref form and the five migrated menus behave as on main, and the earlier notes on update in the hook docs and on DEC-11's decider are resolved.

[Automated review]

delayMs: options.longPressDelayMs ?? MENU_PRESS_LONG_PRESS_MS,
attached?.press.destroy();
attachedRef.current =
menu == null

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

While menuRef.current is null this creates no press, so the element behind triggerRef gets no listener either: a menu rendered only while open never opens on a trigger press-down or a held finger, and isTriggerClickFromPress() stays false, so only the click toggles it. With the menu rendered only while open, a mouse pointerdown on the trigger calls onTriggerPress in the options form and never in this form. Please state this in the hook docs and the changeset (or attach the trigger without a menu).

{
guidance: true,
description:
'Prefer the ref form, useMenuPress(menuRef, {triggerRef, …}), for a new menu: it listens on the menu and the trigger itself, so nothing is spread. The options form, useMenuPress({menuRef, …}), returns menuProps and triggerProps to spread instead.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This recommends the ref form for any new menu, but its press, trigger half included, exists only while the menu element is mounted (useMenuPress.ts:303-305). Suggest adding: "Its press, the trigger's included, exists only while the menu element is mounted; for a menu rendered only while open, use the options form." The dense guidance at line 224 needs the same note.

name: 'menuRef',
type: 'RefObject<HTMLElement | null>',
description:
'The menu or listbox root — the surface whose rows a press picks from. The hook creates one press for the element behind the ref, hands it the latest options at each commit, creates a new one when the ref moves to another node, and destroys it at unmount.',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The factory keeps its creation options; the hook calls the latest callbacks and creates a new press when a value option changes, which ends a press in flight. Suggest: "The hook creates a press for the element behind the ref after each commit, calls the latest callbacks, creates a new press when the menu, its document, the trigger, a value option or the set of callbacks changes, and destroys it at unmount."

getOptions: () => options,
});
const disconnect = core.connect();
// The highlight and the rows' hover focus follow the last input of the

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-blocking (comment style): one 40-word sentence. Suggest:

// The highlight follows the last input in the menu's document. Track the
// page's document, and the menu's own document when it differs, until destroy.

// `cancel` from its own pointer events, as `useMenuPress` does. Both ways use
// this hook's hold time, movement tolerance and timer, so a long press means
// the same thing on a context menu and on a menu trigger.
// `cancel` from its own pointer events. The menu press runs longPressDetector

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-blocking (comment style): one 32-word sentence. Suggest:

// `cancel` from its own pointer events. The menu press runs longPressDetector
// directly. The hold time, tolerance and timer are the same, so a long press
// means the same on a context menu and on a menu trigger.

vjeux added 2 commits November 2, 2026 18:36
createMenuPress.test.ts runs the factory with no React: the press model on
plain elements, every listener gone after destroy (mid-press included), a
trigger moved by destroy and create, and a menu in another document.
useMenuPress.factory.test.tsx checks the ref form only delegates: one factory
per element, the latest callbacks without a new factory, a new factory when a
value option changes, the committed callbacks while a transition suspends,
and every factory StrictMode creates destroyed once. They fail here: the
factory and the ref form come in the next commit.
createMenuPress(menu, options) runs the press model on any element: the row
under a release acts, the highlight follows a held pointer, a mouse press on
the trigger opens the menu, and a finger held on it opens it after the
long-press delay, on the shared long-press detector. It keeps the options it
is created with, and destroy() removes every listener it added.

useMenuPress(menuRef, options) is its React form, with nothing to spread. It
calls the latest callbacks, reads only committed options, and creates a new
factory when the menu, its document, the trigger, a value option or the set
of given callbacks changes. ContextMenu, DropdownMenuSubMenu,
MenuBottomSheetActionList, Selector and TabMenu use it. ContextMenu's press
rides its menu element, which carries the press marker in its markup as
before, never the items sheet's container, so the sheet's Back keeps its
press paint. The options form is unchanged, and its doc keeps its returns.
useLongPressDetector folds into useLongPress, since the menu press runs the
detector directly.

The core API report gains the factory, its types and the ref form. The
DropdownMenu record proposes that the trigger props' press handlers leave
only with a ref path, and the useMenuPress record that the options form is
deprecated together with its callers' migration, both pending owner review.
@vjeux
vjeux force-pushed the feat/create-menu-press branch from 499c662 to fc210bc Compare November 3, 2026 03:34
@vjeux

vjeux commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

At fc210bc:

  • The ref form's press, the trigger's included, exists only while the menu element is mounted. The menuRef and triggerRef rows, both "Prefer the ref form" lines, the hook's and triggerRef's JSDoc, and the changeset now say so, and send a menu rendered only while open to the options form. A case pins it: no press and no onTriggerPress while the menu is unmounted, then both once it mounts. Attaching the trigger without a menu would need the factory to take a menu it resolves later, a signature change for the owners.
  • The menuRef row takes your wording: the latest callbacks, and a new press on a new menu, document, trigger, value option or callback set.
  • Both comment splits are in, with four more long sentences this pull request wrote.

@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 an engineering owner's decision on two proposed spec records; nothing else in the code needs to change.

  • module:DropdownMenu/useMenuPress DEC-9 (with OQ2) keeps useMenuPress({menuRef, …}) undeprecated beside the new useMenuPress(menuRef, options) until the ref form can start a press only on an item. architecture:public-component-api INV22 says the old signature is deprecated under INV9, and INV21 allows a second API for one job only while the first is being deprecated. An owner either approves that exception to INV22 or asks for the deprecation now, with every maintained caller migrated in this patch (spec:AST-017 FR29c).
  • component:DropdownMenu DEC-11 is a new decision no current record states: the trigger's onPointerDown/onContextMenu are deprecated only together with a ref path and every caller's migration.

An engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner. Earlier feedback is addressed: the ref form's docs and changeset now say its press, trigger included, needs a mounted menu. The factory, the ref form and the event-clock change otherwise match INV22, spec:AST-107 FR6 and spec:AST-094 FR16, and a busy-page long press now opens the menu in Chromium where the base drops it.

[Automated review]

This branch was successfully deployed

1 active deployment
Preview — fc210bcb Deployed Nov 3, 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:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant