@utilitywarehouse/hearth-react-native 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +12 -0
- package/build/components/Banner/Banner.d.ts +6 -0
- package/build/components/Banner/Banner.js +161 -0
- package/build/components/Banner/Banner.props.d.ts +82 -0
- package/build/components/Banner/Banner.props.js +1 -0
- package/build/components/Banner/index.d.ts +2 -0
- package/build/components/Banner/index.js +1 -0
- package/build/components/BottomSheet/BottomSheetBackdrop.js +1 -5
- package/build/components/BottomSheet/BottomSheetFlatList.js +1 -5
- package/build/components/BottomSheet/BottomSheetHandle.js +1 -5
- package/build/components/BottomSheet/useBottomSheetLogic.d.ts +1 -1
- package/build/components/Expandable/Expandable.d.ts +6 -0
- package/build/components/Expandable/Expandable.js +44 -0
- package/build/components/Expandable/Expandable.props.d.ts +38 -0
- package/build/components/Expandable/Expandable.props.js +1 -0
- package/build/components/Expandable/index.d.ts +2 -0
- package/build/components/Expandable/index.js +1 -0
- package/build/components/ExpandableCard/ExpandableCard.d.ts +6 -0
- package/build/components/ExpandableCard/ExpandableCard.js +23 -0
- package/build/components/ExpandableCard/ExpandableCard.props.d.ts +69 -0
- package/build/components/ExpandableCard/ExpandableCard.props.js +1 -0
- package/build/components/ExpandableCard/ExpandableCardContent.d.ts +6 -0
- package/build/components/ExpandableCard/ExpandableCardContent.js +14 -0
- package/build/components/ExpandableCard/ExpandableCardExpandedContent.d.ts +11 -0
- package/build/components/ExpandableCard/ExpandableCardExpandedContent.js +18 -0
- package/build/components/ExpandableCard/ExpandableCardGroup.d.ts +6 -0
- package/build/components/ExpandableCard/ExpandableCardGroup.js +17 -0
- package/build/components/ExpandableCard/ExpandableCardGroup.props.d.ts +25 -0
- package/build/components/ExpandableCard/ExpandableCardGroup.props.js +1 -0
- package/build/components/ExpandableCard/ExpandableCardHelperText.d.ts +6 -0
- package/build/components/ExpandableCard/ExpandableCardHelperText.js +13 -0
- package/build/components/ExpandableCard/ExpandableCardIcon.d.ts +9 -0
- package/build/components/ExpandableCard/ExpandableCardIcon.js +19 -0
- package/build/components/ExpandableCard/ExpandableCardLeadingContent.d.ts +6 -0
- package/build/components/ExpandableCard/ExpandableCardLeadingContent.js +5 -0
- package/build/components/ExpandableCard/ExpandableCardText.d.ts +6 -0
- package/build/components/ExpandableCard/ExpandableCardText.js +7 -0
- package/build/components/ExpandableCard/ExpandableCardTrailingContent.d.ts +6 -0
- package/build/components/ExpandableCard/ExpandableCardTrailingContent.js +5 -0
- package/build/components/ExpandableCard/ExpandableCardTrailingIcon.d.ts +9 -0
- package/build/components/ExpandableCard/ExpandableCardTrailingIcon.js +17 -0
- package/build/components/ExpandableCard/ExpandableCardTrigger.d.ts +17 -0
- package/build/components/ExpandableCard/ExpandableCardTrigger.js +7 -0
- package/build/components/ExpandableCard/ExpandableCardTrigger.props.d.ts +44 -0
- package/build/components/ExpandableCard/ExpandableCardTrigger.props.js +1 -0
- package/build/components/ExpandableCard/ExpandableCardTriggerRoot.d.ts +11 -0
- package/build/components/ExpandableCard/ExpandableCardTriggerRoot.js +91 -0
- package/build/components/ExpandableCard/index.d.ts +14 -0
- package/build/components/ExpandableCard/index.js +11 -0
- package/build/components/HighlightBanner/HighlightBanner.d.ts +6 -0
- package/build/components/HighlightBanner/HighlightBanner.js +86 -0
- package/build/components/HighlightBanner/HighlightBanner.props.d.ts +14 -0
- package/build/components/HighlightBanner/HighlightBanner.props.js +1 -0
- package/build/components/HighlightBanner/index.d.ts +2 -0
- package/build/components/HighlightBanner/index.js +1 -0
- package/build/components/Spinner/Spinner.js +0 -2
- package/build/components/Spinner/Spinner.web.d.ts +2 -1
- package/build/components/Spinner/Spinner.web.js +0 -2
- package/build/components/Switch/Switch.web.js +0 -1
- package/build/components/Tabs/TabsList.js +1 -6
- package/build/components/index.d.ts +4 -0
- package/build/components/index.js +4 -0
- package/docs/components/AllComponents.web.tsx +75 -4
- package/docs/components/VariantTitle.tsx +1 -1
- package/package.json +1 -1
- package/src/components/Banner/Banner.docs.mdx +402 -0
- package/src/components/Banner/Banner.props.ts +106 -0
- package/src/components/Banner/Banner.stories.tsx +494 -0
- package/src/components/Banner/Banner.tsx +264 -0
- package/src/components/Banner/index.ts +2 -0
- package/src/components/BottomSheet/BottomSheetBackdrop.tsx +0 -1
- package/src/components/BottomSheet/BottomSheetFlatList.tsx +0 -1
- package/src/components/BottomSheet/BottomSheetHandle.tsx +0 -1
- package/src/components/Card/Card.docs.mdx +10 -2
- package/src/components/Expandable/Expandable.docs.mdx +201 -0
- package/src/components/Expandable/Expandable.props.ts +46 -0
- package/src/components/Expandable/Expandable.stories.tsx +284 -0
- package/src/components/Expandable/Expandable.tsx +92 -0
- package/src/components/Expandable/index.ts +2 -0
- package/src/components/ExpandableCard/ExpandableCard.docs.mdx +312 -0
- package/src/components/ExpandableCard/ExpandableCard.props.ts +85 -0
- package/src/components/ExpandableCard/ExpandableCard.stories.tsx +326 -0
- package/src/components/ExpandableCard/ExpandableCard.tsx +76 -0
- package/src/components/ExpandableCard/ExpandableCardContent.tsx +21 -0
- package/src/components/ExpandableCard/ExpandableCardExpandedContent.tsx +42 -0
- package/src/components/ExpandableCard/ExpandableCardGroup.props.ts +31 -0
- package/src/components/ExpandableCard/ExpandableCardGroup.tsx +40 -0
- package/src/components/ExpandableCard/ExpandableCardHelperText.tsx +21 -0
- package/src/components/ExpandableCard/ExpandableCardIcon.tsx +32 -0
- package/src/components/ExpandableCard/ExpandableCardLeadingContent.tsx +9 -0
- package/src/components/ExpandableCard/ExpandableCardText.tsx +14 -0
- package/src/components/ExpandableCard/ExpandableCardTrailingContent.tsx +9 -0
- package/src/components/ExpandableCard/ExpandableCardTrailingIcon.tsx +30 -0
- package/src/components/ExpandableCard/ExpandableCardTrigger.props.ts +47 -0
- package/src/components/ExpandableCard/ExpandableCardTrigger.tsx +10 -0
- package/src/components/ExpandableCard/ExpandableCardTriggerRoot.tsx +154 -0
- package/src/components/ExpandableCard/index.ts +14 -0
- package/src/components/HighlightBanner/HighlightBanner.docs.mdx +277 -0
- package/src/components/HighlightBanner/HighlightBanner.props.ts +29 -0
- package/src/components/HighlightBanner/HighlightBanner.stories.tsx +259 -0
- package/src/components/HighlightBanner/HighlightBanner.tsx +122 -0
- package/src/components/HighlightBanner/index.ts +2 -0
- package/src/components/Spinner/Spinner.tsx +0 -2
- package/src/components/Spinner/Spinner.web.tsx +0 -2
- package/src/components/Switch/Switch.web.tsx +1 -5
- package/src/components/Tabs/TabsList.tsx +0 -2
- package/src/components/index.ts +4 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react-native';
|
|
2
|
+
import {
|
|
3
|
+
ChevronDownMediumIcon,
|
|
4
|
+
ChevronUpMediumIcon,
|
|
5
|
+
} from '@utilitywarehouse/hearth-react-native-icons';
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
import { View } from 'react-native';
|
|
8
|
+
import { BodyText, Button, Card } from '../../components';
|
|
9
|
+
import Expandable from './Expandable';
|
|
10
|
+
|
|
11
|
+
const meta = {
|
|
12
|
+
title: 'Stories / Expandable',
|
|
13
|
+
component: Expandable,
|
|
14
|
+
parameters: {
|
|
15
|
+
// layout: 'centered',
|
|
16
|
+
},
|
|
17
|
+
argTypes: {
|
|
18
|
+
expanded: {
|
|
19
|
+
control: 'boolean',
|
|
20
|
+
description: 'Whether the content is expanded',
|
|
21
|
+
},
|
|
22
|
+
duration: {
|
|
23
|
+
control: { type: 'number', min: 100, max: 1000, step: 50 },
|
|
24
|
+
description: 'Animation duration in milliseconds',
|
|
25
|
+
},
|
|
26
|
+
accessibilityLabel: {
|
|
27
|
+
control: 'text',
|
|
28
|
+
description: 'Accessibility label for screen readers',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
args: {
|
|
32
|
+
expanded: false,
|
|
33
|
+
duration: 300,
|
|
34
|
+
},
|
|
35
|
+
} satisfies Meta<typeof Expandable>;
|
|
36
|
+
|
|
37
|
+
export default meta;
|
|
38
|
+
type Story = StoryObj<typeof meta>;
|
|
39
|
+
|
|
40
|
+
export const Playground: Story = {
|
|
41
|
+
render: (args: typeof meta.args) => {
|
|
42
|
+
const [expanded, setExpanded] = useState(args.expanded ?? false);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
<Button onPress={() => setExpanded(!expanded)} style={{ marginBottom: 16 }}>
|
|
47
|
+
{expanded ? 'Collapse' : 'Expand'}
|
|
48
|
+
</Button>
|
|
49
|
+
<Expandable {...args} expanded={expanded}>
|
|
50
|
+
<Card>
|
|
51
|
+
<BodyText>
|
|
52
|
+
This is expandable content. It animates smoothly when toggled. You can put any content
|
|
53
|
+
here, and it will expand and collapse with animation.
|
|
54
|
+
</BodyText>
|
|
55
|
+
</Card>
|
|
56
|
+
</Expandable>
|
|
57
|
+
</>
|
|
58
|
+
);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const BasicExample: Story = {
|
|
63
|
+
render: () => {
|
|
64
|
+
const [expanded, setExpanded] = useState(false);
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<Button onPress={() => setExpanded(!expanded)} style={{ marginBottom: 16 }}>
|
|
69
|
+
Toggle Content
|
|
70
|
+
</Button>
|
|
71
|
+
<Expandable expanded={expanded}>
|
|
72
|
+
<Card>
|
|
73
|
+
<BodyText>This content expands and collapses with a smooth animation.</BodyText>
|
|
74
|
+
</Card>
|
|
75
|
+
</Expandable>
|
|
76
|
+
</>
|
|
77
|
+
);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const FastAnimation: Story = {
|
|
82
|
+
render: () => {
|
|
83
|
+
const [expanded, setExpanded] = useState(false);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<>
|
|
87
|
+
<Button onPress={() => setExpanded(!expanded)} style={{ marginBottom: 16 }}>
|
|
88
|
+
Fast Toggle
|
|
89
|
+
</Button>
|
|
90
|
+
<Expandable expanded={expanded} duration={150}>
|
|
91
|
+
<Card>
|
|
92
|
+
<BodyText>This expands and collapses quickly with a 150ms duration.</BodyText>
|
|
93
|
+
</Card>
|
|
94
|
+
</Expandable>
|
|
95
|
+
</>
|
|
96
|
+
);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const SlowAnimation: Story = {
|
|
101
|
+
render: () => {
|
|
102
|
+
const [expanded, setExpanded] = useState(false);
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<>
|
|
106
|
+
<Button onPress={() => setExpanded(!expanded)} style={{ marginBottom: 16 }}>
|
|
107
|
+
Slow Toggle
|
|
108
|
+
</Button>
|
|
109
|
+
<Expandable expanded={expanded} duration={600}>
|
|
110
|
+
<Card>
|
|
111
|
+
<BodyText>This expands and collapses slowly with a 600ms duration.</BodyText>
|
|
112
|
+
</Card>
|
|
113
|
+
</Expandable>
|
|
114
|
+
</>
|
|
115
|
+
);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const LongContent: Story = {
|
|
120
|
+
render: () => {
|
|
121
|
+
const [expanded, setExpanded] = useState(false);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<>
|
|
125
|
+
<Button onPress={() => setExpanded(!expanded)} style={{ marginBottom: 16 }}>
|
|
126
|
+
{expanded ? 'Hide Details' : 'Show Details'}
|
|
127
|
+
</Button>
|
|
128
|
+
<Expandable expanded={expanded}>
|
|
129
|
+
<Card>
|
|
130
|
+
<BodyText>
|
|
131
|
+
This is a longer piece of content that demonstrates how the Expandable component
|
|
132
|
+
handles larger amounts of text. The animation smoothly transitions regardless of
|
|
133
|
+
content length.
|
|
134
|
+
</BodyText>
|
|
135
|
+
<BodyText style={{ marginTop: 12 }}>
|
|
136
|
+
You can include multiple paragraphs, and the component will measure the full height
|
|
137
|
+
and animate accordingly.
|
|
138
|
+
</BodyText>
|
|
139
|
+
<BodyText style={{ marginTop: 12 }}>
|
|
140
|
+
The animation uses Reanimated for optimal performance and smooth transitions.
|
|
141
|
+
</BodyText>
|
|
142
|
+
</Card>
|
|
143
|
+
</Expandable>
|
|
144
|
+
</>
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export const MultipleExpandables: Story = {
|
|
150
|
+
render: () => {
|
|
151
|
+
const [expanded1, setExpanded1] = useState(false);
|
|
152
|
+
const [expanded2, setExpanded2] = useState(false);
|
|
153
|
+
const [expanded3, setExpanded3] = useState(false);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<View style={{ width: 300, gap: 16 }}>
|
|
157
|
+
<View>
|
|
158
|
+
<Button
|
|
159
|
+
onPress={() => setExpanded1(!expanded1)}
|
|
160
|
+
style={{ marginBottom: 8 }}
|
|
161
|
+
iconPosition="right"
|
|
162
|
+
icon={expanded1 ? ChevronUpMediumIcon : ChevronDownMediumIcon}
|
|
163
|
+
>
|
|
164
|
+
Section 1
|
|
165
|
+
</Button>
|
|
166
|
+
<Expandable expanded={expanded1}>
|
|
167
|
+
<Card>
|
|
168
|
+
<BodyText>Content for section 1</BodyText>
|
|
169
|
+
</Card>
|
|
170
|
+
</Expandable>
|
|
171
|
+
</View>
|
|
172
|
+
|
|
173
|
+
<View>
|
|
174
|
+
<Button
|
|
175
|
+
onPress={() => setExpanded2(!expanded2)}
|
|
176
|
+
style={{ marginBottom: 8 }}
|
|
177
|
+
iconPosition="right"
|
|
178
|
+
icon={expanded2 ? ChevronUpMediumIcon : ChevronDownMediumIcon}
|
|
179
|
+
>
|
|
180
|
+
Section 2
|
|
181
|
+
</Button>
|
|
182
|
+
<Expandable expanded={expanded2}>
|
|
183
|
+
<Card>
|
|
184
|
+
<BodyText>Content for section 2 with more detailed information.</BodyText>
|
|
185
|
+
</Card>
|
|
186
|
+
</Expandable>
|
|
187
|
+
</View>
|
|
188
|
+
|
|
189
|
+
<View>
|
|
190
|
+
<Button
|
|
191
|
+
onPress={() => setExpanded3(!expanded3)}
|
|
192
|
+
style={{ marginBottom: 8 }}
|
|
193
|
+
iconPosition="right"
|
|
194
|
+
icon={expanded3 ? ChevronUpMediumIcon : ChevronDownMediumIcon}
|
|
195
|
+
>
|
|
196
|
+
Section 3
|
|
197
|
+
</Button>
|
|
198
|
+
<Expandable expanded={expanded3}>
|
|
199
|
+
<Card>
|
|
200
|
+
<BodyText>
|
|
201
|
+
Content for section 3 with even more information that spans multiple lines.
|
|
202
|
+
</BodyText>
|
|
203
|
+
</Card>
|
|
204
|
+
</Expandable>
|
|
205
|
+
</View>
|
|
206
|
+
</View>
|
|
207
|
+
);
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const ControlledExample: Story = {
|
|
212
|
+
render: () => {
|
|
213
|
+
const [expanded, setExpanded] = useState(false);
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<View style={{ width: 300 }}>
|
|
217
|
+
<View style={{ flexDirection: 'row', gap: 8, marginBottom: 16 }}>
|
|
218
|
+
<Button onPress={() => setExpanded(true)} style={{ flex: 1 }}>
|
|
219
|
+
Expand
|
|
220
|
+
</Button>
|
|
221
|
+
<Button onPress={() => setExpanded(false)} style={{ flex: 1 }}>
|
|
222
|
+
Collapse
|
|
223
|
+
</Button>
|
|
224
|
+
</View>
|
|
225
|
+
<Expandable
|
|
226
|
+
expanded={expanded}
|
|
227
|
+
onExpandedChange={setExpanded}
|
|
228
|
+
accessibilityLabel="Expandable content section"
|
|
229
|
+
>
|
|
230
|
+
<Card>
|
|
231
|
+
<BodyText>
|
|
232
|
+
This is a controlled expandable with separate expand and collapse buttons.
|
|
233
|
+
</BodyText>
|
|
234
|
+
</Card>
|
|
235
|
+
</Expandable>
|
|
236
|
+
</View>
|
|
237
|
+
);
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export const DefaultExpanded: Story = {
|
|
242
|
+
render: () => {
|
|
243
|
+
const [expanded, setExpanded] = useState(true);
|
|
244
|
+
|
|
245
|
+
return (
|
|
246
|
+
<View style={{ width: 300 }}>
|
|
247
|
+
<Button onPress={() => setExpanded(!expanded)} style={{ marginBottom: 16 }}>
|
|
248
|
+
Toggle
|
|
249
|
+
</Button>
|
|
250
|
+
<Expandable expanded={expanded}>
|
|
251
|
+
<Card>
|
|
252
|
+
<BodyText>This content starts expanded by default.</BodyText>
|
|
253
|
+
</Card>
|
|
254
|
+
</Expandable>
|
|
255
|
+
</View>
|
|
256
|
+
);
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
export const WithCallback: Story = {
|
|
261
|
+
render: () => {
|
|
262
|
+
const [expanded, setExpanded] = useState(false);
|
|
263
|
+
const [message, setMessage] = useState('Content is collapsed');
|
|
264
|
+
|
|
265
|
+
const handleExpandedChange = (newExpanded: boolean) => {
|
|
266
|
+
console.log(newExpanded);
|
|
267
|
+
setMessage(newExpanded ? 'Content is expanded' : 'Content is collapsed');
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<View style={{ width: 300 }}>
|
|
272
|
+
<BodyText style={{ marginBottom: 8 }}>{message}</BodyText>
|
|
273
|
+
<Button onPress={() => setExpanded(!expanded)} style={{ marginBottom: 16 }}>
|
|
274
|
+
Toggle
|
|
275
|
+
</Button>
|
|
276
|
+
<Expandable expanded={expanded} onExpandedChange={handleExpandedChange}>
|
|
277
|
+
<Card>
|
|
278
|
+
<BodyText>This example demonstrates the onExpandedChange callback.</BodyText>
|
|
279
|
+
</Card>
|
|
280
|
+
</Expandable>
|
|
281
|
+
</View>
|
|
282
|
+
);
|
|
283
|
+
},
|
|
284
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useDerivedValue,
|
|
6
|
+
useSharedValue,
|
|
7
|
+
withTiming,
|
|
8
|
+
} from 'react-native-reanimated';
|
|
9
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
10
|
+
import { ExpandableProps } from './Expandable.props';
|
|
11
|
+
|
|
12
|
+
const Expandable = ({
|
|
13
|
+
expanded = false,
|
|
14
|
+
onExpandedChange,
|
|
15
|
+
children,
|
|
16
|
+
duration = 200,
|
|
17
|
+
style,
|
|
18
|
+
accessibilityLabel,
|
|
19
|
+
testID,
|
|
20
|
+
animateOpacity = true,
|
|
21
|
+
...props
|
|
22
|
+
}: ExpandableProps) => {
|
|
23
|
+
const height = useSharedValue(0);
|
|
24
|
+
const open = useSharedValue(expanded);
|
|
25
|
+
|
|
26
|
+
// Update open value when expanded prop changes and call callback
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (open.value !== expanded) {
|
|
29
|
+
open.value = expanded;
|
|
30
|
+
onExpandedChange?.(expanded);
|
|
31
|
+
}
|
|
32
|
+
}, [expanded, onExpandedChange, open]);
|
|
33
|
+
|
|
34
|
+
const derivedHeight = useDerivedValue(() =>
|
|
35
|
+
withTiming(height.value * Number(open.value), {
|
|
36
|
+
duration,
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const derivedOpacity = useDerivedValue(() =>
|
|
41
|
+
animateOpacity
|
|
42
|
+
? withTiming(Number(open.value), {
|
|
43
|
+
duration,
|
|
44
|
+
})
|
|
45
|
+
: 1
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const heightStyle = useAnimatedStyle(() => ({
|
|
49
|
+
height: derivedHeight.value,
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
const opacityStyle = useAnimatedStyle(() => ({
|
|
53
|
+
opacity: derivedOpacity.value,
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Animated.View
|
|
58
|
+
style={[styles.container, heightStyle, style]}
|
|
59
|
+
accessible={true}
|
|
60
|
+
accessibilityLabel={accessibilityLabel}
|
|
61
|
+
accessibilityRole="none"
|
|
62
|
+
accessibilityState={{ expanded }}
|
|
63
|
+
testID={testID}
|
|
64
|
+
{...props}
|
|
65
|
+
>
|
|
66
|
+
<Animated.View style={opacityStyle}>
|
|
67
|
+
<View
|
|
68
|
+
onLayout={e => {
|
|
69
|
+
height.value = e.nativeEvent.layout.height;
|
|
70
|
+
}}
|
|
71
|
+
style={styles.content}
|
|
72
|
+
>
|
|
73
|
+
{children}
|
|
74
|
+
</View>
|
|
75
|
+
</Animated.View>
|
|
76
|
+
</Animated.View>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
Expandable.displayName = 'Expandable';
|
|
81
|
+
|
|
82
|
+
const styles = StyleSheet.create(() => ({
|
|
83
|
+
container: {
|
|
84
|
+
overflow: 'hidden',
|
|
85
|
+
},
|
|
86
|
+
content: {
|
|
87
|
+
position: 'absolute',
|
|
88
|
+
width: '100%',
|
|
89
|
+
},
|
|
90
|
+
}));
|
|
91
|
+
|
|
92
|
+
export default Expandable;
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { Canvas, Controls, Meta } from '@storybook/addon-docs/blocks';
|
|
2
|
+
import { BackToTopButton, ViewFigmaButton } from '../../../docs/components';
|
|
3
|
+
import * as Stories from './ExpandableCard.stories';
|
|
4
|
+
|
|
5
|
+
<Meta title="Components / Expandable Card" />
|
|
6
|
+
|
|
7
|
+
<BackToTopButton />
|
|
8
|
+
|
|
9
|
+
<ViewFigmaButton link="https://www.figma.com/design/6NKZXZhFSExXrcbBgc6zTR/Hearth-Components---Tokens?node-id=7222-5934" />
|
|
10
|
+
|
|
11
|
+
# Expandable Card
|
|
12
|
+
|
|
13
|
+
The `ExpandableCard` component is an interactive card that expands to reveal additional content when clicked. It's a Card component with smooth expansion animations, making it perfect for displaying collapsible information sections.
|
|
14
|
+
|
|
15
|
+
- [Playground](#playground)
|
|
16
|
+
- [Usage](#usage)
|
|
17
|
+
- [Props](#props)
|
|
18
|
+
- [`ExpandableCard`](#expandablecard)
|
|
19
|
+
- [`ExpandableCardGroup`](#expandablecardgroup)
|
|
20
|
+
- [Examples](#examples)
|
|
21
|
+
- [Basic Example](#basic-example)
|
|
22
|
+
- [With Leading Icon](#with-leading-icon)
|
|
23
|
+
- [With `Badge`](#with-badge)
|
|
24
|
+
- [With Numeric Value](#with-numeric-value)
|
|
25
|
+
- [`CardGroup`](#cardgroup)
|
|
26
|
+
- [Advanced Usage](#advanced-usage)
|
|
27
|
+
- [Accessibility](#accessibility)
|
|
28
|
+
|
|
29
|
+
## Playground
|
|
30
|
+
|
|
31
|
+
<Canvas of={Stories.Playground} />
|
|
32
|
+
|
|
33
|
+
<Controls of={Stories.Playground} />
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
<Canvas of={Stories.BasicExample} />
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { ExpandableCard, BodyText } from '@utilitywarehouse/hearth-react-native';
|
|
41
|
+
|
|
42
|
+
const MyComponent = () => (
|
|
43
|
+
<ExpandableCard
|
|
44
|
+
heading="Order Details"
|
|
45
|
+
helperText="View your order information"
|
|
46
|
+
expandedContent={
|
|
47
|
+
<>
|
|
48
|
+
<BodyText>Order #12345</BodyText>
|
|
49
|
+
<BodyText>Status: Delivered</BodyText>
|
|
50
|
+
<BodyText>Date: 10 Nov 2025</BodyText>
|
|
51
|
+
</>
|
|
52
|
+
}
|
|
53
|
+
/>
|
|
54
|
+
);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Props
|
|
58
|
+
|
|
59
|
+
### `ExpandableCard`
|
|
60
|
+
|
|
61
|
+
| Prop | Type | Default | Description |
|
|
62
|
+
| ------------------ | ----------------------------- | ------------------- | ------------------------------------------------ |
|
|
63
|
+
| `heading` | `string` | - | The heading text displayed in the trigger |
|
|
64
|
+
| `helperText` | `string` | - | Optional helper text displayed below the heading |
|
|
65
|
+
| `leadingIcon` | `ComponentType` | - | Leading icon component (automatically wrapped) |
|
|
66
|
+
| `leadingContent` | `ReactNode` | - | Leading content (icon or custom element) |
|
|
67
|
+
| `badge` | `BadgeProps` | - | Badge to display |
|
|
68
|
+
| `badgePosition` | `'top' \| 'bottom'` | `'bottom'` | Badge position |
|
|
69
|
+
| `numericValue` | `string \| number` | - | Numeric value to display on the right |
|
|
70
|
+
| `expandedContent` | `ReactNode` | - | The content to show when expanded |
|
|
71
|
+
| `expanded` | `boolean` | - | Whether the card is expanded (controlled) |
|
|
72
|
+
| `onExpandedChange` | `(expanded: boolean) => void` | - | Callback when expanded state changes |
|
|
73
|
+
| `duration` | `number` | `200` | Duration of expansion animation in milliseconds |
|
|
74
|
+
| `animateOpacity` | `boolean` | `true` | Whether to animate opacity during expansion |
|
|
75
|
+
| `disabled` | `boolean` | `false` | Whether the card is disabled |
|
|
76
|
+
| `colorScheme` | `CardColorScheme` | - | Color scheme (inherits from Card component) |
|
|
77
|
+
| `variant` | `CardVariant` | - | Variant (inherits from Card component) |
|
|
78
|
+
| `testID` | `string` | `'expandable-card'` | Test ID for testing |
|
|
79
|
+
|
|
80
|
+
### `ExpandableCardGroup`
|
|
81
|
+
|
|
82
|
+
| Prop | Type | Default | Description |
|
|
83
|
+
| ----------------------- | ----------- | ------------------------- | ---------------------------------------------- |
|
|
84
|
+
| `heading` | `string` | - | Section heading |
|
|
85
|
+
| `helperText` | `string` | - | Helper text displayed below the heading |
|
|
86
|
+
| `headerTrailingContent` | `ReactNode` | - | Trailing content for the header (e.g., a link) |
|
|
87
|
+
| `children` | `ReactNode` | - | The ExpandableCard children |
|
|
88
|
+
| `testID` | `string` | `'expandable-card-group'` | Test ID for testing |
|
|
89
|
+
|
|
90
|
+
## Examples
|
|
91
|
+
|
|
92
|
+
### Basic Example
|
|
93
|
+
|
|
94
|
+
<Canvas of={Stories.BasicExample} />
|
|
95
|
+
|
|
96
|
+
### With Leading Icon
|
|
97
|
+
|
|
98
|
+
You can add a leading icon using the `leadingIcon` prop for simple icons, or use `leadingContent` with an `IconContainer` for more customization.
|
|
99
|
+
|
|
100
|
+
<Canvas of={Stories.WithLeadingIcon} />
|
|
101
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
import { ExpandableCard, BodyText } from '@utilitywarehouse/hearth-react-native';
|
|
104
|
+
import { SettingsMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
105
|
+
|
|
106
|
+
const MyComponent = () => (
|
|
107
|
+
<ExpandableCard
|
|
108
|
+
heading="Settings"
|
|
109
|
+
helperText="Configure your preferences"
|
|
110
|
+
leadingIcon={SettingsMediumIcon}
|
|
111
|
+
expandedContent={
|
|
112
|
+
<>
|
|
113
|
+
<BodyText>• Notifications</BodyText>
|
|
114
|
+
<BodyText>• Privacy</BodyText>
|
|
115
|
+
<BodyText>• Account</BodyText>
|
|
116
|
+
</>
|
|
117
|
+
}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Or use `leadingContent` with `IconContainer` for emphasis variants:
|
|
123
|
+
|
|
124
|
+
<Canvas of={Stories.WithIconContainer} />
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { ExpandableCard, IconContainer, BodyText } from '@utilitywarehouse/hearth-react-native';
|
|
128
|
+
import { ElectricityMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
129
|
+
|
|
130
|
+
const MyComponent = () => (
|
|
131
|
+
<ExpandableCard
|
|
132
|
+
heading="Electricity"
|
|
133
|
+
helperText="Last reading 23/03/24"
|
|
134
|
+
leadingContent={
|
|
135
|
+
<IconContainer icon={ElectricityMediumIcon} size="md" variant="emphasis" color="energy" />
|
|
136
|
+
}
|
|
137
|
+
expandedContent={
|
|
138
|
+
<>
|
|
139
|
+
<BodyText>Current Usage: 245 kWh</BodyText>
|
|
140
|
+
<BodyText>Estimated Cost: £45.50</BodyText>
|
|
141
|
+
</>
|
|
142
|
+
}
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### With `Badge`
|
|
148
|
+
|
|
149
|
+
<Canvas of={Stories.WithBadge} />
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
<ExpandableCard
|
|
153
|
+
heading="New Feature"
|
|
154
|
+
helperText="Check out what's new"
|
|
155
|
+
badge={{ text: 'New', colorScheme: 'cyan' }}
|
|
156
|
+
expandedContent={<BodyText>We've added new features to improve your experience.</BodyText>}
|
|
157
|
+
/>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### With Numeric Value
|
|
161
|
+
|
|
162
|
+
<Canvas of={Stories.WithNumericValue} />
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
<ExpandableCard
|
|
166
|
+
heading="Total Balance"
|
|
167
|
+
helperText="Current account balance"
|
|
168
|
+
numericValue="£123.45"
|
|
169
|
+
expandedContent={
|
|
170
|
+
<>
|
|
171
|
+
<BodyText>Available: £100.00</BodyText>
|
|
172
|
+
<BodyText>Pending: £23.45</BodyText>
|
|
173
|
+
</>
|
|
174
|
+
}
|
|
175
|
+
/>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### `CardGroup`
|
|
179
|
+
|
|
180
|
+
Use `ExpandableCardGroup` to group multiple expandable cards with an optional header.
|
|
181
|
+
|
|
182
|
+
<Canvas of={Stories.CardGroup} />
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
import {
|
|
186
|
+
ExpandableCard,
|
|
187
|
+
ExpandableCardGroup,
|
|
188
|
+
IconContainer,
|
|
189
|
+
Link,
|
|
190
|
+
BodyText,
|
|
191
|
+
} from '@utilitywarehouse/hearth-react-native';
|
|
192
|
+
import { ElectricityMediumIcon, GasMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
193
|
+
|
|
194
|
+
const MyComponent = () => (
|
|
195
|
+
<ExpandableCardGroup
|
|
196
|
+
heading="Your Services"
|
|
197
|
+
helperText="View details for each service"
|
|
198
|
+
headerTrailingContent={<Link href="#">View all</Link>}
|
|
199
|
+
>
|
|
200
|
+
<ExpandableCard
|
|
201
|
+
heading="Electricity"
|
|
202
|
+
helperText="Last reading 23/03/24"
|
|
203
|
+
leadingContent={
|
|
204
|
+
<IconContainer icon={ElectricityMediumIcon} size="md" variant="emphasis" color="energy" />
|
|
205
|
+
}
|
|
206
|
+
expandedContent={
|
|
207
|
+
<>
|
|
208
|
+
<BodyText>Current Usage: 245 kWh</BodyText>
|
|
209
|
+
<BodyText>Estimated Cost: £45.50</BodyText>
|
|
210
|
+
</>
|
|
211
|
+
}
|
|
212
|
+
/>
|
|
213
|
+
<ExpandableCard
|
|
214
|
+
heading="Gas"
|
|
215
|
+
helperText="Last reading 23/03/24"
|
|
216
|
+
leadingContent={
|
|
217
|
+
<IconContainer icon={GasMediumIcon} size="md" variant="emphasis" color="energy" />
|
|
218
|
+
}
|
|
219
|
+
expandedContent={
|
|
220
|
+
<>
|
|
221
|
+
<BodyText>Current Usage: 180 kWh</BodyText>
|
|
222
|
+
<BodyText>Estimated Cost: £32.00</BodyText>
|
|
223
|
+
</>
|
|
224
|
+
}
|
|
225
|
+
/>
|
|
226
|
+
</ExpandableCardGroup>
|
|
227
|
+
);
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Advanced Usage
|
|
231
|
+
|
|
232
|
+
If you need to use the `ExpandableCard` component in a more advanced way, you can use the child components directly for full composition control.
|
|
233
|
+
|
|
234
|
+
<Canvas of={Stories.AdvancedComposition} />
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
import {
|
|
238
|
+
ExpandableCard,
|
|
239
|
+
ExpandableCardTrigger,
|
|
240
|
+
ExpandableCardExpandedContent,
|
|
241
|
+
ExpandableCardContent,
|
|
242
|
+
IconContainer,
|
|
243
|
+
BodyText,
|
|
244
|
+
} from '@utilitywarehouse/hearth-react-native';
|
|
245
|
+
import { BillMediumIcon } from '@utilitywarehouse/hearth-react-native-icons';
|
|
246
|
+
import { useState } from 'react';
|
|
247
|
+
|
|
248
|
+
const MyComponent = () => {
|
|
249
|
+
const [expanded, setExpanded] = useState(false);
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<ExpandableCard expanded={expanded} onExpandedChange={setExpanded}>
|
|
253
|
+
<ExpandableCardTrigger
|
|
254
|
+
onPress={() => setExpanded(!expanded)}
|
|
255
|
+
heading="Custom Account Details"
|
|
256
|
+
helperText="View your detailed account information"
|
|
257
|
+
leadingContent={
|
|
258
|
+
<IconContainer icon={BillMediumIcon} size="md" variant="emphasis" color="pig" />
|
|
259
|
+
}
|
|
260
|
+
numericValue="£123.45"
|
|
261
|
+
isExpanded={expanded}
|
|
262
|
+
/>
|
|
263
|
+
<ExpandableCardExpandedContent isExpanded={expanded}>
|
|
264
|
+
<BodyText>Account Number: 1234567890</BodyText>
|
|
265
|
+
<BodyText>Sort Code: 12-34-56</BodyText>
|
|
266
|
+
<BodyText>Balance: £123.45</BodyText>
|
|
267
|
+
<BodyText>Last Updated: 12/11/25</BodyText>
|
|
268
|
+
</ExpandableCardExpandedContent>
|
|
269
|
+
</ExpandableCard>
|
|
270
|
+
);
|
|
271
|
+
};
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Child Components
|
|
275
|
+
|
|
276
|
+
When using advanced composition, you have access to these components:
|
|
277
|
+
|
|
278
|
+
- **`ExpandableCardTrigger`**: The clickable header section with heading, helper text, leading/trailing content
|
|
279
|
+
- **`ExpandableCardExpandedContent`**: Wrapper for the expanded content area with animation controls
|
|
280
|
+
- **`ExpandableCardContent`**: Content container with proper spacing and gap
|
|
281
|
+
- **`ExpandableCardIcon`**: Styled icon component for leading icons
|
|
282
|
+
- **`ExpandableCardText`**: Styled text for main heading
|
|
283
|
+
- **`ExpandableCardHelperText`**: Styled text for helper/secondary text
|
|
284
|
+
- **`ExpandableCardLeadingContent`**: Container for leading content (icons, images, etc.)
|
|
285
|
+
- **`ExpandableCardTrailingContent`**: Container for trailing content
|
|
286
|
+
- **`ExpandableCardTrailingIcon`**: Styled icon for trailing position
|
|
287
|
+
|
|
288
|
+
## Accessibility
|
|
289
|
+
|
|
290
|
+
The ExpandableCard component includes built-in accessibility support:
|
|
291
|
+
|
|
292
|
+
- Uses `accessibilityRole="button"` to indicate it's interactive
|
|
293
|
+
- Sets `accessibilityState` to communicate expanded/collapsed and disabled states
|
|
294
|
+
- Provides meaningful `accessibilityLabel` combining heading and helper text
|
|
295
|
+
- Chevron icon changes direction to indicate current state
|
|
296
|
+
- Keyboard accessible when used on web
|
|
297
|
+
|
|
298
|
+
## Best Practices
|
|
299
|
+
|
|
300
|
+
1. **Use descriptive headings**: Make sure your heading clearly describes what content will be revealed.
|
|
301
|
+
|
|
302
|
+
2. **Keep expanded content concise**: While you can put any content inside, try to keep it focused and scannable.
|
|
303
|
+
|
|
304
|
+
3. **Group related cards**: Use `ExpandableCardGroup` to organize multiple related expandable cards with a common header.
|
|
305
|
+
|
|
306
|
+
4. **Consider initial state**: Most cards should start collapsed, but you can default to expanded for critical information.
|
|
307
|
+
|
|
308
|
+
5. **Provide visual context**: Use leading icons or icon containers to help users quickly identify the card's purpose.
|
|
309
|
+
|
|
310
|
+
6. **Use numeric values appropriately**: Display counts, amounts, or other quantitative data that adds quick context.
|
|
311
|
+
|
|
312
|
+
7. **Controlled vs Uncontrolled**: Use controlled state (via `expanded` and `onExpandedChange`) when you need to manage expansion state externally, otherwise let the component handle it internally.
|