import { cloneElement, Fragment, isValidElement, useCallback, useId, useMemo, useState, } from 'react' import { Pressable, type StyleProp, type TextStyle, View, type ViewStyle, } from 'react-native' import {Trans, useLingui} from '@lingui/react/macro' import flattenReactChildren from 'react-keyed-flatten-children' import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {useInteractionState} from '#/components/hooks/useInteractionState' import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeftIcon} from '#/components/icons/Chevron' import { Context, ItemContext, useMenuContext, useMenuItemContext, } from '#/components/Menu/context' import { type ContextType, type GroupProps, type ItemIconProps, type ItemProps, type ItemTextProps, type SubmenuProps, type TriggerProps, } from '#/components/Menu/types' import {Text} from '#/components/Typography' import {IS_ANDROID, IS_IOS, IS_NATIVE} from '#/env' // iOS 26's floaty sheet presentation subtracts the bottom safe-area inset from // the requested detent, which eats the visible bottom padding of short (e.g. // single-item) menus. Flooring the native sheet height restores that padding. const IOS_MENU_MIN_HEIGHT = 128 export { type DialogControlProps as MenuControlProps, useDialogControl as useMenuControl, } from '#/components/Dialog' export {useMenuContext} export function Root({ children, control, modal: _modal = true, disableBackdrop: _disableBackdrop = false, dismissGuardRef: _dismissGuardRef, }: React.PropsWithChildren<{ control?: Dialog.DialogControlProps modal?: boolean disableBackdrop?: boolean dismissGuardRef?: React.MutableRefObject }>) { const defaultControl = Dialog.useDialogControl() const [activeSubmenuId, setActiveSubmenuId] = useState(null) const submenuRegistry = useMemo< NonNullable['registry'] >(() => new Map(), []) const openSubmenu = useCallback((id: string) => { setActiveSubmenuId(id) }, []) const closeSubmenu = useCallback(() => { setActiveSubmenuId(null) }, []) const context = useMemo( () => ({ control: control || defaultControl, nativeSubmenu: { activeId: activeSubmenuId, registry: submenuRegistry, open: openSubmenu, close: closeSubmenu, }, }), [ control, defaultControl, activeSubmenuId, submenuRegistry, openSubmenu, closeSubmenu, ], ) return {children} } export function Trigger({ children, label, role = 'button', hint, }: TriggerProps) { const context = useMenuContext() const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() const { state: pressed, onIn: onPressIn, onOut: onPressOut, } = useInteractionState() return children({ IS_NATIVE: true, control: context.control, state: { hovered: false, focused, pressed, }, props: { ref: null, onPress: context.control.open, onFocus, onBlur, onPressIn, onPressOut, accessibilityHint: hint, accessibilityLabel: label, accessibilityRole: role, }, }) } export function Outer({ children, showCancel, }: React.PropsWithChildren<{ showCancel?: boolean style?: StyleProp onCloseAutoFocus?: (event: Event) => void }>) { const context = useMenuContext() const {t: l} = useLingui() return ( {/* Re-wrap with context since Dialogs are portal-ed to root */} {children} {IS_NATIVE && showCancel && } ) } function NativeSubmenuContent() { const t = useTheme() const {t: l} = useLingui() const context = useMenuContext() const activeId = context.nativeSubmenu?.activeId const submenu = activeId ? context.nativeSubmenu?.registry.get(activeId) : undefined if (!submenu) { return null } return ( {submenu.label} {l`Back`} {submenu.children} ) } export function Item({ children, label, style, onPress, destructive = false, ...rest }: ItemProps) { const t = useTheme() const context = useMenuContext() const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() const { state: pressed, onIn: onPressIn, onOut: onPressOut, } = useInteractionState() return ( { if (IS_ANDROID) { /** * Below fix for iOS doesn't work for Android, this does. */ onPress?.(e) context.control.close() } else if (IS_IOS) { /** * Fixes a subtle bug on iOS * {@link https://github.com/bluesky-social/social-app/pull/5849/files#diff-de516ef5e7bd9840cd639213301df38cf03acfcad5bda85a1d63efd249ba79deL124-L127} */ context.control.close(() => { onPress?.(e) }) } }} onPressIn={e => { onPressIn() rest.onPressIn?.(e) }} onPressOut={e => { onPressOut() rest.onPressOut?.(e) }} style={[ a.flex_row, a.align_center, a.gap_sm, a.px_md, a.rounded_md, a.overflow_hidden, a.border, t.atoms.bg_contrast_25, t.atoms.border_contrast_low, {minHeight: 44, paddingVertical: 10}, style, (focused || pressed) && !rest.disabled && [t.atoms.bg_contrast_50], ]}> {children} ) } /** * A nested menu. On native, submenus replace the contents of the current sheet * and provide a back action; on web the Radix implementation presents a * conventional hover/keyboard submenu. */ export function Submenu({children, label, trigger, style}: SubmenuProps) { const t = useTheme() const context = useMenuContext() const id = useId() context.nativeSubmenu?.registry.set(id, {label, children}) const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() const { state: pressed, onIn: onPressIn, onOut: onPressOut, } = useInteractionState() return ( <> context.nativeSubmenu?.open(id)} onPressIn={onPressIn} onPressOut={onPressOut} style={[ a.flex_row, a.align_center, a.gap_sm, a.px_md, a.rounded_md, a.overflow_hidden, a.border, t.atoms.bg_contrast_25, t.atoms.border_contrast_low, {minHeight: 44, paddingVertical: 10}, style, (focused || pressed) && t.atoms.bg_contrast_50, ]}> {trigger} ) } export function ItemText({children, style}: ItemTextProps) { const t = useTheme() const {disabled, destructive} = useMenuItemContext() return ( {children} ) } export function ItemIcon({icon: Comp, 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} ) } /** * NATIVE ONLY - for adding non-pressable items to the menu * * @platform ios, android */ export function ContainerItem({ children, style, }: { children: React.ReactNode style?: StyleProp }) { const t = useTheme() return ( {children} ) } export function LabelText({ children, style, }: { children: React.ReactNode style?: StyleProp }) { const t = useTheme() return ( {children} ) } export function Group({children, style}: GroupProps) { const t = useTheme() return ( {flattenReactChildren(children).map((child, i) => { return isValidElement(child) && (child.type === Item || child.type === ContainerItem || child.type === Submenu) ? ( {i > 0 ? ( ) : null} {cloneElement(child, { // @ts-expect-error cloneElement is not aware of the types style: { borderRadius: 0, borderWidth: 0, }, })} ) : null })} ) } function Cancel() { const {t: l} = useLingui() const context = useMenuContext() return ( ) } export function Divider() { return null }