Skip to content

Commit 769c28d

Browse files
authored
fix: hold keyboard focus on the rooms list for hardware-keyboard navigation (#7467)
1 parent 20720c6 commit 769c28d

2 files changed

Lines changed: 50 additions & 29 deletions

File tree

app/containers/Header/components/HeaderButton/Common.tsx

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { memo } from 'react';
1+
import { forwardRef, memo } from 'react';
22
import { StackActions, useNavigation } from '@react-navigation/native';
33
import { type StyleProp, type ViewStyle } from 'react-native';
4-
import { withKeyboardFocus } from 'react-native-external-keyboard';
4+
import { type KeyboardFocus, withKeyboardFocus } from 'react-native-external-keyboard';
55

66
import I18n from '../../../../i18n';
77
import { isIOS } from '../../../../lib/methods/helpers/deviceInfo';
@@ -17,33 +17,32 @@ interface IHeaderButtonCommon extends IHeaderButtonItem {
1717
}
1818

1919
// Left
20-
export const Drawer = ({
21-
navigation,
22-
testID,
23-
style = {},
24-
onPress = () => navigation?.toggleDrawer(),
25-
...props
26-
}: IHeaderButtonCommon) => {
27-
const { colors } = useTheme();
20+
export const Drawer = forwardRef<KeyboardFocus, IHeaderButtonCommon>(
21+
({ navigation, testID, style = {}, onPress = () => navigation?.toggleDrawer(), ...props }, ref) => {
22+
const { colors } = useTheme();
2823

29-
const item = (
30-
<ItemChildren
31-
autoFocus
32-
accessibilityLabel={I18n.t('Menu')}
33-
iconName='hamburguer'
34-
onPress={onPress}
35-
testID={testID}
36-
color={colors.fontDefault}
37-
{...props}
38-
/>
39-
);
24+
const item = (
25+
<ItemChildren
26+
ref={ref}
27+
autoFocus
28+
accessibilityLabel={I18n.t('Menu')}
29+
iconName='hamburguer'
30+
onPress={onPress}
31+
testID={testID}
32+
color={colors.fontDefault}
33+
{...props}
34+
/>
35+
);
4036

41-
return (
42-
<Container style={style} left>
43-
{item}
44-
</Container>
45-
);
46-
};
37+
return (
38+
<Container style={style} left>
39+
{item}
40+
</Container>
41+
);
42+
}
43+
);
44+
45+
Drawer.displayName = 'HeaderButton.Drawer';
4746

4847
export const CloseModal = memo(({ testID, onPress, ...props }: IHeaderButtonCommon) => {
4948
const { dispatch } = useNavigation();

app/views/RoomsListView/hooks/useHeader.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
import { useNavigation } from '@react-navigation/native';
2-
import { useCallback, useContext, useLayoutEffect, useState } from 'react';
1+
import { useFocusEffect, useNavigation } from '@react-navigation/native';
2+
import { useCallback, useContext, useLayoutEffect, useRef, useState } from 'react';
3+
import { InteractionManager } from 'react-native';
4+
import { type KeyboardFocus } from 'react-native-external-keyboard';
35

46
import * as HeaderButton from '../../../containers/Header/components/HeaderButton';
57
import i18n from '../../../i18n';
68
import { useAppSelector } from '../../../lib/hooks/useAppSelector';
9+
import { useIsAccessibilityNavigationEnabled } from '../../../lib/hooks/useIsAccessibilityNavigationEnabled';
710
import { useMasterDetail } from '../../../lib/hooks/useMasterDetail';
811
import { usePermissions } from '../../../lib/hooks/usePermissions';
912
import { isTablet } from '../../../lib/methods/helpers';
@@ -18,6 +21,8 @@ export const useHeader = () => {
1821

1922
const { searchEnabled, search, startSearch, stopSearch } = useContext(RoomsSearchContext);
2023
const [options, setOptions] = useState<any>(null);
24+
const isAccessibilityNavigationEnabled = useIsAccessibilityNavigationEnabled();
25+
const drawerButtonRef = useRef<KeyboardFocus>(null);
2126
const supportedVersionsStatus = useAppSelector(state => state.supportedVersions.status);
2227
const requirePasswordChange = useAppSelector(state => getUserSelector(state).requirePasswordChange);
2328
const isMasterDetail = useMasterDetail();
@@ -101,6 +106,7 @@ export const useHeader = () => {
101106
const options = {
102107
headerLeft: () => (
103108
<HeaderButton.Drawer
109+
ref={drawerButtonRef}
104110
navigation={navigation}
105111
testID='rooms-list-view-sidebar'
106112
onPress={
@@ -171,5 +177,21 @@ export const useHeader = () => {
171177
search
172178
]);
173179

180+
// The rooms list header persists across native-stack navigation, so autoFocus (mount-only)
181+
// won't re-fire on back-return or after the list/banner render asynchronously. Re-assert focus
182+
// on the drawer button every time the screen is focused so external-keyboard/screen-reader
183+
// navigation always starts from a known element. Regular touch users are left untouched.
184+
useFocusEffect(
185+
useCallback(() => {
186+
if (!isAccessibilityNavigationEnabled) {
187+
return;
188+
}
189+
const task = InteractionManager.runAfterInteractions(() => {
190+
drawerButtonRef.current?.focus();
191+
});
192+
return () => task.cancel();
193+
}, [isAccessibilityNavigationEnabled])
194+
);
195+
174196
return { options };
175197
};

0 commit comments

Comments
 (0)