Skip to content

fix(core): gestures read another window's nodes and touches - #10224

Merged
cixzhang merged 3 commits into
mainfrom
reland/gestures-another-window
Nov 6, 2026
Merged

cixzhang merged 3 commits into
mainfrom
reland/gestures-another-window

Conversation

@cixzhang

@cixzhang cixzhang commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Supersedes #10211 (co-authored by vjeux)

The change of #10211, re-landed whole on current main from a maintainer branch with the author as co-author. Its halves depend on each other: the gesture code, and the nodeType entry it adds to the interaction lanes' scope pattern in .github/workflows/ci.yml.

Every file #10211 changes is byte-identical to its head d78dd80d, merged with main with no conflict. That head:

  • merges main, and the gesture host's input lock keeps main's signature (isElement(recognizer.view) && managed(recognizer.view, recognizer));
  • keeps main's useFingerScroll onEnd and takes isDocument from utils/nodeType;
  • fixes an element that the page creates and then moves into an iframe or a Document Picture-in-Picture window: it keeps its gestures, where checking against the element's own window's classes failed.

A follow-up replaces the WebKit lane's hand-kept scope pattern with one derived from where the interaction specs live; this pattern entry goes away with it.


User impact

A component rendered into another window's document gets no gestures. That is a same-origin iframe or a Document Picture-in-Picture window the page's code renders or moves elements into. There its pans, taps, long presses and swipes never begin, under any pointer. Its finger scrolls read as the program's, and a held layer holds no touch. A press in a scroll box there gets the wrong delay, and on iOS a hold can start a selection. A page whose iframe loads its own Astryx is not affected.

Problem and solution fit

  • Why it fails: the host runs in the page's window but listens in the other document. Its instanceof checks test the page's classes, so it dropped that window's touches.
  • Why that harms the task: the component shows, but touch does nothing.
  • Why that matters: a sheet there cannot be dragged closed.

Expected behavior and authority

  • family:overlay-dismissal FR8: trigger-anchored members render in the page, a same-origin iframe or a Document Picture-in-Picture window.
  • spec:AST-094 FR43: gestures attach for every pointer, with no limit to the page's own document.

Smallest restoration

  • utils/nodeType.ts adds node-type guards. Its isHTMLElement reads the namespace, so an element moved between windows' documents passes.
  • The gesture path's realm-bound checks use them. That covers the host, the modules on its touch path, the scroll-view walk, the selection lock and the layer input hold. Same-document nodes pass as before.
  • The outside-press, focus-trap and keyboard modules import these guards instead of keeping copies.
  • Known limitation: press feedback still listens only in the page's document, so no pressed state shows in another window.

Evidence

  • Reproduction before the change: a pan on an element in a same-origin iframe never begins, in Chromium and WebKit (not-for-landing a4eb18614b).
  • Result after the change: the pan begins and ends there, in both engines.
  • Representative unchanged path: the gestures browser spec and the unit suite.
  • Regression test or other mutation-sensitive proof: on a4eb18614b, eight jsdom cases fail. They cover an iframe's pan, finger scroll and shadow-root finger, its scroll box and fixed bar, another window's style lock, and a switch's label. The iframe group-press click test passed on main only because no press reached the host. Two moved-element cases, a pan's touch-action and a switch's label, failed on this change's earlier head.

Scope

  • One defect is restored.
  • Separable contradictory or unsettled tagalongs were removed or split.
  • No new public API, visual representation, default, or interaction model is hidden under the bug-fix label.
  • Public text and artifacts contain no internal Meta context.

Testing

At this head, which merges main 62f5ebc5e4. Everything below ran on this code.

  • pnpm test:ui: 21,569 passed in 867 files. pnpm test:node: 16,832 passed in 928 files.
  • pnpm -F @astryxdesign/core typecheck: 0 errors. CI=true pnpm lint: 0 errors, and no warning in a changed file. Prettier and review:pre: clean.
  • gestures.interaction.browser.spec.ts and presentedHitTesting.interaction.browser.spec.ts: Chromium 78 passed and 4 skipped; WebKit 70 passed and 12 skipped by design. That includes the trackpad case of fix(gestures): a pan on its own scroller takes a trackpad's scroll, as a finger's pan (spec:AST-094 FR43) #10212.
  • The moved-element browser case fails 3 of 3 in Chromium and in WebKit with the previous isHTMLElement (touch-action auto, not pinch-zoom). It passes 3 of 3 in both here. The two moved-element unit cases fail and pass the same way.
  • The iframe pan case fails 3 of 3 on a4eb18614b in Chromium and in WebKit, and passes here.
  • The gesture components' interaction specs: Chromium 184 passed and 5 skipped. WebKit: 154 passed and 35 skipped by design. They cover Switch, SegmentedControl, Slider, the input hold, BottomSheet, Dialog, DropdownMenu, ContextMenu, the wheel, List rows and the pager.
  • PopoverSecondDocument and DialogSecondDocument, in an iframe and a Picture-in-Picture window: Chromium 33 passed.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
