1 | import color from 'color';
|
2 | import * as React from 'react';
|
3 | import {
|
4 | View,
|
5 | ViewStyle,
|
6 | StyleSheet,
|
7 | StyleProp,
|
8 | TextStyle,
|
9 | I18nManager,
|
10 | GestureResponderEvent,
|
11 | } from 'react-native';
|
12 | import TouchableRipple from '../TouchableRipple/TouchableRipple';
|
13 | import MaterialCommunityIcon from '../MaterialCommunityIcon';
|
14 | import Text from '../Typography/Text';
|
15 | import { withTheme } from '../../core/theming';
|
16 |
|
17 | import { ListAccordionGroupContext } from './ListAccordionGroup';
|
18 |
|
19 | type Props = {
|
20 | |
21 |
|
22 |
|
23 | title: React.ReactNode;
|
24 | |
25 |
|
26 |
|
27 | description?: React.ReactNode;
|
28 | |
29 |
|
30 |
|
31 | left?: (props: { color: string }) => React.ReactNode;
|
32 | |
33 |
|
34 |
|
35 | right?: (props: { isExpanded: boolean }) => React.ReactNode;
|
36 | |
37 |
|
38 |
|
39 |
|
40 |
|
41 | expanded?: boolean;
|
42 | |
43 |
|
44 |
|
45 | onPress?: () => void;
|
46 | |
47 |
|
48 |
|
49 | onLongPress?: (e: GestureResponderEvent) => void;
|
50 | |
51 |
|
52 |
|
53 | children: React.ReactNode;
|
54 | |
55 |
|
56 |
|
57 | theme: ReactNativePaper.Theme;
|
58 | |
59 |
|
60 |
|
61 | style?: StyleProp<ViewStyle>;
|
62 | |
63 |
|
64 |
|
65 | titleStyle?: StyleProp<TextStyle>;
|
66 | |
67 |
|
68 |
|
69 | descriptionStyle?: StyleProp<TextStyle>;
|
70 | |
71 |
|
72 |
|
73 |
|
74 | titleNumberOfLines?: number;
|
75 | |
76 |
|
77 |
|
78 |
|
79 | descriptionNumberOfLines?: number;
|
80 | |
81 |
|
82 |
|
83 | id?: string | number;
|
84 | |
85 |
|
86 |
|
87 | testID?: string;
|
88 | |
89 |
|
90 |
|
91 | accessibilityLabel?: string;
|
92 | };
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 | const ListAccordion = ({
|
138 | left,
|
139 | right,
|
140 | title,
|
141 | description,
|
142 | children,
|
143 | theme,
|
144 | titleStyle,
|
145 | descriptionStyle,
|
146 | titleNumberOfLines = 1,
|
147 | descriptionNumberOfLines = 2,
|
148 | style,
|
149 | id,
|
150 | testID,
|
151 | onPress,
|
152 | onLongPress,
|
153 | expanded: expandedProp,
|
154 | accessibilityLabel,
|
155 | }: Props) => {
|
156 | const [expanded, setExpanded] = React.useState<boolean>(
|
157 | expandedProp || false
|
158 | );
|
159 |
|
160 | const handlePressAction = () => {
|
161 | onPress?.();
|
162 |
|
163 | if (expandedProp === undefined) {
|
164 |
|
165 |
|
166 | setExpanded((expanded) => !expanded);
|
167 | }
|
168 | };
|
169 |
|
170 | const titleColor = color(theme.colors.text).alpha(0.87).rgb().string();
|
171 | const descriptionColor = color(theme.colors.text).alpha(0.54).rgb().string();
|
172 |
|
173 | const expandedInternal = expandedProp !== undefined ? expandedProp : expanded;
|
174 |
|
175 | const groupContext = React.useContext(ListAccordionGroupContext);
|
176 | if (groupContext !== null && !id) {
|
177 | throw new Error(
|
178 | 'List.Accordion is used inside a List.AccordionGroup without specifying an id prop.'
|
179 | );
|
180 | }
|
181 | const isExpanded = groupContext
|
182 | ? groupContext.expandedId === id
|
183 | : expandedInternal;
|
184 | const handlePress =
|
185 | groupContext && id !== undefined
|
186 | ? () => groupContext.onAccordionPress(id)
|
187 | : handlePressAction;
|
188 | return (
|
189 | <View>
|
190 | <View style={{ backgroundColor: theme.colors.background }}>
|
191 | <TouchableRipple
|
192 | style={[styles.container, style]}
|
193 | onPress={handlePress}
|
194 | onLongPress={onLongPress}
|
195 |
|
196 | accessibilityTraits="button"
|
197 | accessibilityComponentType="button"
|
198 | accessibilityRole="button"
|
199 | accessibilityState={{ expanded: isExpanded }}
|
200 | accessibilityLabel={accessibilityLabel}
|
201 | testID={testID}
|
202 | delayPressIn={0}
|
203 | borderless
|
204 | >
|
205 | <View style={styles.row} pointerEvents="none">
|
206 | {left
|
207 | ? left({
|
208 | color: isExpanded ? theme.colors.primary : descriptionColor,
|
209 | })
|
210 | : null}
|
211 | <View style={[styles.item, styles.content]}>
|
212 | <Text
|
213 | selectable={false}
|
214 | numberOfLines={titleNumberOfLines}
|
215 | style={[
|
216 | styles.title,
|
217 | {
|
218 | color: isExpanded ? theme.colors.primary : titleColor,
|
219 | },
|
220 | titleStyle,
|
221 | ]}
|
222 | >
|
223 | {title}
|
224 | </Text>
|
225 | {description ? (
|
226 | <Text
|
227 | selectable={false}
|
228 | numberOfLines={descriptionNumberOfLines}
|
229 | style={[
|
230 | styles.description,
|
231 | {
|
232 | color: descriptionColor,
|
233 | },
|
234 | descriptionStyle,
|
235 | ]}
|
236 | >
|
237 | {description}
|
238 | </Text>
|
239 | ) : null}
|
240 | </View>
|
241 | <View
|
242 | style={[styles.item, description ? styles.multiline : undefined]}
|
243 | >
|
244 | {right ? (
|
245 | right({
|
246 | isExpanded: isExpanded,
|
247 | })
|
248 | ) : (
|
249 | <MaterialCommunityIcon
|
250 | name={isExpanded ? 'chevron-up' : 'chevron-down'}
|
251 | color={titleColor}
|
252 | size={24}
|
253 | direction={I18nManager.isRTL ? 'rtl' : 'ltr'}
|
254 | />
|
255 | )}
|
256 | </View>
|
257 | </View>
|
258 | </TouchableRipple>
|
259 | </View>
|
260 |
|
261 | {isExpanded
|
262 | ? React.Children.map(children, (child) => {
|
263 | if (
|
264 | left &&
|
265 | React.isValidElement(child) &&
|
266 | !child.props.left &&
|
267 | !child.props.right
|
268 | ) {
|
269 | return React.cloneElement(child, {
|
270 | style: [styles.child, child.props.style],
|
271 | });
|
272 | }
|
273 |
|
274 | return child;
|
275 | })
|
276 | : null}
|
277 | </View>
|
278 | );
|
279 | };
|
280 |
|
281 | ListAccordion.displayName = 'List.Accordion';
|
282 |
|
283 | const styles = StyleSheet.create({
|
284 | container: {
|
285 | padding: 8,
|
286 | },
|
287 | row: {
|
288 | flexDirection: 'row',
|
289 | alignItems: 'center',
|
290 | },
|
291 | multiline: {
|
292 | height: 40,
|
293 | alignItems: 'center',
|
294 | justifyContent: 'center',
|
295 | },
|
296 | title: {
|
297 | fontSize: 16,
|
298 | },
|
299 | description: {
|
300 | fontSize: 14,
|
301 | },
|
302 | item: {
|
303 | margin: 8,
|
304 | },
|
305 | child: {
|
306 | paddingLeft: 64,
|
307 | },
|
308 | content: {
|
309 | flex: 1,
|
310 | justifyContent: 'center',
|
311 | },
|
312 | });
|
313 |
|
314 | export default withTheme(ListAccordion);
|