Improve dialogs a11y (#3094)
* Improve a11y on ios * Format * Remove android * Fix androidzio/stable
parent
ebd279ed68
commit
6c9d6f5b05
|
@ -15,7 +15,7 @@ import {useTheme, atoms as a, flatten} from '#/alf'
|
||||||
import {Portal} from '#/components/Portal'
|
import {Portal} from '#/components/Portal'
|
||||||
import {createInput} from '#/components/forms/TextField'
|
import {createInput} from '#/components/forms/TextField'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useDialogStateContext} from '#/state/dialogs'
|
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DialogOuterProps,
|
DialogOuterProps,
|
||||||
|
@ -82,7 +82,7 @@ export function Outer({
|
||||||
const hasSnapPoints = !!sheetOptions.snapPoints
|
const hasSnapPoints = !!sheetOptions.snapPoints
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
const closeCallback = React.useRef<() => void>()
|
const closeCallback = React.useRef<() => void>()
|
||||||
const {openDialogs} = useDialogStateContext()
|
const {setDialogIsOpen} = useDialogStateControlContext()
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Used to manage open/closed, but index is otherwise handled internally by `BottomSheet`
|
* Used to manage open/closed, but index is otherwise handled internally by `BottomSheet`
|
||||||
|
@ -96,11 +96,11 @@ export function Outer({
|
||||||
|
|
||||||
const open = React.useCallback<DialogControlProps['open']>(
|
const open = React.useCallback<DialogControlProps['open']>(
|
||||||
({index} = {}) => {
|
({index} = {}) => {
|
||||||
openDialogs.current.add(control.id)
|
setDialogIsOpen(control.id, true)
|
||||||
// can be set to any index of `snapPoints`, but `0` is the first i.e. "open"
|
// can be set to any index of `snapPoints`, but `0` is the first i.e. "open"
|
||||||
setOpenIndex(index || 0)
|
setOpenIndex(index || 0)
|
||||||
},
|
},
|
||||||
[setOpenIndex, openDialogs, control.id],
|
[setOpenIndex, setDialogIsOpen, control.id],
|
||||||
)
|
)
|
||||||
|
|
||||||
const close = React.useCallback<DialogControlProps['close']>(cb => {
|
const close = React.useCallback<DialogControlProps['close']>(cb => {
|
||||||
|
@ -133,12 +133,12 @@ export function Outer({
|
||||||
closeCallback.current = undefined
|
closeCallback.current = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
openDialogs.current.delete(control.id)
|
setDialogIsOpen(control.id, false)
|
||||||
onClose?.()
|
onClose?.()
|
||||||
setOpenIndex(-1)
|
setOpenIndex(-1)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onClose, setOpenIndex, openDialogs, control.id],
|
[onClose, setOpenIndex, setDialogIsOpen, control.id],
|
||||||
)
|
)
|
||||||
|
|
||||||
const context = React.useMemo(() => ({close}), [close])
|
const context = React.useMemo(() => ({close}), [close])
|
||||||
|
@ -146,38 +146,45 @@ export function Outer({
|
||||||
return (
|
return (
|
||||||
isOpen && (
|
isOpen && (
|
||||||
<Portal>
|
<Portal>
|
||||||
<BottomSheet
|
<View
|
||||||
enableDynamicSizing={!hasSnapPoints}
|
// iOS
|
||||||
enablePanDownToClose
|
accessibilityViewIsModal
|
||||||
keyboardBehavior="interactive"
|
// Android
|
||||||
android_keyboardInputMode="adjustResize"
|
importantForAccessibility="yes"
|
||||||
keyboardBlurBehavior="restore"
|
style={[a.absolute, a.inset_0]}>
|
||||||
topInset={insets.top}
|
<BottomSheet
|
||||||
{...sheetOptions}
|
enableDynamicSizing={!hasSnapPoints}
|
||||||
snapPoints={sheetOptions.snapPoints || ['100%']}
|
enablePanDownToClose
|
||||||
ref={sheet}
|
keyboardBehavior="interactive"
|
||||||
index={openIndex}
|
android_keyboardInputMode="adjustResize"
|
||||||
backgroundStyle={{backgroundColor: 'transparent'}}
|
keyboardBlurBehavior="restore"
|
||||||
backdropComponent={Backdrop}
|
topInset={insets.top}
|
||||||
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
|
{...sheetOptions}
|
||||||
handleStyle={{display: 'none'}}
|
snapPoints={sheetOptions.snapPoints || ['100%']}
|
||||||
onChange={onChange}>
|
ref={sheet}
|
||||||
<Context.Provider value={context}>
|
index={openIndex}
|
||||||
<View
|
backgroundStyle={{backgroundColor: 'transparent'}}
|
||||||
style={[
|
backdropComponent={Backdrop}
|
||||||
a.absolute,
|
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
|
||||||
a.inset_0,
|
handleStyle={{display: 'none'}}
|
||||||
t.atoms.bg,
|
onChange={onChange}>
|
||||||
{
|
<Context.Provider value={context}>
|
||||||
borderTopLeftRadius: 40,
|
<View
|
||||||
borderTopRightRadius: 40,
|
style={[
|
||||||
height: Dimensions.get('window').height * 2,
|
a.absolute,
|
||||||
},
|
a.inset_0,
|
||||||
]}
|
t.atoms.bg,
|
||||||
/>
|
{
|
||||||
{children}
|
borderTopLeftRadius: 40,
|
||||||
</Context.Provider>
|
borderTopRightRadius: 40,
|
||||||
</BottomSheet>
|
height: Dimensions.get('window').height * 2,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</Context.Provider>
|
||||||
|
</BottomSheet>
|
||||||
|
</View>
|
||||||
</Portal>
|
</Portal>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types'
|
||||||
import {Context} from '#/components/Dialog/context'
|
import {Context} from '#/components/Dialog/context'
|
||||||
import {Button, ButtonIcon} from '#/components/Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||||
import {useDialogStateContext} from '#/state/dialogs'
|
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||||
|
|
||||||
export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
|
export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
|
||||||
export * from '#/components/Dialog/types'
|
export * from '#/components/Dialog/types'
|
||||||
|
@ -30,21 +30,21 @@ export function Outer({
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const [isOpen, setIsOpen] = React.useState(false)
|
const [isOpen, setIsOpen] = React.useState(false)
|
||||||
const [isVisible, setIsVisible] = React.useState(true)
|
const [isVisible, setIsVisible] = React.useState(true)
|
||||||
const {openDialogs} = useDialogStateContext()
|
const {setDialogIsOpen} = useDialogStateControlContext()
|
||||||
|
|
||||||
const open = React.useCallback(() => {
|
const open = React.useCallback(() => {
|
||||||
setIsOpen(true)
|
setIsOpen(true)
|
||||||
openDialogs.current.add(control.id)
|
setDialogIsOpen(control.id, true)
|
||||||
}, [setIsOpen, openDialogs, control.id])
|
}, [setIsOpen, setDialogIsOpen, control.id])
|
||||||
|
|
||||||
const close = React.useCallback(async () => {
|
const close = React.useCallback(async () => {
|
||||||
setIsVisible(false)
|
setIsVisible(false)
|
||||||
await new Promise(resolve => setTimeout(resolve, 150))
|
await new Promise(resolve => setTimeout(resolve, 150))
|
||||||
setIsOpen(false)
|
setIsOpen(false)
|
||||||
setIsVisible(true)
|
setIsVisible(true)
|
||||||
openDialogs.current.delete(control.id)
|
setDialogIsOpen(control.id, false)
|
||||||
onClose?.()
|
onClose?.()
|
||||||
}, [onClose, setIsOpen, openDialogs, control.id])
|
}, [onClose, setIsOpen, setDialogIsOpen, control.id])
|
||||||
|
|
||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
control.ref,
|
control.ref,
|
||||||
|
|
|
@ -13,20 +13,20 @@ const DialogContext = React.createContext<{
|
||||||
* The currently open dialogs, referenced by their IDs, generated from
|
* The currently open dialogs, referenced by their IDs, generated from
|
||||||
* `useId`.
|
* `useId`.
|
||||||
*/
|
*/
|
||||||
openDialogs: React.MutableRefObject<Set<string>>
|
openDialogs: string[]
|
||||||
}>({
|
}>({
|
||||||
activeDialogs: {
|
activeDialogs: {
|
||||||
current: new Map(),
|
current: new Map(),
|
||||||
},
|
},
|
||||||
openDialogs: {
|
openDialogs: [],
|
||||||
current: new Set(),
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const DialogControlContext = React.createContext<{
|
const DialogControlContext = React.createContext<{
|
||||||
closeAllDialogs(): boolean
|
closeAllDialogs(): boolean
|
||||||
|
setDialogIsOpen(id: string, isOpen: boolean): void
|
||||||
}>({
|
}>({
|
||||||
closeAllDialogs: () => false,
|
closeAllDialogs: () => false,
|
||||||
|
setDialogIsOpen: () => {},
|
||||||
})
|
})
|
||||||
|
|
||||||
export function useDialogStateContext() {
|
export function useDialogStateContext() {
|
||||||
|
@ -41,15 +41,31 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
const activeDialogs = React.useRef<
|
const activeDialogs = React.useRef<
|
||||||
Map<string, React.MutableRefObject<DialogControlRefProps>>
|
Map<string, React.MutableRefObject<DialogControlRefProps>>
|
||||||
>(new Map())
|
>(new Map())
|
||||||
const openDialogs = React.useRef<Set<string>>(new Set())
|
const [openDialogs, setOpenDialogs] = React.useState<string[]>([])
|
||||||
|
|
||||||
const closeAllDialogs = React.useCallback(() => {
|
const closeAllDialogs = React.useCallback(() => {
|
||||||
activeDialogs.current.forEach(dialog => dialog.current.close())
|
activeDialogs.current.forEach(dialog => dialog.current.close())
|
||||||
return openDialogs.current.size > 0
|
return openDialogs.length > 0
|
||||||
}, [])
|
}, [openDialogs])
|
||||||
|
|
||||||
const context = React.useMemo(() => ({activeDialogs, openDialogs}), [])
|
const setDialogIsOpen = React.useCallback(
|
||||||
const controls = React.useMemo(() => ({closeAllDialogs}), [closeAllDialogs])
|
(id: string, isOpen: boolean) => {
|
||||||
|
setOpenDialogs(prev => {
|
||||||
|
const filtered = prev.filter(dialogId => dialogId !== id) as string[]
|
||||||
|
return isOpen ? [...filtered, id] : filtered
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[setOpenDialogs],
|
||||||
|
)
|
||||||
|
|
||||||
|
const context = React.useMemo(
|
||||||
|
() => ({activeDialogs, openDialogs}),
|
||||||
|
[openDialogs],
|
||||||
|
)
|
||||||
|
const controls = React.useMemo(
|
||||||
|
() => ({closeAllDialogs, setDialogIsOpen}),
|
||||||
|
[closeAllDialogs, setDialogIsOpen],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContext.Provider value={context}>
|
<DialogContext.Provider value={context}>
|
||||||
|
|
|
@ -30,6 +30,7 @@ import {useCloseAnyActiveElement} from '#/state/util'
|
||||||
import * as notifications from 'lib/notifications/notifications'
|
import * as notifications from 'lib/notifications/notifications'
|
||||||
import {Outlet as PortalOutlet} from '#/components/Portal'
|
import {Outlet as PortalOutlet} from '#/components/Portal'
|
||||||
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
||||||
|
import {useDialogStateContext} from '#/state/dialogs'
|
||||||
|
|
||||||
function ShellInner() {
|
function ShellInner() {
|
||||||
const isDrawerOpen = useIsDrawerOpen()
|
const isDrawerOpen = useIsDrawerOpen()
|
||||||
|
@ -55,6 +56,7 @@ function ShellInner() {
|
||||||
const closeAnyActiveElement = useCloseAnyActiveElement()
|
const closeAnyActiveElement = useCloseAnyActiveElement()
|
||||||
// start undefined
|
// start undefined
|
||||||
const currentAccountDid = React.useRef<string | undefined>(undefined)
|
const currentAccountDid = React.useRef<string | undefined>(undefined)
|
||||||
|
const {openDialogs} = useDialogStateContext()
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let listener = {remove() {}}
|
let listener = {remove() {}}
|
||||||
|
@ -78,9 +80,21 @@ function ShellInner() {
|
||||||
}
|
}
|
||||||
}, [currentAccount])
|
}, [currentAccount])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The counterpart to `accessibilityViewIsModal` for Android. This property
|
||||||
|
* applies to the parent of all non-modal views, and prevents TalkBack from
|
||||||
|
* navigating within content beneath an open dialog.
|
||||||
|
*
|
||||||
|
* @see https://reactnative.dev/docs/accessibility#importantforaccessibility-android
|
||||||
|
*/
|
||||||
|
const importantForAccessibility =
|
||||||
|
openDialogs.length > 0 ? 'no-hide-descendants' : undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View style={containerPadding}>
|
<View
|
||||||
|
style={containerPadding}
|
||||||
|
importantForAccessibility={importantForAccessibility}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Drawer
|
<Drawer
|
||||||
renderDrawerContent={renderDrawerContent}
|
renderDrawerContent={renderDrawerContent}
|
||||||
|
|
Loading…
Reference in New Issue