diff --git a/packages/actify/src/components/Menu/Menu.tsx b/packages/actify/src/components/Menu/Menu.tsx deleted file mode 100644 index b803699..0000000 --- a/packages/actify/src/components/Menu/Menu.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { AriaButtonProps, Placement, useMenu, useMenuTrigger } from 'react-aria' -import type { AriaMenuProps, MenuTriggerProps } from '@react-types/menu' -import { useMenuTriggerState, useTreeState } from 'react-stately' - -import { Button } from './../Button' -import { MenuItem } from './MenuItem' -import { MenuItems } from './MenuItems' -import { Popover } from './../Popover' -import React from 'react' -import clsx from 'clsx' -import styles from './menu.module.css' - -interface MenuButtonProps - extends AriaMenuProps, - MenuTriggerProps { - style?: React.CSSProperties - className?: string - label?: string - placement?: Placement - activator?: ( - ref: React.RefObject, - menuTriggerProps: AriaButtonProps<'button'> - ) => React.JSX.Element -} - -export function Menu(props: MenuButtonProps) { - // Create state based on the incoming props - const state = useMenuTriggerState(props) - - // Get props for the menu trigger and menu elements - const ref = React.useRef(null) - const { menuTriggerProps, menuProps } = useMenuTrigger({}, state, ref) - - return ( - <> - {props.label && ( - - )} - {props?.activator?.(ref, menuTriggerProps)} - {state.isOpen && ( - - state.close()} - /> - - )} - - ) -} - -interface MenuProps extends AriaMenuProps { - onClose: () => void - style?: React.CSSProperties - className?: string -} - -function MenuRoot(props: MenuProps) { - // Create state based on the incoming props - const state = useTreeState(props) - - // Get props for the menu element - const ref = React.useRef(null) - const { menuProps } = useMenu(props, state, ref) - - return ( -
    - {[...state.collection].map((item) => - item.type == 'section' ? ( - props.onAction} - /> - ) : ( - props.onAction} - /> - ) - )} -