@vercel

vercel Bot commented Nov 6, 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 6, 2026 7:22am UTC

Request Review

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

astracat-bot Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 6, 2026, 7:25 AM UTC

@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 9680c76

@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · f92cf87

Storybook · Sandbox · built from this PR's head

Modified components

Component Size LoC Visual · A11y · RTL
LayerProvider 4.1 kB (0) · 20 B CSS (0) — ✓ · ✓ · ✗
lab/Schedule 98.8 kB (+109 B) · 3.9 kB CSS (0) — — · — · —
Markdown 161.4 kB (+107 B) · 19.5 kB CSS (0) — — · — · —
138 more components
Component Size LoC Visual · A11y · RTL
Toast 89.0 kB (+100 B) · 9.6 kB CSS (0) — — · — · —
ContextMenu 90.2 kB (+99 B) · 16.5 kB CSS (0) — — · — · —
MoreMenu 86.1 kB (+98 B) · 16.6 kB CSS (0) — — · — · —
charts/ChartTooltip 151.2 kB (+95 B) · 423 B CSS (0) — — · — · —
lab/ChatReactionBar 75.2 kB (+89 B) · 1.1 kB CSS (0) — — · — · —
ContextMenuItem 26.7 kB (+88 B) · 6.1 kB CSS (0) — — · — · —
Table 99.6 kB (+87 B) · 17.2 kB CSS (0) — — · — · —
DropdownMenu 85.8 kB (+85 B) · 16.6 kB CSS (0) — — · — · —
Carousel 58.4 kB (+84 B) · 10.1 kB CSS (0) — — · — · —
richtext/RichTextEditorToolbar 239.6 kB (+84 B) · 454 B CSS (0) — — · — · —
TableCell 91.4 kB (+82 B) · 16.9 kB CSS (0) — — · — · —
TableHeaderCell 91.0 kB (+82 B) · 16.7 kB CSS (0) — — · — · —
SideNav 81.6 kB (+81 B) · 10.7 kB CSS (0) — — · — · —
MobileNav 61.5 kB (+80 B) · 9.9 kB CSS (0) — — · — · —
lab/Comments 281.7 kB (+80 B) · 2.5 kB CSS (0) — — · — · —
lab/ChatEmojiPicker 74.6 kB (+76 B) · 773 B CSS (0) — — · — · —
lab/TransferList 85.4 kB (+73 B) · 1.7 kB CSS (0) — — · — · —
charts/Chart 173.8 kB (+71 B) · 494 B CSS (0) — — · — · —
MultiSelector 88.2 kB (+70 B) · 16.2 kB CSS (0) — — · — · —
TimeInput 144.0 kB (+70 B) · 15.1 kB CSS (0) — — · — · —
Pagination 98.7 kB (+68 B) · 16.9 kB CSS (0) — — · — · —
lab/CommentThread 275.9 kB (+67 B) · 2.4 kB CSS (0) — — · — · —
lab/NavigationScreen 75.1 kB (+66 B) · 1.1 kB CSS (0) — — · — · —
lab/NavigationStack 75.1 kB (+66 B) · 1.1 kB CSS (0) — — · — · —
Switch 94.8 kB (+65 B) · 9.5 kB CSS (0) — — · — · —
AlertDialog 95.6 kB (+64 B) · 13.7 kB CSS (0) — — · — · —
Selector 88.7 kB (+64 B) · 16.5 kB CSS (0) — — · — · —
lab/ListInput 91.0 kB (+64 B) · 1.6 kB CSS (0) — — · — · —
lab/TourStep 66.9 kB (+64 B) · 590 B CSS (0) — — · — · —
Dialog 22.5 kB (+63 B) · 6.7 kB CSS (0) — — · — · —
AppShell 81.5 kB (+61 B) · 12.8 kB CSS (0) — — · — · —
DateRangeInput 81.3 kB (+61 B) · 11.8 kB CSS (0) — — · — · —
SelectableCard 16.9 kB (+57 B) · 7.3 kB CSS (0) — — · — · —
Slider 97.0 kB (+57 B) · 8.3 kB CSS (0) — — · — · —
DateInput 167.1 kB (+56 B) · 16.1 kB CSS (0) — — · — · —
TopNav 63.1 kB (+56 B) · 10.2 kB CSS (0) — — · — · —
BottomSheet 57.0 kB (+55 B) · 7.5 kB CSS (0) — — · — · —
SideNavHeading 66.6 kB (+55 B) · 10.6 kB CSS (0) — — · — · —
lab/TransferListSelector 92.4 kB (+54 B) · 1.9 kB CSS (0) — — · — · —
CommandPalette 59.3 kB (+53 B) · 12.4 kB CSS (0) — — · — · —
TopNavHeading 66.0 kB (+52 B) · 10.4 kB CSS (0) — — · — · —
SegmentedControl 89.0 kB (+51 B) · 6.8 kB CSS (0) — — · — · —
lab/CommentComposer 115.4 kB (+51 B) · 1.9 kB CSS (0) — — · — · —
ClickableCard 16.7 kB (+50 B) · 7.1 kB CSS (0) — — · — · —
DateTimeInput 193.4 kB (+50 B) · 18.0 kB CSS (0) — — · — · —
TabMenu 63.0 kB (+48 B) · 10.4 kB CSS (0) — — · — · —
CheckboxListItem 66.1 kB (+47 B) · 10.9 kB CSS (0) — — · — · —
Drawer 44.5 kB (+47 B) · 8.2 kB CSS (0) — — · — · —
TopNavMegaMenuItem 12.6 kB (+47 B) · 4.2 kB CSS (0) — — · — · —
BreadcrumbItem 75.7 kB (+45 B) · 11.5 kB CSS (0) — — · — · —
Collapsible 25.3 kB (+43 B) · 4.5 kB CSS (0) — — · — · —
Popover 60.8 kB (+43 B) · 13.4 kB CSS (0) — — · — · —
TopNavMenu 65.8 kB (+43 B) · 10.5 kB CSS (0) — — · — · —
CommandPaletteItem 11.9 kB (+42 B) · 3.6 kB CSS (0) — — · — · —
PowerSearch 264.1 kB (+40 B) · 22.9 kB CSS (0) — — · — · —
TopNavItem 12.6 kB (+40 B) · 4.0 kB CSS (0) — — · — · —
Item 16.1 kB (+39 B) · 5.3 kB CSS (0) — — · — · —
SelectorOption 19.5 kB (+39 B) · 5.8 kB CSS (0) — — · — · —
Step 29.8 kB (+39 B) · 5.6 kB CSS (0) — — · — · —
Outline 23.7 kB (+38 B) · 4.1 kB CSS (0) — — · — · —
ButtonGroup 39.9 kB (+37 B) · 3.7 kB CSS (0) — — · — · —
SegmentedControlItem 16.0 kB (+37 B) · 4.3 kB CSS (0) — — · — · —
TreeList 41.5 kB (+36 B) · 5.4 kB CSS (0) — — · — · —
DropdownMenuItem 20.5 kB (+35 B) · 6.0 kB CSS (0) — — · — · —
ListItem 65.2 kB (+35 B) · 10.9 kB CSS (0) — — · — · —
DropdownMenuRadioItem 21.3 kB (+34 B) · 6.4 kB CSS (0) — — · — · —
RadioListItem 62.9 kB (+34 B) · 9.7 kB CSS (0) — — · — · —
BottomSheetSwitcher 17.5 kB (+33 B) · 3.4 kB CSS (0) — — · — · —
Tab 13.3 kB (+33 B) · 4.2 kB CSS (0) — — · — · —
Token 27.4 kB (+33 B) · 4.7 kB CSS (0) — — · — · —
TopNavMegaMenu 60.9 kB (+33 B) · 11.2 kB CSS (0) — — · — · —
Lightbox 59.7 kB (+32 B) · 9.7 kB CSS (0) — — · — · —
NumberInput 62.8 kB (+32 B) · 10.7 kB CSS (0) — — · — · —
ComplexSelector 62.4 kB (+31 B) · 11.0 kB CSS (0) — — · — · —
DropdownMenuCheckboxItem 21.3 kB (+27 B) · 6.4 kB CSS (0) — — · — · —
SideNavItem 61.6 kB (+25 B) · 10.4 kB CSS (0) — — · — · —
lab/SelectableCardGroup 56.1 kB (+23 B) · 162 B CSS (0) — — · — · —
lab/EmojiPicker 77.6 kB (+22 B) · 751 B CSS (0) — — · — · —
Tokenizer 80.3 kB (+21 B) · 17.8 kB CSS (0) — — · — · —
FileInput 60.2 kB (+18 B) · 10.3 kB CSS (0) — — · — · —
TextInput 59.4 kB (+15 B) · 10.5 kB CSS (0) — — · — · —
FieldLabel 42.9 kB (+14 B) · 5.2 kB CSS (0) — — · — · —
richtext/RichTextEditor 290.6 kB (+13 B) · 2.6 kB CSS (0) — — · — · —
CheckboxInput 59.9 kB (+11 B) · 9.0 kB CSS (0) — — · — · —
Banner 53.5 kB (+10 B) · 9.8 kB CSS (0) — — · — · —
HoverCard 24.3 kB (+10 B) · 4.3 kB CSS (0) — — · — · —
ChatComposerInput 66.2 kB (+7 B) · 10.6 kB CSS (0) — — · — · —
ChatLayout 54.9 kB (+7 B) · 9.8 kB CSS (0) — — · — · —
Link 48.5 kB (+7 B) · 6.9 kB CSS (0) — — · — · —
Tooltip 25.8 kB (+7 B) · 4.3 kB CSS (0) — — · — · —
ChatComposerTokenElement 66.2 kB (+6 B) · 10.6 kB CSS (0) — — · — · —
richtext/RichTextView 270.9 kB (+5 B) · 2.3 kB CSS (0) — — · — · —
DrawerHeader 54.6 kB (+3 B) · 12.2 kB CSS (0) — — · — · —
DropdownMenuSubMenu 64.5 kB (+3 B) · 9.4 kB CSS (0) — — · — · —
IconButton 51.6 kB (+2 B) · 9.3 kB CSS (0) — — · — · —
AvatarGroupOverflow 44.4 kB (+1 B) · 5.5 kB CSS (0) — — · — · —
ChatComposer 68.2 kB (+1 B) · 11.2 kB CSS (0) — — · — · —
ChatSendButton 52.0 kB (+1 B) · 9.3 kB CSS (0) — — · — · —
ProgressBar 11.5 kB (+1 B) · 6.2 kB CSS (0) — — · — · —
Spinner 20.3 kB (+1 B) · 6.1 kB CSS (0) — — · — · —
TableSelectionToolbar 61.1 kB (+1 B) · 12.4 kB CSS (0) — — · — · —
Thumbnail 55.9 kB (+1 B) · 10.9 kB CSS (0) — — · — · —
Timestamp 23.0 kB (+1 B) · 9.6 kB CSS (0) — — · — · —
charts/ChartLegend 35.6 kB (0) · 224 B CSS (0) — — · — · —
ChatToolCalls 27.7 kB (0) · 8.3 kB CSS (0) — — · — · —
CommandPaletteInput 27.9 kB (0) · 7.5 kB CSS (0) — — · — · —
Heading 8.0 kB (0) · 5.3 kB CSS (0) — — · — · —
NavHeadingMenu 15.5 kB (0) · 3.5 kB CSS (0) — — · — · —
Text 7.5 kB (0) · 5.3 kB CSS (0) — — · — · —
Timer 9.6 kB (0) · 5.9 kB CSS (0) — — · — · —
ToggleButton 52.4 kB (0) · 9.4 kB CSS (0) — — · — · —
lab/DiffViewer 77.9 kB (0) · 2.1 kB CSS (0) — — · — · —
Avatar 39.4 kB (−1 B) · 5.5 kB CSS (0) — — · — · —
ChatMessageList 23.8 kB (−1 B) · 6.9 kB CSS (0) — — · — · —
CodeBlock 66.1 kB (−1 B) · 10.6 kB CSS (0) — — · — · —
InfoTip 43.0 kB (−2 B) · 5.3 kB CSS (0) — — · — · —
Toolbar 39.4 kB (−2 B) · 7.9 kB CSS (0) — — · — · —
Calendar 62.4 kB (−3 B) · 9.9 kB CSS (0) — — · — · —
TabList 40.1 kB (−3 B) · 6.4 kB CSS (0) — — · — · —
Typeahead 69.9 kB (−3 B) · 11.8 kB CSS (0) — — · — · —
ChatDictationButton 52.1 kB (−4 B) · 9.4 kB CSS (0) — — · — · —
SideNavCollapseButton 52.4 kB (−4 B) · 9.4 kB CSS (0) — — · — · —
lab/CommentPin 52.6 kB (−4 B) · 1.2 kB CSS (0) — — · — · —
DialogHeader 54.4 kB (−5 B) · 10.8 kB CSS (0) — — · — · —
Field 46.7 kB (−5 B) · 6.7 kB CSS (0) — — · — · —
ChatLayoutScrollButton 52.0 kB (−8 B) · 9.5 kB CSS (0) — — · — · —
MobileNavToggle 51.9 kB (−8 B) · 9.3 kB CSS (0) — — · — · —
RadioList 48.2 kB (−8 B) · 6.8 kB CSS (0) — — · — · —
StatusDot 27.5 kB (−9 B) · 5.3 kB CSS (0) — — · — · —
Stepper 53.3 kB (−9 B) · 9.5 kB CSS (0) — — · — · —
AvatarGroup 42.1 kB (−10 B) · 5.6 kB CSS (0) — — · — · —
TextArea 53.8 kB (−10 B) · 8.9 kB CSS (0) — — · — · —
CheckboxList 50.0 kB (−12 B) · 7.5 kB CSS (0) — — · — · —
Button 51.5 kB (−15 B) · 9.3 kB CSS (0) — — · — · —
InputGroup 48.5 kB (−16 B) · 7.3 kB CSS (0) — — · — · —
BaseTypeahead 61.9 kB (−17 B) · 10.3 kB CSS (0) — — · — · —
layer — — — · ✓ · —
Layer — — ✓ · ✓ · —

