Skip to content

Commit aaa331a

Browse files
author
Artur Bien
committed
feat(tabs): add tabs component
1 parent b4c85a3 commit aaa331a

File tree

7 files changed

+257
-10
lines changed

7 files changed

+257
-10
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import React, { useState } from 'react';
2+
import { View } from 'react-native';
3+
import { Tabs, Panel, Text } from 'react95-native';
4+
5+
const AppBarExample = () => {
6+
const [value, setValue] = useState(0);
7+
const [secondValue, setSecondValue] = useState(0);
8+
9+
return (
10+
<Panel variant='clear' style={[{ flex: 1 }]}>
11+
<View style={[{ padding: 8 }]}>
12+
<Tabs value={value} onChange={setValue}>
13+
<Tabs.Tab value={0}>Shoes</Tabs.Tab>
14+
<Tabs.Tab value={1}>Accesories</Tabs.Tab>
15+
<Tabs.Tab value={2}>Clothing</Tabs.Tab>
16+
</Tabs>
17+
<Tabs.Body style={[{ height: 200 }]}>
18+
<Text>{value}</Text>
19+
</Tabs.Body>
20+
</View>
21+
22+
<View style={[{ padding: 8 }]}>
23+
<Tabs stretch value={secondValue} onChange={setSecondValue}>
24+
<Tabs.Tab value={0}>Shoes</Tabs.Tab>
25+
<Tabs.Tab value={1}>A</Tabs.Tab>
26+
<Tabs.Tab value={2}>Clothing</Tabs.Tab>
27+
</Tabs>
28+
<Tabs.Body style={[{ height: 200 }]}>
29+
<Text>{secondValue}</Text>
30+
</Tabs.Body>
31+
</View>
32+
</Panel>
33+
);
34+
};
35+
36+
export default AppBarExample;

example/src/examples/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import MenuExample from './MenuExample';
1313
import ProgressExample from './ProgressExample';
1414
import SelectExample from './SelectExample';
1515
import DesktopExample from './DesktopExample';
16+
import TabsExample from './TabsExample';
1617

