Skip to content

Commit a64a858

Browse files
authored
chore(nimbus): create CollapsibleMotion component (#460)
* chore(nimbus): create CollapsibleMotion * chore(nimbus): add changeset * chore(n): why did I do that * chore(nimbus): fix frontmatter * chore(nimbus): refinements * chore(nimbus): create trigger-less story * chore(nimbus): new dialog trigger interpretation strategy * chore(nimbus): use proper design tokens in stories * chore(nimbus): integrate Presence for animation * chore(n): simplify * chore(n): use correct slot type * chore(n): remove note * chroe(n): what was I thinking * chore(n): we don't actually need this * chore(n): mdx edits, fwiw * chore(n): so consumers can go wild
1 parent b195ba5 commit a64a858

14 files changed

+1129
-0
lines changed

.changeset/silly-radios-occur.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@commercetools/nimbus": patch
3+
---
4+
5+
create CollapsibleMotion component
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
---
2+
id: Components-CollapsibleMotion
3+
title: CollapsibleMotion
4+
description: A compound component for creating smooth collapsible content with accessibility support
5+
order: 999
6+
menu:
7+
- Components
8+
- Layout
9+
- CollapsibleMotion
10+
tags: ["component", "collapsible", "accordion", "disclosure", "animation", "accessibility"]
11+
---
12+
13+
# CollapsibleMotion
14+
15+
A modern, accessible implementation of collapsible content with smooth height animations. Built with React Aria for accessibility and Chakra UI for consistent theming.
16+
17+
## Overview
18+
19+
CollapsibleMotion is a compound component that provides smooth, accessible collapsible content with height animations. It combines React Aria's accessibility features with Chakra UI's theming system to deliver a robust foundation for disclosure patterns, accordions, and expandable content sections.
20+
21+
### Key Features
22+
23+
- **Smooth height animations**: Customizable duration with automatic content measurement
24+
- **Full accessibility support**: Proper ARIA attributes and screen reader compatibility
25+
- **Controlled and uncontrolled modes**: Flexible state management options
26+
- **Focus management**: Content visibility handled properly for screen readers
27+
- **Responsive design**: Works seamlessly across all screen sizes
28+
- **Chakra UI integration**: Consistent theming with the design system
29+
30+
### Resources
31+
32+
Deep dive on details and access design library.
33+
34+
- [React ARIA Disclosure Docs](https://react-spectrum.adobe.com/react-aria/useDisclosure.html)
35+
- [ARIA Disclosure Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/)
36+
37+
## Variables
38+
39+
### Basic Usage
40+
41+
The simplest way to use CollapsibleMotion is in uncontrolled mode with default settings:
42+
43+
```jsx-live
44+
const App = () => {
45+
return (
46+
<CollapsibleMotion.Root defaultExpanded={false}>
47+
<Button mb={4}>Toggle Content</Button>
48+
<CollapsibleMotion.Content>
49+
<Box p={4} bg="gray.50" borderRadius="md">
50+
<Text>
51+
This content will smoothly expand and collapse when the button is clicked.
52+
The animation uses the default duration of 200ms.
53+
</Text>
54+
</Box>
55+
</CollapsibleMotion.Content>
56+
</CollapsibleMotion.Root>
57+
);
58+
};
59+
```
60+
61+
### Controlled Mode
62+
63+
For more complex scenarios, you can control the expanded state from a parent component:
64+
65+
```jsx-live
66+
const App = () => {
67+
const [isExpanded, setIsExpanded] = useState(false);
68+
69+
return (
70+
<div>
71+
<Button
72+
onClick={() => setIsExpanded(!isExpanded)}
73+
mb={4}
74+
>
75+
{isExpanded ? 'Collapse' : 'Expand'} Content
76+
</Button>
77+
78+
<CollapsibleMotion.Root
79+
isExpanded={isExpanded}
80+
onExpandedChange={setIsExpanded}
81+
>
82+
<CollapsibleMotion.Content>
83+
<Box p={4} bg="blue.50" borderRadius="md">
84+
<Text>
85+
This content is controlled by the parent component's state.
86+
The parent can react to changes and implement custom logic.
87+
</Text>
88+
</Box>
89+
</CollapsibleMotion.Content>
90+
</CollapsibleMotion.Root>
91+
</div>
92+
);
93+
};
94+
```
95+
96+
### Custom Animation Settings
97+
98+
You can customize the minimum height and animation duration:
99+
100+
```jsx-live
101+
const App = () => {
102+
return (
103+
<CollapsibleMotion.Root defaultExpanded={false}>
104+
<Button mb={4}>Toggle Content</Button>
105+
<CollapsibleMotion.Content
106+
minHeight={40}
107+
animationDuration={500}
108+
>
109+
<Box p={6} bg="green.50" borderRadius="md" minH="150px">
110+
<Text mb={4}>
111+
This content has a minimum height of 40px when collapsed
112+
and uses a slower animation duration of 500ms.
113+
</Text>
114+
<Text>
115+
The minimum height allows for partial visibility of content
116+
even when collapsed, which can be useful for previews.
117+
</Text>
118+
</Box>
119+
</CollapsibleMotion.Content>
120+
</CollapsibleMotion.Root>
121+
);
122+
};
123+
```
124+
125+
### Dynamic Content
126+
127+
CollapsibleMotion automatically handles content that changes size:
128+
129+
```jsx-live
130+
const App = () => {
131+
const [content, setContent] = useState('short');
132+
133+
const contentMap = {
134+
short: "Short content",
135+
long: "This is much longer content that demonstrates how the component automatically adjusts to content changes. The component re-measures the content height when it changes and handles the animation smoothly."
136+
};
137+
138+
return (
139+
<div>
140+
<Box mb={4}>
141+
<Button
142+
onClick={() => setContent('short')}
143+
mr={2}
144+
variant={content === 'short' ? 'solid' : 'outline'}
145+
size="sm"
146+
>
147+
Short
148+
</Button>
149+
<Button
150+
onClick={() => setContent('long')}
151+
variant={content === 'long' ? 'solid' : 'outline'}
152+
size="sm"
153+
>
154+
Long
155+
</Button>
156+
</Box>
157+
158+
<CollapsibleMotion.Root defaultExpanded={true}>
159+
<Button mb={4}>Toggle Dynamic Content</Button>
160+
<CollapsibleMotion.Content>
161+
<Box p={4} bg="orange.50" borderRadius="md">
162+
<Text>{contentMap[content]}</Text>
163+
</Box>
164+
</CollapsibleMotion.Content>
165+
</CollapsibleMotion.Root>
166+
</div>
167+
);
168+
};
169+
```
170+
171+
## Accessibility
172+
173+
CollapsibleMotion follows WAI-ARIA guidelines for disclosure widgets and meets WCAG 2.1 AA standards.
174+
175+
### Keyboard Navigation
176+
177+
| Key | Action |
178+
| ------- | ---------------------------------- |
179+
| `Tab` | Move focus to/from trigger element |
180+
| `Space` | Activate trigger when focused |
181+
| `Enter` | Activate trigger when focused |
182+
183+
### Screen Reader Support
184+
185+
- Automatically applies proper ARIA attributes (`aria-expanded`, `aria-controls`)
186+
- Content is not focusable when collapsed, improving screen reader experience
187+
- Announces state changes when expanding or collapsing
188+
- Proper semantic markup for assistive technologies
189+
190+
### WCAG Compliance
191+
192+
#### 2. Operable
193+
194+
- **2.1.1 Keyboard**: Full keyboard navigation support through standard button interactions
195+
- **2.4.7 Focus Visible**: Clear focus indicators on trigger elements
196+
197+
#### 3. Understandable
198+
199+
- **3.2.1 On Focus**: No unexpected context changes when focusing elements
200+
201+
#### 4. Robust
202+
203+
- **4.1.2 Name, Role, Value**: Proper ARIA attributes (`aria-expanded`, `aria-controls`) communicate state and relationships
204+
205+
```jsx-live
206+
const App = () => {
207+
return (
208+
<CollapsibleMotion.Root defaultExpanded={false}>
209+
<Button mb={4}>
210+
Accessible Toggle Button
211+
</Button>
212+
<CollapsibleMotion.Content>
213+
<Box p={4} bg="teal.50" borderRadius="md">
214+
<Text mb={2} fontWeight="bold">
215+
Accessibility Features:
216+
</Text>
217+
<ul>
218+
<li>Proper ARIA attributes (aria-expanded, aria-controls)</li>
219+
<li>Focus management for screen readers</li>
220+
<li>Keyboard navigation support</li>
221+
<li>Content is not focusable when collapsed</li>
222+
</ul>
223+
</Box>
224+
</CollapsibleMotion.Content>
225+
</CollapsibleMotion.Root>
226+
);
227+
};
228+
```
229+
230+
## Integration with Other Components
231+
232+
CollapsibleMotion works well with other Nimbus components to create rich interactive experiences:
233+
234+
```jsx-live
235+
const App = () => {
236+
const [expandedSections, setExpandedSections] = useState(new Set());
237+
238+
const toggleSection = (section) => {
239+
const newSections = new Set(expandedSections);
240+
if (newSections.has(section)) {
241+
newSections.delete(section);
242+
} else {
243+
newSections.add(section);
244+
}
245+
setExpandedSections(newSections);
246+
};
247+
248+
return (
249+
<Stack spacing={4}>
250+
{['personal', 'billing', 'preferences'].map(section => (
251+
<Box key={section} border="1px solid" borderColor="gray.200" borderRadius="md">
252+
<CollapsibleMotion.Root
253+
isExpanded={expandedSections.has(section)}
254+
onExpandedChange={() => toggleSection(section)}
255+
>
256+
<Button
257+
w="full"
258+
variant="ghost"
259+
justifyContent="space-between"
260+
p={4}
261+
rightIcon={expandedSections.has(section) ? '−' : '+'}
262+
>
263+
{section.charAt(0).toUpperCase() + section.slice(1)} Information
264+
</Button>
265+
266+
<CollapsibleMotion.Content>
267+
<Box p={4} borderTop="1px solid" borderColor="gray.200">
268+
<Text>
269+
Content for {section} section. This demonstrates how
270+
CollapsibleMotion can be integrated with other components
271+
to create accordion-like interfaces.
272+
</Text>
273+
</Box>
274+
</CollapsibleMotion.Content>
275+
</CollapsibleMotion.Root>
276+
</Box>
277+
))}
278+
</Stack>
279+
);
280+
};
281+
```
282+
283+
## Specs
284+
285+
<PropsTable id="CollapsibleMotionRoot" />
286+
287+
<PropsTable id="CollapsibleMotionContent" />
288+
289+
## Best Practices
290+
291+
### Performance
292+
293+
- **Content Measurement**: The component automatically measures content height, but avoid deeply nested or complex layouts within collapsible content when possible
294+
- **Animation Duration**: Use reasonable animation durations (100-500ms) to balance smoothness with perceived performance
295+
- **Dynamic Content**: The component handles content changes efficiently, but minimize frequent content updates during animations
296+
297+
### Accessibility
298+
299+
- **Trigger Elements**: Always provide clear, descriptive trigger buttons or elements
300+
- **Content Structure**: Use semantic HTML within collapsible content
301+
- **Focus Indicators**: Ensure trigger elements have clear focus indicators
302+
- **Screen Reader Testing**: Test with actual screen readers to verify the experience
303+
304+
### Styling
305+
306+
- **Consistent Animations**: Use consistent animation durations across your application
307+
- **Visual Feedback**: Provide clear visual feedback for expanded/collapsed states
308+
- **Responsive Design**: Consider how collapsible content behaves on different screen sizes
309+
310+
## Related Components
311+
312+
- **Accordion**: For managing multiple collapsible sections
313+
- **Disclosure**: For simpler show/hide functionality without animations
314+
- **Modal**: For larger content that should overlay the page
315+
- **Drawer**: For slide-out content panels
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { defineSlotRecipe } from "@chakra-ui/react/styled-system";
2+
3+
export const collapsibleMotionSlotRecipe = defineSlotRecipe({
4+
slots: ["root", "trigger", "content"],
5+
className: "collapsibleMotion",
6+
base: {
7+
root: {},
8+
trigger: {
9+
cursor: "pointer",
10+
},
11+
content: {},
12+
},
13+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import {
2+
createSlotRecipeContext,
3+
type HTMLChakraProps,
4+
} from "@chakra-ui/react/styled-system";
5+
import type { CollapsibleMotionRootSlotProps } from "./collapsible-motion.types";
6+
7+
const { withProvider, withContext } = createSlotRecipeContext({
8+
key: "collapsibleMotion",
9+
});
10+
11+
export const CollapsibleMotionRootSlot = withProvider<
12+
HTMLDivElement,
13+
CollapsibleMotionRootSlotProps
14+
>("div", "root");
15+
16+
export const CollapsibleMotionTriggerSlot = withContext<
17+
HTMLButtonElement,
18+
HTMLChakraProps<"button">
19+
>("button", "trigger");
20+
21+
export const CollapsibleMotionContentSlot = withContext<
22+
HTMLDivElement,
23+
HTMLChakraProps<"div">
24+
>("div", "content");

0 commit comments

Comments
 (0)