Skip to content

Commit

Permalink
chore: update code comments
Browse files Browse the repository at this point in the history
  • Loading branch information
dominictb committed Aug 12, 2024
1 parent fa5115a commit 61718f8
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ function FocusTrapContainerElement({onContainerElementChanged, ...props}: FocusT
} else if (r) {
r.current = node;
}
if (node) {
onContainerElementChanged?.(node as unknown as HTMLElement);
return () => onContainerElementChanged?.(null);
}
onContainerElementChanged?.(node as unknown as HTMLElement | null);
}}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
Expand Down
38 changes: 36 additions & 2 deletions src/libs/Navigation/OnyxTabNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,30 @@ type OnyxTabNavigatorProps = OnyxTabNavigatorOnyxProps &

screenListeners?: ScreenListeners<NavigationState, MaterialTopTabNavigationEventMap>;

/** Callback to register the focus trap container elements of the current active tab */
/** Callback to register the focus trap container elements of the current active tab.
* Use this in the parent component to get the focus trap container element of the active tab,
* then pass it to the ScreenWrapper so that only focusable elements of the active tab are included in the focus trap
* Check the `IOURequestStartPage.tsx` and `NewChatSelectorPage.tsx` components for example usage
*/
onActiveTabFocusTrapContainerElementChanged?: (containerElement: HTMLElement | null) => void;

/** Callback to register the focus trap container elements of the tab bar */
/** Callback to register the focus trap container elements of the tab bar.
* This callback is useful when the custom-rendered tab bar is supporting the focus trap container element registration (which is the case of `TabSelector.tsx` component).
* Together, with the `onActiveTabFocusTrapContainerElementChanged` callback, we can manage the focus trap of the tab navigator in the parent component.
*/
onTabBarFocusTrapContainerElementChanged?: (containerElement: HTMLElement | null) => void;
};

// eslint-disable-next-line rulesdir/no-inline-named-export
const TopTab = createMaterialTopTabNavigator();

// The TabFocusTrapContext is to collect the focus trap container element of each tab screen.
// This provider is placed in the OnyxTabNavigator component and the consumer is in the TabScreenWithFocusTrapWrapper component.
const TabFocusTrapContext = React.createContext<(tabName: string, containerElement: HTMLElement | null) => void>(() => {});

