Skip to content

Commit 4ecc9d2

Browse files
committed
feat(tabs): simplify tabs
1 parent a5ed041 commit 4ecc9d2

5 files changed

Lines changed: 48 additions & 176 deletions

File tree

src/components/Tabs/Tab.styles.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,31 @@ import { TextStyle, ViewStyle } from 'react-native';
33
import { Theme } from '../../theme/ThemeInterface';
44

55
export interface TabStyles {
6-
containerStyle: ViewStyle;
76
buttonStyle: ViewStyle;
87
textStyle: TextStyle;
9-
dividerStyle: ViewStyle;
8+
focusColor: string;
109
}
1110

12-
export type GetTabStyles = (props: {}, theme: Theme) => TabStyles;
11+
export interface GetTabStyleProps {
12+
shouldFit: boolean;
13+
isActive: boolean;
14+
}
15+
16+
export type GetTabStyles = (props: GetTabStyleProps, theme: Theme) => TabStyles;
1317

14-
export const getTabStyles: GetTabStyles = (props, theme) => {
18+
export const getTabStyles: GetTabStyles = ({ shouldFit, isActive }, theme) => {
1519
return {
1620
buttonStyle: {
17-
borderRadius: 0,
21+
backgroundColor: isActive ? 'white' : 'transparent',
22+
...(shouldFit
23+
? {
24+
flex: 1,
25+
}
26+
: {}),
1827
},
19-
containerStyle: {},
20-
dividerStyle: {
21-
borderRadius: 0,
22-
bottom: 0,
23-
position: 'absolute',
28+
focusColor: theme.colors.background.greyLight,
29+
textStyle: {
30+
color: isActive ? theme.colors.text.primary : theme.colors.text.muted,
2431
},
25-
textStyle: {},
2632
};
2733
};

src/components/Tabs/Tab.tsx

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import * as React from 'react';
22
import { DeepPartial, Omit } from 'ts-essentials';
33

4-
import { initialMeasurements, Measurements } from '../../hooks';
4+
import { Measurements } from '../../hooks';
55
import { useTheme } from '../../theme';
66
import { mergeStyles, ReplaceReturnType } from '../../utils/mergeStyles';
77
import { Button, ButtonProps } from '../Button';
8-
import { ViewMeasure } from '../Helpers';
98
import { GetTabStyles, getTabStyles, TabStyles } from './Tab.styles';
109

1110
export interface TabProps
1211
extends Omit<Omit<ButtonProps, 'onPress'>, 'getStyles'> {
1312
index: number;
1413
isActive?: boolean;
14+
shouldFit?: boolean;
1515
getStyles?: ReplaceReturnType<GetTabStyles, DeepPartial<TabStyles>>;
1616
onPress: (index: number) => void;
1717
onActive?: (index: number, measurements: Measurements) => void;
@@ -26,28 +26,21 @@ export const Tab = (props: TabProps) => {
2626
onActive = () => {
2727
return;
2828
},
29+
shouldFit = true,
2930
...buttonProps
3031
} = props;
31-
const [measurements, setMeasurements] = React.useState(initialMeasurements);
3232
const theme = useTheme();
33-
const { containerStyle, buttonStyle, textStyle } = mergeStyles(
33+
const { buttonStyle, textStyle, focusColor } = mergeStyles(
3434
getTabStyles,
3535
getStyles,
36-
)({}, theme);
37-
38-
React.useEffect(() => {
39-
if (isActive) onActive(index, measurements);
40-
}, [isActive, measurements]);
36+
)({ isActive, shouldFit }, theme);
4137

4238
return (
43-
<ViewMeasure onMeasure={setMeasurements} style={containerStyle}>
44-
<Button
45-
color={isActive ? 'primary' : 'default'}
46-
appearance="minimal"
47-
getStyles={() => ({ buttonStyle, textStyle })}
48-
onPress={() => onPress(index)}
49-
{...buttonProps}
50-
/>
51-
</ViewMeasure>
39+
<Button
40+
color={isActive ? 'primary' : 'default'}
41+
getStyles={() => ({ buttonStyle, focusColor, textStyle })}
42+
onPress={() => onPress(index)}
43+
{...buttonProps}
44+
/>
5245
);
5346
};

src/components/Tabs/Tabs.mdx

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,12 @@ You can use a custom `Tab` component as long as it accepts `isActive`, `onPress`
2020

2121
<Playground>
2222
<Tabs
23-
ActiveBar={undefined} // The indicator of active tab. Currently works only when distribution="fit"
2423
defaultActiveTabIndex={1}
2524
activeTabIndex={undefined}
2625
onPress={undefined}
27-
distribution="fit" // or 'scrollable'
26+
shouldFit
2827
getStyles={(props, theme) => ({
29-
containerStyle: {}, // Only applied when distribution="fit"
30-
tabContainerStyle: {},
31-
buttonStyle: {},
32-
textStyle: {},
33-
dividerStyle: {},
28+
containerStyle: {},
3429
})}
3530
>
3631
{new Array(4).fill(0).map((v, i) => (
@@ -39,18 +34,6 @@ You can use a custom `Tab` component as long as it accepts `isActive`, `onPress`
3934
</Tabs>
4035
</Playground>
4136

42-
### Scrollable
43-
44-
Scrollable Tabs will also inline `Tab`
45-
46-
<Playground>
47-
<Tabs distribution="scrollable">
48-
{new Array(20).fill(0).map((v, i) => (
49-
<Tab key={i} title={`Tab ${i + 1}`} />
50-
))}
51-
</Tabs>
52-
</Playground>
53-
5437
### Props
5538

5639
<Props of={Tabs} />

src/components/Tabs/Tabs.styles.ts

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,21 @@
1-
import { TextStyle, ViewStyle } from 'react-native';
1+
import { ViewStyle } from 'react-native';
22

33
import { Theme } from '../../theme/ThemeInterface';
4-
import { TabsDistribution } from './Tabs';
54

65
export interface TabsStyles {
76
containerStyle: ViewStyle;
8-
tabContainerStyle: ViewStyle;
9-
buttonStyle: ViewStyle;
10-
textStyle: TextStyle;
11-
dividerStyle: ViewStyle;
127
}
8+
export type GetTabsStyles = (props: {}, theme: Theme) => TabsStyles;
139

14-
export interface GetTabsStylesProps {
15-
distribution: TabsDistribution;
16-
}
17-
18-
export type GetTabsStyles = (
19-
props: GetTabsStylesProps,
20-
theme: Theme,
21-
) => TabsStyles;
22-
23-
export const getTabsStyles: GetTabsStyles = ({ distribution }, theme) => {
10+
export const getTabsStyles: GetTabsStyles = (props, theme) => {
2411
return {
25-
buttonStyle: {},
2612
containerStyle: {
13+
backgroundColor: theme.colors.background.greyDefault,
14+
borderRadius: theme.controlBorderRadius.medium,
2715
flex: 1,
2816
flexDirection: 'row',
2917
justifyContent: 'flex-start',
18+
padding: 2,
3019
},
31-
dividerStyle: {},
32-
tabContainerStyle:
33-
distribution === 'fit'
34-
? {
35-
flex: 1,
36-
}
37-
: {},
38-
textStyle: {},
3920
};
4021
};

src/components/Tabs/Tabs.tsx

Lines changed: 12 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,21 @@
11
import * as React from 'react';
2-
import { FlatList, View } from 'react-native';
3-
import { animated, useSpring } from 'react-spring/native.cjs';
2+
import { View } from 'react-native';
43
import { DeepPartial } from 'ts-essentials';
54

6-
import { springDefaultConfig } from '../../constants/Animation';
7-
import { initialMeasurements, Measurements } from '../../hooks';
5+
import { Measurements } from '../../hooks';
86
import { useTheme } from '../../theme';
97
import { mergeStyles, ReplaceReturnType } from '../../utils/mergeStyles';
10-
import { Box } from '../Box';
118
import { TabProps } from './Tab';
129
import { GetTabsStyles, getTabsStyles, TabsStyles } from './Tabs.styles';
1310

1411
export type TabsDistribution = 'scrollable' | 'fit';
1512

16-
const AnimatedView = animated(View);
17-
1813
export interface TabsProps {
19-
/** The indicator of active tab. Currently works only when distribution="fit" */
20-
ActiveBar?: React.ComponentType<ActiveBarProps>;
2114
activeTabIndex?: number;
2215
children: Array<React.ReactElement<TabProps>>;
2316
defaultActiveTabIndex?: number;
2417
getStyles?: ReplaceReturnType<GetTabsStyles, DeepPartial<TabsStyles>>;
25-
distribution?: TabsDistribution;
18+
shouldFit?: boolean;
2619

2720
onPress: (index: number) => void;
2821
}
@@ -36,130 +29,46 @@ export interface ActiveBarProps {
3629
measurements: Measurements;
3730
}
3831

39-
const DefaultActiveBar = (props: ActiveBarProps) => {
40-
const { index, measurements } = props;
41-
const theme = useTheme();
42-
43-
const { x, width } = useSpring({
44-
config: springDefaultConfig,
45-
46-
width: measurements.width,
47-
x: index * measurements.width,
48-
});
49-
50-
return (
51-
<AnimatedView
52-
// @ts-ignore
53-
style={{
54-
backgroundColor: theme.colors.background.primaryDefault,
55-
height: 3,
56-
left: x,
57-
position: 'absolute',
58-
top: measurements.height,
59-
width,
60-
}}
61-
/>
62-
);
63-
};
64-
6532
export const Tabs = (props: TabsProps) => {
6633
const {
67-
ActiveBar = DefaultActiveBar,
6834
children,
6935
activeTabIndex,
7036
defaultActiveTabIndex = 0,
7137
onPress,
7238
getStyles,
73-
distribution = 'fit',
39+
shouldFit = false,
7440
} = props;
7541
const [localActiveTabIndex, setLocalActiveTabIndex] = React.useState(
7642
defaultActiveTabIndex,
7743
);
7844
const theme = useTheme();
7945
const isControlled = !!(activeTabIndex || onPress);
80-
const listRef = React.useRef<FlatList<TabProps>>(null);
81-
const [activeBarProps, setActiveBarProps] = React.useState({
82-
index: (isControlled ? activeTabIndex : localActiveTabIndex) || -1,
83-
measurements: initialMeasurements,
84-
});
85-
86-
React.useEffect(() => {
87-
if (listRef.current && distribution === 'scrollable') {
88-
if (isControlled && activeTabIndex) {
89-
listRef.current.scrollToIndex({
90-
animated: true,
91-
index: Math.max(activeTabIndex - 2, 0),
92-
});
93-
} else if (localActiveTabIndex) {
94-
listRef.current.scrollToIndex({
95-
animated: true,
96-
index: Math.max(localActiveTabIndex - 2, 0),
97-
});
98-
}
99-
}
100-
});
10146

102-
const {
103-
containerStyle,
104-
tabContainerStyle,
105-
textStyle,
106-
buttonStyle,
107-
dividerStyle,
108-
} = mergeStyles(getTabsStyles, getStyles)({ distribution }, theme);
47+
const { containerStyle } = mergeStyles(getTabsStyles, getStyles)({}, theme);
10948

11049
const data = React.Children.map(children, (child, index) => {
11150
if (!child) return null;
11251

113-
const tabCommonProps: Partial<TabProps> = {
114-
getStyles: () => ({
115-
buttonStyle,
116-
containerStyle: tabContainerStyle,
117-
dividerStyle,
118-
textStyle,
119-
}),
120-
index,
121-
onActive: (i, measurements) => {
122-
setActiveBarProps({ index, measurements });
123-
},
124-
};
125-
12652
return isControlled
12753
? {
128-
...tabCommonProps,
54+
index,
12955
isActive: index === activeTabIndex,
13056
onPress: i => onPress(i),
57+
shouldFit,
13158
}
13259
: {
133-
...tabCommonProps,
60+
index,
13461
isActive: index === localActiveTabIndex,
13562
onPress: i => setLocalActiveTabIndex(i),
63+
shouldFit,
13664
};
13765
}) as TabProps[];
13866

13967
const tabs = React.Children.toArray(children);
14068

141-
if (distribution === 'fit') {
142-
return (
143-
<View style={containerStyle}>
144-
{data.map((tabProps, index) =>
145-
React.cloneElement(tabs[index], tabProps),
146-
)}
147-
<ActiveBar {...activeBarProps} />
148-
</View>
149-
);
150-
}
151-
15269
return (
153-
<>
154-
<FlatList
155-
ref={listRef}
156-
horizontal
157-
keyExtractor={item => `${item.index}`}
158-
data={data}
159-
renderItem={({ item, index }) => React.cloneElement(tabs[index], item)}
160-
showsHorizontalScrollIndicator={false}
161-
ListFooterComponent={() => <Box width="100%" testID="zxcv" />}
162-
/>
163-
</>
70+
<View style={containerStyle}>
71+
{data.map((tabProps, index) => React.cloneElement(tabs[index], tabProps))}
72+
</View>
16473
);
16574
};

0 commit comments

Comments
 (0)