- ) -} diff --git a/packages/actify/src/components/Menu/MenuItem.tsx b/packages/actify/src/components/Menu/MenuItem.tsx deleted file mode 100644 index 4555290..0000000 --- a/packages/actify/src/components/Menu/MenuItem.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { mergeProps, useFocusRing, useMenuItem } from 'react-aria' - -import { FocusRing } from './../FocusRing' -import { Item } from './../Item' -import type { Node } from '@react-types/shared' -import React from 'react' -import { Ripple } from './../Ripple' -import { TreeState } from 'react-stately' -import styles from './menu.module.css' - -interface MenuItemProps { - item: Node - state: TreeState - onAction: (key: React.Key) => void - onClose: () => void -} - -export function MenuItem({ - item, - state, - onAction, - onClose -}: MenuItemProps) { - // Get props for the menu item element - const ref = React.useRef(null) - const { focusProps, isFocusVisible } = useFocusRing() - - const { menuItemProps } = useMenuItem( - { - key: item.key, - onAction, - onClose - }, - state, - ref - ) - - const Container = () => ( -
- - -
- ) - - return ( -
  • - }>{item.rendered} - {isFocusVisible && } -
  • - ) -} diff --git a/packages/actify/src/components/Menu/index.ts b/packages/actify/src/components/Menu/index.ts deleted file mode 100644 index 590f9e4..0000000 --- a/packages/actify/src/components/Menu/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './Menu' - -export { Item as MenuItem, Section as MenuItems } from 'react-stately' diff --git a/packages/actify/src/components/Menu/menu.module.css b/packages/actify/src/components/Menu/menu.module.css deleted file mode 100644 index 3cf837d..0000000 --- a/packages/actify/src/components/Menu/menu.module.css +++ /dev/null @@ -1,90 +0,0 @@ -.menu { - border-radius: var( - --md-menu-container-shape, - var(--md-sys-shape-corner-extra-small, 4px) - ); - display: none; - inset: auto; - border: none; - padding: 0px; - overflow: visible; - background-color: rgba(0, 0, 0, 0); - color: inherit; - opacity: 1; - z-index: 20; - position: absolute; - user-select: none; - max-height: inherit; - height: inherit; - min-width: inherit; - max-width: inherit; - scrollbar-width: inherit; - &.open { - display: block; - } -} -.items { - display: block; - list-style-type: none; - margin: 0; - outline: none; - box-sizing: border-box; - background-color: var( - --md-menu-container-color, - rgb(var(--md-sys-color-surface-container, 243 237 247)) - ); - height: inherit; - max-height: inherit; - overflow: auto; - min-width: inherit; - max-width: inherit; - border-radius: inherit; - scrollbar-width: inherit; -} -.animating .items { - overflow: hidden; -} -.item-padding { - padding-block: 8px; -} -.menu-item { - padding: 8px; - --md-ripple-hover-color: var( - --md-menu-item-hover-state-layer-color, - rgb(var(--md-sys-color-on-surface)) - ); - --md-ripple-hover-opacity: var( - --md-menu-item-hover-state-layer-opacity, - 0.08 - ); - --md-ripple-pressed-color: var( - --md-menu-item-pressed-state-layer-color, - rgb(var(--md-sys-color-on-surface)) - ); - --md-ripple-pressed-opacity: var( - --md-menu-item-pressed-state-layer-opacity, - 0.12 - ); - &:focus-visible { - outline: none; - } -} -.list-item { - position: relative; /* for focus ring */ - list-style: none; - background-color: var(--md-menu-item-container-color, transparent); - border-radius: inherit; - display: flex; - flex: 1; - max-width: inherit; - min-width: inherit; - outline: none; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); - &:not(.disabled) { - cursor: pointer; - } -} -.container { - position: absolute; - inset: 0; -} diff --git a/packages/actify/src/components/Menus/Menu.tsx b/packages/actify/src/components/Menus/Menu.tsx index 40150d4..ef42f59 100644 --- a/packages/actify/src/components/Menus/Menu.tsx +++ b/packages/actify/src/components/Menus/Menu.tsx @@ -1,154 +1,98 @@ -'use client' +import { AriaButtonProps, Placement, useMenu, useMenuTrigger } from 'react-aria' +import type { AriaMenuProps, MenuTriggerProps } from '@react-types/menu' +import { useMenuTriggerState, useTreeState } from 'react-stately' -import { EASING } from '../../animations' -import { Elevation } from '../Elevation' +import { Button } from './../Button' +import { MenuItem } from './MenuItem' +import { MenuItems } from './MenuItems' +import { Popover } from './../Popover' import React from 'react' import clsx from 'clsx' import styles from './menu.module.css' -import { useControllableState } from '../../hooks' -import { useOnClickOutside } from '../../hooks' -interface MenuContextProps { - open: boolean - setOpen: (open: boolean) => void +interface MenuButtonProps + extends AriaMenuProps, + MenuTriggerProps { + style?: React.CSSProperties + className?: string + label?: string + placement?: Placement + activator?: ( + ref: React.RefObject, + menuTriggerProps: AriaButtonProps<'button'> + ) => React.JSX.Element } -export const MenuContext = React.createContext(null) -export interface MenuRef { - show: () => void - close: () => void - toggle: () => void -} -interface MenuProps extends Omit, 'ref'> { - anchor?: string - positioning?: 'absolute' | 'popover' | 'fixed' | 'document' - /** Skips the opening and closing animations */ - quick?: boolean - typeaheadDelay?: number - anchorCorner?: string - menuCorner?: string - stayOpenOnOutsideClick?: boolean - stayOpenOnFocusout?: boolean - skipRestoreFocus?: boolean - defaultFocus?: string - noNavigationWrap?: boolean - isSubmenu?: boolean - ref?: React.Ref - open?: boolean - defaultOpen?: boolean - setOpen?: (open: boolean) => void - setFocused?: (focus: boolean) => void -} -const Menu = (props: MenuProps) => { - const { - ref, - style, - children, - className, - setFocused, - defaultOpen, - open: propOpen, - setOpen: propSetOpen, - positioning = 'absolute', - ...rest - } = props - - const menuRef = React.useRef(null) - const slotRef = React.useRef(null) +export function Menu(props: MenuButtonProps) { + // Create state based on the incoming props + const state = useMenuTriggerState(props) - const [open, setOpen] = useControllableState({ - value: propOpen, - onChange: propSetOpen, - defaultValue: defaultOpen - }) + // Get props for the menu trigger and menu elements + const ref = React.useRef(null) + const { menuTriggerProps, menuProps } = useMenuTrigger({}, state, ref) - useOnClickOutside(menuRef, () => { - setOpen(false) - setFocused?.(false) - }) - - React.useImperativeHandle( - ref, - () => ({ - show: () => setOpen(true), - close: () => setOpen(false), - toggle: () => setOpen(!open) - }), - [] + return ( + <> + {props.label && ( + + )} + {props?.activator?.(ref, menuTriggerProps)} + {state.isOpen && ( + + state.close()} + /> + + )} + ) +} - const animateOpen = async () => { - const surfaceEl = menuRef.current - const slotEl = slotRef.current - if (!surfaceEl || !slotEl) return true - const openingUpwards = false - // needs to be imperative because we don't want to mix animation and Lit - // render timing - surfaceEl.classList.toggle(styles['animating'], true) - const height = surfaceEl.offsetHeight - const items = React.Children.toArray(children) - - const FULL_DURATION = 500 - const SURFACE_OPACITY_DURATION = 50 - const ITEM_OPACITY_DURATION = 250 - // We want to fit every child fade-in animation within the full duration of - // the animation. - const DELAY_BETWEEN_ITEMS = - (FULL_DURATION - ITEM_OPACITY_DURATION) / items.length - const surfaceHeightAnimation = surfaceEl.animate( - [{ height: '0px' }, { height: `${height}px` }], - { - duration: FULL_DURATION, - easing: EASING.EMPHASIZED - } - ) - // When we are opening upwards, we want to make sure the last item is always - // in view, so we need to translate it upwards the opposite direction of the - // height animation - const upPositionCorrectionAnimation = slotEl.animate( - [ - { transform: openingUpwards ? `translateY(-${height}px)` : '' }, - { transform: '' } - ], - { duration: FULL_DURATION, easing: EASING.EMPHASIZED } - ) - const surfaceOpacityAnimation = surfaceEl.animate( - [{ opacity: 0 }, { opacity: 1 }], - SURFACE_OPACITY_DURATION - ) - - let resolveAnimation = (_: boolean) => {} - const animationFinished = new Promise((resolve) => { - resolveAnimation = resolve - }) - - surfaceHeightAnimation.addEventListener('finish', () => { - surfaceEl.classList.toggle(styles['animating'], false) - resolveAnimation(false) - }) - return await animationFinished - } +interface MenuProps extends AriaMenuProps { + onClose: () => void + style?: React.CSSProperties + className?: string +} - React.useEffect(() => { - if (open) { - animateOpen() - } - }, [open]) +function MenuRoot(props: MenuProps) { + // Create state based on the incoming props + const state = useTreeState(props) - const classes = clsx(styles['menu'], open && styles['open'], className) + // Get props for the menu element + const ref = React.useRef(null) + const { menuProps } = useMenu(props, state, ref) return ( -
    - - -
    -
    - {children} -
    -
    -
    -
    +
      + {[...state.collection].map((item) => + item.type == 'section' ? ( + props.onAction} + /> + ) : ( + props.onAction} + /> + ) + )} +
    ) } - -export { Menu } diff --git a/packages/actify/src/components/Menus/MenuItem.tsx b/packages/actify/src/components/Menus/MenuItem.tsx index bb5f3fb..10ff6b9 100644 --- a/packages/actify/src/components/Menus/MenuItem.tsx +++ b/packages/actify/src/components/Menus/MenuItem.tsx @@ -1,21 +1,39 @@ -import { Item, ItemProps } from '../../components/Item' +import { mergeProps, useFocusRing, useMenuItem } from 'react-aria' -import { FocusRing } from '../../components/FocusRing' -import { MenuContext } from './Menu' +import { FocusRing } from './../FocusRing' +import { Item } from './../Item' +import type { Node } from '@react-types/shared' import React from 'react' -import { Ripple } from '../../components/Ripple' +import { Ripple } from './../Ripple' +import { TreeState } from 'react-stately' import styles from './menu.module.css' -interface MenuItemProps extends Omit {} +interface MenuItemProps { + item: Node + state: TreeState + onAction: (key: React.Key) => void + onClose: () => void +} -const MenuItem = (props: MenuItemProps) => { - const { children, onClick, ...rest } = props - const context = React.useContext(MenuContext) +export function MenuItem({ + item, + state, + onAction, + onClose +}: MenuItemProps) { + // Get props for the menu item element + const ref = React.useRef(null) + const { focusProps, isFocusVisible } = useFocusRing() - const handleClick = (event: React.MouseEvent) => { - onClick?.(event) - context?.setOpen(false) - } + const { menuItemProps } = useMenuItem( + { + key: item.key, + onAction, + onClose + }, + state, + ref + ) const Container = () => (
    @@ -25,14 +43,13 @@ const MenuItem = (props: MenuItemProps) => { ) return ( -
    -
  • - }> - {children} - -
  • -
    +
  • + }>{item.rendered} + {isFocusVisible && } +
  • ) } - -export { MenuItem } diff --git a/packages/actify/src/components/Menu/MenuItems.tsx b/packages/actify/src/components/Menus/MenuItems.tsx similarity index 100% rename from packages/actify/src/components/Menu/MenuItems.tsx rename to packages/actify/src/components/Menus/MenuItems.tsx diff --git a/packages/actify/src/components/Menus/index.ts b/packages/actify/src/components/Menus/index.ts index c4f0433..590f9e4 100644 --- a/packages/actify/src/components/Menus/index.ts +++ b/packages/actify/src/components/Menus/index.ts @@ -1,2 +1,3 @@ -export { Menu } from './Menu' -export { MenuItem } from './MenuItem' +export * from './Menu' + +export { Item as MenuItem, Section as MenuItems } from 'react-stately' diff --git a/packages/actify/src/components/Menus/menu.module.css b/packages/actify/src/components/Menus/menu.module.css index e7b76d2..3cf837d 100644 --- a/packages/actify/src/components/Menus/menu.module.css +++ b/packages/actify/src/components/Menus/menu.module.css @@ -48,7 +48,7 @@ padding-block: 8px; } .menu-item { - display: flex; + padding: 8px; --md-ripple-hover-color: var( --md-menu-item-hover-state-layer-color, rgb(var(--md-sys-color-on-surface)) @@ -65,8 +65,12 @@ --md-menu-item-pressed-state-layer-opacity, 0.12 ); + &:focus-visible { + outline: none; + } } .list-item { + position: relative; /* for focus ring */ list-style: none; background-color: var(--md-menu-item-container-color, transparent); border-radius: inherit; diff --git a/packages/actify/src/index.ts b/packages/actify/src/index.ts index c7798a7..2392c2e 100644 --- a/packages/actify/src/index.ts +++ b/packages/actify/src/index.ts @@ -23,7 +23,7 @@ export * from './components/Lists' export * from './components/Lists/ListItem' export * from './components/Lists/ListGroup' export * from './components/Lists/ListItemLink' -export * from './components/Menu' +export * from './components/Menus' export * from './components/PopoverMenu/PopoverMenu' export * from './components/PopoverMenu/PopoverMenuItem' export * from './components/LinearProgress'