Skip to content

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

Closed
vjeux wants to merge 5 commits into
meta:mainfrom
vjeux:uikit/c21-gesture-realm
Closed

vjeux wants to merge 5 commits into
meta:mainfrom
vjeux:uikit/c21-gesture-realm

Conversation

@vjeux

@vjeux vjeux commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

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.

The gesture host and the modules on its touch path checked nodes and
events with instanceof against this window's classes. A component
rendered into another window's document, such as a same-origin iframe
or a Document Picture-in-Picture window, has nodes and touches from
that window, so every check failed. Its pans, taps and long presses
never began, its finger scrolls read as the program's, and a held
layer there held no touch.

The checks now read the node type (utils/nodeType.ts), or test an HTML
element against its own window's class. A jsdom iframe and a real
iframe in Chromium and WebKit each recognize a finger there. The
WebKit proofs' scope adds utils/nodeType, which their fixtures bundle.

The group-press click test in an iframe now asserts the press was
claimed. It passed on main only because the iframe's press never
reached the host.
@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 6:19am 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, 6:58 AM UTC

@github-actions

github-actions Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

🔎 Preview · d78dd80

Storybook · Sandbox · built from this PR's head

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


Generated by PR Enrichment workflow | View current CI run

@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 1d797ef

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

Thanks for this fix. Gestures now begin for a component rendered into another window's document, and the new iframe tests fail on main and pass here. Changes are requested for three issues:

  • isInsideScrollView still checks node instanceof Element (packages/core/src/utils/scrollView.ts:101), so in another window's document the 150 ms press delay ignores scroll containers and fixed bars, against spec:AST-094 FR11.
  • styleOf still checks style instanceof Object (packages/core/src/utils/selectionLock.ts:711), so the selection lock writes no style in another window and the iOS WebKit press lock does nothing there, against spec:AST-094 FR41.
  • utils/nodeType.ts duplicates the node-type guards in Layer/outsidePress.ts and the own-window isHTMLElement in hooks/useFocusTrap.ts, so make those modules import the shared guards and delete their copies.

An iframe test for each of the first two would keep them fixed.

[Automated review]

return (
target instanceof Element &&
isInsideScrollView(target, {readsTouchAction})
isElement(target) && isInsideScrollView(target, {readsTouchAction})

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.

isInsideScrollView still walks ancestors with node instanceof Element (utils/scrollView.ts:101). For another window's nodes this answer then ignores scroll containers and fixed bars, so the 150 ms press delay is wrong there (spec:AST-094 FR11). Use isElement(node) there and add an iframe case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 28c4762. The walk now uses isElement(node), and two iframe cases cover a scroll box and a fixed bar.

} from './flatTree';
import {subscribeInput} from './inputStream';
import {isIosWebKit} from './touchQueries';
import {isElement, isNode} from './nodeType';

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.

styleOf (line 711) still returns style instanceof Object ? style : null. Another window's style object fails that check, so lockStyles writes nothing there, and the iOS WebKit lock (line 111) is empty in an iframe (spec:AST-094 FR41). Check for a style object without instanceof, and add an iframe case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 28c4762: styleOf now checks for an object. jsdom makes every window's style in this realm. So the new case uses a style object without a prototype, which fails instanceof Object the same way.

Comment on lines +17 to +23
export function isNode(value: unknown): value is Node {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as {nodeType?: unknown}).nodeType === 'number'
);
}

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.

Layer/outsidePress.ts:121-138 already has these isNode, isElement and isShadowRoot guards, and hooks/useFocusTrap.ts:144-152 has an own-window isHTMLElement. Make those modules import from here and delete their copies, so one definition remains.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 28c4762: Layer/outsidePress.ts and hooks/useFocusTrap.ts now import these guards, and their copies are gone.

A touch in another window's document still missed two checks. The
scroll-view walk tested each ancestor with instanceof Element, so it
skipped that window's scroll boxes and fixed bars, and the press delay
inside a scroll view was wrong there. The selection lock tested the
inline style with instanceof Object, so on iOS WebKit it wrote no lock
there. Both now use checks that hold for any window. The outside-press
and focus-trap modules import the shared node checks instead of keeping
their own copies.

@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 a draft from a fork. Once it is ready for review, a maintainer adds the ci:run label so CI runs.

