feat: allow List.Accordion to behave as a controlled component (#638)

fixes #616, #635
closes #618
This commit is contained in:
Lucas Vieira
2018-11-05 19:13:41 -02:00
committed by Satyajit Sahoo
parent 8764f60370
commit decbedbd82
5 changed files with 336 additions and 27 deletions

View File

@@ -9,15 +9,28 @@ type Props = {
theme: Theme,
};
class ListAccordionExample extends React.Component<Props> {
type State = {
expanded: boolean,
};
class ListAccordionExample extends React.Component<Props, State> {
static title = 'List.Accordion';
state = {
expanded: true,
};
_handlePress = () => {
this.setState({ expanded: !this.state.expanded });
};
render() {
const {
theme: {
colors: { background },
},
} = this.props;
return (
<ScrollView style={[styles.container, { backgroundColor: background }]}>
<List.Section title="Expandable list item">
@@ -28,6 +41,14 @@ class ListAccordionExample extends React.Component<Props> {
<List.Item title="List item 1" />
<List.Item title="List item 2" />
</List.Accordion>
<List.Accordion
left={props => <List.Icon {...props} icon="folder" />}
title="Start expanded"
expanded={this.state.expanded}
onPress={this._handlePress}
>
<List.Item title="List item 1" />
</List.Accordion>
</List.Section>
<Divider />
<List.Section title="Expandable & multiline list item">

View File

@@ -22,6 +22,16 @@ type Props = {
* Callback which returns a React element to display on the left side.
*/
left?: (props: { color: string }) => React.Node,
/**
* Whether the accordion is expanded
* If this prop is provided, the accordion will behave as a "controlled component".
* You'll need to update this prop when you want to toggle the component or on `onPress`.
*/
expanded?: boolean,
/**
* Function to execute on press.
*/
onPress?: () => mixed,
/**
* Content of the section.
*/
@@ -51,15 +61,40 @@ type State = {
* import * as React from 'react';
* import { List, Checkbox } from 'react-native-paper';
*
* const MyComponent = () => (
* <List.Accordion
* title="Accordion"
* left={props => <List.Icon {...props} icon="folder" />}
* >
* <List.Item title="First item" />
* <List.Item title="Second item" />
* </List.Accordion>
* );
* class MyComponent extends React.Component {
* state = {
* expanded: true
* }
*
* _handlePress = () =>
* this.setState({
* expanded: !this.state.expanded
* });
*
* render() {
* return (
* <List.Section title="Accordions">
* <List.Accordion
* title="Uncontrolled Accordion"
* left={props => <List.Icon {...props} icon="folder" />}
* >
* <List.Item title="First item" />
* <List.Item title="Second item" />
* </List.Accordion>
*
* <List.Accordion
* title="Controlled Accordion"
* left={props => <List.Icon {...props} icon="folder" />}
* expanded={this.state.expanded}
* onPress={this._handlePress}
* >
* <List.Item title="First item" />
* <List.Item title="Second item" />
* </List.Accordion>
* </List.Section>
* );
* }
* }
*
* export default MyComponent;
* ```
@@ -68,13 +103,20 @@ class ListAccordion extends React.Component<Props, State> {
static displayName = 'List.Accordion';
state = {
expanded: false,
expanded: this.props.expanded || false,
};
_handlePress = () =>
this.setState(state => ({
expanded: !state.expanded,
}));
_handlePress = () => {
this.props.onPress && this.props.onPress();
if (this.props.expanded === undefined) {
// Only update state of the `expanded` prop was not passed
// If it was passed, the component will act as a controlled component
this.setState(state => ({
expanded: !state.expanded,
}));
}
};
render() {
const { left, title, description, children, theme, style } = this.props;
@@ -87,6 +129,11 @@ class ListAccordion extends React.Component<Props, State> {
.rgb()
.string();
const expanded =
this.props.expanded !== undefined
? this.props.expanded
: this.state.expanded;
return (
<View>
<TouchableRipple
@@ -99,9 +146,7 @@ class ListAccordion extends React.Component<Props, State> {
<View style={styles.row} pointerEvents="none">
{left
? left({
color: this.state.expanded
? theme.colors.primary
: descriptionColor,
color: expanded ? theme.colors.primary : descriptionColor,
})
: null}
<View style={[styles.item, styles.content]}>
@@ -110,9 +155,7 @@ class ListAccordion extends React.Component<Props, State> {
style={[
styles.title,
{
color: this.state.expanded
? theme.colors.primary
: titleColor,
color: expanded ? theme.colors.primary : titleColor,
},
]}
>
@@ -134,18 +177,14 @@ class ListAccordion extends React.Component<Props, State> {
</View>
<View style={[styles.item, description && styles.multiline]}>
<Icon
source={
this.state.expanded
? 'keyboard-arrow-up'
: 'keyboard-arrow-down'
}
source={expanded ? 'keyboard-arrow-up' : 'keyboard-arrow-down'}
color={titleColor}
size={24}
/>
</View>
</View>
</TouchableRipple>
{this.state.expanded
{expanded
? React.Children.map(children, child => {
if (
left &&

View File

@@ -53,3 +53,15 @@ it('renders list accordion with left items', () => {
expect(tree).toMatchSnapshot();
});
it('renders expanded accordion', () => {
const tree = renderer
.create(
<ListAccordion title="Accordion item 1" expanded>
<ListItem title="List item 1" />
</ListAccordion>
)
.toJSON();
expect(tree).toMatchSnapshot();
});

View File

@@ -1,5 +1,240 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders expanded accordion 1`] = `
<View>
<View
accessibilityComponentType="button"
accessibilityTraits="button"
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Array [
Object {
"padding": 8,
},
undefined,
]
}
>
<View
pointerEvents="none"
style={
Object {
"flexDirection": "row",
}
}
>
<View
style={
Array [
Object {
"margin": 8,
},
Object {
"flex": 1,
"justifyContent": "center",
},
]
}
>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"color": "#000000",
"fontFamily": "Helvetica Neue",
"textAlign": "left",
"writingDirection": "ltr",
},
Array [
Object {
"fontSize": 16,
},
Object {
"color": "#6200ee",
},
],
]
}
theme={
Object {
"colors": Object {
"accent": "#03dac4",
"backdrop": "rgba(0, 0, 0, 0.5)",
"background": "#f6f6f6",
"disabled": "rgba(0, 0, 0, 0.26)",
"error": "#B00020",
"placeholder": "rgba(0, 0, 0, 0.54)",
"primary": "#6200ee",
"surface": "#ffffff",
"text": "#000000",
},
"dark": false,
"fonts": Object {
"light": "HelveticaNeue-Light",
"medium": "HelveticaNeue-Medium",
"regular": "Helvetica Neue",
"thin": "HelveticaNeue-Thin",
},
"roundness": 4,
}
}
>
Accordion item 1
</Text>
</View>
<View
style={
Array [
Object {
"margin": 8,
},
undefined,
]
}
>
<Text
accessibilityElementsHidden={true}
accessible={true}
allowFontScaling={false}
ellipsizeMode="tail"
importantForAccessibility="no-hide-descendants"
pointerEvents="none"
style={
Array [
Object {
"color": "rgba(0, 0, 0, 0.87)",
"fontSize": 24,
},
Array [
Object {
"transform": Array [
Object {
"scaleX": 1,
},
],
},
Object {
"backgroundColor": "transparent",
},
],
Object {
"fontFamily": "Material Icons",
"fontStyle": "normal",
"fontWeight": "normal",
},
]
}
>
</Text>
</View>
</View>
</View>
<View
accessible={true}
isTVSelectable={true}
onResponderGrant={[Function]}
onResponderMove={[Function]}
onResponderRelease={[Function]}
onResponderTerminate={[Function]}
onResponderTerminationRequest={[Function]}
onStartShouldSetResponder={[Function]}
style={
Array [
Object {
"padding": 8,
},
undefined,
]
}
>
<View
style={
Object {
"flexDirection": "row",
}
}
>
<View
pointerEvents="none"
style={
Array [
Object {
"margin": 8,
},
Object {
"flex": 1,
"justifyContent": "center",
},
]
}
>
<Text
accessible={true}
allowFontScaling={true}
ellipsizeMode="tail"
numberOfLines={1}
style={
Array [
Object {
"color": "#000000",
"fontFamily": "Helvetica Neue",
"textAlign": "left",
"writingDirection": "ltr",
},
Array [
Object {
"fontSize": 16,
},
Object {
"color": "rgba(0, 0, 0, 0.87)",
},
],
]
}
theme={
Object {
"colors": Object {
"accent": "#03dac4",
"backdrop": "rgba(0, 0, 0, 0.5)",
"background": "#f6f6f6",
"disabled": "rgba(0, 0, 0, 0.26)",
"error": "#B00020",
"placeholder": "rgba(0, 0, 0, 0.54)",
"primary": "#6200ee",
"surface": "#ffffff",
"text": "#000000",
},
"dark": false,
"fonts": Object {
"light": "HelveticaNeue-Light",
"medium": "HelveticaNeue-Medium",
"regular": "Helvetica Neue",
"thin": "HelveticaNeue-Thin",
},
"roundness": 4,
}
}
>
List item 1
</Text>
</View>
</View>
</View>
</View>
`;
exports[`renders list accordion with children 1`] = `
<View>
<View

View File

@@ -4,6 +4,8 @@ import { ThemeShape, IconSource } from '../types';
export interface AccordionProps {
title: React.ReactNode;
description?: React.ReactNode;
expanded?: boolean;
onPress?: () => any;
left?: (props: { color: string }) => React.ReactNode;
children: React.ReactNode;
theme?: ThemeShape;