Skip to content

fix(core): audit BottomSheetSwitcher - #6184

Merged
cixzhang merged 7 commits into
mainfrom
night-watch-component-audit/BottomSheetSwitcher/2026-09-09
Sep 10, 2026
Merged

fix(core): audit BottomSheetSwitcher#6184
cixzhang merged 7 commits into
mainfrom
night-watch-component-audit/BottomSheetSwitcher/2026-09-09

Conversation

@cixzhang

@cixzhang cixzhang commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Why

core/BottomSheetSwitcher was the first eligible component in the refreshed Night Watch queue. The audit found that a nested layer inside a non-modal switcher could not own Escape. Independent exact-head review then caught two related compatibility regressions: modal switchers disappeared from the released hasActiveFocusTrapEscape() signal, and a consumer onKeyDown using stopPropagation() could strand a non-modal flow.

What changed

  • joins modal and non-modal switcher flows to the shared layer-dismissal stack and gives descendants an explicit depth scope;
  • preserves the historical modal focus-trap compatibility signal without registering a second dismissal owner;
  • routes unprevented local Escape through the shared owner after consumer handlers, so stopPropagation() does not strand the sheet while preventDefault() still cancels dismissal;
  • adds red-first public-seam coverage for nested ordering, lower platform-close rejection, compatibility signaling, and BaseProps event collisions;
  • adds the missing schema-v3 observational component contract and closes consumer-doc, Storybook, narrow-viewport, and RTL evidence gaps.

No public prop, export, default, or compatibility promise changes. The separate non-modal top-layer migration remains out of scope.

Audit result

Sixteen exact-head Chromium frames

Retained findings

  • BLOCK — B14 / AST-027: hasScrim={false} still uses dialog.show() plus page-level z-index: 1000, so clipping ancestors or unrelated stacking contexts can make the sheet unreachable. The required Layer/top-layer migration is broader and potentially compatibility-affecting.
  • BLOCK — C3: switcher activation still moves focus from an Effect in the shared BottomSheet item path. A safe replacement must preserve controlled handoffs and needs ownership beyond this focused audit.
  • FIX / manual gaps: the switcher-owned scrim has no public theming target, the inline handoff state machine lacks a focused boundary, and safe behavior for non-matching/duplicate sheetId values is unsettled.
  • Not a finding: current AST-002/FR16 permits one semantic input such as hasScrim to derive cohesive modality, focus, scroll, backdrop, and hosting outcomes. Separating them remains an unscored future API question.

Shared/systemic observations are not scored twice: BottomSheetPanel's drag pill measures below 3:1 in the audited themes, and Matcha supporting text measures 4.41:1 under the existing theme-token contrast class.

Validation

  • Red-first public seam: 4 expected failures across the original and independent-review baselines, then 4 passes after.
  • Exact-head focused BottomSheetSwitcher, compatibility-shim, and layer-stack tests: 87 passed.
  • Exact-head component axe: 4 opened stories, 0 violations.
  • Exact-head curated RTL: 1 verified N/A, 0 gaps, 0 stale entries.
  • Exact-head lint, build, knowledge, sync, changeset, use-client, docs-type, Storybook-type, docsite generate/test, Storybook build, and browser checks pass. The new review-flow example is registered in the current ShadCN route lock.
  • 16 exact-head Chromium frames cover modal/no-scrim, hug/capped, three steps, handoff, light/dark, Matcha, RTL, 320px, and reduced motion; every sensor receipt is bound to 745876d….
  • GitHub CI at 745876d…: all automated checks pass, including docsite-test, test, build, pr-a11y, pr-rtl, and Stable visual regression. Independent approval and the manual acceptance contexts remain pending.

Reconciliation needed

  • #5813 touches the same controller and still calls Switcher dismissal adoption partial. Its rebase must preserve BottomSheetStackContext, the switcher's one shared-stack registration, LayerDepthProvider, local Escape routing, and compatibility signaling.
  • #6042 is draft Stack-only authority, but its AST-026 draft still says the Switcher adoption gap remains. That sentence will be stale after this fix; this audit does not project any other Stack decision onto Switcher.

