Skip to content

Commit ab1a247

Browse files
authored
Refactor TabController to use Reanimated v2 API (#1325)
* WIP - Refactor TabController to use Reanimated v2 API * Fix focus scroll and other animation issues * Fix TS errors * Fix transition issue conflicted by page carousel scrolling * Fix onChangeIndex callback * remove font weight from selected label style * Clean up * cleanup code * export TabController2 separately * Rename loaded to shouldLoad to make it more readable * Fix naming * Fix exception in TabBarItem * Fix issue with timing of lazy loading in TabPage * Fix TS error of PageCarousel ref * Support missing features: spreadItems, indicatorInsets * Round value in the right place * improve code * Remove redundant fragment * declare itemStates type * remove itemStates from context and cleanup code * Fix support for passing custom style to TabBarItem * Fix lint, include generated types * export tabController2 in index.ts as well * Export TabController2 in generateTypes index * Fix reanimated exception with attempt to access stylesheet objects * Pass labelStyle and selectedLabelStyle to expose the potential error in TabBarItem * Add comments * Fix typings
1 parent ec5aa97 commit ab1a247

File tree

22 files changed

+1514
-11
lines changed

22 files changed

+1514
-11
lines changed

demo/src/screens/componentScreens/TabControllerScreen/index.tsx

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, {Component} from 'react';
2-
import {ActivityIndicator} from 'react-native';
2+
import {ActivityIndicator, StyleSheet} from 'react-native';
33
import {Assets, TabController, Colors, View, Text, Button, TabControllerItemProps} from 'react-native-ui-lib';
44
import {gestureHandlerRootHOC} from 'react-native-gesture-handler';
55
import _ from 'lodash';
@@ -20,7 +20,6 @@ interface State {
2020
}
2121

2222
class TabControllerScreen extends Component<{}, State> {
23-
2423
constructor(props: {}) {
2524
super(props);
2625
this.state = {
@@ -36,12 +35,23 @@ class TabControllerScreen extends Component<{}, State> {
3635
}
3736

3837
generateTabItems = (fewItems = this.state.fewItems): TabControllerItemProps[] => {
39-
let items: TabControllerItemProps[] = _.chain(TABS)
38+
const items: TabControllerItemProps[] = _.chain(TABS)
4039
.take(fewItems ? 3 : TABS.length)
41-
.map<TabControllerItemProps>(tab => ({label: tab, key: tab}))
40+
.map<TabControllerItemProps>((tab, index) => ({
41+
label: tab,
42+
key: tab,
43+
icon: index === 2 ? Assets.icons.demo.dashboard : undefined,
44+
badge: index === 5 ? {label: '2'} : undefined
45+
}))
4246
.value();
4347

44-
const addItem: TabControllerItemProps = {icon: Assets.icons.demo.add, key: 'add', ignore: true, width: 60, onPress: this.onAddItem};
48+
const addItem: TabControllerItemProps = {
49+
icon: Assets.icons.demo.add,
50+
key: 'add',
51+
ignore: true,
52+
width: 60,
53+
onPress: this.onAddItem
54+
};
4555

4656
return fewItems ? items : [...items, addItem];
4757
};
@@ -93,7 +103,7 @@ class TabControllerScreen extends Component<{}, State> {
93103
renderLoadingPage() {
94104
return (
95105
<View flex center>
96-
<ActivityIndicator size="large" />
106+
<ActivityIndicator size="large"/>
97107
<Text text60L marginT-10>
98108
Loading
99109
</Text>
@@ -108,13 +118,13 @@ class TabControllerScreen extends Component<{}, State> {
108118
return (
109119
<Container {...containerProps}>
110120
<TabController.TabPage index={0}>
111-
<Tab1 />
121+
<Tab1/>
112122
</TabController.TabPage>
113123
<TabController.TabPage index={1}>
114-
<Tab2 />
124+
<Tab2/>
115125
</TabController.TabPage>
116126
<TabController.TabPage index={2} lazy lazyLoadTime={1500} renderLoading={this.renderLoadingPage}>
117-
<Tab3 />
127+
<Tab3/>
118128
</TabController.TabPage>
119129

120130
{_.map(_.takeRight(TABS, TABS.length - 3), (title, index) => {
@@ -147,10 +157,12 @@ class TabControllerScreen extends Component<{}, State> {
147157
// uppercase
148158
// indicatorStyle={{backgroundColor: 'green', height: 3}}
149159
// indicatorInsets={0}
150-
// spreadItems={false}
160+
spreadItems={!fewItems}
161+
backgroundColor={fewItems ? 'transparent' : undefined}
151162
// labelColor={'green'}
152163
// selectedLabelColor={'red'}
153-
// labelStyle={{fontSize: 20}}
164+
labelStyle={styles.labelStyle}
165+
selectedLabelStyle={styles.selectedLabelStyle}
154166
// iconColor={'green'}
155167
// selectedIconColor={'blue'}
156168
enableShadow
@@ -192,3 +204,12 @@ class TabControllerScreen extends Component<{}, State> {
192204
}
193205

194206
export default gestureHandlerRootHOC(TabControllerScreen);
207+
208+
const styles = StyleSheet.create({
209+
labelStyle: {
210+
fontSize: 16
211+
},
212+
selectedLabelStyle: {
213+
fontSize: 16
214+
}
215+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react';
2+
import { ViewProps, ScrollViewProps } from 'react-native';
3+
import { ForwardRefInjectedProps } from '../../commons/forwardRef';
4+
export declare type FadedScrollViewProps = ViewProps & ScrollViewProps & {
5+
children?: React.ReactNode | React.ReactNode[];
6+
};
7+
declare type Props = FadedScrollViewProps & ForwardRefInjectedProps;
8+
declare const _default: React.ComponentType<Props>;
9+
export default _default;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/// <reference types="react" />
2+
/**
3+
* @description: TabController's Page Carousel
4+
* @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/TabControllerScreen/index.tsx
5+
* @notes: You must pass `asCarousel` flag to TabController and render your TabPages inside a PageCarousel
6+
*/
7+
declare function PageCarousel({ ...props }: {
8+
[x: string]: any;
9+
}): JSX.Element;
10+
export default PageCarousel;
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import React from 'react';
2+
import { StyleProp, ViewStyle } from 'react-native';
3+
import { TabControllerItemProps } from './TabBarItem';
4+
export interface TabControllerBarProps {
5+
/**
6+
* The list of tab bar items
7+
*/
8+
items?: TabControllerItemProps[];
9+
/**
10+
* Tab Bar height
11+
*/
12+
height?: number;
13+
/**
14+
* Show Tab Bar bottom shadow
15+
*/
16+
enableShadow?: boolean;
17+
/**
18+
* custom shadow style
19+
*/
20+
shadowStyle?: StyleProp<ViewStyle>;
21+
/**
22+
* custom style for the selected indicator
23+
*/
24+
indicatorStyle?: StyleProp<ViewStyle>;
25+
/**
26+
* custom label style
27+
*/
28+
labelStyle?: TabControllerItemProps['labelStyle'];
29+
/**
30+
* custom selected label style
31+
*/
32+
selectedLabelStyle?: TabControllerItemProps['selectedLabelStyle'];
33+
/**
34+
* the default label color
35+
*/
36+
labelColor?: string;
37+
/**
38+
* the selected label color
39+
*/
40+
selectedLabelColor?: string;
41+
/**
42+
* whether to change the text to uppercase
43+
*/
44+
uppercase?: boolean;
45+
/**
46+
* icon tint color
47+
*/
48+
iconColor?: string;
49+
/**
50+
* icon selected tint color
51+
*/
52+
selectedIconColor?: string;
53+
/**
54+
* TODO: rename to feedbackColor
55+
* Apply background color on press for TouchableOpacity
56+
*/
57+
activeBackgroundColor?: string;
58+
/**
59+
* The TabBar background Color
60+
*/
61+
backgroundColor?: string;
62+
/**
63+
* The TabBar container width
64+
*/
65+
containerWidth?: number;
66+
/**
67+
* Pass to center selected item
68+
*/
69+
centerSelected?: boolean;
70+
/**
71+
* Whether the tabBar should be spread (default: true)
72+
*/
73+
spreadItems?: boolean;
74+
/**
75+
* The indicator insets (default: Spacings.s4, set to 0 to make it wide as the item)
76+
*/
77+
indicatorInsets?: number;
78+
/**
79+
* Additional styles for the container
80+
*/
81+
containerStyle?: StyleProp<ViewStyle>;
82+
/**
83+
* Used as a testing identifier
84+
*/
85+
testID?: string;
86+
}
87+
declare const _default: React.ComponentClass<TabControllerBarProps & {
88+
useCustomTheme?: boolean | undefined;
89+
}, any>;
90+
export default _default;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react';
2+
import Reanimated from 'react-native-reanimated';
3+
interface TabControllerContext {
4+
selectedIndex?: number;
5+
items?: any[];
6+
asCarousel?: boolean;
7+
containerWidth: Reanimated.SharedValue<number>;
8+
pageWidth?: number;
9+
/** static page index */
10+
currentPage: Reanimated.SharedValue<number>;
11+
/** transition page index (can be a fraction when transitioning between pages) */
12+
targetPage: Reanimated.SharedValue<number>;
13+
carouselOffset: Reanimated.SharedValue<number>;
14+
}
15+
declare const TabBarContext: React.Context<TabControllerContext>;
16+
export default TabBarContext;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/// <reference types="react" />
2+
import { TextStyle, LayoutChangeEvent, StyleProp, ViewStyle } from 'react-native';
3+
import Reanimated from 'react-native-reanimated';
4+
import { BadgeProps } from '../badge';
5+
export interface TabControllerItemProps {
6+
/**
7+
* label of the tab
8+
*/
9+
label?: string;
10+
/**
11+
* custom label style
12+
*/
13+
labelStyle?: StyleProp<TextStyle>;
14+
/**
15+
* custom selected label style
16+
*/
17+
selectedLabelStyle?: StyleProp<TextStyle>;
18+
/**
19+
* the default label color
20+
*/
21+
labelColor?: string;
22+
/**
23+
* the selected label color
24+
*/
25+
selectedLabelColor?: string;
26+
/**
27+
* icon of the tab
28+
*/
29+
icon?: number;
30+
/**
31+
* icon tint color
32+
*/
33+
iconColor?: string;
34+
/**
35+
* icon selected tint color
36+
*/
37+
selectedIconColor?: string;
38+
/**
39+
* Badge component props to display next the item label
40+
*/
41+
badge?: BadgeProps;
42+
/**
43+
* A fixed width for the item
44+
*/
45+
width?: number;
46+
/**
47+
* ignore of the tab
48+
*/
49+
ignore?: boolean;
50+
/**
51+
* callback for when pressing a tab
52+
*/
53+
onPress?: (index: number) => void;
54+
/**
55+
* whether to change the text to uppercase
56+
*/
57+
uppercase?: boolean;
58+
/**
59+
* The active opacity when pressing a tab
60+
*/
61+
activeOpacity?: number;
62+
/**
63+
* TODO: rename to feedbackColor
64+
* Apply background color on press for TouchableOpacity
65+
*/
66+
activeBackgroundColor?: string;
67+
/**
68+
* Pass custom style
69+
*/
70+
style?: StyleProp<ViewStyle>;
71+
/**
72+
* Used as a testing identifier
73+
*/
74+
testID?: string;
75+
}
76+
interface Props extends TabControllerItemProps {
77+
index: number;
78+
targetPage: any;
79+
currentPage: Reanimated.Adaptable<number>;
80+
onLayout?: (event: LayoutChangeEvent, index: number) => void;
81+
}
82+
/**
83+
* @description: TabController's TabBarItem
84+
* @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/TabControllerScreen/index.tsx
85+
* @notes: Must be rendered as a direct child of TabController.TabBar.
86+
*/
87+
export default function TabBarItem({ index, label, labelColor, selectedLabelColor, labelStyle, selectedLabelStyle, icon, badge, uppercase, activeOpacity, activeBackgroundColor, testID, ignore, style, ...props }: Props): JSX.Element;
88+
export {};
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { PropsWithChildren } from 'react';
2+
export interface TabControllerPageProps {
3+
/**
4+
* The index of the the TabPage
5+
*/
6+
index: number;
7+
/**
8+
* Whether this page should be loaded lazily
9+
*/
10+
lazy?: boolean;
11+
/**
12+
* How long to wait till lazy load complete (good for showing loader screens)
13+
*/
14+
lazyLoadTime?: number;
15+
/**
16+
* Render a custom loading page when lazy loading
17+
*/
18+
renderLoading?: () => JSX.Element;
19+
/**
20+
* Used as a testing identifier
21+
*/
22+
testID?: string;
23+
}
24+
/**
25+
* @description: TabController's TabPage
26+
* @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/TabControllerScreen/index.tsx
27+
*/
28+
export default function TabPage({ testID, index, lazy, renderLoading, ...props }: PropsWithChildren<TabControllerPageProps>): JSX.Element;
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React, { PropsWithChildren } from 'react';
2+
import { TabControllerItemProps } from './TabBarItem';
3+
export { TabControllerItemProps };
4+
export interface TabControllerProps {
5+
/**
6+
* The list of tab bar items
7+
*/
8+
items: TabControllerItemProps[];
9+
/**
10+
* Initial selected index
11+
*/
12+
selectedIndex: number;
13+
/**
14+
* callback for when index has change (will not be called on ignored items)
15+
*/
16+
onChangeIndex?: (index: number, prevIndex: number | null) => void;
17+
/**
18+
* When using TabController.PageCarousel this should be turned on
19+
*/
20+
asCarousel?: boolean;
21+
/**
22+
* Pass for custom carousel page width
23+
*/
24+
carouselPageWidth?: number;
25+
}
26+
/**
27+
* @description: A performant solution for a tab controller with lazy load mechanism
28+
* @example: https://github.com/wix/react-native-ui-lib/blob/master/demo/src/screens/componentScreens/TabControllerScreen/index.tsx
29+
* @notes: This component is based on react-native-gesture-handler
30+
* @important: On Android, if using react-native-navigation, make sure to wrap your screen with gestureHandlerRootHOC
31+
* @importantLink: https://kmagiera.github.io/react-native-gesture-handler/docs/getting-started.html#with-wix-react-native-navigation-https-githubcom-wix-react-native-navigation
32+
*/
33+
declare function TabController({ selectedIndex, asCarousel, items, onChangeIndex, carouselPageWidth, children }: PropsWithChildren<TabControllerProps>): JSX.Element | null;
34+
declare namespace TabController {
35+
var TabBar: React.ComponentClass<import("./TabBar").TabControllerBarProps & {
36+
useCustomTheme?: boolean | undefined;
37+
}, any>;
38+
var TabBarItem: typeof import("./TabBarItem").default;
39+
var TabPage: typeof import("./TabPage").default;
40+
var PageCarousel: typeof import("./PageCarousel").default;
41+
}
42+
declare const _default: React.ComponentClass<TabControllerProps & {
43+
useCustomTheme?: boolean | undefined;
44+
}, any> & typeof TabController;
45+
export default _default;

0 commit comments

Comments
 (0)