@dotconnor/grommet
Version:
focus on the essential experience
346 lines (303 loc) • 12.9 kB
JavaScript
exports.__esModule = true;
exports.Menu = void 0;
var _react = _interopRequireWildcard(require("react"));
var _styledComponents = _interopRequireWildcard(require("styled-components"));
var _defaultProps = require("../../default-props");
var _Box = require("../Box");
var _Button = require("../Button");
var _DropButton = require("../DropButton");
var _Keyboard = require("../Keyboard");
var _Text = require("../Text");
var _utils = require("../../utils");
function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; }
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }
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%;}", ";"], 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 Menu = /*#__PURE__*/(0, _react.forwardRef)(function (props, ref) {
var a11yTitle = props.a11yTitle,
children = props.children,
disabled = props.disabled,
dropAlign = props.dropAlign,
dropBackground = props.dropBackground,
dropProps = props.dropProps,
dropTarget = props.dropTarget,
justifyContent = props.justifyContent,
icon = props.icon,
items = props.items,
label = props.label,
messages = props.messages,
onKeyDown = props.onKeyDown,
open = props.open,
plain = props.plain,
size = props.size,
rest = _objectWithoutPropertiesLoose(props, ["a11yTitle", "children", "disabled", "dropAlign", "dropBackground", "dropProps", "dropTarget", "justifyContent", "icon", "items", "label", "messages", "onKeyDown", "open", "plain", "size"]);
var theme = (0, _react.useContext)(_styledComponents.ThemeContext) || _defaultProps.defaultProps.theme;
var iconColor = (0, _utils.normalizeColor)(theme.menu.icons.color || 'control', theme);
var align = dropProps.align || dropAlign;
var controlButtonIndex = (0, _react.useMemo)(function () {
if (align.top === 'top') return -1;
if (align.bottom === 'bottom') return items.length;
return undefined;
}, [align, items]);
var buttonRefs = {};
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 _useState = (0, _react.useState)(constants.none),
activeItemIndex = _useState[0],
setActiveItemIndex = _useState[1];
var _useState2 = (0, _react.useState)(open || false),
isOpen = _useState2[0],
setOpen = _useState2[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);
}, []);
var onSelectMenuItem = function onSelectMenuItem(event) {
if (isOpen) {
if (activeItemIndex >= 0) {
event.preventDefault();
event.stopPropagation();
buttonRefs[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 === items.length - 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 === items.length - 1 || activeItemIndex === constants.none) {
// place focus on the first menu item
index = 0;
} else {
index = activeItemIndex + 1;
}
setActiveItemIndex(index);
buttonRefs[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 - 1 < 0) {
if (constants.controlTop && activeItemIndex - 1 === controlButtonIndex) {
index = items.length;
} else {
index = items.length - 1;
}
} else {
index = activeItemIndex - 1;
}
setActiveItemIndex(index);
buttonRefs[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"]) {
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[items.length] = r;
},
a11yTitle: a11yTitle || messages.closeMenu || 'Close Menu',
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
}, buttonProps), typeof content === 'function' ? function () {
return content(_extends({}, props, {
drop: true
}));
} : content));
return /*#__PURE__*/_react["default"].createElement(_Keyboard.Keyboard, {
onDown: onNextMenuItem,
onUp: onPreviousMenuItem,
onEnter: onSelectMenuItem,
onSpace: onSelectMenuItem,
onEsc: onDropClose,
onTab: onDropClose,
onKeyDown: onKeyDown
}, /*#__PURE__*/_react["default"].createElement(_DropButton.DropButton, _extends({
ref: ref
}, rest, buttonProps, {
a11yTitle: a11yTitle || messages.openMenu || 'Open Menu',
disabled: disabled,
dropAlign: align,
dropTarget: dropTarget,
dropProps: dropProps,
open: isOpen,
onOpen: onDropOpen,
onClose: onDropClose,
dropContent: /*#__PURE__*/_react["default"].createElement(_Keyboard.Keyboard, {
onTab: function onTab(event) {
return event.shiftKey ? onPreviousMenuItem(event) : onNextMenuItem(event);
},
onEnter: onSelectMenuItem
}, /*#__PURE__*/_react["default"].createElement(ContainerBox, {
background: dropBackground || theme.menu.background
}, align.top === 'top' ? controlMirror : undefined, /*#__PURE__*/_react["default"].createElement(_Box.Box, {
overflow: "auto"
}, items.map(function (item, index) {
// Determine whether the label is done as a child or
// as an option Button kind property.
var child = !theme.button.option ? /*#__PURE__*/_react["default"].createElement(_Box.Box, {
align: "start",
pad: "small",
direction: "row",
gap: item.gap
}, item.reverse && item.label, item.icon, !item.reverse && item.label) : undefined; // if we have a child, turn on plain, and hoverIndicator
return (
/*#__PURE__*/
// eslint-disable-next-line react/no-array-index-key
_react["default"].createElement(_Box.Box, {
key: index,
flex: false
}, /*#__PURE__*/_react["default"].createElement(_Button.Button, _extends({
ref: function ref(r) {
buttonRefs[index] = r;
},
onFocus: function onFocus() {
return setActiveItemIndex(index);
},
active: activeItemIndex === index,
focusIndicator: false,
plain: !child ? undefined : true,
align: "start",
kind: !child ? 'option' : undefined,
hoverIndicator: !child ? undefined : 'background'
}, !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))
);
})), align.bottom === 'bottom' ? controlMirror : undefined))
}), content));
});
Menu.defaultProps = {
dropAlign: {
top: 'top',
left: 'left'
},
dropProps: {},
items: [],
messages: {
openMenu: 'Open Menu',
closeMenu: 'Close Menu'
},
justifyContent: 'start'
};
Menu.displayName = 'Menu';
var MenuDoc;
if (process.env.NODE_ENV !== 'production') {
MenuDoc = require('./doc').doc(Menu); // eslint-disable-line global-require
}
var MenuWrapper = MenuDoc || Menu;
exports.Menu = MenuWrapper;
;