Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Improve dialogs a11y (#3094)

* Improve a11y on ios

* Format

* Remove android

* Fix android

authored by

Eric Bailey and committed by
GitHub
6c9d6f5b ebd279ed

+91 -54
+45 -38
src/components/Dialog/index.tsx
··· 15 15 import {Portal} from '#/components/Portal' 16 16 import {createInput} from '#/components/forms/TextField' 17 17 import {logger} from '#/logger' 18 - import {useDialogStateContext} from '#/state/dialogs' 18 + import {useDialogStateControlContext} from '#/state/dialogs' 19 19 20 20 import { 21 21 DialogOuterProps, ··· 82 82 const hasSnapPoints = !!sheetOptions.snapPoints 83 83 const insets = useSafeAreaInsets() 84 84 const closeCallback = React.useRef<() => void>() 85 - const {openDialogs} = useDialogStateContext() 85 + const {setDialogIsOpen} = useDialogStateControlContext() 86 86 87 87 /* 88 88 * Used to manage open/closed, but index is otherwise handled internally by `BottomSheet` ··· 96 96 97 97 const open = React.useCallback<DialogControlProps['open']>( 98 98 ({index} = {}) => { 99 - openDialogs.current.add(control.id) 99 + setDialogIsOpen(control.id, true) 100 100 // can be set to any index of `snapPoints`, but `0` is the first i.e. "open" 101 101 setOpenIndex(index || 0) 102 102 }, 103 - [setOpenIndex, openDialogs, control.id], 103 + [setOpenIndex, setDialogIsOpen, control.id], 104 104 ) 105 105 106 106 const close = React.useCallback<DialogControlProps['close']>(cb => { ··· 133 133 closeCallback.current = undefined 134 134 } 135 135 136 - openDialogs.current.delete(control.id) 136 + setDialogIsOpen(control.id, false) 137 137 onClose?.() 138 138 setOpenIndex(-1) 139 139 } 140 140 }, 141 - [onClose, setOpenIndex, openDialogs, control.id], 141 + [onClose, setOpenIndex, setDialogIsOpen, control.id], 142 142 ) 143 143 144 144 const context = React.useMemo(() => ({close}), [close]) ··· 146 146 return ( 147 147 isOpen && ( 148 148 <Portal> 149 - <BottomSheet 150 - enableDynamicSizing={!hasSnapPoints} 151 - enablePanDownToClose 152 - keyboardBehavior="interactive" 153 - android_keyboardInputMode="adjustResize" 154 - keyboardBlurBehavior="restore" 155 - topInset={insets.top} 156 - {...sheetOptions} 157 - snapPoints={sheetOptions.snapPoints || ['100%']} 158 - ref={sheet} 159 - index={openIndex} 160 - backgroundStyle={{backgroundColor: 'transparent'}} 161 - backdropComponent={Backdrop} 162 - handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} 163 - handleStyle={{display: 'none'}} 164 - onChange={onChange}> 165 - <Context.Provider value={context}> 166 - <View 167 - style={[ 168 - a.absolute, 169 - a.inset_0, 170 - t.atoms.bg, 171 - { 172 - borderTopLeftRadius: 40, 173 - borderTopRightRadius: 40, 174 - height: Dimensions.get('window').height * 2, 175 - }, 176 - ]} 177 - /> 178 - {children} 179 - </Context.Provider> 180 - </BottomSheet> 149 + <View 150 + // iOS 151 + accessibilityViewIsModal 152 + // Android 153 + importantForAccessibility="yes" 154 + style={[a.absolute, a.inset_0]}> 155 + <BottomSheet 156 + enableDynamicSizing={!hasSnapPoints} 157 + enablePanDownToClose 158 + keyboardBehavior="interactive" 159 + android_keyboardInputMode="adjustResize" 160 + keyboardBlurBehavior="restore" 161 + topInset={insets.top} 162 + {...sheetOptions} 163 + snapPoints={sheetOptions.snapPoints || ['100%']} 164 + ref={sheet} 165 + index={openIndex} 166 + backgroundStyle={{backgroundColor: 'transparent'}} 167 + backdropComponent={Backdrop} 168 + handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} 169 + handleStyle={{display: 'none'}} 170 + onChange={onChange}> 171 + <Context.Provider value={context}> 172 + <View 173 + style={[ 174 + a.absolute, 175 + a.inset_0, 176 + t.atoms.bg, 177 + { 178 + borderTopLeftRadius: 40, 179 + borderTopRightRadius: 40, 180 + height: Dimensions.get('window').height * 2, 181 + }, 182 + ]} 183 + /> 184 + {children} 185 + </Context.Provider> 186 + </BottomSheet> 187 + </View> 181 188 </Portal> 182 189 ) 183 190 )
+6 -6
src/components/Dialog/index.web.tsx
··· 12 12 import {Context} from '#/components/Dialog/context' 13 13 import {Button, ButtonIcon} from '#/components/Button' 14 14 import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' 15 - import {useDialogStateContext} from '#/state/dialogs' 15 + import {useDialogStateControlContext} from '#/state/dialogs' 16 16 17 17 export {useDialogControl, useDialogContext} from '#/components/Dialog/context' 18 18 export * from '#/components/Dialog/types' ··· 30 30 const {gtMobile} = useBreakpoints() 31 31 const [isOpen, setIsOpen] = React.useState(false) 32 32 const [isVisible, setIsVisible] = React.useState(true) 33 - const {openDialogs} = useDialogStateContext() 33 + const {setDialogIsOpen} = useDialogStateControlContext() 34 34 35 35 const open = React.useCallback(() => { 36 36 setIsOpen(true) 37 - openDialogs.current.add(control.id) 38 - }, [setIsOpen, openDialogs, control.id]) 37 + setDialogIsOpen(control.id, true) 38 + }, [setIsOpen, setDialogIsOpen, control.id]) 39 39 40 40 const close = React.useCallback(async () => { 41 41 setIsVisible(false) 42 42 await new Promise(resolve => setTimeout(resolve, 150)) 43 43 setIsOpen(false) 44 44 setIsVisible(true) 45 - openDialogs.current.delete(control.id) 45 + setDialogIsOpen(control.id, false) 46 46 onClose?.() 47 - }, [onClose, setIsOpen, openDialogs, control.id]) 47 + }, [onClose, setIsOpen, setDialogIsOpen, control.id]) 48 48 49 49 useImperativeHandle( 50 50 control.ref,
+25 -9
src/state/dialogs/index.tsx
··· 13 13 * The currently open dialogs, referenced by their IDs, generated from 14 14 * `useId`. 15 15 */ 16 - openDialogs: React.MutableRefObject<Set<string>> 16 + openDialogs: string[] 17 17 }>({ 18 18 activeDialogs: { 19 19 current: new Map(), 20 20 }, 21 - openDialogs: { 22 - current: new Set(), 23 - }, 21 + openDialogs: [], 24 22 }) 25 23 26 24 const DialogControlContext = React.createContext<{ 27 25 closeAllDialogs(): boolean 26 + setDialogIsOpen(id: string, isOpen: boolean): void 28 27 }>({ 29 28 closeAllDialogs: () => false, 29 + setDialogIsOpen: () => {}, 30 30 }) 31 31 32 32 export function useDialogStateContext() { ··· 41 41 const activeDialogs = React.useRef< 42 42 Map<string, React.MutableRefObject<DialogControlRefProps>> 43 43 >(new Map()) 44 - const openDialogs = React.useRef<Set<string>>(new Set()) 44 + const [openDialogs, setOpenDialogs] = React.useState<string[]>([]) 45 45 46 46 const closeAllDialogs = React.useCallback(() => { 47 47 activeDialogs.current.forEach(dialog => dialog.current.close()) 48 - return openDialogs.current.size > 0 49 - }, []) 48 + return openDialogs.length > 0 49 + }, [openDialogs]) 50 50 51 - const context = React.useMemo(() => ({activeDialogs, openDialogs}), []) 52 - const controls = React.useMemo(() => ({closeAllDialogs}), [closeAllDialogs]) 51 + const setDialogIsOpen = React.useCallback( 52 + (id: string, isOpen: boolean) => { 53 + setOpenDialogs(prev => { 54 + const filtered = prev.filter(dialogId => dialogId !== id) as string[] 55 + return isOpen ? [...filtered, id] : filtered 56 + }) 57 + }, 58 + [setOpenDialogs], 59 + ) 60 + 61 + const context = React.useMemo( 62 + () => ({activeDialogs, openDialogs}), 63 + [openDialogs], 64 + ) 65 + const controls = React.useMemo( 66 + () => ({closeAllDialogs, setDialogIsOpen}), 67 + [closeAllDialogs, setDialogIsOpen], 68 + ) 53 69 54 70 return ( 55 71 <DialogContext.Provider value={context}>
+15 -1
src/view/shell/index.tsx
··· 30 30 import * as notifications from 'lib/notifications/notifications' 31 31 import {Outlet as PortalOutlet} from '#/components/Portal' 32 32 import {MutedWordsDialog} from '#/components/dialogs/MutedWords' 33 + import {useDialogStateContext} from '#/state/dialogs' 33 34 34 35 function ShellInner() { 35 36 const isDrawerOpen = useIsDrawerOpen() ··· 55 56 const closeAnyActiveElement = useCloseAnyActiveElement() 56 57 // start undefined 57 58 const currentAccountDid = React.useRef<string | undefined>(undefined) 59 + const {openDialogs} = useDialogStateContext() 58 60 59 61 React.useEffect(() => { 60 62 let listener = {remove() {}} ··· 78 80 } 79 81 }, [currentAccount]) 80 82 83 + /** 84 + * The counterpart to `accessibilityViewIsModal` for Android. This property 85 + * applies to the parent of all non-modal views, and prevents TalkBack from 86 + * navigating within content beneath an open dialog. 87 + * 88 + * @see https://reactnative.dev/docs/accessibility#importantforaccessibility-android 89 + */ 90 + const importantForAccessibility = 91 + openDialogs.length > 0 ? 'no-hide-descendants' : undefined 92 + 81 93 return ( 82 94 <> 83 - <View style={containerPadding}> 95 + <View 96 + style={containerPadding} 97 + importantForAccessibility={importantForAccessibility}> 84 98 <ErrorBoundary> 85 99 <Drawer 86 100 renderDrawerContent={renderDrawerContent}