Fix dropdown immediately closing on Enter (#3745)

* Move dropdown content into separate component

* Fix dropdown with keyboard

* No-op is sufficient
zio/stable
dan 2024-04-28 21:29:43 +01:00 committed by GitHub
parent 1dd3d6657c
commit 2a08931127
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 111 additions and 76 deletions

View File

@ -1,27 +1,26 @@
/* eslint-disable react/prop-types */ /* eslint-disable react/prop-types */
import React from 'react' import React from 'react'
import {View, Pressable, ViewStyle, StyleProp} from 'react-native' import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import {atoms as a, flatten, useTheme, web} from '#/alf'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {useInteractionState} from '#/components/hooks/useInteractionState' import {useInteractionState} from '#/components/hooks/useInteractionState'
import {atoms as a, useTheme, flatten, web} from '#/alf' import {Context} from '#/components/Menu/context'
import {Text} from '#/components/Typography'
import { import {
ContextType, ContextType,
TriggerProps,
ItemProps,
GroupProps, GroupProps,
ItemTextProps,
ItemIconProps, ItemIconProps,
ItemProps,
ItemTextProps,
RadixPassThroughTriggerProps, RadixPassThroughTriggerProps,
TriggerProps,
} from '#/components/Menu/types' } from '#/components/Menu/types'
import {Context} from '#/components/Menu/context'
import {Portal} from '#/components/Portal' import {Portal} from '#/components/Portal'
import {Text} from '#/components/Typography'
export function useMenuControl(): Dialog.DialogControlProps { export function useMenuControl(): Dialog.DialogControlProps {
const id = React.useId() const id = React.useId()
@ -135,10 +134,22 @@ export function Trigger({children, label}: TriggerProps) {
}, },
props: { props: {
...props, ...props,
// disable on web, use `onPress` // No-op override to prevent false positive that interprets mobile scroll as a tap.
onPointerDown: () => false, // This requires the custom onPress handler below to compensate.
onPress: () => // https://github.com/radix-ui/primitives/issues/1912
control.isOpen ? control.close() : control.open(), onPointerDown: undefined,
onPress: () => {
if (window.event instanceof KeyboardEvent) {
// The onPointerDown hack above is not relevant to this press, so don't do anything.
return
}
// Compensate for the disabled onPointerDown above by triggering it manually.
if (control.isOpen) {
control.close()
} else {
control.open()
}
},
onFocus: onFocus, onFocus: onFocus,
onBlur: onBlur, onBlur: onBlur,
onMouseEnter, onMouseEnter,

View File

@ -1,12 +1,13 @@
import React from 'react' import React from 'react'
import {Pressable, StyleSheet, Text, View, ViewStyle} from 'react-native'
import {IconProp} from '@fortawesome/fontawesome-svg-core'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import {Pressable, StyleSheet, View, Text, ViewStyle} from 'react-native'
import {IconProp} from '@fortawesome/fontawesome-svg-core'
import {MenuItemCommonProps} from 'zeego/lib/typescript/menu' import {MenuItemCommonProps} from 'zeego/lib/typescript/menu'
import {HITSLOP_10} from 'lib/constants'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {HITSLOP_10} from 'lib/constants'
// Custom Dropdown Menu Components // Custom Dropdown Menu Components
// == // ==
@ -64,15 +65,9 @@ export function NativeDropdown({
accessibilityHint, accessibilityHint,
triggerStyle, triggerStyle,
}: React.PropsWithChildren<Props>) { }: React.PropsWithChildren<Props>) {
const pal = usePalette('default')
const theme = useTheme()
const dropDownBackgroundColor =
theme.colorScheme === 'dark' ? pal.btn : pal.view
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false)
const buttonRef = React.useRef<HTMLButtonElement>(null) const buttonRef = React.useRef<HTMLButtonElement>(null)
const menuRef = React.useRef<HTMLDivElement>(null) const menuRef = React.useRef<HTMLDivElement>(null)
const {borderColor: separatorColor} =
theme.colorScheme === 'dark' ? pal.borderDark : pal.border
React.useEffect(() => { React.useEffect(() => {
function clickHandler(e: MouseEvent) { function clickHandler(e: MouseEvent) {
@ -114,14 +109,27 @@ export function NativeDropdown({
return ( return (
<DropdownMenuRoot open={open} onOpenChange={o => setOpen(o)}> <DropdownMenuRoot open={open} onOpenChange={o => setOpen(o)}>
<DropdownMenu.Trigger asChild onPointerDown={e => e.preventDefault()}> <DropdownMenu.Trigger asChild>
<Pressable <Pressable
ref={buttonRef as unknown as React.Ref<View>} ref={buttonRef as unknown as React.Ref<View>}
testID={testID} testID={testID}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={accessibilityLabel} accessibilityLabel={accessibilityLabel}
accessibilityHint={accessibilityHint} accessibilityHint={accessibilityHint}
onPress={() => setOpen(o => !o)} onPointerDown={e => {
// Prevent false positive that interpret mobile scroll as a tap.
// This requires the custom onPress handler below to compensate.
// https://github.com/radix-ui/primitives/issues/1912
e.preventDefault()
}}
onPress={() => {
if (window.event instanceof KeyboardEvent) {
// The onPointerDown hack above is not relevant to this press, so don't do anything.
return
}
// Compensate for the disabled onPointerDown above by triggering it manually.
setOpen(o => !o)
}}
hitSlop={HITSLOP_10} hitSlop={HITSLOP_10}
style={triggerStyle}> style={triggerStyle}>
{children} {children}
@ -129,53 +137,53 @@ export function NativeDropdown({
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Portal> <DropdownMenu.Portal>
<DropdownMenu.Content <DropdownContent items={items} menuRef={menuRef} />
ref={menuRef} </DropdownMenu.Portal>
style={ </DropdownMenuRoot>
StyleSheet.flatten([ )
styles.content, }
dropDownBackgroundColor,
]) as React.CSSProperties function DropdownContent({
} items,
loop> menuRef,
{items.map((item, index) => { }: {
if (item.label === 'separator') { items: DropdownItem[]
return ( menuRef: React.RefObject<HTMLDivElement>
<DropdownMenu.Separator }) {
key={getKey(item.label, index, item.testID)} const pal = usePalette('default')
style={ const theme = useTheme()
StyleSheet.flatten([ const dropDownBackgroundColor =
styles.separator, theme.colorScheme === 'dark' ? pal.btn : pal.view
{backgroundColor: separatorColor}, const {borderColor: separatorColor} =
]) as React.CSSProperties theme.colorScheme === 'dark' ? pal.borderDark : pal.border
}
/> return (
) <DropdownMenu.Content
} ref={menuRef}
if (index > 1 && items[index - 1].label === 'separator') { style={
return ( StyleSheet.flatten([
<DropdownMenu.Group styles.content,
key={getKey(item.label, index, item.testID)}> dropDownBackgroundColor,
<DropdownMenuItem ]) as React.CSSProperties
key={getKey(item.label, index, item.testID)} }
onSelect={item.onPress}> loop>
<Text {items.map((item, index) => {
selectable={false} if (item.label === 'separator') {
style={[pal.text, styles.itemTitle]}> return (
{item.label} <DropdownMenu.Separator
</Text> key={getKey(item.label, index, item.testID)}
{item.icon && ( style={
<FontAwesomeIcon StyleSheet.flatten([
icon={item.icon.web} styles.separator,
size={20} {backgroundColor: separatorColor},
color={pal.colors.textLight} ]) as React.CSSProperties
/> }
)} />
</DropdownMenuItem> )
</DropdownMenu.Group> }
) if (index > 1 && items[index - 1].label === 'separator') {
} return (
return ( <DropdownMenu.Group key={getKey(item.label, index, item.testID)}>
<DropdownMenuItem <DropdownMenuItem
key={getKey(item.label, index, item.testID)} key={getKey(item.label, index, item.testID)}
onSelect={item.onPress}> onSelect={item.onPress}>
@ -190,11 +198,27 @@ export function NativeDropdown({
/> />
)} )}
</DropdownMenuItem> </DropdownMenuItem>
) </DropdownMenu.Group>
})} )
</DropdownMenu.Content> }
</DropdownMenu.Portal> return (
</DropdownMenuRoot> <DropdownMenuItem
key={getKey(item.label, index, item.testID)}
onSelect={item.onPress}>
<Text selectable={false} style={[pal.text, styles.itemTitle]}>
{item.label}
</Text>
{item.icon && (
<FontAwesomeIcon
icon={item.icon.web}
size={20}
color={pal.colors.textLight}
/>
)}
</DropdownMenuItem>
)
})}
</DropdownMenu.Content>
) )
} }