Skip to content
Draft
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2bcc49a
feat(ContextMenu): open from a held finger on iOS 26's timeline
vjeux Nov 7, 2026
812d3a4
test(ContextMenu): the hold's menu commits in its deciding task (red)
vjeux Nov 7, 2026
0776b40
fix(ContextMenu): the hold commits its menu in the task that decides it
vjeux Nov 7, 2026
e8f5ca1
test(ContextMenu): the menu mounts, closed, at the hold's commit (red)
vjeux Nov 7, 2026
0fbd309
fix(ContextMenu): the hold mounts the menu at its commit, closed
vjeux Nov 7, 2026
32e75c7
test(ContextMenu): a mouse press closes the menu in its own document
vjeux Nov 7, 2026
ba77ca8
Merge commit 'c8b4cba38181263ffa8fcc897975b61154cb91b0' into HEAD
vjeux Nov 7, 2026
df6fd04
docs(ContextMenu): PR1 names the outside-press watch and the touch query
vjeux Nov 7, 2026
abb70e1
test(ContextMenu): a starved page fails the 735 ms proof by name
vjeux Nov 7, 2026
b8a9055
Merge commit '62c7802e873ed3d020192e8218e5e1bcd4f8fad2' into HEAD
vjeux Nov 7, 2026
e66ff1d
Merge commit '92784f658939912fae623dd497cd35e499e13878' into HEAD
vjeux Nov 8, 2026
24c03c1
test(ContextMenu): a starved page's late timer fails by name in WebKit
vjeux Nov 8, 2026
339bf7e
docs(ContextMenu): PR1 names each presentation's query subscriptions
vjeux Nov 8, 2026
3d78dae
docs(ContextMenu): the record names the touch open's focus gap
vjeux Nov 8, 2026
20dc83f
Merge branch 'main' into uikit/c20-contextmenu-press
cixzhang Nov 8, 2026
a9188e6
Merge main
vjeux Nov 10, 2026
a5a0e12
test(ContextMenu): a finger that lifts before 400 ms is a plain tap
vjeux Nov 10, 2026
a6253f3
test(ContextMenu): a late drift past 11.5 px, a known Chromium gap
vjeux Nov 10, 2026
3c39a05
test(ContextMenu): a late drift past 11.5 px cancels in every engine
vjeux Nov 10, 2026
a4e0404
fix(ContextMenu): the menu waits for the finger's late moves
vjeux Nov 10, 2026
3b76a8c
test(ContextMenu): a finger inside the tolerance at the menu's moment
vjeux Nov 10, 2026
88cc0d8
fix(ContextMenu): the press reads each move's own time from the arena
vjeux Nov 10, 2026
46bc0da
test(ContextMenu): coalesced late samples and an on-time move
vjeux Nov 10, 2026
cad79ec
fix(ContextMenu): judge each sample at its time, wait only when late
vjeux Nov 10, 2026
889aded
test(ContextMenu): moves held behind a block decide before the menu
vjeux Nov 11, 2026
45bfb44
fix(ContextMenu): a late menu timer waits for moves behind the block
vjeux Nov 11, 2026
d743922
test(ContextMenu): the press's page time comes from the arena's clock
vjeux Nov 11, 2026
cae3644
fix(ContextMenu): the press reads the page's time from the arena
vjeux Nov 11, 2026
03eb7d7
refactor(ContextMenu): the touch layer imports its recognizer directly
vjeux Nov 11, 2026
f37d633
test(ContextMenu): a drift queued behind a block still ends the hold
vjeux Nov 11, 2026
8575ec7
docs(records): why the press keeps its own wait, and outsidePress types
vjeux Nov 11, 2026
22ae97e
docs(ContextMenu): name the measured table and the moved default
vjeux Nov 11, 2026
7c8cb86
Merge main
vjeux Nov 11, 2026
56b2f62
test(ContextMenu): a finger in a second document on its own clock
vjeux Nov 11, 2026
650a125
Merge main
vjeux Nov 11, 2026
fac565a
docs(ContextMenu): the record owns the touch hold's two modules
vjeux Nov 11, 2026
69e6dd8
docs(ContextMenu): the hold's comments match what this code does
vjeux Nov 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/contextmenu-outside-press-watch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[fix] ContextMenu closes on an outside press through the layer's outside-press watch. A mouse or pen press closes it at its `mousedown`, in the menu's own document, so a menu in an iframe or a Picture-in-Picture window no longer closes on a press in the page.

@vjeux
7 changes: 7 additions & 0 deletions .changeset/contextmenu-touch-timeline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@astryxdesign/core': patch
---

[feat] ContextMenu opens from a held finger on iOS 26's measured timeline where the primary pointer is coarse. The hold keeps UIKit's measured moments. At 400 ms the press takes the finger: its lift no longer sends a click, and the page cannot scroll under it. The menu opens at 735 ms if the finger is still down, or as soon as it lifts after 400 ms. A finger that lifts before 400 ms, or drifts more than 11.5 px on either axis before the menu opens, gets no menu. The page stays free to scroll while the menu is up: a touch outside the menu closes it as it begins to pan, or at its lift. A touch open lights no row: the menu takes the focus, and a row lights only under the finger, as in UIKit (keyboard and pointer opens still focus the first item). A second finger cancels the press before 400 ms; after it, the menu still opens. A fine pointer (a mouse) keeps the right-click menu, and a finger there keeps today's 500 ms long press. Pass `presentation="popover"` to keep today's menu: the menu at the pointer, opened by a finger held for 500 ms.

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 (a miss in earlier rounds): in UIKit the opening finger slides onto a row. Here it cannot until #8133, which is component:ContextMenu FR5's recorded gap, so only a new finger lights a row. Suggest: "a row lights only under a finger pressed on it".