Package size: @astryxdesign/core 6.70 MB packed (+959 B); no new dependencies.

API changes: none in this pull request

Committed API report behind this build: cli 19 rows, core 21 rows; run pnpm api:report.

Touched components checked; add ci:run-visual, ci:run-a11y or ci:run-rtl to run the full suites.

CI impact: +1.6 job-min per update · pool +1.4% · time to green −8.8 min · standard runners
Before After Change
Job-minutes per PR update 116.8 118.4 +1.6
Runner pool, runner-minutes a day 53,692 54,424 +732 (+1.4%)
Average concurrent jobs 37.3 37.8 +0.5
Time to green (median → this run) 28.3 min 19.5 min −8.8 min
Runs a day 460 460 0

Changed jobs: pr-interaction-webkit changed: standard

Assumptions: before is each job's average over the last 20 pull-request runs; after keeps that for unchanged jobs and uses this run's minutes for added or changed ones, weighted by how often each changed job ran in those runs; runs a day averages the seven whole days before today; time to green compares this run with the median of those runs that ran the same jobs.


Generated by PR Enrichment workflow | View full report

@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. Gestures now begin for a component portalled into a same-origin iframe or a Picture-in-Picture window, and the new iframe tests fail on main and pass here, but two things need fixing:

  • isHTMLElement (packages/core/src/utils/nodeType.ts:44-52) checks an element against its document's window, so an element the page creates and then moves into an iframe or a Picture-in-Picture window fails it, and in Chromium such an element loses the touch-action it had on main (pinch-zoom there, auto here, against spec:AST-094 FR18) while its switch label gains a second hidden switch; test namespaceURI === 'http://www.w3.org/1999/xhtml' instead and add a moved-element case.
  • isNodeTarget (packages/core/src/keyboard/keyboardHeight.ts:100) repeats the new isNode, and GestureHost.ts keeps its own instanceof view.HTMLElement and instanceof view.Node checks (lines 1286, 2096 and 2720), so a later fix to the shared guards misses them; have both modules use utils/nodeType.ts.

