@szhsin/react-menu
Version:
React component for building accessible menu, dropdown, submenu, context menu, and more
766 lines (714 loc) • 23 kB
TypeScript
import React = require('react');
//
// base types
// ----------------------------------------------------------------------
export type MenuState = 'opening' | 'open' | 'closing' | 'closed';
export type MenuAlign = 'start' | 'center' | 'end';
export type MenuDirection = 'left' | 'right' | 'top' | 'bottom';
export type MenuPosition = 'auto' | 'anchor' | 'initial';
export type MenuOverflow = 'auto' | 'visible' | 'hidden';
export type MenuReposition = 'auto' | 'initial';
export type MenuViewScroll = 'auto' | 'close' | 'initial';
export type MenuItemTypeProp = 'checkbox' | 'radio';
export type CloseReason = 'click' | 'cancel' | 'blur' | 'scroll';
/**
* - `'first'` focus the first item in the menu.
* - `'last'` focus the last item in the menu.
* - `number` focus item at the specific position (zero-based).
*/
export type FocusPosition = 'first' | 'last' | number;
export type ClassNameProp<M = undefined> = string | ((modifiers: M) => string);
export type RenderProp<M, R = React.ReactNode> = R | ((modifiers: M) => R);
export interface BaseProps<M = undefined>
extends Omit<React.HTMLAttributes<HTMLElement>, 'className' | 'children'> {
ref?: React.Ref<any>;
/**
* Can be a string or a function which receives a modifier object and returns a CSS `class` string.
*/
className?: ClassNameProp<M>;
}
export interface Event {
/**
* The `value` prop passed to the `MenuItem` being clicked.
* It's useful for identifying which menu item is clicked.
*/
value?: any;
/**
* Indicates the key if the event is triggered by keyboard. Can be 'Enter', ' '(Space) or 'Escape'.
*/
key?: string;
}
export interface MenuCloseEvent extends Event {
/**
* The reason that causes the close event.
*/
reason: CloseReason;
}
export interface RadioChangeEvent extends Event {
/**
* The `name` prop passed to the `MenuRadioGroup` when the menu item is in a radio group.
*/
name?: string;
/**
* Set this property on event object to control whether to keep menu open after menu item is activated.
* Leaving it `undefined` will behave in accordance with WAI-ARIA Authoring Practices.
*/
keepOpen?: boolean;
/**
* Setting this property on event object to `true` will skip `onItemClick` event on root menu component.
*/
stopPropagation?: boolean;
/**
* DOM event object (React synthetic event)
*/
syntheticEvent: MouseEvent | KeyboardEvent;
}
export interface ClickEvent extends RadioChangeEvent {
/**
* Indicates if the menu item is checked, only for `MenuItem` type="checkbox".
*/
checked?: boolean;
}
export interface MenuChangeEvent {
/**
* Indicates if the menu is open or closed.
*/
open: boolean;
}
export interface EventHandler<E> {
(event: E): void;
}
export interface RectElement {
getBoundingClientRect(): {
left: number;
right: number;
top: number;
bottom: number;
width: number;
height: number;
};
}
//
// Menu common types
// ----------------------------------------------------------------------
export type MenuModifiers = Readonly<{
/**
* Indicates the state of menu.
*/
state: MenuState;
/**
* Alignment of menu with anchor element.
*/
align: MenuAlign;
/**
* Computed direction in which the menu expands.
*/
dir: MenuDirection;
}>;
export type MenuArrowModifiers = Readonly<{
/**
* Computed direction in which the menu expands.
*
* *Please note arrow points to the opposite direction of this value.*
*/
dir: MenuDirection;
}>;
export interface MenuStateEvents {
/**
* An event is fired whenever the menu is opened or closed.
*/
onMenuChange?: EventHandler<MenuChangeEvent>;
}
export interface MenuStateOptions extends MenuStateEvents {
/**
* Enable menu to be mounted in the open state.
*/
initialOpen?: boolean;
/**
* By default menu isn't mounted into DOM until it's opened for the first time.
* Setting the prop to `true` will change this behaviour,
* which also enables menu and its items to be server rendered.
*/
initialMounted?: boolean;
/**
* By default menu remains in DOM when it's closed.
* Setting the prop to `true` will change this behaviour.
*/
unmountOnClose?: boolean;
/**
* Enable or disable transition effects in `Menu`, `MenuItem`, and any descendent `SubMenu`.
*
* You can set 'open', 'close' and 'item' at the same time with one boolean value or separately with an object.
*
* *If you enable transition on menu, make sure to add your own animation styles,
* or import `'@szhsin/react-menu/dist/transitions/slide.css'`,
* otherwise menu cannot be closed or have visible delay when closed.*
*
* @example [CodeSandbox Demo](https://codesandbox.io/s/react-menu-sass-i1wxo)
*/
transition?:
| boolean
| {
open?: boolean;
close?: boolean;
item?: boolean;
};
/**
* A fallback timeout in `ms` to stop transition if `onAnimationEnd` events are not fired.
*
* *Note: this value should be greater than or equal to the duration of
* transition animation applied on menu.*
*
* @default 500
*/
transitionTimeout?: number;
}
export interface Hoverable {
disabled?: boolean;
}
/**
* Common props for `Menu`, `SubMenu` and `ControlledMenu`
*/
export interface BaseMenuProps extends Omit<BaseProps, 'style'> {
/**
* Can be a string or a function which receives a modifier object and returns a CSS `class` string.
*/
menuClassName?: ClassNameProp<MenuModifiers>;
/**
* This prop is forwarded to the `style` prop of menu DOM element.
*/
menuStyle?: React.CSSProperties;
/**
* Set `true` to display an arrow pointing to its anchor element.
*/
arrow?: boolean;
/**
* Properties of this object are spread to the menu arrow DOM element.
*/
arrowProps?: Omit<BaseProps<MenuArrowModifiers>, 'ref'>;
/**
* @deprecated This prop is currently ignored. It will be removed in the next major version.
*/
focusProps?: React.HTMLAttributes<HTMLElement>;
/**
* Add a gap (gutter) between menu and its anchor element.
* The value (in pixels) can be negative.
* @default 0
*/
gap?: number;
/**
* Shift menu's position away from its anchor element.
* The value (in pixels) can be negative.
* @default 0
*/
shift?: number;
/**
* Set alignment of menu with anchor element.
* @default 'start'
*/
align?: MenuAlign;
/**
* Set direction in which menu expands against anchor element.
* @default 'bottom'
*/
direction?: MenuDirection;
/**
* Set the position of menu related to its anchor element:
*
* - 'auto' menu position is adjusted to have it contained within the viewport,
* even if it will be detached from the anchor element.
* This option allows to display menu in the viewport as much as possible.
*
* - 'anchor' menu position is adjusted to have it contained within the viewport,
* but it will be kept attached to the edges of anchor element.
*
* - 'initial' menu always stays at its initial position.
* @default 'auto'
*/
position?: MenuPosition;
/**
* Make the menu list scrollable or hidden when there is not enough viewport space to
* display all menu items. The prop is similar to the CSS `overflow` property.
* @default 'visible'
*/
overflow?: MenuOverflow;
/**
* Set computed overflow amount down to a child `MenuGroup`.
* The `MenuGroup` should have `takeOverflow` prop set as `true` accordingly.
*/
setDownOverflow?: boolean;
/**
* Any valid React node or a render function that returns one.
*/
children?: RenderProp<MenuModifiers>;
}
/**
* Common props for `Menu` and `ControlledMenu`
*/
export interface RootMenuProps
extends BaseMenuProps,
Omit<MenuStateOptions, 'initialOpen' | 'onMenuChange'> {
/**
* Properties of this object are spread to the root DOM element containing the menu.
*/
containerProps?: Omit<React.HTMLAttributes<HTMLElement>, 'className'>;
/**
* A ref object attached to a DOM element within which menu will be positioned.
* If not provided, the nearest ancestor which has CSS `overflow` set to a value
* other than 'visible' or the browser viewport will serve as the bounding box.
*/
boundingBoxRef?: React.RefObject<Element | RectElement>;
/**
* Specify bounding box padding in pixels. Use a syntax similar to the CSS
* `padding` property but sizing units are discarded.
* @example '10', '5 10', '1 2 4', or '2 5 3 1'
*/
boundingBoxPadding?: string;
/**
* Set the behaviour of menu and any of its descendent submenus when window is scrolling:
* - 'initial' The window scroll event is ignored and has no effect on menu.
* - 'auto' Menu will reposition itself based on the value of `position` prop when window is scrolling.
* - 'close' menu will be closed when window is scrolled.
* @default 'initial'
*/
viewScroll?: MenuViewScroll;
/**
* - If `true`, menu is rendered as a direct child of `document.body`,
* - or you can specify a target element in the DOM as menu container.
*
* Portal allows menu to visually “break out” of its container. Typical use cases may include:
* - An ancestor container is positioned and CSS `overflow` is set to a value other than `visible`.
* - You have a DOM structure that creates a complex hierarchy of stacking contexts,
* and menu is overlapped regardless of `z-index` value.
*/
portal?:
| boolean
| {
/**
* A DOM node under which menu will be rendered.
*/
target?: Element | null;
/**
* When `target` is null, setting this value `true` prevents menu from rendering into the DOM hierarchy of its parent component.
*/
stablePosition?: boolean;
};
/**
* Specify when menu is repositioned:
* - 'initial' Don't automatically reposition menu. Set to this value when you want
* to explicitly reposition menu using the `repositionFlag` prop.
* - 'auto' Reposition menu whenever itself or the anchor has changed in size, using the `ResizeObserver` API.
* @default 'auto'
*/
reposition?: MenuReposition;
/**
* Use this prop to explicitly reposition menu. Whenever the prop has a new value,
* menu position will be recalculated and updated.
* You might use a counter and increase it every time.
*
* *Warning: don't update this prop in rapid succession,
* which is inefficient and might cause infinite rendering of component.
* E.g., don't change the value of this prop in `window` scroll event.*
*/
repositionFlag?: number | string;
/**
* Set a delay in `ms` before opening a submenu when mouse moves over it.
* @default 300
*/
submenuOpenDelay?: number;
/**
* Set a delay in `ms` before closing a submenu when it's open and mouse is
* moving over other items in the parent menu list.
* @default 150
*/
submenuCloseDelay?: number;
/**
* Set a CSS `class` on the container element of menu for theming purpose.
*/
theming?: string;
/**
* Event fired when descendent menu items are clicked.
*/
onItemClick?: EventHandler<ClickEvent>;
}
export interface MenuInstance {
/**
* Open menu and optionally request which menu item will be hovered.
*/
openMenu: (position?: FocusPosition, alwaysUpdate?: boolean) => void;
/**
* Close menu
*/
closeMenu: () => void;
}
/**
* Common props for `Menu` and `SubMenu`
*/
export interface UncontrolledMenuProps extends MenuStateEvents {
/**
* Menu component ref which can be used to programmatically open or close menu.
*/
instanceRef?: React.Ref<MenuInstance>;
}
//
// MenuButton
// ----------------------------------------------------------------------
export type MenuButtonModifiers = Readonly<{
/**
* Indicates if the associated menu is open.
*/
open: boolean;
}>;
export interface MenuButtonProps extends BaseProps<MenuButtonModifiers> {
disabled?: boolean;
children?: React.ReactNode;
}
export const MenuButton: React.NamedExoticComponent<MenuButtonProps>;
//
// Menu
// ----------------------------------------------------------------------
export interface MenuProps extends RootMenuProps, UncontrolledMenuProps {
/**
* Can be a `MenuButton`, a `button` element, or a React component.
* It also can be a render function that returns one.
*
* If a React component is provided, it needs to implement the following contracts:
* - Accepts a `ref` prop that is forwarded to an element to which menu will be positioned.
* The element should be able to receive focus.
* - Accepts `onClick` and `onKeyDown` event props.
*/
menuButton: RenderProp<MenuButtonModifiers, React.ReactElement>;
}
export const Menu: React.NamedExoticComponent<MenuProps>;
//
// ControlledMenu
// ----------------------------------------------------------------------
export interface ControlledMenuProps extends RootMenuProps {
/**
* Viewport coordinates to which context menu will be positioned.
*
* *Use this prop only for context menu*
*/
anchorPoint?: {
x: number;
y: number;
};
/**
* A ref object attached to a DOM element to which menu will be positioned.
*
* *Don't set this prop for context menu*
*/
anchorRef?: React.RefObject<Element | RectElement>;
/**
* If `true`, the menu list element will gain focus after menu is open.
* @default true
*/
captureFocus?: boolean;
/**
* Controls the state of menu. When the prop is `undefined`, menu will be unmounted from DOM.
*/
state?: MenuState;
/**
* Sets which menu item receives focus (hover) when menu opens.
* You will usually set this prop when the menu is opened by keyboard events.
*
* *Note: If you don't intend to update focus (hover) position,
* it's important to keep this prop's identity stable when your component re-renders.*
*/
menuItemFocus?: {
position?: FocusPosition;
alwaysUpdate?: boolean;
};
/**
* Set the return value of `useMenuState` to this prop.
*/
endTransition?: () => void;
/**
* Event fired when menu is about to close.
*/
onClose?: EventHandler<MenuCloseEvent>;
}
export const ControlledMenu: React.NamedExoticComponent<ControlledMenuProps>;
//
// SubMenu
// ----------------------------------------------------------------------
export type SubMenuItemModifiers = Readonly<{
/**
* Indicates if the submenu is open.
*/
open: boolean;
/**
* Indicates if the submenu item is being hovered and has focus.
*/
hover: boolean;
/**
* Indicates if the submenu and item are disabled.
*/
disabled: boolean;
}>;
export interface SubMenuProps extends BaseMenuProps, Hoverable, UncontrolledMenuProps {
/**
* Properties of this object are spread to the submenu item DOM element.
*/
itemProps?: BaseProps<SubMenuItemModifiers>;
/**
* The submenu `label` can be a `string` or JSX element, or a render function that returns one.
*/
label?: RenderProp<SubMenuItemModifiers>;
/**
* - `undefined` submenu opens when the label item is hovered or clicked. This is the default behaviour.
* - 'clickOnly' submenu opens when the label item is clicked.
* - 'none' submenu doesn't open with mouse or keyboard events;
* you can call the `openMenu` function on `instanceRef` to open submenu programmatically.
*/
openTrigger?: 'none' | 'clickOnly';
/**
* If true, the submenu will be rendered directly under the root container
* instead of nested inside its parent menu.
* @default false
*/
portal?: boolean;
}
export const SubMenu: React.NamedExoticComponent<SubMenuProps>;
//
// MenuItem
// ----------------------------------------------------------------------
export type MenuItemModifiers = Readonly<{
/**
* 'radio' for radio item, 'checkbox' for checkbox item, or `undefined` for other items.
*/
type?: MenuItemTypeProp;
/**
* Indicates if the menu item is disabled.
*/
disabled: boolean;
/**
* Indicates if the menu item is being hovered and has focus.
*/
hover: boolean;
/**
* Indicates if the menu item is checked when it's a radio or checkbox item.
*/
checked: boolean;
/**
* Indicates if the menu item has a URL link.
*/
anchor: boolean;
}>;
export interface MenuItemProps extends Omit<BaseProps<MenuItemModifiers>, 'onClick'>, Hoverable {
/**
* Any value provided to this prop will be available in the event object of click events.
*
* It's useful for helping identify which menu item is clicked when you
* listen on `onItemClick` event on root menu component.
*/
value?: any;
/**
* If provided, menu item renders an HTML `<a>` element with this `href` attribute.
*/
href?: string;
rel?: string;
target?: string;
download?: string;
/**
* Set this prop to make the item a checkbox or radio menu item.
*/
type?: MenuItemTypeProp;
/**
* Set `true` if a checkbox menu item is checked.
*
* *Please note radio menu item doesn't use this prop.*
*/
checked?: boolean;
/**
* Event fired when the menu item is clicked.
*/
onClick?: EventHandler<ClickEvent>;
/**
* Any valid React node or a render function that returns one.
*/
children?: RenderProp<MenuItemModifiers>;
}
export const MenuItem: React.NamedExoticComponent<MenuItemProps>;
//
// FocusableItem
// ----------------------------------------------------------------------
export type FocusableItemModifiers = Readonly<{
/**
* Indicates if the focusable item is disabled.
*/
disabled: boolean;
/**
* Indicates if the focusable item is being hovered.
*/
hover: boolean;
/**
* Always `true` for a focusable item.
*/
focusable: true;
}>;
export interface FocusableItemProps extends BaseProps<FocusableItemModifiers>, Hoverable {
/**
* A render function that returns a React node.
*/
children: (modifiers: {
/**
* Indicates if the focusable item is disabled.
*/
disabled: boolean;
/**
* Indicates if the focusable item is being hovered.
*/
hover: boolean;
/**
* A ref to be attached to the element which should receive focus when this focusable item is hovered.
*
* If you render a React component, it needs to expose a `focus` method or implement ref forwarding.
*/
ref: React.RefObject<any>;
/**
* A function that requests to close the root menu.
* @param {string} key Indicate which key initiates the close request.
*/
closeMenu: (key?: string) => void;
}) => React.ReactNode;
}
/**
* A component to wrap focusable element (input, button) in a menu item.
* It manages focus automatically among other menu items during mouse and keyboard interactions.
*
* @example https://szhsin.github.io/react-menu/#focusable-item
*/
export const FocusableItem: React.NamedExoticComponent<FocusableItemProps>;
//
// MenuDivider
// ----------------------------------------------------------------------
export const MenuDivider: React.NamedExoticComponent<BaseProps>;
//
// MenuHeader
// ----------------------------------------------------------------------
export interface MenuHeaderProps extends BaseProps {
children?: React.ReactNode;
}
export const MenuHeader: React.NamedExoticComponent<MenuHeaderProps>;
//
// MenuGroup
// ----------------------------------------------------------------------
export interface MenuGroupProps extends BaseProps {
children?: React.ReactNode;
/**
* Set `true` to apply overflow of the parent menu to the group.
* Only one `MenuGroup` in a menu should set this prop as `true`.
*/
takeOverflow?: boolean;
}
/**
* A component to wrap a subset related menu items and make them scrollable.
*
* @example https://szhsin.github.io/react-menu/#menu-overflow
*/
export const MenuGroup: React.NamedExoticComponent<MenuGroupProps>;
//
// MenuRadioGroup
// ----------------------------------------------------------------------
export interface MenuRadioGroupProps extends BaseProps {
/**
* Optionally set the radio group name.
*
* The name will be passed to the `onRadioChange` event.
* It's useful for identifying radio groups if you attach the same event handler to multiple groups.
*/
name?: string;
/**
* Set value of the radio group.
*
* The child menu item which has the same value (strict equality ===) as the
* radio group is marked as checked.
*/
value?: any;
children?: React.ReactNode;
/**
* Event fired when a child menu item is clicked (selected).
*/
onRadioChange?: EventHandler<RadioChangeEvent>;
}
export const MenuRadioGroup: React.NamedExoticComponent<MenuRadioGroupProps>;
/**
* A custom Hook which helps manage the states of `ControlledMenu`.
*/
export function useMenuState(options?: MenuStateOptions): [
{
/**
* Menu state which should be forwarded to `ControlledMenu`.
*/
state?: MenuState;
/**
* Stop transition animation. This function value should be forwarded to `ControlledMenu`.
*/
endTransition: () => void;
},
/**
* Open or close menu.
*
* - If no parameter is supplied, this function will toggle state between open and close phases.
* - You can set a boolean parameter to explicitly switch into one of the two phases.
*/
(open?: boolean) => void
];
export type ClickEventProps = Required<
Pick<React.HTMLAttributes<Element>, 'onMouseDown' | 'onClick'>
>;
export type HoverEventProps = Required<
Pick<React.HTMLAttributes<Element>, 'onMouseEnter' | 'onMouseLeave'>
>;
export type ToggleEvent = (open: boolean, event: Parameters<React.MouseEventHandler>[0]) => void;
/**
* A Hook which works with `ControlledMenu` to create click (toggle) menu.
*
* @returns props which should be given to the anchor element.
*/
export function useClick(
/**
* Menu state can be a boolean or the state returned from `useMenuState`
*/
state: boolean | MenuState | undefined,
/**
* A callback function that should open or close menu, receiving React synthetic event which triggered the callback.
*/
onToggle: ToggleEvent
): ClickEventProps;
/**
* A Hook which works with `ControlledMenu` to create hover menu.
*/
export function useHover(
/**
* Menu state can be a boolean or the state returned from `useMenuState`
*/
state: boolean | MenuState | undefined,
/**
* A callback function that should open or close menu, receiving React synthetic event which triggered the callback.
*/
onToggle: ToggleEvent,
options?: {
/**
* Specify an open delay in `ms`.
* @default 100
*/
openDelay?: number;
/**
* Specify a close delay in `ms`.
* @default 300
*/
closeDelay?: number;
}
): {
/**
* Props which should be given to the anchor element.
*/
anchorProps: HoverEventProps & ClickEventProps;
/**
* Props which should be given to the menu.
*/
hoverProps: HoverEventProps;
};
export {};