The three earlier findings are fixed: the scroll-view walk and the selection lock's style check now accept another window's nodes, each with an iframe test, and the outside-press and focus-trap modules use the shared guards. Gestures now begin for a component rendered into another window's document, and the new tests fail on main and pass here.

[Automated review]

@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 28c4762

@vjeux
vjeux marked this pull request as ready for review November 6, 2026 05:31
Main's pan on its own scroller now takes a trackpad's scroll (meta#10212),
and its lock asks whether the host styles the element for that
recognizer. This change reads another window's nodes in the same check.
The merge keeps both: the lock tests the view with isElement and passes
the recognizer to managed.

@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 on elements that another window's document creates, and the new iframe tests fail on main and pass here, but two things need fixing:

  • Miss: 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 its derived touch-action (pinch-zoom on main, auto here) while its switch label gains a second switch input; test namespaceURI === 'http://www.w3.org/1999/xhtml' instead and add a moved-element case.
  • Miss: isNodeTarget (packages/core/src/keyboard/keyboardHeight.ts:100) repeats the new isNode, and GestureHost.ts:1269 keeps its own instanceof view.HTMLElement check, so a fix to the shared guards misses them; have both use utils/nodeType.ts.

CI has not run on this head yet: the branch is in a fork, so a maintainer adds the ci:run label for CI to run.

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

In Chromium an element keeps the window that created it when it moves to another document. So for an element the page creates and then moves into an iframe or a Picture-in-Picture window, this answers false where main's instanceof HTMLElement answered true. In a real Picture-in-Picture window, such an element with a pan loses its derived touch-action (GestureHost.ts:1411: pinch-zoom on main, auto here), and such a switch label gains a second switch input (haptics.ts:116-118). Test the namespace instead, as Lab's NavigationStack.tsx:644-648 does:

return isElement(value) && value.namespaceURI === 'http://www.w3.org/1999/xhtml';

Then add a moved-element case to the iframe browser spec.

Comment on lines +17 to +23
export function isNode(value: unknown): value is Node {
return (
typeof value === 'object' &&
value !== null &&
typeof (value as {nodeType?: unknown}).nodeType === 'number'
);
}

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 packages/core/src/keyboard/keyboardHeight.ts:100-102 is the same typeof nodeType === 'number' test, and GestureHost.ts:1269 keeps its own instanceof view.HTMLElement. Make keyboardHeight.ts import isNode and drop its copy, and call isHTMLElement(target) at GestureHost.ts:1269, so a fix to these guards reaches every caller.

vjeux added 2 commits November 5, 2026 21:57
Main's finger-scroll hook now syncs in a layout effect (meta#10172). This
change imports isDocument from the shared node checks there and drops
the hook's own copy. The merge keeps main's import and the shared check.
An element the page makes and then moves into another window's
document, as Document Picture-in-Picture does, keeps the page's
classes. The HTML element check now reads the namespace, so such an
element keeps its derived touch-action and its switch label. The
gesture host's click retarget and blur checks, and the keyboard
height reader, use the shared node checks too.
@vjeux
vjeux marked this pull request as draft November 6, 2026 06:11
@cixzhang

cixzhang commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

/approve-spec d78dd80

@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 comes from a fork; a maintainer adds the ci:run label so CI runs.

Gestures now begin on elements in a same-origin iframe or a Document Picture-in-Picture window, under a mouse and a finger. The new iframe unit cases and the iframe pan browser case fail on main and pass here, and an element the page moves into that window keeps the behavior it had. No blocking findings remain.

[Automated review]

@vjeux
vjeux marked this pull request as ready for review November 6, 2026 06:38
cixzhang added a commit that referenced this pull request Nov 6, 2026
Takes #10211 at d78dd80, which fixes the moved-element case, merged with main.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
cixzhang added a commit that referenced this pull request Nov 6, 2026
* fix(core): gestures read another window's nodes and touches

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

* fix(core): gestures read another window's nodes and touches

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

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

* Merge branch 'main' into reland/gestures-another-window
@vjeux

vjeux commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #10224, which landed this change.

@vjeux vjeux closed this Nov 6, 2026
@vjeux
vjeux deleted the uikit/c21-gesture-realm branch November 6, 2026 08:28
@vjeux
vjeux restored the uikit/c21-gesture-realm branch November 6, 2026 10:43

This branch was successfully deployed

1 active deployment
Preview — d78dd80d 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.

2 participants