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:
165
src/components/atoms/Collapse/Collapse.stories.tsx
Normal file
165
src/components/atoms/Collapse/Collapse.stories.tsx
Normal 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>
|
||||
),
|
||||
};
|
||||
43
src/components/atoms/Collapse/Collapse.tsx
Normal file
43
src/components/atoms/Collapse/Collapse.tsx
Normal 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;
|
||||
2
src/components/atoms/Collapse/index.ts
Normal file
2
src/components/atoms/Collapse/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './Collapse';
|
||||
export * from './Collapse';
|
||||
Reference in New Issue
Block a user