@rc-component/menu
Version:
menu ui component for react
451 lines (428 loc) • 16.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _classnames = _interopRequireDefault(require("classnames"));
var _rcOverflow = _interopRequireDefault(require("rc-overflow"));
var _useControlledState = _interopRequireDefault(require("@rc-component/util/lib/hooks/useControlledState"));
var _useId = _interopRequireDefault(require("@rc-component/util/lib/hooks/useId"));
var _isEqual = _interopRequireDefault(require("@rc-component/util/lib/isEqual"));
var _warning = _interopRequireDefault(require("@rc-component/util/lib/warning"));
var _react = _interopRequireWildcard(require("react"));
var React = _react;
var _reactDom = require("react-dom");
var _IdContext = require("./context/IdContext");
var _MenuContext = _interopRequireDefault(require("./context/MenuContext"));
var _PathContext = require("./context/PathContext");
var _PrivateContext = _interopRequireDefault(require("./context/PrivateContext"));
var _useAccessibility = require("./hooks/useAccessibility");
var _useKeyRecords = _interopRequireWildcard(require("./hooks/useKeyRecords"));
var _useMemoCallback = _interopRequireDefault(require("./hooks/useMemoCallback"));
var _MenuItem = _interopRequireDefault(require("./MenuItem"));
var _SubMenu = _interopRequireDefault(require("./SubMenu"));
var _nodeUtil = require("./utils/nodeUtil");
var _warnUtil = require("./utils/warnUtil");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _extends() { _extends = Object.assign ? Object.assign.bind() : 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); }
/**
* Menu modify after refactor:
* ## Add
* - disabled
*
* ## Remove
* - openTransitionName
* - openAnimation
* - onDestroy
* - siderCollapsed: Seems antd do not use this prop (Need test in antd)
* - collapsedWidth: Seems this logic should be handle by antd Layout.Sider
*/
// optimize for render
const EMPTY_LIST = [];
const Menu = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
prefixCls = 'rc-menu',
rootClassName,
style,
className,
styles,
classNames: menuClassNames,
tabIndex = 0,
items,
children,
direction,
id,
// Mode
mode = 'vertical',
inlineCollapsed,
// Disabled
disabled,
disabledOverflow,
// Open
subMenuOpenDelay = 0.1,
subMenuCloseDelay = 0.1,
forceSubMenuRender,
defaultOpenKeys,
openKeys,
// Active
activeKey,
defaultActiveFirst,
// Selection
selectable = true,
multiple = false,
defaultSelectedKeys,
selectedKeys,
onSelect,
onDeselect,
// Level
inlineIndent = 24,
// Motion
motion,
defaultMotions,
// Popup
triggerSubMenuAction = 'hover',
builtinPlacements,
// Icon
itemIcon,
expandIcon,
overflowedIndicator = '...',
overflowedIndicatorPopupClassName,
// Function
getPopupContainer,
// Events
onClick,
onOpenChange,
onKeyDown,
// Deprecated
openAnimation,
openTransitionName,
// Internal
_internalRenderMenuItem,
_internalRenderSubMenuItem,
_internalComponents,
popupRender,
...restProps
} = props;
const [childList, measureChildList] = React.useMemo(() => [(0, _nodeUtil.parseItems)(children, items, EMPTY_LIST, _internalComponents, prefixCls), (0, _nodeUtil.parseItems)(children, items, EMPTY_LIST, {}, prefixCls)], [children, items, _internalComponents]);
const [mounted, setMounted] = React.useState(false);
const containerRef = React.useRef();
const uuid = (0, _useId.default)(id ? `rc-menu-uuid-${id}` : 'rc-menu-uuid');
const isRtl = direction === 'rtl';
// ========================= Warn =========================
if (process.env.NODE_ENV !== 'production') {
(0, _warning.default)(!openAnimation && !openTransitionName, '`openAnimation` and `openTransitionName` is removed. Please use `motion` or `defaultMotion` instead.');
}
// ========================= Open =========================
const [innerOpenKeys, setMergedOpenKeys] = (0, _useControlledState.default)(defaultOpenKeys, openKeys);
const mergedOpenKeys = innerOpenKeys || EMPTY_LIST;
// React 18 will merge mouse event which means we open key will not sync
// ref: https://github.com/ant-design/ant-design/issues/38818
const triggerOpenKeys = (keys, forceFlush = false) => {
function doUpdate() {
setMergedOpenKeys(keys);
onOpenChange?.(keys);
}
if (forceFlush) {
(0, _reactDom.flushSync)(doUpdate);
} else {
doUpdate();
}
};
// >>>>> Cache & Reset open keys when inlineCollapsed changed
const [inlineCacheOpenKeys, setInlineCacheOpenKeys] = React.useState(mergedOpenKeys);
const mountRef = React.useRef(false);
// ========================= Mode =========================
const [mergedMode, mergedInlineCollapsed] = React.useMemo(() => {
if ((mode === 'inline' || mode === 'vertical') && inlineCollapsed) {
return ['vertical', inlineCollapsed];
}
return [mode, false];
}, [mode, inlineCollapsed]);
const isInlineMode = mergedMode === 'inline';
const [internalMode, setInternalMode] = React.useState(mergedMode);
const [internalInlineCollapsed, setInternalInlineCollapsed] = React.useState(mergedInlineCollapsed);
React.useEffect(() => {
setInternalMode(mergedMode);
setInternalInlineCollapsed(mergedInlineCollapsed);
if (!mountRef.current) {
return;
}
// Synchronously update MergedOpenKeys
if (isInlineMode) {
setMergedOpenKeys(inlineCacheOpenKeys);
} else {
// Trigger open event in case its in control
triggerOpenKeys(EMPTY_LIST);
}
}, [mergedMode, mergedInlineCollapsed]);
// ====================== Responsive ======================
const [lastVisibleIndex, setLastVisibleIndex] = React.useState(0);
const allVisible = lastVisibleIndex >= childList.length - 1 || internalMode !== 'horizontal' || disabledOverflow;
// Cache
React.useEffect(() => {
if (isInlineMode) {
setInlineCacheOpenKeys(mergedOpenKeys);
}
}, [mergedOpenKeys]);
React.useEffect(() => {
mountRef.current = true;
return () => {
mountRef.current = false;
};
}, []);
// ========================= Path =========================
const {
registerPath,
unregisterPath,
refreshOverflowKeys,
isSubPathKey,
getKeyPath,
getKeys,
getSubPathKeys
} = (0, _useKeyRecords.default)();
const registerPathContext = React.useMemo(() => ({
registerPath,
unregisterPath
}), [registerPath, unregisterPath]);
const pathUserContext = React.useMemo(() => ({
isSubPathKey
}), [isSubPathKey]);
React.useEffect(() => {
refreshOverflowKeys(allVisible ? EMPTY_LIST : childList.slice(lastVisibleIndex + 1).map(child => child.key));
}, [lastVisibleIndex, allVisible]);
// ======================== Active ========================
const [mergedActiveKey, setMergedActiveKey] = (0, _useControlledState.default)(activeKey || defaultActiveFirst && childList[0]?.key, activeKey);
const onActive = (0, _useMemoCallback.default)(key => {
setMergedActiveKey(key);
});
const onInactive = (0, _useMemoCallback.default)(() => {
setMergedActiveKey(undefined);
});
(0, _react.useImperativeHandle)(ref, () => {
return {
list: containerRef.current,
focus: options => {
const keys = getKeys();
const {
elements,
key2element,
element2key
} = (0, _useAccessibility.refreshElements)(keys, uuid);
const focusableElements = (0, _useAccessibility.getFocusableElements)(containerRef.current, elements);
let shouldFocusKey;
if (mergedActiveKey && keys.includes(mergedActiveKey)) {
shouldFocusKey = mergedActiveKey;
} else {
shouldFocusKey = focusableElements[0] ? element2key.get(focusableElements[0]) : childList.find(node => !node.props.disabled)?.key;
}
const elementToFocus = key2element.get(shouldFocusKey);
if (shouldFocusKey && elementToFocus) {
elementToFocus?.focus?.(options);
}
},
findItem: ({
key: itemKey
}) => {
const keys = getKeys();
const {
key2element
} = (0, _useAccessibility.refreshElements)(keys, uuid);
return key2element.get(itemKey) || null;
}
};
});
// ======================== Select ========================
// >>>>> Select keys
const [internalSelectKeys, setMergedSelectKeys] = (0, _useControlledState.default)(defaultSelectedKeys || [], selectedKeys);
const mergedSelectKeys = React.useMemo(() => {
if (Array.isArray(internalSelectKeys)) {
return internalSelectKeys;
}
if (internalSelectKeys === null || internalSelectKeys === undefined) {
return EMPTY_LIST;
}
return [internalSelectKeys];
}, [internalSelectKeys]);
// >>>>> Trigger select
const triggerSelection = info => {
if (selectable) {
// Insert or Remove
const {
key: targetKey
} = info;
const exist = mergedSelectKeys.includes(targetKey);
let newSelectKeys;
if (multiple) {
if (exist) {
newSelectKeys = mergedSelectKeys.filter(key => key !== targetKey);
} else {
newSelectKeys = [...mergedSelectKeys, targetKey];
}
} else {
newSelectKeys = [targetKey];
}
setMergedSelectKeys(newSelectKeys);
// Trigger event
const selectInfo = {
...info,
selectedKeys: newSelectKeys
};
if (exist) {
onDeselect?.(selectInfo);
} else {
onSelect?.(selectInfo);
}
}
// Whatever selectable, always close it
if (!multiple && mergedOpenKeys.length && internalMode !== 'inline') {
triggerOpenKeys(EMPTY_LIST);
}
};
// ========================= Open =========================
/**
* Click for item. SubMenu do not have selection status
*/
const onInternalClick = (0, _useMemoCallback.default)(info => {
onClick?.((0, _warnUtil.warnItemProp)(info));
triggerSelection(info);
});
const onInternalOpenChange = (0, _useMemoCallback.default)((key, open) => {
let newOpenKeys = mergedOpenKeys.filter(k => k !== key);
if (open) {
newOpenKeys.push(key);
} else if (internalMode !== 'inline') {
// We need find all related popup to close
const subPathKeys = getSubPathKeys(key);
newOpenKeys = newOpenKeys.filter(k => !subPathKeys.has(k));
}
if (!(0, _isEqual.default)(mergedOpenKeys, newOpenKeys, true)) {
triggerOpenKeys(newOpenKeys, true);
}
});
// ==================== Accessibility =====================
const triggerAccessibilityOpen = (key, open) => {
const nextOpen = open ?? !mergedOpenKeys.includes(key);
onInternalOpenChange(key, nextOpen);
};
const onInternalKeyDown = (0, _useAccessibility.useAccessibility)(internalMode, mergedActiveKey, isRtl, uuid, containerRef, getKeys, getKeyPath, setMergedActiveKey, triggerAccessibilityOpen, onKeyDown);
// ======================== Effect ========================
React.useEffect(() => {
setMounted(true);
}, []);
// ======================= Context ========================
const privateContext = React.useMemo(() => ({
_internalRenderMenuItem,
_internalRenderSubMenuItem
}), [_internalRenderMenuItem, _internalRenderSubMenuItem]);
// ======================== Render ========================
// >>>>> Children
const wrappedChildList = internalMode !== 'horizontal' || disabledOverflow ? childList :
// Need wrap for overflow dropdown that do not response for open
childList.map((child, index) =>
/*#__PURE__*/
// Always wrap provider to avoid sub node re-mount
React.createElement(_MenuContext.default, {
key: child.key,
overflowDisabled: index > lastVisibleIndex,
classNames: menuClassNames,
styles: styles
}, child));
// >>>>> Container
const container = /*#__PURE__*/React.createElement(_rcOverflow.default, _extends({
id: id,
ref: containerRef,
prefixCls: `${prefixCls}-overflow`,
component: "ul",
itemComponent: _MenuItem.default,
className: (0, _classnames.default)(prefixCls, `${prefixCls}-root`, `${prefixCls}-${internalMode}`, className, {
[`${prefixCls}-inline-collapsed`]: internalInlineCollapsed,
[`${prefixCls}-rtl`]: isRtl
}, rootClassName),
dir: direction,
style: style,
role: "menu",
tabIndex: tabIndex,
data: wrappedChildList,
renderRawItem: node => node,
renderRawRest: omitItems => {
// We use origin list since wrapped list use context to prevent open
const len = omitItems.length;
const originOmitItems = len ? childList.slice(-len) : null;
return /*#__PURE__*/React.createElement(_SubMenu.default, {
eventKey: _useKeyRecords.OVERFLOW_KEY,
title: overflowedIndicator,
disabled: allVisible,
internalPopupClose: len === 0,
popupClassName: overflowedIndicatorPopupClassName
}, originOmitItems);
},
maxCount: internalMode !== 'horizontal' || disabledOverflow ? _rcOverflow.default.INVALIDATE : _rcOverflow.default.RESPONSIVE,
ssr: "full",
"data-menu-list": true,
onVisibleChange: newLastIndex => {
setLastVisibleIndex(newLastIndex);
},
onKeyDown: onInternalKeyDown
}, restProps));
// >>>>> Render
return /*#__PURE__*/React.createElement(_PrivateContext.default.Provider, {
value: privateContext
}, /*#__PURE__*/React.createElement(_IdContext.IdContext.Provider, {
value: uuid
}, /*#__PURE__*/React.createElement(_MenuContext.default, {
prefixCls: prefixCls,
rootClassName: rootClassName,
classNames: menuClassNames,
styles: styles,
mode: internalMode,
openKeys: mergedOpenKeys,
rtl: isRtl
// Disabled
,
disabled: disabled
// Motion
,
motion: mounted ? motion : null,
defaultMotions: mounted ? defaultMotions : null
// Active
,
activeKey: mergedActiveKey,
onActive: onActive,
onInactive: onInactive
// Selection
,
selectedKeys: mergedSelectKeys
// Level
,
inlineIndent: inlineIndent
// Popup
,
subMenuOpenDelay: subMenuOpenDelay,
subMenuCloseDelay: subMenuCloseDelay,
forceSubMenuRender: forceSubMenuRender,
builtinPlacements: builtinPlacements,
triggerSubMenuAction: triggerSubMenuAction,
getPopupContainer: getPopupContainer
// Icon
,
itemIcon: itemIcon,
expandIcon: expandIcon
// Events
,
onItemClick: onInternalClick,
onOpenChange: onInternalOpenChange,
popupRender: popupRender
}, /*#__PURE__*/React.createElement(_PathContext.PathUserContext.Provider, {
value: pathUserContext
}, container), /*#__PURE__*/React.createElement("div", {
style: {
display: 'none'
},
"aria-hidden": true
}, /*#__PURE__*/React.createElement(_PathContext.PathRegisterContext.Provider, {
value: registerPathContext
}, measureChildList)))));
});
var _default = exports.default = Menu;