Skip to content

Commit fd931ea

Browse files
authored
Support rendering TabController inside a ScrollView (with a header) (#3235)
* Support rendering TabController inside a ScrollView (with a header) * Wrap screen with gestureHandlerRootHOC * Small fix - code review
1 parent abf77aa commit fd931ea

File tree

7 files changed

+96
-9
lines changed

7 files changed

+96
-9
lines changed

demo/src/screens/MenuStructure.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export const navigationData = {
114114
{title: 'Modal', tags: 'modal topbar screen', screen: 'unicorn.screens.ModalScreen'},
115115
{title: 'StateScreen', tags: 'empty state screen', screen: 'unicorn.screens.EmptyStateScreen'},
116116
{title: 'TabController', tags: 'tabbar controller native', screen: 'unicorn.components.TabControllerScreen'},
117+
{title: 'TabControllerWithStickyHeader', tags: 'tabbar controller native sticky header', screen: 'unicorn.components.TabControllerWithStickyHeaderScreen'},
117118
{title: 'Timeline', tags: 'timeline', screen: 'unicorn.components.TimelineScreen'},
118119
{
119120
title: 'withScrollEnabler',
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React, {Component} from 'react';
2+
import {ScrollView} from 'react-native';
3+
import {View, Text, Card, Image, TabController} from 'react-native-ui-lib';
4+
import _ from 'lodash';
5+
6+
const IMAGE_URL =
7+
'https://images.pexels.com/photos/1598505/pexels-photo-1598505.jpeg?auto=compress&cs=tinysrgb&w=500&dpr=1';
8+
const items = [
9+
{key: 'tab1', label: 'Tab 1'},
10+
{key: 'tab2', label: 'Tab 2'}
11+
];
12+
13+
export default class TabControllerWithStickyHeaderScreen extends Component {
14+
renderHeader = () => {
15+
return (
16+
<View bg-red30 height={280} bottom>
17+
<Image source={{uri: IMAGE_URL}} style={{flex: 1}}/>
18+
</View>
19+
);
20+
};
21+
22+
renderTab1 = () => {
23+
return (
24+
<View bg-green80 paddingT-s5>
25+
{_.times(7, i => {
26+
return (
27+
<Card key={i} height={100} marginB-s5 marginH-s5 center>
28+
<Text text40>item {i}</Text>
29+
</Card>
30+
);
31+
})}
32+
</View>
33+
);
34+
};
35+
36+
renderTab2 = () => {
37+
return (
38+
<View bg-orange40 paddingT-s5>
39+
{_.times(15, i => {
40+
return (
41+
<View key={i} height={100} marginB-s5 marginH-s5 center bg-orange60>
42+
<Text text40> item {i}</Text>
43+
</View>
44+
);
45+
})}
46+
</View>
47+
);
48+
};
49+
50+
render() {
51+
return (
52+
<TabController items={items} nestedInScrollView>
53+
<ScrollView
54+
// stickyHeaderHiddenOnScroll
55+
stickyHeaderIndices={[1]}
56+
>
57+
{this.renderHeader()}
58+
<TabController.TabBar/>
59+
60+
<View flex>
61+
<TabController.TabPage index={0}>{this.renderTab1()}</TabController.TabPage>
62+
<TabController.TabPage index={1}>{this.renderTab2()}</TabController.TabPage>
63+
</View>
64+
</ScrollView>
65+
</TabController>
66+
);
67+
}
68+
}

demo/src/screens/componentScreens/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export function registerScreens(registrar) {
5555
registrar('unicorn.components.StepperScreen', () => require('./StepperScreen').default);
5656
registrar('unicorn.components.SwitchScreen', () => require('./SwitchScreen').default);
5757
registrar('unicorn.components.TabControllerScreen', () => require('./TabControllerScreen').default);
58+
registrar('unicorn.components.TabControllerWithStickyHeaderScreen', () => gestureHandlerRootHOC(require('./TabControllerWithStickyHeaderScreen').default));
5859
registrar('unicorn.components.TextFieldScreen', () => require('./TextFieldScreen').default);
5960
registrar('unicorn.components.TextScreen', () => require('./TextScreen').default);
6061
registrar('unicorn.components.ToastsScreen', () => require('./ToastsScreen').default);

src/components/tabController/TabBarContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ interface TabControllerContext {
66
items?: any[];
77
itemsCount: number;
88
asCarousel?: boolean;
9+
nestedInScrollView?: boolean;
910
containerWidth: number;
1011
pageWidth: number;
1112
/** static page index */

src/components/tabController/TabPage.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default function TabPage({
4444
lazyLoadTime = 100,
4545
...props
4646
}: PropsWithChildren<TabControllerPageProps>) {
47-
const {currentPage, asCarousel, containerWidth} = useContext(TabBarContext);
47+
const {currentPage, asCarousel, nestedInScrollView, containerWidth} = useContext(TabBarContext);
4848
const [shouldLoad, setLoaded] = useState(!lazy);
4949
// const [focused, setFocused] = useState(false);
5050

@@ -80,19 +80,22 @@ export default function TabPage({
8080

8181
const animatedPageStyle = useAnimatedStyle(() => {
8282
const isActive = Math.round(currentPage.value) === index;
83-
return {
83+
84+
// TODO: Fix to proper animated style once Reanimated export AnimatedStyleProp
85+
const style: any = {
8486
opacity: isActive || asCarousel ? 1 : 0,
8587
zIndex: isActive || asCarousel ? 1 : 0
8688
};
89+
90+
if (nestedInScrollView) {
91+
style.position = isActive ? 'relative' : 'absolute';
92+
}
93+
94+
return style;
8795
});
8896

8997
const _style = useMemo(() => {
90-
return [
91-
!asCarousel && styles.page,
92-
animatedPageStyle,
93-
{width: asCarousel ? containerWidth : undefined},
94-
style
95-
];
98+
return [!asCarousel && styles.page, animatedPageStyle, {width: asCarousel ? containerWidth : undefined}, style];
9699
}, [asCarousel, animatedPageStyle, containerWidth, style]);
97100

98101
return (

src/components/tabController/apis/tabController.api.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@
1919
"description": "When using TabController.PageCarousel this should be turned on",
2020
"default": "false"
2121
},
22+
{
23+
"name": "nestedInScrollView",
24+
"type": "boolean",
25+
"description": "Pass when TabController is render inside a ScrollView (with a header)",
26+
"note": "Does not work with asCarousel",
27+
"default": "false"
28+
},
2229
{"name": "carouselPageWidth;", "type": "number", "description": "Pass for custom carousel page width"}
2330
],
2431
"snippet": [

src/components/tabController/index.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ export interface TabControllerProps {
3737
* When using TabController.PageCarousel this should be turned on
3838
*/
3939
asCarousel?: boolean;
40+
/**
41+
* Pass when TabController is render inside a ScrollView (with a header)
42+
*/
43+
nestedInScrollView?: boolean;
4044
/**
4145
* Pass for custom carousel page width
4246
*/
@@ -65,6 +69,7 @@ const TabController = React.forwardRef((props: PropsWithChildren<TabControllerPr
6569
const {
6670
initialIndex = 0,
6771
asCarousel = false,
72+
nestedInScrollView = false,
6873
items,
6974
onChangeIndex = _.noop,
7075
carouselPageWidth,
@@ -123,6 +128,7 @@ const TabController = React.forwardRef((props: PropsWithChildren<TabControllerPr
123128
initialIndex,
124129
asCarousel,
125130
pageWidth,
131+
nestedInScrollView,
126132
/* Items */
127133
items,
128134
ignoredItems,
@@ -135,7 +141,7 @@ const TabController = React.forwardRef((props: PropsWithChildren<TabControllerPr
135141
onChangeIndex,
136142
setCurrentIndex
137143
};
138-
}, [initialIndex, asCarousel, items, onChangeIndex, screenWidth]);
144+
}, [initialIndex, asCarousel, items, onChangeIndex, screenWidth, nestedInScrollView]);
139145

140146
return <TabBarContext.Provider value={context}>{children}</TabBarContext.Provider>;
141147
});

0 commit comments

Comments
 (0)