Skip to content

Commit

Permalink
feat(tabs): add tabs component
Browse files Browse the repository at this point in the history
  • Loading branch information
Artur Bien committed Dec 5, 2020
1 parent b4c85a3 commit aaa331a
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 10 deletions.
36 changes: 36 additions & 0 deletions example/src/examples/TabsExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { useState } from 'react';
import { View } from 'react-native';
import { Tabs, Panel, Text } from 'react95-native';

const AppBarExample = () => {
const [value, setValue] = useState(0);
const [secondValue, setSecondValue] = useState(0);

return (
<Panel variant='clear' style={[{ flex: 1 }]}>
<View style={[{ padding: 8 }]}>
<Tabs value={value} onChange={setValue}>
<Tabs.Tab value={0}>Shoes</Tabs.Tab>
<Tabs.Tab value={1}>Accesories</Tabs.Tab>
<Tabs.Tab value={2}>Clothing</Tabs.Tab>
</Tabs>
<Tabs.Body style={[{ height: 200 }]}>
<Text>{value}</Text>
</Tabs.Body>
</View>

<View style={[{ padding: 8 }]}>
<Tabs stretch value={secondValue} onChange={setSecondValue}>
<Tabs.Tab value={0}>Shoes</Tabs.Tab>
<Tabs.Tab value={1}>A</Tabs.Tab>
<Tabs.Tab value={2}>Clothing</Tabs.Tab>
</Tabs>
<Tabs.Body style={[{ height: 200 }]}>
<Text>{secondValue}</Text>
</Tabs.Body>
</View>
</Panel>
);
};

export default AppBarExample;
2 changes: 2 additions & 0 deletions example/src/examples/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import MenuExample from './MenuExample';
import ProgressExample from './ProgressExample';
import SelectExample from './SelectExample';
import DesktopExample from './DesktopExample';
import TabsExample from './TabsExample';

