@szhsin/react-menu
Advanced tools
Comparing version 2.0.1 to 2.1.0
@@ -14,3 +14,3 @@ import React = require('react'); | ||
export type MenuItemTypeProp = 'checkbox' | 'radio'; | ||
export type FocusPosition = 'initial' | 'first' | 'last'; | ||
export type FocusPosition = 'initial' | 'first' | 'last' | number; | ||
export type CloseReason = 'click' | 'cancel' | 'blur' | 'scroll'; | ||
@@ -116,3 +116,3 @@ type DirStyleKey = '$left' | '$right' | '$top' | '$bottom'; | ||
// Menu, SubMenu and ControlledMenu | ||
interface SharedMenuProps extends Omit<BaseProps, 'styles'> { | ||
interface BaseMenuProps extends Omit<BaseProps, 'styles'> { | ||
menuClassName?: ClassNameProp<MenuModifiers>; | ||
@@ -133,3 +133,3 @@ menuStyles?: StylesProp<MenuModifiers, MenuStyleKeys>; | ||
// Menu and ControlledMenu | ||
interface BaseMenuProps extends MenuStateOptions, SharedMenuProps { | ||
interface RootMenuProps extends BaseMenuProps, MenuStateOptions { | ||
containerProps?: Omit<React.HTMLAttributes<HTMLElement>, 'className'>; | ||
@@ -148,2 +148,13 @@ boundingBoxRef?: React.RefObject<Element | RectElement>; | ||
export interface MenuInstance { | ||
openMenu: (position?: FocusPosition, alwaysUpdate?: boolean) => void; | ||
closeMenu: () => void; | ||
} | ||
// Menu and SubMenu | ||
interface UncontrolledMenuProps { | ||
instanceRef?: React.Ref<MenuInstance>; | ||
onMenuChange?: EventHandler<MenuChangeEvent>; | ||
} | ||
// | ||
@@ -166,5 +177,4 @@ // MenuButton | ||
// ---------------------------------------------------------------------- | ||
export interface MenuProps extends BaseMenuProps { | ||
export interface MenuProps extends RootMenuProps, UncontrolledMenuProps { | ||
menuButton: RenderProp<MenuButtonModifiers, React.ReactElement>; | ||
onMenuChange?: EventHandler<MenuChangeEvent>; | ||
} | ||
@@ -177,3 +187,3 @@ | ||
// ---------------------------------------------------------------------- | ||
export interface ControlledMenuProps extends BaseMenuProps { | ||
export interface ControlledMenuProps extends RootMenuProps { | ||
anchorPoint?: { | ||
@@ -188,3 +198,4 @@ x: number; | ||
menuItemFocus?: { | ||
position: FocusPosition | ||
position?: FocusPosition; | ||
alwaysUpdate?: boolean; | ||
}; | ||
@@ -206,6 +217,5 @@ onClose?: EventHandler<MenuCloseEvent>; | ||
export interface SubMenuProps extends SharedMenuProps, Hoverable { | ||
export interface SubMenuProps extends BaseMenuProps, Hoverable, UncontrolledMenuProps { | ||
itemProps?: BaseProps<SubMenuItemModifiers>; | ||
label?: RenderProp<SubMenuItemModifiers>; | ||
onMenuChange?: EventHandler<MenuChangeEvent>; | ||
} | ||
@@ -212,0 +222,0 @@ |
@@ -1,2 +0,2 @@ | ||
import React, { Children, cloneElement, forwardRef, useContext, useState, useMemo, useLayoutEffect, useEffect, useRef, useReducer, useCallback, memo } from 'react'; | ||
import React, { Children, cloneElement, forwardRef, useContext, useState, useMemo, useLayoutEffect, useEffect, useRef, useReducer, useCallback, useImperativeHandle, memo } from 'react'; | ||
import ReactDOM, { unstable_batchedUpdates, createPortal } from 'react-dom'; | ||
@@ -66,8 +66,8 @@ import PropTypes from 'prop-types'; | ||
const isProd = process.env.NODE_ENV === 'production'; | ||
const isMenuOpen = state => !!state && state[0] === 'o'; | ||
const batchedUpdates = unstable_batchedUpdates || (callback => callback()); | ||
const values = Object.values || (obj => Object.keys(obj).map(key => obj[key])); | ||
const floatEqual = (a, b, diff = 0.0001) => Math.abs(a - b) < diff; | ||
const isProd = process.env.NODE_ENV === 'production'; | ||
const isMenuOpen = state => state === 'open' || state === 'opening'; | ||
const getTransition = (transition, name) => Boolean(transition && transition[name]) || transition === true; | ||
const getTransition = (transition, name) => !!(transition && transition[name]) || transition === true; | ||
const safeCall = (fn, ...args) => typeof fn === 'function' ? fn(...args) : fn; | ||
@@ -136,4 +136,4 @@ const getName = component => component && component['_szhsinMenu']; | ||
if (!isProd && index === undefined && !isDisabled) { | ||
const error = `[react-menu] Validate item '${node && node.toString()}' failed. | ||
You're probably creating your own components or HOC over MenuItem, SubMenu or FocusableItem. | ||
const error = `[React-Menu] Validate item '${node && node.toString()}' failed. | ||
You're probably creating wrapping components or HOC over MenuItem, SubMenu or FocusableItem. | ||
To create wrapping components, see: https://codesandbox.io/s/react-menu-wrapping-q0b59 | ||
@@ -179,5 +179,5 @@ To create HOCs, see: https://codesandbox.io/s/react-menu-hoc-0bipn`; | ||
if (name === 'MenuGroup') { | ||
const takeOverflow = Boolean(child.props.takeOverflow); | ||
const takeOverflow = !!child.props.takeOverflow; | ||
const descOverflow = desc.descendOverflow; | ||
if (!isProd && (descendOverflow === descOverflow ? descOverflow : takeOverflow)) throw new Error('[react-menu] Only one MenuGroup in a menu is allowed to have takeOverflow prop.'); | ||
if (!isProd && (descendOverflow === descOverflow ? descOverflow : takeOverflow)) throw new Error('[React-Menu] Only one MenuGroup in a menu is allowed to have takeOverflow prop.'); | ||
descendOverflow = descendOverflow || descOverflow || takeOverflow; | ||
@@ -203,3 +203,3 @@ } | ||
}); | ||
const sharedMenuPropTypes = { | ||
const menuPropTypes = { | ||
className: PropTypes.string, | ||
@@ -216,3 +216,3 @@ ...stylePropTypes('menu'), | ||
}; | ||
const menuPropTypesBase = { ...sharedMenuPropTypes, | ||
const rootMenuPropTypes = { ...menuPropTypes, | ||
containerProps: PropTypes.object, | ||
@@ -238,3 +238,7 @@ initialMounted: PropTypes.bool, | ||
}; | ||
const sharedMenuDefaultProp = { | ||
const uncontrolledMenuPropTypes = { | ||
instanceRef: PropTypes.oneOfType([PropTypes.object, PropTypes.func]), | ||
onMenuChange: PropTypes.func | ||
}; | ||
const menuDefaultProps = { | ||
offsetX: 0, | ||
@@ -247,3 +251,3 @@ offsetY: 0, | ||
}; | ||
const menuDefaultPropsBase = { ...sharedMenuDefaultProp, | ||
const rootMenuDefaultProps = { ...menuDefaultProps, | ||
reposition: 'auto', | ||
@@ -269,5 +273,5 @@ viewScroll: 'initial', | ||
const [active, setActive] = useState(false); | ||
const activeKeys = [Keys.SPACE, Keys.ENTER, ...moreKeys]; | ||
const activeKeys = [Keys.ENTER, Keys.SPACE, ...moreKeys]; | ||
const cancelActive = () => setActive(false); | ||
const cancelActive = () => active && setActive(false); | ||
@@ -282,3 +286,3 @@ return { | ||
onKeyDown: e => { | ||
if (isHovering && !isDisabled && activeKeys.includes(e.key)) { | ||
if (!active && isHovering && !isDisabled && activeKeys.indexOf(e.key) !== -1) { | ||
setActive(true); | ||
@@ -288,3 +292,3 @@ } | ||
onKeyUp: e => { | ||
if (activeKeys.includes(e.key)) { | ||
if (activeKeys.indexOf(e.key) !== -1) { | ||
setActive(false); | ||
@@ -294,3 +298,3 @@ } | ||
onBlur: e => { | ||
if (!e.currentTarget.contains(e.relatedTarget)) { | ||
if (active && !e.currentTarget.contains(e.relatedTarget)) { | ||
setActive(false); | ||
@@ -349,3 +353,3 @@ } | ||
const sanitiseKey = key => key.charAt(0) === '$' ? key.slice(1) : key; | ||
const sanitiseKey = key => key[0] === '$' ? key.slice(1) : key; | ||
@@ -408,3 +412,3 @@ const useFlatStyles = (styles, modifiers) => useMemo(() => { | ||
const onBlur = e => { | ||
if (!e.currentTarget.contains(e.relatedTarget)) { | ||
if (isHovering && !e.currentTarget.contains(e.relatedTarget)) { | ||
dispatch({ | ||
@@ -479,9 +483,8 @@ type: HoverIndexActionTypes.UNSET, | ||
const menuState = useMenuState(options); | ||
const [menuItemFocus, setMenuItemFocus] = useState({ | ||
position: FocusPositions.INITIAL | ||
}); | ||
const [menuItemFocus, setMenuItemFocus] = useState({}); | ||
const openMenu = (position = FocusPositions.INITIAL) => { | ||
const openMenu = (position, alwaysUpdate) => { | ||
setMenuItemFocus({ | ||
position | ||
position, | ||
alwaysUpdate | ||
}); | ||
@@ -512,2 +515,3 @@ menuState.toggleMenu(true); | ||
"aria-disabled": disabled || undefined, | ||
type: "button", | ||
disabled: disabled | ||
@@ -1073,3 +1077,3 @@ }, restProps, { | ||
case Keys.SPACE: | ||
if (e.target && e.target.className.includes(menuClass)) { | ||
if (e.target && e.target.className.indexOf(menuClass) !== -1) { | ||
e.preventDefault(); | ||
@@ -1097,3 +1101,3 @@ } | ||
if (!containerRef.current) { | ||
if (!isProd) throw new Error('[react-menu] Menu cannot be positioned properly as container ref is null. If you initialise isOpen prop to true for ControlledMenu, please see this link for a solution: https://github.com/szhsin/react-menu/issues/2#issuecomment-719166062'); | ||
if (!isProd) throw new Error('[React-Menu] Menu cannot be positioned properly as container ref is null. If you initialise isOpen prop to true for ControlledMenu, please see this link for a solution: https://github.com/szhsin/react-menu/issues/2#issuecomment-719166062'); | ||
return; | ||
@@ -1235,3 +1239,3 @@ } | ||
}, [rootAnchorRef, anchorScrollingRef, scrollingRef, isOpen, overflow, onClose, viewScroll, handlePosition]); | ||
const hasOverflow = Boolean(overflowData) && overflowData.overflowAmt > 0; | ||
const hasOverflow = !!overflowData && overflowData.overflowAmt > 0; | ||
useEffect(() => { | ||
@@ -1287,16 +1291,27 @@ if (hasOverflow || !isOpen || !parentScrollingRef) return; | ||
if (!closeTransition) setOverflowData(); | ||
return; | ||
} | ||
const id = setTimeout(() => { | ||
if (!isOpen || !menuRef.current || menuRef.current.contains(document.activeElement)) return; | ||
if (!menuRef.current) return; | ||
const { | ||
position, | ||
alwaysUpdate | ||
} = menuItemFocus || {}; | ||
if (!alwaysUpdate && menuRef.current.contains(document.activeElement)) return; | ||
if (_captureFocus) menuRef.current.focus(); | ||
if (menuItemFocus.position === FocusPositions.FIRST) { | ||
if (position === FocusPositions.FIRST) { | ||
dispatch({ | ||
type: HoverIndexActionTypes.FIRST | ||
}); | ||
} else if (menuItemFocus.position === FocusPositions.LAST) { | ||
} else if (position === FocusPositions.LAST) { | ||
dispatch({ | ||
type: HoverIndexActionTypes.LAST | ||
}); | ||
} else if (position >= 0 && position < menuItemsCount.current) { | ||
dispatch({ | ||
type: HoverIndexActionTypes.SET, | ||
index: position | ||
}); | ||
} | ||
@@ -1532,3 +1547,3 @@ }, openTransition ? 170 : 100); | ||
}); | ||
ControlledMenu.propTypes = { ...menuPropTypesBase, | ||
ControlledMenu.propTypes = { ...rootMenuPropTypes, | ||
state: PropTypes.oneOf(values(MenuStateMap)), | ||
@@ -1543,10 +1558,9 @@ anchorPoint: PropTypes.exact({ | ||
menuItemFocus: PropTypes.exact({ | ||
position: PropTypes.string | ||
position: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | ||
alwaysUpdate: PropTypes.bool | ||
}), | ||
onClose: PropTypes.func | ||
}; | ||
ControlledMenu.defaultProps = { ...menuDefaultPropsBase, | ||
menuItemFocus: { | ||
position: FocusPositions.INITIAL | ||
} | ||
ControlledMenu.defaultProps = { ...rootMenuDefaultProps, | ||
menuItemFocus: {} | ||
}; | ||
@@ -1558,2 +1572,3 @@ | ||
menuButton, | ||
instanceRef, | ||
onMenuChange, | ||
@@ -1577,3 +1592,3 @@ ...restProps | ||
if (skipOpen.current) return; | ||
openMenu(e.detail === 0 ? FocusPositions.FIRST : FocusPositions.INITIAL); | ||
openMenu(e.detail === 0 ? FocusPositions.FIRST : undefined); | ||
}; | ||
@@ -1617,2 +1632,6 @@ | ||
useMenuChange(onMenuChange, isOpen); | ||
useImperativeHandle(instanceRef, () => ({ | ||
openMenu, | ||
closeMenu: () => toggleMenu(false) | ||
})); | ||
const menuProps = { ...restProps, | ||
@@ -1628,7 +1647,7 @@ ...stateProps, | ||
}); | ||
Menu.propTypes = { ...menuPropTypesBase, | ||
menuButton: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired, | ||
onMenuChange: PropTypes.func | ||
Menu.propTypes = { ...rootMenuPropTypes, | ||
...uncontrolledMenuPropTypes, | ||
menuButton: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired | ||
}; | ||
Menu.defaultProps = menuDefaultPropsBase; | ||
Menu.defaultProps = rootMenuDefaultProps; | ||
@@ -1643,2 +1662,3 @@ const SubMenu = withHovering( /*#__PURE__*/memo(function SubMenu({ | ||
isHovering, | ||
instanceRef, | ||
captureFocus: _1, | ||
@@ -1649,3 +1669,3 @@ repositionFlag: _2, | ||
}) { | ||
const isDisabled = Boolean(disabled); | ||
const isDisabled = !!disabled; | ||
validateIndex(index, isDisabled, label); | ||
@@ -1692,2 +1712,7 @@ const { | ||
const setHover = () => !isHovering && dispatch({ | ||
type: HoverIndexActionTypes.SET, | ||
index | ||
}); | ||
const delayOpen = delay => { | ||
@@ -1734,4 +1759,4 @@ dispatch({ | ||
if (isOpen) { | ||
itemRef.current.focus(); | ||
toggleMenu(false); | ||
itemRef.current.focus(); | ||
handled = true; | ||
@@ -1758,4 +1783,4 @@ } | ||
switch (e.key) { | ||
case Keys.ENTER: | ||
case Keys.SPACE: | ||
case Keys.ENTER: | ||
case Keys.RIGHT: | ||
@@ -1781,2 +1806,16 @@ openMenu(FocusPositions.FIRST); | ||
useMenuChange(onMenuChange, isOpen); | ||
useImperativeHandle(instanceRef, () => ({ | ||
openMenu: (...args) => { | ||
if (isParentOpen) { | ||
setHover(); | ||
openMenu(...args); | ||
} | ||
}, | ||
closeMenu: () => { | ||
if (isOpen) { | ||
itemRef.current.focus(); | ||
toggleMenu(false); | ||
} | ||
} | ||
})); | ||
const modifiers = useMemo(() => Object.freeze({ | ||
@@ -1797,6 +1836,3 @@ open: isOpen, | ||
onMouseLeave: handleMouseLeave, | ||
onMouseDown: () => !isHovering && dispatch({ | ||
type: HoverIndexActionTypes.SET, | ||
index | ||
}), | ||
onMouseDown: setHover, | ||
onClick: handleClick, | ||
@@ -1844,10 +1880,10 @@ onKeyUp: handleKeyUp | ||
}), 'SubMenu'); | ||
SubMenu.propTypes = { ...sharedMenuPropTypes, | ||
SubMenu.propTypes = { ...menuPropTypes, | ||
...uncontrolledMenuPropTypes, | ||
disabled: PropTypes.bool, | ||
label: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), | ||
itemProps: PropTypes.shape({ ...stylePropTypes() | ||
}), | ||
onMenuChange: PropTypes.func | ||
}) | ||
}; | ||
SubMenu.defaultProps = { ...sharedMenuDefaultProp, | ||
SubMenu.defaultProps = { ...menuDefaultProps, | ||
direction: 'right' | ||
@@ -1871,3 +1907,3 @@ }; | ||
}) { | ||
const isDisabled = Boolean(disabled); | ||
const isDisabled = !!disabled; | ||
validateIndex(index, isDisabled, children); | ||
@@ -1891,4 +1927,4 @@ const ref = useRef(); | ||
const isCheckBox = type === 'checkbox'; | ||
const isAnchor = Boolean(href) && !isDisabled && !isRadio && !isCheckBox; | ||
const isChecked = isRadio ? radioGroup.value === value : isCheckBox ? Boolean(checked) : false; | ||
const isAnchor = !!href && !isDisabled && !isRadio && !isCheckBox; | ||
const isChecked = isRadio ? radioGroup.value === value : isCheckBox ? !!checked : false; | ||
@@ -1918,4 +1954,4 @@ const handleClick = e => { | ||
switch (e.key) { | ||
case Keys.ENTER: | ||
case Keys.SPACE: | ||
case Keys.ENTER: | ||
if (isAnchor) { | ||
@@ -2000,3 +2036,3 @@ ref.current.click(); | ||
}) { | ||
const isDisabled = Boolean(disabled); | ||
const isDisabled = !!disabled; | ||
validateIndex(index, isDisabled, children); | ||
@@ -2003,0 +2039,0 @@ const ref = useRef(null); |
{ | ||
"name": "@szhsin/react-menu", | ||
"version": "2.0.1", | ||
"version": "2.1.0", | ||
"description": "React component for building accessible menu, dropdown, submenu, context menu and more.", | ||
@@ -37,3 +37,3 @@ "author": "Zheng Song", | ||
"prop-types": "^15.7.2", | ||
"react-transition-state": "^0.3.0" | ||
"react-transition-state": "^1.0.1" | ||
}, | ||
@@ -40,0 +40,0 @@ "devDependencies": { |
# React-Menu | ||
> An accessible, responsive, and customisable React menu library. | ||
> An accessible React menu library. | ||
**[Live examples and documentation](https://szhsin.github.io/react-menu/)** | ||
**[Live examples and docs](https://szhsin.github.io/react-menu/)** | ||
@@ -15,4 +15,4 @@ [](https://www.npmjs.com/package/@szhsin/react-menu) | ||
- Unlimited levels of submenu. | ||
- Supports dropdown or context menu. | ||
- Supports radio and checkbox menu items. | ||
- Supports context menu. | ||
- Flexible menu positioning. | ||
@@ -38,25 +38,27 @@ - Customisable styling. | ||
import { | ||
Menu, | ||
MenuItem, | ||
MenuButton, | ||
SubMenu | ||
Menu, | ||
MenuItem, | ||
MenuButton, | ||
SubMenu | ||
} from '@szhsin/react-menu'; | ||
import '@szhsin/react-menu/dist/index.css'; | ||
export default function Example() { | ||
return ( | ||
<Menu menuButton={<MenuButton>Open menu</MenuButton>}> | ||
<MenuItem>New File</MenuItem> | ||
<SubMenu label="Open"> | ||
<MenuItem>index.html</MenuItem> | ||
<MenuItem>example.js</MenuItem> | ||
<MenuItem>about.css</MenuItem> | ||
</SubMenu> | ||
<MenuItem>Save</MenuItem> | ||
</Menu> | ||
); | ||
export default function App() { | ||
return ( | ||
<Menu menuButton={<MenuButton>Open menu</MenuButton>}> | ||
<MenuItem>New File</MenuItem> | ||
<MenuItem>Save</MenuItem> | ||
<SubMenu label="Edit"> | ||
<MenuItem>Cut</MenuItem> | ||
<MenuItem>Copy</MenuItem> | ||
<MenuItem>Paste</MenuItem> | ||
</SubMenu> | ||
<MenuItem>Print...</MenuItem> | ||
</Menu> | ||
); | ||
} | ||
``` | ||
[More examples and documentation](https://szhsin.github.io/react-menu/) | ||
[Edit on CodeSandbox](https://codesandbox.io/s/react-menu-basic-3ez3c)<br> | ||
**[Visit more examples and docs](https://szhsin.github.io/react-menu/)** | ||
@@ -63,0 +65,0 @@ ## License |
Sorry, the diff of this file is too big to display
152023
4657
66
+ Addedreact-transition-state@1.1.5(transitive)
- Removedreact-transition-state@0.3.0(transitive)