@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
TypeScript
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;