Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move Dialogs to Radix #5648

Merged
merged 6 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@
"@lingui/react": "^4.5.0",
"@mattermost/react-native-paste-input": "^0.7.1",
"@miblanchard/react-native-slider": "^2.3.1",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-dismissable-layer": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-focus-guards": "^1.1.1",
"@radix-ui/react-focus-scope": "^1.1.0",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-masked-view/masked-view": "0.3.0",
"@react-native-menu/menu": "^1.1.0",
Expand All @@ -82,7 +85,6 @@
"@react-navigation/native": "^6.1.17",
"@react-navigation/native-stack": "^6.9.26",
"@sentry/react-native": "5.24.3",
"@tamagui/focus-scope": "^1.84.1",
"@tanstack/query-async-storage-persister": "^5.25.0",
"@tanstack/react-query": "^5.8.1",
"@tanstack/react-query-persist-client": "^5.25.0",
Expand Down Expand Up @@ -278,7 +280,10 @@
"**/zod": "3.23.8",
"**/expo-constants": "16.0.1",
"**/expo-device": "6.0.2",
"@react-native/babel-preset": "0.74.1"
"@react-native/babel-preset": "0.74.1",
"@radix-ui/react-dropdown-menu": "2.1.2",
"@radix-ui/react-context-menu": "2.2.2",
"@radix-ui/react-focus-scope": "1.1.0"
},
"jest": {
"preset": "jest-expo/ios",
Expand Down
36 changes: 16 additions & 20 deletions src/components/Dialog/index.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Copy link
Contributor

@haileyok haileyok Oct 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove this library now?

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'
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"
Expand All @@ -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,
Expand All @@ -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>
)
Expand Down
9 changes: 6 additions & 3 deletions src/components/Menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ItemTextProps,
TriggerProps,
} from '#/components/Menu/types'
import {PortalComponent} from '#/components/Portal'
import {Text} from '#/components/Typography'

export {
Expand Down Expand Up @@ -77,25 +78,27 @@ 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()

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>
Expand Down
9 changes: 8 additions & 1 deletion src/components/Prompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions src/components/dialogs/GifSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/lib/hooks/useWebBodyScrollLock.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {useEffect} from 'react'

import {isWeb} from '#/platform/detection'

let refCount = 0
Expand Down
18 changes: 0 additions & 18 deletions src/view/com/composer/Composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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})
}, [])
Expand Down
19 changes: 12 additions & 7 deletions src/view/com/composer/text-input/web/EmojiPicker.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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>
Expand Down
14 changes: 7 additions & 7 deletions src/view/com/util/UserAvatar.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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'

Expand All @@ -50,6 +51,7 @@ interface UserAvatarProps extends BaseUserAvatarProps {

interface EditableUserAvatarProps extends BaseUserAvatarProps {
onSelectNewAvatar: (img: RNImage | null) => void
Portal?: PortalComponent
}

interface PreviewableUserAvatarProps extends BaseUserAvatarProps {
Expand Down Expand Up @@ -266,6 +268,7 @@ let EditableUserAvatar = ({
size,
avatar,
onSelectNewAvatar,
Portal,
}: EditableUserAvatarProps): React.ReactNode => {
const t = useTheme()
const pal = usePalette('default')
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand Down
16 changes: 8 additions & 8 deletions src/view/com/util/UserBanner.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -25,18 +25,21 @@ 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({
type,
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()
Expand Down Expand Up @@ -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}>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we even need this anymore

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh cool! I'll follow up with a PR to remove those, wanna test thoroughly first so won't do it here

<Menu.Root>
<Menu.Trigger label={_(msg`Edit avatar`)}>
{({props}) => (
<TouchableOpacity
{...props}
activeOpacity={0.8}
testID="changeBannerBtn">
<Pressable {...props} testID="changeBannerBtn">
{banner ? (
<Image
testID="userBannerImage"
Expand All @@ -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
Expand Down
Loading
Loading