1718
export default [
1819
{ name: 'ButtonExample', component: ButtonExample, title: 'Button' },
@@ -30,6 +31,7 @@ export default [
3031
{ name: 'ProgressExample', component: ProgressExample, title: 'Progress' },
3132
{ name: 'SelectExample', component: SelectExample, title: 'Select' },
3233
{ name: 'DesktopExample', component: DesktopExample, title: 'Desktop' },
34+
{ name: 'TabsExample', component: TabsExample, title: 'Tabs' },
3335
].sort((a, b) => {
3436
/* Sort screens alphabetically */
3537
if (a.title < b.title) return -1;

src/Tabs/Tabs.tsx

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import React, { useState } from 'react';
2+
import {
3+
StyleProp,
4+
StyleSheet,
5+
ViewStyle,
6+
TouchableHighlight,
7+
View,
8+
} from 'react-native';
9+
10+
import { original as theme } from '../common/themes';
11+
import { border, padding, margin, blockSizes } from '../common/styles';
12+
import { Border } from '../common/styleElements';
13+
import { Text, Panel } from '..';
14+
15+
type TabsProps = {
16+
children?: React.ReactNode;
17+
style?: StyleProp<ViewStyle>;
18+
value: any;
19+
onChange?: () => void;
20+
stretch?: boolean;
21+
};
22+
23+
const Tabs = ({
24+
value,
25+
onChange,
26+
children,
27+
stretch = false,
28+
...rest
29+
}: TabsProps) => {
30+
const childrenWithProps = React.Children.map(children, child => {
31+
if (!React.isValidElement(child)) {
32+
return null;
33+
}
34+
const tabProps = {
35+
selected: child.props.value === value,
36+
onPress: onChange,
37+
stretch,
38+
};
39+
return React.cloneElement(child, tabProps);
40+
});
41+
42+
return (
43+
<View style={[styles.tabs]} {...rest}>
44+
{childrenWithProps}
45+
<View style={[styles.tabBodyBorder]} />
46+
</View>
47+
);
48+
};
49+
50+
type TabBodyProps = {
51+
children?: React.ReactNode;
52+
style?: StyleProp<ViewStyle>;
53+
};
54+
55+
const Body = ({ children, style, ...rest }: TabBodyProps) => {
56+
return (
57+
<Panel style={[styles.body, style]} {...rest}>
58+
{children}
59+
</Panel>
60+
);
61+
};
62+
63+
type TabProps = {
64+
children?: React.ReactNode;
65+
style?: StyleProp<ViewStyle>;
66+
value: any;
67+
onPress?: () => void;
68+
selected?: boolean;
69+
stretch?: boolean;
70+
};
71+
72+
const Tab = ({
73+
value,
74+
onPress,
75+
selected,
76+
stretch,
77+
children,
78+
...rest
79+
}: TabProps) => {
80+
const [isPressed, setIsPressed] = useState(false);
81+
82+
return (
83+
<TouchableHighlight
84+
onPress={() => onPress(value)}
85+
onHideUnderlay={() => setIsPressed(false)}
86+
onShowUnderlay={() => setIsPressed(true)}
87+
underlayColor='none'
88+
style={[
89+
styles.tab,
90+
{ zIndex: selected ? 1 : 0 },
91+
stretch ? { flexGrow: 1 } : { width: 'auto' },
92+
selected ? margin(0, -8) : margin(0, 0),
93+
]}
94+
{...rest}
95+
>
96+
<View
97+
pointerEvents='none'
98+
style={[
99+
styles.tabContent,
100+
{
101+
height: selected ? blockSizes.md + 4 : blockSizes.md,
102+
},
103+
selected ? padding(0, 16) : padding(0, 10),
104+
]}
105+
>
106+
{/* TODO: add 'background' boolean prop to Border component since its usually used with background color */}
107+
<Border
108+
radius={6}
109+
style={[
110+
{
111+
backgroundColor: theme.material,
112+
},
113+
]}
114+
sharedStyle={{
115+
borderBottomWidth: 0,
116+
borderBottomLeftRadius: 0,
117+
borderBottomRightRadius: 0,
118+
}}
119+
/>
120+
<Text>{children}</Text>
121+
<View style={[styles.mask]} />
122+
{isPressed && <View style={[styles.focusOutline]} />}
123+
</View>
124+
</TouchableHighlight>
125+
);
126+
};
127+
128+
const styles = StyleSheet.create({
129+
tabs: {
130+
display: 'flex',
131+
flexDirection: 'row',
132+
alignItems: 'flex-end',
133+
paddingHorizontal: 8,
134+
zIndex: 1,
135+
bottom: -2,
136+
},
137+
body: {
138+
display: 'flex',
139+
padding: 16,
140+
},
141+
tab: {
142+
alignSelf: 'flex-end',
143+
},
144+
tabContent: {
145+
justifyContent: 'center',
146+
width: 'auto',
147+
},
148+
tabBodyBorder: {
149+
height: 4,
150+
position: 'absolute',
151+
left: 4,
152+
right: 4,
153+
bottom: -2,
154+
backgroundColor: theme.borderLight,
155+
borderTopWidth: 2,
156+
borderTopColor: theme.borderLightest,
157+
},
158+
mask: {
159+
height: 4,
160+
position: 'absolute',
161+
left: 4,
162+
right: 4,
163+
bottom: -2,
164+
backgroundColor: theme.material,
165+
},
166+
focusOutline: {
167+
position: 'absolute',
168+
left: 6,
169+
top: 6,
170+
bottom: 4,
171+
right: 6,
172+
...border.focusOutline,
173+
},
174+
});
175+
176+
Tabs.Tab = Tab;
177+
Tabs.Body = Body;
178+
179+
export default Tabs;

src/Tabs/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from './Tabs';

src/common/styleElements.tsx

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type BorderProps = {
99
invert?: boolean;
1010
variant?: 'default' | 'well' | 'outside' | 'cutout';
1111
style?: object;
12+
sharedStyle?: object;
1213
radius?: number;
1314
children?: React.ReactNode;
1415
};
@@ -17,7 +18,8 @@ export const Border = ({
1718
invert = false,
1819
variant = 'default',
1920
style = {},
20-
radius,
21+
sharedStyle = {},
22+
radius = 0,
2123
children,
2224
}: BorderProps) => {
2325
const wrapper: StyleProp<ViewStyle> = [];
@@ -37,26 +39,34 @@ export const Border = ({
3739
inner = [border.cutoutInner];
3840
}
3941

40-
const sharedStyles = [
41-
borderStyles.position,
42-
{
43-
borderRadius: radius || 0,
44-
},
45-
];
42+
const getSharedStyles = (function () {
43+
let r = radius + 4;
44+
45+
return () => {
46+
r -= 2;
47+
return [
48+
borderStyles.position,
49+
sharedStyle,
50+
{
51+
borderRadius: radius ? r : 0,
52+
},
53+
];
54+
};
55+
})();
4656

4757
return (
4858
<View
4959
style={[
50-
sharedStyles,
60+
getSharedStyles(),
5161
invert ? borderStyles.invert : {},
5262
...wrapper,
5363
style,
5464
]}
5565
>
5666
{outer ? (
57-
<View style={[sharedStyles, ...outer]}>
67+
<View style={[getSharedStyles(), ...outer]}>
5868
{inner ? (
59-
<View style={[sharedStyles, ...inner]}>{children}</View>
69+
<View style={[getSharedStyles(), ...inner]}>{children}</View>
6070
) : (
6171
children
6272
)}

src/common/styles.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,21 @@ export const blockSizes = {
117117
md: 35,
118118
lg: 43,
119119
};
120+
121+
export function padding(a: number, b?: number, c?: number, d?: number) {
122+
return {
123+
paddingTop: a,
124+
paddingRight: b || a,
125+
paddingBottom: c || a,
126+
paddingLeft: d || b || a,
127+
};
128+
}
129+
130+
export function margin(a: number, b?: number, c?: number, d?: number) {
131+
return {
132+
marginTop: a,
133+
marginRight: b || a,
134+
marginBottom: c || a,
135+
marginLeft: d || b || a,
136+
};
137+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ export { default as Progress } from './Progress';
1313
export { default as Select } from './Select';
1414
export { default as Desktop } from './Desktop';
1515
export { default as Menu } from './Menu';
16+
export { default as Tabs } from './Tabs';
1617

1718
export * from './common/themes';

0 commit comments

Comments
 (0)