Fix dropdown immediately closing on Enter (#3745)
* Move dropdown content into separate component * Fix dropdown with keyboard * No-op is sufficientzio/stable
parent
1dd3d6657c
commit
2a08931127
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue