From fc82d2f6d5e8a93f0e7ce4861c5205c8a4b49c30 Mon Sep 17 00:00:00 2001
From: Samuel Newman <mozzius@protonmail.com>
Date: Tue, 8 Oct 2024 20:00:49 +0300
Subject: Move Dialogs to Radix (#5648)

* Use Redix FocusTrap (#5638)

* Use Redix FocusTrap

* force resolutions on radix libs

* add focus guards

* use @radix-ui/dismissable-layer for escape handling

* fix banner menu keypress by using `Pressable`

* add menu in dialog example to storybook

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

* use DismissableLayer/FocusScope for composer

* fix storybook dialog

* thread Portal through Prompt and avatar/banner

* fix dialog style regression

* remove tamagui

---------

Co-authored-by: Eric Bailey <git@esb.lol>
---
 src/view/shell/Composer.web.tsx | 111 +++++++++++++++++++++++++---------------
 1 file changed, 69 insertions(+), 42 deletions(-)

(limited to 'src/view/shell/Composer.web.tsx')

diff --git a/src/view/shell/Composer.web.tsx b/src/view/shell/Composer.web.tsx
index ee1ed6622..d25cae010 100644
--- a/src/view/shell/Composer.web.tsx
+++ b/src/view/shell/Composer.web.tsx
@@ -1,24 +1,42 @@
 import React from 'react'
 import {StyleSheet, View} from 'react-native'
+import {DismissableLayer} from '@radix-ui/react-dismissable-layer'
+import {useFocusGuards} from '@radix-ui/react-focus-guards'
+import {FocusScope} from '@radix-ui/react-focus-scope'
 
 import {useWebBodyScrollLock} from '#/lib/hooks/useWebBodyScrollLock'
-import {useComposerState} from 'state/shell/composer'
+import {useModals} from '#/state/modals'
+import {ComposerOpts, useComposerState} from '#/state/shell/composer'
 import {
   EmojiPicker,
   EmojiPickerState,
-} from 'view/com/composer/text-input/web/EmojiPicker.web'
+} from '#/view/com/composer/text-input/web/EmojiPicker.web'
 import {useBreakpoints, useTheme} from '#/alf'
-import {ComposePost} from '../com/composer/Composer'
+import {ComposePost, useComposerCancelRef} from '../com/composer/Composer'
 
 const BOTTOM_BAR_HEIGHT = 61
 
 export function Composer({}: {winHeight: number}) {
-  const t = useTheme()
-  const {gtMobile} = useBreakpoints()
   const state = useComposerState()
   const isActive = !!state
+
   useWebBodyScrollLock(isActive)
 
+  // rendering
+  // =
+
+  if (!isActive) {
+    return <View />
+  }
+
+  return <Inner state={state} />
+}
+
+function Inner({state}: {state: ComposerOpts}) {
+  const ref = useComposerCancelRef()
+  const {isModalActive} = useModals()
+  const t = useTheme()
+  const {gtMobile} = useBreakpoints()
   const [pickerState, setPickerState] = React.useState<EmojiPickerState>({
     isOpen: false,
     pos: {top: 0, left: 0, right: 0, bottom: 0},
@@ -39,49 +57,58 @@ export function Composer({}: {winHeight: number}) {
     }))
   }, [])
 
-  // rendering
-  // =
-
-  if (!isActive) {
-    return <View />
-  }
+  useFocusGuards()
 
   return (
-    <View style={styles.mask} aria-modal accessibilityViewIsModal>
-      <View
-        style={[
-          styles.container,
-          !gtMobile && styles.containerMobile,
-          t.atoms.bg,
-          t.atoms.border_contrast_medium,
-        ]}>
-        <ComposePost
-          replyTo={state.replyTo}
-          quote={state.quote}
-          quoteCount={state?.quoteCount}
-          onPost={state.onPost}
-          mention={state.mention}
-          openEmojiPicker={onOpenPicker}
-          text={state.text}
-          imageUris={state.imageUris}
-        />
-      </View>
-      <EmojiPicker state={pickerState} close={onClosePicker} />
-    </View>
+    <FocusScope loop trapped asChild>
+      <DismissableLayer
+        role="dialog"
+        aria-modal
+        style={{
+          position: 'fixed',
+          top: 0,
+          left: 0,
+          width: '100%',
+          height: '100%',
+          backgroundColor: '#000c',
+          display: 'flex',
+          flexDirection: 'column',
+          alignItems: 'center',
+        }}
+        onFocusOutside={evt => evt.preventDefault()}
+        onInteractOutside={evt => evt.preventDefault()}
+        onDismiss={() => {
+          // TEMP: remove when all modals are ALF'd -sfn
+          if (!isModalActive) {
+            ref.current?.onPressCancel()
+          }
+        }}>
+        <View
+          style={[
+            styles.container,
+            !gtMobile && styles.containerMobile,
+            t.atoms.bg,
+            t.atoms.border_contrast_medium,
+          ]}>
+          <ComposePost
+            cancelRef={ref}
+            replyTo={state.replyTo}
+            quote={state.quote}
+            quoteCount={state?.quoteCount}
+            onPost={state.onPost}
+            mention={state.mention}
+            openEmojiPicker={onOpenPicker}
+            text={state.text}
+            imageUris={state.imageUris}
+          />
+        </View>
+        <EmojiPicker state={pickerState} close={onClosePicker} />
+      </DismissableLayer>
+    </FocusScope>
   )
 }
 
 const styles = StyleSheet.create({
-  mask: {
-    // @ts-ignore
-    position: 'fixed',
-    top: 0,
-    left: 0,
-    width: '100%',
-    height: '100%',
-    backgroundColor: '#000c',
-    alignItems: 'center',
-  },
   container: {
     marginTop: 50,
     maxWidth: 600,
-- 
cgit 1.4.1

