Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .changeset/bottom-sheet-switcher-active-sheets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@astryxdesign/core': patch
---

[feat] Add the BottomSheetSwitcher ordered-path API (`activeSheets`).

`BottomSheetSwitcher` now accepts a controlled `activeSheets` list — the
bottom-to-top path of open sheet ids — plus `onActiveSheetsChange(nextIds,
{reason, dismissedSheetId})` and `finalFocusRef`. Appending an id pushes a
drill-in sheet above the current one (which stays mounted, inert, and visually
receded), removing the final id pops back and restores that sheet's recorded
focus, replacing a suffix changes branch, and `[]` closes the flow. Only the
top sheet is interactive; implicit dismissal (Escape, modal-scrim click, or
swipe, per the top sheet's `purpose`) requests the path without its final id.
Invalid, repeated, unknown, or ambiguously registered ids present the longest
valid prefix and warn in development.

The change is additive: the released singular `activeSheet` /
`onActiveSheetChange` pair keeps its exact shipped behavior as a length ≤ 1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Singular ids now go through the prefix rule: an activeSheet that no mounted sheet registers used to open the dialog and now stays closed with a warning naming activeSheets[0]. Either keep the shipped singular handling or describe the change here.

projection of the list, `hasScrim` keeps its released meaning at every path
length, and no exports change.

@imdreamrunner
109 changes: 106 additions & 3 deletions apps/storybook/stories/BottomSheetSwitcher.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,15 @@ interface NotificationOverviewSheetProps {
height: NotificationSheetHeight;
onCancel: () => void;
onContinue: () => void;
/** Pushes a stacked help sheet above this step (activeSheets flows). */
onHelp?: () => void;
}

function NotificationOverviewSheet({
height,
onCancel,
onContinue,
onHelp,
}: NotificationOverviewSheetProps) {
return (
<BottomSheet
Expand Down Expand Up @@ -88,6 +91,13 @@ function NotificationOverviewSheet({
</Text>
</VStack>
</VStack>
{onHelp != null && (
<Button
label="How do notifications work?"
variant="secondary"
onClick={onHelp}
/>
)}
<HStack gap={2} hAlign="end">
<Button label="Cancel" variant="secondary" onClick={onCancel} />
<Button label="Continue" onClick={onContinue} />
Expand Down Expand Up @@ -248,25 +258,118 @@ const openFlow: NonNullable<Story['play']> = async ({canvasElement}) => {
}
};

/**
* The unified flow on the ordered `activeSheets` path: the same three setup
* steps as the singular flow (each step replaces the last), plus a stacked
* help sheet pushed above step 1 — the covered step stays mounted and recedes
* behind it, and Back pops one level.
*/
function MultiStepPathExample({
initialSheets,
}: {
initialSheets: ReadonlyArray<string>;
}) {
const [activeSheets, setActiveSheets] =
useState<ReadonlyArray<string>>(initialSheets);

return (
<>
<Button
label="Set up notifications"
onClick={() => setActiveSheets(['overview'])}
/>
<BottomSheetSwitcher
activeSheets={activeSheets}
onActiveSheetsChange={setActiveSheets}>
{/* Hug-height help content sized to closely match the first step's
natural height, so the covered step's recede reads clearly behind
it (prototype parity pending spec:AST-044 OQ5). */}
<NotificationOverviewSheet
height="hug"
onCancel={() => setActiveSheets([])}
onContinue={() => setActiveSheets(['frequency'])}
onHelp={() => setActiveSheets(['overview', 'help'])}
/>
<BottomSheet sheetId="help" label="How notifications work" height="hug">
<Section padding={4}>
<VStack gap={4}>
<VStack gap={1}>
<Heading level={3}>How notifications work</Heading>
<Text type="supporting" color="secondary">
Stacked above step 1
</Text>
</VStack>
<Divider />
<Text type="supporting" color="secondary">
This sheet is stacked on the ordered path: the first step stays
mounted and recedes behind it, keeping your place.
</Text>
<VStack gap={3}>
<VStack gap={1}>
<Text type="label">Back returns one level</Text>
<Text type="supporting" color="secondary">
Popping the path reveals the step below with its state and
focus intact.
</Text>
</VStack>
<VStack gap={1}>
<Text type="label">Private by default</Text>
<Text type="supporting" color="secondary">
Notifications never share your activity with other people.
</Text>
</VStack>
</VStack>
<HStack gap={2} hAlign="end">
<Button
label="Back"
onClick={() =>
setActiveSheets(current => current.slice(0, -1))
}
/>
</HStack>
</VStack>
</Section>
</BottomSheet>
<NotificationFrequencySheet
height="hug"
onBack={() => setActiveSheets(['overview'])}
onContinue={() => setActiveSheets(['channels'])}
/>
<NotificationChannelsSheet
height="hug"
onBack={() => setActiveSheets(['frequency'])}
onFinish={() => setActiveSheets([])}
/>
</BottomSheetSwitcher>
</>
);
}

export const MultiStepStackedHelp: Story = {
name: 'Multi-step + Stacked Help (activeSheets)',
render: () => <MultiStepPathExample initialSheets={['overview', 'help']} />,
};

export const HugContent: Story = {
name: 'Hug content',
name: 'Legacy Singular (activeSheet) — Hug content',
render: () => <MultiStepSwitcherExample height="hug" />,
play: openFlow,
};

export const Capped: Story = {
name: 'Legacy Singular (activeSheet) — Capped',
render: () => <MultiStepSwitcherExample height="capped" />,
play: openFlow,
};

export const NoScrim: Story = {
name: 'No scrim',
name: 'Legacy Singular (activeSheet) — No scrim',
render: () => <MultiStepSwitcherExample height="hug" hasScrim={false} />,
play: openFlow,
};

export const NarrowViewport: Story = {
name: 'Narrow viewport',
name: 'Legacy Singular (activeSheet) — Narrow viewport',
parameters: {viewport: {defaultViewport: 'mobile1'}},
render: () => <MultiStepSwitcherExample height="hug" />,
play: openFlow,
Expand Down
8 changes: 5 additions & 3 deletions internal/shadcn-registry/routes.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -1292,9 +1292,11 @@
"aliases": []
},
{
"name": "example-bottom-sheet-switcher-review-flow",
"path": "examples/bottom-sheet-switcher/review-flow",
"aliases": []
"name": "example-bottom-sheet-switcher-multi-step-flow-singular-form",
"path": "examples/bottom-sheet-switcher/multi-step-flow-singular-form",
"aliases": [
"examples/bottom-sheet-switcher/review-flow"
]
},
{
"name": "example-bottom-sheet-height-variants",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@
export const doc = {
type: 'block',
exampleFor: 'BottomSheetSwitcher',
name: 'Bottom Sheet Switcher — Review flow',
displayName: 'Bottom Sheet Switcher — Review flow',
name: 'Bottom Sheet Switcher — Multi-step flow (singular form)',
displayName: 'Bottom Sheet Switcher — Multi-step flow (singular form)',
registry: {aliases: ['bottom-sheet-switcher/review-flow']},
description:
'A two-step form flow that lets a person review settings, confirm them, or move back without replacing the shared dialog.',
'A three-step flow on the released singular activeSheet form: each step replaces the previous sheet inside one shared dialog, with no stacking.',
isReady: true,
isShowcase: false,
aspectRatio: 3 / 4,
componentsUsed: [
'BottomSheet',
'BottomSheetSwitcher',
'Button',
'CheckboxInput',
'Divider',
'Heading',
'RadioList',
'Section',
'Stack',
'Text',
Expand Down
Loading
Loading