From 6c9d6f5b05953988cb4fb1556bf435805479e07e Mon Sep 17 00:00:00 2001
From: Eric Bailey <git@esb.lol>
Date: Mon, 4 Mar 2024 15:37:11 -0600
Subject: Improve dialogs a11y (#3094)

* Improve a11y on ios

* Format

* Remove android

* Fix android
---
 src/state/dialogs/index.tsx | 34 +++++++++++++++++++++++++---------
 1 file changed, 25 insertions(+), 9 deletions(-)

(limited to 'src/state/dialogs/index.tsx')

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}>
-- 
cgit 1.4.1


From 0f9f08b1ef795215975c7b041d0e94a992d22124 Mon Sep 17 00:00:00 2001
From: Hailey <me@haileyok.com>
Date: Fri, 8 Mar 2024 14:31:24 -0800
Subject: 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>
---
 src/components/Dialog/context.ts |  6 ++--
 src/components/Dialog/types.ts   |  2 +-
 src/state/dialogs/index.tsx      | 60 +++++++++++++++++++++++++++-------------
 src/view/shell/index.tsx         | 19 ++++---------
 4 files changed, 49 insertions(+), 38 deletions(-)

(limited to 'src/state/dialogs/index.tsx')

diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts
index 9b571e8e9..859f8edd7 100644
--- a/src/components/Dialog/context.ts
+++ b/src/components/Dialog/context.ts
@@ -21,8 +21,7 @@ export function useDialogControl(): DialogOuterProps['control'] {
     open: () => {},
     close: () => {},
   })
-  const {activeDialogs, openDialogs} = useDialogStateContext()
-  const isOpen = openDialogs.includes(id)
+  const {activeDialogs} = useDialogStateContext()
 
   React.useEffect(() => {
     activeDialogs.current.set(id, control)
@@ -36,7 +35,6 @@ export function useDialogControl(): DialogOuterProps['control'] {
     () => ({
       id,
       ref: control,
-      isOpen,
       open: () => {
         control.current.open()
       },
@@ -44,6 +42,6 @@ export function useDialogControl(): DialogOuterProps['control'] {
         control.current.close(cb)
       },
     }),
-    [id, control, isOpen],
+    [id, control],
   )
 }
diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts
index fa9398fe0..4fc60ec39 100644
--- a/src/components/Dialog/types.ts
+++ b/src/components/Dialog/types.ts
@@ -22,7 +22,7 @@ export type DialogControlRefProps = {
 export type DialogControlProps = DialogControlRefProps & {
   id: string
   ref: React.RefObject<DialogControlRefProps>
-  isOpen: boolean
+  isOpen?: boolean
 }
 
 export type DialogContextProps = {
diff --git a/src/state/dialogs/index.tsx b/src/state/dialogs/index.tsx
index 90aaca4f8..951105a50 100644
--- a/src/state/dialogs/index.tsx
+++ b/src/state/dialogs/index.tsx
@@ -1,8 +1,9 @@
 import React from 'react'
+import {SharedValue, useSharedValue} from 'react-native-reanimated'
 import {DialogControlRefProps} from '#/components/Dialog'
 import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
 
-const DialogContext = React.createContext<{
+interface IDialogContext {
   /**
    * The currently active `useDialogControl` hooks.
    */
@@ -13,13 +14,18 @@ const DialogContext = React.createContext<{
    * The currently open dialogs, referenced by their IDs, generated from
    * `useId`.
    */
-  openDialogs: string[]
-}>({
-  activeDialogs: {
-    current: new Map(),
-  },
-  openDialogs: [],
-})
+  openDialogs: React.MutableRefObject<Set<string>>
+  /**
+   * 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
+   */
+  importantForAccessibility: SharedValue<'auto' | 'no-hide-descendants'>
+}
+
+const DialogContext = React.createContext<IDialogContext>({} as IDialogContext)
 
 const DialogControlContext = React.createContext<{
   closeAllDialogs(): boolean
@@ -41,26 +47,42 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
   const activeDialogs = React.useRef<
     Map<string, React.MutableRefObject<DialogControlRefProps>>
   >(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(() => {
     activeDialogs.current.forEach(dialog => dialog.current.close())
-    return openDialogs.length > 0
-  }, [openDialogs])
+    return openDialogs.current.size > 0
+  }, [])
 
   const setDialogIsOpen = React.useCallback(
     (id: string, isOpen: boolean) => {
-      setOpenDialogs(prev => {
-        const filtered = prev.filter(dialogId => dialogId !== id) as string[]
-        return isOpen ? [...filtered, id] : filtered
-      })
+      if (isOpen) {
+        openDialogs.current.add(id)
+        importantForAccessibility.value = 'no-hide-descendants'
+      } else {
+        openDialogs.current.delete(id)
+        if (openDialogs.current.size < 1) {
+          importantForAccessibility.value = 'auto'
+        }
+      }
     },
-    [setOpenDialogs],
+    [importantForAccessibility],
   )
 
-  const context = React.useMemo(
-    () => ({activeDialogs, openDialogs}),
-    [openDialogs],
+  const context = React.useMemo<IDialogContext>(
+    () => ({
+      activeDialogs: {
+        current: new Map(),
+      },
+      openDialogs: {
+        current: new Set(),
+      },
+      importantForAccessibility,
+    }),
+    [importantForAccessibility],
   )
   const controls = React.useMemo(
     () => ({closeAllDialogs, setDialogIsOpen}),
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index bdba79174..76a7f8fb3 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -30,7 +30,8 @@ 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'
+import {useDialogStateContext} from 'state/dialogs'
+import Animated from 'react-native-reanimated'
 
 function ShellInner() {
   const isDrawerOpen = useIsDrawerOpen()
@@ -54,9 +55,9 @@ function ShellInner() {
   const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
   const {hasSession, currentAccount} = useSession()
   const closeAnyActiveElement = useCloseAnyActiveElement()
+  const {importantForAccessibility} = useDialogStateContext()
   // start undefined
   const currentAccountDid = React.useRef<string | undefined>(undefined)
-  const {openDialogs} = useDialogStateContext()
 
   React.useEffect(() => {
     let listener = {remove() {}}
@@ -80,19 +81,9 @@ 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
+      <Animated.View
         style={containerPadding}
         importantForAccessibility={importantForAccessibility}>
         <ErrorBoundary>
@@ -106,7 +97,7 @@ function ShellInner() {
             <TabsNavigator />
           </Drawer>
         </ErrorBoundary>
-      </View>
+      </Animated.View>
       <Composer winHeight={winDim.height} />
       <ModalsContainer />
       <MutedWordsDialog />
-- 
cgit 1.4.1