A non-blocking wording suggestion for the styleOf comment is inline. Required CI has not run on this head yet.

[Automated review]

Comment on lines +44 to +52
export function isHTMLElement(value: unknown): value is HTMLElement {
if (!isElement(value)) {
return false;
}
const ownClass =
value.ownerDocument.defaultView?.HTMLElement ??
(typeof HTMLElement === 'undefined' ? undefined : HTMLElement);
return ownClass != null && value instanceof ownClass;
}

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.

An element the page creates and then appends into an iframe or a Picture-in-Picture window keeps the page's prototype, so value instanceof value.ownerDocument.defaultView.HTMLElement is false for it, where main's instanceof HTMLElement was true. In Chromium a pan recognizer on such an element gets touch-action: pinch-zoom on main and auto here (GestureHost.ts:1429 skips applyStyle, against spec:AST-094 FR18), and attachTapHaptic adds a second hidden switch to its label.

The HTML namespace holds in every window:

export function isHTMLElement(value: unknown): value is HTMLElement {
  return isElement(value) && value.namespaceURI === 'http://www.w3.org/1999/xhtml';
}

A moved-element case beside the iframe ones would keep it fixed.

*/

/** Whether `value` is a DOM node, from this window or another one. */
export function isNode(value: unknown): value is 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.

