Skip to content

Commit

Permalink
refactor(ui): tab-view enhancements (#363)
Browse files Browse the repository at this point in the history
  • Loading branch information
artyorsh authored and 32penkin committed Apr 22, 2019
1 parent a5fafb1 commit 8d8e404
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 46 deletions.
10 changes: 10 additions & 0 deletions src/framework/ui/tab/tab.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import {
TouchableOpacity,
TouchableOpacityProps,
StyleSheet,
StyleProp,
TextStyle,
} from 'react-native';
import {
allWithRest,
styled,
StyledComponentProps,
StyleType,
Expand All @@ -14,8 +17,10 @@ import {
Text as TextComponent,
Props as TextProps,
} from '../text/text.component';
import { TextStyleProps } from '../common/props';

interface TabProps {
style?: StyleProp<TextStyle>;
title?: string;
icon?: (style: StyleType) => React.ReactElement<ImageProps>;
selected?: boolean;
Expand All @@ -40,6 +45,9 @@ export class Tab extends React.Component<Props> {
};

private getComponentStyle = (source: StyleType): StyleType => {
const derivedStyle: TextStyle = StyleSheet.flatten(this.props.style);
const { rest: derivedContainerStyle, ...derivedTextStyle } = allWithRest(derivedStyle, TextStyleProps);

const {
textMarginVertical,
textFontSize,
Expand All @@ -56,6 +64,7 @@ export class Tab extends React.Component<Props> {
return {
container: {
...containerParameters,
...derivedContainerStyle,
...styles.container,
},
icon: {
Expand All @@ -71,6 +80,7 @@ export class Tab extends React.Component<Props> {
lineHeight: textLineHeight,
fontWeight: textFontWeight,
color: textColor,
...derivedTextStyle,
...styles.title,
},
};
Expand Down
20 changes: 14 additions & 6 deletions src/framework/ui/tab/tab.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,12 @@ describe('@tab-view: component checks', () => {
};

it('* emits onSelect with correct args', () => {
const screenWidth: number = 375;

const onSelect = jest.fn();

const component = render(
<Mock
contentWidth={375}
onSelect={onSelect}>
<Mock onSelect={onSelect}>
<ChildMock>
<View/>
</ChildMock>
Expand All @@ -169,10 +169,18 @@ describe('@tab-view: component checks', () => {

const scrollView: ReactTestInstance = component.getByType(ScrollView);

fireEvent(scrollView, 'layout', {
nativeEvent: {
layout: {
width: screenWidth,
},
},
});

fireEvent(scrollView, 'momentumScrollEnd', {
nativeEvent: {
contentOffset: {
x: 375,
x: screenWidth,
},
},
});
Expand All @@ -189,7 +197,7 @@ describe('@tab-view: component checks', () => {
});

const component: RenderAPI = render(
<Mock contentWidth={375} shouldLoadComponent={shouldLoadComponent}>
<Mock shouldLoadComponent={shouldLoadComponent}>
<ChildMock>
<View/>
</ChildMock>
Expand All @@ -202,7 +210,7 @@ describe('@tab-view: component checks', () => {
const scrollView: ReactTestInstance = component.getByType(ScrollView);
const unloadedChild: ReactTestInstance = scrollView.props.children[disabledComponentIndex];

expect(unloadedChild.props.children).toEqual(undefined);
expect(unloadedChild.props.children).toBeNull();
});
});

5 changes: 4 additions & 1 deletion src/framework/ui/tab/tabBar.component.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import {
StyleSheet,
TextStyle,
View,
ViewProps,
} from 'react-native';
Expand Down Expand Up @@ -66,9 +67,11 @@ export class TabBar extends React.Component<Props> {
};

private renderComponentChild = (element: TabElement, index: number): TabElement => {
const derivedStyle: TextStyle = StyleSheet.flatten(element.props.style);

return React.cloneElement(element, {
key: index,
style: { flex: 1 },
style: { ...derivedStyle, flex: 1 },
selected: index === this.props.selectedIndex,
onSelect: () => this.onChildPress(index),
});
Expand Down
5 changes: 1 addition & 4 deletions src/framework/ui/tab/tabBarIndicator.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@ export class TabBarIndicator extends React.Component<Props> {
public componentDidUpdate() {
const { selectedPosition: index } = this.props;

this.scrollToIndex({
index,
animated: true,
});
this.scrollToIndex({ index, animated: true });
}

/**
Expand Down
3 changes: 1 addition & 2 deletions src/framework/ui/tab/tabView.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class TabView extends React.Component<Props> {
const { children, ...elementProps } = element.props;

return {
tab: React.createElement(Tab, { key: index, ...elementProps }),
tab: React.cloneElement(element, { key: index, ...elementProps }),
content: children,
};
};
Expand Down Expand Up @@ -107,7 +107,6 @@ export class TabView extends React.Component<Props> {
<ViewPager
ref={this.viewPagerRef}
selectedIndex={selectedIndex}
contentWidth={contentWidth}
shouldLoadComponent={this.props.shouldLoadComponent}
onOffsetChange={this.onPagerOffsetChange}
onSelect={this.onPagerSelect}>
Expand Down
56 changes: 38 additions & 18 deletions src/framework/ui/viewPager/viewPager.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import {
View,
ScrollView,
ScrollViewProps,
LayoutChangeEvent,
StyleSheet,
ViewProps,
} from 'react-native';
import { ScrollEvent } from '../common/type';

type ChildElement = React.ReactElement<any>;

interface ViewPagerProps {
contentWidth: number;
children: ChildElement | ChildElement[];
selectedIndex?: number;
shouldLoadComponent?: (index: number) => boolean;
Expand All @@ -27,6 +29,7 @@ export class ViewPager extends React.Component<Props> {
};

private scrollViewRef: React.RefObject<ScrollView> = React.createRef();
private contentWidth: number = 0;

public componentDidMount() {
const { selectedIndex: index } = this.props;
Expand All @@ -41,15 +44,12 @@ export class ViewPager extends React.Component<Props> {
public componentDidUpdate() {
const { selectedIndex: index } = this.props;

this.scrollToIndex({
index,
animated: true,
});
this.scrollToIndex({ index, animated: true });
}

public scrollToIndex(params: { index: number; animated?: boolean }) {
const { index, ...rest } = params;
const offset: number = this.props.contentWidth * index;
const offset: number = this.contentWidth * index;

this.scrollToOffset({ offset, ...rest });
}
Expand All @@ -70,47 +70,67 @@ export class ViewPager extends React.Component<Props> {

private onScrollEnd = (event: ScrollEvent) => {
const { x: offset } = event.nativeEvent.contentOffset;
const { selectedIndex: derivedSelectedIndex, contentWidth } = this.props;
const { selectedIndex: derivedSelectedIndex } = this.props;

const selectedIndex: number = offset / contentWidth;
const selectedIndex: number = offset / this.contentWidth;

if (selectedIndex !== derivedSelectedIndex && this.props.onSelect) {
this.props.onSelect(selectedIndex);
this.props.onSelect(Math.round(selectedIndex));
}
};

private onLayout = (event: LayoutChangeEvent) => {
const { width } = event.nativeEvent.layout;
this.contentWidth = width;

if (this.props.onLayout) {
this.props.onLayout(event);
}
};

private renderComponentChild = (element: ChildElement, index: number): ChildElement => {
const { shouldLoadComponent: shouldLoad, contentWidth } = this.props;
const { shouldLoadComponent } = this.props;

const contentView: ChildElement | null = shouldLoadComponent(index) ? element : null;

return React.createElement(View, {
...element.props,
key: index,
width: contentWidth,
children: shouldLoad(index) ? element : undefined,
});
style: styles.contentViewContainer,
}, contentView);
};

private renderComponentChildren = (source: ChildElement | ChildElement[]): ChildElement[] => {
return React.Children.map(source, this.renderComponentChild);
};

public render(): React.ReactNode {
const { children, ...derivedProps } = this.props;
const { contentContainerStyle, children, ...derivedProps } = this.props;
const componentChildren: ChildElement[] = this.renderComponentChildren(children);

return (
<ScrollView
{...derivedProps}
ref={this.scrollViewRef}
bounces={false}
contentContainerStyle={[styles.contentContainer, contentContainerStyle]}
showsHorizontalScrollIndicator={false}
{...derivedProps}
ref={this.scrollViewRef}
scrollEventThrottle={16}
horizontal={true}
pagingEnabled={true}
onScroll={this.onScroll}
onMomentumScrollEnd={this.onScrollEnd}>
onMomentumScrollEnd={this.onScrollEnd}
onLayout={this.onLayout}>
{componentChildren}
</ScrollView>
);
}
}

const styles = StyleSheet.create({
contentContainer: {
flex: 1,
},
contentViewContainer: {
width: '100%',
},
});
32 changes: 18 additions & 14 deletions src/framework/ui/viewPager/viewPager.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,31 @@ describe('@view-pager: component checks', () => {
);

it('* emits onSelect with correct args', () => {
const screenWidth: number = 375;

const onSelect = jest.fn();

const component: RenderAPI = render(
<Mock
contentWidth={375}
onSelect={onSelect}>
<Mock onSelect={onSelect}>
<ChildMock/>
<ChildMock/>
</Mock>,
);

const scrollView: ReactTestInstance = component.getByType(ScrollView);

fireEvent(scrollView, 'layout', {
nativeEvent: {
layout: {
width: screenWidth,
},
},
});

fireEvent(scrollView, 'momentumScrollEnd', {
nativeEvent: {
contentOffset: {
x: 375,
x: screenWidth,
},
},
});
Expand All @@ -50,12 +59,11 @@ describe('@view-pager: component checks', () => {
});

it('* emits onOffsetChange with correct args', () => {

const onOffsetChange = jest.fn();

const component: RenderAPI = render(
<Mock
contentWidth={375}
onOffsetChange={onOffsetChange}>
<Mock onOffsetChange={onOffsetChange}>
<ChildMock/>
<ChildMock/>
</Mock>,
Expand All @@ -77,9 +85,7 @@ describe('@view-pager: component checks', () => {
const shouldLoadComponent = jest.fn();

render(
<Mock
contentWidth={375}
shouldLoadComponent={shouldLoadComponent}>
<Mock shouldLoadComponent={shouldLoadComponent}>
<ChildMock/>
<ChildMock/>
</Mock>,
Expand All @@ -97,9 +103,7 @@ describe('@view-pager: component checks', () => {
});

const component: RenderAPI = render(
<Mock
contentWidth={375}
shouldLoadComponent={shouldLoadComponent}>
<Mock shouldLoadComponent={shouldLoadComponent}>
<ChildMock/>
<ChildMock/>
</Mock>,
Expand All @@ -109,7 +113,7 @@ describe('@view-pager: component checks', () => {

const unloadedChild = scrollView.props.children[disabledComponentIndex];

expect(unloadedChild.props.children).toEqual(undefined);
expect(unloadedChild.props.children).toBeNull();
});

});
Expand Down
1 change: 0 additions & 1 deletion src/playground/src/ui/screen/viewPager.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ class ViewPagerScreen extends React.Component<Props, State> {
return (
<ViewPager
selectedIndex={this.state.selectedIndex}
contentWidth={Dimensions.get('window').width}
onSelect={this.onIndexChange}>
<View style={this.props.themedStyle.tabContainer}>
<Text>Tab 1</Text>
Expand Down

0 comments on commit 8d8e404

Please sign in to comment.