import {forwardRef, useCallback, useId, useMemo, useRef, useState} from 'react' import { Pressable, type StyleProp, type TextStyle, View, type ViewStyle, } from 'react-native' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {DropdownMenu} from 'radix-ui' import {userStyle} from '#/lib/userstyles' import {useA11y} from '#/state/a11y' import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' import {atoms as a, flatten, flattenToCSS, useTheme, web} from '#/alf' import type * as Dialog from '#/components/Dialog' import {useInteractionState} from '#/components/hooks/useInteractionState' import { Context, ItemContext, useMenuContext, useMenuItemContext, } from '#/components/Menu/context' import { type ContextType, type GroupProps, type ItemIconProps, type ItemProps, type ItemTextProps, type RadixPassThroughTriggerProps, type SubmenuProps, type TriggerProps, } from '#/components/Menu/types' import {Portal} from '#/components/Portal' import {Text} from '#/components/Typography' export {type DialogControlProps as MenuControlProps} from '#/components/Dialog' export {useMenuContext} export function useMenuControl(): Dialog.DialogControlProps { const id = useId() const [isOpen, setIsOpen] = useState(false) const open = useCallback(() => setIsOpen(true), []) const close = useCallback(() => setIsOpen(false), []) return useMemo( () => ({ id, ref: {current: null}, isOpen, open, close, }), [id, isOpen, open, close], ) } export function Root({ children, control, modal = true, disableBackdrop = false, dismissGuardRef, }: React.PropsWithChildren<{ control?: Dialog.DialogControlProps modal?: boolean disableBackdrop?: boolean dismissGuardRef?: React.MutableRefObject }>) { const {_} = useLingui() const defaultControl = useMenuControl() const context = useMemo( () => ({ control: control || defaultControl, }), [control, defaultControl], ) const onOpenChange = useCallback( (open: boolean) => { if (!open && dismissGuardRef?.current) { return } if (open === context.control.isOpen) { return } if (open) { context.control.open() } else { context.control.close() } }, [context.control, dismissGuardRef], ) return ( {modal && !disableBackdrop && context.control.isOpen && ( context.control.close()} accessibilityHint="" accessibilityLabel={_( msg`Context menu backdrop, click to close the menu.`, )} /> )} {children} ) } const RadixTriggerPassThrough = forwardRef( ( props: { children: ( props: RadixPassThroughTriggerProps & { ref: React.Ref }, ) => React.ReactNode }, ref, ) => { // @ts-expect-error Radix provides no types of this stuff return props.children({...props, ref}) }, ) RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough' export function Trigger({ children, label, role = 'button', hint, }: TriggerProps) { const {control} = useMenuContext() const { state: hovered, onIn: onMouseEnter, onOut: onMouseLeave, } = useInteractionState() const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() return ( {props => children({ IS_NATIVE: false, control, state: { hovered, focused, pressed: false, }, props: { ...props, // No-op override to prevent false positive that interprets mobile scroll as a tap. // This requires the custom onPress handler below to compensate. // https://github.com/radix-ui/primitives/issues/1912 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, onBlur: onBlur, onMouseEnter, onMouseLeave, accessibilityHint: hint, accessibilityLabel: label, accessibilityRole: role, }, }) } ) } export function Outer({ children, style, onCloseAutoFocus, side, align, label, }: React.PropsWithChildren<{ showCancel?: boolean side?: React.ComponentProps['side'] align?: React.ComponentProps['align'] label?: string style?: StyleProp onCloseAutoFocus?: React.ComponentProps< typeof DropdownMenu.Content >['onCloseAutoFocus'] }>) { const t = useTheme() const {reduceMotionEnabled} = useA11y() return ( {children} {/* Disabled until we can fix positioning */} ) } export function Item({ children, label, onPress, style, destructive = false, ...rest }: ItemProps) { const t = useTheme() const {control} = useMenuContext() const { state: hovered, onIn: onMouseEnter, onOut: onMouseLeave, } = useInteractionState() const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() return ( { const element = node as unknown as HTMLElement | null element?.classList.add('wsky-menu__item') }} className="radix-dropdown-item" accessibilityHint="" accessibilityLabel={label} onPress={e => { onPress(e) /** * Ported forward from Radix * @see https://www.radix-ui.com/primitives/docs/components/dropdown-menu#item */ if (!e.defaultPrevented) { control.close() } }} onFocus={onFocus} onBlur={onBlur} // need `flatten` here for Radix compat style={flatten([ a.flex_row, a.align_center, a.gap_lg, a.py_sm, a.rounded_xs, a.overflow_hidden, {minHeight: 32, paddingHorizontal: 10}, web({outline: 0}), (hovered || focused) && !rest.disabled && [ web({outline: '0 !important'}), t.name === 'light' ? t.atoms.bg_contrast_25 : t.atoms.bg_contrast_50, ], style, ])} {...web({ onMouseEnter, onMouseLeave, })}> {children} ) } /** A conventional hover- and keyboard-accessible web submenu. */ export function Submenu({children, label, trigger, style}: SubmenuProps) { const t = useTheme() const {reduceMotionEnabled} = useA11y() const [open, setOpen] = useState(false) const triggerRef = useRef>(null) const { state: hovered, onIn: onMouseEnter, onOut: onMouseLeave, } = useInteractionState() const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() return ( { triggerRef.current = node const element = node as unknown as HTMLElement | null element?.classList.add('wsky-menu__item') }} className="radix-dropdown-item" accessibilityHint="" accessibilityLabel={label} onPress={() => setOpen(true)} onKeyDown={(event: React.KeyboardEvent) => { if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return const current = event.currentTarget const parentMenu = current.closest('[role="menu"]') if (!parentMenu) return const items = Array.from( parentMenu.querySelectorAll( '[role="menuitem"], [role="menuitemcheckbox"], [role="menuitemradio"]', ), ).filter(item => item.getAttribute('aria-disabled') !== 'true') const currentIndex = items.indexOf(current) if (currentIndex === -1) return event.preventDefault() const offset = event.key === 'ArrowDown' ? 1 : -1 const nextIndex = (currentIndex + offset + items.length) % items.length items[nextIndex]?.focus() }} onFocus={onFocus} onBlur={onBlur} style={flatten([ a.flex_row, a.align_center, a.gap_lg, a.py_sm, a.rounded_xs, a.overflow_hidden, {minHeight: 32, paddingHorizontal: 10}, web({outline: 0}), (hovered || focused) && [ web({outline: '0 !important'}), t.name === 'light' ? t.atoms.bg_contrast_25 : t.atoms.bg_contrast_50, ], style, ])} {...web({onMouseEnter, onMouseLeave})}> {trigger} { const closeKey = document.documentElement.dir === 'rtl' ? 'ArrowRight' : 'ArrowLeft' if (event.key !== closeKey) return // Radix normally focuses the trigger before the submenu has // finished closing. Re-focus it after unmount so the parent // menu's roving-focus position is restored to this item. event.preventDefault() setOpen(false) window.requestAnimationFrame(() => { const trigger = triggerRef.current as unknown as HTMLElement trigger?.focus() }) }} className="dropdown-menu-transform-origin dropdown-menu-constrain-size"> {children} ) } export function ItemText({children, style}: ItemTextProps) { const t = useTheme() const {disabled, destructive} = useMenuItemContext() return ( {children} ) } export function ItemIcon({icon: Comp, position = 'left', fill}: ItemIconProps) { const t = useTheme() const {disabled, destructive} = useMenuItemContext() return ( ) } export function ItemRadio({selected}: {selected: boolean}) { const t = useTheme() const enableSquareButtons = useEnableSquareButtons() return ( {selected ? ( ) : null} ) } export function LabelText({ children, style, }: { children: React.ReactNode style?: StyleProp }) { const t = useTheme() return ( {children} ) } export function Group({children}: GroupProps) { return children } export function Divider() { const t = useTheme() return ( ) } export function ContainerItem(_props: {children?: React.ReactNode}) { return null }