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
parent
8f623c3bdf
commit
0f9f08b1ef
|
@ -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],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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}),
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
Loading…
Reference in New Issue