Skip to content

Commit 5d9c146

Browse files
authored
Merge pull request Expensify#81536 from marufsharifi/fix/a11y-bottomsheet-close-screen-reader
Fix Screen Reader issue preventing bottom sheet from closing without selection
2 parents 84b6a5f + 89e5105 commit 5d9c146

3 files changed

Lines changed: 36 additions & 4 deletions

File tree

src/CONST/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8842,6 +8842,9 @@ const CONST = {
88428842
SEND_BUTTON: 'AttachmentModal-SendButton',
88438843
IMAGE_ZOOM: 'AttachmentModal-ImageZoom',
88448844
},
8845+
MODAL: {
8846+
DISMISS_DIALOG: 'Modal-DismissDialog',
8847+
},
88458848
ATTACHMENT_PREVIEW: {
88468849
VIDEO_THUMBNAIL: 'AttachmentPreview-VideoThumbnail',
88478850
IMAGE_THUMBNAIL: 'AttachmentPreview-ImageThumbnail',

src/components/Modal/BaseModal.tsx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
2-
import type {LayoutChangeEvent} from 'react-native';
2+
import type {GestureResponderEvent, LayoutChangeEvent} from 'react-native';
33
// Animated required for side panel navigation
44
// eslint-disable-next-line no-restricted-imports
55
import {Animated, View} from 'react-native';
66
import ColorSchemeWrapper from '@components/ColorSchemeWrapper';
77
import NavigationBar from '@components/NavigationBar';
8+
import {PressableWithoutFeedback} from '@components/Pressable';
89
import ScreenWrapperOfflineIndicatorContext from '@components/ScreenWrapper/ScreenWrapperOfflineIndicatorContext';
910
import ScrollView from '@components/ScrollView';
1011
import useKeyboardState from '@hooks/useKeyboardState';
12+
import useLocalize from '@hooks/useLocalize';
1113
import usePrevious from '@hooks/usePrevious';
1214
import useResponsiveLayout from '@hooks/useResponsiveLayout';
1315
import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
@@ -78,6 +80,7 @@ function BaseModal({
7880
const theme = useTheme();
7981
const styles = useThemeStyles();
8082
const StyleUtils = useStyleUtils();
83+
const {translate} = useLocalize();
8184
const {windowWidth, windowHeight} = useWindowDimensions();
8285
// We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply correct modal width
8386
const canUseTouchScreen = canUseTouchScreenCheck();
@@ -95,6 +98,7 @@ function BaseModal({
9598

9699
const shouldCallHideModalOnUnmount = useRef(false);
97100
const hideModalCallbackRef = useRef<(callHideCallback: boolean) => void>(undefined);
101+
const bottomDockedDismissButtonRef = useRef<View>(null);
98102

99103
const wasVisible = usePrevious(isVisible);
100104

@@ -177,8 +181,8 @@ function BaseModal({
177181
onModalShow();
178182
}, [onModalShow, shouldSetModalVisibility, type]);
179183

180-
const handleBackdropPress = (e?: KeyboardEvent) => {
181-
if (e?.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) {
184+
const handleBackdropPress = (e?: GestureResponderEvent | KeyboardEvent) => {
185+
if (e && 'key' in e && e.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) {
182186
return;
183187
}
184188

@@ -311,6 +315,8 @@ function BaseModal({
311315
const isBottomDockedModalInLandscapeMode = type === CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED && isInLandscapeMode;
312316

313317
const shouldWrapChildrenInScrollView = shouldWrapModalChildrenInScrollViewIfBottomDockedInLandscapeMode && isBottomDockedModalInLandscapeMode;
318+
const shouldShowBottomDockedDismissButton = isSmallScreenWidth && type === CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED && !!(onBackdropPress ?? onClose);
319+
const modalInitialFocus = shouldShowBottomDockedDismissButton ? () => bottomDockedDismissButtonRef.current : initialFocus;
314320

315321
return (
316322
<ModalContext.Provider value={modalContextValue}>
@@ -348,7 +354,7 @@ function BaseModal({
348354
onSwipeComplete={onClose}
349355
swipeDirection={swipeDirection}
350356
shouldPreventScrollOnFocus={shouldPreventScrollOnFocus}
351-
initialFocus={initialFocus}
357+
initialFocus={modalInitialFocus}
352358
swipeThreshold={swipeThreshold}
353359
isVisible={isVisible}
354360
backdropColor={theme.overlay}
@@ -389,6 +395,19 @@ function BaseModal({
389395
ref={ref}
390396
fsClass={forwardedFSClass}
391397
>
398+
{shouldShowBottomDockedDismissButton && (
399+
<PressableWithoutFeedback
400+
ref={bottomDockedDismissButtonRef}
401+
onPress={handleBackdropPress}
402+
role={CONST.ROLE.BUTTON}
403+
accessibilityRole={CONST.ROLE.BUTTON}
404+
accessibilityLabel={translate('common.dismiss')}
405+
sentryLabel={CONST.SENTRY_LABEL.MODAL.DISMISS_DIALOG}
406+
style={styles.bottomDockedModalDismissButton}
407+
>
408+
<View />
409+
</PressableWithoutFeedback>
410+
)}
392411
<ColorSchemeWrapper>{shouldWrapChildrenInScrollView ? <ScrollView>{children}</ScrollView> : children}</ColorSchemeWrapper>
393412
</Animated.View>
394413
{!keyboardStateContextValue?.isKeyboardActive && <NavigationBar />}

src/styles/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3646,6 +3646,16 @@ const staticStyles = (theme: ThemeColors) =>
36463646
backgroundColor: theme.overlay,
36473647
},
36483648

3649+
bottomDockedModalDismissButton: {
3650+
position: 'absolute',
3651+
top: 0,
3652+
left: 0,
3653+
right: 0,
3654+
height: variables.iconSizeXSmall,
3655+
backgroundColor: theme.transparent,
3656+
zIndex: 1,
3657+
},
3658+
36493659
invisibleOverlay: {
36503660
backgroundColor: theme.transparent,
36513661
zIndex: 1000,

0 commit comments

Comments
 (0)