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.
86 changes: 86 additions & 0 deletions src/components/Tabs/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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],
position: 'bottom',
scrollContentStyle: {padding: 20},
style: {backgroundColor: 'red'},
};

const validData2 = {
scenes: [validScene1, validScene2, validScene3, validScene4],
initialTab: 0,
onPressTabCn: jest.fn(),
};

const invalidData1 = {
...validData1,
scenes: 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();
});
});

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();
});
});
});
173 changes: 173 additions & 0 deletions src/components/Tabs/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/* eslint-disable react-hooks/rules-of-hooks */
import React, {FC, ReactNode, useEffect, useRef, useState} 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 {viewportWidth} from '../../scale';
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[];
initialTab?: number;
position?: keyPosition;
onPressTabCb?: (activeTab: number) => void;
scrollContentStyle?: ViewStyle;
style?: ViewStyle;
}

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

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

const [activeTab, setActiveTab] = useState(initialTab);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dam788 los ajustes de estilos quedaron bien, revisate que, cuando le paso un valor a initialTab que no sea 0, me rompe con este error:

Invariant Violation: scrollToIndex should be used in conjunction with getItemLayout or onScrollToIndexFailed, otherwise there is no way to know the location of offscreen indices or handle failures.

Cualquier cosa, si vos haces la prueba y no te pasa avisame y lo vemos a ver si estoy bardeando con algo.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

si gonza, me encontre con unos problemas que no habia tenido en cuenta. Ahi los resolvi, gracias!


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 calculateTabs = isScrollViewTab ? {width: viewportWidth / 3} : {flex: 1};
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: {
...calculateTabs,
justifyContent: 'center',
alignItems: 'center',
borderBottomWidth: scaledForDevice(2, moderateScale),
dam788 marked this conversation as resolved.
Show resolved Hide resolved
},
title: {
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);
return onPressTabCb(idx);
};

return (
<BaseButton
key={title + index}
style={{...styles.tabButton, borderBottomColor}}
disabled={disabled}
onPress={() => handleOnPress(index)}>
<Text style={{...styles.title, color: textColor}} 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;
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,
};
13 changes: 13 additions & 0 deletions src/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/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);
Loading
Loading