Skip to content

Commit

Permalink
Merge pull request #45982 from dominictb/fix/43719-shift-tab-test
Browse files Browse the repository at this point in the history
fix: enable tab on IOU request start page
  • Loading branch information
tgolen authored Aug 12, 2024
2 parents 1adcf8e + 61718f8 commit 4ef1202
Show file tree
Hide file tree
Showing 13 changed files with 357 additions and 119 deletions.
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;
9 changes: 9 additions & 0 deletions src/components/FocusTrap/FocusTrapContainerElement/index.tsx
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;
29 changes: 29 additions & 0 deletions src/components/FocusTrap/FocusTrapContainerElement/index.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* 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;
if (typeof r === 'function') {
r(node);
} else if (r) {
r.current = node;
}
onContainerElementChanged?.(node as unknown as HTMLElement | null);
}}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
);
}

FocusTrapContainerElement.displayName = 'FocusTrapContainerElement';

export default React.forwardRef(FocusTrapContainerElement);
5 changes: 5 additions & 0 deletions src/components/FocusTrap/FocusTrapForScreen/FocusTrapProps.ts
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'];
};

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

0 comments on commit 4ef1202

Please sign in to comment.