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