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

fix: enable tab on IOU request start page #45982

Merged
merged 12 commits into from
Aug 12, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type {ViewProps} from 'react-native';

type FocusTrapContainerElementProps = ViewProps & {
/** Callback to register focus trap container element */
onContainerElementChanged?: (element: HTMLElement | null) => void;
};

export default FocusTrapContainerElementProps;
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type FocusTrapContainerElementProps from './FocusTrapContainerElementProps';

function FocusTrapContainerElement({children}: FocusTrapContainerElementProps) {
return children;
}

FocusTrapContainerElement.displayName = 'FocusTrapContainerElement';

export default FocusTrapContainerElement;
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* A wrapper View component allowing us to register a container element for a FocusTrap
*/
import type {ForwardedRef} from 'react';
import React from 'react';
import {View} from 'react-native';
import type FocusTrapContainerElementProps from './FocusTrapContainerElementProps';

function FocusTrapContainerElement({onContainerElementChanged, ...props}: FocusTrapContainerElementProps, ref?: ForwardedRef<View>) {
return (
<View
ref={(node) => {
const r = ref;
Copy link
Contributor

Choose a reason for hiding this comment

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

Can ref be referenced directly? If so, please remove this unnecessary variable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since we have ref.current = node, we'll need to assign to a local variable to avoid eslint error

if (typeof r === 'function') {
r(node);
} else if (r) {
r.current = node;
}
if (node) {
onContainerElementChanged?.(node as unknown as HTMLElement);
return () => onContainerElementChanged?.(null);
}
}}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
);
}

FocusTrapContainerElement.displayName = 'FocusTrapContainerElement';

export default React.forwardRef(FocusTrapContainerElement);
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type FocusTrap from 'focus-trap-react';

type FocusTrapForScreenProps = {
children: React.ReactNode;

/** Overrides the focus trap settings */
focusTrapSettings?: Pick<FocusTrap.Props, 'containerElements' | 'focusTrapOptions' | 'active'>;
};

export default FocusTrapForScreenProps;
9 changes: 7 additions & 2 deletions src/components/FocusTrap/FocusTrapForScreen/index.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus';
import CONST from '@src/CONST';
import type FocusTrapProps from './FocusTrapProps';

