Skip to content

Commit

Permalink
Use Redix FocusTrap (#5638)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
estrattonbailey and mozzius committed Oct 8, 2024
1 parent efcf8a6 commit f3d5644
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 347 deletions.
10 changes: 8 additions & 2 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 Down Expand Up @@ -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",
Expand Down
32 changes: 12 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'
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,10 @@ export function Inner({
shadowRadius: 30,
},
flatten(style),
]}>
{children}
])}>
<DismissableLayer onFocusOutside={preventDefault} onDismiss={close}>
{children}
</DismissableLayer>
</Animated.View>
</FocusScope>
)
Expand Down
9 changes: 3 additions & 6 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 @@ -346,10 +346,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,7 +360,7 @@ 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>
Expand Down
11 changes: 4 additions & 7 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 Down Expand Up @@ -90,14 +90,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"
Expand All @@ -115,7 +112,7 @@ 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>
Expand Down
54 changes: 54 additions & 0 deletions src/view/screens/Storybook/Dialogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -68,6 +73,7 @@ export function Dialogs() {
scrollable.open()
prompt.open()
basic.open()
withMenu.open()
}}
label="Open basic dialog">
<ButtonText>Open all dialogs</ButtonText>
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -185,6 +200,45 @@ export function Dialogs() {
</Dialog.Inner>
</Dialog.Outer>

<Dialog.Outer control={withMenu}>
<Dialog.Inner label="test">
<Portal.Provider>
<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>

<Portal.Outlet />
</Portal.Provider>
</Dialog.Inner>
</Dialog.Outer>

<Dialog.Outer control={scrollable}>
<Dialog.ScrollableInner
accessibilityDescribedBy="dialog-description"
Expand Down
4 changes: 2 additions & 2 deletions src/view/screens/Storybook/Menus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading

0 comments on commit f3d5644

Please sign in to comment.