Skip to content

feat(core): prototype BottomSheetStack - #5813

Draft
imdreamrunner wants to merge 1 commit into
mainfrom
bottom-sheet-stack-prototype
Draft

imdreamrunner wants to merge 1 commit into
mainfrom
bottom-sheet-stack-prototype

Conversation

@imdreamrunner

Copy link
Copy Markdown
Contributor

Summary

This is a working prototype for API review of BottomSheetStack.

  • adds a controlled ordered-stack API beside BottomSheetSwitcher
  • keeps one shared native dialog, scrim, focus boundary, dismissal owner, and scroll lock
  • keeps covered sheets mounted but inert and accessibility-hidden
  • scales/translates the nearest two covered sheets while the top sheet enters or exits
  • dismisses only the top sheet on Escape, scrim click, or swipe
  • preserves focus within a covered sheet and restores it when that sheet becomes active again

Proposed API

const [openSheetIds, setOpenSheetIds] =
  useState<ReadonlyArray<string>>([]);

const push = (sheetId: string) =>
  setOpenSheetIds(current => [...current, sheetId]);
const pop = () =>
  setOpenSheetIds(current => current.slice(0, -1));

<BottomSheetStack
  openSheetIds={openSheetIds}
  onOpenSheetIdsChange={setOpenSheetIds}>
  <BottomSheet sheetId="filters" label="Filters">
    <Filters />
    <Button label="Show details" onClick={() => push('details')} />
  </BottomSheet>
  <BottomSheet sheetId="details" label="Filter details">
    <FilterDetails />
    <Button label="Back" onClick={pop} />
  </BottomSheet>
</BottomSheetStack>

openSheetIds is ordered bottom-to-top. An empty array closes the stack. BottomSheetSwitcher remains the mutually-exclusive replacement-step API; it is not widened to accept arrays.

Design choices

  • The ordered controlled array is the source of truth; no imperative push() / pop() API and no hidden ordering inferred from independent child booleans.
  • BottomSheet keeps its existing sheetId, content, height, snap-point, and purpose props.
  • Stacking motion is opinionated in the first API rather than exposing Silk-style arbitrary stackingAnimation functions.
  • Append and single suffix-pop receive the full transition. Other controlled array changes converge to the requested state without promising bespoke motion.

Validation

  • pnpm lint:strict
  • pnpm build
  • pnpm check:repo
  • Core, Core docs, CLI strict/template docs, and Storybook typechecks
  • BottomSheet suite: 211 tests passed
  • Storybook production build
  • Real Chromium check at 390×844: two levels measured 0.96 / -8px; three levels measured 0.92 / -16px, 0.96 / -8px, 1 / 0px; only the top layer was interactive
  • Full pnpm test was also run. Its six failures reproduce on current main: two ContextMenu BottomSheet tests, two known story-tree failures, and two discovery timeouts that pass in isolation.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Sep 1, 2026
@vercel

vercel Bot commented Sep 1, 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 1, 2026 5:14am UTC

Request Review

@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Sep 1, 2026
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Sep 1, 2026
github-actions Bot added a commit that referenced this pull request Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

PR Analysis Report

Preview availability: Storybook and Sandbox were not published for this CI run.

Modified Components

BottomSheet (@astryxdesign/core)
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 3922 -
Complexity N/A Very High (376) -

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

194 added · 0 removed. View the report

A repository maintainer can accept these exact frames: /accept-visual 33472630735/1 <why every changed frame is correct>

Added — After
Added — After visual regression frame

Added — After
Added — After visual regression frame

Added — After
Added — After visual regression frame


Generated by PR Enrichment workflow | View full report

@astracat-bot

astracat-bot Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Oct 20, 2026, 12:43 AM 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.

Approval waits on a maintainer providing or identifying the governing spec for stacked bottom sheets: whether Core adopts this separate BottomSheetStack, with its controlled openSheetIds list and fixed receding motion, or one of the AST-044 options in #6042, #6728, #7503 and #7648. Whatever that decision, two defects need fixing: the non-modal hasScrim={false} shell orders itself with z-index: 1000, which AST-027 DEC-3 rules out in favor of the browser top layer, and a listed top sheet that unmounts leaves an unnamed, scroll-locked modal with nothing operable. The branch also needs a rebase onto main, where it conflicts in the overlay-dismissal family record and three BottomSheet files, and the stack's push and pop detection should not depend on a ref that rendering reads but only an effect updates.

[Automated review]

},
dialogNonModal: {
pointerEvents: 'none',
zIndex: 1000,

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 non-modal shell orders the whole stack against the page with z-index: 1000 plus show(). AST-027 DEC-3 says a dialog's cross-surface order "comes from the browser host and Layer contract, not a number", and it rejects "a large z-index as a substitute for Layer". Could the first version ship modal-only, or route the non-modal mode through Layer and the native top layer?

Comment on lines +231 to +232
const isStackVisible =
topSheet != null || visibleTransition.exitingSheet != null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

isStackVisible counts the last listed id even when no BottomSheet with that id is mounted. If the top sheet unmounts while its id stays listed, the dialog stays modal and scroll-locked, loses its accessible name, and only an inert covered sheet remains. BottomSheetSwitcher guards this case with unmountedSheetIds; the same guard here, plus a development warning, would close or recover the stack.

if (stacksEqual(previousSheets, openSheetIds)) {
return;
}
committedOpenSheetIdsRef.current = [...openSheetIds];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

After mount, committedOpenSheetIdsRef is updated only here, yet render reads it (lines 220–228) to choose every sheet's phase, inert and aria-hidden. react-component-runtime INV4 says a ref mutation must not be the sole source for rendered output. Keeping the previous stack in state and adjusting it during render would also remove this state-setting effect.

This branch was successfully deployed

1 active deployment
Preview — 841aefde Deployed Sep 1, 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 needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant