UNPKG

8.56 kBTypeScriptView Raw
1import color from 'color';
2import * as React from 'react';
3import {
4 View,
5 ViewStyle,
6 StyleSheet,
7 StyleProp,
8 TextStyle,
9 I18nManager,
10 GestureResponderEvent,
11} from 'react-native';
12import TouchableRipple from '../TouchableRipple/TouchableRipple';
13import MaterialCommunityIcon from '../MaterialCommunityIcon';
14import Text from '../Typography/Text';
15import { withTheme } from '../../core/theming';
16
17import { ListAccordionGroupContext } from './ListAccordionGroup';
18
19type Props = {
20 /**
21 * Title text for the list accordion.
22 */
23 title: React.ReactNode;
24 /**
25 * Description text for the list accordion.
26 */
27 description?: React.ReactNode;
28 /**
29 * Callback which returns a React element to display on the left side.
30 */
31 left?: (props: { color: string }) => React.ReactNode;
32 /**
33 * Callback which returns a React element to display on the right side.
34 */
35 right?: (props: { isExpanded: boolean }) => React.ReactNode;
36 /**
37 * Whether the accordion is expanded
38 * If this prop is provided, the accordion will behave as a "controlled component".
39 * You'll need to update this prop when you want to toggle the component or on `onPress`.
40 */
41 expanded?: boolean;
42 /**
43 * Function to execute on press.
44 */
45 onPress?: () => void;
46 /**
47 * Function to execute on long press.
48 */
49 onLongPress?: (e: GestureResponderEvent) => void;
50 /**
51 * Content of the section.
52 */
53 children: React.ReactNode;
54 /**
55 * @optional
56 */
57 theme: ReactNativePaper.Theme;
58 /**
59 * Style that is passed to the wrapping TouchableRipple element.
60 */
61 style?: StyleProp<ViewStyle>;
62 /**
63 * Style that is passed to Title element.
64 */
65 titleStyle?: StyleProp<TextStyle>;
66 /**
67 * Style that is passed to Description element.
68 */
69 descriptionStyle?: StyleProp<TextStyle>;
70 /**
71 * Truncate Title text such that the total number of lines does not
72 * exceed this number.
73 */
74 titleNumberOfLines?: number;
75 /**
76 * Truncate Description text such that the total number of lines does not
77 * exceed this number.
78 */
79 descriptionNumberOfLines?: number;
80 /**
81 * Id is used for distinguishing specific accordion when using List.AccordionGroup. Property is required when using List.AccordionGroup and has no impact on behavior when using standalone List.Accordion.
82 */
83 id?: string | number;
84 /**
85 * TestID used for testing purposes
86 */
87 testID?: string;
88 /**
89 * Accessibility label for the TouchableRipple. This is read by the screen reader when the user taps the touchable.
90 */
91 accessibilityLabel?: string;
92};
93
94/**
95 * A component used to display an expandable list item.
96 *
97 * <div class="screenshots">
98 * <img class="medium" src="screenshots/list-accordion-1.png" />
99 * <img class="medium" src="screenshots/list-accordion-2.png" />
100 * <img class="medium" src="screenshots/list-accordion-3.png" />
101 * </div>
102 *
103 * ## Usage
104 * ```js
105 * import * as React from 'react';
106 * import { List } from 'react-native-paper';
107 *
108 * const MyComponent = () => {
109 * const [expanded, setExpanded] = React.useState(true);
110 *
111 * const handlePress = () => setExpanded(!expanded);
112 *
113 * return (
114 * <List.Section title="Accordions">
115 * <List.Accordion
116 * title="Uncontrolled Accordion"
117 * left={props => <List.Icon {...props} icon="folder" />}>
118 * <List.Item title="First item" />
119 * <List.Item title="Second item" />
120 * </List.Accordion>
121 *
122 * <List.Accordion
123 * title="Controlled Accordion"
124 * left={props => <List.Icon {...props} icon="folder" />}
125 * expanded={expanded}
126 * onPress={handlePress}>
127 * <List.Item title="First item" />
128 * <List.Item title="Second item" />
129 * </List.Accordion>
130 * </List.Section>
131 * );
132 * };
133 *
134 * export default MyComponent;
135 * ```
136 */
137const 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 // Only update state of the `expanded` prop was not passed
165 // If it was passed, the component will act as a controlled component
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 // @ts-expect-error We keep old a11y props for backwards compat with old RN versions
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
281ListAccordion.displayName = 'List.Accordion';
282
283const 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
314export default withTheme(ListAccordion);