UNPKG

@react-md/menu

Version:

Create menus that auto-position themselves within the viewport and adhere to the accessibility guidelines

442 lines (441 loc) 16.1 kB
import type { ButtonProps } from "@react-md/button"; import type { IconRotatorProps, TextIconSpacingProps } from "@react-md/icon"; import type { ListElement, ListItemProps, ListProps } from "@react-md/list"; import type { RenderConditionalPortalProps } from "@react-md/portal"; import type { SheetPosition, SheetProps, SheetVerticalSize } from "@react-md/sheet"; import type { FixedPositioningTransitionCallbacks, ScaleTransitionHookOptions, TransitionScrollCallback } from "@react-md/transition"; import type { CalculateFixedPositionOptions, KeyboardFocusHookOptions, LabelA11y, PositionAnchor, PropsWithRef, RequireAtLeastOne } from "@react-md/utils"; import type { CSSProperties, Dispatch, HTMLAttributes, KeyboardEventHandler, MouseEventHandler, ReactNode, Ref, RefObject, SetStateAction } from "react"; /** @remarks \@since 5.0.0 */ export declare type MenuTransitionProps = Omit<ScaleTransitionHookOptions<HTMLDivElement>, "transitionIn" | "vertical" | "nodeRef">; /** @remarks \@since 5.0.0 */ export interface MenuOrientationProps { /** * Boolean if the menu should be rendered horizontally instead of vertically. * This will also update the `aria-orientation`. * * @defaultValue `false` */ horizontal?: boolean; } /** * This allows the menu to be conditionally rendered as a `Sheet` instead of a * menu. * * - `false` - always render as a `Menu` * - `true` - always render as a `Sheet` * - `"phone"` - render as a sheet only when the {@link AppSize} is considered * phone (`isPhone === true`). * * @defaultValue `false` * @remarks \@since 5.0.0 */ export declare type RenderMenuAsSheet = boolean | "phone"; /** @remarks \@since 5.0.0 */ export interface MenuConfiguration extends MenuOrientationProps { /** {@inheritDoc RenderMenuAsSheet} */ renderAsSheet?: RenderMenuAsSheet; /** * @see {@link SheetPosition} * @defaultValue `"bottom"` */ sheetPosition?: SheetPosition; /** * @see {@link SheetVerticalSize} * @defaultValue `"touch"` */ sheetVerticalSize?: SheetVerticalSize; /** * Any children to render above the sheet's menu implementation. This would * normally be something like a `<DialogHeader>` or `AppBar`. * * @defaultValue `null` */ sheetHeader?: ReactNode; /** * Any children to render below the sheet's menu implementation. This would * normally be something like a `<DialogFooter>`. * * @defaultValue `null` */ sheetFooter?: ReactNode; } /** @remarks \@since 5.0.0 */ export declare type MenuConfigurationContext = Required<MenuConfiguration>; /** @remarks \@since 5.0.0 */ export interface MenuWidgetProps extends HTMLAttributes<HTMLDivElement>, KeyboardFocusHookOptions<HTMLDivElement>, MenuOrientationProps { /** * An id required for a11y. */ id: string; /** * Boolean if the menu should not gain the elevation styles and should only be * set to `true` when rendering within a `Sheet`. * * @defaultValue `false` */ disableElevation?: boolean; } /** @remarks \@since 5.0.0 */ export interface MenuProps extends RenderConditionalPortalProps, MenuTransitionProps, MenuWidgetProps, MenuListProps { /** * Boolean if the menu is currently visible. */ visible: boolean; } /** @remarks \@since 5.0.0 */ export interface DropdownMenuConfigurationProps { /** * An optional `aria-label` that should be applied to the `Menu` component. If * this is `undefined`, an `aria-labelledby` will be provided to the `Menu` * instead linking to the {@link id} of the `Button`. */ menuLabel?: string; /** * The {@link PositionAnchor} to use for the menu. Here's the default value * for the anchor: * * - horizontal - `BELOW_CENTER_ANCHOR` * - a submenu - `TOP_RIGHT_ANCHOR` * - default - `TOP_INNER_RIGHT_ANCHOR` */ anchor?: PositionAnchor; /** {@inheritDoc CalculateFixedPositionOptions} */ fixedPositionOptions?: Readonly<CalculateFixedPositionOptions>; /** * A function that can be used to get the * {@link CalculateFixedPositionOptions} dynamically. */ getFixedPositionOptions?(): Readonly<CalculateFixedPositionOptions>; /** * Boolean if the menu should close if the page is scrolled. The default * behavior is to just update the position of the menu relative to the menu * button until it can no longer be visible within the viewport. * * @defaultValue `false` */ closeOnScroll?: boolean; /** * Boolean if the page should no longer be scrollable while the menu is * visible. * * @defaultValue `false` */ preventScroll?: boolean; /** * Boolean if the menu should close instead of repositioning itself if the * browser window is resized. * * @defaultValue `false` */ closeOnResize?: boolean; /** * Boolean if the menu component should not gain focus once it has mounted. * This should really only be set to `true` if the enter transition has been * disabled. * * @defaultValue `timeout === 0` */ disableFocusOnMount?: boolean; /** * Boolean if the toggle element should no longer gain focus when the menu * loses visibility. * * Note: The toggle element will not gain focus if: * - the menu closed via resizing the browser window * - the menu closes because the menu is no longer within the viewport * - the current `document.activeElement` has moved outside of the menu * - this generally means the `MenuItem`'s action cause the focus to move * already * - the current `document.activeElement` is an link (`<a href="">`) * - links should generally handle focus behavior themselves * * @defaultValue `timeout === 0` */ disableFocusOnUnmount?: boolean; } /** * Since the `useMenu` hook was designed to work with `react-md` components, * this is an object of props that should be passed to the {@link Menu} * component to get the correct menu functionality. * * @remarks \@since 5.0.0 */ export declare type ProvidedMenuProps = Required<Pick<MenuProps, "id" | "style" | "visible" | "onClick" | "onKeyDown">> & Required<FixedPositioningTransitionCallbacks> & RequireAtLeastOne<LabelA11y>; /** * Props that should be passed to a `Button` or `MenuItem` component to toggle * the visibility of a {@link Menu}. * * @remarks \@since 5.0.0 */ export interface ProvidedMenuToggleProps<E extends HTMLElement> { /** * This will always be set to `"menu"`. */ "aria-haspopup": HTMLAttributes<E>["aria-haspopup"]; /** * This will be set to `true` only while the menu is `visible`. */ "aria-expanded": HTMLAttributes<E>["aria-expanded"]; /** * This will be set to `${baseId}-toggle` and is used for providing an * accessible label to the menu if the {@link BaseMenuHookOptions.menuLabel} * was not provided. * * @see {@link BaseMenuHookOptions.baseId} */ id: string; /** * A click handler that will toggle the visibility of the menu. * * @see {@link HoverModeHookReturnValue.onClick} */ onClick: MouseEventHandler<E>; /** * The event handler will allow the menu to become visible by with `ArrowUp` * or `ArrowDown` for horizontal menus and `ArrowLeft` or `ArrowRight` for * vertical menus. This will also allow the focus to move between menus within * a `MenuBar` with the `ArrowLeft` and `ArrowRight` keys. */ onKeyDown: KeyboardEventHandler<E>; /** * The event handler will allow a `Menu` within a `MenuBar` to gain * visibility. */ onMouseEnter: MouseEventHandler<E>; /** * This handler just cancels the `hoverTimeout` from the `MenuBar`. */ onMouseLeave: MouseEventHandler<E>; } /** * This type was created since the `useContextMenu` only requires the menu * related props from the `useMenu` hook and I didn't want to duplicate the * information between the two. * * @remarks \@since 5.0.0 */ export interface BaseMenuHookOptions extends DropdownMenuConfigurationProps, MenuOrientationProps, FixedPositioningTransitionCallbacks { /** * This is the `id` for the toggle element for a `DropdownMenu` that is * required for a11y. This is used to also create the `Menu` component's `id` * as `${baseId}-menu`. */ baseId: string; /** * An optional style object to merge with the `Menu`'s fixed positioning * style. * * @see {@link useFixedPositioning} * @see {@link FixedPositionStyle} */ style?: CSSProperties; /** * Boolean if the menu is currently visible. */ visible: boolean; /** * This should be the second argument for the `useState` hook. * * ```ts * const [visible, setVisible] = useState(false); * ``` * * This is used to update the visibility of the menu based on click and * keyboard events. */ setVisible: Dispatch<SetStateAction<boolean>>; /** * Boolean if the menu is being rendered as a menuitem instead of a button. * Setting this to `true` implements the * {@link ProvidedMenuToggleProps.onKeyDown} functionality. * * @defaultValue `false` */ menuitem?: boolean; /** {@inheritDoc TransitionScrollCallback} */ onFixedPositionScroll?: TransitionScrollCallback<HTMLElement, HTMLDivElement>; /** * An optional function to call if the page resizes while the menu is visible. */ onFixedPositionResize?: EventListener; } /** @remarks \@since 5.0.0 */ export interface BaseMenuHookReturnValue { /** * Maybe don't need to provide. */ menuRef: Ref<HTMLDivElement>; /** * An object of props that should be provided to the {@link Menu} component. */ menuProps: ProvidedMenuProps; /** * A ref containing the menu DivHTMLElement if you need access to it for your * use case. */ menuNodeRef: RefObject<HTMLDivElement>; } /** @remarks \@since 5.0.0 */ export declare type MenuButtonTextIconSpacingProps = Pick<TextIconSpacingProps, "icon" | "iconAfter">; /** @remarks \@since 5.0.0 */ export declare type MenuButtonIconRotatorProps = Omit<IconRotatorProps, "children" | "rotated">; /** @remarks \@since 5.0.0 */ export interface BaseMenuButtonProps extends ButtonProps, MenuButtonTextIconSpacingProps { /** * An id required for accessibility and will be passed to the `<Button>` * component. * * @see {@link BaseMenuHookOptions.baseId} */ id: string; /** * Boolean if the dropdown icon should be included with the button children. * * @defaultValue `buttonType === "icon"` */ disableDropdownIcon?: boolean; /** * Any additional props to pass to the {@link IconRotator} component that * surrounds the {@link buttonChildren} */ iconRotatorProps?: Readonly<MenuButtonIconRotatorProps>; /** * Any additional props to pass to the {@link TextIconSpacing} component that * surrounds the optional dropdown icon. */ textIconSpacingProps?: Readonly<Omit<TextIconSpacingProps, "children" | keyof MenuButtonTextIconSpacingProps>>; } /** @remarks \@since 5.1.0 */ export interface MenuListProps { /** * An optional style to provide to the `List` component that surrounds the * `MenuItem` within a `Menu`. */ listStyle?: CSSProperties; /** * An optional className to provide to the `List` component that surrounds the * `MenuItem` within a `Menu`. */ listClassName?: string; /** * Any additional props to pass to the `List` component that surrounds the * `Menu`'s `MenuItem`s. */ listProps?: Readonly<PropsWithRef<Omit<ListProps, "horizontal">, ListElement>>; } /** @remarks \@since 5.0.0 */ export interface BaseMenuRendererProps extends RenderConditionalPortalProps, MenuConfiguration { /** * Any additional props that should be passed to the {@link Menu} component. * * Note: use the {@link menuStyle} and {@link menuClassName} props instead of * including `style` or `className` here. */ menuProps?: Readonly<Omit<MenuWidgetProps, "id" | "children">>; /** * An optional style object that should be merged with the menu's fixed * positioning styles. */ menuStyle?: CSSProperties; /** * An optional className that should be passed to the menu component. */ menuClassName?: string; /** * Any additional props that should be passed to the {@link Sheet} component. * * Note: use the {@link sheetStyle} and {@link sheetClassName} props instead * of including `style` or `className` here. */ sheetProps?: Readonly<Omit<SheetProps, "id" | "visible" | "onRequestClose" | "children">>; /** * An optional style object that should be passed to the sheet. */ sheetStyle?: CSSProperties; /** * An optional className that should be passed to the sheet component. */ sheetClassName?: string; /** * Any additional props that should be added to the sheet's menu * implementation. You probably won't ever need to use this. */ sheetMenuProps?: HTMLAttributes<HTMLDivElement>; } /** @remarks \@since 5.0.0 */ export interface BaseDropdownMenuProps extends DropdownMenuConfigurationProps, BaseMenuRendererProps, MenuTransitionProps { } /** @remarks \@since 5.0.0 */ export interface DropdownMenuButtonProps extends BaseMenuButtonProps, BaseDropdownMenuProps, MenuButtonTextIconSpacingProps, MenuListProps { /** * The children to display in the button. This should normally be text or an * icon. * * Note: If this is an icon, set the {@link buttonType} to `"icon"` to get the * correct styling and remove the dropdown icon. */ buttonChildren: ReactNode; } /** * @remarks \@since 5.0.0 */ export interface MenuItemProps extends Omit<ListItemProps, "role"> { /** * An optional id for the menu item. This is generally recommended, but it can * be ignored. */ id?: string; /** * The current role for the menu item. This will eventually be updated for * some of the other `menuitem*` widgets. * * @defaultValue `"menuitem"` */ role?: "menuitem"; /** * The tab index for the menu item. This should always stay at `-1`. */ tabIndex?: number; } /** @remarks \@since 5.0.0 */ export interface BaseMenuItemButtonProps extends MenuItemProps { /** * An id required for accessibility and will be passed to the `<MenuItem>` * component. * * @see {@link BaseMenuHookOptions.baseId} */ id: string; /** * Boolean if the dropdown icon should be set to the * {@link ListItemProps.rightAddon}. * * @defaultValue `typeof rightAddon !== "undefined"` */ disableDropdownIcon?: boolean; /** * Any additional props to pass to the {@link IconRotator} component that * surrounds the {@link buttonChildren} */ iconRotatorProps?: Readonly<MenuButtonIconRotatorProps>; } /** @remarks \@since 5.0.0 */ export interface DropdownMenuItemProps extends BaseDropdownMenuProps, BaseMenuItemButtonProps, MenuListProps { /** * The children to display in the menuitem acting as a button. */ buttonChildren: ReactNode; } /** * I couldn't really figure out a nice way to have the type inferred * automatically based on parent menus, so this is the "best" way to allow * some type-safety and autocompletion I could think of for `DropdownMenu`s. * * All this type will do is make sure you don't apply both button specific props * with menu item specific props at the same time but won't catch any errors * around providing button props to a menuitem or menuitem props to a button. * * @remarks \@since 5.0.0 */ export declare type DropdownMenuProps = DropdownMenuButtonProps | DropdownMenuItemProps;