export default [
{ name: 'ButtonExample', component: ButtonExample, title: 'Button' },
Expand All @@ -30,6 +31,7 @@ export default [
{ name: 'ProgressExample', component: ProgressExample, title: 'Progress' },
{ name: 'SelectExample', component: SelectExample, title: 'Select' },
{ name: 'DesktopExample', component: DesktopExample, title: 'Desktop' },
{ name: 'TabsExample', component: TabsExample, title: 'Tabs' },
].sort((a, b) => {
/* Sort screens alphabetically */
if (a.title < b.title) return -1;
Expand Down
179 changes: 179 additions & 0 deletions src/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import React, { useState } from 'react';
import {
StyleProp,
StyleSheet,
ViewStyle,
TouchableHighlight,
View,
} from 'react-native';

import { original as theme } from '../common/themes';
import { border, padding, margin, blockSizes } from '../common/styles';
import { Border } from '../common/styleElements';
import { Text, Panel } from '..';

type TabsProps = {
children?: React.ReactNode;
style?: StyleProp<ViewStyle>;
value: any;
onChange?: () => void;
stretch?: boolean;
};

const Tabs = ({
value,
onChange,
children,
stretch = false,
...rest
}: TabsProps) => {
const childrenWithProps = React.Children.map(children, child => {
if (!React.isValidElement(child)) {
return null;
}
const tabProps = {
selected: child.props.value === value,
onPress: onChange,
stretch,
};
return React.cloneElement(child, tabProps);
});

return (
<View style={[styles.tabs]} {...rest}>
{childrenWithProps}
<View style={[styles.tabBodyBorder]} />
</View>
);
};

type TabBodyProps = {
children?: React.ReactNode;
style?: StyleProp<ViewStyle>;
};

const Body = ({ children, style, ...rest }: TabBodyProps) => {
return (
<Panel style={[styles.body, style]} {...rest}>
{children}
</Panel>
);
};

type TabProps = {
children?: React.ReactNode;
style?: StyleProp<ViewStyle>;
value: any;
onPress?: () => void;
selected?: boolean;
stretch?: boolean;
};

const Tab = ({
value,
onPress,
selected,
stretch,
children,
...rest
}: TabProps) => {
const [isPressed, setIsPressed] = useState(false);

return (
<TouchableHighlight
onPress={() => onPress(value)}
onHideUnderlay={() => setIsPressed(false)}
onShowUnderlay={() => setIsPressed(true)}
underlayColor='none'
style={[
styles.tab,
{ zIndex: selected ? 1 : 0 },
stretch ? { flexGrow: 1 } : { width: 'auto' },
selected ? margin(0, -8) : margin(0, 0),
]}
{...rest}
>
<View
pointerEvents='none'
style={[
styles.tabContent,
{
height: selected ? blockSizes.md + 4 : blockSizes.md,
},
selected ? padding(0, 16) : padding(0, 10),
]}
>
{/* TODO: add 'background' boolean prop to Border component since its usually used with background color */}
<Border
radius={6}
style={[
{
backgroundColor: theme.material,
},
]}
sharedStyle={{
borderBottomWidth: 0,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
}}
/>
<Text>{children}</Text>
<View style={[styles.mask]} />
{isPressed && <View style={[styles.focusOutline]} />}
</View>
</TouchableHighlight>
);
};

const styles = StyleSheet.create({
tabs: {
display: 'flex',
flexDirection: 'row',
alignItems: 'flex-end',
paddingHorizontal: 8,
zIndex: 1,
bottom: -2,
},
body: {
display: 'flex',
padding: 16,
},
tab: {
alignSelf: 'flex-end',
},
tabContent: {
justifyContent: 'center',
width: 'auto',
},
tabBodyBorder: {
height: 4,
position: 'absolute',
left: 4,
right: 4,
bottom: -2,
backgroundColor: theme.borderLight,
borderTopWidth: 2,
borderTopColor: theme.borderLightest,
},
mask: {
height: 4,
position: 'absolute',
left: 4,
right: 4,
bottom: -2,
backgroundColor: theme.material,
},
focusOutline: {
position: 'absolute',
left: 6,
top: 6,
bottom: 4,
right: 6,
...border.focusOutline,
},
});

Tabs.Tab = Tab;
Tabs.Body = Body;

export default Tabs;
1 change: 1 addition & 0 deletions src/Tabs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './Tabs';
30 changes: 20 additions & 10 deletions src/common/styleElements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type BorderProps = {
invert?: boolean;
variant?: 'default' | 'well' | 'outside' | 'cutout';
style?: object;
sharedStyle?: object;
radius?: number;
children?: React.ReactNode;
};
Expand All @@ -17,7 +18,8 @@ export const Border = ({
invert = false,
variant = 'default',
style = {},
radius,
sharedStyle = {},
radius = 0,
children,
}: BorderProps) => {
const wrapper: StyleProp<ViewStyle> = [];
Expand All @@ -37,26 +39,34 @@ export const Border = ({
inner = [border.cutoutInner];
}

const sharedStyles = [
borderStyles.position,
{
borderRadius: radius || 0,
},
];
const getSharedStyles = (function () {
let r = radius + 4;

return () => {
r -= 2;
return [
borderStyles.position,
sharedStyle,
{
borderRadius: radius ? r : 0,
},
];
};
})();

return (
<View
style={[
sharedStyles,
getSharedStyles(),
invert ? borderStyles.invert : {},
...wrapper,
style,
]}
>
{outer ? (
<View style={[sharedStyles, ...outer]}>
<View style={[getSharedStyles(), ...outer]}>
{inner ? (
<View style={[sharedStyles, ...inner]}>{children}</View>
<View style={[getSharedStyles(), ...inner]}>{children}</View>
) : (
children
)}
Expand Down
18 changes: 18 additions & 0 deletions src/common/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,21 @@ export const blockSizes = {
md: 35,
lg: 43,
};

export function padding(a: number, b?: number, c?: number, d?: number) {
return {
paddingTop: a,
paddingRight: b || a,
paddingBottom: c || a,
paddingLeft: d || b || a,
};
}

export function margin(a: number, b?: number, c?: number, d?: number) {
return {
marginTop: a,
marginRight: b || a,
marginBottom: c || a,
marginLeft: d || b || a,
};
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export { default as Progress } from './Progress';
export { default as Select } from './Select';
export { default as Desktop } from './Desktop';
export { default as Menu } from './Menu';
export { default as Tabs } from './Tabs';

export * from './common/themes';

0 comments on commit aaa331a

Please sign in to comment.