isNodeTarget in keyboard/keyboardHeight.ts:100 is this same check, and GestureHost.ts keeps its own realm checks at lines 1286 (instanceof view.HTMLElement), 2096 and 2720 (instanceof view.Node). Import isNode and isHTMLElement from here in both modules and delete isNodeTarget, so a fix to these guards reaches every caller.

Comment on lines +709 to +710
* An element's inline style, when it has one (HTML and SVG elements). It is
* checked as an object, so another window's style passes too.

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: "It is checked as an object" restates the code in the passive voice. Keep only the reason, for example: "Another window's style fails instanceof Object in this window."

Takes #10211 at d78dd80, which fixes the moved-element case, merged with main.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec 4d00e70

@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: required checks are still running on this head.

Gestures now begin for a component portalled into a same-origin iframe or a Document Picture-in-Picture window, and the new iframe tests fail on main and pass here. Both earlier findings are fixed: isHTMLElement reads the namespace, so an element the page moves into another window keeps its touch-action and switch label, and keyboardHeight.ts and GestureHost.ts now use the shared guards.

The earlier non-blocking wording suggestion on the styleOf comment in selectionLock.ts still applies.

[Automated review]

@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

/approve-spec f92cf87

@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: required checks have not finished on this head yet.

Gestures now begin for a component portalled into a same-origin iframe or a Document Picture-in-Picture window, and the new iframe tests fail on main and pass here. The merge from main leaves the patch unchanged, and both earlier findings stay fixed.

The earlier non-blocking wording suggestion on the styleOf comment in selectionLock.ts still applies.

[Automated review]

