Skip to content

Commit 654ef3c

Browse files
authored
Merge pull request Expensify#79826 from TaduJR/fix-Screen-Reader-Many-Pages-The-embedded-links-cannot-be-focused-and-activated
fix: Screen Reader: Many Pages: The embedded links cannot be focused and activated
2 parents d91fdbc + 948a870 commit 654ef3c

4 files changed

Lines changed: 66 additions & 1 deletion

File tree

src/components/HTMLEngineProvider/HTMLRenderers/AccountManagerLinkRenderer.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-ht
44
import {TNodeChildrenRenderer} from 'react-native-render-html';
55
import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils';
66
import Text from '@components/Text';
7+
import useEnterKeyHandler from '@hooks/useEnterKeyHandler';
78
import useOnyx from '@hooks/useOnyx';
89
import useThemeStyles from '@hooks/useThemeStyles';
910
import Navigation from '@libs/Navigation/Navigation';
11+
import CONST from '@src/CONST';
1012
import ONYXKEYS from '@src/ONYXKEYS';
1113
import ROUTES from '@src/ROUTES';
1214

@@ -35,11 +37,16 @@ function AccountManagerLinkRenderer({tnode, style}: AccountManagerLinkRendererPr
3537
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(accountManagerReportID));
3638
}, [accountManagerReportID]);
3739

40+
const handleKeyDown = useEnterKeyHandler(navigateToAccountManager);
41+
3842
return (
3943
<Text
4044
style={[style as TextStyle, linkStyle]}
4145
onPress={navigateToAccountManager}
46+
onKeyDown={handleKeyDown}
4247
suppressHighlighting
48+
role={CONST.ROLE.LINK}
49+
tabIndex={0}
4350
>
4451
<TNodeChildrenRenderer tnode={tnode} />
4552
</Text>

src/components/HTMLEngineProvider/HTMLRenderers/ConciergeLinkRenderer.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-ht
44
import {TNodeChildrenRenderer} from 'react-native-render-html';
55
import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils';
66
import Text from '@components/Text';
7+
import useEnterKeyHandler from '@hooks/useEnterKeyHandler';
78
import useOnyx from '@hooks/useOnyx';
89
import useThemeStyles from '@hooks/useThemeStyles';
910
import {navigateToConciergeChat as navigateToConciergeChatAction} from '@userActions/Report';
11+
import CONST from '@src/CONST';
1012
import ONYXKEYS from '@src/ONYXKEYS';
1113

1214
type ConciergeLinkRendererProps = CustomRendererProps<TText | TPhrasing>;
@@ -35,11 +37,16 @@ function ConciergeLinkRenderer({tnode, style}: ConciergeLinkRendererProps) {
3537
];
3638
}
3739

40+
const handleKeyDown = useEnterKeyHandler(navigateToConciergeChat);
41+
3842
return (
3943
<Text
4044
style={[style as TextStyle, linkStyle]}
4145
onPress={navigateToConciergeChat}
46+
onKeyDown={handleKeyDown}
4247
suppressHighlighting
48+
role={CONST.ROLE.LINK}
49+
tabIndex={0}
4350
>
4451
<TNodeChildrenRenderer tnode={tnode} />
4552
</Text>

src/components/Pressable/GenericPressable/implementation/BaseGenericPressable.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,29 @@ function GenericPressable({
149149
return KeyboardShortcut.subscribe(shortcutKey, onKeyboardShortcutPressHandler, descriptionKey, modifiers, true, false, 0, false);
150150
}, [keyboardShortcut, onKeyboardShortcutPressHandler]);
151151

152+
const isRoleLink = rest.role === CONST.ROLE.LINK;
153+
154+
/**
155+
* Handles keyboard events for the pressable element.
156+
* If a custom onKeyDown handler is provided, it delegates to that handler.
157+
* Otherwise, for elements with role="link", it triggers onPress when Enter is pressed
158+
* to comply with W3C APG Link Pattern (https://www.w3.org/WAI/ARIA/apg/patterns/link/).
159+
*/
160+
const handleKeyDown = useCallback(
161+
(event: React.KeyboardEvent) => {
162+
if (onKeyDown) {
163+
onKeyDown(event as unknown as React.KeyboardEvent<Element>);
164+
return;
165+
}
166+
167+
if (isRoleLink && event.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) {
168+
event.preventDefault();
169+
onPressHandler(event.nativeEvent as unknown as KeyboardEvent);
170+
}
171+
},
172+
[onKeyDown, isRoleLink, onPressHandler],
173+
);
174+
152175
return (
153176
<Pressable
154177
hitSlop={shouldUseAutoHitSlop ? hitSlop : undefined}
@@ -157,7 +180,7 @@ function GenericPressable({
157180
disabled={fullDisabled}
158181
onPress={!isDisabled ? singleExecution(onPressHandler) : undefined}
159182
onLongPress={!isDisabled && onLongPress ? onLongPressHandler : undefined}
160-
onKeyDown={!isDisabled ? onKeyDown : undefined}
183+
onKeyDown={!isDisabled ? handleKeyDown : undefined}
161184
onPressIn={!isDisabled ? onPressIn : undefined}
162185
onPressOut={!isDisabled ? onPressOut : undefined}
163186
dataSet={{...(isRoleButton ? {[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true} : {}), ...(dataSet ?? {})}}

src/hooks/useEnterKeyHandler.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type {KeyboardEvent} from 'react';
2+
import {useCallback} from 'react';
3+
import CONST from '@src/CONST';
4+
5+
/**
6+
* Custom hook that returns a keyboard event handler which triggers a callback
7+
* when the Enter key is pressed.
8+
*
9+
* @param callback - The function to execute when Enter key is pressed
10+
* @returns A keyboard event handler function
11+
*
12+
* @example
13+
* const handleKeyDown = useEnterKeyHandler(() => navigation.navigate('Home'));
14+
*/
15+
function useEnterKeyHandler(callback: () => void) {
16+
return useCallback(
17+
(event: KeyboardEvent) => {
18+
if (event.key !== CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) {
19+
return;
20+
}
21+
event.preventDefault();
22+
callback();
23+
},
24+
[callback],
25+
);
26+
}
27+
28+
export default useEnterKeyHandler;

0 commit comments

Comments
 (0)