grommet
Version:
focus on the essential experience
424 lines (414 loc) • 17.5 kB
JavaScript
;
exports.__esModule = true;
exports.Menu = void 0;
var _react = _interopRequireWildcard(require("react"));
var _styledComponents = _interopRequireDefault(require("styled-components"));
var _Box = require("../Box");
var _Button = require("../Button");
var _DropButton = require("../DropButton");
var _Keyboard = require("../Keyboard");
var _Text = require("../Text");
var _utils = require("../../utils");
var _MessageContext = require("../../contexts/MessageContext");
var _propTypes = require("./propTypes");
var _useThemeValue2 = require("../../utils/useThemeValue");
var _excluded = ["a11yTitle", "aria-label", "children", "disabled", "dropAlign", "dropBackground", "dropProps", "dropTarget", "justifyContent", "icon", "items", "label", "messages", "onKeyDown", "open", "plain", "size"],
_excluded2 = ["align"];
function _interopRequireDefault(e) { return e && e.__esModule ? e : { "default": e }; }
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, "default": e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
var ContainerBox = (0, _styledComponents["default"])(_Box.Box).withConfig({
displayName: "Menu__ContainerBox",
componentId: "sc-17fcys9-0"
})(["max-height:inherit;@media screen and (-ms-high-contrast:active),(-ms-high-contrast:none){width:100%;}&:focus{outline:none;}", ";"], function (props) {
return props.theme.menu.extend;
});
/* Notes on keyboard interactivity (based on W3) // For details reference: https://www.w3.org/TR/wai-aria-practices/#menu
To open menu when menu button is focused:
- Space/Enter/Up arrow/Down arrow will open menu
To navigate within menu:
- Up/down arrow keys can be used and will loop through options
(keeping focus within the Menu)
- Tab can be used, but once the last menu item is reached, Tab will close the
Menu and continue through page content.
To close the menu:
- Tabbing beyond the first or last menu item.
- Esc will close the menu
- Select a menu item
To make a selection:
- Enter key is pressed.
- Space is pressed.
*/
var defaultItems = [];
var Menu = exports.Menu = /*#__PURE__*/(0, _react.forwardRef)(function (props, ref) {
var a11yTitle = props.a11yTitle,
ariaLabel = props['aria-label'],
children = props.children,
disabled = props.disabled,
dropAlign = props.dropAlign,
dropBackground = props.dropBackground,
dropProps = props.dropProps,
dropTarget = props.dropTarget,
_props$justifyContent = props.justifyContent,
justifyContent = _props$justifyContent === void 0 ? 'start' : _props$justifyContent,
icon = props.icon,
_props$items = props.items,
items = _props$items === void 0 ? defaultItems : _props$items,
label = props.label,
messages = props.messages,
onKeyDown = props.onKeyDown,
open = props.open,
plain = props.plain,
size = props.size,
rest = _objectWithoutPropertiesLoose(props, _excluded);
var _useThemeValue = (0, _useThemeValue2.useThemeValue)(),
theme = _useThemeValue.theme,
passThemeFlag = _useThemeValue.passThemeFlag;
var _useContext = (0, _react.useContext)(_MessageContext.MessageContext),
format = _useContext.format;
var iconColor = (0, _utils.normalizeColor)(theme.menu.icons.color || 'control', theme);
// need to destructure the align otherwise it will get passed through
// to DropButton and override prop values
var _theme$menu$drop = theme.menu.drop,
themeDropAlign = _theme$menu$drop.align,
themeDropProps = _objectWithoutPropertiesLoose(_theme$menu$drop, _excluded2);
var a11y = ariaLabel || a11yTitle;
// total number of menu items
var itemCount = (0, _react.useMemo)(function () {
var count = 0;
if (items && Array.isArray(items[0])) {
items.forEach(function (group) {
count += group.length;
});
} else count = items.length;
return count;
}, [items]);
var align = dropProps && dropProps.align || dropAlign || themeDropAlign;
var controlButtonIndex = (0, _react.useMemo)(function () {
if (align.top === 'top') return -1;
if (align.bottom === 'bottom') return itemCount;
return undefined;
}, [align, itemCount]);
// Keeps track of whether menu options should be mirrored
// when there's not enough space below DropButton. This state
// is modified on /Drop/DropContainer.js.
var _useState = (0, _react.useState)(),
alignControlMirror = _useState[0],
setAlignControlMirror = _useState[1];
var initialAlignTop = alignControlMirror === align.top;
var dropContainerRef = (0, _react.useRef)();
var buttonRefs = (0, _react.useRef)([]);
var constants = (0, _react.useMemo)(function () {
return {
none: 'none',
tab: 9,
// Menu control button included on top of menu items
controlTop: align.top === 'top' || undefined,
// Menu control button included on the bottom of menu items
controlBottom: align.bottom === 'bottom' || undefined,
controlButtonIndex: controlButtonIndex
};
}, [align, controlButtonIndex]);
var _useState2 = (0, _react.useState)(constants.none),
activeItemIndex = _useState2[0],
setActiveItemIndex = _useState2[1];
var _useState3 = (0, _react.useState)(open || false),
isOpen = _useState3[0],
setOpen = _useState3[1];
var MenuIcon = isOpen && theme.menu.icons.up ? theme.menu.icons.up : theme.menu.icons.down;
var onDropClose = (0, _react.useCallback)(function () {
setActiveItemIndex(constants.none);
setOpen(false);
}, [constants.none]);
var onDropOpen = (0, _react.useCallback)(function () {
setOpen(true);
}, []);
(0, _react.useEffect)(function () {
// need to wait for Drop to be ready
var timer = setTimeout(function () {
if (isOpen) {
var optionsNode = dropContainerRef.current;
if (optionsNode) {
optionsNode.focus();
}
}
}, 100);
return function () {
return clearTimeout(timer);
};
}, [isOpen]);
var onSelectMenuItem = function onSelectMenuItem(event) {
if (isOpen) {
if (activeItemIndex >= 0) {
event.preventDefault();
event.stopPropagation();
buttonRefs.current[activeItemIndex].click();
}
} else {
onDropOpen();
}
};
var isTab = function isTab(event) {
return event.keyCode === constants.tab || event.which === constants.tab;
};
var onNextMenuItem = function onNextMenuItem(event) {
event.preventDefault();
if (!isOpen) {
onDropOpen();
} else if (isTab(event) && (!constants.controlBottom && activeItemIndex === itemCount - 1 || constants.controlBottom && activeItemIndex === controlButtonIndex)) {
// User has reached end of the menu, this tab will close
// the menu drop because there are no more "next items" to access
onDropClose();
} else {
var index;
if (
// This checks if the user has reached the end of the menu.
// In the case the the menu control button is located at the
// bottom of the menu, it checks if the user has reached the button.
// Otherwise, it checks if the user is at the last menu item.
constants.controlBottom && activeItemIndex === controlButtonIndex || !constants.controlBottom && activeItemIndex === itemCount - 1 || activeItemIndex === constants.none) {
// place focus on the first menu item
index = 0;
} else {
index = activeItemIndex + 1;
}
setActiveItemIndex(index);
if (buttonRefs.current[index]) {
buttonRefs.current[index].focus();
}
}
};
var onPreviousMenuItem = function onPreviousMenuItem(event) {
event.preventDefault();
if (!isOpen) {
onDropOpen();
} else if (isTab(event) && (constants.controlTop && activeItemIndex === controlButtonIndex || !constants.controlTop && activeItemIndex - 1 < 0)) {
// User has reached beginning of the menu, this tab will close
// the menu drop because there are no more "previous items" to access
onDropClose();
} else {
var index;
if (activeItemIndex === 'none') {
index = itemCount - 1;
} else if (activeItemIndex - 1 < 0) {
if (constants.controlTop && activeItemIndex - 1 === controlButtonIndex) {
index = itemCount;
} else {
index = itemCount - 1;
}
} else {
index = activeItemIndex - 1;
}
setActiveItemIndex(index);
if (buttonRefs.current[index]) {
buttonRefs.current[index].focus();
}
}
};
var menuIcon = icon !== false ? icon !== true && icon || /*#__PURE__*/_react["default"].createElement(MenuIcon, {
color: iconColor,
size: size
}) : null;
var buttonProps = {
plain: plain,
size: size
};
var content;
if (children) {
content = children;
} else if (!theme.button["default"]) {
/*
Not adding a theme object now because this code path
is not used in the HPE theme, but we may add theme
support here in the future.
*/
content = /*#__PURE__*/_react["default"].createElement(_Box.Box, {
direction: "row",
justify: justifyContent,
align: "center",
pad: "small",
gap: label && icon !== false ? 'small' : undefined
}, /*#__PURE__*/_react["default"].createElement(_Text.Text, {
size: size
}, label), menuIcon);
} else {
// when a theme has theme.button.default, keep content as
// undefined so we can rely on Button label & icon props
buttonProps = {
icon: menuIcon,
label: label,
plain: plain,
reverse: true,
size: size
};
content = undefined;
}
var controlMirror = /*#__PURE__*/_react["default"].createElement(_Box.Box, {
flex: false
}, /*#__PURE__*/_react["default"].createElement(_Button.Button, _extends({
ref: function ref(r) {
// make it accessible at the end of all menu items
buttonRefs.current[itemCount] = r;
},
a11yTitle: a11y || format({
id: 'menu.closeMenu',
messages: messages
}),
active: activeItemIndex === controlButtonIndex,
focusIndicator: false,
hoverIndicator: "background",
onClick: onDropClose,
onFocus: function onFocus() {
return setActiveItemIndex(controlButtonIndex);
}
// On first tab into menu, the control button should not
// be able to receive tab focus because the focus should
// go to the first menu item instead.
,
tabIndex: activeItemIndex === constants.none ? '-1' : undefined
}, theme.menu.item, buttonProps), typeof content === 'function' ? function () {
return content(_extends({}, props, {
drop: true
}));
} : content));
var menuItem = function menuItem(item, index) {
var _theme$menu$item, _theme$menu$item2, _theme$menu$item3, _theme$menu$item4;
// Determine whether the label is done as a child or
// as an option Button kind property.
var child = !theme.button.option ?
/*#__PURE__*/
/*
Not adding a theme object now because this code path
is not used in the HPE theme, but we may add theme
support here in the future.
*/
_react["default"].createElement(_Box.Box, {
align: ((_theme$menu$item = theme.menu.item) == null ? void 0 : _theme$menu$item.align) || 'start',
pad: "small",
direction: "row",
gap: item.gap || ((_theme$menu$item2 = theme.menu.item) == null ? void 0 : _theme$menu$item2.gap),
justify: item.justify || ((_theme$menu$item3 = theme.menu.item) == null ? void 0 : _theme$menu$item3.justify)
}, item.reverse && item.label, item.icon, !item.reverse && item.label) : undefined;
// if we have a child, turn on plain, and hoverIndicator
return (
/*#__PURE__*/
// lint isn't flagging this but we shouldn't use index as a key
// see no-array-index-key lint rule
_react["default"].createElement(_Box.Box, {
key: index,
flex: false,
role: "none"
}, /*#__PURE__*/_react["default"].createElement(_Button.Button, _extends({
ref: function ref(r) {
buttonRefs.current[index] = r;
},
role: "menuitem",
onFocus: function onFocus() {
setActiveItemIndex(index);
},
active: activeItemIndex === index,
focusIndicator: false,
plain: !child ? undefined : true,
align: "start",
kind: !child ? 'option' : undefined,
hoverIndicator: !child ? undefined : 'background'
}, theme.menu.item, {
justify: item.justify || ((_theme$menu$item4 = theme.menu.item) == null ? void 0 : _theme$menu$item4.justify)
}, !child ? item : _extends({}, item, {
gap: undefined,
icon: undefined,
label: undefined,
reverse: undefined
}), {
onClick: function onClick() {
if (item.onClick) {
item.onClick.apply(item, arguments);
}
if (item.close !== false) {
onDropClose();
}
}
}), child))
);
};
var menuContent;
var grouped = itemCount && Array.isArray(items[0]);
if (grouped) {
var index = 0;
menuContent = items.map(function (group, groupIndex) {
var _theme$menu$group, _theme$menu$group2, _theme$menu$group3;
return /*#__PURE__*/_react["default"].createElement(_Box.Box
// eslint-disable-next-line react/no-array-index-key
, {
key: groupIndex
// ensure menu groups don't collapse if vertical space on screen
// causes scrolling within the menu
,
flex: false
}, groupIndex > 0 && /*#__PURE__*/_react["default"].createElement(_Box.Box, {
pad: theme.menu.group.separator.pad
}, /*#__PURE__*/_react["default"].createElement(_Box.Box, {
border: {
side: 'top',
color: (_theme$menu$group = theme.menu.group) == null || (_theme$menu$group = _theme$menu$group.separator) == null ? void 0 : _theme$menu$group.color,
size: (_theme$menu$group2 = theme.menu.group) == null || (_theme$menu$group2 = _theme$menu$group2.separator) == null ? void 0 : _theme$menu$group2.size
}
})), /*#__PURE__*/_react["default"].createElement(_Box.Box, _extends({}, theme.menu.container, (_theme$menu$group3 = theme.menu.group) == null ? void 0 : _theme$menu$group3.container), group.map(function (item) {
// item index needs to be its index in the entire menu as if
// it were a flat array
var currentIndex = index;
index += 1;
return menuItem(item, currentIndex);
})));
});
} else menuContent = items.map(function (item, index) {
return menuItem(item, index);
});
return /*#__PURE__*/_react["default"].createElement(_Keyboard.Keyboard, {
onDown: onDropOpen,
onUp: onDropOpen,
onSpace: onSelectMenuItem,
onEsc: onDropClose,
onTab: onDropClose,
onKeyDown: onKeyDown
}, /*#__PURE__*/_react["default"].createElement(_DropButton.DropButton, _extends({
ref: ref
}, rest, buttonProps, {
a11yTitle: a11y || format({
id: 'menu.openMenu',
messages: messages
}),
"aria-haspopup": "menu",
"aria-expanded": isOpen,
onAlign: setAlignControlMirror,
disabled: disabled,
dropAlign: align,
dropTarget: dropTarget,
dropProps: dropProps || themeDropProps,
open: isOpen,
onOpen: onDropOpen,
onClose: onDropClose,
dropContent: /*#__PURE__*/_react["default"].createElement(_Keyboard.Keyboard, {
onTab: function onTab(event) {
return event.shiftKey ? onPreviousMenuItem(event) : onNextMenuItem(event);
},
onDown: onNextMenuItem,
onUp: onPreviousMenuItem,
onEnter: onSelectMenuItem
}, /*#__PURE__*/_react["default"].createElement(ContainerBox, _extends({
ref: dropContainerRef,
tabIndex: -1,
background: dropBackground || theme.menu.background
}, passThemeFlag), alignControlMirror === 'top' && align.bottom !== 'top' && align.top !== 'bottom' ? controlMirror : undefined, /*#__PURE__*/_react["default"].createElement(_Box.Box, _extends({
overflow: "auto",
role: "menu",
a11yTitle: a11y
}, !grouped ? theme.menu.container : {}), menuContent), !initialAlignTop &&
// don't show controlMirror if caller is using
// align.bottom === 'top'
alignControlMirror === 'bottom' && align.bottom !== 'top' && align.top !== 'bottom' ? controlMirror : undefined))
}), content));
});
Menu.displayName = 'Menu';
Menu.propTypes = _propTypes.MenuPropTypes;