chakra-ui
Version:
Responsive and accessible React UI components built with React and Emotion
405 lines (355 loc) • 12.3 kB
JavaScript
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 };