Skip to content

fix(BottomSheet): focus returned after a pointer close gets no focus ring - #7388

Merged
cixzhang merged 1 commit into
meta:mainfrom
vjeux:sheet-focus-return-last-input
Oct 13, 2026
Merged

cixzhang merged 1 commit into
meta:mainfrom
vjeux:sheet-focus-return-last-input

Conversation

@vjeux

@vjeux vjeux commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

User impact

After a pointer close, the control a BottomSheet returns focus to can show the focus ring, as if on a keyboard: in iOS Safari after a tap pick from a DropdownMenu bottom-sheet presentation whose trigger the caller renders (renderTrigger), and in Chromium when a keyboard-opened sheet is closed with the mouse.

Problem and solution fit

  • Why it occurs: when the exit motion ends, the sheet closes its native dialog, which restores focus to the opener, then calls a plain focus() on finalFocusRef or the opener. Nothing consults the last input: WebKit rings the restored focus after a tap, Chromium carries the ring over from a keyboard-focused row, and focusing the already-focused element changes nothing.
  • Why it harms the task: it signals keyboard focus right after a pointer.
  • Why it matters: every adaptive overlay presented as a sheet (DropdownMenu, Selector, MultiSelector) returns focus through this code. Their built-in triggers hide the ring with a style; a caller-rendered trigger or any other target cannot.

Expected behavior and authority

Smallest restoration

Evidence

  • Reproduction before the change: the new browser spec on a main build plus the story fails its keyboard-open, pointer-pick case in Chromium (:focus-visible true). iOS 26.2 Simulator, real Mobile Safari, same story, a tap on the trigger then on "Edit project": on main the trigger is focused, :focus-visible, outline: solid 2px (2 of 2, screenshots match).
  • Result after the change: the spec passes; on the simulator the trigger is focused, not :focus-visible, outline: none (2 of 2).
  • Representative unchanged path: a keyboard open closed by Escape keeps the ring (spec, unit); existing Dialog and BottomSheet focus tests pass.
  • Regression test or other mutation-sensitive proof: four unit cases (pointer close, explicit target, a ring already drawn cleared first, keyboard close unchanged); the three pointer cases fail on main. No pull-request workflow runs the browser spec; release checks run its Chromium half.

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

pnpm exec vitest run packages/core/src/BottomSheet/BottomSheet.test.tsx -t "focus restore"
  main's BottomSheet.tsx: 3 failed, 3 passed · with the change: 6 passed

pnpm exec vitest run packages/core/src/BottomSheet packages/core/src/DropdownMenu packages/core/src/Dialog \
  packages/core/src/Selector packages/core/src/MultiSelector packages/core/src/Layer packages/core/src/utils \
  packages/core/src/docPropLiterals.test.ts packages/core/src/docPropReferences.test.ts
  Test Files  60 passed (60)   Tests  1745 passed (1745)

pnpm storybook:build
ASTRYX_STORYBOOK_DIR=<main + story build> pnpm exec playwright test BottomSheetFocusReturn.a11y.browser.spec.ts --project=chromium
  main: 1 failed (keyboard open, pointer pick: :focus-visible true), 2 passed
pnpm exec playwright test BottomSheetFocusReturn.a11y.browser.spec.ts BottomSheetKeyboard.a11y.browser.spec.ts \
  BottomSheetDrag.a11y.chromium.spec.ts Dialog.a11y.browser.spec.ts MenuHeaderFooter.a11y.chromium.spec.ts --project=chromium
  45 passed   (the config's WebKit project would not launch on my machine)

iOS 26.2 Simulator (iPhone 17 Pro), Mobile Safari, real taps, Storybook builds of main + story and of this branch:
  tap the trigger, wait for the sheet, tap "Edit project", read the trigger
  main:   focused, :focus-visible true,  outline solid 2px   (2 of 2 runs)
  branch: focused, :focus-visible false, outline none        (2 of 2 runs)

pnpm -F @astryxdesign/core typecheck, pnpm lint:strict, pnpm check:knowledge   ✓

…ring

When its exit motion ends, the standalone BottomSheet closes its native
dialog, which restores focus to the opener, then calls a plain focus() on
finalFocusRef or the opener. Nothing consults the last input, so WebKit
rings the returned focus after a tap and Chromium carries a keyboard ring
over to it after a mouse close. A DropdownMenu bottom-sheet trigger the
caller renders shows it, since only built-in triggers hide it with a style.

The sheet now turns on the shared modality tracking and returns focus with
focusForLastInput: after pointer input it asks for focusVisible: false,
clearing a ring the native close already drew; after keyboard input it
focuses as before. The helper is Dialog's, moved into utils/focusReturn.ts
with the same hunks as meta#7382, so the two merge in either order.

Tests: four unit cases (three fail on main), a cross-browser spec on a new
DropdownMenu story (custom trigger, bottom-sheet presentation).
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 12, 2026
@astracat-bot

astracat-bot Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 12, 2026, 6:05 PM UTC

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

This fix looks correct and complete. A bottom sheet now returns focus with a focus ring only after keyboard input, as the interaction-modality rule (INV1) asks, and the keyboard path and focus destination are unchanged. The new unit cases fail without the fix and pass with it, and the new browser spec passes in Chromium and WebKit: the returned trigger no longer matches :focus-visible after a pointer pick, and an Escape close still keeps the ring. Nothing else is needed from you.

[Automated review]

@github-actions

Copy link
Copy Markdown
Contributor

PR Analysis Report

Preview availability: Storybook and Sandbox are not both ready on the exact-head Vercel preview.

No new or modified components detected.

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.9KB 1.2KB

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

44 of 448 shot(s) changed.

A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.

component story theme mode pixels
Selector Indicator Space Bottom Sheet Narrow End RTL neutral light 754,077
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,188
Chat Full AI Chat probe light 238,081
Chat Full AI Chat probe dark 237,048
Markdown Default probe light 159,625
Markdown Default probe dark 158,986
Collapsible Dividers probe dark 66,445
Collapsible Dividers probe light 66,445
Chat Default neutral dark 33,205
Chat Default neutral light 32,876
Chat Empty State probe light 23,531
Chat Empty State probe dark 22,622
ChatAutoScroll Scroll Behavior Comparison neutral dark 15,378
ChatAutoScroll Scroll Behavior Comparison neutral light 14,107
Chat With Attachments probe light 13,406
Chat With Attachments probe dark 11,232
ProgressBar With Target Mark probe dark 7,895
ProgressBar With Target Mark probe light 7,895
TableRowExpansion Detail Panel neutral dark 5,150
TableRowExpansion Detail Panel neutral light 4,068

and 24 more.


Generated by PR Enrichment workflow | View full report

@vercel

vercel Bot commented Oct 12, 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 Oct 12, 2026 6:28pm UTC

Request Review

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

CodeBunny approved at this head; CI green. Approved.

@cixzhang
cixzhang merged commit d6b5c78 into meta:main Oct 13, 2026
43 checks passed
@vjeux
vjeux deleted the sheet-focus-return-last-input branch October 13, 2026 01:51

This branch was successfully deployed

1 active deployment
Preview — abc9b962 Deployed Oct 12, 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.

3 participants