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

feat: fix the janky request money tab swipe on web #26022

Merged
merged 5 commits into from
Aug 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/components/TabSelector/TabIcon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {StyleSheet, View, Animated} from 'react-native';
import React from 'react';
import PropTypes from 'prop-types';
import Icon from '../Icon';
import themeColors from '../../styles/themes/default';

const propTypes = {
/** Icon to display on tab */
icon: PropTypes.func,

/** Animated opacity value while the label is inactive state */
inactiveOpacity: PropTypes.number,

/** Animated opacity value while the label is in active state */
activeOpacity: PropTypes.number,
};

const defaultProps = {
icon: '',
inactiveOpacity: 1,
activeOpacity: 0,
};

function TabIcon({icon, activeOpacity, inactiveOpacity}) {
return (
<View>
Copy link
Contributor

@burczu burczu Aug 29, 2023

Choose a reason for hiding this comment

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

is this wrapping <View> necessary? wouldn't be fragment (<>) enough?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. Without it, the layout breaks.

<Animated.View style={{opacity: inactiveOpacity}}>
<Icon
src={icon}
fill={themeColors.icon}
/>
</Animated.View>
<Animated.View style={[StyleSheet.absoluteFill, {opacity: activeOpacity}]}>
<Icon
src={icon}
fill={themeColors.iconMenu}
/>
</Animated.View>
</View>
);
}

TabIcon.propTypes = propTypes;
TabIcon.defaultProps = defaultProps;
TabIcon.displayName = 'TabIcon';

export default TabIcon;
40 changes: 40 additions & 0 deletions src/components/TabSelector/TabLabel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {StyleSheet, View, Text, Animated} from 'react-native';
import React from 'react';
import PropTypes from 'prop-types';
import styles from '../../styles/styles';

const propTypes = {
/** Title of the tab */
title: PropTypes.string,

/** Animated opacity value while the label is inactive state */
inactiveOpacity: PropTypes.number,

/** Animated opacity value while the label is in active state */
activeOpacity: PropTypes.number,
};

const defaultProps = {
title: '',
inactiveOpacity: 1,
activeOpacity: 0,
};

function TabLabel({title, activeOpacity, inactiveOpacity}) {
return (
<View>
Copy link
Contributor

Choose a reason for hiding this comment

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

same here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same reply.

<Animated.View style={[{opacity: activeOpacity}]}>
<Text style={styles.tabText(true)}>{title}</Text>
</Animated.View>
<Animated.View style={[StyleSheet.absoluteFill, {opacity: inactiveOpacity}]}>
<Text style={styles.tabText(false)}>{title}</Text>
</Animated.View>
</View>
);
}

TabLabel.propTypes = propTypes;
TabLabel.defaultProps = defaultProps;
TabLabel.displayName = 'TabLabel';

export default TabLabel;
49 changes: 46 additions & 3 deletions src/components/TabSelector/TabSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import TabSelectorItem from './TabSelectorItem';
import CONST from '../../CONST';
import useLocalize from '../../hooks/useLocalize';
import styles from '../../styles/styles';
import themeColors from '../../styles/themes/default';

