Skip to content

Commit ec5703d

Browse files
committed
ExpandableSection - add minHeight (#3040)
* ExpandableSection - add minHeight * Polish screen * Fix docs * Whatever
1 parent 0877979 commit ec5703d

File tree

8 files changed

+150
-104
lines changed

8 files changed

+150
-104
lines changed

demo/src/assets/icons/info.png

868 Bytes
Loading
1.3 KB
Loading
1.75 KB
Loading
2.63 KB
Loading
3.41 KB
Loading

demo/src/screens/componentScreens/ExpandableSectionScreen.tsx

Lines changed: 102 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,135 @@
1-
import _ from 'lodash';
21
import React, {PureComponent} from 'react';
32
import {ScrollView, StyleSheet} from 'react-native';
4-
import {Card, Text, Image, ListItem, Carousel, View, ExpandableSection, Switch} from 'react-native-ui-lib';
3+
import {Text, View, ExpandableSection, SegmentedControl, Colors, Icon} from 'react-native-ui-lib';
54

6-
const cardImage2 = require('../../assets/images/empty-state.jpg');
7-
const cardImage = require('../../assets/images/card-example.jpg');
85
const chevronDown = require('../../assets/icons/chevronDown.png');
96
const chevronUp = require('../../assets/icons/chevronUp.png');
7+
const infoIcon = require('../../assets/icons/info.png');
8+
const DEFAULT = undefined;
9+
const PARTIALLY_EXPANDED_HEIGHT = 100;
10+
const FULLY_EXPANDED_HEIGHT = 300;
1011

1112
class ExpandableSectionScreen extends PureComponent {
1213
state = {
1314
expanded: false,
14-
top: false
15+
minHeight: DEFAULT
1516
};
1617

17-
elements = [
18-
<Card key={0} style={{marginBottom: 10}} onPress={() => this.onExpand()}>
19-
<Card.Section
20-
content={[
21-
{text: 'Card #1', text70: true, grey10: true},
22-
{text: 'card description', text90: true, grey50: true}
23-
]}
24-
style={{padding: 20}}
25-
/>
26-
<Card.Section imageSource={cardImage2} imageStyle={{height: 120}}/>
27-
</Card>,
28-
<Card key={1} style={{marginBottom: 10}} onPress={() => this.onExpand()}>
29-
<Card.Section
30-
content={[
31-
{text: 'Card #2', text70: true, grey10: true},
32-
{text: 'card description', text90: true, grey50: true}
33-
]}
34-
style={{padding: 20}}
35-
/>
36-
<Card.Section imageSource={cardImage} imageStyle={{height: 120}}/>
37-
</Card>,
38-
<Card key={2} style={{marginBottom: 10}} onPress={() => this.onExpand()}>
39-
<Card.Section
40-
content={[
41-
{text: 'Card #3', text70: true, grey10: true},
42-
{text: 'card description', text90: true, grey50: true}
43-
]}
44-
style={{padding: 20}}
45-
/>
46-
<Card.Section imageSource={cardImage2} imageStyle={{height: 120}}/>
47-
</Card>
48-
];
49-
50-
onExpand() {
18+
onExpand = () => {
5119
this.setState({
5220
expanded: !this.state.expanded
5321
});
54-
}
22+
};
5523

56-
getChevron() {
57-
if (this.state.expanded) {
58-
return this.state.top ? chevronDown : chevronUp;
59-
}
60-
return this.state.top ? chevronUp : chevronDown;
24+
getChevron(expanded: boolean) {
25+
return expanded ? chevronUp : chevronDown;
6126
}
6227

63-
getHeaderElement() {
28+
renderReadMoreHeader = () => {
29+
const {expanded} = this.state;
6430
return (
65-
<View margin-10 spread row>
66-
<Text grey10 text60>
67-
ExpandableSection sectionHeader
31+
<View marginH-page marginT-10 row>
32+
<Text text80 marginL-40 marginR-5 $textPrimary>
33+
Read More
34+
</Text>
35+
<Icon style={styles.icon} source={this.getChevron(expanded)} tintColor={Colors.$iconPrimary}/>
36+
</View>
37+
);
38+
};
39+
40+
renderHeader = (text: string,
41+
expanded: boolean,
42+
{disabled, showInfo}: {disabled?: boolean; showInfo?: boolean} = {}) => {
43+
return (
44+
<View marginH-page marginV-20 spread row>
45+
<View row>
46+
{showInfo ? <Icon source={infoIcon} marginR-10 tintColor={disabled ? Colors.grey40 : undefined}/> : null}
47+
<Text text60 marginL-4 grey40={disabled}>
48+
{text}
49+
</Text>
50+
</View>
51+
<Icon style={styles.icon} source={this.getChevron(expanded)} tintColor={disabled ? Colors.grey40 : undefined}/>
52+
</View>
53+
);
54+
};
55+
56+
renderContent() {
57+
return (
58+
<View marginH-60>
59+
<Text text80>
60+
If you have any questions, comments, or concerns, please don&apos;t hesitate to get in touch with us. You can
61+
easily reach out to us through our contact form on our website.
62+
</Text>
63+
<Text text80>
64+
Alternatively, you can reach us via email at [email protected], where our team is ready to assist you promptly. If
65+
you prefer speaking with someone directly, feel free to give us a call at 1-833-350-1066.
6866
</Text>
69-
<Image style={styles.icon} source={this.getChevron()}/>
7067
</View>
7168
);
7269
}
7370

74-
getBodyElement() {
71+
renderOptions = () => {
72+
return (
73+
<View marginH-page>
74+
<Text text70BO marginB-8>
75+
Minimum Height
76+
</Text>
77+
<Text text80 marginB-16>
78+
The expandable section can be either fully collapsed, partially expanded to reveal some of the items, or fully
79+
expanded by default.
80+
</Text>
81+
<SegmentedControl
82+
activeColor={Colors.$textDefaultLight}
83+
activeBackgroundColor={Colors.$backgroundInverted}
84+
segments={[{label: 'Default'}, {label: 'Partially'}, {label: 'Fully Expanded'}]}
85+
onChangeIndex={index => {
86+
switch (index) {
87+
case 0:
88+
return this.setState({minHeight: DEFAULT});
89+
case 1:
90+
return this.setState({minHeight: PARTIALLY_EXPANDED_HEIGHT});
91+
case 2:
92+
return this.setState({minHeight: FULLY_EXPANDED_HEIGHT});
93+
}
94+
}}
95+
/>
96+
</View>
97+
);
98+
};
99+
100+
renderExpandableSection = () => {
101+
const {expanded, minHeight} = this.state;
75102
return (
76-
<Carousel>
77-
{_.map(this.elements, (element, key) => {
78-
return (
79-
<View key={key} margin-12>
80-
{element}
81-
</View>
82-
);
83-
})}
84-
</Carousel>
103+
<ExpandableSection
104+
top={minHeight === PARTIALLY_EXPANDED_HEIGHT}
105+
expanded={expanded}
106+
sectionHeader={
107+
minHeight === PARTIALLY_EXPANDED_HEIGHT
108+
? this.renderReadMoreHeader()
109+
: this.renderHeader('How can I contact you?', expanded, {showInfo: true})
110+
}
111+
onPress={this.onExpand}
112+
minHeight={minHeight}
113+
>
114+
{this.renderContent()}
115+
</ExpandableSection>
85116
);
117+
};
118+
119+
renderNextItem() {
120+
return this.renderHeader('Where are you located?', false, {disabled: true, showInfo: true});
86121
}
87122

88123
render() {
89-
const {expanded, top} = this.state;
90-
124+
const {minHeight} = this.state;
91125
return (
92126
<ScrollView>
93-
<View row center margin-20>
94-
<Text grey10 text70 marginR-10>
95-
Open section on top
96-
</Text>
97-
<Switch
98-
value={this.state.top}
99-
onValueChange={() => {
100-
this.setState({top: !this.state.top});
101-
}}
102-
/>
103-
</View>
104-
<ExpandableSection
105-
top={top}
106-
expanded={expanded}
107-
sectionHeader={this.getHeaderElement()}
108-
onPress={() => this.onExpand()}
109-
>
110-
{this.getBodyElement()}
111-
</ExpandableSection>
112-
<ListItem>
113-
<Text grey10 text60 marginL-10>
114-
{'The next item'}
115-
</Text>
116-
</ListItem>
127+
<Text text40 margin-20>
128+
ExpandableSection
129+
</Text>
130+
{this.renderOptions()}
131+
{this.renderExpandableSection()}
132+
{minHeight !== PARTIALLY_EXPANDED_HEIGHT ? this.renderNextItem() : null}
117133
</ScrollView>
118134
);
119135
}

src/components/expandableSection/expandableSection.api.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616
"type": "() => void",
1717
"description": "Called when pressing the header of the ExpandableSection"
1818
},
19-
{"name": "testID","type": "string","description": "testing identifier"}
19+
{
20+
"name": "minHeight",
21+
"type": "number",
22+
"description": "Set a minimum height for the expandableSection. If the children height is less than the minHeight, the expandableSection will collapse to that height. If the children height is greater than the minHeight, the expandableSection will result with only the children rendered (sectionHeader will not be rendered)"
23+
},
24+
{"name": "testID", "type": "string", "description": "testing identifier"}
2025
],
2126
"snippet": [
2227
"<ExpandableSection",

src/components/expandableSection/index.tsx

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import React, {useMemo, useState} from 'react';
1+
import React, {useCallback, useMemo, useState} from 'react';
22
import {LayoutChangeEvent, StyleSheet} from 'react-native';
33
import {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
4+
import {useDidUpdate} from '../../hooks';
45
import View from '../view';
56
import TouchableOpacity from '../touchableOpacity';
67

@@ -25,6 +26,12 @@ export type ExpandableSectionProps = {
2526
* action for when pressing the header of the expandableSection
2627
*/
2728
onPress?: () => void;
29+
/**
30+
* Set a minimum height for the expandableSection
31+
* If the children height is less than the minHeight, the expandableSection will collapse to that height
32+
* If the children height is greater than the minHeight, the expandableSection will result with only the children rendered (sectionHeader will not be rendered)
33+
*/
34+
minHeight?: number;
2835
/**
2936
* Testing identifier
3037
*/
@@ -38,25 +45,39 @@ export type ExpandableSectionProps = {
3845
*/
3946

4047
function ExpandableSection(props: ExpandableSectionProps) {
41-
const {expanded, sectionHeader, onPress, children, top, testID} = props;
48+
const {minHeight, expanded, sectionHeader, onPress, children, top, testID} = props;
4249
const [height, setHeight] = useState(0);
4350
const animatedHeight = useSharedValue(0);
51+
const shouldShowSectionHeader = !minHeight || height > minHeight;
4452

4553
const onLayout = (event: LayoutChangeEvent) => {
46-
const onLayoutHeight = event.nativeEvent.layout.height;
54+
const layoutHeight = event.nativeEvent.layout.height;
4755

48-
if (onLayoutHeight > 0 && height !== onLayoutHeight) {
49-
setHeight(onLayoutHeight);
56+
if (layoutHeight > 0 && height !== layoutHeight) {
57+
setHeight(layoutHeight);
5058
}
5159
};
5260

53-
const expandableStyle = useAnimatedStyle(() => {
54-
animatedHeight.value = expanded ? withTiming(height) : withTiming(0);
61+
const animateHeight = useCallback((shouldAnimate = true) => {
62+
const collapsedHeight = Math.min(minHeight ?? 0, height);
63+
const toValue = expanded ? height : collapsedHeight;
64+
animatedHeight.value = shouldAnimate ? withTiming(toValue) : toValue;
65+
},
66+
[animatedHeight, expanded, height, minHeight]);
67+
68+
useDidUpdate(() => {
69+
animateHeight(false);
70+
}, [height, minHeight]);
71+
72+
useDidUpdate(() => {
73+
animateHeight();
74+
}, [expanded]);
5575

76+
const expandableStyle = useAnimatedStyle(() => {
5677
return {
5778
height: animatedHeight.value
5879
};
59-
}, [expanded, height]);
80+
}, []);
6081

6182
const style = useMemo(() => [styles.hidden, expandableStyle], [expandableStyle]);
6283

@@ -74,15 +95,19 @@ function ExpandableSection(props: ExpandableSectionProps) {
7495
);
7596
};
7697

77-
return (
78-
<View style={styles.hidden}>
79-
{top && renderChildren()}
80-
<TouchableOpacity onPress={onPress} testID={testID} accessibilityState={accessibilityState}>
81-
{sectionHeader}
82-
</TouchableOpacity>
83-
{!top && renderChildren()}
84-
</View>
85-
);
98+
if (shouldShowSectionHeader) {
99+
return (
100+
<View style={styles.hidden}>
101+
{top && renderChildren()}
102+
<TouchableOpacity onPress={onPress} testID={testID} accessibilityState={accessibilityState}>
103+
{sectionHeader}
104+
</TouchableOpacity>
105+
{!top && renderChildren()}
106+
</View>
107+
);
108+
} else {
109+
return renderChildren();
110+
}
86111
}
87112

88113
export default ExpandableSection;

0 commit comments

Comments
 (0)