From f3d564449d371fa37cb946fecb59c5d58023e808 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 8 Oct 2024 05:22:56 -0500 Subject: [PATCH] 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 --- package.json | 10 +- src/components/Dialog/index.web.tsx | 32 +- src/view/com/util/UserAvatar.tsx | 9 +- src/view/com/util/UserBanner.tsx | 11 +- src/view/screens/Storybook/Dialogs.tsx | 54 +++ src/view/screens/Storybook/Menus.tsx | 4 +- yarn.lock | 506 ++++++++++--------------- 7 files changed, 279 insertions(+), 347 deletions(-) diff --git a/package.json b/package.json index 9b56efcaf51..906b24c812a 100644 --- a/package.json +++ b/package.json @@ -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", @@ -278,7 +281,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", diff --git a/src/components/Dialog/index.web.tsx b/src/components/Dialog/index.web.tsx index 7b9cfb69318..576bc8f415f 100644 --- a/src/components/Dialog/index.web.tsx +++ b/src/components/Dialog/index.web.tsx @@ -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 ( - + - {children} + ])}> + + {children} + ) diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 43555ccb47c..dbd68f8ef53 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -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' @@ -346,10 +346,7 @@ let EditableUserAvatar = ({ {({props}) => ( - + {avatar ? ( - + )} diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index 622cb2129d1..2815de3bd87 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -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' @@ -90,14 +90,11 @@ export function UserBanner({ // setUserBanner is only passed as prop on the EditProfile component return onSelectNewBanner ? ( - + {({props}) => ( - + {banner ? ( - + )} diff --git a/src/view/screens/Storybook/Dialogs.tsx b/src/view/screens/Storybook/Dialogs.tsx index a0a2a275510..08c679428d5 100644 --- a/src/view/screens/Storybook/Dialogs.tsx +++ b/src/view/screens/Storybook/Dialogs.tsx @@ -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"> Open all dialogs @@ -95,6 +101,15 @@ export function Dialogs() { Open basic dialog + + + )} + + + + console.log('item 1')}> + Item 1 + + console.log('item 2')}> + Item 2 + + + + + + + + + +