Exact-head audit eligibility report

{
  "schemaVersion": 1,
  "component": "BottomSheetSwitcher",
  "package": "core",
  "auditMode": "N",
  "rubricVersion": "1.16.2",
  "heads": {
    "repository": "745876d581fec65828e959dd79d42ec16ed1d2e4",
    "componentContract": "745876d581fec65828e959dd79d42ec16ed1d2e4"
  },
  "inventory": {
    "closed": true,
    "gaps": ["B14", "C3", "T7", "C22", "manual:sheetId-invalidity"]
  },
  "unresolvedGaps": {
    "objective": [
      "B14: non-modal dialog remains vulnerable to ancestor clipping and unrelated stacking contexts",
      "C3: switcher item focus is still moved from an Effect",
      "T7: the switcher-owned scrim has no public theming target",
      "C22: the handoff state machine has no focused hook or utility boundary"
    ],
    "manual": [
      "Safe behavior for non-matching or duplicate sheetId values is unsettled",
      "Separating hasScrim-derived outcomes is an optional future API direction, not a current violation",
      "PR #5813 must preserve shared dismissal registration and compatibility behavior when reconciling its controller changes",
      "PR #6042 contains a draft statement that becomes stale after this fix"
    ]
  },
  "remediations": [
    {
      "ruleId": "A4",
      "beforeEvidence": [
        "https://github.com/facebook/astryx/blob/c29c48f3ef437ebcdeeda43d420796db9e318171/pr-6184/BottomSheetSwitcher/before/focused-tests-red.txt"
      ],
      "afterEvidence": [
        "https://github.com/facebook/astryx/blob/c29c48f3ef437ebcdeeda43d420796db9e318171/pr-6184/BottomSheetSwitcher/after/focused-tests.txt"
      ]
    },
    {
      "ruleId": "architecture:public-component-api/INV5/INV9",
      "beforeEvidence": [
        "https://github.com/facebook/astryx/blob/c29c48f3ef437ebcdeeda43d420796db9e318171/pr-6184/BottomSheetSwitcher/review-before/focused-tests-red-1.16.2.txt"
      ],
      "afterEvidence": [
        "https://github.com/facebook/astryx/blob/c29c48f3ef437ebcdeeda43d420796db9e318171/pr-6184/BottomSheetSwitcher/review-after/focused-tests-1.16.2.txt"
      ]
    },
    {
      "ruleId": "P2/P28/B15",
      "beforeEvidence": [
        "https://github.com/facebook/astryx/blob/c29c48f3ef437ebcdeeda43d420796db9e318171/pr-6184/BottomSheetSwitcher/review-before/focused-tests-red-1.16.2.txt"
      ],
      "afterEvidence": [
        "https://github.com/facebook/astryx/blob/c29c48f3ef437ebcdeeda43d420796db9e318171/pr-6184/BottomSheetSwitcher/review-after/focused-tests-1.16.2.txt"
      ]
    },
    {
      "ruleId": "X3/X4/X5b/X7/X10/X14/X16",
      "beforeEvidence": [
        "https://github.com/facebook/astryx/blob/c29c48f3ef437ebcdeeda43d420796db9e318171/pr-6184/BottomSheetSwitcher/README.md#objective-remediations"
      ],
      "afterEvidence": [
        "https://github.com/facebook/astryx/blob/745876d581fec65828e959dd79d42ec16ed1d2e4/packages/core/src/BottomSheet/BottomSheetSwitcher.doc.mjs",
        "https://github.com/facebook/astryx/blob/745876d581fec65828e959dd79d42ec16ed1d2e4/packages/cli/assets/templates/blocks/components/BottomSheet/BottomSheetSwitcherReviewFlow.tsx"
      ]
    },
    {
      "ruleId": "I17",
      "beforeEvidence": [
        "https://github.com/facebook/astryx/blob/c29c48f3ef437ebcdeeda43d420796db9e318171/pr-6184/BottomSheetSwitcher/before/rtl-audit.txt"
      ],
      "afterEvidence": [
        "https://github.com/facebook/astryx/blob/c29c48f3ef437ebcdeeda43d420796db9e318171/pr-6184/BottomSheetSwitcher/after/rtl-audit-report.json"
      ]
    }
  ],
  "approvals": [
    {"name": "spec-owner-approval", "state": "passed"},
    {"name": "visual-acceptance", "state": "passed"},
    {"name": "independent-review", "state": "passed"}
  ],
  "checks": [
    {"name": "exact-head-local-focused", "state": "passed"},
    {"name": "exact-head-component-axe", "state": "passed"},
    {"name": "exact-head-rtl", "state": "passed"},
    {"name": "exact-head-chromium-receipts", "state": "passed"},
    {"name": "github-ci", "state": "passed"},
    {"name": "audit-eligibility", "state": "absent"}
  ],
  "eligibility": {
    "eligible": false,
    "reasons": [
      "spec:AST-029 is accepted rather than shipped",
      "the trusted audit-eligibility validator/status is not shipped",
      "two retained BLOCKs remain: B14 and C3",
      "hosting, focus-lifecycle, and identifier behavior still require human ownership"
    ]
  }
}

