Skip to content

fix(Dialog, Drawer): under a finger, a tap or a drag during the entrance reaches nothing - #8630

Closed
vjeux wants to merge 1272 commits into
meta:mainfrom
vjeux:uikit/c28-dialog-drawer-entrance-hold
Closed

vjeux wants to merge 1272 commits into
meta:mainfrom
vjeux:uikit/c28-dialog-drawer-entrance-hold

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8342 and its per-event hold offer (#8342 (comment)). Review only this PR's change: vjeux/astryx@0f68401...7aa9117

Why

On a phone, a tap that lands while a Dialog or a Drawer is still entering closes it, runs the button under the finger, or reaches the page; a drag scrolls its content, or the page beside a non-modal drawer. The person loses what they just opened.

What led to it

Part of the touch-presentations program (spec:AST-094). UIKit delivers no touch until a presentation finishes, and FR35, as amended by #8618, asks the same of Astryx. Dialog and Drawer enter through CSS and take every input from the first frame.

Options and tradeoffs

  • Block all input during the entrance: simplest, but it would hold keyboard and screen-reader users, and a mouse, which FR35 says keeps today's behavior.
  • Hold pointer input, deciding per event, with feat(Layer): one input hold, first used by Dialog, Drawer and MobileNav (spec:AST-094 FR35) #8342's shared holdInput: a finger's or a pen's press reaches nothing until the entrance ends; keyboard, screen readers and a mouse are untouched, even on a touch screen. The cost is size: Dialog and Drawer now load the shared hold.

Why this choice

The second is what FR35 asks, and deciding per event keeps a mouse on a touch screen unchanged too. The hold lasts exactly as long as the layer's own entrance animations, read with getAnimations() as it opens, so a theme's duration, Reduce Motion or a close midway ends it with them. No API, style or default changes.

How to check it

In Chrome's or Safari's phone emulation, on the Dialog stories (the Drawer stories behave the same):

  1. Open a dialog and tap its backdrop while it is still entering: nothing happens, and it finishes opening. Before, it closed.
  2. Tap a button in it mid-entrance: nothing happens. Tap again once it has settled: the button works.
  3. Click the backdrop with a mouse mid-entrance: it closes, as before.
  4. Press Escape mid-entrance: it closes.

Evidence

vjeux and others added 30 commits October 26, 2026 09:29
meta#8270's selection lock writes no style where iOS WebKit does not run: it
cancels selectstart while the press holds. The press tracker's case reads
that, in jsdom, as the lock in force during the press and gone after.
…er reports no highlight change

The press ends inside the release guard, so the control hears no
onHighlightChange(false): the handler that disables it clears its own
highlight, as the review asks.
… in the edge zone

spec:AST-094 FR18's screen-edge refinement. A pan from one edge derives a
touch-action that takes the axis across that edge (pan-x for a side edge,
pan-y for top or bottom) only when its view is the edge zone: a thin
element along the edge, no wider than `edgeRegion` across it. There the
browser has nothing to scroll, so the pan still decides at its 15 px
hysteresis without losing the touch. On any wider view it leaves the
browser every touch behavior, so the author's scrolling there is untouched.
The view's size comes from a new optional `viewSize` on the gesture
environment (the default measures the element). Pans from several edges
are unchanged.

The edge-zone tests fail on the previous head. The pan and swipe tests also
read their own table groups (UIKIT_PAN, UIKIT_VELOCITY, UIKIT_SWIPE), which
completes the per-group imports in this change.
…-action mark, per-group UIKit imports) onto cixzhang's daf723b
Where the primary pointer is fine, one attached perEventPointer press
tracker made the host install every document touch and pointer listener
and the window click listener for as long as it stayed attached. The
tracker's element now listens for a finger's press itself (passive, on the
element only); the host adds its document and window listeners at that
press's start, hands it the press, and removes them once its contacts end,
so they last only the press (spec:AST-094 FR19's exception for a press's
feedback). A mouse press on the element adds nothing.

Test: an idle attached tracker leaves no document or window touch, pointer
or click listener; a finger's press paints and adds them; its lift removes
them. It fails on the previous head.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…nter trackers on a fine pointer) as a dependency
… an entrance catching an exit starts where it is

