Fix reactivity of dialogs (Dialogs Pt. 1) (#3146)

* Improve a11y on ios

* Format

* Remove android

* Fix android

* Revert some changes

* use sharedvalue for `importantForAccessibility`

* add back `isOpen`

* fix some more types

---------

Co-authored-by: Eric Bailey <git@esb.lol>
zio/stable
Hailey 2024-03-08 14:31:24 -08:00 committed by GitHub
parent 8f623c3bdf
commit 0f9f08b1ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 49 additions and 38 deletions

View File

@ -21,8 +21,7 @@ export function useDialogControl(): DialogOuterProps['control'] {
open: () => {}, open: () => {},
close: () => {}, close: () => {},
}) })
const {activeDialogs, openDialogs} = useDialogStateContext() const {activeDialogs} = useDialogStateContext()
const isOpen = openDialogs.includes(id)
React.useEffect(() => { React.useEffect(() => {
activeDialogs.current.set(id, control) activeDialogs.current.set(id, control)
@ -36,7 +35,6 @@ export function useDialogControl(): DialogOuterProps['control'] {
() => ({ () => ({
id, id,
ref: control, ref: control,
isOpen,
open: () => { open: () => {
control.current.open() control.current.open()
}, },
@ -44,6 +42,6 @@ export function useDialogControl(): DialogOuterProps['control'] {
control.current.close(cb) control.current.close(cb)
}, },
}), }),
[id, control, isOpen], [id, control],
) )
} }

View File

@ -22,7 +22,7 @@ export type DialogControlRefProps = {
export type DialogControlProps = DialogControlRefProps & { export type DialogControlProps = DialogControlRefProps & {
id: string id: string
ref: React.RefObject<DialogControlRefProps> ref: React.RefObject<DialogControlRefProps>
isOpen: boolean isOpen?: boolean
} }
export type DialogContextProps = { export type DialogContextProps = {

View File

@ -1,8 +1,9 @@
import React from 'react' import React from 'react'
import {SharedValue, useSharedValue} from 'react-native-reanimated'
import {DialogControlRefProps} from '#/components/Dialog' import {DialogControlRefProps} from '#/components/Dialog'
import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context' import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
const DialogContext = React.createContext<{ interface IDialogContext {
/** /**
* The currently active `useDialogControl` hooks. * The currently active `useDialogControl` hooks.
*/ */
@ -13,13 +14,18 @@ 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: string[] openDialogs: React.MutableRefObject<Set<string>>
}>({ /**
activeDialogs: { * The counterpart to `accessibilityViewIsModal` for Android. This property
current: new Map(), * applies to the parent of all non-modal views, and prevents TalkBack from
}, * navigating within content beneath an open dialog.
openDialogs: [], *
}) * @see https://reactnative.dev/docs/accessibility#importantforaccessibility-android
*/
importantForAccessibility: SharedValue<'auto' | 'no-hide-descendants'>
}
const DialogContext = React.createContext<IDialogContext>({} as IDialogContext)
const DialogControlContext = React.createContext<{ const DialogControlContext = React.createContext<{
closeAllDialogs(): boolean closeAllDialogs(): boolean
@ -41,26 +47,42 @@ 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, setOpenDialogs] = React.useState<string[]>([]) const openDialogs = React.useRef<Set<string>>(new Set())
const importantForAccessibility = useSharedValue<
'auto' | 'no-hide-descendants'
>('auto')
const closeAllDialogs = React.useCallback(() => { const closeAllDialogs = React.useCallback(() => {
activeDialogs.current.forEach(dialog => dialog.current.close()) activeDialogs.current.forEach(dialog => dialog.current.close())
return openDialogs.length > 0 return openDialogs.current.size > 0
}, [openDialogs]) }, [])
const setDialogIsOpen = React.useCallback( const setDialogIsOpen = React.useCallback(
(id: string, isOpen: boolean) => { (id: string, isOpen: boolean) => {
setOpenDialogs(prev => { if (isOpen) {
const filtered = prev.filter(dialogId => dialogId !== id) as string[] openDialogs.current.add(id)
return isOpen ? [...filtered, id] : filtered importantForAccessibility.value = 'no-hide-descendants'
}) } else {
openDialogs.current.delete(id)
if (openDialogs.current.size < 1) {
importantForAccessibility.value = 'auto'
}
}
}, },
[setOpenDialogs], [importantForAccessibility],
) )
const context = React.useMemo( const context = React.useMemo<IDialogContext>(
() => ({activeDialogs, openDialogs}), () => ({
[openDialogs], activeDialogs: {
current: new Map(),
},
openDialogs: {
current: new Set(),
},
importantForAccessibility,
}),
[importantForAccessibility],
) )
const controls = React.useMemo( const controls = React.useMemo(
() => ({closeAllDialogs, setDialogIsOpen}), () => ({closeAllDialogs, setDialogIsOpen}),

View File

@ -30,7 +30,8 @@ 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' import {useDialogStateContext} from 'state/dialogs'
import Animated from 'react-native-reanimated'
function ShellInner() { function ShellInner() {
const isDrawerOpen = useIsDrawerOpen() const isDrawerOpen = useIsDrawerOpen()
@ -54,9 +55,9 @@ function ShellInner() {
const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
const {hasSession, currentAccount} = useSession() const {hasSession, currentAccount} = useSession()
const closeAnyActiveElement = useCloseAnyActiveElement() const closeAnyActiveElement = useCloseAnyActiveElement()
const {importantForAccessibility} = useDialogStateContext()
// 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() {}}
@ -80,19 +81,9 @@ 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 <Animated.View
style={containerPadding} style={containerPadding}
importantForAccessibility={importantForAccessibility}> importantForAccessibility={importantForAccessibility}>
<ErrorBoundary> <ErrorBoundary>
@ -106,7 +97,7 @@ function ShellInner() {
<TabsNavigator /> <TabsNavigator />
</Drawer> </Drawer>
</ErrorBoundary> </ErrorBoundary>
</View> </Animated.View>
<Composer winHeight={winDim.height} /> <Composer winHeight={winDim.height} />
<ModalsContainer /> <ModalsContainer />
<MutedWordsDialog /> <MutedWordsDialog />