UNPKG

chakra-ui

Version:

Responsive and accessible React UI components built with React and Emotion

405 lines (355 loc) 12.3 kB
import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties"; import _extends from "@babel/runtime/helpers/extends"; import _slicedToArray from "@babel/runtime/helpers/slicedToArray"; /** @jsx jsx */ import { jsx } from "@emotion/core"; import { useId } from "@reach/auto-id"; import { bool, func, string } from "prop-types"; import { createContext, forwardRef, useContext, useEffect, useRef, useState } from "react"; import { Manager, Popper, Reference } from "react-popper"; import Box from "../Box"; import PseudoBox from "../PseudoBox"; import Text from "../Text"; import { useUIMode } from "../ThemeProvider"; import usePrevious from "../usePrevious"; import { getFocusables, mergeRefs } from "../utils"; import { useMenuItemStyle, useMenuListStyle } from "./styles"; import Divider from "../Divider"; var MenuContext = createContext(); var Menu = function Menu(_ref) { var children = _ref.children, isOpen = _ref.isOpen, autoSelect = _ref.autoSelect, _ref$closeOnBlur = _ref.closeOnBlur, closeOnBlur = _ref$closeOnBlur === void 0 ? true : _ref$closeOnBlur, _ref$closeOnSelect = _ref.closeOnSelect, closeOnSelect = _ref$closeOnSelect === void 0 ? true : _ref$closeOnSelect, placement = _ref.placement; var _useUIMode = useUIMode(), mode = _useUIMode.mode; var _useState = useState({ isOpen: isOpen || false, index: -1 }), _useState2 = _slicedToArray(_useState, 2), state = _useState2[0], setState = _useState2[1]; var menuId = "menu-".concat(useId()); var buttonId = "menubutton-".concat(useId()); var menuRef = useRef(null); var buttonRef = useRef(null); var focusableItems = useRef(null); useEffect(function () { var focusables = getFocusables(menuRef.current).filter(function (node) { return ["menuitem", "menuitemradio", "menuitemcheckbox"].includes(node.getAttribute("role")); }); focusableItems.current = menuRef.current ? focusables : []; initTabIndex(); }, []); var updateTabIndex = function updateTabIndex(index) { if (focusableItems.current.length > 0) { var nodeAtIndex = focusableItems.current[index]; focusableItems.current.forEach(function (node) { if (node !== nodeAtIndex) { node.setAttribute("tabindex", -1); } }); nodeAtIndex.setAttribute("tabindex", 0); } }; var resetTabIndex = function resetTabIndex() { focusableItems.current.forEach(function (node) { return node.setAttribute("tabindex", -1); }); }; var initTabIndex = function initTabIndex() { focusableItems.current.forEach(function (node, index) { return index === 0 && node.setAttribute("tabindex", 0); }); }; var wasPreviouslyOpen = usePrevious(state.isOpen); useEffect(function () { if (state.index !== -1) { focusableItems.current[state.index].focus(); updateTabIndex(state.index); } if (state.index === -1 && !state.isOpen && wasPreviouslyOpen) { buttonRef.current && buttonRef.current.focus(); } if (state.index === -1 && state.isOpen) { menuRef.current && menuRef.current.focus(); } }, [state, wasPreviouslyOpen]); var focusOnFirstItem = function focusOnFirstItem() { setState({ isOpen: true, index: 0 }); }; var openMenu = function openMenu() { setState(_extends({}, state, { isOpen: true })); }; var focusAtIndex = function focusAtIndex(index) { setState(_extends({}, state, { index: index })); }; var focusOnLastItem = function focusOnLastItem() { setState({ isOpen: true, index: focusableItems.current.length - 1 }); }; var closeMenu = function closeMenu() { setState({ isOpen: false, index: -1 }); resetTabIndex(); }; var context = { state: state, focusAtIndex: focusAtIndex, focusOnLastItem: focusOnLastItem, focusOnFirstItem: focusOnFirstItem, closeMenu: closeMenu, buttonRef: buttonRef, menuRef: menuRef, focusableItems: focusableItems, menuId: menuId, buttonId: buttonId, openMenu: openMenu, autoSelect: autoSelect, closeOnSelect: closeOnSelect, closeOnBlur: closeOnBlur, placement: placement, mode: mode }; return jsx(MenuContext.Provider, { value: context }, jsx(Manager, null, typeof children === "function" ? children({ isOpen: isOpen, onClose: closeMenu }) : children)); }; export function useMenuContext() { var context = useContext(MenuContext); if (context === undefined) { throw new Error("useMenuContext must be used within a MenuContext Provider"); } return context; } ////////////////////////////////////////////////////////////////////////////////////////// var MenuButton = forwardRef(function (_ref2, _ref4) { var _ref2$as = _ref2.as, Comp = _ref2$as === void 0 ? "button" : _ref2$as, props = _objectWithoutProperties(_ref2, ["as"]); var _useMenuContext = useMenuContext(), isOpen = _useMenuContext.state.isOpen, focusOnLastItem = _useMenuContext.focusOnLastItem, focusOnFirstItem = _useMenuContext.focusOnFirstItem, closeMenu = _useMenuContext.closeMenu, menuId = _useMenuContext.menuId, buttonId = _useMenuContext.buttonId, autoSelect = _useMenuContext.autoSelect, openMenu = _useMenuContext.openMenu, buttonRef = _useMenuContext.buttonRef; return jsx(Reference, null, function (_ref3) { var referenceRef = _ref3.ref; return jsx(Comp, _extends({ "aria-haspopup": "menu", "aria-expanded": isOpen, "aria-controls": menuId, id: buttonId, role: "button", ref: function ref(node) { return mergeRefs([buttonRef, referenceRef, _ref4], node); }, onClick: function onClick() { if (isOpen) { closeMenu(); } else { autoSelect ? focusOnFirstItem() : openMenu(); } }, onKeyDown: function onKeyDown(event) { if (event.key === "ArrowDown") { event.preventDefault(); focusOnFirstItem(); } if (event.key === "ArrowUp") { event.preventDefault(); focusOnLastItem(); } } }, props)); }); }); ////////////////////////////////////////////////////////////////////////////////////////// var MenuList = function MenuList(_ref5) { var onKeyDown = _ref5.onKeyDown, onBlur = _ref5.onBlur, props = _objectWithoutProperties(_ref5, ["onKeyDown", "onBlur"]); var _useMenuContext2 = useMenuContext(), _useMenuContext2$stat = _useMenuContext2.state, index = _useMenuContext2$stat.index, isOpen = _useMenuContext2$stat.isOpen, focusAtIndex = _useMenuContext2.focusAtIndex, focusOnFirstItem = _useMenuContext2.focusOnFirstItem, focusOnLastItem = _useMenuContext2.focusOnLastItem, closeMenu = _useMenuContext2.closeMenu, focusableItems = _useMenuContext2.focusableItems, buttonRef = _useMenuContext2.buttonRef, menuId = _useMenuContext2.menuId, buttonId = _useMenuContext2.buttonId, menuRef = _useMenuContext2.menuRef, placement = _useMenuContext2.placement, closeOnBlur = _useMenuContext2.closeOnBlur; var handleKeyDown = function handleKeyDown(event) { var count = focusableItems.current.length; var nextIndex; if (event.key === "ArrowDown") { event.preventDefault(); nextIndex = (index + 1) % count; focusAtIndex(nextIndex); } else if (event.key === "ArrowUp") { nextIndex = (index - 1 + count) % count; focusAtIndex(nextIndex); } else if (event.key === "Home") { focusOnFirstItem(); } else if (event.key === "End") { focusOnLastItem(); } else if (event.key === "Tab") { event.preventDefault(); } else if (event.key === "Escape") { closeMenu(); } // Set focus based on first character if (/^[a-z0-9_-]$/i.test(event.key)) { event.stopPropagation(); event.preventDefault(); var foundNode = focusableItems.current.find(function (item) { return item.textContent.toLowerCase().startsWith(event.key); }); if (foundNode) { nextIndex = focusableItems.current.indexOf(foundNode); focusAtIndex(nextIndex); } } onKeyDown && onKeyDown(event); }; // Close the menu on blur var handleBlur = function handleBlur(event) { if (closeOnBlur && isOpen && menuRef.current && !menuRef.current.contains(event.relatedTarget) && !buttonRef.current.contains(event.relatedTarget)) { closeMenu(); } onBlur && onBlur(event); }; var styleProps = useMenuListStyle(); return jsx(Popper, { placement: placement }, function (_ref6) { var _ref7 = _ref6.ref; return jsx(Box, _extends({ maxWidth: "xs", borderRadius: "md", role: "menu", ref: function ref(node) { return mergeRefs([menuRef, _ref7], node); }, id: menuId, py: 2, position: "absolute", "aria-labelledby": buttonId, onKeyDown: handleKeyDown, onBlur: handleBlur, tabIndex: -1, hidden: !isOpen }, styleProps, props)); }); }; ////////////////////////////////////////////////////////////////////////////////////////// var MenuItem = forwardRef(function (_ref8, ref) { var isDisabled = _ref8.isDisabled, _onClick = _ref8.onClick, _onMouseLeave = _ref8.onMouseLeave, _onKeyDown = _ref8.onKeyDown, _onMouseMove = _ref8.onMouseMove, _ref8$role = _ref8.role, role = _ref8$role === void 0 ? "menuitem" : _ref8$role, props = _objectWithoutProperties(_ref8, ["isDisabled", "onClick", "onMouseLeave", "onKeyDown", "onMouseMove", "role"]); var _useMenuContext3 = useMenuContext(), focusableItems = _useMenuContext3.focusableItems, focusAtIndex = _useMenuContext3.focusAtIndex, closeOnSelect = _useMenuContext3.closeOnSelect, closeMenu = _useMenuContext3.closeMenu; var styleProps = useMenuItemStyle(); return jsx(PseudoBox, _extends({ as: "button", ref: ref, minHeight: "32px", alignItems: "center", textAlign: "left", px: 4, role: role, tabIndex: -1, disabled: isDisabled, "aria-disabled": isDisabled, onClick: function onClick(event) { if (isDisabled) { event.stopPropagation(); event.preventDefault(); return; } _onClick && _onClick(event); closeOnSelect && closeMenu(); }, onMouseMove: function onMouseMove(event) { if (isDisabled) { event.stopPropagation(); event.preventDefault(); return; } var nextIndex = focusableItems.current.indexOf(event.currentTarget); focusAtIndex(nextIndex); _onMouseMove && _onMouseMove(event); }, onMouseLeave: function onMouseLeave(event) { focusAtIndex(-1); _onMouseLeave && _onMouseLeave(event); }, onKeyDown: function onKeyDown(event) { if (isDisabled) return; if (event.key === "Enter" || event.key === " ") { event.preventDefault(); _onClick && _onClick(); closeOnSelect && closeMenu(); } _onKeyDown && _onKeyDown(event); } }, styleProps, props)); }); process.env.NODE_ENV !== "production" ? MenuItem.propTypes = { isDisabled: bool, onKeyDown: func, onClick: func, onMouseMove: func, role: string } : void 0; ////////////////////////////////////////////////////////////////////////////////////////// var MenuDivider = function MenuDivider(props) { return jsx(Divider, _extends({ orientation: "horizontal" }, props)); }; ////////////////////////////////////////////////////////////////////////////////////////// var MenuGroup = function MenuGroup(_ref9) { var children = _ref9.children, label = _ref9.label, rest = _objectWithoutProperties(_ref9, ["children", "label"]); return jsx(Box, { role: "presentation" }, label && jsx(Text, _extends({ mx: 4, my: 2, fontWeight: "semibold", fontSize: "sm" }, rest), label), children); }; ////////////////////////////////////////////////////////////////////////////////////////// export default Menu; export { MenuButton, MenuDivider, MenuGroup, MenuList, MenuItem };