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,179 +2,216 @@
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 = {
translateY: AnimatedValue,
barSynced: boolean,
}
const TAB_ICONS = {
proxiwash: 'tshirt-crew',
services: 'account-circle',
planning: 'calendar-range',
planex: 'clock',
}; };
class CustomTabBar extends React.Component<Props, State> { type PropsType = {
state: {
index: number,
routes: Array<RouteType>,
},
descriptors: {
[key: string]: {
options: {
tabBarLabel: string,
title: string,
},
},
},
navigation: StackNavigationProp,
theme: CustomTheme,
};
static TAB_BAR_HEIGHT = 48; type StateType = {
// eslint-disable-next-line flowtype/no-weak-types
translateY: any,
};
state = { const TAB_ICONS = {
translateY: new Animated.Value(0), proxiwash: 'tshirt-crew',
} services: 'account-circle',
planning: 'calendar-range',
planex: 'clock',
};
syncTabBar = (route, index) => { class CustomTabBar extends React.Component<PropsType, StateType> {
const state = this.props.state; static TAB_BAR_HEIGHT = 48;
const isFocused = state.index === index;
if (isFocused) { constructor() {
const stackState = route.state; super();
const stackRoute = stackState ? stackState.routes[stackState.index] : undefined; this.state = {
const params: { collapsible: Collapsible } = stackRoute ? stackRoute.params : undefined; translateY: new Animated.Value(0),
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
* *
* @param route Destination route * @param route Destination route
* @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;
type: 'tabPress', const event = navigation.emit({
target: route.key, type: 'tabPress',
canPreventDefault: true, target: route.key,
canPreventDefault: true,
});
if (currentIndex !== destIndex && !event.defaultPrevented)
navigation.navigate(route.name);
}
/**
* Navigates to tetris screen on home button long press
*
* @param route
*/
onItemLongPress(route: RouteType) {
const {navigation} = this.props;
const event = navigation.emit({
type: 'tabLongPress',
target: route.key,
canPreventDefault: true,
});
if (route.name === 'home' && !event.defaultPrevented)
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
*
* @param route
* @param focused
* @returns {null}
*/
getTabBarIcon = (route: RouteType, focused: boolean): React.Node => {
let icon = TAB_ICONS[route.name];
icon = focused ? icon : `${icon}-outline`;
if (route.name !== 'home') return icon;
return null;
};
/**
* Gets a tab icon render.
* If the given route is focused, it syncs the tab bar and header bar animations together
*
* @param route The route for the icon
* @param index The index of the current route
* @returns {*}
*/
getRenderIcon = (route: RouteType, index: number): React.Node => {
const {props} = this;
const {state} = props;
const {options} = props.descriptors[route.key];
let label;
if (options.tabBarLabel != null) label = options.tabBarLabel;
else if (options.title != null) label = options.title;
else label = route.name;
const onPress = () => {
this.onItemPress(route, state.index, index);
};
const onLongPress = () => {
this.onItemLongPress(route);
};
const isFocused = state.index === index;
const color = isFocused
? props.theme.colors.primary
: props.theme.colors.tabIcon;
if (route.name !== 'home') {
return (
<TabIcon
onPress={onPress}
onLongPress={onLongPress}
icon={this.getTabBarIcon(route, isFocused)}
color={color}
label={label}
focused={isFocused}
extraData={state.index > index}
key={route.key}
/>
);
}
return (
<TabHomeIcon
onPress={onPress}
onLongPress={onLongPress}
focused={isFocused}
key={route.key}
tabBarHeight={CustomTabBar.TAB_BAR_HEIGHT}
/>
);
};
getIcons(): React.Node {
const {props} = this;
return props.state.routes.map(this.getRenderIcon);
}
syncTabBar = (route: RouteType, index: number) => {
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
}); });
if (currentIndex !== destIndex && !event.defaultPrevented) }
this.props.navigation.navigate(route.name);
} }
};
/** render(): React.Node {
* Navigates to tetris screen on home button long press const {props, state} = this;
* props.navigation.addListener('state', this.onRouteChange);
* @param route const icons = this.getIcons();
*/ // $FlowFixMe
onItemLongPress(route: Object) { return (
const event = this.props.navigation.emit({ <Animated.View
type: 'tabLongPress', useNativeDriver
target: route.key, style={{
canPreventDefault: true, flexDirection: 'row',
}); height: CustomTabBar.TAB_BAR_HEIGHT,
if (route.name === "home" && !event.defaultPrevented) width: '100%',
this.props.navigation.navigate('game-start'); position: 'absolute',
} bottom: 0,
left: 0,
/** backgroundColor: props.theme.colors.surface,
* Gets an icon for the given route if it is not the home one as it uses a custom button transform: [{translateY: state.translateY}],
* }}>
* @param route {icons}
* @param focused </Animated.View>
* @returns {null} );
*/ }
tabBarIcon = (route, focused) => {
let icon = TAB_ICONS[route.name];
icon = focused ? icon : icon + ('-outline');
if (route.name !== "home")
return icon;
else
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.
* If the given route is focused, it syncs the tab bar and header bar animations together
*
* @param route The route for the icon
* @param index The index of the current route
* @returns {*}
*/
renderIcon = (route, index) => {
const state = this.props.state;
const {options} = this.props.descriptors[route.key];
const label =
options.tabBarLabel != null
? options.tabBarLabel
: options.title != null
? options.title
: route.name;
const onPress = () => this.onItemPress(route, state.index, index);
const onLongPress = () => this.onItemLongPress(route);
const isFocused = state.index === index;
const color = isFocused ? this.props.theme.colors.primary : this.props.theme.colors.tabIcon;
if (route.name !== "home") {
return <TabIcon
onPress={onPress}
onLongPress={onLongPress}
icon={this.tabBarIcon(route, isFocused)}
color={color}
label={label}
focused={isFocused}
extraData={state.index > index}
key={route.key}
/>
} else
return <TabHomeIcon
onPress={onPress}
onLongPress={onLongPress}
focused={isFocused}
key={route.key}
tabBarHeight={CustomTabBar.TAB_BAR_HEIGHT}
/>
};
getIcons() {
return this.props.state.routes.map(this.renderIcon);
}
render() {
this.props.navigation.addListener('state', this.onRouteChange);
const icons = this.getIcons();
return (
<Animated.View
useNativeDriver
style={{
flexDirection: 'row',
height: CustomTabBar.TAB_BAR_HEIGHT,
width: '100%',
position: 'absolute',
bottom: 0,
left: 0,
backgroundColor: this.props.theme.colors.surface,
transform: [{translateY: this.state.translateY}],
}}
>
{icons}
</Animated.View>
);
}
} }
export default withTheme(CustomTabBar); export default withTheme(CustomTabBar);

View file

@ -1,106 +1,133 @@
// @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) {
super(props);
Animatable.initializeRegistryWithDefinitions({
fabFocusIn: {
'0': {
scale: 1,
translateY: 0,
},
'0.9': {
scale: 1.2,
translateY: -9,
},
'1': {
scale: 1.1,
translateY: -7,
},
},
fabFocusOut: {
'0': {
scale: 1.1,
translateY: -6,
},
'1': {
scale: 1,
translateY: 0,
},
},
});
}
focusedIcon = require('../../../assets/tab-icon.png'); shouldComponentUpdate(nextProps: PropsType): boolean {
unFocusedIcon = require('../../../assets/tab-icon-outline.png'); const {focused} = this.props;
return nextProps.focused !== focused;
}
constructor(props) { getIconRender = ({
super(props); size,
Animatable.initializeRegistryWithDefinitions({ color,
fabFocusIn: { }: {
"0": { size: number,
scale: 1, translateY: 0 color: string,
}, }): React.Node => {
"0.9": { const {focused} = this.props;
scale: 1.2, translateY: -9 if (focused)
}, return (
"1": { <Image
scale: 1.1, translateY: -7 source={FOCUSED_ICON}
}, style={{
}, width: size,
fabFocusOut: { height: size,
"0": { tintColor: color,
scale: 1.1, translateY: -6 }}
}, />
"1": { );
scale: 1, translateY: 0 return (
}, <Image
} source={UNFOCUSED_ICON}
}); style={{
} width: size,
height: size,
iconRender = ({size, color}) => tintColor: color,
this.props.focused }}
? <Image />
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 {
const props = this.props;
return (
<View
style={{
flex: 1,
justifyContent: 'center',
}}>
<TouchableRipple
onPress={props.onPress}
onLongPress={props.onLongPress}
borderless={true}
rippleColor={Platform.OS === 'android' ? this.props.theme.colors.primary : 'transparent'}
style={{
position: 'absolute',
bottom: 0,
left: 0,
width: '100%',
height: this.props.tabBarHeight + 30,
marginBottom: -15,
}}
>
<AnimatedFAB
duration={200}
easing={"ease-out"}
animation={props.focused ? "fabFocusIn" : "fabFocusOut"}
icon={this.iconRender}
style={{
marginTop: 15,
marginLeft: 'auto',
marginRight: 'auto'
}}/>
</TouchableRipple>
</View>
);
}
render(): React.Node {
const {props} = this;
return (
<View
style={{
flex: 1,
justifyContent: 'center',
}}>
<TouchableRipple
onPress={props.onPress}
onLongPress={props.onLongPress}
borderless
rippleColor={
Platform.OS === 'android'
? props.theme.colors.primary
: 'transparent'
}
style={{
position: 'absolute',
bottom: 0,
left: 0,
width: '100%',
height: props.tabBarHeight + 30,
marginBottom: -15,
}}>
<AnimatedFAB
duration={200}
easing="ease-out"
animation={props.focused ? 'fabFocusIn' : 'fabFocusOut'}
icon={this.getIconRender}
style={{
marginTop: 15,
marginLeft: 'auto',
marginRight: 'auto',
}}
/>
</TouchableRipple>
</View>
);
}
} }
export default withTheme(TabHomeIcon); export default withTheme(TabHomeIcon);