Auto-merge was never enabled. This PR merged manually after independent approval; the final tracker row records the landed squash commit.

@vercel

vercel Bot commented Sep 9, 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 Sep 10, 2026 3:34pm UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Sep 9, 2026
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Sep 9, 2026
@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Sep 9, 2026
github-actions Bot added a commit that referenced this pull request Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

Modified Components

BottomSheet (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 3186 -
Complexity N/A Very High (258) -
Layer (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 1374 -
Complexity N/A Very High (100) -

Bundle Size Summary

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

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

Status: No visual change across 4 compared shot(s).


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@cixzhang cixzhang left a comment

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.

Semantic verdict: request changes

Builders who inspect active Escape traps or contain key events now get stale state or a non-modal sheet that no longer closes.

  • Keep an open modal switcher visible through hasActiveFocusTrapEscape() until that public shim is removed through an explicit compatibility change.
  • Preserve built-in non-modal Escape dismissal when onKeyDown only calls stopPropagation(); preventDefault() should remain the cancellation path.

The new child-first ordering itself works: Chromium closes the nested layer first and the switcher on the next Escape. Focused A/B tests reproduce both regressions against current main.

[Reviewed by Robohands]

@cixzhang cixzhang left a comment

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.

Semantic verdict: request changes

Builders coordinating Escape behavior still hit two regressions at this head:

The intended nested ordering works in Chromium: the first Escape closes the child and the second closes the host. Please preserve both released behaviors while keeping that order.

[Reviewed by Robohands]

@cixzhang cixzhang left a comment

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.

Semantic verdict: request changes

The main-only merge leaves both earlier regressions at this head. In Chromium:

  • A modal switcher reports false through the exported hasActiveFocusTrapEscape() compatibility shim while its focus trap is active.
  • For a non-modal switcher, an onKeyDown handler that only calls stopPropagation() now keeps the sheet open; before this change, only preventDefault() cancelled built-in dismissal.

The intended ordering works: a nested tooltip closes on the first Escape and the switcher on the second. Please preserve both released behaviors while keeping that child-first order.

[Reviewed by Robohands]

@astracat-bot

astracat-bot Bot commented Sep 9, 2026

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
Queued (for maintainers only) Sep 10, 2026, 3:14 PM UTC

@cixzhang

Copy link
Copy Markdown
Contributor Author

/approve-spec 745876d

@github-actions github-actions Bot removed the needs:spec-owner-review Current knowledge records await owner approval label Sep 10, 2026
@cixzhang
cixzhang merged commit 65dedfe into main Sep 10, 2026
26 of 27 checks passed
@cixzhang
cixzhang deleted the night-watch-component-audit/BottomSheetSwitcher/2026-09-09 branch September 10, 2026 15:59
github-actions Bot added a commit that referenced this pull request Sep 10, 2026
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