diff --git a/package-lock.json b/package-lock.json index 66015a4955a..63b8fc13582 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,6 +64,7 @@ "expo-av": "~13.10.4", "expo-image": "1.11.0", "expo-image-manipulator": "11.8.0", + "focus-trap-react": "^10.2.3", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", "jest-expo": "50.0.1", @@ -21287,6 +21288,28 @@ "node": ">=0.4.0" } }, + "node_modules/focus-trap": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", + "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "dependencies": { + "tabbable": "^6.2.0" + } + }, + "node_modules/focus-trap-react": { + "version": "10.2.3", + "resolved": "https://registry.npmjs.org/focus-trap-react/-/focus-trap-react-10.2.3.tgz", + "integrity": "sha512-YXBpFu/hIeSu6NnmV2xlXzOYxuWkoOtar9jzgp3lOmjWLWY59C/b8DtDHEAV4SPU07Nd/t+nS/SBNGkhUBFmEw==", + "dependencies": { + "focus-trap": "^7.5.4", + "tabbable": "^6.2.0" + }, + "peerDependencies": { + "prop-types": "^15.8.1", + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, "node_modules/follow-redirects": { "version": "1.15.5", "funding": [ @@ -35100,6 +35123,11 @@ "version": "3.2.4", "license": "MIT" }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, "node_modules/table": { "version": "6.8.1", "dev": true, diff --git a/package.json b/package.json index 697b9168dcd..acacff401e5 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ "expo-av": "~13.10.4", "expo-image": "1.11.0", "expo-image-manipulator": "11.8.0", + "focus-trap-react": "^10.2.3", "htmlparser2": "^7.2.0", "idb-keyval": "^6.2.1", "jest-expo": "50.0.1", diff --git a/patches/react-native-web+0.19.12+006+remove focus trap from modal.patch b/patches/react-native-web+0.19.12+006+remove focus trap from modal.patch new file mode 100644 index 00000000000..14dbc88b0b1 --- /dev/null +++ b/patches/react-native-web+0.19.12+006+remove focus trap from modal.patch @@ -0,0 +1,20 @@ +diff --git a/node_modules/react-native-web/dist/exports/Modal/index.js b/node_modules/react-native-web/dist/exports/Modal/index.js +index d5df021..e2c46cf 100644 +--- a/node_modules/react-native-web/dist/exports/Modal/index.js ++++ b/node_modules/react-native-web/dist/exports/Modal/index.js +@@ -86,13 +86,11 @@ var Modal = /*#__PURE__*/React.forwardRef((props, forwardedRef) => { + onDismiss: onDismissCallback, + onShow: onShowCallback, + visible: visible +- }, /*#__PURE__*/React.createElement(ModalFocusTrap, { +- active: isActive + }, /*#__PURE__*/React.createElement(ModalContent, _extends({}, rest, { + active: isActive, + onRequestClose: onRequestClose, + ref: forwardedRef, + transparent: transparent +- }), children)))); ++ }), children))); + }); + export default Modal; +\ No newline at end of file diff --git a/src/CONST.ts b/src/CONST.ts index d3132b17a2e..4924fd4d8b8 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3461,6 +3461,8 @@ const CONST = { TIMER: 'timer', /** Use for toolbars containing action buttons or components. */ TOOLBAR: 'toolbar', + /** Use for navigation elements */ + NAVIGATION: 'navigation', }, TRANSLATION_KEYS: { ATTACHMENT: 'common.attachment', diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx index f252cc5b734..2cc6a0ecec4 100644 --- a/src/components/ContextMenuItem.tsx +++ b/src/components/ContextMenuItem.tsx @@ -52,6 +52,9 @@ type ContextMenuItemProps = { /** Handles what to do when the item is focused */ onFocus?: () => void; + + /** Handles what to do when the item loose focus */ + onBlur?: () => void; }; type ContextMenuItemHandle = { @@ -74,6 +77,7 @@ function ContextMenuItem( shouldPreventDefaultFocusOnPress = true, buttonRef = {current: null}, onFocus = () => {}, + onBlur = () => {}, }: ContextMenuItemProps, ref: ForwardedRef, ) { @@ -130,6 +134,7 @@ function ContextMenuItem( focused={isFocused} interactive={isThrottledButtonActive} onFocus={onFocus} + onBlur={onBlur} /> ); } diff --git a/src/components/FocusTrap/BOTTOM_TAB_SCREENS.ts b/src/components/FocusTrap/BOTTOM_TAB_SCREENS.ts new file mode 100644 index 00000000000..e6af466b12c --- /dev/null +++ b/src/components/FocusTrap/BOTTOM_TAB_SCREENS.ts @@ -0,0 +1,6 @@ +import type {BottomTabName} from '@libs/Navigation/types'; +import SCREENS from '@src/SCREENS'; + +const BOTTOM_TAB_SCREENS: BottomTabName[] = [SCREENS.HOME, SCREENS.SETTINGS.ROOT]; + +export default BOTTOM_TAB_SCREENS; diff --git a/src/components/FocusTrap/FocusTrapForModal/FocusTrapForModalProps.ts b/src/components/FocusTrap/FocusTrapForModal/FocusTrapForModalProps.ts new file mode 100644 index 00000000000..6bc2350a6c5 --- /dev/null +++ b/src/components/FocusTrap/FocusTrapForModal/FocusTrapForModalProps.ts @@ -0,0 +1,6 @@ +type FocusTrapForModalProps = { + children: React.ReactNode; + active: boolean; +}; + +export default FocusTrapForModalProps; diff --git a/src/components/FocusTrap/FocusTrapForModal/index.tsx b/src/components/FocusTrap/FocusTrapForModal/index.tsx new file mode 100644 index 00000000000..01632998b07 --- /dev/null +++ b/src/components/FocusTrap/FocusTrapForModal/index.tsx @@ -0,0 +1,9 @@ +import type FocusTrapForModalProps from './FocusTrapForModalProps'; + +function FocusTrapForModal({children}: FocusTrapForModalProps) { + return children; +} + +FocusTrapForModal.displayName = 'FocusTrapForModal'; + +export default FocusTrapForModal; diff --git a/src/components/FocusTrap/FocusTrapForModal/index.web.tsx b/src/components/FocusTrap/FocusTrapForModal/index.web.tsx new file mode 100644 index 00000000000..161e3f1b7f8 --- /dev/null +++ b/src/components/FocusTrap/FocusTrapForModal/index.web.tsx @@ -0,0 +1,23 @@ +import FocusTrap from 'focus-trap-react'; +import React from 'react'; +import sharedTrapStack from '@components/FocusTrap/sharedTrapStack'; +import type FocusTrapForModalProps from './FocusTrapForModalProps'; + +function FocusTrapForModal({children, active}: FocusTrapForModalProps) { + return ( + + {children} + + ); +} + +FocusTrapForModal.displayName = 'FocusTrapForModal'; + +export default FocusTrapForModal; diff --git a/src/components/FocusTrap/FocusTrapForScreen/FocusTrapProps.ts b/src/components/FocusTrap/FocusTrapForScreen/FocusTrapProps.ts new file mode 100644 index 00000000000..d2f6e532344 --- /dev/null +++ b/src/components/FocusTrap/FocusTrapForScreen/FocusTrapProps.ts @@ -0,0 +1,5 @@ +type FocusTrapForScreenProps = { + children: React.ReactNode; +}; + +export default FocusTrapForScreenProps; diff --git a/src/components/FocusTrap/FocusTrapForScreen/index.tsx b/src/components/FocusTrap/FocusTrapForScreen/index.tsx new file mode 100644 index 00000000000..ae7ece116d1 --- /dev/null +++ b/src/components/FocusTrap/FocusTrapForScreen/index.tsx @@ -0,0 +1,9 @@ +import type FocusTrapProps from './FocusTrapProps'; + +function FocusTrapForScreen({children}: FocusTrapProps) { + return children; +} + +FocusTrapForScreen.displayName = 'FocusTrapForScreen'; + +export default FocusTrapForScreen; diff --git a/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx new file mode 100644 index 00000000000..6a1409ab4a9 --- /dev/null +++ b/src/components/FocusTrap/FocusTrapForScreen/index.web.tsx @@ -0,0 +1,72 @@ +import {useFocusEffect, useIsFocused, useRoute} from '@react-navigation/native'; +import FocusTrap from 'focus-trap-react'; +import React, {useCallback, useMemo} from 'react'; +import BOTTOM_TAB_SCREENS from '@components/FocusTrap/BOTTOM_TAB_SCREENS'; +import SCREENS_WITH_AUTOFOCUS from '@components/FocusTrap/SCREENS_WITH_AUTOFOCUS'; +import sharedTrapStack from '@components/FocusTrap/sharedTrapStack'; +import TOP_TAB_SCREENS from '@components/FocusTrap/TOP_TAB_SCREENS'; +import WIDE_LAYOUT_INACTIVE_SCREENS from '@components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import type FocusTrapProps from './FocusTrapProps'; + +let activeRouteName = ''; +function FocusTrapForScreen({children}: FocusTrapProps) { + const isFocused = useIsFocused(); + const route = useRoute(); + const {isSmallScreenWidth} = useWindowDimensions(); + + const isActive = useMemo(() => { + // 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; + } + + // in top tabs only focus trap for currently shown tab should be active + if (TOP_TAB_SCREENS.find((screen) => screen === route.name)) { + return isFocused; + } + + // Focus trap can't be active on these screens if the layout is wide because they may be displayed side by side. + if (WIDE_LAYOUT_INACTIVE_SCREENS.includes(route.name) && !isSmallScreenWidth) { + return false; + } + return true; + }, [isFocused, isSmallScreenWidth, route.name]); + + useFocusEffect( + useCallback(() => { + activeRouteName = route.name; + }, [route]), + ); + + return ( + { + if (SCREENS_WITH_AUTOFOCUS.includes(activeRouteName)) { + return false; + } + return undefined; + }, + setReturnFocus: (element) => { + if (SCREENS_WITH_AUTOFOCUS.includes(activeRouteName)) { + return false; + } + return element; + }, + }} + > + {children} + + ); +} + +FocusTrapForScreen.displayName = 'FocusTrapForScreen'; + +export default FocusTrapForScreen; diff --git a/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts b/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts new file mode 100644 index 00000000000..2a77b52e311 --- /dev/null +++ b/src/components/FocusTrap/SCREENS_WITH_AUTOFOCUS.ts @@ -0,0 +1,15 @@ +import {CENTRAL_PANE_WORKSPACE_SCREENS} from '@libs/Navigation/AppNavigator/Navigators/FullScreenNavigator'; +import SCREENS from '@src/SCREENS'; + +const SCREENS_WITH_AUTOFOCUS: string[] = [ + ...Object.keys(CENTRAL_PANE_WORKSPACE_SCREENS), + SCREENS.REPORT, + SCREENS.REPORT_DESCRIPTION_ROOT, + SCREENS.PRIVATE_NOTES.EDIT, + SCREENS.SETTINGS.PROFILE.STATUS, + SCREENS.SETTINGS.PROFILE.PRONOUNS, + SCREENS.NEW_TASK.DETAILS, + SCREENS.MONEY_REQUEST.CREATE, +]; + +export default SCREENS_WITH_AUTOFOCUS; diff --git a/src/components/FocusTrap/TOP_TAB_SCREENS.ts b/src/components/FocusTrap/TOP_TAB_SCREENS.ts new file mode 100644 index 00000000000..6bee36b8688 --- /dev/null +++ b/src/components/FocusTrap/TOP_TAB_SCREENS.ts @@ -0,0 +1,5 @@ +import CONST from '@src/CONST'; + +const TOP_TAB_SCREENS: string[] = [CONST.TAB.NEW_CHAT, CONST.TAB.NEW_ROOM, CONST.TAB_REQUEST.DISTANCE, CONST.TAB_REQUEST.MANUAL, CONST.TAB_REQUEST.SCAN]; + +export default TOP_TAB_SCREENS; diff --git a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts new file mode 100644 index 00000000000..2d0c51edbba --- /dev/null +++ b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts @@ -0,0 +1,36 @@ +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; + +// Screens that should not have active focus trap when rendered on wide screen in order to allow Tab navigation in LHP and RHP +const WIDE_LAYOUT_INACTIVE_SCREENS: string[] = [ + NAVIGATORS.BOTTOM_TAB_NAVIGATOR, + SCREENS.HOME, + SCREENS.SETTINGS.ROOT, + SCREENS.REPORT, + SCREENS.SETTINGS.PROFILE.ROOT, + SCREENS.SETTINGS.PREFERENCES.ROOT, + SCREENS.SETTINGS.SECURITY, + SCREENS.SETTINGS.WALLET.ROOT, + SCREENS.SETTINGS.ABOUT, + SCREENS.SETTINGS.WORKSPACES, + SCREENS.WORKSPACE.INITIAL, + SCREENS.WORKSPACE.PROFILE, + SCREENS.WORKSPACE.CARD, + SCREENS.WORKSPACE.WORKFLOWS, + SCREENS.WORKSPACE.WORKFLOWS_APPROVER, + SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY, + SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET, + SCREENS.WORKSPACE.REIMBURSE, + SCREENS.WORKSPACE.BILLS, + SCREENS.WORKSPACE.INVOICES, + SCREENS.WORKSPACE.TRAVEL, + SCREENS.WORKSPACE.MEMBERS, + SCREENS.WORKSPACE.CATEGORIES, + SCREENS.WORKSPACE.MORE_FEATURES, + SCREENS.WORKSPACE.TAGS, + SCREENS.WORKSPACE.TAXES, + SCREENS.WORKSPACE.DISTANCE_RATES, + SCREENS.SEARCH.CENTRAL_PANE, +]; + +export default WIDE_LAYOUT_INACTIVE_SCREENS; diff --git a/src/components/FocusTrap/sharedTrapStack.ts b/src/components/FocusTrap/sharedTrapStack.ts new file mode 100644 index 00000000000..d0fa12f3fe9 --- /dev/null +++ b/src/components/FocusTrap/sharedTrapStack.ts @@ -0,0 +1,6 @@ +import type {FocusTrap as FocusTrapHandler} from 'focus-trap'; + +// focus-trap is capable of managing many traps. It's necessary for RHP and modals +const trapStack: FocusTrapHandler[] = []; + +export default trapStack; diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 78cd92dbc22..94a241637eb 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -158,7 +158,7 @@ function HeaderWithBackButton({ } }} style={[styles.touchableButtonImage]} - role="button" + role={CONST.ROLE.BUTTON} accessibilityLabel={translate('common.back')} id={CONST.BACK_BUTTON_NATIVE_ID} > diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 554ae9d4a84..7ed6e2ea2c0 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -281,6 +281,9 @@ type MenuItemBaseProps = { /** Handles what to do when the item is focused */ onFocus?: () => void; + /** Handles what to do when the item loose focus */ + onBlur?: () => void; + /** Optional account id if it's user avatar or policy id if it's workspace avatar */ avatarID?: number | string; }; @@ -365,6 +368,7 @@ function MenuItem( isPaneMenu = false, shouldPutLeftPaddingWhenNoIcon = false, onFocus, + onBlur, avatarID, }: MenuItemProps, ref: PressableRef, @@ -462,7 +466,7 @@ function MenuItem( }; return ( - + {!!label && !isLabelHoverable && ( {label} diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index e24c5b7c9c8..f76fc94dbf8 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -2,6 +2,7 @@ import React, {forwardRef, useCallback, useEffect, useMemo, useRef} from 'react' import {View} from 'react-native'; import ReactNativeModal from 'react-native-modal'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; +import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import useKeyboardState from '@hooks/useKeyboardState'; import usePrevious from '@hooks/usePrevious'; import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; @@ -214,7 +215,7 @@ function BaseModal( // a conflict between RN core and Reanimated shadow tree operations // position absolute is needed to prevent the view from interfering with flex layout collapsable={false} - style={[styles.pAbsolute]} + style={[styles.pAbsolute, {zIndex: 1}]} > : undefined} > - - - {children} + + + + + {children} + + - + diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 0bdf83bf4f2..1ed819ca853 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -12,6 +12,7 @@ import CONST from '@src/CONST'; import type {AnchorPosition} from '@src/styles'; import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; import FocusableMenuItem from './FocusableMenuItem'; +import FocusTrapForModal from './FocusTrap/FocusTrapForModal'; import * as Expensicons from './Icon/Expensicons'; import type {MenuItemProps} from './MenuItem'; import MenuItem from './MenuItem'; @@ -210,39 +211,41 @@ function PopoverMenu({ shouldSetModalVisibility={shouldSetModalVisibility} shouldEnableNewFocusManagement={shouldEnableNewFocusManagement} > - - {!!headerText && enteredSubMenuIndexes.length === 0 && {headerText}} - {enteredSubMenuIndexes.length > 0 && renderBackButtonItem()} - {currentMenuItems.map((item, menuIndex) => ( - selectItem(menuIndex)} - focused={focusedIndex === menuIndex} - displayInDefaultIconColor={item.displayInDefaultIconColor} - shouldShowRightIcon={item.shouldShowRightIcon} - iconRight={item.iconRight} - shouldPutLeftPaddingWhenNoIcon={item.shouldPutLeftPaddingWhenNoIcon} - label={item.label} - isLabelHoverable={item.isLabelHoverable} - floatRightAvatars={item.floatRightAvatars} - floatRightAvatarSize={item.floatRightAvatarSize} - shouldShowSubscriptRightAvatar={item.shouldShowSubscriptRightAvatar} - disabled={item.disabled} - onFocus={() => setFocusedIndex(menuIndex)} - success={item.success} - containerStyle={item.containerStyle} - /> - ))} - + + + {!!headerText && enteredSubMenuIndexes.length === 0 && {headerText}} + {enteredSubMenuIndexes.length > 0 && renderBackButtonItem()} + {currentMenuItems.map((item, menuIndex) => ( + selectItem(menuIndex)} + focused={focusedIndex === menuIndex} + displayInDefaultIconColor={item.displayInDefaultIconColor} + shouldShowRightIcon={item.shouldShowRightIcon} + iconRight={item.iconRight} + shouldPutLeftPaddingWhenNoIcon={item.shouldPutLeftPaddingWhenNoIcon} + label={item.label} + isLabelHoverable={item.isLabelHoverable} + floatRightAvatars={item.floatRightAvatars} + floatRightAvatarSize={item.floatRightAvatarSize} + shouldShowSubscriptRightAvatar={item.shouldShowSubscriptRightAvatar} + disabled={item.disabled} + onFocus={() => setFocusedIndex(menuIndex)} + success={item.success} + containerStyle={item.containerStyle} + /> + ))} + + ); } diff --git a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx index cf960df5c2c..e866f76e123 100644 --- a/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx +++ b/src/components/Pressable/GenericPressable/BaseGenericPressable.tsx @@ -86,12 +86,11 @@ function GenericPressable( if (shouldUseHapticsOnLongPress) { HapticFeedback.longPress(); } - if (ref && 'current' in ref) { + if (ref && 'current' in ref && nextFocusRef) { ref.current?.blur(); + Accessibility.moveAccessibilityFocus(nextFocusRef); } onLongPress(event); - - Accessibility.moveAccessibilityFocus(nextFocusRef); }, [shouldUseHapticsOnLongPress, onLongPress, nextFocusRef, ref, isDisabled], ); @@ -107,11 +106,11 @@ function GenericPressable( if (shouldUseHapticsOnPress) { HapticFeedback.press(); } - if (ref && 'current' in ref) { + if (ref && 'current' in ref && nextFocusRef) { ref.current?.blur(); + Accessibility.moveAccessibilityFocus(nextFocusRef); } const onPressResult = onPress(event); - Accessibility.moveAccessibilityFocus(nextFocusRef); return onPressResult; }, [shouldUseHapticsOnPress, onPress, nextFocusRef, ref, isDisabled], diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx index 1e9cfd23f66..c5b133d7428 100644 --- a/src/components/ScreenWrapper.tsx +++ b/src/components/ScreenWrapper.tsx @@ -19,6 +19,7 @@ import type {CentralPaneNavigatorParamList, RootStackParamList} from '@libs/Navi import toggleTestToolsModal from '@userActions/TestTool'; import CONST from '@src/CONST'; import CustomDevMenu from './CustomDevMenu'; +import FocusTrapForScreens from './FocusTrap/FocusTrapForScreen'; import HeaderGap from './HeaderGap'; import KeyboardAvoidingView from './KeyboardAvoidingView'; import OfflineIndicator from './OfflineIndicator'; @@ -100,7 +101,9 @@ type ScreenWrapperProps = { shouldShowOfflineIndicatorInWideScreen?: boolean; }; -const ScreenWrapperStatusContext = createContext({didScreenTransitionEnd: false}); +type ScreenWrapperStatusContextType = {didScreenTransitionEnd: boolean}; + +const ScreenWrapperStatusContext = createContext(undefined); function ScreenWrapper( { @@ -239,53 +242,55 @@ function ScreenWrapper( } return ( - + - - - - - {isDevelopment && } - - { - // If props.children is a function, call it to provide the insets to the children. - typeof children === 'function' - ? children({ - insets, - safeAreaPaddingBottomStyle, - didScreenTransitionEnd, - }) - : children - } - - {isSmallScreenWidth && shouldShowOfflineIndicator && } - {!isSmallScreenWidth && shouldShowOfflineIndicatorInWideScreen && ( - - )} - - + + + + {isDevelopment && } + + { + // If props.children is a function, call it to provide the insets to the children. + typeof children === 'function' + ? children({ + insets, + safeAreaPaddingBottomStyle, + didScreenTransitionEnd, + }) + : children + } + {isSmallScreenWidth && shouldShowOfflineIndicator && } + {!isSmallScreenWidth && shouldShowOfflineIndicatorInWideScreen && ( + + )} + + + + - + ); }} diff --git a/src/hooks/useSyncFocus/index.ts b/src/hooks/useSyncFocus/index.ts index afc4e7e351a..f8b3349bc22 100644 --- a/src/hooks/useSyncFocus/index.ts +++ b/src/hooks/useSyncFocus/index.ts @@ -1,7 +1,7 @@ -import {useLayoutEffect} from 'react'; +import {useContext, useLayoutEffect} from 'react'; import type {RefObject} from 'react'; import type {View} from 'react-native'; -import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus'; +import {ScreenWrapperStatusContext} from '@components/ScreenWrapper'; /** * Custom React hook created to handle sync of focus on an element when the user navigates through the app with keyboard. @@ -9,7 +9,11 @@ import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionSt * To maintain consistency when an element is focused in the app, the focus() method is additionally called on the focused element to eliminate the difference between native browser focus and application focus. */ const useSyncFocus = (ref: RefObject, isFocused: boolean, shouldSyncFocus = true) => { - const {didScreenTransitionEnd} = useScreenWrapperTranstionStatus(); + // this hook can be used outside ScreenWrapperStatusContext (eg. in Popovers). So we to check if the context is present. + // If we are outside context we don't have to look at transition status + const contextValue = useContext(ScreenWrapperStatusContext); + + const didScreenTransitionEnd = contextValue ? contextValue.didScreenTransitionEnd : true; useLayoutEffect(() => { if (!isFocused || !shouldSyncFocus || !didScreenTransitionEnd) { diff --git a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx index 1745e350c62..64485872544 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {View} from 'react-native'; +import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -14,7 +15,7 @@ const RootStack = createCustomFullScreenNavigator(); type Screens = Partial React.ComponentType>>; -const centralPaneWorkspaceScreens = { +const CENTRAL_PANE_WORKSPACE_SCREENS = { [SCREENS.WORKSPACE.PROFILE]: () => require('../../../../pages/workspace/WorkspaceProfilePage').default as React.ComponentType, [SCREENS.WORKSPACE.CARD]: () => require('../../../../pages/workspace/card/WorkspaceCardPage').default as React.ComponentType, [SCREENS.WORKSPACE.WORKFLOWS]: () => require('../../../../pages/workspace/workflows/WorkspaceWorkflowsPage').default as React.ComponentType, @@ -38,25 +39,28 @@ function FullScreenNavigator() { const screenOptions = getRootNavigatorScreenOptions(isSmallScreenWidth, styles, StyleUtils); return ( - - - - {Object.entries(centralPaneWorkspaceScreens).map(([screenName, componentGetter]) => ( + + + - ))} - - + {Object.entries(CENTRAL_PANE_WORKSPACE_SCREENS).map(([screenName, componentGetter]) => ( + + ))} + + + ); } FullScreenNavigator.displayName = 'FullScreenNavigator'; +export {CENTRAL_PANE_WORKSPACE_SCREENS}; export default FullScreenNavigator; diff --git a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx index d2d71a5dc2a..9c17d5da53a 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx @@ -3,6 +3,7 @@ import React, {useCallback, useEffect} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import NoDropZone from '@components/DragAndDrop/NoDropZone'; +import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useOnboardingLayout from '@hooks/useOnboardingLayout'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -59,25 +60,27 @@ function OnboardingModalNavigator() { onClick={handleOuterClick} style={styles.onboardingNavigatorOuterView} > - e.stopPropagation()} - style={styles.OnboardingNavigatorInnerView(shouldUseNarrowLayout)} - > - - - - - - + + e.stopPropagation()} + style={styles.OnboardingNavigatorInnerView(shouldUseNarrowLayout)} + > + + + + + + + ); diff --git a/src/pages/OnboardingPersonalDetails/index.tsx b/src/pages/OnboardingPersonalDetails/index.tsx index d6a408ee738..f0b32e6d627 100644 --- a/src/pages/OnboardingPersonalDetails/index.tsx +++ b/src/pages/OnboardingPersonalDetails/index.tsx @@ -1,14 +1,23 @@ import React from 'react'; +import {View} from 'react-native'; +import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen'; +import useThemeStyles from '@hooks/useThemeStyles'; import BaseOnboardingPersonalDetails from './BaseOnboardingPersonalDetails'; import type {OnboardingPersonalDetailsProps} from './types'; function OnboardingPersonalDetails({...rest}: Omit) { + const styles = useThemeStyles(); + return ( - + + + + + ); } diff --git a/src/pages/OnboardingPurpose/index.tsx b/src/pages/OnboardingPurpose/index.tsx index d7abedacbb8..bbe698e0970 100644 --- a/src/pages/OnboardingPurpose/index.tsx +++ b/src/pages/OnboardingPurpose/index.tsx @@ -1,15 +1,24 @@ import React from 'react'; +import {View} from 'react-native'; +import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen'; +import useThemeStyles from '@hooks/useThemeStyles'; import BaseOnboardingPurpose from './BaseOnboardingPurpose'; import type {OnboardingPurposeProps} from './types'; function OnboardingPurpose({...rest}: OnboardingPurposeProps) { + const styles = useThemeStyles(); + return ( - + + + + + ); } diff --git a/src/pages/OnboardingWork/index.tsx b/src/pages/OnboardingWork/index.tsx index ba1b8aaeb10..4323ad1c2ab 100644 --- a/src/pages/OnboardingWork/index.tsx +++ b/src/pages/OnboardingWork/index.tsx @@ -1,14 +1,22 @@ import React from 'react'; +import {View} from 'react-native'; +import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen'; +import useThemeStyles from '@hooks/useThemeStyles'; import BaseOnboardingWork from './BaseOnboardingWork'; import type {OnboardingWorkProps} from './types'; function OnboardingWork({...rest}: Omit) { + const styles = useThemeStyles(); return ( - + + + + + ); } diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 46ebdd75176..7a95c371f17 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -8,6 +8,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {ContextMenuItemHandle} from '@components/ContextMenuItem'; import ContextMenuItem from '@components/ContextMenuItem'; +import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; @@ -163,6 +164,7 @@ function BaseReportActionContextMenu({ disabledIndexes, maxIndex: filteredContextMenuActions.length - 1, isActive: shouldEnableArrowNavigation, + disableCyclicTraversal: true, }); /** @@ -229,64 +231,67 @@ function BaseReportActionContextMenu({ return ( (isVisible || shouldKeepOpen) && ( - - {filteredContextMenuActions.map((contextAction, index) => { - const closePopup = !isMini; - const payload: ContextMenuActionPayload = { - reportAction: reportAction as ReportAction, - reportID, - draftMessage, - selection, - close: () => setShouldKeepOpen(false), - openContextMenu: () => setShouldKeepOpen(true), - interceptAnonymousUser, - openOverflowMenu, - setIsEmojiPickerActive, - }; - - if ('renderContent' in contextAction) { - return contextAction.renderContent(closePopup, payload); - } - - const {textTranslateKey} = contextAction; - const isKeyInActionUpdateKeys = - textTranslateKey === 'reportActionContextMenu.editAction' || - textTranslateKey === 'reportActionContextMenu.deleteAction' || - textTranslateKey === 'reportActionContextMenu.deleteConfirmation'; - const text = textTranslateKey && (isKeyInActionUpdateKeys ? translate(textTranslateKey, {action: reportAction}) : translate(textTranslateKey)); - const transactionPayload = textTranslateKey === 'reportActionContextMenu.copyToClipboard' && transaction && {transaction}; - const isMenuAction = textTranslateKey === 'reportActionContextMenu.menu'; - - return ( - { - menuItemRefs.current[index] = ref; - }} - buttonRef={isMenuAction ? threedotRef : {current: null}} - icon={contextAction.icon} - text={text ?? ''} - successIcon={contextAction.successIcon} - successText={contextAction.successTextTranslateKey ? translate(contextAction.successTextTranslateKey) : undefined} - isMini={isMini} - key={contextAction.textTranslateKey} - onPress={(event) => - interceptAnonymousUser( - () => contextAction.onPress?.(closePopup, {...payload, ...transactionPayload, event, ...(isMenuAction ? {anchorRef: threedotRef} : {})}), - contextAction.isAnonymousAction, - ) - } - description={contextAction.getDescription?.(selection) ?? ''} - isAnonymousAction={contextAction.isAnonymousAction} - isFocused={focusedIndex === index} - shouldPreventDefaultFocusOnPress={contextAction.shouldPreventDefaultFocusOnPress} - onFocus={() => setFocusedIndex(index)} - /> - ); - })} - + + + {filteredContextMenuActions.map((contextAction, index) => { + const closePopup = !isMini; + const payload: ContextMenuActionPayload = { + reportAction: reportAction as ReportAction, + reportID, + draftMessage, + selection, + close: () => setShouldKeepOpen(false), + openContextMenu: () => setShouldKeepOpen(true), + interceptAnonymousUser, + openOverflowMenu, + setIsEmojiPickerActive, + }; + + if ('renderContent' in contextAction) { + return contextAction.renderContent(closePopup, payload); + } + + const {textTranslateKey} = contextAction; + const isKeyInActionUpdateKeys = + textTranslateKey === 'reportActionContextMenu.editAction' || + textTranslateKey === 'reportActionContextMenu.deleteAction' || + textTranslateKey === 'reportActionContextMenu.deleteConfirmation'; + const text = textTranslateKey && (isKeyInActionUpdateKeys ? translate(textTranslateKey, {action: reportAction}) : translate(textTranslateKey)); + const transactionPayload = textTranslateKey === 'reportActionContextMenu.copyToClipboard' && transaction && {transaction}; + const isMenuAction = textTranslateKey === 'reportActionContextMenu.menu'; + + return ( + { + menuItemRefs.current[index] = ref; + }} + buttonRef={isMenuAction ? threedotRef : {current: null}} + icon={contextAction.icon} + text={text ?? ''} + successIcon={contextAction.successIcon} + successText={contextAction.successTextTranslateKey ? translate(contextAction.successTextTranslateKey) : undefined} + isMini={isMini} + key={contextAction.textTranslateKey} + onPress={(event) => + interceptAnonymousUser( + () => contextAction.onPress?.(closePopup, {...payload, ...transactionPayload, event, ...(isMenuAction ? {anchorRef: threedotRef} : {})}), + contextAction.isAnonymousAction, + ) + } + description={contextAction.getDescription?.(selection) ?? ''} + isAnonymousAction={contextAction.isAnonymousAction} + isFocused={focusedIndex === index} + shouldPreventDefaultFocusOnPress={contextAction.shouldPreventDefaultFocusOnPress} + onFocus={() => setFocusedIndex(index)} + onBlur={() => (index === filteredContextMenuActions.length - 1 || index === 1) && setFocusedIndex(-1)} + /> + ); + })} + + ) ); }