From 317e0cda7a30d21f35229c096b6ef3284819d19a Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 5 Mar 2024 21:15:42 -0600 Subject: Add `Menu` component (#3097) * Add POC menu abstraction * Better platform handling * Remove ignore * Add some menu items * Add controlled dropdown * Pass through a11y props * Ignore uninitialized context * Tweaks * Usability improvements * Rename handlers to props * Add radix comment * Ignore known type * Remove todo * Move storybook item * Improve Group matching * Adjust theming --- src/components/Menu/index.web.tsx | 247 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 src/components/Menu/index.web.tsx (limited to 'src/components/Menu/index.web.tsx') diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx new file mode 100644 index 000000000..ca2e40566 --- /dev/null +++ b/src/components/Menu/index.web.tsx @@ -0,0 +1,247 @@ +import React from 'react' +import {View, Pressable} from 'react-native' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' + +import * as Dialog from '#/components/Dialog' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import {atoms as a, useTheme, flatten, web} from '#/alf' +import {Text} from '#/components/Typography' + +import { + ContextType, + TriggerProps, + ItemProps, + GroupProps, + ItemTextProps, + ItemIconProps, +} from '#/components/Menu/types' +import {Context} from '#/components/Menu/context' + +export function useMenuControl(): Dialog.DialogControlProps { + const id = React.useId() + const [isOpen, setIsOpen] = React.useState(false) + + return React.useMemo( + () => ({ + id, + ref: {current: null}, + isOpen, + open() { + setIsOpen(true) + }, + close() { + setIsOpen(false) + }, + }), + [id, isOpen, setIsOpen], + ) +} + +export function useMemoControlContext() { + return React.useContext(Context) +} + +export function Root({ + children, + control, +}: React.PropsWithChildren<{ + control?: Dialog.DialogOuterProps['control'] +}>) { + const defaultControl = useMenuControl() + const context = React.useMemo( + () => ({ + control: control || defaultControl, + }), + [control, defaultControl], + ) + const onOpenChange = React.useCallback( + (open: boolean) => { + if (context.control.isOpen && !open) { + context.control.close() + } else if (!context.control.isOpen && open) { + context.control.open() + } + }, + [context.control], + ) + + return ( + + + {children} + + + ) +} + +export function Trigger({children, label, style}: TriggerProps) { + const {control} = React.useContext(Context) + const { + state: hovered, + onIn: onMouseEnter, + onOut: onMouseLeave, + } = useInteractionState() + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + + return ( + + { + control.open() + }} + {...web({ + onMouseEnter, + onMouseLeave, + })}> + {children({ + isNative: false, + control, + state: { + hovered, + focused, + pressed: false, + }, + props: {}, + })} + + + ) +} + +export function Outer({children}: React.PropsWithChildren<{}>) { + const t = useTheme() + + return ( + + + + {children} + + + + + + ) +} + +export function Item({children, label, onPress, ...rest}: ItemProps) { + const t = useTheme() + const {control} = React.useContext(Context) + const { + state: hovered, + onIn: onMouseEnter, + onOut: onMouseLeave, + } = useInteractionState() + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + + return ( + + { + 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_sm, + a.py_sm, + a.rounded_xs, + {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, + ], + ])} + {...web({ + onMouseEnter, + onMouseLeave, + })}> + {children} + + + ) +} + +export function ItemText({children, style}: ItemTextProps) { + const t = useTheme() + return ( + + {children} + + ) +} + +export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) { + const t = useTheme() + return ( + + ) +} + +export function Group({children}: GroupProps) { + return children +} + +export function Divider() { + const t = useTheme() + return ( + + ) +} -- cgit 1.4.1