Improve tab components to match linter

This commit is contained in:
Arnaud Vergnet 2020-08-04 23:49:18 +02:00
parent 0117b25cd8
commit aa992d20b2
3 changed files with 417 additions and 350 deletions

View file

@ -2,23 +2,44 @@
import * as React from 'react'; import * as React from 'react';
import {withTheme} from 'react-native-paper'; import {withTheme} from 'react-native-paper';
import TabIcon from "./TabIcon"; import Animated from 'react-native-reanimated';
import TabHomeIcon from "./TabHomeIcon"; import {Collapsible} from 'react-navigation-collapsible';
import {Animated} from 'react-native'; import {StackNavigationProp} from '@react-navigation/stack';
import {Collapsible} from "react-navigation-collapsible"; import TabIcon from './TabIcon';
import TabHomeIcon from './TabHomeIcon';
import type {CustomTheme} from '../../managers/ThemeManager';
type Props = { type RouteType = {
state: Object, name: string,
descriptors: Object, key: string,
navigation: Object, params: {collapsible: Collapsible},
theme: Object, state: {
collapsibleStack: Object, index: number,
} routes: Array<RouteType>,
},
};
type State = { type PropsType = {
translateY: AnimatedValue, state: {
barSynced: boolean, index: number,
} routes: Array<RouteType>,
},
descriptors: {
[key: string]: {
options: {
tabBarLabel: string,
title: string,
},
},
},
navigation: StackNavigationProp,
theme: CustomTheme,
};
type StateType = {
// eslint-disable-next-line flowtype/no-weak-types
translateY: any,
};
const TAB_ICONS = { const TAB_ICONS = {
proxiwash: 'tshirt-crew', proxiwash: 'tshirt-crew',
@ -27,29 +48,15 @@ const TAB_ICONS = {
planex: 'clock', planex: 'clock',
}; };
class CustomTabBar extends React.Component<Props, State> { class CustomTabBar extends React.Component<PropsType, StateType> {
static TAB_BAR_HEIGHT = 48; static TAB_BAR_HEIGHT = 48;
state = { constructor() {
super();
this.state = {
translateY: new Animated.Value(0), translateY: new Animated.Value(0),
}
syncTabBar = (route, index) => {
const state = this.props.state;
const isFocused = state.index === index;
if (isFocused) {
const stackState = route.state;
const stackRoute = stackState ? stackState.routes[stackState.index] : undefined;
const params: { collapsible: Collapsible } = stackRoute ? stackRoute.params : undefined;
const collapsible = params ? params.collapsible : undefined;
if (collapsible) {
this.setState({
translateY: Animated.multiply(-1.5, collapsible.translateY), // Hide tab bar faster than header bar
});
}
}
}; };
}
/** /**
* Navigates to the given route if it is different from the current one * Navigates to the given route if it is different from the current one
@ -58,14 +65,15 @@ class CustomTabBar extends React.Component<Props, State> {
* @param currentIndex The current route index * @param currentIndex The current route index
* @param destIndex The destination route index * @param destIndex The destination route index
*/ */
onItemPress(route: Object, currentIndex: number, destIndex: number) { onItemPress(route: RouteType, currentIndex: number, destIndex: number) {
const event = this.props.navigation.emit({ const {navigation} = this.props;
const event = navigation.emit({
type: 'tabPress', type: 'tabPress',
target: route.key, target: route.key,
canPreventDefault: true, canPreventDefault: true,
}); });
if (currentIndex !== destIndex && !event.defaultPrevented) if (currentIndex !== destIndex && !event.defaultPrevented)
this.props.navigation.navigate(route.name); navigation.navigate(route.name);
} }
/** /**
@ -73,16 +81,25 @@ class CustomTabBar extends React.Component<Props, State> {
* *
* @param route * @param route
*/ */
onItemLongPress(route: Object) { onItemLongPress(route: RouteType) {
const event = this.props.navigation.emit({ const {navigation} = this.props;
const event = navigation.emit({
type: 'tabLongPress', type: 'tabLongPress',
target: route.key, target: route.key,
canPreventDefault: true, canPreventDefault: true,
}); });
if (route.name === "home" && !event.defaultPrevented) if (route.name === 'home' && !event.defaultPrevented)
this.props.navigation.navigate('game-start'); navigation.navigate('game-start');
} }
/**
* Finds the active route and syncs the tab bar animation with the header bar
*/
onRouteChange = () => {
const {props} = this;
props.state.routes.map(this.syncTabBar);
};
/** /**
* Gets an icon for the given route if it is not the home one as it uses a custom button * Gets an icon for the given route if it is not the home one as it uses a custom button
* *
@ -90,22 +107,13 @@ class CustomTabBar extends React.Component<Props, State> {
* @param focused * @param focused
* @returns {null} * @returns {null}
*/ */
tabBarIcon = (route, focused) => { getTabBarIcon = (route: RouteType, focused: boolean): React.Node => {
let icon = TAB_ICONS[route.name]; let icon = TAB_ICONS[route.name];
icon = focused ? icon : icon + ('-outline'); icon = focused ? icon : `${icon}-outline`;
if (route.name !== "home") if (route.name !== 'home') return icon;
return icon;
else
return null; return null;
}; };
/**
* Finds the active route and syncs the tab bar animation with the header bar
*/
onRouteChange = () => {
this.props.state.routes.map(this.syncTabBar)
}
/** /**
* Gets a tab icon render. * Gets a tab icon render.
* If the given route is focused, it syncs the tab bar and header bar animations together * If the given route is focused, it syncs the tab bar and header bar animations together
@ -114,49 +122,79 @@ class CustomTabBar extends React.Component<Props, State> {
* @param index The index of the current route * @param index The index of the current route
* @returns {*} * @returns {*}
*/ */
renderIcon = (route, index) => { getRenderIcon = (route: RouteType, index: number): React.Node => {
const state = this.props.state; const {props} = this;
const {options} = this.props.descriptors[route.key]; const {state} = props;
const label = const {options} = props.descriptors[route.key];
options.tabBarLabel != null let label;
? options.tabBarLabel if (options.tabBarLabel != null) label = options.tabBarLabel;
: options.title != null else if (options.title != null) label = options.title;
? options.title else label = route.name;
: route.name;
const onPress = () => this.onItemPress(route, state.index, index); const onPress = () => {
const onLongPress = () => this.onItemLongPress(route); this.onItemPress(route, state.index, index);
};
const onLongPress = () => {
this.onItemLongPress(route);
};
const isFocused = state.index === index; const isFocused = state.index === index;
const color = isFocused ? this.props.theme.colors.primary : this.props.theme.colors.tabIcon; const color = isFocused
if (route.name !== "home") { ? props.theme.colors.primary
return <TabIcon : props.theme.colors.tabIcon;
if (route.name !== 'home') {
return (
<TabIcon
onPress={onPress} onPress={onPress}
onLongPress={onLongPress} onLongPress={onLongPress}
icon={this.tabBarIcon(route, isFocused)} icon={this.getTabBarIcon(route, isFocused)}
color={color} color={color}
label={label} label={label}
focused={isFocused} focused={isFocused}
extraData={state.index > index} extraData={state.index > index}
key={route.key} key={route.key}
/> />
} else );
return <TabHomeIcon }
return (
<TabHomeIcon
onPress={onPress} onPress={onPress}
onLongPress={onLongPress} onLongPress={onLongPress}
focused={isFocused} focused={isFocused}
key={route.key} key={route.key}
tabBarHeight={CustomTabBar.TAB_BAR_HEIGHT} tabBarHeight={CustomTabBar.TAB_BAR_HEIGHT}
/> />
);
}; };
getIcons() { getIcons(): React.Node {
return this.props.state.routes.map(this.renderIcon); const {props} = this;
return props.state.routes.map(this.getRenderIcon);
} }
render() { syncTabBar = (route: RouteType, index: number) => {
this.props.navigation.addListener('state', this.onRouteChange); const {state} = this.props;
const isFocused = state.index === index;
if (isFocused) {
const stackState = route.state;
const stackRoute =
stackState != null ? stackState.routes[stackState.index] : null;
const params: {collapsible: Collapsible} | null =
stackRoute != null ? stackRoute.params : null;
const collapsible = params != null ? params.collapsible : null;
if (collapsible != null) {
this.setState({
translateY: Animated.multiply(-1.5, collapsible.translateY), // Hide tab bar faster than header bar
});
}
}
};
render(): React.Node {
const {props, state} = this;
props.navigation.addListener('state', this.onRouteChange);
const icons = this.getIcons(); const icons = this.getIcons();
// $FlowFixMe
return ( return (
<Animated.View <Animated.View
useNativeDriver useNativeDriver
@ -167,10 +205,9 @@ class CustomTabBar extends React.Component<Props, State> {
position: 'absolute', position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
backgroundColor: this.props.theme.colors.surface, backgroundColor: props.theme.colors.surface,
transform: [{translateY: this.state.translateY}], transform: [{translateY: state.translateY}],
}} }}>
>
{icons} {icons}
</Animated.View> </Animated.View>
); );

View file

@ -1,70 +1,95 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {Image, Platform, View} from "react-native"; import {Image, Platform, View} from 'react-native';
import {FAB, TouchableRipple, withTheme} from 'react-native-paper'; import {FAB, TouchableRipple, withTheme} from 'react-native-paper';
import * as Animatable from "react-native-animatable"; import * as Animatable from 'react-native-animatable';
import FOCUSED_ICON from '../../../assets/tab-icon.png';
import UNFOCUSED_ICON from '../../../assets/tab-icon-outline.png';
import type {CustomTheme} from '../../managers/ThemeManager';
type Props = { type PropsType = {
focused: boolean, focused: boolean,
onPress: Function, onPress: () => void,
onLongPress: Function, onLongPress: () => void,
theme: Object, theme: CustomTheme,
tabBarHeight: number, tabBarHeight: number,
} };
const AnimatedFAB = Animatable.createAnimatableComponent(FAB); const AnimatedFAB = Animatable.createAnimatableComponent(FAB);
/** /**
* Abstraction layer for Agenda component, using custom configuration * Abstraction layer for Agenda component, using custom configuration
*/ */
class TabHomeIcon extends React.Component<Props> { class TabHomeIcon extends React.Component<PropsType> {
constructor(props: PropsType) {
focusedIcon = require('../../../assets/tab-icon.png');
unFocusedIcon = require('../../../assets/tab-icon-outline.png');
constructor(props) {
super(props); super(props);
Animatable.initializeRegistryWithDefinitions({ Animatable.initializeRegistryWithDefinitions({
fabFocusIn: { fabFocusIn: {
"0": { '0': {
scale: 1, translateY: 0 scale: 1,
translateY: 0,
}, },
"0.9": { '0.9': {
scale: 1.2, translateY: -9 scale: 1.2,
translateY: -9,
}, },
"1": { '1': {
scale: 1.1, translateY: -7 scale: 1.1,
translateY: -7,
}, },
}, },
fabFocusOut: { fabFocusOut: {
"0": { '0': {
scale: 1.1, translateY: -6 scale: 1.1,
translateY: -6,
},
'1': {
scale: 1,
translateY: 0,
}, },
"1": {
scale: 1, translateY: 0
}, },
}
}); });
} }
iconRender = ({size, color}) => shouldComponentUpdate(nextProps: PropsType): boolean {
this.props.focused const {focused} = this.props;
? <Image return nextProps.focused !== focused;
source={this.focusedIcon}
style={{width: size, height: size, tintColor: color}}
/>
: <Image
source={this.unFocusedIcon}
style={{width: size, height: size, tintColor: color}}
/>;
shouldComponentUpdate(nextProps: Props): boolean {
return (nextProps.focused !== this.props.focused);
} }
render(): React$Node { getIconRender = ({
const props = this.props; size,
color,
}: {
size: number,
color: string,
}): React.Node => {
const {focused} = this.props;
if (focused)
return (
<Image
source={FOCUSED_ICON}
style={{
width: size,
height: size,
tintColor: color,
}}
/>
);
return (
<Image
source={UNFOCUSED_ICON}
style={{
width: size,
height: size,
tintColor: color,
}}
/>
);
};
render(): React.Node {
const {props} = this;
return ( return (
<View <View
style={{ style={{
@ -74,33 +99,35 @@ class TabHomeIcon extends React.Component<Props> {
<TouchableRipple <TouchableRipple
onPress={props.onPress} onPress={props.onPress}
onLongPress={props.onLongPress} onLongPress={props.onLongPress}
borderless={true} borderless
rippleColor={Platform.OS === 'android' ? this.props.theme.colors.primary : 'transparent'} rippleColor={
Platform.OS === 'android'
? props.theme.colors.primary
: 'transparent'
}
style={{ style={{
position: 'absolute', position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
width: '100%', width: '100%',
height: this.props.tabBarHeight + 30, height: props.tabBarHeight + 30,
marginBottom: -15, marginBottom: -15,
}} }}>
>
<AnimatedFAB <AnimatedFAB
duration={200} duration={200}
easing={"ease-out"} easing="ease-out"
animation={props.focused ? "fabFocusIn" : "fabFocusOut"} animation={props.focused ? 'fabFocusIn' : 'fabFocusOut'}
icon={this.iconRender} icon={this.getIconRender}
style={{ style={{
marginTop: 15, marginTop: 15,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto' marginRight: 'auto',
}}/> }}
/>
</TouchableRipple> </TouchableRipple>
</View> </View>
); );
} }
} }
export default withTheme(TabHomeIcon); export default withTheme(TabHomeIcon);

View file

@ -1,53 +1,57 @@
// @flow // @flow
import * as React from 'react'; import * as React from 'react';
import {View} from "react-native"; import {View} from 'react-native';
import {TouchableRipple, withTheme} from 'react-native-paper'; import {TouchableRipple, withTheme} from 'react-native-paper';
import type {MaterialCommunityIconsGlyphs} from "react-native-vector-icons/MaterialCommunityIcons"; import type {MaterialCommunityIconsGlyphs} from 'react-native-vector-icons/MaterialCommunityIcons';
import MaterialCommunityIcons from "react-native-vector-icons/MaterialCommunityIcons"; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import * as Animatable from "react-native-animatable"; import * as Animatable from 'react-native-animatable';
import type {CustomTheme} from '../../managers/ThemeManager';
type Props = { type PropsType = {
focused: boolean, focused: boolean,
color: string, color: string,
label: string, label: string,
icon: MaterialCommunityIconsGlyphs, icon: MaterialCommunityIconsGlyphs,
onPress: Function, onPress: () => void,
onLongPress: Function, onLongPress: () => void,
theme: Object, theme: CustomTheme,
extraData: any, extraData: null | boolean | number | string,
} };
/** /**
* Abstraction layer for Agenda component, using custom configuration * Abstraction layer for Agenda component, using custom configuration
*/ */
class TabIcon extends React.Component<Props> { class TabIcon extends React.Component<PropsType> {
firstRender: boolean; firstRender: boolean;
constructor(props) { constructor(props: PropsType) {
super(props); super(props);
Animatable.initializeRegistryWithDefinitions({ Animatable.initializeRegistryWithDefinitions({
focusIn: { focusIn: {
"0": { '0': {
scale: 1, translateY: 0 scale: 1,
translateY: 0,
}, },
"0.9": { '0.9': {
scale: 1.3, translateY: 7 scale: 1.3,
translateY: 7,
}, },
"1": { '1': {
scale: 1.2, translateY: 6 scale: 1.2,
translateY: 6,
}, },
}, },
focusOut: { focusOut: {
"0": { '0': {
scale: 1.2, translateY: 6 scale: 1.2,
translateY: 6,
},
'1': {
scale: 1,
translateY: 0,
}, },
"1": {
scale: 1, translateY: 0
}, },
}
}); });
this.firstRender = true; this.firstRender = true;
} }
@ -56,32 +60,33 @@ class TabIcon extends React.Component<Props> {
this.firstRender = false; this.firstRender = false;
} }
shouldComponentUpdate(nextProps: Props): boolean { shouldComponentUpdate(nextProps: PropsType): boolean {
return (nextProps.focused !== this.props.focused) const {props} = this;
|| (nextProps.theme.dark !== this.props.theme.dark) return (
|| (nextProps.extraData !== this.props.extraData); nextProps.focused !== props.focused ||
nextProps.theme.dark !== props.theme.dark ||
nextProps.extraData !== props.extraData
);
} }
render(): React$Node { render(): React.Node {
const props = this.props; const {props} = this;
return ( return (
<TouchableRipple <TouchableRipple
onPress={props.onPress} onPress={props.onPress}
onLongPress={props.onLongPress} onLongPress={props.onLongPress}
borderless={true} borderless
rippleColor={this.props.theme.colors.primary} rippleColor={props.theme.colors.primary}
style={{ style={{
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
}} }}>
>
<View> <View>
<Animatable.View <Animatable.View
duration={200} duration={200}
easing={"ease-out"} easing="ease-out"
animation={props.focused ? "focusIn" : "focusOut"} animation={props.focused ? 'focusIn' : 'focusOut'}
useNativeDriver useNativeDriver>
>
<MaterialCommunityIcons <MaterialCommunityIcons
name={props.icon} name={props.icon}
color={props.color} color={props.color}
@ -93,16 +98,14 @@ class TabIcon extends React.Component<Props> {
/> />
</Animatable.View> </Animatable.View>
<Animatable.Text <Animatable.Text
animation={props.focused ? "fadeOutDown" : "fadeIn"} animation={props.focused ? 'fadeOutDown' : 'fadeIn'}
useNativeDriver useNativeDriver
style={{ style={{
color: props.color, color: props.color,
marginLeft: 'auto', marginLeft: 'auto',
marginRight: 'auto', marginRight: 'auto',
fontSize: 10, fontSize: 10,
}} }}>
>
{props.label} {props.label}
</Animatable.Text> </Animatable.Text>
</View> </View>