- decay accepts touchRelease and hands it to the spring back's transition,
  so a spring back a touch drag's release starts cross-fades on a fine
  pointer too (design:reduced-motion DR2); the README and coast.ts say so.
- A move taken back mid-dip (animate({x: 100}) then animate({x: 0})) left
  an emptied landing that kept the dip going down to 0 and back; a dip
  left with nothing to land now fades straight back in from where it is.
- An entrance by role caught mid-exit opened its fade at 0 and headed for
  the exit's 0; it now starts from the opacity on screen and fades back to
  what the exit would have restored, the exit's landing taken over as a
  named opacity's is.
Three tests fail on the previous head.
…to play(curves)

decay's doc keeps play(curves)'s 'play it with play or animate' beside the
new touchRelease sentence.
… per-event press tracker's listener lifetime

Where the pointer is fine, a per-event press tracker's element gets passive
touchstart and pointerdown listeners and no style, and the host's document
and window listeners come only for a finger's press on it; a per-event
recognizer keeps its listeners and style. The host's header now names the
carve-out, and AttachOptions and the gestures README stop saying the host
styles the tracker's element there.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…cross-fade for a spring back

With decay taking touchRelease, a spring back from past an edge also
cross-fades on a fine pointer when a touch drag's release starts it
(design:reduced-motion DR2); the header and the changeset now say so, as
decay's docs, the README and coast.ts do.
…Hold`)

`architecture:layer-runtime` INV14 already uses "presentation hold" for
keeping a layer's chosen presentation from open until the close settles, a
latch. This module does something else: it drops input while a presentation
animates. Renamed as cixzhang asked on this pull request: `Layer/inputHold.ts`
exports `holdInput`, `useInputHold`, `isInputShield` and `isInHeldLayer`, its
types are `InputHoldScope`, `InputHoldEnd` and `InputHoldOptions`, and its
tests, fixture, browser spec and changeset take the same name, with "input
hold" in the docs and comments. Behavior is unchanged.
# Conflicts:
#	internal/interaction-harness/README.md
#	internal/interaction-harness/package.json
#	internal/interaction-harness/src/harness.interaction.browser.spec.ts
#	internal/interaction-harness/src/story.ts
…the pointer is fine

Beside an attached per-event recognizer or press tracker, a
watchFingerScrolls watch kept every document touch and pointer listener and
the window click listener installed for its whole life where the primary
pointer is fine. A watch now counts only under a coarse pointer; under a
fine one the page listeners still come only for a finger's press on a
per-event press tracker (spec:AST-094 FR24), and the watch hears that press.

The idle fine-pointer test now adds a finger-scroll watch and still finds no
page listener; it fails on the previous head.

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

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Changes requested: the touch hold works on phones, but it also runs on desktops and touchscreen laptops, where spec:AST-094 FR19 and FR24 keep touch work off. Please gate holdEntranceInput on a coarse primary pointer (keeping the per-event check inside that gate), let a mouse press that comes before any movement through, and get the root-entry case of scripts/touch-support-bundle.test.mjs passing, for example by landing #8671 first. Then rebase onto #8342 and move the @astryxdesign/build StyleX changes to their own pull request, so this one carries only the entrance hold.

[Automated review]

Comment on lines +18 to +21
* pinch still zooms the page. A mouse keeps today's behavior: the hold decides
* by each event's pointer, never by the primary pointer, so a mouse or a
* trackpad keeps every click on a touch screen, and a finger is held on a
* laptop whose primary pointer is a mouse.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This makes the hold run where the primary pointer is fine: spec:AST-094 FR19 and FR24 (and architecture:react-component-runtime INV13) keep touch listeners and the finger hold off there. Returning null unless isCoarsePointer(layer.ownerDocument.defaultView), with the per-event check kept inside that gate, would keep a mouse on a touch screen unheld and leave desktops untouched.

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.

On the four points:

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.

Since the offer's FR24 change (b16ad3c), nothing listens while no layer is held, so the hold can't know a pointer used before it began: a mouse press made before the mouse moves during the hold still meets the shield and reaches nothing, in every layer.

Comment thread packages/core/src/Layer/inputHold.ts Outdated
if (view == null) {
return null;
}
latestPointer.delete(doc);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Forgetting the last pointer here means a mouse press made before any movement meets the shield (its pointerdown and mousedown are swallowed), against FR35's “A mouse keeps today's behavior”. Keeping the document's latest pointer type across the hold's start would let a layer a mouse opened pass mouse hits from the start.

vjeux added 5 commits October 27, 2026 07:40
…t press (spec:AST-094 FR35)

Red on the offer as it stands: a hold forgets the page's latest pointer at
its start, so its shield starts taking hits, and a mouse press made before
any movement meets it (its press and click swallowed), against FR35's "A
mouse keeps today's behavior".

- A presentation's shield starts letting a mouse through when a mouse is
  the page's latest pointer, and the gesture host's gate passes it from
  the start; a finger as the latest pointer starts it taking hits, and a
  dismissal's shield takes every hit whatever the pointer.
- In the browser: once the page has held input, a layer that opens under
  a resting mouse takes the mouse's press before any movement, as with no
  hold, while a finger's tap on it is still held.
… the start (spec:AST-094 FR35)

The hold forgot the page's latest pointer at its start, so its shield
started taking hits and a mouse press made before any movement met it.

From a document's first hold on, a passive watch of its pointers keeps the
latest one, between holds too. A presentation's shield starts taking no
hits when that pointer is a mouse, and the gesture host's gate passes the
mouse from the start; a finger's press makes the shield take hits again,
captured to it if it landed past it, as before. A dismissal's shield still
takes every hit.

What stays: nothing watches a document's pointers before its first hold,
so in the first layer a page holds, a mouse press made before any movement
still meets the shield. Reading the opening click instead fails in WebKit,
which reports a tap's click as a mouse's, and starting the shield down
would let a finger's first contact reach what lies beneath (WebKit shows
the control under it as :active).
… action during the entrance (spec:AST-094 FR35)

After a first presentation, a key opens the dialog again with the mouse
resting where its action appears; a press there with no movement first lands
on the action, as with no hold. On the previous offer it met the shield.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approval needs an engineering owner's decision: on main, spec:AST-094 FR13, FR19 and FR24 and architecture:react-component-runtime INV13 keep touch handling off where the primary pointer is fine, but this entrance hold also holds a finger and adds touch listeners there, relying on FR43 from the unmerged #8646. An engineering owner can settle that by landing #8646 or by commenting /approve-spec <full head SHA> on this pull request, which the author may do as an engineering owner; otherwise the hold should apply only where the primary pointer is coarse. Independently of that decision, these need changes:

  • Rebase onto the landed #8342 stack so the diff is this change's own 12 files; 24,964 of its 26,175 authored lines come from that stack.
  • CI's node tests fail scripts/touch-support-bundle.test.mjs "is absent from the root entry", because Dialog now imports the gesture host; #8671 or an equivalent test change needs to land first.
  • In the first Dialog or Drawer a page opens, a mouse press made without moving meets the input shield during the entrance instead of the control under the cursor (Layer/inputHold.ts:58-60), so the changeset's and docs' "a mouse or a trackpad is unchanged" does not hold yet.

[Automated review]

Comment thread packages/core/src/Layer/inputHold.ts Outdated
Comment on lines +58 to +60
* stays the hold's and a light-dismissing layer stays open). Nothing watches
* a document's pointers before its first hold, so in the first layer a page
* holds, a mouse press made before any movement still meets the shield. A

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In the first layer a page holds, a still mouse press meets the shield, so in the first Dialog or Drawer a page opens, a mouse or trackpad press made without moving does not reach the control under the cursor. spec:AST-094 FR35 says "A mouse keeps today's behavior", and the changeset and both doc descriptions say a mouse is unchanged. Starting this pointer watch before the first hold (for example when Dialog or Drawer mounts) and adding a first-presentation still-mouse case to DialogTouchEntrance.interaction.browser.spec.ts would close the gap.

import {layerStructureReset} from '../Layer/layerStructureReset.stylex';
import {LayerContentBoundary} from '../Layer/layerScopedContext';
import {LayerDepthProvider} from '../Layer/LayerDepthContext';
import {holdEntranceInput} from '../Layer/holdEntranceInput';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Through inputHold.ts, this import brings gestures/GestureHost and eleven other gesture modules into Core's root entry (56,018 minified bytes), so main's scripts/touch-support-bundle.test.mjs fails "is absent from the root entry" on this head (CI test-node 2/4). #8671's per-component boundary, or another owner-approved change to that test, has to land first.

vjeux added 9 commits October 27, 2026 09:49
Red on the offer as it stands, against the merged FR24: "the number of
document- or window-level touch listeners MUST NOT grow with the number of
component instances, ... and nothing touch-related remains after the last
instance unmounts" (with architecture:react-component-runtime INV5: a
document singleton persists across consumer gaps only where current
authority names that lifetime).

- A hold adds its window listeners only while a layer is held, once per
  document however many layers are held, and removes every one after the
  last hold ends (two held layers add 28 today, and the pointer watch
  stays for good).
- It registers the gesture host's gate only while a layer is held and
  releases it after the last hold ends (it stays registered today).
- It remembers no pointer while nothing is held: a mouse that moved before
  a layer opened leaves its presentation's shield taking hits until the
  mouse moves again during the hold.

The browser case for a mouse resting before the layer opened goes: with
nothing listening at rest, that press meets the shield again.
Against the merged FR24 ("the number of document- or window-level touch
listeners MUST NOT grow with the number of component instances, ... and
nothing touch-related remains after the last instance unmounts", with
architecture:react-component-runtime INV5), the hold is a touch resource:

- One press hold per document, shared by every held layer: its window
  listeners come with the document's first held layer and go once no
  layer is held and the presses it holds have ended. Two held layers no
  longer add two sets.
- The gesture host's gate is registered with the document's first held
  layer and released after the last hold ends, rather than staying for
  the page's lifetime.
- The pointer watch that stayed on the window from a page's first hold on
  goes. Only the pointers heard while a layer is held count, and nothing
  is remembered once the last hold ends: a mouse press made before the
  mouse has moved during the hold meets the shield again.
…ing listens while nothing is held) into the entrance hold's base
…mit (spec:AST-094 FR35, FR24)

Since the input hold listens to nothing while nothing is held (FR24), it
cannot know a pointer used before it began: during the entrance a mouse
press made before the mouse moves meets the shield, and once the mouse
moves its press reaches the action. On the previous offer the still press
reached the action.
…'s to its end (spec:AST-094 FR35)

Red on the offer as it stands (found by meta#8630's Dialog case): a mouse that
has not moved during a presentation's hold presses, its pointerdown meets
the shield, and the shield then steps aside because that pointerdown named
a mouse. The release is hit-tested on what lies beneath (an action), so the
browser sends the click to the common ancestor of the two targets, the
dialog, which Dialog reads as a backdrop click and closes on.

- The shield stays up through a mouse press that begins on it, a drag
  included; only a mouse moving with no button down lets it through.
- The gesture host's gate holds what lands on a shield, a mouse's press
  included.
- In the browser: a still mouse press during a dialog's hold lands its
  release and click on the shield and reaches nothing; once the mouse
  moves, its press reaches the dialog's recognizer, as with no hold.
- The gate case moves its mouse before pressing, as a mouse that passes
  now must.
…s to its end (spec:AST-094 FR35)

Found by meta#8630's Dialog case: a mouse that had not moved during a
presentation's hold pressed, its pointerdown met the shield, and the shield
stepped aside because that pointerdown named a mouse. The release was
hit-tested on what lay beneath, so the browser sent the click to the
common ancestor of the two targets, the dialog, which Dialog read as a
backdrop click and closed on.

Only a mouse moving with no button down lowers a presentation's shields
now; a finger's or a pen's press or move raises them as before. A mouse
press that begins on a shield keeps it up to its end, so its release and
click land on the shield, and the gesture host's gate holds whatever lands
on a shield, a mouse's press included.
…a press that began on the shield stays its own) into the entrance hold's base

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Approval needs an engineering owner's decision: spec:AST-094 FR35 holds a finger while a touch presentation enters, which FR13 scopes to a coarse primary pointer, but this entrance hold also runs where the primary pointer is fine, holding a finger and adding window listeners in components that use no gesture, where FR43, FR24 and architecture:react-component-runtime INV13 let only components that use a gesture listen. An engineering owner can approve that by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner; otherwise the hold should apply only where the primary pointer is coarse. Independently of that decision, these need changes:

  • During every Dialog or Drawer entrance, a mouse press made before the pointer moves lands on the input shield and reaches nothing (Layer/inputHold.ts:58-61), against FR35's "A mouse keeps today's behavior" and the changeset's "a mouse or a trackpad is unchanged".
  • Core's root entry now carries the gesture host, so scripts/touch-support-bundle.test.mjs "is absent from the root entry" fails; #8671 or an equivalent test change needs to land first.
  • Rebase onto the landed #8342 stack so the diff is this change's own 12 files; 25,087 of its 26,303 authored lines come from that stack.

[Automated review]

return [...state.layers];
}
heldDocuments.delete(doc);
latestPointer.delete(doc);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Forgetting the latest pointer here means a presentation that opens while nothing else is held always starts with its shield raised for a mouse, so a mouse or trackpad press made before the pointer moves lands on the shield and reaches nothing (lines 58-61), in every Dialog and Drawer entrance. spec:AST-094 FR35 says "A mouse keeps today's behavior", and its verification row fails a changed mouse path. Seeding the pointer from input already heard, for example by recording the pointer type in the interaction-modality store whose document pointerdown listener Dialog already installs (Drawer would need the same tracking), would let a presentation a mouse opened pass mouse hits from the start without a new idle listener.

Comment on lines +18 to +21
* pinch still zooms the page. A mouse keeps today's behavior: the hold decides
* by each event's pointer, never by the primary pointer, so a mouse or a
* trackpad keeps every click on a touch screen, and a finger is held on a
* laptop whose primary pointer is a mouse.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

#8646 has landed, but its FR43 governs gestures and never names the entrance hold, while FR35 reaches a touch presentation, which FR13 applies under a coarse primary pointer; FR43 and INV13 still let only a component that uses a gesture listen under a fine pointer. Holding a finger on a mouse-primary laptop, with the hold's 14 window listeners for every entrance, needs an engineering owner's /approve-spec on this head; otherwise return null unless the primary pointer is coarse, keeping the per-event check inside that gate.

'@astryxdesign/core': patch
---

[fix] Under a finger, a tap or a drag that lands while a `Dialog` or a `Drawer` is still entering no longer closes it, runs one of its actions, moves focus or scrolls the page: it reaches nothing until the entrance has played, and the dialog or drawer finishes entering and stays open. Keys and assistive technology stay live during the entrance, the content stays in the accessibility tree, a pinch still zooms the page, and a mouse or a trackpad is unchanged, even on a touch screen.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

"a mouse or a trackpad is unchanged, even on a touch screen" does not hold at this head: during every entrance a mouse press made before the pointer moves lands on the shield (Layer/inputHold.ts:58-61). The same sentence is in Dialog.doc.mjs:72 and Drawer.doc.mjs:175; fix the hold, or narrow all three.

@cixzhang

Copy link
Copy Markdown
Contributor

/approve-spec 7aa9117

@vjeux
vjeux marked this pull request as draft October 29, 2026 22:38
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
- MobileNav: the touch-presentation proofs stop the page's clock and run
  it on by exact amounts, so each probe and tap lands inside the slide-in
  or the close, and the close is read exactly (at most 250 ms) (from
  meta#8374).
- Dialog and Drawer: each entrance check plays one drag and proves it
  arrived (a capture-phase touch probe added before the page's scripts);
  the page's movement is read from the visual viewport (from meta#8630).
- Layer: a hold under a moving touch is released at an exact step of the
  drag, and the document's log covers the touch from its start to its
  end.
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 30, 2026
…av (spec:AST-094 FR35)

Layer/inputHold holds a layer's pointer input while its presentation
enters or leaves: holdInput(layer, {until, scope}) raises a shield (scope
'viewport' while it enters; 'layer' while it leaves, its content inert),
decides by each event's pointer (a finger's or a pen's press is held to
its end; a mouse passes once it moves with no button down, DEC-16), never
cancels a touch's start, and cancels a one-finger move so a pinch still
zooms (FR10). The gesture host asks the hold for each touch (holdsTouch),
so the hold imports nothing from gestures/ and a component in the root
entry may use it statically (architecture:react-component-runtime INV13).

Its first users: Dialog and Drawer hold a finger's tap or drag during
their entrance (holdEntranceInput). MobileNav's drawer takes no finger
input while it slides in (a 'viewport' hold until the slide-in's
transition ends) or out (a 'layer' hold from the close request until the
dialog closes), and AppShell lets the slide-out play to its end.

Every browser proof lands its input at an exact point (an entrance held
where it is, the page's clock, or an exact step of a drag) and plays once.

From meta#8342 and its per-event offer, meta#8630 (Dialog and Drawer) and meta#8374
(MobileNav).

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

vjeux commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

Folded into #8342 under the maintainers' section plan; the commit credits this PR's authors. Closing; the branch stays.

@vjeux vjeux closed this Oct 30, 2026
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 31, 2026
…av (spec:AST-094 FR35)

Layer/inputHold holds a layer's pointer input while its presentation
enters or leaves: holdInput(layer, {until, scope}) raises a shield (scope
'viewport' while it enters; 'layer' while it leaves, its content inert),
decides by each event's pointer (a finger's or a pen's press is held to
its end; a mouse passes once it moves with no button down, DEC-16), never
cancels a touch's start, and cancels a one-finger move so a pinch still
zooms (FR10). The gesture host asks the hold for each touch (holdsTouch),
so the hold imports nothing from gestures/ and a component in the root
entry may use it statically (architecture:react-component-runtime INV13).

Its first users: Dialog and Drawer hold a finger's tap or drag during
their entrance (holdEntranceInput). MobileNav's drawer takes no finger
input while it slides in (a 'viewport' hold until the slide-in's
transition ends) or out (a 'layer' hold from the close request until the
dialog closes), and AppShell lets the slide-out play to its end.

Every browser proof lands its input at an exact point (an entrance held
where it is, the page's clock, or an exact step of a drag) and plays once.

From meta#8342 and its per-event offer, meta#8630 (Dialog and Drawer) and meta#8374
(MobileNav).

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Oct 31, 2026
…av (spec:AST-094 FR35)

Layer/inputHold holds a layer's pointer input while its presentation
enters or leaves. holdInput(layer, {until, scope}) raises a shield:
'viewport' over the whole viewport while a modal layer enters; 'surface'
over the layer's own box while a non-modal one enters, the page around it
live; 'layer' over the viewport, its content inert, while a layer leaves.
It decides by each event's pointer (a finger's or a pen's press is held to
its end; a mouse passes once it moves with no button down, DEC-16), never
cancels a touch's start, and cancels a one-finger move so a pinch still
zooms (FR10). The gesture host asks the hold for each touch (holdsTouch),
so the hold imports nothing from gestures/ and a component in the root
entry may use it statically (architecture:react-component-runtime INV13).

Its first users hold only under a coarse primary pointer, a touch
presentation (FR13): Dialog and Drawer hold a finger's tap or drag during
their entrance (holdEntranceInput), a non-modal Drawer only on its panel;
MobileNav's drawer takes no finger input while it slides in; AppShell lets
the slide-out play to its end. Under a fine primary pointer nothing is held
and nothing listens (FR24). Where the primary pointer is fine and the
screen also takes touch, a finger is not held yet; AST-094's current-state
impact and Drawer's record name that gap.

Every browser proof lands its input at an exact point (an entrance held
where it is, the page's clock, or an exact step of a drag) and plays once.

From meta#8342 and its per-event offer, meta#8630 (Dialog and Drawer) and meta#8374
(MobileNav).

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 1, 2026
…av (spec:AST-094 FR35)

Layer/inputHold holds a layer's pointer input while its presentation
enters or leaves. holdInput(layer, {until, scope}) raises a shield:
'viewport' over the whole viewport while a modal layer enters; 'surface'
over the layer's own box while a non-modal one enters, the page around it
live; 'layer' over the viewport, its content inert, while a layer leaves.
It decides by each event's pointer (a finger's or a pen's press is held to
its end; a mouse passes once it moves with no button down, DEC-16), never
cancels a touch's start, and cancels a one-finger move so a pinch still
zooms (FR10). The gesture host asks the hold for each touch (holdsTouch),
so the hold imports nothing from gestures/ and a component in the root
entry may use it statically (architecture:react-component-runtime INV13).

Its first users hold only under a coarse primary pointer, a touch
presentation (FR13): Dialog and Drawer hold a finger's tap or drag during
their entrance (holdEntranceInput), a non-modal Drawer only on its panel;
MobileNav's drawer takes no finger input while it slides in; AppShell lets
the slide-out play to its end. Under a fine primary pointer nothing is held
and nothing listens (FR24). Where the primary pointer is fine and the
screen also takes touch, a finger is not held yet; AST-094's current-state
impact and Drawer's record name that gap.

Every browser proof lands its input at an exact point (an entrance held
where it is, the page's clock, or an exact step of a drag) and plays once.

The WebKit interaction lane's scope includes Layer/inputHold.ts: the
gesture host imports it, so the gesture specs' fixture bundles it.

From meta#8342 and its per-event offer, meta#8630 (Dialog and Drawer) and meta#8374
(MobileNav).

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 1, 2026
…av (spec:AST-094 FR35)

Layer/inputHold holds a layer's pointer input while its presentation
enters or leaves. holdInput(layer, {until, scope}) raises a shield:
'viewport' over the whole viewport while a modal layer enters; 'surface'
over the layer's own box while a non-modal one enters, the page around it
live; 'layer' over the viewport, its content inert, while a layer leaves.
It decides by each event's pointer (a finger's or a pen's press is held to
its end; a mouse passes once it moves with no button down, DEC-16), never
cancels a touch's start, and cancels a one-finger move so a pinch still
zooms (FR10). The gesture host asks the hold for each touch (holdsTouch),
so the hold imports nothing from gestures/ and a component in the root
entry may use it statically (architecture:react-component-runtime INV13).

Its first users hold only under a coarse primary pointer, a touch
presentation (FR13): Dialog and Drawer hold a finger's tap or drag during
their entrance (holdEntranceInput), a non-modal Drawer only on its panel;
MobileNav's drawer takes no finger input while it slides in; AppShell lets
the slide-out play to its end. Under a fine primary pointer nothing is held
and nothing listens (FR24). Where the primary pointer is fine and the
screen also takes touch, a finger is not held yet; AST-094's current-state
impact and Drawer's record name that gap.

Every browser proof lands its input at an exact point (an entrance held
where it is, the page's clock, or an exact step of a drag) and plays once.

The WebKit interaction lane's scope includes Layer/inputHold.ts: the
gesture host imports it, so the gesture specs' fixture bundles it.

From meta#8342 and its per-event offer, meta#8630 (Dialog and Drawer) and meta#8374
(MobileNav).

Co-authored-by: Cindy Zhang <cindyxz@meta.com>
vjeux added a commit to vjeux/astryx that referenced this pull request Nov 2, 2026
…av (spec:AST-094 FR35)

Layer/inputHold holds a layer's pointer input while its presentation
enters or leaves. holdInput(layer, {until, scope}) raises a shield:
'viewport' over the whole viewport while a modal layer enters; 'surface'
over the layer's own box while a non-modal one enters, the page around it
live; 'layer' over the viewport, its content inert, while a layer leaves.
It decides by each event's pointer (a finger's or a pen's press is held to
its end; a mouse passes once it moves with no button down, DEC-16), never
cancels a touch's start, and cancels a one-finger move so a pinch still
zooms (FR10). The gesture host asks the hold for each touch (holdsTouch),
so the hold imports nothing from gestures/ and a component in the root
entry may use it statically (architecture:react-component-runtime INV13).

Its first users hold only under a coarse primary pointer, a touch
presentation (FR13): Dialog and Drawer hold a finger's tap or drag during
their entrance (holdEntranceInput), a non-modal Drawer only on its panel;
MobileNav's drawer takes no finger input while it slides in; AppShell lets
the slide-out play to its end. Under a fine primary pointer nothing is held
and nothing listens (FR24). Where the primary pointer is fine and the
screen also takes touch, a finger is not held yet; AST-094's current-state
impact and Drawer's record name that gap.

Every browser proof lands its input at an exact point (an entrance held
where it is, the page's clock, or an exact step of a drag) and plays once.

The WebKit interaction lane's scope includes Layer/inputHold.ts: the
gesture host imports it, so the gesture specs' fixture bundles it.

MobileNav's record states what the rest of the slide-out after the close
does: it takes no input and is hidden from assistive technology (FR5). A
browser case proves both against the open drawer.

From meta#8342 and its per-event offer, meta#8630 (Dialog and Drawer) and meta#8374
(MobileNav).

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

This branch was successfully deployed

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

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants