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>
This commit is contained in:
@@ -10,7 +10,9 @@ import {
|
||||
import Animated, {FadeIn, FadeInDown} from 'react-native-reanimated'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {FocusScope} from '@tamagui/focus-scope'
|
||||
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 {logger} from '#/logger'
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
@@ -31,6 +33,7 @@ export * from '#/components/Dialog/utils'
|
||||
export {Input} from '#/components/forms/TextField'
|
||||
|
||||
const stopPropagation = (e: any) => e.stopPropagation()
|
||||
const preventDefault = (e: any) => e.preventDefault()
|
||||
|
||||
export function Outer({
|
||||
children,
|
||||
@@ -85,21 +88,6 @@ export function Outer({
|
||||
[close, open],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isOpen) return
|
||||
|
||||
function handler(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape') {
|
||||
e.stopPropagation()
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handler)
|
||||
|
||||
return () => document.removeEventListener('keydown', handler)
|
||||
}, [close, isOpen])
|
||||
|
||||
const context = React.useMemo(
|
||||
() => ({
|
||||
close,
|
||||
@@ -168,9 +156,11 @@ export function Inner({
|
||||
accessibilityDescribedBy,
|
||||
}: DialogInnerProps) {
|
||||
const t = useTheme()
|
||||
const {close} = React.useContext(Context)
|
||||
const {gtMobile} = useBreakpoints()
|
||||
useFocusGuards()
|
||||
return (
|
||||
<FocusScope loop enabled trapped>
|
||||
<FocusScope loop asChild trapped>
|
||||
<Animated.View
|
||||
role="dialog"
|
||||
aria-role="dialog"
|
||||
@@ -183,7 +173,7 @@ export function Inner({
|
||||
onTouchEnd={stopPropagation}
|
||||
entering={FadeInDown.duration(100)}
|
||||
// exiting={FadeOut.duration(100)}
|
||||
style={[
|
||||
style={flatten([
|
||||
a.relative,
|
||||
a.rounded_md,
|
||||
a.w_full,
|
||||
@@ -198,8 +188,14 @@ export function Inner({
|
||||
shadowRadius: 30,
|
||||
},
|
||||
flatten(style),
|
||||
]}>
|
||||
{children}
|
||||
])}>
|
||||
<DismissableLayer
|
||||
onInteractOutside={preventDefault}
|
||||
onFocusOutside={preventDefault}
|
||||
onDismiss={close}
|
||||
style={{display: 'flex', flexDirection: 'column'}}>
|
||||
{children}
|
||||
</DismissableLayer>
|
||||
</Animated.View>
|
||||
</FocusScope>
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
ItemTextProps,
|
||||
TriggerProps,
|
||||
} from '#/components/Menu/types'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export {
|
||||
@@ -77,9 +78,11 @@ export function Trigger({children, label}: TriggerProps) {
|
||||
export function Outer({
|
||||
children,
|
||||
showCancel,
|
||||
Portal,
|
||||
}: React.PropsWithChildren<{
|
||||
showCancel?: boolean
|
||||
style?: StyleProp<ViewStyle>
|
||||
Portal?: PortalComponent
|
||||
}>) {
|
||||
const context = React.useContext(Context)
|
||||
const {_} = useLingui()
|
||||
@@ -87,15 +90,15 @@ export function Outer({
|
||||
return (
|
||||
<Dialog.Outer
|
||||
control={context.control}
|
||||
nativeOptions={{preventExpansion: true}}>
|
||||
nativeOptions={{preventExpansion: true}}
|
||||
Portal={Portal}>
|
||||
<Dialog.Handle />
|
||||
{/* Re-wrap with context since Dialogs are portal-ed to root */}
|
||||
<Context.Provider value={context}>
|
||||
<Dialog.ScrollableInner label={_(msg`Menu`)} style={[a.pt_sm]}>
|
||||
<Dialog.ScrollableInner label={_(msg`Menu`)} style={[a.py_sm]}>
|
||||
<View style={[a.gap_lg]}>
|
||||
{children}
|
||||
{isNative && showCancel && <Cancel />}
|
||||
<View style={[{height: a.pb_lg.paddingBottom}]} />
|
||||
</View>
|
||||
</Dialog.ScrollableInner>
|
||||
</Context.Provider>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {Button, ButtonColor, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {BottomSheetViewProps} from '../../modules/bottom-sheet'
|
||||
|
||||
export {
|
||||
type DialogControlProps as PromptControlProps,
|
||||
@@ -27,10 +28,12 @@ export function Outer({
|
||||
control,
|
||||
testID,
|
||||
Portal,
|
||||
nativeOptions,
|
||||
}: React.PropsWithChildren<{
|
||||
control: Dialog.DialogControlProps
|
||||
testID?: string
|
||||
Portal?: PortalComponent
|
||||
nativeOptions?: Omit<BottomSheetViewProps, 'children'>
|
||||
}>) {
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const titleId = React.useId()
|
||||
@@ -42,7 +45,11 @@ export function Outer({
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control} testID={testID} Portal={Portal}>
|
||||
<Dialog.Outer
|
||||
control={control}
|
||||
testID={testID}
|
||||
Portal={Portal}
|
||||
nativeOptions={{preventExpansion: true, ...nativeOptions}}>
|
||||
<Dialog.Handle />
|
||||
<Context.Provider value={context}>
|
||||
<Dialog.ScrollableInner
|
||||
|
||||
@@ -23,14 +23,14 @@ import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||
import {ListMethods} from '#/view/com/util/List'
|
||||
import {atoms as a, ios, native, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {useThrottledValue} from '#/components/hooks/useThrottledValue'
|
||||
import {ArrowLeft_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
|
||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||
import {Button, ButtonIcon, ButtonText} from '../Button'
|
||||
import {ListFooter, ListMaybePlaceholder} from '../Lists'
|
||||
import {PortalComponent} from '../Portal'
|
||||
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
|
||||
export function GifSelectDialog({
|
||||
controlRef,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {useEffect} from 'react'
|
||||
|
||||
import {isWeb} from '#/platform/detection'
|
||||
|
||||
let refCount = 0
|
||||
|
||||
@@ -65,7 +65,6 @@ import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
import {emitPostCreated} from '#/state/events'
|
||||
import {ComposerImage, pasteImage} from '#/state/gallery'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useModals} from '#/state/modals'
|
||||
import {useRequireAltTextEnabled} from '#/state/preferences'
|
||||
import {
|
||||
toPostLanguages,
|
||||
@@ -146,7 +145,6 @@ export const ComposePost = ({
|
||||
const queryClient = useQueryClient()
|
||||
const currentDid = currentAccount!.did
|
||||
const {data: currentProfile} = useProfileQuery({did: currentDid})
|
||||
const {isModalActive} = useModals()
|
||||
const {closeComposer} = useComposerControls()
|
||||
const pal = usePalette('default')
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
@@ -303,22 +301,6 @@ export const ComposePost = ({
|
||||
}
|
||||
}, [onPressCancel, closeAllDialogs, closeAllModals])
|
||||
|
||||
// listen to escape key on desktop web
|
||||
const onEscape = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
onPressCancel()
|
||||
}
|
||||
},
|
||||
[onPressCancel],
|
||||
)
|
||||
useEffect(() => {
|
||||
if (isWeb && !isModalActive) {
|
||||
window.addEventListener('keydown', onEscape)
|
||||
return () => window.removeEventListener('keydown', onEscape)
|
||||
}
|
||||
}, [onEscape, isModalActive])
|
||||
|
||||
const onNewLink = useCallback((uri: string) => {
|
||||
dispatch({type: 'embed_add_uri', uri})
|
||||
}, [])
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
View,
|
||||
} from 'react-native'
|
||||
import Picker from '@emoji-mart/react'
|
||||
import {DismissableLayer} from '@radix-ui/react-dismissable-layer'
|
||||
|
||||
import {textInputWebEmitter} from '#/view/com/composer/text-input/textInputWebEmitter'
|
||||
import {atoms as a} from '#/alf'
|
||||
@@ -143,13 +144,17 @@ export function EmojiPicker({state, close, pinToTop}: IProps) {
|
||||
{/* eslint-disable-next-line react-native-a11y/has-valid-accessibility-descriptors */}
|
||||
<TouchableWithoutFeedback onPress={e => e.stopPropagation()}>
|
||||
<View style={[{position: 'absolute'}, position]}>
|
||||
<Picker
|
||||
data={async () => {
|
||||
return (await import('./EmojiPickerData.json')).default
|
||||
}}
|
||||
onEmojiSelect={onInsert}
|
||||
autoFocus={true}
|
||||
/>
|
||||
<DismissableLayer
|
||||
onFocusOutside={evt => evt.preventDefault()}
|
||||
onDismiss={close}>
|
||||
<Picker
|
||||
data={async () => {
|
||||
return (await import('./EmojiPickerData.json')).default
|
||||
}}
|
||||
onEmojiSelect={onInsert}
|
||||
autoFocus={true}
|
||||
/>
|
||||
</DismissableLayer>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {memo, useMemo} from 'react'
|
||||
import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {Image, Pressable, StyleSheet, View} from 'react-native'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import Svg, {Circle, Path, Rect} from 'react-native-svg'
|
||||
import {AppBskyActorDefs, ModerationUI} from '@atproto/api'
|
||||
@@ -30,6 +30,7 @@ import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||
import {Link} from '#/components/Link'
|
||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
||||
|
||||
@@ -50,6 +51,7 @@ interface UserAvatarProps extends BaseUserAvatarProps {
|
||||
|
||||
interface EditableUserAvatarProps extends BaseUserAvatarProps {
|
||||
onSelectNewAvatar: (img: RNImage | null) => void
|
||||
Portal?: PortalComponent
|
||||
}
|
||||
|
||||
interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
|
||||
@@ -266,6 +268,7 @@ let EditableUserAvatar = ({
|
||||
size,
|
||||
avatar,
|
||||
onSelectNewAvatar,
|
||||
Portal,
|
||||
}: EditableUserAvatarProps): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const pal = usePalette('default')
|
||||
@@ -346,10 +349,7 @@ let EditableUserAvatar = ({
|
||||
<Menu.Root>
|
||||
<Menu.Trigger label={_(msg`Edit avatar`)}>
|
||||
{({props}) => (
|
||||
<TouchableOpacity
|
||||
{...props}
|
||||
activeOpacity={0.8}
|
||||
testID="changeAvatarBtn">
|
||||
<Pressable {...props} testID="changeAvatarBtn">
|
||||
{avatar ? (
|
||||
<HighPriorityImage
|
||||
testID="userAvatarImage"
|
||||
@@ -363,10 +363,10 @@ let EditableUserAvatar = ({
|
||||
<View style={[styles.editButtonContainer, pal.btn]}>
|
||||
<CameraFilled height={14} width={14} style={t.atoms.text} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Pressable>
|
||||
)}
|
||||
</Menu.Trigger>
|
||||
<Menu.Outer showCancel>
|
||||
<Menu.Outer showCancel Portal={Portal}>
|
||||
<Menu.Group>
|
||||
{isNative && (
|
||||
<Menu.Item
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import {StyleSheet, TouchableOpacity, View} from 'react-native'
|
||||
import {Pressable, StyleSheet, View} from 'react-native'
|
||||
import {Image as RNImage} from 'react-native-image-crop-picker'
|
||||
import {Image} from 'expo-image'
|
||||
import {ModerationUI} from '@atproto/api'
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import {StreamingLive_Stroke2_Corner0_Rounded as Library} from '#/components/icons/StreamingLive'
|
||||
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {PortalComponent} from '#/components/Portal'
|
||||
import {openCamera, openCropper, openPicker} from '../../../lib/media/picker'
|
||||
|
||||
export function UserBanner({
|
||||
@@ -32,11 +33,13 @@ export function UserBanner({
|
||||
banner,
|
||||
moderation,
|
||||
onSelectNewBanner,
|
||||
Portal,
|
||||
}: {
|
||||
type?: 'labeler' | 'default'
|
||||
banner?: string | null
|
||||
moderation?: ModerationUI
|
||||
onSelectNewBanner?: (img: RNImage | null) => void
|
||||
Portal?: PortalComponent
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
@@ -90,14 +93,11 @@ export function UserBanner({
|
||||
|
||||
// setUserBanner is only passed as prop on the EditProfile component
|
||||
return onSelectNewBanner ? (
|
||||
<EventStopper onKeyDown={false}>
|
||||
<EventStopper onKeyDown={true}>
|
||||
<Menu.Root>
|
||||
<Menu.Trigger label={_(msg`Edit avatar`)}>
|
||||
{({props}) => (
|
||||
<TouchableOpacity
|
||||
{...props}
|
||||
activeOpacity={0.8}
|
||||
testID="changeBannerBtn">
|
||||
<Pressable {...props} testID="changeBannerBtn">
|
||||
{banner ? (
|
||||
<Image
|
||||
testID="userBannerImage"
|
||||
@@ -115,10 +115,10 @@ export function UserBanner({
|
||||
<View style={[styles.editButtonContainer, pal.btn]}>
|
||||
<CameraFilled height={14} width={14} style={t.atoms.text} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Pressable>
|
||||
)}
|
||||
</Menu.Trigger>
|
||||
<Menu.Outer showCancel>
|
||||
<Menu.Outer showCancel Portal={Portal}>
|
||||
<Menu.Group>
|
||||
{isNative && (
|
||||
<Menu.Item
|
||||
|
||||
@@ -7,14 +7,19 @@ import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {createPortalGroup} from '#/components/Portal'
|
||||
import * as Prompt from '#/components/Prompt'
|
||||
import {H3, P, Text} from '#/components/Typography'
|
||||
import {PlatformInfo} from '../../../../modules/expo-bluesky-swiss-army'
|
||||
|
||||
const Portal = createPortalGroup()
|
||||
|
||||
export function Dialogs() {
|
||||
const scrollable = Dialog.useDialogControl()
|
||||
const basic = Dialog.useDialogControl()
|
||||
const prompt = Prompt.usePromptControl()
|
||||
const withMenu = Dialog.useDialogControl()
|
||||
const testDialog = Dialog.useDialogControl()
|
||||
const {closeAllDialogs} = useDialogStateControlContext()
|
||||
const unmountTestDialog = Dialog.useDialogControl()
|
||||
@@ -68,6 +73,7 @@ export function Dialogs() {
|
||||
scrollable.open()
|
||||
prompt.open()
|
||||
basic.open()
|
||||
withMenu.open()
|
||||
}}
|
||||
label="Open basic dialog">
|
||||
<ButtonText>Open all dialogs</ButtonText>
|
||||
@@ -95,6 +101,15 @@ export function Dialogs() {
|
||||
<ButtonText>Open basic dialog</ButtonText>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
color="primary"
|
||||
size="small"
|
||||
onPress={() => withMenu.open()}
|
||||
label="Open dialog with menu in it">
|
||||
<ButtonText>Open dialog with menu in it</ButtonText>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
@@ -185,6 +200,44 @@ export function Dialogs() {
|
||||
</Dialog.Inner>
|
||||
</Dialog.Outer>
|
||||
|
||||
<Dialog.Outer control={withMenu}>
|
||||
<Portal.Provider>
|
||||
<Dialog.Inner label="test">
|
||||
<H3 nativeID="dialog-title">Dialog with Menu</H3>
|
||||
<Menu.Root>
|
||||
<Menu.Trigger label="Open menu">
|
||||
{({props}) => (
|
||||
<Button
|
||||
style={a.mt_2xl}
|
||||
label="Open menu"
|
||||
color="primary"
|
||||
variant="solid"
|
||||
size="large"
|
||||
{...props}>
|
||||
<ButtonText>Open Menu</ButtonText>
|
||||
</Button>
|
||||
)}
|
||||
</Menu.Trigger>
|
||||
<Menu.Outer Portal={Portal.Portal}>
|
||||
<Menu.Group>
|
||||
<Menu.Item
|
||||
label="Item 1"
|
||||
onPress={() => console.log('item 1')}>
|
||||
<Menu.ItemText>Item 1</Menu.ItemText>
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
label="Item 2"
|
||||
onPress={() => console.log('item 2')}>
|
||||
<Menu.ItemText>Item 2</Menu.ItemText>
|
||||
</Menu.Item>
|
||||
</Menu.Group>
|
||||
</Menu.Outer>
|
||||
</Menu.Root>
|
||||
</Dialog.Inner>
|
||||
<Portal.Outlet />
|
||||
</Portal.Provider>
|
||||
</Dialog.Outer>
|
||||
|
||||
<Dialog.Outer control={scrollable}>
|
||||
<Dialog.ScrollableInner
|
||||
accessibilityDescribedBy="dialog-description"
|
||||
|
||||
@@ -2,9 +2,9 @@ import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {Text} from '#/components/Typography'
|
||||
// import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
|
||||
export function Menus() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user