Add Collapse atom for progressive disclosure

Thin MUI Collapse wrapper with unmountOnExit default. Used in the
arrangement wizard to reveal fields after a selection is made
(slide-down animation). Stories include interactive toggle and
wizard field-reveal pattern demo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 14:16:55 +11:00
parent 43f0360252
commit 41efb78335
3 changed files with 210 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { Collapse } from './Collapse';
import Box from '@mui/material/Box';
import { Typography } from '../Typography';
import { Button } from '../Button';
const meta: Meta<typeof Collapse> = {
title: 'Atoms/Collapse',
component: Collapse,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
argTypes: {
in: {
control: 'boolean',
description: 'Whether the content is expanded',
},
timeout: {
control: 'number',
description: 'Transition duration in ms (or { enter, exit })',
table: { defaultValue: { summary: '300' } },
},
},
};
export default meta;
type Story = StoryObj<typeof Collapse>;
// ─── Default (controlled via args) ──────────────────────────────────────────
/** Toggle the `in` control to expand/collapse */
export const Default: Story = {
args: {
in: true,
},
render: (args) => (
<Box sx={{ width: 400 }}>
<Collapse {...args}>
<Box
sx={{
p: 3,
bgcolor: 'var(--fa-color-brand-50)',
borderRadius: 1,
border: '1px solid',
borderColor: 'divider',
}}
>
<Typography variant="body1">
This content is revealed with a smooth slide-down animation.
</Typography>
</Box>
</Collapse>
</Box>
),
};
// ─── Interactive toggle ─────────────────────────────────────────────────────
/** Click the button to toggle progressive disclosure */
export const Interactive: Story = {
render: () => {
const [open, setOpen] = useState(false);
return (
<Box sx={{ width: 400 }}>
<Button variant="soft" color="secondary" onClick={() => setOpen(!open)} sx={{ mb: 2 }}>
{open ? 'Hide details' : 'Show details'}
</Button>
<Collapse in={open}>
<Box
sx={{
p: 3,
bgcolor: 'var(--fa-color-brand-50)',
borderRadius: 1,
border: '1px solid',
borderColor: 'divider',
}}
>
<Typography variant="body1" sx={{ mb: 1 }}>
Additional details are revealed here.
</Typography>
<Typography variant="body2" color="text.secondary">
This pattern is used in the wizard for progressive disclosure fields appear after a
previous selection is made.
</Typography>
</Box>
</Collapse>
</Box>
);
},
};
// ─── Wizard field reveal ────────────────────────────────────────────────────
/** Simulates the wizard pattern: selecting an option reveals the next field */
export const WizardFieldReveal: Story = {
render: () => {
const [step, setStep] = useState(0);
return (
<Box sx={{ width: 400 }}>
<Typography variant="label" sx={{ mb: 1, display: 'block' }}>
Who is this funeral being arranged for?
</Typography>
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
<Button
variant={step >= 1 ? 'contained' : 'soft'}
color={step >= 1 ? 'primary' : 'secondary'}
size="large"
fullWidth
onClick={() => setStep(1)}
>
Myself
</Button>
<Button
variant={step >= 2 ? 'contained' : 'soft'}
color={step >= 2 ? 'primary' : 'secondary'}
size="large"
fullWidth
onClick={() => setStep(2)}
>
Someone else
</Button>
</Box>
<Collapse in={step >= 2}>
<Box sx={{ mt: 1 }}>
<Typography variant="label" sx={{ mb: 1, display: 'block' }}>
Has the person died?
</Typography>
<Box sx={{ display: 'flex', gap: 2 }}>
<Button variant="soft" color="secondary" size="large" fullWidth>
Yes
</Button>
<Button variant="soft" color="secondary" size="large" fullWidth>
No
</Button>
</Box>
</Box>
</Collapse>
</Box>
);
},
};
// ─── Collapsed ──────────────────────────────────────────────────────────────
/** Content is hidden (collapsed state) */
export const Collapsed: Story = {
args: {
in: false,
},
render: (args) => (
<Box sx={{ width: 400 }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
The content below is collapsed:
</Typography>
<Collapse {...args}>
<Box sx={{ p: 3, bgcolor: 'var(--fa-color-brand-50)', borderRadius: 1 }}>
<Typography variant="body1">You should not see this.</Typography>
</Box>
</Collapse>
</Box>
),
};

View File

@@ -0,0 +1,43 @@
import React from 'react';
import MuiCollapse from '@mui/material/Collapse';
import type { CollapseProps as MuiCollapseProps } from '@mui/material/Collapse';
// ─── Types ───────────────────────────────────────────────────────────────────
/** Props for the FA Collapse component */
export interface CollapseProps extends MuiCollapseProps {
/** Whether the content is expanded */
in: boolean;
/** Content to reveal/hide */
children: React.ReactNode;
}
// ─── Component ───────────────────────────────────────────────────────────────
/**
* Progressive disclosure wrapper for the FA design system.
*
* Thin wrapper around MUI Collapse with sensible defaults for the
* arrangement wizard's progressive disclosure pattern (fields revealed
* after a selection is made).
*
* Uses a smooth slide-down animation. Unmounts children when collapsed
* to keep the DOM clean and prevent focus on hidden fields.
*
* Usage:
* ```tsx
* <Collapse in={hasSelectedOption}>
* <FormField label="Next question" />
* </Collapse>
* ```
*/
export const Collapse = React.forwardRef<HTMLDivElement, CollapseProps>(
({ children, ...props }, ref) => (
<MuiCollapse ref={ref} unmountOnExit {...props}>
{children}
</MuiCollapse>
),
);
Collapse.displayName = 'Collapse';
export default Collapse;

View File

@@ -0,0 +1,2 @@
export { default } from './Collapse';
export * from './Collapse';