View file

@ -1,114 +1,117 @@
// @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 = {
focused: boolean,
color: string,
label: string,
icon: MaterialCommunityIconsGlyphs,
onPress: Function,
onLongPress: Function,
theme: Object,
extraData: any,
}
type PropsType = {
focused: boolean,
color: string,
label: string,
icon: MaterialCommunityIconsGlyphs,
onPress: () => void,
onLongPress: () => void,
theme: CustomTheme,
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: PropsType) {
super(props);
Animatable.initializeRegistryWithDefinitions({
focusIn: {
'0': {
scale: 1,
translateY: 0,
},
'0.9': {
scale: 1.3,
translateY: 7,
},
'1': {
scale: 1.2,
translateY: 6,
},
},
focusOut: {
'0': {
scale: 1.2,
translateY: 6,
},
'1': {
scale: 1,
translateY: 0,
},
},
});
this.firstRender = true;
}
constructor(props) { componentDidMount() {
super(props); this.firstRender = false;
Animatable.initializeRegistryWithDefinitions({ }
focusIn: {
"0": {
scale: 1, translateY: 0
},
"0.9": {
scale: 1.3, translateY: 7
},
"1": {
scale: 1.2, translateY: 6
},
},
focusOut: {
"0": {
scale: 1.2, translateY: 6
},
"1": {
scale: 1, translateY: 0
},
}
});
this.firstRender = true;
}
componentDidMount() { shouldComponentUpdate(nextProps: PropsType): boolean {
this.firstRender = false; const {props} = this;
} return (
nextProps.focused !== props.focused ||
nextProps.theme.dark !== props.theme.dark ||
nextProps.extraData !== props.extraData
);
}
shouldComponentUpdate(nextProps: Props): boolean { render(): React.Node {
return (nextProps.focused !== this.props.focused) const {props} = this;
|| (nextProps.theme.dark !== this.props.theme.dark) return (
|| (nextProps.extraData !== this.props.extraData); <TouchableRipple
} onPress={props.onPress}
onLongPress={props.onLongPress}
render(): React$Node { borderless
const props = this.props; rippleColor={props.theme.colors.primary}
return ( style={{
<TouchableRipple flex: 1,
onPress={props.onPress} justifyContent: 'center',
onLongPress={props.onLongPress} }}>
borderless={true} <View>
rippleColor={this.props.theme.colors.primary} <Animatable.View
style={{ duration={200}
flex: 1, easing="ease-out"
justifyContent: 'center', animation={props.focused ? 'focusIn' : 'focusOut'}
}} useNativeDriver>
> <MaterialCommunityIcons
<View> name={props.icon}
<Animatable.View color={props.color}
duration={200} size={26}
easing={"ease-out"} style={{
animation={props.focused ? "focusIn" : "focusOut"} marginLeft: 'auto',
useNativeDriver marginRight: 'auto',
> }}
<MaterialCommunityIcons />
name={props.icon} </Animatable.View>
color={props.color} <Animatable.Text
size={26} animation={props.focused ? 'fadeOutDown' : 'fadeIn'}
style={{ useNativeDriver
marginLeft: 'auto', style={{
marginRight: 'auto', color: props.color,
}} marginLeft: 'auto',
/> marginRight: 'auto',
</Animatable.View> fontSize: 10,
<Animatable.Text }}>
animation={props.focused ? "fadeOutDown" : "fadeIn"} {props.label}
useNativeDriver </Animatable.Text>
</View>
style={{ </TouchableRipple>
color: props.color, );
marginLeft: 'auto', }
marginRight: 'auto',
fontSize: 10,
}}
>
{props.label}
</Animatable.Text>
</View>
</TouchableRipple>
);
}
} }
export default withTheme(TabIcon); export default withTheme(TabIcon);