@cixzhang
cixzhang merged commit 3060510 into main Nov 6, 2026
102 of 105 checks passed
cixzhang added a commit to vjeux/astryx that referenced this pull request Nov 6, 2026
cixzhang added a commit to vjeux/astryx that referenced this pull request Nov 6, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 6, 2026
e69dd39 merged onto main 3060510. One conflict: the WebKit lane's
scope line in ci.yml, which main's meta#10224 also edits; resolved as
main's line plus meta#10094's token-references path.
@cixzhang
cixzhang deleted the reland/gestures-another-window branch November 6, 2026 10:24
cixzhang added a commit that referenced this pull request Nov 6, 2026
…s close (#8186)

* chore(core): the layer runtime's exit copy, for a touch presentation's close

placeLayerExitCopy puts a static, inert copy of a closing layer where the
layer was drawn, in the top layer, for a touch presentation's close to play
on: the browser hides a popover the moment it closes. The copy keeps the
layer's box, transform, presented look, overflow and scroll positions; has
no ids, anchor names, form names or form owner; is inert and hidden from assistive
technology; and its descendants' keyframe animations and transitions do not
start again. A refused show during the closing toggle waits, hidden, until
the toggle is over. A layer inside other layers gets its copy after the
outermost one, with what it inherited there (its color scheme included),
so a close that takes the layers around it too still draws the copy. That
copy is shown again above the copy the same close then places for a layer
around it. A layer has one copy. The layer-runtime record states
it, and that the presentation plays its exit on the copy (AST-094 FR7).

Core's shared inertCopy now takes every copied native control and
form-associated custom element out of any form with form="", as its
documentation says, so the swap's copies join no form either. It also
sets anchor-name: none on each copied element that has an anchor name, so
no live anchor gets a second element.

* Merge branch 'main' into uikit/c26-layer-exit-copy

* Merge branch 'main' into uikit/c26-layer-exit-copy

* fix(core): the exit copy keeps no closed layer

The exit copy found the inner layers to raise through a module-level
set of every copied layer. A layer React removed on close stayed
reachable there until a later close showed a copy. The inner layers
are inside the closing layer, so they are now found there, and
nothing keeps a layer once it is gone.

Chromium 150 and later refuse a show during another popover's toggle,
so the copy can open in the next frame. The browser spec's
light-dismiss case now reads the copy after a frame, as its
nested-close cases do. The cap check's comment now names only the
copy cap.

* docs(core): the exit copy's JSDoc states the empty-canvas rule once

The empty-canvas rule stays in the first paragraph, and the inert-copy
paragraph no longer repeats it or `inertCopy`'s own contract.

* Merge main into the layer exit copy branch (layer-runtime.md verification table keeps both rows)

* Merge main into the layer exit copy branch (#8147 landed)

* Merge main into the layer exit copy branch (placeOver's beforeNode beside keepInherited)

* Merge main into the layer exit copy branch (#10224 landed)

* fix(core): the exit copy reads its host's style before the insertion

keepInherited read the outer layer's parent's computed style right
after the copy was inserted there: a style read after a write. It
reads only that host and writes only on the still-detached copy, so it
now runs before the insertion. What the copy inherits is unchanged.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
cixzhang added a commit that referenced this pull request Nov 6, 2026
…s back into it (#8187)

* chore(core): the layer runtime's exit copy, for a touch presentation's close

placeLayerExitCopy puts a static, inert copy of a closing layer where the
layer was drawn, in the top layer, for a touch presentation's close to play
on: the browser hides a popover the moment it closes. The copy keeps the
layer's box, transform, presented look, overflow and scroll positions; has
no ids, anchor names, form names or form owner; is inert and hidden from assistive
technology; and its descendants' keyframe animations and transitions do not
start again. A refused show during the closing toggle waits, hidden, until
the toggle is over. A layer inside other layers gets its copy after the
outermost one, with what it inherited there (its color scheme included),
so a close that takes the layers around it too still draws the copy. That
copy is shown again above the copy the same close then places for a layer
around it. A layer has one copy. The layer-runtime record states
it, and that the presentation plays its exit on the copy (AST-094 FR7).

Core's shared inertCopy now takes every copied native control and
form-associated custom element out of any form with form="", as its
documentation says, so the swap's copies join no form either. It also
sets anchor-name: none on each copied element that has an anchor name, so
no live anchor gets a second element.

* Merge branch 'main' into uikit/c26-layer-exit-copy

* feat(Popover): under a finger, it grows out of its trigger and shrinks back into it

Under a coarse primary pointer, Popover grows out of its trigger on the
theme duration step nearest iOS's growth (`--duration-medium-min`) and the
theme's entrance easing, and shrinks back into it on the theme's exit
duration and easing, on the layer runtime's exit copy. A fine primary
pointer keeps the layer's own entrance.

* Merge branch 'main' into uikit/c26-layer-exit-copy

* Merge the updated lower stack branch into uikit/c26-popover-present (own diff unchanged)

* feat(Popover): a touch during the growth is ignored

While the popover grows, the layer runtime's input hold takes every touch,
as UIKit ignores touches while it presents a popover. A tap outside does not
dismiss it and reaches nothing under it, and a pinch still zooms the page.
This folds in the hold that #8223 carried, so the growth and its hold land
together.

* fix(core): the exit copy keeps no closed layer

The exit copy found the inner layers to raise through a module-level
set of every copied layer. A layer React removed on close stayed
reachable there until a later close showed a copy. The inner layers
are inside the closing layer, so they are now found there, and
nothing keeps a layer once it is gone.

Chromium 150 and later refuse a show during another popover's toggle,
so the copy can open in the next frame. The browser spec's
light-dismiss case now reads the copy after a frame, as its
nested-close cases do. The cap check's comment now names only the
copy cap.

* docs(core): the exit copy's JSDoc states the empty-canvas rule once

The empty-canvas rule stays in the first paragraph, and the inert-copy
paragraph no longer repeats it or `inertCopy`'s own contract.

* Merge the updated lower stack branch into uikit/c26-popover-present (own diff unchanged)

* Merge main into the layer exit copy branch (layer-runtime.md verification table keeps both rows)

* Merge the updated lower stack branch into uikit/c26-popover-present (own diff unchanged)

* Merge main into the layer exit copy branch (#8147 landed)

* Merge the updated lower stack branch into the Popover branch (#8147 landed; uikit index header lists popover and zoomMedia)

* Merge the updated lower stack branch into the Popover branch

* Merge main into the layer exit copy branch (placeOver's beforeNode beside keepInherited)

* Merge the updated lower stack branch into the Popover branch (#10224 landed)

* Merge main into the layer exit copy branch (#10224 landed)

* feat(Popover): the touch presentation starts in the next frame

The growth and its input hold now start on the first frame after the
open, not in the task that opens the popover, so the opening tap's
task lays out no more than a mouse opening does. Folded from #8662, so
the group's squashes apply in order: these lines are this pull
request's own.

* docs(Popover): record the next-frame window with the code

The input hold starts on the first frame after the open, so a touch that
lands within that frame reaches the page. spec:AST-094 and Popover's
record state that window here, beside the code that opens it. Moved from
#8662.

* fix(core): the exit copy reads its host's style before the insertion

keepInherited read the outer layer's parent's computed style right
after the copy was inserted there: a style read after a write. It
reads only that host and writes only on the still-detached copy, so it
now runs before the insertion. What the copy inherits is unchanged.

* Merge the updated lower stack branch into the Popover branch

* perf(Popover): the growth reads its boxes and timing before it writes

The frame that starts a touch presentation wrote the CSS entrance off
and the start scale, read both boxes, wrote the origin and the start
frame, then read the theme's entrance tokens. Each read after a write
restyled or laid the page out again. It now reads both boxes and the
entrance timing in one pass, then writes, the start frame first: the
motion engine may read the element's will-change as it sets it. The
box needs no reset first: the opening task already took the CSS
entrance off, and a dismissal leaves the element at full size.

Shown again during the dismissal, the popover reads its entrance
timing before its first write too.

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
cixzhang added a commit that referenced this pull request Nov 6, 2026
…rink (#9823)

* chore(core): the layer runtime's exit copy, for a touch presentation's close

placeLayerExitCopy puts a static, inert copy of a closing layer where the
layer was drawn, in the top layer, for a touch presentation's close to play
on: the browser hides a popover the moment it closes. The copy keeps the
layer's box, transform, presented look, overflow and scroll positions; has
no ids, anchor names, form names or form owner; is inert and hidden from assistive
technology; and its descendants' keyframe animations and transitions do not
start again. A refused show during the closing toggle waits, hidden, until
the toggle is over. A layer inside other layers gets its copy after the
outermost one, with what it inherited there (its color scheme included),
so a close that takes the layers around it too still draws the copy. That
copy is shown again above the copy the same close then places for a layer
around it. A layer has one copy. The layer-runtime record states
it, and that the presentation plays its exit on the copy (AST-094 FR7).

Core's shared inertCopy now takes every copied native control and
form-associated custom element out of any form with form="", as its
documentation says, so the swap's copies join no form either. It also
sets anchor-name: none on each copied element that has an anchor name, so
no live anchor gets a second element.

* Merge branch 'main' into uikit/c26-layer-exit-copy

* feat(Popover): under a finger, it grows out of its trigger and shrinks back into it

Under a coarse primary pointer, Popover grows out of its trigger on the
theme duration step nearest iOS's growth (`--duration-medium-min`) and the
theme's entrance easing, and shrinks back into it on the theme's exit
duration and easing, on the layer runtime's exit copy. A fine primary
pointer keeps the layer's own entrance.

* test(Popover): browser proofs of the touch presentation's grow and shrink

Real-browser proofs, in Chromium and WebKit, that under a finger Popover
grows out of its trigger on the theme's entrance tokens and shrinks back on
its exit tokens, keeps advancing through a blocked main thread, does no
Layout or Paint per frame, and cross-fades under Reduce Motion, with
storyboards of both.

* Merge branch 'main' into uikit/c26-layer-exit-copy

* Merge the updated lower stack branch into uikit/c26-popover-present (own diff unchanged)

* Merge the updated lower stack branch into uikit/c27-popover-present-proofs (own diff unchanged)

* Merge #8187's input hold into the Popover touch proofs

* feat(Popover): a touch during the growth is ignored

While the popover grows, the layer runtime's input hold takes every touch,
as UIKit ignores touches while it presents a popover. A tap outside does not
dismiss it and reaches nothing under it, and a pinch still zooms the page.
This folds in the hold that #8223 carried, so the growth and its hold land
together.

* test(Popover): browser proofs of the hold while the popover grows

A tap outside, a tap on the trigger and a pan while the popover grows do
nothing, a pinch still zooms, and a storyboard shows the growth going on
under a tap outside. Folded from #8223.

* fix(core): the exit copy keeps no closed layer

The exit copy found the inner layers to raise through a module-level
set of every copied layer. A layer React removed on close stayed
reachable there until a later close showed a copy. The inner layers
are inside the closing layer, so they are now found there, and
nothing keeps a layer once it is gone.

Chromium 150 and later refuse a show during another popover's toggle,
so the copy can open in the next frame. The browser spec's
light-dismiss case now reads the copy after a frame, as its
nested-close cases do. The cap check's comment now names only the
copy cap.

* docs(core): the exit copy's JSDoc states the empty-canvas rule once

The empty-canvas rule stays in the first paragraph, and the inert-copy
paragraph no longer repeats it or `inertCopy`'s own contract.

* Merge the updated lower stack branch into uikit/c26-popover-present (own diff unchanged)

* Merge the updated lower stack branch into uikit/c27-popover-present-proofs (own diff unchanged)

* test(Popover): the blocked-main-thread proof blocks from the page and asserts the verdict

The page now starts the block once the growth has run a frame, and the
proof asserts the harness verdict, which fails a judged part under 4
frames. Here the growth runs 600 ms at a constant speed, so each frame
moves several pixels and a block that starts late on a loaded machine
still lands inside it.

* Merge main into the layer exit copy branch (layer-runtime.md verification table keeps both rows)

* Merge the updated lower stack branch into uikit/c26-popover-present (own diff unchanged)

* Merge the updated lower stack branch into uikit/c27-popover-present-proofs (own diff unchanged)

* Merge main into the layer exit copy branch (#8147 landed)

* Merge the updated lower stack branch into the Popover branch (#8147 landed; uikit index header lists popover and zoomMedia)

* Merge the updated lower stack branch into the Popover branch (#8147 landed; uikit index header lists popover and zoomMedia)

* Merge the updated lower stack branch into the Popover branch

* Merge main into the layer exit copy branch (placeOver's beforeNode beside keepInherited)

* Merge the updated lower stack branch into the Popover branch

* Merge the updated lower stack branch into the Popover branch (#10224 landed)

* Merge main into the layer exit copy branch (#10224 landed)

* Merge #8187 (next-frame start folded in) into the Popover touch proofs

* feat(Popover): the touch presentation starts in the next frame

The growth and its input hold now start on the first frame after the
open, not in the task that opens the popover, so the opening tap's
task lays out no more than a mouse opening does. Folded from #8662, so
the group's squashes apply in order: these lines are this pull
request's own.

* test(Popover): browser proofs of the next-frame start

The touch-motion spec's cases for the growth that starts on the first
frame after the open, folded from #8662 with the change they prove.

* docs(Popover): record the next-frame window with the code

The input hold starts on the first frame after the open, so a touch that
lands within that frame reaches the page. spec:AST-094 and Popover's
record state that window here, beside the code that opens it. Moved from
#8662.

* Merge #8187 (next-frame records moved in) into the Popover touch proofs

* fix(core): the exit copy reads its host's style before the insertion

keepInherited read the outer layer's parent's computed style right
after the copy was inserted there: a style read after a write. It
reads only that host and writes only on the still-detached copy, so it
now runs before the insertion. What the copy inherits is unchanged.

* Merge the updated lower stack branch into the Popover branch

* perf(Popover): the growth reads its boxes and timing before it writes

The frame that starts a touch presentation wrote the CSS entrance off
and the start scale, read both boxes, wrote the origin and the start
frame, then read the theme's entrance tokens. Each read after a write
restyled or laid the page out again. It now reads both boxes and the
entrance timing in one pass, then writes, the start frame first: the
motion engine may read the element's will-change as it sets it. The
box needs no reset first: the opening task already took the CSS
entrance off, and a dismissal leaves the element at full size.

Shown again during the dismissal, the popover reads its entrance
timing before its first write too.

* Merge the updated lower stack branch into the Popover touch proofs

Co-authored-by: Cindy Zhang <cindyxz@meta.com>

This branch was successfully deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant