Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ function TopLevelNavigationTabBar({state}: TopLevelNavigationTabBarProps) {
!shouldUseNarrowLayout ? styles.borderRight : {},
shouldDisplayLHB ? StyleUtils.positioning.l0 : StyleUtils.positioning.b0,
]}
accessibilityElementsHidden={!isReadyToDisplayBottomBar}
aria-hidden={!isReadyToDisplayBottomBar}
>
{/* We are not rendering NavigationTabBar conditionally for two reasons
1. It's faster to hide/show it than mount a new when needed.
Expand Down
11 changes: 8 additions & 3 deletions src/components/ScreenWrapper/ScreenWrapperContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import KeyboardAvoidingView from '@components/KeyboardAvoidingView';
import ModalContext from '@components/Modal/ModalContext';
import useBottomSafeSafeAreaPaddingStyle from '@hooks/useBottomSafeSafeAreaPaddingStyle';
import useInitialDimensions from '@hooks/useInitialWindowDimensions';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useTackInputFocus from '@hooks/useTackInputFocus';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import {isMobile, isMobileWebKit, isSafari} from '@libs/Browser';
import type {ForwardedFSClassProps} from '@libs/Fullstory/types';
import getPlatform from '@libs/getPlatform';
import addViewportResizeListener from '@libs/VisualViewport';
import toggleTestToolsModal from '@userActions/TestTool';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -108,8 +110,8 @@ function ScreenWrapperContainer({
includePaddingTop = true,
includeSafeAreaPaddingBottom = false,
isFocused = true,
ref,
forwardedFSClass,
ref,
}: ScreenWrapperContainerProps) {
const {windowHeight} = useWindowDimensions(shouldUseCachedViewportHeight);
const {initialHeight} = useInitialDimensions();
Expand All @@ -119,6 +121,8 @@ function ScreenWrapperContainer({
const {isBlurred} = useInputBlurState();
const {setIsBlurred} = useInputBlurActions();
const isAvoidingViewportScroll = useTackInputFocus(isFocused && shouldEnableMaxHeight && shouldAvoidScrollOnVirtualViewport && isMobileWebKit());
const {shouldUseNarrowLayout} = useResponsiveLayout();
const shouldHideFromAccessibility = shouldUseNarrowLayout && getPlatform() === CONST.PLATFORM.WEB && isMobile() && !isFocused;

const isUsingEdgeToEdgeMode = enableEdgeToEdgeBottomSafeAreaPadding !== undefined;
const shouldKeyboardOffsetBottomSafeAreaPadding = shouldKeyboardOffsetBottomSafeAreaPaddingProp ?? isUsingEdgeToEdgeMode;
Expand Down Expand Up @@ -210,6 +214,9 @@ function ScreenWrapperContainer({
{...panResponder.panHandlers}
testID={testID}
fsClass={forwardedFSClass}
tabIndex={-1}
accessibilityElementsHidden={shouldHideFromAccessibility}
aria-hidden={shouldHideFromAccessibility}
>
<View
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
Expand Down Expand Up @@ -239,7 +246,5 @@ function ScreenWrapperContainer({
);
}

ScreenWrapperContainer.displayName = 'ScreenWrapperContainer';

export default React.memo(ScreenWrapperContainer);
export type {ScreenWrapperContainerProps};
53 changes: 50 additions & 3 deletions src/components/ScreenWrapper/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {useFocusEffect, useIsFocused, useNavigation, usePreventRemove} from '@react-navigation/native';
import {isSingleNewDotEntrySelector} from '@selectors/HybridApp';
import type {ReactNode} from 'react';
import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react';
import type {StyleProp, ViewStyle} from 'react-native';
import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
import type {StyleProp, View, ViewStyle} from 'react-native';
import {DeviceEventEmitter, Keyboard} from 'react-native';
import type {EdgeInsets} from 'react-native-safe-area-context';
import CustomDevMenu from '@components/CustomDevMenu';
Expand All @@ -16,7 +16,11 @@ import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings';
import useThemeStyles from '@hooks/useThemeStyles';
import Accessibility from '@libs/Accessibility';
import {isMobile} from '@libs/Browser';
import type {ForwardedFSClassProps} from '@libs/Fullstory/types';
import getPlatform from '@libs/getPlatform';
import mergeRefs from '@libs/mergeRefs';
import NarrowPaneContext from '@libs/Navigation/AppNavigator/Navigators/NarrowPaneContext';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
Expand Down Expand Up @@ -104,10 +108,13 @@ function ScreenWrapper({
const navigationFallback = useNavigation<PlatformStackNavigationProp<RootNavigatorParamList>>();
const navigation = navigationProp ?? navigationFallback;
const isFocused = useIsFocused();
const screenWrapperRef = useRef<View>(null);
const mergedScreenWrapperRef = mergeRefs(screenWrapperRef, ref);

// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout for a case where we want to show the offline indicator only on small screens
// eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
const {isSmallScreenWidth} = useResponsiveLayout();
const shouldMoveAccessibilityFocus = getPlatform() === CONST.PLATFORM.WEB && isMobile();

const styles = useThemeStyles();
const {isDevelopment} = useEnvironment();
Expand Down Expand Up @@ -220,6 +227,46 @@ function ScreenWrapper({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
if (!shouldMoveAccessibilityFocus || !didScreenTransitionEnd || !isFocused) {
return;
}

if (typeof document === 'undefined') {
return;
}

const element = screenWrapperRef.current as unknown as HTMLElement | null;
if (!element) {
return;
}

const activeElement = document.activeElement as HTMLElement | null;
if (activeElement && element.contains(activeElement)) {
return;
}

const focusTargets = element.querySelectorAll<HTMLElement>('button, [href], [role="button"], [role="link"], [tabindex]:not([tabindex="-1"])');
for (const focusTarget of focusTargets) {
const isDisabledTarget = focusTarget.matches(':disabled') || focusTarget.getAttribute('aria-disabled')?.toLowerCase() === 'true';
if (isDisabledTarget || focusTarget.getAttribute('aria-hidden') === 'true') {
continue;
}

if (focusTarget === activeElement) {
return;
}

focusTarget.focus();
const focusedElement = document.activeElement as HTMLElement | null;
if (focusedElement === focusTarget || (focusedElement && focusTarget.contains(focusedElement))) {
return;
}
}

Accessibility.moveAccessibilityFocus(screenWrapperRef as unknown as Parameters<typeof Accessibility.moveAccessibilityFocus>[0]);
}, [didScreenTransitionEnd, isFocused, shouldMoveAccessibilityFocus]);

useFocusEffect(
useCallback(() => {
// On iOS, the transitionEnd event doesn't trigger some times. As such, we need to set a timeout
Expand Down Expand Up @@ -255,7 +302,7 @@ function ScreenWrapper({
return (
<FocusTrapForScreen focusTrapSettings={focusTrapSettings}>
<ScreenWrapperContainer
ref={ref}
ref={mergedScreenWrapperRef}
style={[styles.flex1, style]}
bottomContent={bottomContent}
didScreenTransitionEnd={didScreenTransitionEnd}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,12 @@ function createModalStackNavigator<ParamList extends ParamListBase>(screens: Scr

return (
// This container is necessary to hide card translation during transition. Without it the user would see un-clipped cards.
<View style={[styles.modalStackNavigatorContainer, styles.modalStackNavigatorContainerWidth(isSmallScreenWidth)]}>
<View
style={[styles.modalStackNavigatorContainer, styles.modalStackNavigatorContainerWidth(isSmallScreenWidth)]}
accessibilityViewIsModal={isSmallScreenWidth}
aria-modal={isSmallScreenWidth || undefined}
role={isSmallScreenWidth ? 'dialog' : undefined}
>
<ModalStackNavigator.Navigator>
{Object.keys(screens as Required<Screens>).map((name) => (
<ModalStackNavigator.Screen
Expand Down
10 changes: 5 additions & 5 deletions tests/ui/SearchPageTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ describe('SearchPageNarrow', () => {
expect(screen.getByTestId('SearchPageNarrow')).toBeTruthy();

// Initially, there are two NavigationTabBars on screen: one from TopLevelNavigationTabBar and one from SearchPageNarrow.
let navigationTabBars = screen.getAllByTestId('NavigationTabBar');
let navigationTabBars = screen.getAllByTestId('NavigationTabBar', {includeHiddenElements: true});
expect(navigationTabBars).toHaveLength(2);

const searchAutocompleteInput = await screen.findByTestId('search-autocomplete-text-input');
Expand All @@ -126,12 +126,12 @@ describe('SearchPageNarrow', () => {
});

await waitFor(() => {
navigationTabBars = screen.getAllByTestId('NavigationTabBar');
navigationTabBars = screen.getAllByTestId('NavigationTabBar', {includeHiddenElements: true});
expect(navigationTabBars).toHaveLength(1);
});

await waitFor(() => {
const topLevelNavigationTabBar = screen.getByTestId('TopLevelNavigationTabBar');
const topLevelNavigationTabBar = screen.getByTestId('TopLevelNavigationTabBar', {includeHiddenElements: true});
expect(topLevelNavigationTabBar).toHaveStyle({pointerEvents: 'none', opacity: 0});
});

Expand All @@ -142,12 +142,12 @@ describe('SearchPageNarrow', () => {
});

await waitFor(() => {
navigationTabBars = screen.getAllByTestId('NavigationTabBar');
navigationTabBars = screen.getAllByTestId('NavigationTabBar', {includeHiddenElements: true});
expect(navigationTabBars).toHaveLength(2);
});

await waitFor(() => {
const topLevelNavigationTabBar = screen.getByTestId('TopLevelNavigationTabBar');
const topLevelNavigationTabBar = screen.getByTestId('TopLevelNavigationTabBar', {includeHiddenElements: true});
expect(topLevelNavigationTabBar).toHaveStyle({pointerEvents: 'auto', opacity: 1});
});
});
Expand Down
Loading