const propTypes = {
/* Navigation state provided by React Navigation */
Expand All @@ -21,10 +22,18 @@ const propTypes = {

/* Callback fired when tab is pressed */
onTabPress: PropTypes.func,

/* AnimatedValue for the position of the screen while swiping */
position: PropTypes.shape({
Copy link
Contributor

Choose a reason for hiding this comment

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

prop description is missing here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added.

interpolate: PropTypes.func.isRequired,
}),
};

const defaultProps = {
onTabPress: () => {},
position: {
interpolate: () => {},
},
};

const getIcon = (route) => {
Expand All @@ -49,12 +58,44 @@ const getTitle = (route, translate) => {
}
};

function TabSelector({state, navigation, onTabPress}) {
const getOpacity = (position, routesLength, tabIndex, active) => {
const activeValue = active ? 1 : 0;
const inactiveValue = active ? 0 : 1;

if (routesLength > 1) {
const inputRange = Array.from({length: routesLength}, (v, i) => i);

return position.interpolate({
inputRange,
outputRange: _.map(inputRange, (i) => (i === tabIndex ? activeValue : inactiveValue)),
});
}
return activeValue;
};

const getBackgroundColor = (position, routesLength, tabIndex) => {
if (routesLength > 1) {
const inputRange = Array.from({length: routesLength}, (v, i) => i);

return position.interpolate({
inputRange,
outputRange: _.map(inputRange, (i) => (i === tabIndex ? themeColors.midtone : themeColors.appBG)),
});
}
return themeColors.midtone;
};

function TabSelector({state, navigation, onTabPress, position}) {
const {translate} = useLocalize();

return (
<View style={styles.tabSelector}>
{_.map(state.routes, (route, index) => {
const isFocused = state.index === index;
const activeOpacity = getOpacity(position, state.routes.length, index, true);
const inactiveOpacity = getOpacity(position, state.routes.length, index, false);
const backgroundColor = getBackgroundColor(position, state.routes.length, index);

const isFocused = index === state.index;

const onPress = () => {
const event = navigation.emit({
Expand All @@ -73,11 +114,13 @@ function TabSelector({state, navigation, onTabPress}) {

return (
<TabSelectorItem
isSelected={isFocused}
key={route.name}
title={getTitle(route.name, translate)}
icon={getIcon(route.name)}
onPress={onPress}
activeOpacity={activeOpacity}
inactiveOpacity={inactiveOpacity}
backgroundColor={backgroundColor}
/>
);
})}
Expand Down
50 changes: 33 additions & 17 deletions src/components/TabSelector/TabSelectorItem.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {Text} from 'react-native';
import {Animated} from 'react-native';
import React from 'react';
import PropTypes from 'prop-types';
import Icon from '../Icon';
import themeColors from '../../styles/themes/default';
import styles from '../../styles/styles';
import PressableWithFeedback from '../Pressable/PressableWithFeedback';
import TabIcon from './TabIcon';
import TabLabel from './TabLabel';

const propTypes = {
/** Function to call when onPress */
Expand All @@ -13,34 +13,50 @@ const propTypes = {
/** Icon to display on tab */
icon: PropTypes.func,

/** True if tab is the selected item */
isSelected: PropTypes.bool,

/** Title of the tab */
title: PropTypes.string,

/** Animated background color value for the tab button */
// eslint-disable-next-line
backgroundColor: PropTypes.any,

/** Animated opacity value while the label is inactive state */
inactiveOpacity: PropTypes.number,

/** Animated opacity value while the label is in active state */
activeOpacity: PropTypes.number,
};

const defaultProps = {
onPress: () => {},
icon: () => {},
isSelected: false,
title: '',
backgroundColor: '',
inactiveOpacity: 1,
activeOpacity: 0,
};

function TabSelectorItem(props) {
const AnimatedPressableWithFeedback = Animated.createAnimatedComponent(PressableWithFeedback);

function TabSelectorItem({icon, title, onPress, backgroundColor, activeOpacity, inactiveOpacity}) {
return (
<PressableWithFeedback
accessibilityLabel={props.title}
style={[styles.tabSelectorButton(props.isSelected)]}
<AnimatedPressableWithFeedback
accessibilityLabel={title}
style={[styles.tabSelectorButton, {backgroundColor}]}
wrapperStyle={[styles.flex1]}
onPress={props.onPress}
onPress={onPress}
>
<Icon
src={props.icon}
fill={props.isSelected ? themeColors.iconMenu : themeColors.icon}
<TabIcon
icon={icon}
activeOpacity={activeOpacity}
inactiveOpacity={inactiveOpacity}
/>
<TabLabel
title={title}
activeOpacity={activeOpacity}
inactiveOpacity={inactiveOpacity}
/>
<Text style={styles.tabText(props.isSelected)}>{props.title}</Text>
</PressableWithFeedback>
</AnimatedPressableWithFeedback>
);
}

Expand Down
3 changes: 2 additions & 1 deletion src/pages/iou/MoneyRequestSelectorPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,12 @@ function MoneyRequestSelectorPage(props) {
{(canUseScanReceipts || canUseDistanceRequests) && iouType === CONST.IOU.MONEY_REQUEST_TYPE.REQUEST ? (
<OnyxTabNavigator
id={CONST.TAB.RECEIPT_TAB_ID}
tabBar={({state, navigation}) => (
tabBar={({state, navigation, position}) => (
<TabSelector
state={state}
navigation={navigation}
onTabPress={resetMoneyRequestInfo}
position={position}
/>
)}
>
Expand Down
5 changes: 2 additions & 3 deletions src/styles/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -3660,15 +3660,14 @@ const styles = {
height: 450,
},

tabSelectorButton: (isSelected) => ({
tabSelectorButton: {
height: 40,
padding: 12,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
borderRadius: variables.buttonBorderRadius,
backgroundColor: isSelected ? themeColors.midtone : themeColors.appBG,
}),
},

tabSelector: {
flexDirection: 'row',
Expand Down