function FocusTrapForScreen({children}: FocusTrapProps) {
function FocusTrapForScreen({children, focusTrapSettings}: FocusTrapProps) {
const isFocused = useIsFocused();
const route = useRoute();
const {isSmallScreenWidth} = useWindowDimensions();

const isActive = useMemo(() => {
if (typeof focusTrapSettings?.active !== 'undefined') {
return focusTrapSettings.active;
}
// Focus trap can't be active on bottom tab screens because it would block access to the tab bar.
if (BOTTOM_TAB_SCREENS.find((screen) => screen === route.name)) {
return false;
Expand All @@ -31,12 +34,13 @@ function FocusTrapForScreen({children}: FocusTrapProps) {
return false;
}
return true;
}, [isFocused, isSmallScreenWidth, route.name]);
}, [isFocused, isSmallScreenWidth, route.name, focusTrapSettings?.active]);

return (
<FocusTrap
active={isActive}
paused={!isFocused}
containerElements={focusTrapSettings?.containerElements?.length ? focusTrapSettings.containerElements : undefined}
focusTrapOptions={{
trapStack: sharedTrapStack,
allowOutsideClick: true,
Expand All @@ -59,6 +63,7 @@ function FocusTrapForScreen({children}: FocusTrapProps) {
}
return element;
},
...(focusTrapSettings?.focusTrapOptions ?? {}),
}}
>
{children}
Expand Down
7 changes: 6 additions & 1 deletion src/components/ScreenWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import toggleTestToolsModal from '@userActions/TestTool';
import CONST from '@src/CONST';
import CustomDevMenu from './CustomDevMenu';
import FocusTrapForScreens from './FocusTrap/FocusTrapForScreen';
import type FocusTrapForScreenProps from './FocusTrap/FocusTrapForScreen/FocusTrapProps';
import HeaderGap from './HeaderGap';
import KeyboardAvoidingView from './KeyboardAvoidingView';
import OfflineIndicator from './OfflineIndicator';
Expand Down Expand Up @@ -99,6 +100,9 @@ type ScreenWrapperProps = {

/** Whether to show offline indicator on wide screens */
shouldShowOfflineIndicatorInWideScreen?: boolean;

/** Overrides the focus trap default settings */
focusTrapSettings?: FocusTrapForScreenProps['focusTrapSettings'];
dominictb marked this conversation as resolved.
Show resolved Hide resolved
};

type ScreenWrapperStatusContextType = {didScreenTransitionEnd: boolean};
Expand Down Expand Up @@ -126,6 +130,7 @@ function ScreenWrapper(
shouldAvoidScrollOnVirtualViewport = true,
shouldShowOfflineIndicatorInWideScreen = false,
shouldUseCachedViewportHeight = false,
focusTrapSettings,
}: ScreenWrapperProps,
ref: ForwardedRef<View>,
) {
Expand Down Expand Up @@ -242,7 +247,7 @@ function ScreenWrapper(
}

return (
<FocusTrapForScreens>
<FocusTrapForScreens focusTrapSettings={focusTrapSettings}>
<View
ref={ref}
style={[styles.flex1, {minHeight}]}
Expand Down
94 changes: 50 additions & 44 deletions src/components/TabSelector/TabSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {MaterialTopTabBarProps} from '@react-navigation/material-top-tabs/l
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import type {Animated} from 'react-native';
import {View} from 'react-native';
import FocusTrapContainerElement from '@components/FocusTrap/FocusTrapContainerElement';
import * as Expensicons from '@components/Icon/Expensicons';
import type {LocaleContextProps} from '@components/LocaleContextProvider';
import useLocalize from '@hooks/useLocalize';
Expand All @@ -14,6 +15,9 @@ import TabSelectorItem from './TabSelectorItem';
type TabSelectorProps = MaterialTopTabBarProps & {
/* Callback fired when tab is pressed */
onTabPress?: (name: string) => void;

/** Callback to register focus trap container element */
onFocusTrapContainerElementChanged?: (element: HTMLElement | null) => void;
};

type IconAndTitle = {
Expand Down Expand Up @@ -53,7 +57,7 @@ function getOpacity(position: Animated.AnimatedInterpolation<number>, routesLeng
return activeValue;
}

function TabSelector({state, navigation, onTabPress = () => {}, position}: TabSelectorProps) {
function TabSelector({state, navigation, onTabPress = () => {}, position, onFocusTrapContainerElementChanged}: TabSelectorProps) {
const {translate} = useLocalize();
const theme = useTheme();
const styles = useThemeStyles();
Expand Down Expand Up @@ -83,49 +87,51 @@ function TabSelector({state, navigation, onTabPress = () => {}, position}: TabSe
}, [defaultAffectedAnimatedTabs, state.index]);

return (
<View style={styles.tabSelector}>
{state.routes.map((route, index) => {
const activeOpacity = getOpacity(position, state.routes.length, index, true, affectedAnimatedTabs);
const inactiveOpacity = getOpacity(position, state.routes.length, index, false, affectedAnimatedTabs);
const backgroundColor = getBackgroundColor(state.routes.length, index, affectedAnimatedTabs);
const isActive = index === state.index;
const {icon, title} = getIconAndTitle(route.name, translate);

const onPress = () => {
if (isActive) {
return;
}

setAffectedAnimatedTabs([state.index, index]);

const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});

if (!event.defaultPrevented) {
// The `merge: true` option makes sure that the params inside the tab screen are preserved
navigation.navigate({key: route.key, merge: true});
}

onTabPress(route.name);
};

return (
<TabSelectorItem
key={route.name}
icon={icon}
title={title}
onPress={onPress}
activeOpacity={activeOpacity}
inactiveOpacity={inactiveOpacity}
backgroundColor={backgroundColor}
isActive={isActive}
/>
);
})}
</View>
<FocusTrapContainerElement onContainerElementChanged={onFocusTrapContainerElementChanged}>
<View style={styles.tabSelector}>
{state.routes.map((route, index) => {
const activeOpacity = getOpacity(position, state.routes.length, index, true, affectedAnimatedTabs);
const inactiveOpacity = getOpacity(position, state.routes.length, index, false, affectedAnimatedTabs);
const backgroundColor = getBackgroundColor(state.routes.length, index, affectedAnimatedTabs);
const isActive = index === state.index;
const {icon, title} = getIconAndTitle(route.name, translate);

const onPress = () => {
if (isActive) {
return;
}

setAffectedAnimatedTabs([state.index, index]);

const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});

if (!event.defaultPrevented) {
// The `merge: true` option makes sure that the params inside the tab screen are preserved
navigation.navigate({key: route.key, merge: true});
}

onTabPress(route.name);
};

return (
<TabSelectorItem
key={route.name}
icon={icon}
title={title}
onPress={onPress}
activeOpacity={activeOpacity}
inactiveOpacity={inactiveOpacity}
backgroundColor={backgroundColor}
isActive={isActive}
/>
);
})}
</View>
</FocusTrapContainerElement>
);
}

Expand Down
2 changes: 2 additions & 0 deletions src/components/TabSelector/TabSelectorItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import {Animated, StyleSheet} from 'react-native';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import type IconAsset from '@src/types/utils/IconAsset';
import TabIcon from './TabIcon';
import TabLabel from './TabLabel';
Expand Down Expand Up @@ -37,6 +38,7 @@ function TabSelectorItem({icon, title = '', onPress = () => {}, backgroundColor
style={[styles.tabSelectorButton]}
wrapperStyle={[styles.flex1]}
onPress={onPress}
role={CONST.ROLE.BUTTON}
>
{({hovered}) => (
<Animated.View style={[styles.tabSelectorButton, StyleSheet.absoluteFill, styles.tabBackground(hovered, isActive, backgroundColor)]}>
Expand Down
Loading
Loading