Skip to content

fix(MobileNav): block background pans only on the scrim, so no drawer element restricts touch-action for a layer inside it - #10112

Closed
cixzhang wants to merge 5 commits into
mainfrom
l1/appshell-drawer-touch
Closed

cixzhang wants to merge 5 commits into
mainfrom
l1/appshell-drawer-touch

Conversation

@cixzhang

@cixzhang cixzhang commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

User impact

A Dialog opened from inside the open mobile navigation drawer, such as a search dialog, was reported as not scrolling with a finger on a phone, while the wheel and the keyboard still scrolled it.

  1. Why: the drawer's dialog sets touch-action: pinch-zoom, and its content sets pan-y pinch-zoom. A Dialog rendered inside the drawer is their DOM descendant, even though it paints in the top layer.
  2. Why that can block it: a browser that resolves touch-action through every DOM ancestor applies the drawer's block to the nested Dialog, and a descendant cannot allow back what an ancestor blocks. Chromium stops at the nearest scroll container, so it already scrolls the nested results; a phone check on the device the report came from is pending; until it reports, this pull request claims only the change below, not a restored behavior.

The fix

The gesture block moves to the scrim, the area beside the drawer.

  • A new scrim element behind the drawer blocks pans and allows pinch, as the dialog did, so a pan beside the drawer stays off the page and pinch-zoom still works (AR4, WCAG 1.4.4).
  • The dialog, the drawer, its header and its content declare no touch-action, so a layer rendered anywhere inside the drawer keeps its own. FR3's scroll lock already holds the page under the drawer.
  • A tap on the scrim closes the nav, as a tap on the dialog did.
  • The drawer stays the dialog's first child, above the scrim, so existing selectors still find it.

Testing

  • MobileNavNestedTouch.a11y.chromium.spec.ts (new): opens a Dialog from inside the drawer on a touch phone viewport. It checks three things: no ancestor of the Dialog's content up to the drawer restricts panning, a finger drag scrolls the results, the scrim computes pinch-zoom, and the header computes auto.
  • MobileNavNestedTouch.test.tsx (new): a tap on the scrim closes the nav; a tap on drawer content doesn't.
  • MobileNavWidth, MobileNavPinchZoom and MobileNavTouchPresentation specs: their backdrop checks now count any element inside the dialog and outside the drawer, which includes the scrim.
  • The MobileNav and AppShell unit suites pass (181).
  • A "Nested Dialog" story shows the case.

Chromium 154 with a touch phone viewport, against Storybooks built by Vercel from this head (after) and from main plus only this story (before):

Before (main) After
Ancestors of the nested Dialog's results that restrict panning drawer content pan-y pinch-zoom, dialog pinch-zoom none
Scrim / header / dialog touch-action — / auto / pinch-zoom pinch-zoom / auto / auto
A finger drag scrolls the results 252 px 250 px

Chromium stops resolving touch-action at the nearest scroll container, so it already scrolled the nested results before the change. The browser where an ancestor's block reaches the nested layer is what the report hit; that phone check is pending. The ancestor walk in the spec fails on main and passes here.

A browser resolves touch-action through DOM ancestors, and a descendant
cannot allow back what an ancestor blocks. The drawer's dialog declared
touch-action: pinch-zoom (and its content pan-y pinch-zoom), so a Dialog
opened inside the drawer could not be scrolled with a finger.

The gesture block moves to the scrim: a new element beside the drawer
blocks pans and allows pinch, as the dialog did, and so does the header,
which does not scroll. The dialog, the drawer and its content declare no
touch-action, so nested layers keep their own. A tap on the scrim closes
the nav, as a tap on the dialog did. The drawer stays the dialog's first
child (z-index above the scrim), so existing selectors still find it.

A Chromium spec opens a Dialog from inside the drawer: no ancestor of its
content up to the drawer restricts panning, a finger drag scrolls its
results, and the scrim and header still block pans and allow pinch. A
story shows the nested Dialog.
@vercel

vercel Bot commented Nov 5, 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 5, 2026 6:49pm UTC

