Skip to content

feat(core): createMenuPress runs the menu press model without React - #9504

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

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

Conversation

@vjeux

@vjeux vjeux commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Batch: Menus · createMenuPress, the menu press without React.

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. createMenuPress(menu, options) is that factory, and Core's menus run on it. useMenuPress(options) drives the factory's core through its props and stays as released, an INV22 known gap. Nothing is deprecated.

Current specification

  • architecture:public-component-api INV22 decides the factory and its element-taking signature; spec:AST-107 FR6.
  • For the spec owners' stamp: module:DropdownMenu/useMenuPress DEC-9 (one factory, and Core's menus on it) and component:DropdownMenu DEC-11 (the trigger's press handlers stay); component:DropdownMenu FR9 and spec:AST-055 FR2 name the trigger's ref path. No deprecation or cleanup rows.

Public delta and caller burden

  • createMenuPress(menu, {trigger?, itemSelector, startBehavior?, …}) returns {isTriggerClickFromPress, cancel, update, handleMenuPointerDown, handleTriggerPointerDown, handleTriggerContextMenu, destroy}; update(options) merges options from the next event without ending a press; startBehavior: 'items' starts a press only on an item.
  • useMenuPress(options) keeps its signature, types and behavior. No caller has to change. The API report adds only createMenuPress and its types.
  • One behavior changes for a finger in Core's menus: a finger's press-down that a page handler stops or cancels starts no press (DEC-9).

Design and alternatives

  • Core's six menus run on an internal ref form over the factory, so the press listens on the menu and the trigger itself; startBehavior replaces menuPressOnItems.ts.
  • In Core's menus a mouse or pen press behaves exactly as before: they forward it from React handlers. A finger's press-down acts at the window.
  • Rejected: a new factory for each change of options, which update() replaces; a public ref form of the hook, since the options form stays its one public form.

Evidence

  • Without startBehavior, the five item-only menus fail six identity cases.
  • Without the forwarded-event check, two cases fail; with an update() that merges nothing, four.
  • Desktop parity: a mouse press on DropdownMenu's trigger and a press-drag-release in Core's 11 menus, under 9 page stops and cancels plus NavHeadingMenu's cancel: 110/110 on main and here; 40 fail on the previous press.

Writing rubric results

R1, R2, R3: none. R4: DEC-9 names two rejected alternatives. R5: none. R6: none.

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.

Stack: #9910 → #9504 → #8458.

This PR edits #9910's changeset, so the two merge in one release.

Testing

At f64e8bb, DEC-9 says the options form drives the factory's core and stays an INV22 known gap, and a comment in interactionModality.ts names Core's menus instead of a ref form. check:knowledge, Prettier, review:pre and the CLI search, resolution, discovery and doc-compiler tests (57 of 57) pass there.

At 67ef7d2, the internal useDropdownMenuPress goes: it had no production caller. Its three touch-model cases now run on Core's internal ref form, with the forwards DropdownMenu uses, and the other 48 cases stay on the public options form. The core type check, lint, strict lint, Prettier and the menu press units (443 of 443: the hook, the factory, desktop parity and DropdownMenu) pass there.

At b078608, the hook's options form is no longer deprecated. The public ref-form overload and its two types go, with the codemod, its tests and changeset, and the DEP-0019 and CLN-0030 rows; DEC-9 and DEC-11 now say what the owner ruled. Core's menus keep an internal ref form over the factory, so their behavior is unchanged. The regenerated API report differs from main's only in the createMenuPress rows; main's two stale presentation rows for DropdownMenu and MoreMenu are left to their own fix. Local gates at 73a60e9, which differs only in record and doc text: the core type check, pnpm lint, strict lint, Prettier, the knowledge, changeset and SYNC checks, review:pre, the core ui suite (18,550 passed; the 4 RadioListItem and Switch source-build tests fail the same way on main in this checkout), touch-support-bundle and resolution-compat (23 of 23), and the CLI node suite (9,805 passed; text-json-parity's two experimental-render cases and resolve-theme fail the same way on main 67ad509; icon-sets and two debug-result-summary cases fail only on the loaded machine and pass alone). On a fresh Storybook build: the Chromium a11y specs of the menus, the navigation menus and Selector (266 of 266) and the menu interaction specs (9 of 9).

At 93f24b8, the stack is rebased onto main 5083ead, which carries the DropdownMenu touch model, the lit row and the held paint; here they run in the factory's core. The public factory never reads the touch model; Core's menus pass it through their internal ref form. NOT-FOR-LANDING arms without the touch model (223e154) or without the lit row and held paint (c35edc2) each fail five of its tests. In WebKit, through the container recipe, the drag-highlight and hand-off specs pass (7 passed; 2 pen cases skip, since WebKit has no pen input).

At fe52a5b, a finger's press in a menu inside a shadow root starts again: the press reads the press-down's target at the menu's own listener, before it defers to the window, where the target is the shadow host. Three factory cases fail with 1120baa's factory (NOT-FOR-LANDING arm ff3f843); reading composedPath()[0] instead (arm db044c1) fails the nested-row case. The shadow-root Chromium spec's finger slide passes here and fails with 1120baa's factory.

The desktop parity file passes 110 of 110 here and on main a343c79. NOT-FOR-LANDING arms: 4f1c189, without followsDocument, and d794cac, without #9910, fail the factory's document-move case; b22776e, the previous press, hook and menus under this pull request's tests, fails 45 (40 parity rows, the finger cancel cases). no-comment-provenance --check fails the same way on main: its baseline is stale there.

@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 4, 2026 7:06pm 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
Contributor

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Nov 4, 2026, 7:26 PM 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
Contributor

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
Contributor

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
Contributor

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
Contributor

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
Contributor

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.

@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, 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
Contributor

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
Contributor

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
Contributor

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
Contributor

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.

@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
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, 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
Contributor

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
Contributor

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
Contributor

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
Contributor

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
Contributor

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
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, 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]

Comment thread packages/core/src/hooks/useMenuPress.ts Outdated
delayMs: options.longPressDelayMs ?? MENU_PRESS_LONG_PRESS_MS,
attached?.press.destroy();
attachedRef.current =
menu == null

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.

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
Contributor

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
Contributor

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
Contributor

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.

Comment thread packages/core/src/hooks/useLongPress.ts Outdated
// `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
Contributor

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

@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 proposed spec changes, and three fixes are needed either way.

  • The changeset says DropdownMenu, BreadcrumbItem, NavHeadingMenu, TopNavMenu and the SideNav and TopNav heading menus move to the ref form "with no change in behavior", but a NavHeadingMenu caller's onPointerDown now runs after the press starts and its preventDefault() no longer cancels the press (NavHeadingMenu.tsx:225); name this and DropdownMenu's handler-order changes in the changeset, or run the caller's handler first again.
  • useMenuPress.doc.mjs (lines 27, 123 and 174), the createMenuPress JSDoc (createMenuPress.ts:827-829) and the useMenuPress.ts header (lines 11-15) still describe the previous revision: a new press on every option change, no update(), and six menus on the options form.
  • The @deprecated tags on DropdownMenuTriggerProps.onPointerDown and onContextMenu (DropdownMenu.tsx:375, :380) and the changeset name no replacement, which AST-017 FR29b requires; say to forward ref to the control.

No current spec decides three parts of this change: update(), startBehavior and the forwarded trigger handlers (proposed DEC-9), deprecating the two trigger press handlers (proposed DEC-11), and NavHeadingMenu's new handler order. 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. The factory, the ref form, the deprecation of the options form and the caller migration otherwise match architecture:public-component-api INV22 and AST-017 FR29c, and the touched suites pass.

[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. `update(options)` merges new options, which apply from the next event without ending a press in progress. `startBehavior: 'items'` starts a press only on an item, so other content in the menu keeps its own clicks. `destroy()` removes every listener it added. `useMenuPress(menuRef, options)` is its React form: `triggerRef` names the trigger, and there is nothing to spread. Its press exists only while the menu element is mounted, so a menu that opens on a press of its trigger keeps its element mounted while closed. The options form, `useMenuPress({menuRef, …})` with its `menuProps` and `triggerProps`, is deprecated; it still works as before. `DropdownMenuTriggerProps.onPointerDown` and `onContextMenu` are deprecated too: the menu listens on the trigger itself. DropdownMenu, BreadcrumbItem, NavHeadingMenu, TopNavMenu and the SideNav and TopNav heading menus move to the ref form with no change in behavior.

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.

"No change in behavior" does not hold for NavHeadingMenu: a caller's onPointerDown now runs after the factory's native listener, sees defaultPrevented already true for a finger, and can no longer cancel the press (it could through composeEventHandlers before). DropdownMenu's menu now also receives a caller onPointerDown the press handler used to replace, and button.onPointerDown runs after the press opens the menu. Please name these here.

ENABLED_ITEM_SELECTOR,
),
)}
onPointerDown={onPointerDownProp}

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.

