UNPKG

@rc-component/menu

Version:
451 lines (428 loc) 16.2 kB
"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;