Request Review

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

astracat-bot Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Nov 5, 2026, 7:16 PM UTC

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

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


Generated by PR Enrichment workflow | View current CI run

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested. The scrim keeps tap-to-close and pinch zoom working, but the change breaks existing browser tests and does not yet show the failure it fixes.

  • MobileNavWidth.a11y.chromium.spec.ts (line 81), MobileNavPinchZoom.a11y.chromium.spec.ts (line 141) and MobileNavTouchPresentation.a11y.chromium.spec.ts (line 161, used by the tests at lines 291 and 353) still expect the <dialog> to be the hit target beside the drawer, so they fail now that the scrim takes those hits; treat any non-drawer element inside the dialog as backdrop there.
  • The new MobileNavNestedTouch.a11y.chromium.spec.ts fails at line 53 because the Nested Dialog story's 40 inline Text results fit inside the 60dvh dialog, so the results region never scrolls and never takes its group role; render one block row per result.
  • In Chromium with real touch input, a Dialog inside the drawer whose results overflow already scrolls on main (251 px for a 200 px drag, the same as this branch), so the title and description claim a failure the tests do not reproduce; reproduce it on main in the browser where it fails, name that browser and the cause, and add a check that fails on main, or drop the fix claim.
  • @astryxdesign/core source changes without a Changeset, which the changeset coverage check rejects; add one with pnpm changeset:new --packages @astryxdesign/core --category fix, worded to match what the previous point settles.

The comment above styles.dialog (MobileNav.tsx lines 121–123) repeats the ancestor-blocking cause, so keep only the reason that holds once the real cause is known. CI has not run on this draft yet, and each failure above reproduces with the repository's own Chromium specs.

[Automated review]

Comment on lines +792 to +796
<div
ref={scrimRef}
aria-hidden="true"
{...stylex.props(styles.scrim)}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This scrim now takes every hit beside the drawer, so MobileNavWidth.a11y.chromium.spec.ts:81 (expects dialog, gets div), MobileNavPinchZoom.a11y.chromium.spec.ts:141 and MobileNavTouchPresentation.a11y.chromium.spec.ts:161 fail, though all pass on main. Treat any non-drawer element inside the dialog as backdrop in those checks, for example hit.closest('dialog') === dialog && !hit.closest('[tabindex="-1"]'). With that change the backdrop pinch passes here.

Comment on lines +121 to +123
// No touch-action here: a browser resolves it through DOM ancestors, so a
// block on the dialog would also stop touch scrolling in a layer opened
// inside the drawer. The scrim and the header block pans instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking (INV29): on main in Chromium, a nested results scroller pans under a finger despite this dialog's pinch-zoom, because the touch-action intersection stops at the nearest scroll container. Keep only the reason that holds, for example: "The scrim and the header block pans. The dialog sets no touch-action, so a layer in the drawer keeps its own."

