@fluentui/react-northstar
Version:
A themable React component library.
490 lines (485 loc) • 17.8 kB
JavaScript
import _extends from "@babel/runtime/helpers/esm/extends";
import _invoke from "lodash/invoke";
import { menuItemBehavior, submenuBehavior } from '@fluentui/accessibility';
import { EventListener } from '@fluentui/react-component-event-listener';
import { focusAsync, mergeVariablesOverrides, useTelemetry, useAutoControlled, useFluentContext, getElementType, useUnhandledProps, useAccessibility, useStyles, useContextSelectors, useOnIFrameFocus } from '@fluentui/react-bindings';
import { Ref, handleRef } from '@fluentui/react-component-ref';
import * as customPropTypes from '@fluentui/react-proptypes';
import * as PropTypes from 'prop-types';
import * as React from 'react';
import { childrenExist, createShorthand, doesNodeContainClick, commonPropTypes, isFromKeyboard as isEventFromKeyboard, setWhatInputSource } from '../../utils';
import { Menu } from './Menu';
import { MenuItemIcon } from './MenuItemIcon';
import { MenuItemContent } from './MenuItemContent';
import { MenuItemIndicator } from './MenuItemIndicator';
import { MenuItemWrapper } from './MenuItemWrapper';
import { Popper, partitionPopperPropsFromShorthand } from '../../utils/positioner';
import { MenuContext } from './menuContext';
import { ChevronEndIcon } from '@fluentui/react-icons-northstar';
export var menuItemClassName = 'ui-menu__item';
export var menuItemSlotClassNames = {
submenu: menuItemClassName + "__submenu"
};
/**
* A MenuItem is an actionable item within a Menu.
*/
export var MenuItem = /*#__PURE__*/function () {
var MenuItem = /*#__PURE__*/React.forwardRef(function (inputProps, ref) {
var context = useFluentContext();
var _useTelemetry = useTelemetry(MenuItem.displayName, context.telemetry),
setStart = _useTelemetry.setStart,
setEnd = _useTelemetry.setEnd;
setStart();
var parentProps = useContextSelectors(MenuContext, {
active: function active(v) {
return v.activeIndex === inputProps.index;
},
onItemClick: function onItemClick(v) {
return v.onItemClick;
},
onItemSelect: function onItemSelect(v) {
return v.onItemSelect;
},
variables: function variables(v) {
return v.variables;
},
slotProps: function slotProps(v) {
return v.slotProps.item;
},
accessibility: function accessibility(v) {
return v.behaviors.item;
}
}); // TODO: we should improve typings for the useContextSelectors
var props = Object.assign({}, parentProps.slotProps, {
active: parentProps.active,
variables: parentProps.variables,
accessibility: parentProps.accessibility
}, inputProps);
var _props$accessibility = props.accessibility,
accessibility = _props$accessibility === void 0 ? menuItemBehavior : _props$accessibility,
children = props.children,
content = props.content,
icon = props.icon,
wrapper = props.wrapper,
primary = props.primary,
secondary = props.secondary,
active = props.active,
vertical = props.vertical,
indicator = props.indicator,
disabled = props.disabled,
underlined = props.underlined,
iconOnly = props.iconOnly,
inSubmenu = props.inSubmenu,
pills = props.pills,
pointing = props.pointing,
className = props.className,
design = props.design,
styles = props.styles,
variables = props.variables,
on = props.on,
index = props.index;
var _partitionPopperProps = partitionPopperPropsFromShorthand(props.menu),
menu = _partitionPopperProps[0],
positioningProps = _partitionPopperProps[1];
var _useAutoControlled = useAutoControlled({
defaultValue: props.defaultMenuOpen,
value: props.menuOpen,
initialValue: false
}),
menuOpen = _useAutoControlled[0],
setMenuOpen = _useAutoControlled[1];
useOnIFrameFocus(menuOpen, context.target, function (e) {
setMenuOpen(function (__) {
_invoke(props, 'onMenuOpenChange', e, Object.assign({}, props, {
menuOpen: false
}));
return false;
});
});
var _React$useState = React.useState(false),
isFromKeyboard = _React$useState[0],
setIsFromKeyboard = _React$useState[1];
var ElementType = getElementType(props);
var unhandledProps = useUnhandledProps(MenuItem.handledProps, props);
var getA11yProps = useAccessibility(accessibility, {
debugName: Menu.displayName,
actionHandlers: {
performClick: function performClick(event) {
return !event.defaultPrevented && handleClick(event);
},
openMenu: function (_openMenu) {
function openMenu(_x) {
return _openMenu.apply(this, arguments);
}
openMenu.toString = function () {
return _openMenu.toString();
};
return openMenu;
}(function (event) {
return openMenu(event);
}),
closeAllMenusAndFocusNextParentItem: function closeAllMenusAndFocusNextParentItem(event) {
return closeAllMenus(event);
},
closeMenu: function (_closeMenu) {
function closeMenu(_x2) {
return _closeMenu.apply(this, arguments);
}
closeMenu.toString = function () {
return _closeMenu.toString();
};
return closeMenu;
}(function (event) {
return closeMenu(event);
}),
closeMenuAndFocusTrigger: function closeMenuAndFocusTrigger(event) {
return closeMenu(event, true);
},
doNotNavigateNextParentItem: function doNotNavigateNextParentItem(event) {
event.stopPropagation();
},
closeAllMenus: function (_closeAllMenus) {
function closeAllMenus(_x3) {
return _closeAllMenus.apply(this, arguments);
}
closeAllMenus.toString = function () {
return _closeAllMenus.toString();
};
return closeAllMenus;
}(function (event) {
return closeAllMenus(event);
})
},
mapPropsToBehavior: function mapPropsToBehavior() {
return {
menuOpen: menuOpen,
hasMenu: !!menu,
disabled: disabled,
vertical: vertical,
active: active // for tabBehavior
};
},
rtl: context.rtl
});
var _useStyles = useStyles(MenuItem.displayName, {
className: menuItemClassName,
mapPropsToStyles: function mapPropsToStyles() {
return {
primary: primary,
underlined: underlined,
active: active,
vertical: vertical,
pointing: pointing,
secondary: secondary,
disabled: disabled,
iconOnly: iconOnly,
pills: pills,
inSubmenu: inSubmenu,
isFromKeyboard: isFromKeyboard
};
},
mapPropsToInlineStyles: function mapPropsToInlineStyles() {
return {
className: className,
design: design,
styles: styles,
variables: mergeVariablesOverrides(parentProps.variables, variables)
};
},
rtl: context.rtl
}),
classes = _useStyles.classes,
resolvedStyles = _useStyles.styles;
var menuRef = React.useRef();
var itemRef = React.useRef();
var handleWrapperBlur = function handleWrapperBlur(e) {
if (!props.inSubmenu && !e.currentTarget.contains(e.relatedTarget)) {
trySetMenuOpen(false, e);
}
};
var dismissOnScroll = function dismissOnScroll(e) {
if (!isSubmenuOpen()) return;
// we only need to dismiss if the scroll happens outside the menu
if (!menuRef.current.contains(e.target)) {
trySetMenuOpen(false, e);
}
};
var outsideClickHandler = function outsideClickHandler(e) {
if (!isSubmenuOpen()) return;
if (!doesNodeContainClick(itemRef.current, e, context.target) && !doesNodeContainClick(menuRef.current, e, context.target)) {
trySetMenuOpen(false, e);
}
};
var performClick = function performClick(e) {
if (menu) {
if (doesNodeContainClick(menuRef.current, e, context.target)) {
// submenu was clicked => close it and propagate
trySetMenuOpen(false, e, function () {
return focusAsync(itemRef.current);
});
} else {
// the menuItem element was clicked => toggle the open/close and stop propagation
trySetMenuOpen(active && on !== 'hover' ? !menuOpen : true, e);
e.stopPropagation();
e.preventDefault();
}
}
};
var handleClick = function handleClick(e) {
if (disabled) {
e.preventDefault();
return;
}
performClick(e);
_invoke(props, 'onClick', e, props);
_invoke(parentProps, 'onItemClick', e, props);
};
var handleBlur = function handleBlur(e) {
setIsFromKeyboard(false);
_invoke(props, 'onBlur', e, props);
};
var handleFocus = function handleFocus(e) {
setIsFromKeyboard(isEventFromKeyboard());
_invoke(props, 'onFocus', e, props);
};
var isSubmenuOpen = function isSubmenuOpen() {
return !!(menu && menuOpen);
};
var closeAllMenus = function closeAllMenus(e) {
if (!isSubmenuOpen()) {
return;
}
trySetMenuOpen(false, e, function () {
if (!inSubmenu) {
focusAsync(itemRef.current);
}
});
// avoid spacebar scrolling the page
if (!inSubmenu) {
e.preventDefault();
}
};
var closeMenu = function closeMenu(e, forceTriggerFocus) {
if (!isSubmenuOpen()) {
return;
}
var shouldStopPropagation = inSubmenu || props.vertical;
trySetMenuOpen(false, e, function () {
if (forceTriggerFocus || shouldStopPropagation) {
focusAsync(itemRef.current);
}
});
if (forceTriggerFocus || shouldStopPropagation) {
e.stopPropagation();
}
};
var openMenu = function openMenu(e) {
if (menu && !menuOpen) {
trySetMenuOpen(true, e);
_invoke(parentProps, 'onItemSelect', e, index);
_invoke(props, 'onActiveChanged', e, Object.assign({}, props, {
active: true
}));
e.stopPropagation();
e.preventDefault();
}
};
var rootHandlers = Object.assign({}, !wrapper && Object.assign({
onClick: handleClick
}, on === 'hover' && {
onMouseEnter: function onMouseEnter(e) {
setWhatInputSource(context.target, 'mouse');
trySetMenuOpen(true, e);
_invoke(props, 'onMouseEnter', e, props);
_invoke(parentProps, 'onItemSelect', e, index);
},
onMouseLeave: function onMouseLeave(e) {
trySetMenuOpen(false, e);
_invoke(props, 'onMouseLeave', e, props);
}
}));
var trySetMenuOpen = function trySetMenuOpen(newValue, e, onStateChanged) {
setMenuOpen(newValue);
// The reason why post-effect is not passed as callback to trySetState method
// is that in 'controlled' mode the post-effect is applied before final re-rendering
// which cause a broken behavior: for e.g. when it is needed to focus submenu trigger on ESC.
// TODO: all DOM post-effects should be applied at componentDidMount & componentDidUpdated stages.
onStateChanged && onStateChanged();
_invoke(props, 'onMenuOpenChange', e, Object.assign({}, props, {
menuOpen: newValue
}));
};
var menuItemInner = /*#__PURE__*/React.createElement(Ref, {
innerRef: function innerRef(node) {
itemRef.current = node;
handleRef(ref, node);
}
}, /*#__PURE__*/React.createElement(ElementType, _extends({}, getA11yProps('root', Object.assign({
className: classes.root,
disabled: disabled,
onBlur: handleBlur,
onFocus: handleFocus,
onClick: handleClick
}, unhandledProps)), rootHandlers), childrenExist(children) ? children : /*#__PURE__*/React.createElement(React.Fragment, null, createShorthand(MenuItemIcon, icon, {
defaultProps: function defaultProps() {
return getA11yProps('icon', {
hasContent: !!content,
iconOnly: iconOnly
});
}
}), createShorthand(MenuItemContent, content, {
defaultProps: function defaultProps() {
return getA11yProps('content', {
hasIcon: !!icon,
hasMenu: !!menu,
inSubmenu: inSubmenu,
vertical: vertical
});
}
}), menu && createShorthand(MenuItemIndicator, indicator, {
defaultProps: function defaultProps() {
return getA11yProps('indicator', {
iconOnly: iconOnly,
vertical: vertical,
inSubmenu: inSubmenu,
active: active,
primary: primary,
underlined: underlined
});
}
}))));
var handleWrapperOverrides = function handleWrapperOverrides(predefinedProps) {
return Object.assign({
onBlur: function onBlur(e) {
handleWrapperBlur(e);
_invoke(predefinedProps, 'onBlur', e, props);
}
}, on === 'hover' && {
onMouseEnter: function onMouseEnter(e) {
setWhatInputSource(context.target, 'mouse');
trySetMenuOpen(true, e);
_invoke(predefinedProps, 'onMouseEnter', e, props);
_invoke(parentProps, 'onItemSelect', e, index);
},
onMouseLeave: function onMouseLeave(e) {
trySetMenuOpen(false, e);
_invoke(predefinedProps, 'onMouseLeave', e, props);
}
});
};
var maybeSubmenu = menu && menuOpen ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Ref, {
innerRef: menuRef
}, /*#__PURE__*/React.createElement(Popper, _extends({
align: vertical ? 'top' : context.rtl ? 'end' : 'start',
position: vertical ? context.rtl ? 'before' : 'after' : 'below',
targetRef: itemRef
}, positioningProps), createShorthand(Menu, menu, {
defaultProps: function defaultProps() {
return {
accessibility: submenuBehavior,
className: menuItemSlotClassNames.submenu,
vertical: true,
primary: props.primary,
secondary: props.secondary,
submenu: true,
styles: resolvedStyles.menu,
indicator: props.indicator
};
},
overrideProps: function overrideProps(predefinedProps) {
return {
onClick: function onClick(e) {
handleClick(e);
_invoke(predefinedProps, 'onClick', e, props);
}
};
}
}))), /*#__PURE__*/React.createElement(EventListener, {
listener: outsideClickHandler,
target: context.target,
type: "click"
}), /*#__PURE__*/React.createElement(EventListener, {
listener: dismissOnScroll,
target: context.target,
type: "wheel",
capture: true
}), /*#__PURE__*/React.createElement(EventListener, {
listener: dismissOnScroll,
target: context.target,
type: "touchmove",
capture: true
})) : null;
if (wrapper) {
var wrapperElement = createShorthand(MenuItemWrapper, wrapper, {
defaultProps: function defaultProps() {
return getA11yProps('wrapper', {
active: active,
disabled: disabled,
iconOnly: iconOnly,
isFromKeyboard: isFromKeyboard,
pills: pills,
pointing: pointing,
secondary: secondary,
underlined: underlined,
vertical: vertical,
primary: primary,
on: on,
variables: variables
});
},
overrideProps: function overrideProps(predefinedProps) {
return Object.assign({
children: /*#__PURE__*/React.createElement(React.Fragment, null, menuItemInner, maybeSubmenu)
}, handleWrapperOverrides(predefinedProps));
}
});
setEnd();
return wrapperElement;
}
setEnd();
return menuItemInner;
});
MenuItem.displayName = 'MenuItem';
MenuItem.propTypes = Object.assign({}, commonPropTypes.createCommon({
content: 'shorthand'
}), {
active: PropTypes.bool,
disabled: PropTypes.bool,
icon: customPropTypes.shorthandAllowingChildren,
on: PropTypes.oneOf(['hover']),
iconOnly: PropTypes.bool,
index: PropTypes.number,
itemPosition: PropTypes.number,
itemsCount: PropTypes.number,
onClick: PropTypes.func,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
pills: PropTypes.bool,
pointing: PropTypes.oneOf(['start', 'end', true, false]),
primary: customPropTypes.every([customPropTypes.disallow(['secondary']), PropTypes.bool]),
secondary: customPropTypes.every([customPropTypes.disallow(['primary']), PropTypes.bool]),
underlined: PropTypes.bool,
vertical: PropTypes.bool,
wrapper: PropTypes.oneOfType([PropTypes.node, PropTypes.object]),
menu: PropTypes.oneOfType([customPropTypes.itemShorthand, customPropTypes.collectionShorthand]),
menuOpen: PropTypes.bool,
defaultMenuOpen: PropTypes.bool,
onActiveChanged: PropTypes.func,
inSubmenu: PropTypes.bool,
indicator: customPropTypes.shorthandAllowingChildren,
onMenuOpenChange: PropTypes.func
});
MenuItem.handledProps = Object.keys(MenuItem.propTypes);
MenuItem.shorthandConfig = {
mappedProp: 'content'
};
MenuItem.defaultProps = {
as: 'a',
wrapper: {},
indicator: /*#__PURE__*/React.createElement(ChevronEndIcon, {
outline: true
})
};
return MenuItem;
}();
//# sourceMappingURL=MenuItem.js.map