Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

J UI p 150 creacion componente tabs #38

Merged
merged 17 commits into from
Jul 22, 2024
9 changes: 9 additions & 0 deletions .storybook/preview-head.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@
stroke: #2979FF;
overflow: visible;
}

.r-zIndex-184en5c {
display: grid;
grid-template-columns: repeat(2, 1fr);
}

.css-view-1dbjc4n.r-scrollSnapAlign-cpa5s6 {
padding: 0 25%;
}
</style>


Expand Down
Binary file not shown.
95 changes: 95 additions & 0 deletions src/components/Tabs/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import React from 'react';
import {Pressable, Text, View} from 'react-native';
import {create} from 'react-test-renderer';
import Tabs from './';

const validScene1 = {
title: 'Title1',
scene: (
<View>
<Text>View</Text>
</View>
),
};
const validScene2 = {
title: 'Title2',
scene: (
<View>
<Text>View</Text>
</View>
),
};
const validScene3 = {
title: 'Title3',
scene: (
<View>
<Text>View</Text>
</View>
),
};
const validScene4 = {
title: 'Title4',
scene: (
<View>
<Text>View</Text>
</View>
),
};

const validData1 = {
scenes: [validScene1],
indexChanger: [0, () => {}],
position: 'bottom',
scrollContentStyle: {padding: 20},
style: {backgroundColor: 'red'},
};

const validData2 = {
scenes: [validScene1, validScene2, validScene3, validScene4],
indexChanger: [0, () => {}],
};

const invalidData1 = {
...validData1,
scenes: null,
};
const invalidData2 = {
...validData1,
indexChanger: null,
};

const spyUseEffect = jest.spyOn(React, 'useEffect');
const spyUseRef = jest.spyOn(React, 'useRef');

spyUseEffect.mockImplementation((f) => f());
spyUseRef.mockReturnValueOnce({current: {scrollToIndex: jest.fn()}});

describe('Tabs', () => {
describe('should be null when', () => {
it('scenes is not array or is empty', () => {
const {toJSON} = create(<Tabs {...(invalidData1 as any)} />);
expect(toJSON()).toBeNull();
});

it('indexChanger is not array or is empty', () => {
const {toJSON} = create(<Tabs {...(invalidData2 as any)} />);
expect(toJSON()).toBeNull();
});
});

describe('should render correct when', () => {
it('has minimum data', () => {
const {toJSON} = create(<Tabs {...validData2} />);
expect(toJSON()).toBeTruthy();
});

it('has valid data and press tab', () => {
const {root} = create(<Tabs {...(validData1 as any)} />);
const [ButtonComp] = root.findAllByType(Pressable);
const {onPress} = ButtonComp.props;
onPress();

expect(root).toBeTruthy();
});
});
});
175 changes: 175 additions & 0 deletions src/components/Tabs/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/* eslint-disable react-hooks/rules-of-hooks */
import React, {FC, ReactNode, useEffect, useRef} from 'react';
import {StyleSheet, View, ViewStyle, ScrollView, FlatList} from 'react-native';
import {moderateScale, scaledForDevice} from '../../scale';
import {base, grey, primary} from '../../theme/palette';
import BaseButton from '../BaseButton';
import Text from '../Text';
import {isObject} from './utils';

interface Scene {
title: string;
scene: ReactNode;
disabled?: boolean;
}

export const positions = {
top: 'top',
bottom: 'bottom',
};

export type positionsType = typeof positions;
export type keyPosition = keyof positionsType;

interface TabsProps {
scenes: Scene[];
indexChanger: any;
dam788 marked this conversation as resolved.
Show resolved Hide resolved
position?: keyPosition;
scrollContentStyle?: ViewStyle;
style?: ViewStyle;
}

interface TitleTabProps {
title: string;
index: number;
disabled: boolean | undefined;
}