content={
<LayoutContent label="Search results">
{SEARCH_RESULTS.map(result => (
<Text key={result} type="body">

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.

These Text results render inline, so all 40 fit in about 400 px inside the 60dvh dialog. The region never overflows, so it takes no group role or name (LayoutContent FR4), and nothing scrolls. Wrap each result in a block row so the story shows the scrolling its doc comment describes.

await page.waitForTimeout(600); // the slide-in
await nav.getByRole('button', {name: 'Search'}).tap();
const results = page.getByRole('group', {name: 'Search results'});
await expect(results).toBeVisible();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This fails at this head with element(s) not found: the story's results region fits, so it never gets the group role (see the story comment). Once the story overflows, the drag check at line 100 passes on main too: the same 200 px finger drag scrolls 251 px on both, so this spec does not yet guard the fix.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested. The story and changeset points from the last review are fixed; two blocking points remain.

  • MobileNavWidth.a11y.chromium.spec.ts (line 81), MobileNavPinchZoom.a11y.chromium.spec.ts (line 141) and MobileNavTouchPresentation.a11y.chromium.spec.ts (line 161, used by the tests at lines 291 and 353) still treat the <dialog> as the hit target beside the drawer, so all four tests fail now that the scrim takes those hits; count any non-drawer element inside the dialog as backdrop there.
  • The title, description and changeset say a Dialog inside the drawer could not scroll with a finger, but in Chromium with real touch input the Nested Dialog story's results already scroll on main (250 px for a 200 px swipe, 252 px on this branch) and the new spec's drag check passes there too; reproduce the failure on main in the browser where it happens, with a check that fails there by behavior, or reword the title, description and changeset to what this branch changes.

The comment above styles.dialog (lines 121–123) still gives the ancestor cause; keep only the reason that holds. CI has not run on this draft, and each point reproduces with the repository's own Chromium specs.

[Automated review]

ref={scrimRef}
aria-hidden="true"
{...stylex.props(styles.scrim)}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This scrim takes every hit beside the drawer, so MobileNavWidth.a11y.chromium.spec.ts:81 (expects dialog, gets div), MobileNavPinchZoom.a11y.chromium.spec.ts:141 and MobileNavTouchPresentation.a11y.chromium.spec.ts:161 (tests at lines 291 and 353) fail, though all pass on main. Count any non-drawer element inside the dialog as backdrop in those checks, for example hit.closest('dialog') === dialog && !hit.closest('[tabindex="-1"]'). A pinch on the scrim does zoom the page, so only the checks are stale.

Comment on lines +121 to +123
// No touch-action here: a browser resolves it through DOM ancestors, so a
// block on the dialog would also stop touch scrolling in a layer opened
// inside the drawer. The scrim and the header block pans instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking (INV29): on main in Chromium a nested results scroller pans under a finger despite this dialog's pinch-zoom, because the effective touch-action stops at the nearest scroll container. Keep only the reason that holds, for example: "The scrim and the header block pans. The dialog sets no touch-action, so a layer in the drawer keeps its own."

touchPoints: [],
});
await sleep(600);
expect(await results.evaluate(element => element.scrollTop)).toBeGreaterThan(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This drag check also passes on main: with the Nested Dialog story in place there and only line 75 removed, the same 200 px drag scrolls the results (250 px, against 252 px here). Line 75 fails on main only because getComputedStyle reports the declared touch-action, not whether a pan is allowed. So the spec does not yet guard the fix; it needs a step that fails on main by behavior.

'@astryxdesign/core': patch
---

[fix] A Dialog or other layer opened inside the open `MobileNav` drawer scrolls with a finger again. The drawer blocks background pans on its scrim and header instead of on the whole dialog, so a nested layer keeps its own touch scrolling. Pinch-zoom still works over the nav.

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.

"scrolls with a finger again" claims a failure that does not reproduce on main in Chromium, where a Dialog inside the drawer already scrolls under a finger, and the released drawer already ships these declarations. Reword this to the change this branch shows, or name the browser where it fails once that is reproduced.

Per review: MobileNavWidth, MobileNavPinchZoom and MobileNavTouchPresentation
treated the dialog element as the only hit target beside the drawer; any
element inside the dialog and outside the drawer is backdrop now. The
dialog comment, the changeset and the nested spec's header say what this
change does: Chromium already scrolls a nested results list (it stops
resolving touch-action at the nearest scroll container), so the ancestor
walk is the guard and the drag a regression check.
@cixzhang cixzhang changed the title fix(MobileNav): a layer opened inside the drawer keeps touch scrolling fix(MobileNav): block background pans on the scrim and header, so a layer in the drawer keeps its own touch scrolling Nov 5, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested. The backdrop checks now count the scrim and every MobileNav browser test passes; two points about what the change claims to fix remain.

  • The changeset still calls this a fix for nested touch scrolling "in a browser that resolves touch-action through DOM ancestors", but nothing in the pull request reproduces that failure (in Chromium the Nested Dialog story's results scroll the same on main and here, and the phone check the description mentions is not attached), so attach the phone before and after naming the browser and version, or describe only the change, for example that no drawer element restricts touch-action for a layer rendered in its content.
  • Miss: the changeset covers "a Dialog or other layer opened inside" the drawer, but the header now declares touch-action: pinch-zoom (MobileNav.tsx line 253) over the caller's header content, so under the premise in the description a layer rendered in the header keeps that restriction; scope the wording to layers in the drawer's content, or take the block off the header.

Two comments in MobileNav.tsx (lines 171–174 and 703–704) could be shorter; see the inline notes. CI has not run on this draft.

[Automated review]

'@astryxdesign/core': patch
---

[fix] The open `MobileNav` drawer blocks background pans on its scrim and header instead of on the whole drawer, so a Dialog or other layer opened inside it keeps its own touch scrolling in a browser that resolves `touch-action` through DOM ancestors. Pinch-zoom still works over the nav.

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.

Nothing in this pull request reproduces the failure this fixes: in Chromium the Nested Dialog story's results scroll 251 px on main and 249 px here under the same 200 px swipe, and the Pointer Events resolution stops at the nearest scroll container. Attach the phone before and after, naming the browser and version, or describe the change itself, for example: "…so no drawer element restricts touch-action for a layer rendered in its content."

paddingInline: spacingVars['--spacing-2'],
flexShrink: 0,
// The header does not scroll, so a pan on it would reach the page.
touchAction: 'pinch-zoom',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Miss: the caller's header content renders inside this element, so under the premise in the description a layer rendered in the header (results for the search field the Tall Header story puts here) keeps a pinch-zoom ancestor, as Content-region layers did on main. Scope the changeset to layers in the drawer's content, or take this block off the header (FR3's scroll lock already holds the page).

Comment on lines +171 to +174
// The area beside the drawer. Stop pans passing through (pull-to-refresh,
// background scroll), but leave pinch to the browser: the open nav covers
// the whole viewport, and pinch-zoom is how phone readers resize text
// (WCAG 1.4.4).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking (INV29): the second sentence runs 30 words over three topics. For example: "Pans here would reach the page, so block them. Leave pinch to the browser. The nav covers the viewport, and pinch is how readers enlarge text."

Comment on lines +703 to +704
// Only close on the overlay beside the drawer (the dialog itself or its
// scrim), not on the drawer or its children.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking (INV29): this restates the condition below it. Drop it, or keep only a reason the code cannot show.

…hanges

Per review: the header declared pinch-zoom over the caller's header
content, so a layer rendered there would keep the restriction. FR3's
scroll lock already holds the page, so only the scrim blocks pans now.
The changeset describes the change (no drawer element restricts
touch-action for a layer inside it) rather than a failure Chromium does
not show. Shorter comments.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested. The changeset now describes the change, the header no longer restricts touch-action, and every MobileNav browser and unit test passes; only the title is left.

  • The title says the scrim and header block background pans, but this push removed the header's block (the new spec expects the header to compute auto), so retitle to the change, for example "fix(MobileNav): block background pans only on the scrim, so no drawer element restricts touch-action for a layer inside it".
  • Miss: the title's "so a layer in the drawer keeps its own touch scrolling" still claims the restoration the changeset dropped, which nothing in the pull request reproduces (in Chromium the Nested Dialog story's results scroll the same on main and here), and the same retitle fixes it.

The description's User impact still points to a phone check "below" that is not attached. CI has not run on this draft.

[Automated review]

@cixzhang cixzhang changed the title fix(MobileNav): block background pans on the scrim and header, so a layer in the drawer keeps its own touch scrolling fix(MobileNav): block background pans only on the scrim, so no drawer element restricts touch-action for a layer inside it Nov 5, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

The title now matches the change, the scrim keeps tap-to-close and pinch zoom working, and every MobileNav browser and unit test passes. One optional fix: the Testing section says the new spec "checks three things" and then lists four.

[Automated review]

@cixzhang cixzhang closed this Nov 6, 2026
@cixzhang
cixzhang deleted the l1/appshell-drawer-touch branch November 6, 2026 03:39

This branch was successfully deployed

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

1 participant