@vjeux
69 changes: 69 additions & 0 deletions apps/storybook/stories/ContextMenu.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -647,3 +647,72 @@ export const BottomSheetSubmenu: Story = {
}
},
};

const touchStyles = stylex.create({
card: {
width: '200px',
height: '120px',
display: 'grid',
placeItems: 'center',
borderRadius: '12px',
backgroundColor: '#e5e7eb',
color: '#374151',
userSelect: 'none',
},
});

export const TouchLongPress: Story = {
name: 'Touch / long press',
parameters: {
docs: {
description: {
story:
'Where the primary pointer is coarse (a touch screen), hold a finger on the card. As on iOS, a haptic plays at 400 ms where the device can play one, and the menu opens at 735 ms, or as soon as the finger lifts after 400 ms. A finger that lifts before 400 ms, or drifts more than 11.5 px on either axis before the menu opens, gets no menu. While the menu is up, the page still scrolls: a touch outside the menu closes it as it begins to pan.',
},
},
},
render: () => (
<ContextMenu
data-testid="touch-context-menu"
label="Photo actions"
items={[
{label: 'Copy', icon: ClipboardIcon, onClick: () => {}},
{label: 'Share', icon: ShareIcon, onClick: () => {}},
{label: 'Save to Files', icon: ArrowDownTrayIcon, onClick: () => {}},
{type: 'divider'},
{
label: 'Delete',
icon: TrashIcon,
variant: 'destructive',
onClick: () => {},
},
]}>
<div {...stylex.props(touchStyles.card)}>Hold for actions</div>
</ContextMenu>
),
};

export const TouchLongPressPopover: Story = {
name: 'Touch / long press, presentation="popover"',
parameters: {
docs: {
description: {
story:
'With `presentation="popover"`, ContextMenu keeps today’s menu on every device: a finger held for 500 ms opens it at the finger, with no growth and no iOS timeline. A finger on a device whose primary pointer is fine (a mouse or a trackpad) gets the same long press.',
},
},
},
render: () => (
<ContextMenu
data-testid="touch-context-menu"
label="Photo actions"
presentation="popover"
items={[
{label: 'Copy', icon: ClipboardIcon, onClick: () => {}},
{label: 'Share', icon: ShareIcon, onClick: () => {}},
{label: 'Save to Files', icon: ArrowDownTrayIcon, onClick: () => {}},
]}>
<div {...stylex.props(touchStyles.card)}>Hold for actions</div>
</ContextMenu>
),
};
117 changes: 63 additions & 54 deletions docs/specs/components/ContextMenu/ContextMenu.spec.md

Large diffs are not rendered by default.

9 changes: 4 additions & 5 deletions packages/core/src/ContextMenu/ContextMenu.doc.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,7 @@ export const docs = {
name: 'presentation',
type: "'popover' | 'bottom-sheet' | 'adaptive'",
description:
'Presentation policy. `popover` opens at the pointer position, `bottom-sheet` always uses an action sheet, and `adaptive` uses the BottomSheet on a compact touch screen: a coarse primary pointer, and a viewport narrower than the theme `md` breakpoint (768px by default) or shorter than 500px.',
default: "'popover'",
'Presentation policy. Omitted, the menu opens at the pointer position, and under a finger a held finger opens it on iOS timing (the touch model). `popover` opens at the pointer position on every device, and a held finger opens it at 500 ms (the long press), as before; so does a finger where the primary pointer is fine. `bottom-sheet` always uses an action sheet, and `adaptive` uses the BottomSheet on a compact touch screen: a coarse primary pointer, and a viewport narrower than the theme `md` breakpoint (768px by default) or shorter than 500px.',
},
{
name: 'isDisabled',
Expand All @@ -160,7 +159,7 @@ export const docs = {
usage: {
anatomy,
description:
'A right-click context menu that appears at the cursor position. Use to provide contextual actions for specific elements or regions without cluttering the UI with visible buttons.',
'A right-click context menu that appears at the cursor position. Use to provide contextual actions for specific elements or regions without cluttering the UI with visible buttons. Under a finger, where the primary pointer is coarse, a held finger opens it with iOS timing: the menu opens at 735 ms, or as soon as the finger lifts after 400 ms.',
bestPractices: [
{
guidance: true,
Expand Down Expand Up @@ -224,7 +223,7 @@ export const docs = {
export const docsZh = {
usage: {
description:
'右键点击时在光标位置出现的上下文菜单。用于为特定元素或区域提供上下文操作,而不使 UI 杂乱。',
'右键点击时在光标位置出现的上下文菜单。用于为特定元素或区域提供上下文操作,而不使 UI 杂乱。在主指针为粗指针(触摸屏)时,按住手指即可打开,时序与 iOS 相同:735 毫秒时菜单打开;若手指在 400 毫秒后抬起,菜单在抬起时立即打开。',
bestPractices: [
{guidance: true, description: '保持菜单项简洁和面向操作。'},
{guidance: true, description: '有很多项时使用分组和分隔线。'},
Expand Down Expand Up @@ -260,7 +259,7 @@ export const docsDense = {
usage: {
anatomy,
description:
'A right-click context menu that appears at the cursor position. Use to provide contextual actions for specific elements or regions.',
'A right-click context menu that appears at the cursor position. Use to provide contextual actions for specific elements or regions. Under a finger (a coarse primary pointer): a held finger opens it at 735 ms, or on a lift after 400 ms.',
bestPractices: [
{guidance: true, description: 'Keep items concise and action-oriented.'},
{
Expand Down
Loading