const Tabs: FC<TabsProps> = ({
scenes,
indexChanger,
position = positions.top,
scrollContentStyle = {},
style = {},
...props
}) => {
if (!scenes || !Array.isArray(scenes) || !scenes.length) {
return null;
}
if (!indexChanger || !Array.isArray(indexChanger) || !indexChanger.length) {
return null;
}

const [activeTab, setActiveTab] = indexChanger;
dam788 marked this conversation as resolved.
Show resolved Hide resolved

const scrollViewRef = useRef<any>(null);

const validScenes = scenes.filter(({scene, title}) => scene && title);
const areScenesValid = !!validScenes && Array.isArray(validScenes) && !!validScenes.length;
const isValidCurrentScene =
!!validScenes[activeTab] &&
isObject(validScenes[activeTab]) &&
!!Object.keys(validScenes[activeTab]).length;

const contentDirection = position === positions.bottom ? 'column-reverse' : 'column';

const isScrollViewTab = scenes?.length && scenes.length > 3;
const hasPaddingHorizontal = isScrollViewTab ? {paddingHorizontal: 25} : {};
const contentMargin = position === positions.bottom ? {marginBottom: 45} : {marginTop: 45};

const styles = StyleSheet.create({
wrapper: {
flex: 1,
position: 'relative',
flexDirection: contentDirection,
},
wrapperTab: {
position: 'absolute',
width: '100%',
height: scaledForDevice(48, moderateScale),
flexDirection: 'row',
backgroundColor: base.white,
zIndex: 1,
elevation: scaledForDevice(5, moderateScale),
},
tabButton: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
borderBottomWidth: scaledForDevice(2, moderateScale),
dam788 marked this conversation as resolved.
Show resolved Hide resolved
},
title: {
...hasPaddingHorizontal,
textTransform: 'uppercase',
fontFamily: 'Roboto-Medium',
},
content: {
...contentMargin,
},
});

useEffect(() => {
if (isScrollViewTab) {
scrollViewRef?.current?.scrollToIndex({
index: activeTab,
animated: true,
});
}
}, [activeTab, isScrollViewTab]);

const TitleTab: FC<TitleTabProps> = ({title, disabled, index}) => {
const borderBottomColor = index === activeTab ? primary.main : base.white;
const textColor = index === activeTab ? primary.main : grey[400];

const handleOnPress = (idx: number) => setActiveTab(idx);
dam788 marked this conversation as resolved.
Show resolved Hide resolved

return (
<BaseButton
key={title + index}
style={{...styles.tabButton, borderBottomColor}}
disabled={disabled}
onPress={() => handleOnPress(index)}>
<Text
style={{...styles.title, color: textColor}}
adjustsFontSizeToFit={true}
selectable={false}
numberOfLines={1}>
{title}
</Text>
</BaseButton>
);
};

const renderItem = ({item, index}: any) => (
<TitleTab title={item.title} disabled={item.disabled} index={index} />
);

return (
<View style={[styles.wrapper, style]} {...props}>
{!isScrollViewTab && (
<View style={styles.wrapperTab}>
{areScenesValid &&
validScenes.map((scene, idx) => (
<TitleTab
key={scene.title}
title={scene.title}
disabled={scene.disabled}
index={idx}
/>
))}
</View>
)}

{isScrollViewTab && (
<FlatList
style={styles.wrapperTab}
data={scenes}
renderItem={renderItem}
ref={scrollViewRef}
horizontal
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
keyExtractor={(item, index) => item.title + index}
/>
)}

{isValidCurrentScene && (
<ScrollView contentContainerStyle={scrollContentStyle} style={styles.content}>
{validScenes[activeTab].scene}
</ScrollView>
)}
</View>
);
};

export default Tabs;
13 changes: 13 additions & 0 deletions src/components/Tabs/utils/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {isObject} from './';

describe('isObject', () => {
it('is true when is object type', () => {
const response = isObject({});
expect(response).toBe(true);
});

it('is false when is object type', () => {
const response = isObject([]);
expect(response).toBe(false);
});
});
1 change: 1 addition & 0 deletions src/components/Tabs/utils/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const isObject = (obj: Object) => !!(obj && obj.constructor === Object);
dam788 marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import BaseButton from './components/BaseButton';
import Button from './components/Button/index';
import FullScreenMessage from './components/FullScreenMessage';
import LayoutWithBottomButtons from './components/LayoutWithBottomButtons';
import Tabs from './components/Tabs';
import * as getScale from './scale';

export {
Expand Down Expand Up @@ -52,4 +53,5 @@ export {
getScale,
LayoutWithBottomButtons,
FullScreenMessage,
Tabs,
};
Loading
Loading