// This takes all the same props as MaterialTopTabsNavigator: https://reactnavigation.org/docs/material-top-tab-navigator/#props,
// except ID is now required, and it gets a `selectedTab` from Onyx
// It also takes 2 more optional callbacks to manage the focus trap container elements of the tab bar and the active tab
function OnyxTabNavigator({
id,
selectedTab,
Expand All @@ -59,8 +69,10 @@ function OnyxTabNavigator({
screenListeners,
...rest
}: OnyxTabNavigatorProps) {
// Mapping of tab name to focus trap container element
const [focusTrapContainerElementMapping, setFocusTrapContainerElementMapping] = useState<Record<string, HTMLElement>>({});

// This callback is used to register the focus trap container element of each avaiable tab screen
const setTabFocusTrapContainerElement = useCallback((tabName: string, containerElement: HTMLElement | null) => {
setFocusTrapContainerElementMapping((prevMapping) => {
const resultMapping = {...prevMapping};
Expand All @@ -73,6 +85,10 @@ function OnyxTabNavigator({
});
}, []);

/**
* This is a TabBar wrapper component that includes the focus trap container element callback.
* In `TabSelector.tsx` component, the callback prop to register focus trap container element is supported out of the box
*/
const TabBarWithFocusTrapInclusion = useCallback(
(props: TabSelectorProps) => {
return (
Expand All @@ -86,6 +102,7 @@ function OnyxTabNavigator({
[onTabBarFocusTrapContainerElementChanged, TabBar],
);

// If the selected tab changes, we need to update the focus trap container element of the active tab
useEffect(() => {
onActiveTabFocusTrapContainerElementChanged?.(selectedTab ? focusTrapContainerElementMapping[selectedTab] : null);
}, [selectedTab, focusTrapContainerElementMapping, onActiveTabFocusTrapContainerElementChanged]);
Expand Down Expand Up @@ -119,6 +136,23 @@ function OnyxTabNavigator({
);
}

/**
* We should use this wrapper for each tab screen. This will help register the focus trap container element of each tab screen.
* In the OnyxTabNavigator component, depending on the selected tab, we will further register the correct container element of the current active tab to the parent focus trap.
* This must be used if we want to include all tabbable elements of one tab screen in the parent focus trap if that tab screen is active.
* Example usage (check the `IOURequestStartPage.tsx` and `NewChatSelectorPage.tsx` components for more info)
* ```tsx
* <OnyxTabNavigator>
* <Tab.Screen>
* {() => (
* <TabScreenWithFocusTrapWrapper>
* <Content />
* </TabScreenWithFocusTrapWrapper>
* )}
* </Tab.Screen>
* </OnyxTabNavigator>
* ```
*/
function TabScreenWithFocusTrapWrapper({children}: {children?: React.ReactNode}) {
const route = useRoute();
const styles = useThemeStyles();
Expand Down
1 change: 1 addition & 0 deletions src/pages/NewChatPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ function NewChatPage({isGroupChat}: NewChatPageProps) {
includePaddingTop={false}
shouldEnablePickerAvoiding={false}
testID={NewChatPage.displayName}
// Disable the focus trap of this page to activate the parent focus trap in `NewChatSelectorPage`.
focusTrapSettings={{active: false}}
>
<KeyboardAvoidingView
Expand Down
18 changes: 10 additions & 8 deletions src/pages/NewChatSelectorPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {useNavigation} from '@react-navigation/native';
import React, {useCallback, useMemo, useState} from 'react';
import {View} from 'react-native';
import FocusTrapContainerElement from '@components/FocusTrap/FocusTrapContainerElement';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
Expand All @@ -16,10 +15,12 @@ function NewChatSelectorPage() {
const {translate} = useLocalize();
const styles = useThemeStyles();
const navigation = useNavigation();
// The focus trap container elements of the header and back button, tab bar, and active tab
const [headerWithBackBtnContainerElement, setHeaderWithBackButtonContainerElement] = useState<HTMLElement | null>(null);
const [tabBarContainerElement, setTabBarContainerElement] = useState<HTMLElement | null>(null);
const [activeTabContainerElement, setActiveTabContainerElement] = useState<HTMLElement | null>(null);

// Theoretically, the focus trap container element can be null (due to component unmount/remount), so we filter out the null elements
const containerElements = useMemo(() => {
return [headerWithBackBtnContainerElement, tabBarContainerElement, activeTabContainerElement].filter((element) => !!element) as HTMLElement[];
}, [headerWithBackBtnContainerElement, tabBarContainerElement, activeTabContainerElement]);
Expand All @@ -37,13 +38,14 @@ function NewChatSelectorPage() {
testID={NewChatSelectorPage.displayName}
focusTrapSettings={{containerElements}}
>
<FocusTrapContainerElement onContainerElementChanged={setHeaderWithBackButtonContainerElement}>
<View style={[styles.w100]}>
<HeaderWithBackButton
title={translate('sidebarScreen.fabNewChat')}
onBackButtonPress={navigation.goBack}
/>
</View>
<FocusTrapContainerElement
onContainerElementChanged={setHeaderWithBackButtonContainerElement}
style={[styles.w100]}
>
<HeaderWithBackButton
title={translate('sidebarScreen.fabNewChat')}
onBackButtonPress={navigation.goBack}
/>
</FocusTrapContainerElement>

<OnyxTabNavigator
Expand Down
2 changes: 1 addition & 1 deletion src/pages/iou/request/IOURequestStartPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ function IOURequestStartPage({
shouldEnableMinHeight={DeviceCapabilities.canUseTouchScreen()}
headerGapStyles={isDraggingOver ? [styles.receiptDropHeaderGap] : []}
testID={IOURequestStartPage.displayName}
focusTrapSettings={{containerElements: focusTrapContainerElements, focusTrapOptions: {preventScroll: true}}}
focusTrapSettings={{containerElements: focusTrapContainerElements}}
>
{({safeAreaPaddingBottomStyle}) => (
<DragAndDropProvider
Expand Down
1 change: 1 addition & 0 deletions src/pages/workspace/WorkspaceNewRoomPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ function WorkspaceNewRoomPage({policies, reports, formState, session, activePoli
includePaddingTop={false}
shouldEnablePickerAvoiding={false}
testID={WorkspaceNewRoomPage.displayName}
// Disable the focus trap of this page to activate the parent focus trap in `NewChatSelectorPage`.
focusTrapSettings={{active: false}}
>
{({insets}) =>
Expand Down

0 comments on commit 61718f8

Please sign in to comment.