This handler now runs after the press has started. A finger that slides from one row to another picks the second row even when this onPointerDown calls preventDefault(), where nothing acted before. Either restore the order (the caller first, and a cancelled event skips the press) or record the new order with an owner's approval.

name: 'menuRef',
type: 'RefObject<HTMLElement | null>',
description:
'The menu or listbox root — the surface whose rows a press picks from. 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. While the element is unmounted there is no press, not even on the trigger.',

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.

At this head a changed trigger, value option or callback set goes through update() on the same press (useMenuPress.ts:238-251); only a new menu or document creates a new press. The cancel row (line 123) also lists the handle as {isTriggerClickFromPress, cancel, destroy}, and the dense rows (174, 201) disagree with it. The returns table omits the ref form's handleTriggerPointerDown and handleTriggerContextMenu.

Comment on lines +827 to +829
* Destroy it to remove every listener it added. It keeps the options
* it is created with: to change them or the trigger, destroy it and create
* another.

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.

This contradicts update() (line 167). Suggest: "Destroy it to remove every listener it added. update changes its options or trigger from the next event."

Comment thread packages/core/src/hooks/useMenuPress.ts Outdated
Comment on lines +11 to +15
* @position Core hook; a thin wrapper over createMenuPress, in its ref form
* for ContextMenu, DropdownMenuSubMenu, MenuBottomSheetActionList, Selector
* and TabMenu. The options form, which returns props to spread, stays with
* DropdownMenu (its trigger's props), BreadcrumbItem, NavHeadingMenu,
* TopNavMenu, and the SideNav and TopNav heading menus.

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.

Every Core menu now uses the ref form, so this header is out of date. Suggest: "@position Core hook; a thin wrapper over createMenuPress. Every Core menu uses the ref form; the options form is deprecated."

Comment on lines +375 to +376
* @deprecated The menu listens on the trigger itself, so spreading this
* adds nothing.

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.

AST-017 FR29b asks every deprecation warning to name the replacement. Suggest: @deprecated Forward ref to the control; the menu listens on it. (same for onContextMenu at line 380 and in the changeset). A control that takes these handlers but not ref keeps the press only through them, so "adds nothing" is not true for it.

@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 about one behavior this pull request carries; no other change is needed.

This review covers the whole diff against main, including the useAttachment commit from #9910. The record changes (module:DropdownMenu/useMenuPress DEC-9, DEP-0019 and CLN-0030, and component:DropdownMenu DEC-11 with the amended FR9 and spec:AST-055 FR2) are owner-approved for this head, and the ref form now attaches through useAttachment, which resolves the earlier shared-lifecycle finding.

What must be settled:

  • useAttachment now releases and re-attaches every attached element (gesture recognizers, press tracker, finger scroll, pager, animator and the menu press) after the element moves to another document, at its component's next commit and for every pointer (packages/core/src/hooks/useAttachment.ts:79-95). No current record decides this: architecture:react-component-runtime INV5 and INV6 cover a replaced or unmounted node, not one that moves to another document. An engineering owner, the author included, can approve it in a comment on this pull request, or add it to a record and approve that with /approve-spec <full head SHA>.

Two non-blocking suggestions are inline: a comment in useMenuPress.ts that repeats another, and changeset wording that says the re-attach happens at the next render.

[Automated review]

nodes.every(
(node, index) =>
node === current.nodes[index] &&
node.ownerDocument === current.documents[index],

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.

This makes every useAttachment caller (gesture recognizers, press tracker, finger scroll, pager, animator, menu press) release and re-attach after its node moves to another document, for every pointer. No current record settles that yet: architecture:react-component-runtime INV5 and INV6 cover a replaced or unmounted node. Approval waits on an engineering owner's decision on this behavior.

Comment thread packages/core/src/hooks/useMenuPress.ts Outdated
Comment on lines +265 to +266
// Attaches a factory to the menu behind the ref. The options form attaches
// none: its ref is one that never holds a menu.

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 (INV29): this restates the NO_MENU comment at line 240 and narrates the useAttachment call below. Suggest deleting it; line 240 already gives the reason.

'@astryxdesign/core': patch
---

[fix] An element's gesture recognizers, press tracking, pager and animator attach again when the element moves to another document, such as a Picture-in-Picture window, instead of staying bound to the old one.

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 re-attach happens at the component's next commit, not at the move itself. Suggest: "attach again in the document an element moved to, such as a Picture-in-Picture window, at its component's next render".

@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 578e4a5

@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. The record changes are approved for this head and Core's menus keep their mouse behavior, but the astryx upgrade codemod still rewrites two shapes that change a trigger's press without leaving a TODO, which spec:AST-017 FR29c rules out.

  • Miss: a trigger that a component element or a render callback mounts is rewritten without the placement check the menu already gets (migrate-use-menu-press-to-ref-form.mjs:426-446), and because the ref form reads triggerRef only at the caller's commits, a trigger mounted in a later commit loses its press after the rewrite.
  • Miss: a renderTrigger forward such as <MyButton {...p} {...tooltipProps} onPointerDown={p.onPointerDown} /> is dropped (migrate-use-menu-press-to-ref-form.mjs:724-733) although it is what lets the menu's handler win over the other spread, so after the rewrite a mouse press-down no longer opens the menu.

Leaving the first shape with a TODO and the second unchanged, and adding both to the codemod's tests, resolves this. Separately, the attachment-follows-document changeset says no caller opts in, but useMenuPress does in this pull request.

[Automated review]

ref == null ||
// A component may drop the two handlers, or give its ref to an
// imperative handle that takes no listeners.
!isDomElement(spread.parent.parent.node) ||

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 loop checks only that the trigger element is a DOM element, not where it sits. The ref form reads triggerRef.current only at the caller's commits (useMenuPress.ts:168, :280), so a trigger inside a component element or a render callback that mounts it in a later commit loses its press after the rewrite, with no TODO (spec:AST-017 FR29c). Apply the menu's placement check (rendersWithComponent, or isAlwaysRendered) to each trigger spread, and add those two shapes to the TODO test.

// still passes the handler, under any ancestor.
if (
attribute == null ||
!takesTriggerRef(j, attribute.parent.node, [param.name], [])

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: takesTriggerRef accepts any element that spreads the props, so <MyButton {...p} {...tooltipProps} onPointerDown={p.onPointerDown} /> loses its forward, though the forward is what overrides tooltipProps.onPointerDown. After the rewrite a mouse press-down no longer opens the menu. Drop a forward only when no other spread sits between the props spread and the forward; otherwise leave it, as the other kept forwards are left.

@vjeux
vjeux force-pushed the feat/create-menu-press branch from 578e4a5 to 724cd1c Compare November 4, 2026 10:10
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Nov 4, 2026
@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 724cd1c

@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: one codemod case still breaks the migration guarantee. The record changes (module:DropdownMenu/useMenuPress DEC-9, DEP-0019 and CLN-0030, and component:DropdownMenu DEC-11 with the amended FR9 and spec:AST-055 FR2) are owner-approved for this head, and Core's menus keep their mouse behavior.

Blocking finding:

  • Miss: astryx upgrade rewrites a trigger that reached the press only through its triggerProps spread to pass its ref as triggerRef, with no TODO, so a mouse press held 300 ms or more and released on that trigger no longer closes the menu it opened (migrate-use-menu-press-to-ref-form.mjs:444-449), which breaks the migration guarantee in spec:AST-017 FR29c; leave that call unchanged with a TODO and add it to the TODO test and DEP-0019's codemod row.

The two codemod cases raised last time are fixed and covered by new tests.

[Automated review]

if (!isLocalRefObject(ref, spread)) {
return null;
}
plan.triggerRef = cloneChain(j, ref);

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: in the options form this trigger was never the press's trigger for a release, so a release on it counted as outside the menu and dismissed it. Passing it as triggerRef makes the release land on the trigger, so a mouse press held 300 ms or more (or slid into the menu and back) and released here no longer closes the menu. That changes the old result with no TODO (spec:AST-017 FR29c). Leave the call unchanged with a TODO when the trigger is reached only through its spread, and add the case to the TODO test.

@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 546162f

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

Approved pending CI: CI has not run on this head yet. Its required checks wait for a maintainer to add the ci:run label, since the branch is in a fork.

The record changes (DEC-9, DEP-0019, CLN-0030, DEC-11 and the FR9 and AST-055 FR2 amendments) are owner-approved for this head, the codemod now leaves a TODO for a trigger the old call reached only through its spread, and the touched suites pass. Next: a maintainer adds the ci:run label so CI runs; this pull request builds on #9910, which lands first.

  • Optional follow-up: INV22's known-gaps line in docs/architecture/public-component-api.md still lists useMenuPress.

[Automated review]

@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 1120baa

@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: one regression needs a fix before approval. The record changes (DEC-9, DEP-0019, CLN-0030, DEC-11 and the FR9 and AST-055 FR2 amendments) are approved for this head, and the touched suites pass.

  • Miss: a finger's press never starts in a Core menu rendered inside an open shadow root, because the press deferred to the window reads event.target there, where the browser has retargeted it to the shadow host (packages/core/src/hooks/createMenuPress.ts:724, after the deferral at :998); a finger slide from Apple to Banana in the shadow-root Selector story picks Banana on main and nothing here, so take the target from composedPath()[0] before deferring and add a shadow-root finger case to the factory tests and the shadow-root browser spec.

CI's workflows have not run on this head yet. This pull request builds on #9910, which lands first.

[Automated review]

}
// The innermost model owns the press: a flyout is a DOM descendant of
// its parent menu, and both would otherwise track the same pointer.
const target = event.target as Element | null;

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.

For a finger, this runs from the window listener that afterPageHandlers adds (line 998), and by then event.target is retargeted to the shadow host when the menu sits in an open shadow root. closest(MENU_PRESS_ROOT_SELECTOR) then misses the menu, so no finger press starts, and the event is already taken, so the React forward cannot start it either. In Chromium a finger slide from Apple to Banana in the shadow-root Selector story picks Banana on main and nothing on this head. Reading the target from composedPath()[0] while the element's own listener runs, before deferring, keeps the released behavior; the startBehavior: 'items' check at line 735 needs the same target.

@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec fe52a5b

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

Approved pending CI: CI has not run on this head yet, because the pull request is still a draft; marking it ready for review runs it.

The shadow-root finger regression is fixed: a finger's press in a menu inside a shadow root starts again, and three new factory cases and a Chromium spec case cover it. The record changes (DEC-9, DEP-0019, CLN-0030, DEC-11 and the FR9 and AST-055 FR2 amendments) are approved for this head, and the touched suites pass, desktop parity included.

Non-blocking:

  • architecture:public-component-api INV22 still lists useMenuPress among its known gaps; a follow-up record edit can drop it.
  • The description says "OQ2 closed", but module:DropdownMenu/useMenuPress has no OQ2.

This pull request builds on #9910, which lands first.

[Automated review]

vjeux added 6 commits November 4, 2026 05:49
… for a caller that asks

An attachment that listens on its node's document, as the menu press does, must attach again when the node moves into another document, such as a Picture-in-Picture window. useAttachment gains an internal followsDocument option for that. No current caller passes it, so the gesture hooks, the pager and useAnimator keep their behavior: a document move leaves their attachment as it is, and useAnimator keeps its values.
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.
…u moves to the ref form

createMenuPress gains update(options): new options merge with the ones it
has, apply from the next event, and never end a press in progress. Its
startBehavior option, 'items', starts a press only on an item, so other
content in the menu keeps its own clicks and a modified mouse press on a
link item stays the browser's. A trigger event that a caller still forwards
reaches the press once.

useMenuPress's ref form passes each commit's options through update() and
creates a new factory only for a new menu or document. DropdownMenu,
BreadcrumbItem, NavHeadingMenu, TopNavMenu and the SideNav and TopNav
heading menus move to the ref form with no change in behavior, and
menuPressOnItems.ts goes. The options form and
DropdownMenuTriggerProps.onPointerDown and onContextMenu are deprecated,
to go at the next minor. The module record's DEC-9 and the component
record's DEC-11 say so, and OQ2 is closed.
DEP-0019 deprecates useMenuPress({menuRef, ...}) and the trigger press
handlers now; CLN-0027 removes them in 0.8.0. An astryx upgrade codemod
moves calls to the ref form and drops plain forwards of the handlers,
leaving a TODO wherever it cannot prove the result. DEC-9 and DEC-11 are
settled.

The press listens for the press-down on the menu's document, after the
page's own handlers, so a caller's handler runs first again and one that
cancels a press-down inside the menu starts no press; DropdownMenu forwards
its Button's events in the released order.
…ld stops the codemod

The press listens on the menu and its trigger themselves, and acts on a press-down once it reaches the document, after the page's own handlers. That document listener lives only while the press-down dispatches, so no press-down listener stays on the document while idle (spec:AST-094 FR24; architecture:react-component-runtime INV13), and PR1 keeps its text. component:DropdownMenu FR9 and spec:AST-055 FR2 name the ref path DEC-11 leaves. The codemod walks a menu up only through DOM elements and fragments: a component element may hold its children back while closed, so such a menu gets a TODO.
@cixzhang

cixzhang commented Nov 4, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 93f24b8

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

Approved pending CI: CI has not run on this head yet, because the pull request is still a draft; marking it ready for review runs it.

The record changes (DEC-9, DEP-0019, CLN-0030, DEC-11 and the FR9 and AST-055 FR2 amendments) are approved for this head. The rebase moves DropdownMenu's touch model and the held-finger lit row into createMenuPress's shared core: the touched suites and the core type check pass, a mouse behaves as on main in every Core menu, and a finger slide in DropdownMenu lights the same row as on main.

Non-blocking:

  • useDropdownMenuPress (packages/core/src/hooks/useMenuPress.ts:298) has no production caller now that DropdownMenu uses useCoreMenuPress; move its test cases to useCoreMenuPress and delete it, or say why it stays.
  • architecture:public-component-api INV22 still lists useMenuPress among its known gaps; a follow-up record edit can drop it.

This pull request carries #9910's useAttachment change as its first commit.

[Automated review]

Comment thread packages/core/src/hooks/useMenuPress.ts Outdated
@@ -333,640 +298,107 @@ export function useMenuPress(options: UseMenuPressOptions): UseMenuPressReturn {
export function useDropdownMenuPress(

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: useDropdownMenuPress has no production caller now that DropdownMenu uses useCoreMenuPress; only useMenuPress.test.tsx calls it. Point those cases at useCoreMenuPress and delete it, or say why it stays.

@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: the deprecations in this pull request need an explicit owner decision before it can be approved.

The record changes (DEC-9, DEP-0019, CLN-0030, DEC-11 and the FR9 and AST-055 FR2 amendments) are approved for this head, CI passes, and Core's menus keep their mouse behavior; what is missing is an owner decision that names each deprecated symbol, which general spec guidance such as INV22, a Decider: line or the spec approval does not provide.

Blocking findings:

  • Newly live check: Callers of useMenuPress({menuRef, …}) start getting deprecation warnings, and the docs and release notes mark it deprecated and removed in 0.8.0 (packages/core/src/hooks/useMenuPress.ts:261); that needs an explicit owner decision for the useMenuPress options form. Found by: deprecation rule.
  • Newly live check: Code that imports UseMenuPressOptions starts getting deprecation warnings (packages/core/src/hooks/useMenuPress.ts:82); that needs an explicit owner decision for UseMenuPressOptions. Found by: deprecation rule.
  • Newly live check: Code that imports UseMenuPressReturn starts getting deprecation warnings (packages/core/src/hooks/useMenuPress.ts:138); that needs an explicit owner decision for UseMenuPressReturn. Found by: deprecation rule.
  • Newly live check: Custom DropdownMenu triggers are set to get deprecation warnings for DropdownMenuTriggerProps.onPointerDown in 0.8.0, which CLN-0030 records (docs/specs/components/DropdownMenu/modules/useMenuPress.spec.md:126); that needs an explicit owner decision for DropdownMenuTriggerProps.onPointerDown. Found by: deprecation rule.
  • Newly live check: The same CLN-0030 line schedules DropdownMenuTriggerProps.onContextMenu for deprecation in 0.8.0; that needs an explicit owner decision for DropdownMenuTriggerProps.onContextMenu. Found by: deprecation rule.

To resolve them, cite in the description an explicit owner decision that names each symbol, or take those deprecations out until one exists. Separately, useDropdownMenuPress (packages/core/src/hooks/useMenuPress.ts:298) no longer has a production caller and could move its test cases to useCoreMenuPress and go.

[Automated review]

Comment thread packages/core/src/hooks/useMenuPress.ts Outdated
/**
* The options form: props to spread on the menu root and the trigger. The ref
* form above listens on both itself, so nothing is spread.
* @deprecated Use the ref form, `useMenuPress(menuRef, options)`.

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: Callers of useMenuPress({menuRef, …}) start getting deprecation warnings, and the docs and release notes mark it deprecated and removed in 0.8.0; that needs an explicit owner decision for the useMenuPress options form, cited in the description. Found by: deprecation rule.

Comment thread packages/core/src/hooks/useMenuPress.ts Outdated
* the activation to a control inside it (a link) can tell the model's click
* from the browser's stray one.
* The options form's options: the menu and the trigger as refs.
* @deprecated Use the ref form, `useMenuPress(menuRef, options)`.

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: Code that imports UseMenuPressOptions starts getting deprecation warnings; that needs an explicit owner decision for UseMenuPressOptions. Found by: deprecation rule.

Comment thread packages/core/src/hooks/useMenuPress.ts Outdated

/**
* What the options form returns: props to spread on the menu and the trigger.
* @deprecated Use the ref form, `useMenuPress(menuRef, options)`.

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: Code that imports UseMenuPressReturn starts getting deprecation warnings; that needs an explicit owner decision for UseMenuPressReturn. Found by: deprecation rule.

| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| id | `CLN-0030` |
| lifecycle | `DEP-0019` |
| delta | `useMenuPress`'s options form, `UseMenuPressOptions` and `UseMenuPressReturn` are removed; `DropdownMenuTriggerProps`'s `onPointerDown` and `onContextMenu` are deprecated, as the press moves to the trigger's own listener |

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 row commits 0.8.0 to deprecating DropdownMenuTriggerProps.onPointerDown and onContextMenu, so custom-trigger callers are set to get deprecation warnings then; that needs an explicit owner decision for each of the two handlers. Found by: deprecation rule.

@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 of this pull request waits on an engineering owner's decision on its record changes at this head, plus one wording fix.

module:DropdownMenu/useMenuPress DEC-9 (the createMenuPress handle's update and event forwards, startBehavior, MenuPressDownEvent, and the rule that a finger's press-down a page handler stops or cancels starts no press in Core's menus) and component:DropdownMenu DEC-11, with the amended FR9 and spec:AST-055 FR2, changed since their last approval; an engineering owner records approval by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner.

Blocking finding:

  • DEC-9's first sentence (docs/specs/components/DropdownMenu/modules/useMenuPress.spec.md:337) says the options form delegates to the factory as INV22 asks, but useMenuPress(options) drives the factory's internal core and never calls createMenuPress (packages/core/src/hooks/useMenuPress.ts:315), so please reword it to say the options form stays as released and remains an INV22 known gap.

The earlier deprecation findings no longer apply now that nothing is deprecated, Core's menus keep their mouse behavior (the desktop parity tests pass here and on main), and CI has not run on this head yet because the pull request is a draft; two non-blocking suggestions are inline.

[Automated review]

**Reference:** `module:DropdownMenu/useMenuPress/DEC-9`
**Decider:** `cixzhang`, `2026-11-04`

The options form and Core's menus delegate to the factory, as

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.

useMenuPress(options) does not delegate to createMenuPress: it builds the internal createMenuPressCore and returns its handlers as props (packages/core/src/hooks/useMenuPress.ts:315-325), and useMenuPress.factory.test.tsx:548-555 asserts createMenuPress is never called. INV22 asks for delegation to the public factory with the same options, and its known-gap list still names useMenuPress. Suggested: "Core's menus run on the factory. The options form drives the factory's core through the props it returns and stays as released, so it remains an INV22 known gap."

* and the same Effect's cleanup releases it, so a detached, replaced or moved
* node releases the old document before the new one is held.
* With `holdsDocument` false it holds no element document, for a hook whose
* element another tracker already holds (useMenuPress's ref form, through

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 pull request no longer has a ref form of useMenuPress; the caller is Core's internal useCoreMenuPress. Suggested: "for a hook whose element another tracker already holds (Core's menus, through createMenuPress)."

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. Without React, createMenuPress(menu, options) takes these options with the trigger as an element (trigger), plus startBehavior ('items' starts a press only on an item), and returns {isTriggerClickFromPress, cancel, update, handleMenuPointerDown, handleTriggerPointerDown, handleTriggerContextMenu, destroy}. Its destroy removes every listener it added.",

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 sentence is the only consumer doc for createMenuPress, and it sits inside the cancel return row, so a reader looking up cancel gets the factory's whole API. createAnimator has its own createAnimator.doc.mjs; a createMenuPress doc entry would match it.

The options form, useMenuPress(options), keeps its released contract and is
not deprecated. It drives the factory's core through the props it returns, so
it remains an INV22 known gap. The public ref-form overload and its two types
go; Core's menus keep an internal ref form over the factory. The codemod, its
tests and its changeset go, with the DEP-0019 and CLN-0030 rows.
createMenuPress(menu, options) is unchanged, so the API report shows only it
and its types as new.

The internal useDropdownMenuPress had no production caller, so it goes too.
Its touch-model cases run on Core's ref form; the other cases stay on the
public options form.

@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 of this pull request waits on an engineering owner's decision on its record changes at this head; no other change is required.

module:DropdownMenu/useMenuPress DEC-9 (the createMenuPress handle's update and event forwards, startBehavior, MenuPressDownEvent, keeping useMenuPress(options) as released, and the rule that a finger's press-down a page handler stops or cancels starts no press in Core's menus) and component:DropdownMenu DEC-11, with the amended FR9 and spec:AST-055 FR2, changed since their last approval; an engineering owner records the decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner.

There are no blocking findings: the earlier DEC-9 wording now matches the code, Core's menus keep their mouse behavior (the desktop parity tests pass here and on main), and nothing is deprecated. CI's product jobs have not run on this head because the pull request is a draft, and two non-blocking suggestions are inline.

[Automated review]

* opening, highlighting, activating and dismissing
* @output createMenuPress, its option and handle types, the marker attributes,
* the press constants and the activation mark
* @position Core factory; useMenuPress is its React wrapper. The transition

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 (non-blocking, INV29): useMenuPress drives this module's core (createMenuPressCore) and never calls createMenuPress, which DEC-9 now says too. Suggested:

 * @position Core factory. useMenuPress drives its core; Core's menus wrap it
 *   through useCoreMenuPress. The transition table lives in menuPressGesture.ts.

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. Without React, createMenuPress(menu, options) takes these options with the trigger as an element (trigger), plus startBehavior ('items' starts a press only on an item), and returns {isTriggerClickFromPress, cancel, update, handleMenuPointerDown, handleTriggerPointerDown, handleTriggerContextMenu, destroy}. Its destroy removes every listener it added.",

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 factory's only consumer doc sits inside the hook's cancel row, so a reader looking up cancel gets the whole createMenuPress API. A createMenuPress.doc.mjs entry, as createAnimator.doc.mjs has, would give it its own page.

@vjeux

vjeux commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Closing: the maintainers decided not to add createMenuPress, and useMenuPress stays as shipped, so nothing remains here.

This branch was successfully deployed

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

Labels

ci:run A maintainer lets this outside contributor's pull request run CI (AST-030 FR17) 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.

2 participants