about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/Dialog/index.tsx83
-rw-r--r--src/components/Dialog/index.web.tsx12
-rw-r--r--src/state/dialogs/index.tsx34
-rw-r--r--src/view/shell/index.tsx16
4 files changed, 91 insertions, 54 deletions
diff --git a/src/components/Dialog/index.tsx b/src/components/Dialog/index.tsx
index ef4f4741b..fa375b0f4 100644
--- a/src/components/Dialog/index.tsx
+++ b/src/components/Dialog/index.tsx
@@ -15,7 +15,7 @@ import {useTheme, atoms as a, flatten} from '#/alf'
 import {Portal} from '#/components/Portal'
 import {createInput} from '#/components/forms/TextField'
 import {logger} from '#/logger'
-import {useDialogStateContext} from '#/state/dialogs'
+import {useDialogStateControlContext} from '#/state/dialogs'
 
 import {
   DialogOuterProps,
@@ -82,7 +82,7 @@ export function Outer({
   const hasSnapPoints = !!sheetOptions.snapPoints
   const insets = useSafeAreaInsets()
   const closeCallback = React.useRef<() => void>()
-  const {openDialogs} = useDialogStateContext()
+  const {setDialogIsOpen} = useDialogStateControlContext()
 
   /*
    * 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']>(
     ({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"
       setOpenIndex(index || 0)
     },
-    [setOpenIndex, openDialogs, control.id],
+    [setOpenIndex, setDialogIsOpen, control.id],
   )
 
   const close = React.useCallback<DialogControlProps['close']>(cb => {
@@ -133,12 +133,12 @@ export function Outer({
           closeCallback.current = undefined
         }
 
-        openDialogs.current.delete(control.id)
+        setDialogIsOpen(control.id, false)
         onClose?.()
         setOpenIndex(-1)
       }
     },
-    [onClose, setOpenIndex, openDialogs, control.id],
+    [onClose, setOpenIndex, setDialogIsOpen, control.id],
   )
 
   const context = React.useMemo(() => ({close}), [close])
@@ -146,38 +146,45 @@ export function Outer({
   return (
     isOpen && (
       <Portal>
-        <BottomSheet
-          enableDynamicSizing={!hasSnapPoints}
-          enablePanDownToClose
-          keyboardBehavior="interactive"
-          android_keyboardInputMode="adjustResize"
-          keyboardBlurBehavior="restore"
-          topInset={insets.top}
-          {...sheetOptions}
-          snapPoints={sheetOptions.snapPoints || ['100%']}
-          ref={sheet}
-          index={openIndex}
-          backgroundStyle={{backgroundColor: 'transparent'}}
-          backdropComponent={Backdrop}
-          handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
-          handleStyle={{display: 'none'}}
-          onChange={onChange}>
-          <Context.Provider value={context}>
-            <View
-              style={[
-                a.absolute,
-                a.inset_0,
-                t.atoms.bg,
-                {
-                  borderTopLeftRadius: 40,
-                  borderTopRightRadius: 40,
-                  height: Dimensions.get('window').height * 2,
-                },
-              ]}
-            />
-            {children}
-          </Context.Provider>
-        </BottomSheet>
+        <View
+          // iOS
+          accessibilityViewIsModal
+          // Android
+          importantForAccessibility="yes"
+          style={[a.absolute, a.inset_0]}>
+          <BottomSheet
+            enableDynamicSizing={!hasSnapPoints}
+            enablePanDownToClose
+            keyboardBehavior="interactive"
+            android_keyboardInputMode="adjustResize"
+            keyboardBlurBehavior="restore"
+            topInset={insets.top}
+            {...sheetOptions}
+            snapPoints={sheetOptions.snapPoints || ['100%']}
+            ref={sheet}
+            index={openIndex}
+            backgroundStyle={{backgroundColor: 'transparent'}}
+            backdropComponent={Backdrop}
+            handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
+            handleStyle={{display: 'none'}}
+            onChange={onChange}>
+            <Context.Provider value={context}>
+              <View
+                style={[
+                  a.absolute,
+                  a.inset_0,
+                  t.atoms.bg,
+                  {
+                    borderTopLeftRadius: 40,
+                    borderTopRightRadius: 40,
+                    height: Dimensions.get('window').height * 2,
+                  },
+                ]}
+              />
+              {children}
+            </Context.Provider>
+          </BottomSheet>
+        </View>
       </Portal>
     )
   )
diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx
index 32163e735..3a7f73342 100644
--- a/src/components/Dialog/index.web.tsx
+++ b/src/components/Dialog/index.web.tsx
@@ -12,7 +12,7 @@ import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types'
 import {Context} from '#/components/Dialog/context'
 import {Button, ButtonIcon} from '#/components/Button'
 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 * from '#/components/Dialog/types'
@@ -30,21 +30,21 @@ export function Outer({
   const {gtMobile} = useBreakpoints()
   const [isOpen, setIsOpen] = React.useState(false)
   const [isVisible, setIsVisible] = React.useState(true)
-  const {openDialogs} = useDialogStateContext()
+  const {setDialogIsOpen} = useDialogStateControlContext()
 
   const open = React.useCallback(() => {
     setIsOpen(true)
-    openDialogs.current.add(control.id)
-  }, [setIsOpen, openDialogs, control.id])
+    setDialogIsOpen(control.id, true)
+  }, [setIsOpen, setDialogIsOpen, control.id])
 
   const close = React.useCallback(async () => {
     setIsVisible(false)
     await new Promise(resolve => setTimeout(resolve, 150))
     setIsOpen(false)
     setIsVisible(true)
-    openDialogs.current.delete(control.id)
+    setDialogIsOpen(control.id, false)
     onClose?.()
-  }, [onClose, setIsOpen, openDialogs, control.id])
+  }, [onClose, setIsOpen, setDialogIsOpen, control.id])
 
   useImperativeHandle(
     control.ref,
diff --git a/src/state/dialogs/index.tsx b/src/state/dialogs/index.tsx
index 9fc70c178..90aaca4f8 100644
--- a/src/state/dialogs/index.tsx
+++ b/src/state/dialogs/index.tsx
@@ -13,20 +13,20 @@ const DialogContext = React.createContext<{
    * The currently open dialogs, referenced by their IDs, generated from
    * `useId`.
    */
-  openDialogs: React.MutableRefObject<Set<string>>
+  openDialogs: string[]
 }>({
   activeDialogs: {
     current: new Map(),
   },
-  openDialogs: {
-    current: new Set(),
-  },
+  openDialogs: [],
 })
 
 const DialogControlContext = React.createContext<{
   closeAllDialogs(): boolean
+  setDialogIsOpen(id: string, isOpen: boolean): void
 }>({
   closeAllDialogs: () => false,
+  setDialogIsOpen: () => {},
 })
 
 export function useDialogStateContext() {
@@ -41,15 +41,31 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   const activeDialogs = React.useRef<
     Map<string, React.MutableRefObject<DialogControlRefProps>>
   >(new Map())
-  const openDialogs = React.useRef<Set<string>>(new Set())
+  const [openDialogs, setOpenDialogs] = React.useState<string[]>([])
 
   const closeAllDialogs = React.useCallback(() => {
     activeDialogs.current.forEach(dialog => dialog.current.close())
-    return openDialogs.current.size > 0
-  }, [])
+    return openDialogs.length > 0
+  }, [openDialogs])
+
+  const setDialogIsOpen = React.useCallback(
+    (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}), [])
-  const controls = React.useMemo(() => ({closeAllDialogs}), [closeAllDialogs])
+  const context = React.useMemo(
+    () => ({activeDialogs, openDialogs}),
+    [openDialogs],
+  )
+  const controls = React.useMemo(
+    () => ({closeAllDialogs, setDialogIsOpen}),
+    [closeAllDialogs, setDialogIsOpen],
+  )
 
   return (
     <DialogContext.Provider value={context}>
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index d895d8851..bdba79174 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -30,6 +30,7 @@ import {useCloseAnyActiveElement} from '#/state/util'
 import * as notifications from 'lib/notifications/notifications'
 import {Outlet as PortalOutlet} from '#/components/Portal'
 import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
+import {useDialogStateContext} from '#/state/dialogs'
 
 function ShellInner() {
   const isDrawerOpen = useIsDrawerOpen()
@@ -55,6 +56,7 @@ function ShellInner() {
   const closeAnyActiveElement = useCloseAnyActiveElement()
   // start undefined
   const currentAccountDid = React.useRef<string | undefined>(undefined)
+  const {openDialogs} = useDialogStateContext()
 
   React.useEffect(() => {
     let listener = {remove() {}}
@@ -78,9 +80,21 @@ function ShellInner() {
     }
   }, [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 (
     <>
-      <View style={containerPadding}>
+      <View
+        style={containerPadding}
+        importantForAccessibility={importantForAccessibility}>
         <ErrorBoundary>
           <Drawer
             renderDrawerContent={renderDrawerContent}