From fcf54995a6528ec2f8f00a192b97b6ded92ca058 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 6 Apr 2021 17:39:39 +0800 Subject: [PATCH 01/93] start up --- .prettierrc | 3 +- assets/menu.less | 6 + docs/demo/debug.md | 3 + docs/examples/debug.tsx | 16 + package.json | 1 + src/DOMWrap.tsx | 372 ------------------- src/Divider.tsx | 25 -- src/Menu.tsx | 531 ++++++--------------------- src/MenuItem.tsx | 289 ++------------- src/MenuItemGroup.tsx | 63 ---- src/SubMenu.tsx | 710 ------------------------------------ src/SubPopupMenu.tsx | 483 ------------------------ src/dataUtil.ts | 29 ++ src/index.ts | 35 ++ src/index.tsx | 20 - src/interface.ts | 65 +--- src/placements.ts | 52 --- src/sugar/Divider.tsx | 4 + src/sugar/MenuItem.tsx | 6 + src/sugar/MenuItemGroup.tsx | 4 + src/sugar/SubMenu.tsx | 4 + src/util.ts | 166 --------- src/utils/isMobile.ts | 121 ------ src/utils/legacyUtil.ts | 58 --- 24 files changed, 264 insertions(+), 2802 deletions(-) create mode 100644 assets/menu.less create mode 100644 docs/demo/debug.md create mode 100644 docs/examples/debug.tsx delete mode 100644 src/DOMWrap.tsx delete mode 100644 src/Divider.tsx delete mode 100644 src/MenuItemGroup.tsx delete mode 100644 src/SubMenu.tsx delete mode 100644 src/SubPopupMenu.tsx create mode 100644 src/dataUtil.ts create mode 100644 src/index.ts delete mode 100644 src/index.tsx delete mode 100644 src/placements.ts create mode 100644 src/sugar/Divider.tsx create mode 100644 src/sugar/MenuItem.tsx create mode 100644 src/sugar/MenuItemGroup.tsx create mode 100644 src/sugar/SubMenu.tsx delete mode 100644 src/util.ts delete mode 100644 src/utils/isMobile.ts delete mode 100644 src/utils/legacyUtil.ts diff --git a/.prettierrc b/.prettierrc index 27dd8afb..73e9b594 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,5 +3,6 @@ "semi": true, "singleQuote": true, "tabWidth": 2, - "trailingComma": "all" + "trailingComma": "all", + "arrowParens": "avoid" } diff --git a/assets/menu.less b/assets/menu.less new file mode 100644 index 00000000..4d75a8f5 --- /dev/null +++ b/assets/menu.less @@ -0,0 +1,6 @@ +@menuPrefixCls: rc-menu; + +.@{menuPrefixCls} { + display: flex; + flex-wrap: nowrap; +} \ No newline at end of file diff --git a/docs/demo/debug.md b/docs/demo/debug.md new file mode 100644 index 00000000..4f7c1fe5 --- /dev/null +++ b/docs/demo/debug.md @@ -0,0 +1,3 @@ +## single + + \ No newline at end of file diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx new file mode 100644 index 00000000..034b78d3 --- /dev/null +++ b/docs/examples/debug.tsx @@ -0,0 +1,16 @@ +/* eslint no-console:0 */ + +import React from 'react'; +import Menu from '../../src'; +import '../../assets/index.less'; +import '../../assets/menu.less'; + +// const menuOptions = [{ key: 'bamboo' }, { key: 'light', label: 'Light' }]; + +export default () => { + return ( + + Navigation One + + ); +}; diff --git a/package.json b/package.json index 7df3220d..9ea6e1bd 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "classnames": "2.x", "mini-store": "^3.0.1", "rc-motion": "^2.0.1", + "rc-overflow": "^1.2.0-alpha.0", "rc-trigger": "^5.1.2", "rc-util": "^5.7.0", "resize-observer-polyfill": "^1.5.0", diff --git a/src/DOMWrap.tsx b/src/DOMWrap.tsx deleted file mode 100644 index 2f994d1f..00000000 --- a/src/DOMWrap.tsx +++ /dev/null @@ -1,372 +0,0 @@ -import * as React from 'react'; -import ResizeObserver from 'resize-observer-polyfill'; -import SubMenu from './SubMenu'; -import { getWidth, setStyle, menuAllProps } from './util'; -import type { MenuMode } from './interface'; - -const MENUITEM_OVERFLOWED_CLASSNAME = 'menuitem-overflowed'; -const FLOAT_PRECISION_ADJUST = 0.5; - -interface DOMWrapProps { - className?: string; - children?: React.ReactElement[]; - mode?: MenuMode; - prefixCls?: string; - level?: number; - theme?: string; - overflowedIndicator?: React.ReactNode; - visible?: boolean; - tag?: string; - style?: React.CSSProperties; -} - -interface DOMWrapState { - lastVisibleIndex: number; -} - -class DOMWrap extends React.Component { - static defaultProps = { - tag: 'div', - className: '', - }; - - overflowedIndicatorWidth: number; - - resizeObserver = null; - - mutationObserver = null; - - // original scroll size of the list - originalTotalWidth = 0; - - // copy of overflowed items - overflowedItems: React.ReactElement[] = []; - - // cache item of the original items (so we can track the size and order) - menuItemSizes: number[] = []; - - cancelFrameId: number = null; - - state: DOMWrapState = { - lastVisibleIndex: undefined, - }; - - childRef = React.createRef(); - - componentDidMount() { - this.setChildrenWidthAndResize(); - if (this.props.level === 1 && this.props.mode === 'horizontal') { - const menuUl = this.childRef.current; - if (!menuUl) { - return; - } - this.resizeObserver = new ResizeObserver((entries) => { - entries.forEach(() => { - const { cancelFrameId } = this; - cancelAnimationFrame(cancelFrameId); - this.cancelFrameId = requestAnimationFrame( - this.setChildrenWidthAndResize, - ); - }); - }); - - [].slice - .call(menuUl.children) - .concat(menuUl) - .forEach((el: HTMLElement) => { - this.resizeObserver.observe(el); - }); - - if (typeof MutationObserver !== 'undefined') { - this.mutationObserver = new MutationObserver(() => { - this.resizeObserver.disconnect(); - [].slice - .call(menuUl.children) - .concat(menuUl) - .forEach((el: HTMLElement) => { - this.resizeObserver.observe(el); - }); - this.setChildrenWidthAndResize(); - }); - this.mutationObserver.observe(menuUl, { - attributes: false, - childList: true, - subTree: false, - }); - } - } - } - - componentWillUnmount() { - if (this.resizeObserver) { - this.resizeObserver.disconnect(); - } - if (this.mutationObserver) { - this.mutationObserver.disconnect(); - } - cancelAnimationFrame(this.cancelFrameId); - } - - // get all valid menuItem nodes - getMenuItemNodes = (): HTMLElement[] => { - const { prefixCls } = this.props; - const ul = this.childRef.current; - if (!ul) { - return []; - } - - // filter out all overflowed indicator placeholder - return [].slice - .call(ul.children) - .filter( - (node: HTMLElement) => - node.className.split(' ').indexOf(`${prefixCls}-overflowed-submenu`) < - 0, - ); - }; - - getOverflowedSubMenuItem = ( - keyPrefix: string, - overflowedItems: React.ReactElement[], - renderPlaceholder?: boolean, - ): React.ReactElement => { - const { overflowedIndicator, level, mode, prefixCls, theme } = this.props; - if (level !== 1 || mode !== 'horizontal') { - return null; - } - // put all the overflowed item inside a submenu - // with a title of overflow indicator ('...') - const copy = this.props.children[0]; - const { - children: throwAway, - title, - style: propStyle, - ...rest - } = copy.props; - - let style: React.CSSProperties = { ...propStyle }; - let key = `${keyPrefix}-overflowed-indicator`; - let eventKey = `${keyPrefix}-overflowed-indicator`; - - if (overflowedItems.length === 0 && renderPlaceholder !== true) { - style = { - ...style, - display: 'none', - }; - } else if (renderPlaceholder) { - style = { - ...style, - visibility: 'hidden', - // prevent from taking normal dom space - position: 'absolute', - }; - key = `${key}-placeholder`; - eventKey = `${eventKey}-placeholder`; - } - - const popupClassName = theme ? `${prefixCls}-${theme}` : ''; - const props = {}; - menuAllProps.forEach((k) => { - if (rest[k] !== undefined) { - props[k] = rest[k]; - } - }); - - return ( - - {overflowedItems} - - ); - }; - - // memorize rendered menuSize - setChildrenWidthAndResize = () => { - if (this.props.mode !== 'horizontal') { - return; - } - const ul = this.childRef.current; - - if (!ul) { - return; - } - - const ulChildrenNodes = ul.children; - - if (!ulChildrenNodes || ulChildrenNodes.length === 0) { - return; - } - - const lastOverflowedIndicatorPlaceholder = ul.children[ - ulChildrenNodes.length - 1 - ] as HTMLElement; - - // need last overflowed indicator for calculating length; - setStyle(lastOverflowedIndicatorPlaceholder, 'display', 'inline-block'); - - const menuItemNodes = this.getMenuItemNodes(); - - // reset display attribute for all hidden elements caused by overflow to calculate updated width - // and then reset to original state after width calculation - - const overflowedItems = menuItemNodes.filter( - (c) => c.className.split(' ').indexOf(MENUITEM_OVERFLOWED_CLASSNAME) >= 0, - ); - - overflowedItems.forEach((c) => { - setStyle(c, 'display', 'inline-block'); - }); - - this.menuItemSizes = menuItemNodes.map((c) => getWidth(c, true)); - - overflowedItems.forEach((c) => { - setStyle(c, 'display', 'none'); - }); - this.overflowedIndicatorWidth = getWidth( - ul.children[ul.children.length - 1] as HTMLElement, - true, - ); - this.originalTotalWidth = this.menuItemSizes.reduce( - (acc, cur) => acc + cur, - 0, - ); - this.handleResize(); - // prevent the overflowed indicator from taking space; - setStyle(lastOverflowedIndicatorPlaceholder, 'display', 'none'); - }; - - handleResize = () => { - if (this.props.mode !== 'horizontal') { - return; - } - - const ul = this.childRef.current; - if (!ul) { - return; - } - const width = getWidth(ul); - - this.overflowedItems = []; - let currentSumWidth = 0; - - // index for last visible child in horizontal mode - let lastVisibleIndex: number; - - // float number comparison could be problematic - // e.g. 0.1 + 0.2 > 0.3 =====> true - // thus using FLOAT_PRECISION_ADJUST as buffer to help the situation - if (this.originalTotalWidth > width + FLOAT_PRECISION_ADJUST) { - lastVisibleIndex = -1; - - this.menuItemSizes.forEach((liWidth) => { - currentSumWidth += liWidth; - if (currentSumWidth + this.overflowedIndicatorWidth <= width) { - lastVisibleIndex += 1; - } - }); - } - - this.setState({ lastVisibleIndex }); - }; - - renderChildren(children: React.ReactElement[]) { - // need to take care of overflowed items in horizontal mode - const { lastVisibleIndex } = this.state; - return (children || []).reduce( - ( - acc: React.ReactElement[], - childNode: React.ReactElement, - index: number, - ) => { - let item = childNode; - if (this.props.mode === 'horizontal') { - let overflowed = this.getOverflowedSubMenuItem( - childNode.props.eventKey, - [], - ); - if ( - lastVisibleIndex !== undefined && - this.props.className.indexOf(`${this.props.prefixCls}-root`) !== -1 - ) { - if (index > lastVisibleIndex) { - item = React.cloneElement( - childNode, - // 这里修改 eventKey 是为了防止隐藏状态下还会触发 openkeys 事件 - { - style: { display: 'none' }, - eventKey: `${childNode.props.eventKey}-hidden`, - /** - * Legacy code. Here `className` never used: - * https://github.com/react-component/menu/commit/4cd6b49fce9d116726f4ea00dda85325d6f26500#diff-e2fa48f75c2dd2318295cde428556a76R240 - */ - className: `${MENUITEM_OVERFLOWED_CLASSNAME}`, - }, - ); - } - if (index === lastVisibleIndex + 1) { - this.overflowedItems = children - .slice(lastVisibleIndex + 1) - .map((c) => - React.cloneElement( - c, - // children[index].key will become '.$key' in clone by default, - // we have to overwrite with the correct key explicitly - { key: c.props.eventKey, mode: 'vertical-left' }, - ), - ); - - overflowed = this.getOverflowedSubMenuItem( - childNode.props.eventKey, - this.overflowedItems, - ); - } - } - - const ret: React.ReactElement[] = [...acc, overflowed, item]; - - if (index === children.length - 1) { - // need a placeholder for calculating overflowed indicator width - ret.push( - this.getOverflowedSubMenuItem(childNode.props.eventKey, [], true), - ); - } - return ret; - } - return [...acc, item]; - }, - [], - ); - } - - render() { - const { - visible, - prefixCls, - overflowedIndicator, - mode, - level, - tag, - children, - theme, - ...rest - } = this.props; - - const Tag = tag as any; - - return ( - - {this.renderChildren(children)} - - ); - } -} - -export default DOMWrap; diff --git a/src/Divider.tsx b/src/Divider.tsx deleted file mode 100644 index 0444d421..00000000 --- a/src/Divider.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from 'react'; - -export interface DividerProps { - className?: string; - rootPrefixCls?: string; - style?: React.CSSProperties; - disabled?: boolean; -} - -const Divider: React.FC = ({ - className, - rootPrefixCls, - style, -}) => ( -
  • -); - -Divider.defaultProps = { - // To fix keyboard UX. - disabled: true, - className: '', - style: {}, -}; - -export default Divider; diff --git a/src/Menu.tsx b/src/Menu.tsx index 5048476a..be5d5b7b 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -1,438 +1,113 @@ import * as React from 'react'; -import { Provider, create } from 'mini-store'; -import omit from 'rc-util/lib/omit'; -import type { CSSMotionProps } from 'rc-motion'; -import SubPopupMenu, { getActiveKey } from './SubPopupMenu'; -import { noop } from './util'; -import type { - RenderIconType, - SelectInfo, - SelectEventHandler, - DestroyEventHandler, - MenuMode, - OpenAnimation, - MiniStore, - BuiltinPlacements, - TriggerSubMenuAction, - MenuClickEventHandler, -} from './interface'; -import { getMotion } from './utils/legacyUtil'; +import { useMemo } from 'react'; +import classNames from 'classnames'; +import Overflow from 'rc-overflow'; +import type { MenuItemData, MenuMode } from './interface'; +import { convertToData } from './dataUtil'; +import MenuItem from './MenuItem'; + +export const MenuContext = React.createContext({ + prefixCls: '', +}); export interface MenuProps extends Omit, 'onClick' | 'onSelect'> { - defaultSelectedKeys?: string[]; - defaultActiveFirst?: boolean; - selectedKeys?: string[]; - defaultOpenKeys?: string[]; - openKeys?: string[]; - mode?: MenuMode; - getPopupContainer?: (node: HTMLElement) => HTMLElement; - onClick?: MenuClickEventHandler; - onSelect?: SelectEventHandler; - onOpenChange?: (openKeys: React.Key[]) => void; - onDeselect?: SelectEventHandler; - onDestroy?: DestroyEventHandler; - subMenuOpenDelay?: number; - subMenuCloseDelay?: number; - forceSubMenuRender?: boolean; - triggerSubMenuAction?: TriggerSubMenuAction; - level?: number; - selectable?: boolean; - multiple?: boolean; - activeKey?: string; prefixCls?: string; - builtinPlacements?: BuiltinPlacements; - itemIcon?: RenderIconType; - expandIcon?: RenderIconType; - overflowedIndicator?: React.ReactNode; - /** Menu motion define */ - motion?: CSSMotionProps; - - /** Default menu motion of each mode */ - defaultMotions?: Partial<{ [key in MenuMode | 'other']: CSSMotionProps }>; - - /** @deprecated Please use `motion` instead */ - openTransitionName?: string; - /** @deprecated Please use `motion` instead */ - openAnimation?: OpenAnimation; - - /** direction of menu */ - direction?: 'ltr' | 'rtl'; - inlineCollapsed?: boolean; - - /** SiderContextProps of layout in ant design */ - siderCollapsed?: boolean; - collapsedWidth?: string | number; -} - -export interface MenuState { - switchingModeFromInline: boolean; - prevProps: MenuProps; - inlineOpenKeys: string[]; - store: MiniStore; + mode?: MenuMode; + options?: MenuItemData[]; + children?: React.ReactNode; + + // defaultSelectedKeys?: string[]; + // defaultActiveFirst?: boolean; + // selectedKeys?: string[]; + // defaultOpenKeys?: string[]; + // openKeys?: string[]; + + // getPopupContainer?: (node: HTMLElement) => HTMLElement; + // onClick?: MenuClickEventHandler; + // onSelect?: SelectEventHandler; + // onOpenChange?: (openKeys: React.Key[]) => void; + // onDeselect?: SelectEventHandler; + // onDestroy?: DestroyEventHandler; + // subMenuOpenDelay?: number; + // subMenuCloseDelay?: number; + // forceSubMenuRender?: boolean; + // triggerSubMenuAction?: TriggerSubMenuAction; + // level?: number; + // selectable?: boolean; + // multiple?: boolean; + // activeKey?: string; + // builtinPlacements?: BuiltinPlacements; + // itemIcon?: RenderIconType; + // expandIcon?: RenderIconType; + // overflowedIndicator?: React.ReactNode; + // /** Menu motion define */ + // motion?: CSSMotionProps; + + // /** Default menu motion of each mode */ + // defaultMotions?: Partial<{ [key in MenuMode | 'other']: CSSMotionProps }>; + + // /** @deprecated Please use `motion` instead */ + // openTransitionName?: string; + // /** @deprecated Please use `motion` instead */ + // openAnimation?: OpenAnimation; + + // /** direction of menu */ + // direction?: 'ltr' | 'rtl'; + + // inlineCollapsed?: boolean; + + // /** SiderContextProps of layout in ant design */ + // siderCollapsed?: boolean; + // collapsedWidth?: string | number; } -class Menu extends React.Component { - static defaultProps = { - selectable: true, - onClick: noop, - onSelect: noop, - onOpenChange: noop, - onDeselect: noop, - defaultSelectedKeys: [], - defaultOpenKeys: [], - subMenuOpenDelay: 0.1, - subMenuCloseDelay: 0.1, - triggerSubMenuAction: 'hover', - prefixCls: 'rc-menu', - className: '', - mode: 'vertical', - style: {}, - builtinPlacements: {}, - overflowedIndicator: ···, - }; - - constructor(props: MenuProps) { - super(props); - - this.isRootMenu = true; - - let selectedKeys = props.defaultSelectedKeys; - let openKeys = props.defaultOpenKeys; - if ('selectedKeys' in props) { - selectedKeys = props.selectedKeys || []; - } - if ('openKeys' in props) { - openKeys = props.openKeys || []; - } - - this.store = create({ - selectedKeys, - openKeys, - activeKey: { '0-menu-': getActiveKey(props, props.activeKey) }, - }); - - this.state = { - switchingModeFromInline: false, - prevProps: props, - inlineOpenKeys: [], - store: this.store, - }; - } - - isRootMenu: boolean; - - store: MiniStore; - - innerMenu: typeof SubPopupMenu; - - prevOpenKeys: string[]; - - static getDerivedStateFromProps(nextProps: MenuProps, prevState: MenuState) { - const { prevProps, store } = prevState; - const prevStoreState = store.getState(); - const newStoreState: any = {}; - const newState: Partial = { - prevProps: nextProps, - }; - if (prevProps.mode === 'inline' && nextProps.mode !== 'inline') { - newState.switchingModeFromInline = true; - } - - if ('openKeys' in nextProps) { - newStoreState.openKeys = nextProps.openKeys || []; - } else { - // [Legacy] Old code will return after `openKeys` changed. - // Not sure the reason, we should keep this logic still. - if ( - (nextProps.inlineCollapsed && !prevProps.inlineCollapsed) || - (nextProps.siderCollapsed && !prevProps.siderCollapsed) - ) { - newState.switchingModeFromInline = true; - newState.inlineOpenKeys = prevStoreState.openKeys; - newStoreState.openKeys = []; - } - - if ( - (!nextProps.inlineCollapsed && prevProps.inlineCollapsed) || - (!nextProps.siderCollapsed && prevProps.siderCollapsed) - ) { - newStoreState.openKeys = prevState.inlineOpenKeys; - newState.inlineOpenKeys = []; - } - } - - if (Object.keys(newStoreState).length) { - store.setState(newStoreState); - } - - return newState; - } - - componentDidMount() { - this.updateMiniStore(); - this.updateMenuDisplay(); - } - - componentDidUpdate(prevProps: MenuProps) { - const { siderCollapsed, inlineCollapsed, onOpenChange } = this.props; - if ( - (!prevProps.inlineCollapsed && inlineCollapsed) || - (!prevProps.siderCollapsed && siderCollapsed) - ) { - onOpenChange([]); - } - this.updateMiniStore(); - this.updateMenuDisplay(); - } - - updateMenuDisplay() { - const { - props: { collapsedWidth }, - store, - prevOpenKeys, - } = this; - // https://github.com/ant-design/ant-design/issues/8587 - const hideMenu = - this.getInlineCollapsed() && - (collapsedWidth === 0 || - collapsedWidth === '0' || - collapsedWidth === '0px'); - if (hideMenu) { - this.prevOpenKeys = store.getState().openKeys.concat(); - this.store.setState({ - openKeys: [], - }); - } else if (prevOpenKeys) { - this.store.setState({ - openKeys: prevOpenKeys, - }); - this.prevOpenKeys = null; - } - } - - onSelect = (selectInfo: SelectInfo) => { - const { props } = this; - if (props.selectable) { - // root menu - let { selectedKeys } = this.store.getState(); - const selectedKey = selectInfo.key; - if (props.multiple) { - selectedKeys = selectedKeys.concat([selectedKey]); - } else { - selectedKeys = [selectedKey]; - } - if (!('selectedKeys' in props)) { - this.store.setState({ - selectedKeys, - }); - } - props.onSelect({ - ...selectInfo, - selectedKeys, - }); - } - }; - - onClick: MenuClickEventHandler = (e) => { - const mode = this.getRealMenuMode(); - const { - store, - props: { onOpenChange }, - } = this; - - if (mode !== 'inline' && !('openKeys' in this.props)) { - // closing vertical popup submenu after click it - store.setState({ - openKeys: [], - }); - onOpenChange([]); - } - this.props.onClick(e); - }; - - // onKeyDown needs to be exposed as a instance method - // e.g., in rc-select, we need to navigate menu item while - // current active item is rc-select input box rather than the menu itself - onKeyDown = (e: React.KeyboardEvent, callback) => { - this.innerMenu.getWrappedInstance().onKeyDown(e, callback); - }; - - onOpenChange = (event) => { - const { props } = this; - const openKeys = this.store.getState().openKeys.concat(); - let changed = false; - const processSingle = (e) => { - let oneChanged = false; - if (e.open) { - oneChanged = openKeys.indexOf(e.key) === -1; - if (oneChanged) { - openKeys.push(e.key); - } - } else { - const index = openKeys.indexOf(e.key); - oneChanged = index !== -1; - if (oneChanged) { - openKeys.splice(index, 1); - } - } - changed = changed || oneChanged; - }; - if (Array.isArray(event)) { - // batch change call - event.forEach(processSingle); - } else { - processSingle(event); - } - if (changed) { - if (!('openKeys' in this.props)) { - this.store.setState({ openKeys }); - } - props.onOpenChange(openKeys); - } - }; - - onDeselect = (selectInfo: SelectInfo) => { - const { props } = this; - if (props.selectable) { - const selectedKeys = this.store.getState().selectedKeys.concat(); - const selectedKey = selectInfo.key; - const index = selectedKeys.indexOf(selectedKey); - if (index !== -1) { - selectedKeys.splice(index, 1); - } - if (!('selectedKeys' in props)) { - this.store.setState({ - selectedKeys, - }); - } - props.onDeselect({ - ...selectInfo, - selectedKeys, - }); - } - }; - - getRealMenuMode() { - const { mode } = this.props; - const { switchingModeFromInline } = this.state; - const inlineCollapsed = this.getInlineCollapsed(); - if (switchingModeFromInline && inlineCollapsed) { - return 'inline'; - } - return inlineCollapsed ? 'vertical' : mode; - } - - getInlineCollapsed() { - const { inlineCollapsed, siderCollapsed } = this.props; - if (siderCollapsed !== undefined) { - return siderCollapsed; - } - return inlineCollapsed; - } - - // Restore vertical mode when menu is collapsed responsively when mounted - // https://github.com/ant-design/ant-design/issues/13104 - // TODO: not a perfect solution, - // looking a new way to avoid setting switchingModeFromInline in this situation - onMouseEnter = (e: React.MouseEvent) => { - this.restoreModeVerticalFromInline(); - const { onMouseEnter } = this.props; - if (onMouseEnter) { - onMouseEnter(e); - } - }; - - onTransitionEnd = (e: React.TransitionEvent) => { - // when inlineCollapsed menu width animation finished - // https://github.com/ant-design/ant-design/issues/12864 - const widthCollapsed = - e.propertyName === 'width' && e.target === e.currentTarget; - - // Fix SVGElement e.target.className.indexOf is not a function - // https://github.com/ant-design/ant-design/issues/15699 - const { className } = e.target as HTMLElement | SVGElement; - // SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, - // unless during an animation. - const classNameValue = - Object.prototype.toString.call(className) === '[object SVGAnimatedString]' - ? className.animVal - : className; - - // Fix for , - // the width transition won't trigger when menu is collapsed - // https://github.com/ant-design/ant-design-pro/issues/2783 - const iconScaled = - e.propertyName === 'font-size' && classNameValue.indexOf('anticon') >= 0; - if (widthCollapsed || iconScaled) { - this.restoreModeVerticalFromInline(); - } - }; - - restoreModeVerticalFromInline() { - const { switchingModeFromInline } = this.state; - if (switchingModeFromInline) { - this.setState({ - switchingModeFromInline: false, - }); - } - } - - setInnerMenu = (node) => { - this.innerMenu = node; - }; - - updateMiniStore() { - if ('selectedKeys' in this.props) { - this.store.setState({ - selectedKeys: this.props.selectedKeys || [], - }); - } - if ('openKeys' in this.props) { - this.store.setState({ - openKeys: this.props.openKeys || [], - }); - } - } - - render() { - let props: MenuProps & { parentMenu?: Menu } = { - ...omit(this.props, [ - 'collapsedWidth', - 'siderCollapsed', - 'defaultMotions', - ]), - }; - const mode = this.getRealMenuMode(); - props.className += ` ${props.prefixCls}-root`; - if (props.direction === 'rtl') { - props.className += ` ${props.prefixCls}-rtl`; - } - props = { - ...props, - mode, - onClick: this.onClick, - onOpenChange: this.onOpenChange, - onDeselect: this.onDeselect, - onSelect: this.onSelect, - onMouseEnter: this.onMouseEnter, - onTransitionEnd: this.onTransitionEnd, - parentMenu: this, - motion: getMotion(this.props, this.state, mode), - }; - - delete props.openAnimation; - delete props.openTransitionName; - - return ( - - - {this.props.children} - - - ); - } -} +const Menu: React.FC = ({ + prefixCls = 'rc-menu', + style, + className, + tabIndex = 0, + + // Data + options, + children, +}) => { + const parsedOptions = useMemo(() => { + if (options) { + return options; + } + + return convertToData(children); + }, [options, children]); + + const menuContext = useMemo(() => ({ prefixCls }), [prefixCls]); + + const node = ( + item.key} + /> + ); + + // return ( + //
      + // ); + + return ( + {node} + ); +}; export default Menu; diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index 26b030ea..070cc16d 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -1,262 +1,51 @@ import * as React from 'react'; -import KeyCode from 'rc-util/lib/KeyCode'; -import classNames from 'classnames'; -import omit from 'rc-util/lib/omit'; -import { connect } from 'mini-store'; -import { noop, menuAllProps } from './util'; -import type { - SelectEventHandler, - HoverEventHandler, - DestroyEventHandler, - RenderIconType, - MenuHoverEventHandler, - MenuClickEventHandler, - MenuMode, - LegacyFunctionRef, -} from './interface'; - -/* eslint react/no-is-mounted:0 */ +import { MenuContext } from './Menu'; export interface MenuItemProps extends Omit< React.HTMLAttributes, 'onClick' | 'onMouseEnter' | 'onMouseLeave' | 'onSelect' > { - /** @deprecated No place to use this. Should remove */ - attribute?: Record; - rootPrefixCls?: string; - eventKey?: React.Key; - className?: string; - style?: React.CSSProperties; - active?: boolean; children?: React.ReactNode; - selectedKeys?: string[]; - disabled?: boolean; - title?: string; - onItemHover?: HoverEventHandler; - onSelect?: SelectEventHandler; - onClick?: MenuClickEventHandler; - onDeselect?: SelectEventHandler; - parentMenu?: React.ReactInstance; - onDestroy?: DestroyEventHandler; - onMouseEnter?: MenuHoverEventHandler; - onMouseLeave?: MenuHoverEventHandler; - multiple?: boolean; - isSelected?: boolean; - manualRef?: LegacyFunctionRef; - itemIcon?: RenderIconType; - role?: string; - mode?: MenuMode; - inlineIndent?: number; - level?: number; - direction?: 'ltr' | 'rtl'; -} - -export class MenuItem extends React.Component { - static isMenuItem = true; - - static defaultProps = { - onSelect: noop, - onMouseEnter: noop, - onMouseLeave: noop, - manualRef: noop, - }; - - node: HTMLLIElement; - - componentDidMount() { - // invoke customized ref to expose component to mixin - this.callRef(); - } - - componentDidUpdate() { - this.callRef(); - } - - componentWillUnmount() { - const { props } = this; - if (props.onDestroy) { - props.onDestroy(props.eventKey); - } - } - - public onKeyDown = ( - e: React.KeyboardEvent, - ): boolean | undefined => { - const { keyCode } = e; - if (keyCode === KeyCode.ENTER) { - this.onClick(e as any); - return true; - } - return undefined; - }; - - onMouseLeave: React.MouseEventHandler = (e) => { - const { eventKey, onItemHover, onMouseLeave } = this.props; - onItemHover({ - key: eventKey, - hover: false, - }); - onMouseLeave({ - key: eventKey, - domEvent: e, - }); - }; - onMouseEnter: React.MouseEventHandler = (e) => { - const { eventKey, onItemHover, onMouseEnter } = this.props; - onItemHover({ - key: eventKey, - hover: true, - }); - onMouseEnter({ - key: eventKey, - domEvent: e, - }); - }; - - onClick: React.MouseEventHandler = (e) => { - const { - eventKey, - multiple, - onClick, - onSelect, - onDeselect, - isSelected, - } = this.props; - const info = { - key: eventKey, - keyPath: [eventKey], - item: this, - domEvent: e, - }; - onClick(info); - if (multiple) { - if (isSelected) { - onDeselect(info); - } else { - onSelect(info); - } - } else if (!isSelected) { - onSelect(info); - } - }; - - getPrefixCls() { - return `${this.props.rootPrefixCls}-item`; - } - - getActiveClassName() { - return `${this.getPrefixCls()}-active`; - } - - getSelectedClassName() { - return `${this.getPrefixCls()}-selected`; - } - - getDisabledClassName() { - return `${this.getPrefixCls()}-disabled`; - } - - saveNode = (node: HTMLLIElement) => { - this.node = node; - }; - - callRef() { - if (this.props.manualRef) { - this.props.manualRef(this); - } - } - - render() { - const props = { ...this.props }; - const className = classNames(this.getPrefixCls(), props.className, { - [this.getActiveClassName()]: !props.disabled && props.active, - [this.getSelectedClassName()]: props.isSelected, - [this.getDisabledClassName()]: props.disabled, - }); - let attrs: { - title?: string; - className?: string; - role?: string; - 'aria-disabled'?: boolean; - 'aria-selected'?: boolean; - } = { - ...props.attribute, - title: typeof props.title === 'string' ? props.title : undefined, - className, - // set to menuitem by default - role: props.role || 'menuitem', - 'aria-disabled': props.disabled, - }; - - if (props.role === 'option') { - // overwrite to option - attrs = { - ...attrs, - role: 'option', - 'aria-selected': props.isSelected, - }; - } else if (props.role === null || props.role === 'none') { - // sometimes we want to specify role inside
    • element - //
    • Link
    • would be a good example - // in this case the role on
    • should be "none" to - // remove the implied listitem role. - // https://www.w3.org/TR/wai-aria-practices-1.1/examples/menubar/menubar-1/menubar-1.html - attrs.role = 'none'; - } - // In case that onClick/onMouseLeave/onMouseEnter is passed down from owner - const mouseEvent = { - onClick: props.disabled ? null : this.onClick, - onMouseLeave: props.disabled ? null : this.onMouseLeave, - onMouseEnter: props.disabled ? null : this.onMouseEnter, - }; - const style = { - ...props.style, - }; - if (props.mode === 'inline') { - if (props.direction === 'rtl') { - style.paddingRight = props.inlineIndent * props.level; - } else { - style.paddingLeft = props.inlineIndent * props.level; - } - } - menuAllProps.forEach((key) => delete props[key]); - delete props.direction; - let icon = this.props.itemIcon; - if (typeof this.props.itemIcon === 'function') { - // TODO: This is a bug which should fixed after TS refactor - icon = React.createElement(this.props.itemIcon as any, this.props); - } - return ( -
    • - {props.children} - {icon} -
    • - ); - } + /** @deprecated No place to use this. Should remove */ + // attribute?: Record; + // rootPrefixCls?: string; + // eventKey?: React.Key; + // className?: string; + // style?: React.CSSProperties; + // active?: boolean; + // selectedKeys?: string[]; + // disabled?: boolean; + // title?: string; + // onItemHover?: HoverEventHandler; + // onSelect?: SelectEventHandler; + // onClick?: MenuClickEventHandler; + // onDeselect?: SelectEventHandler; + // parentMenu?: React.ReactInstance; + // onDestroy?: DestroyEventHandler; + // onMouseEnter?: MenuHoverEventHandler; + // onMouseLeave?: MenuHoverEventHandler; + // multiple?: boolean; + // isSelected?: boolean; + // manualRef?: LegacyFunctionRef; + // itemIcon?: RenderIconType; + // role?: string; + // mode?: MenuMode; + // inlineIndent?: number; + // level?: number; + // direction?: 'ltr' | 'rtl'; } -const connected = connect( - ({ activeKey, selectedKeys }, { eventKey, subMenuKey }) => ({ - active: activeKey[subMenuKey] === eventKey, - // selectedKeys should be array in any circumstance - // when it is not, we have fallback logic for https://github.com/ant-design/ant-design/issues/29430 - isSelected: Array.isArray(selectedKeys) - ? selectedKeys.indexOf(eventKey) !== -1 - : selectedKeys === eventKey, - }), -)(MenuItem); +const MenuItem: React.FC = ({ children }) => { + const { prefixCls } = React.useContext(MenuContext); + const itemCls = `${prefixCls}-item`; + + return ( +
    • + {children} +
    • + ); +}; -export default connected; +export default MenuItem; diff --git a/src/MenuItemGroup.tsx b/src/MenuItemGroup.tsx deleted file mode 100644 index b5b3f3e7..00000000 --- a/src/MenuItemGroup.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import * as React from 'react'; -import { menuAllProps } from './util'; -import type { MenuClickEventHandler } from './interface'; - -export interface MenuItemGroupProps { - disabled?: boolean; - renderMenuItem?: ( - item: React.ReactElement, - index: number, - key: string, - ) => React.ReactElement; - index?: number; - className?: string; - subMenuKey?: string; - rootPrefixCls?: string; - title?: React.ReactNode; - onClick?: MenuClickEventHandler; - direction?: 'ltr' | 'rtl'; -} - -class MenuItemGroup extends React.Component { - static isMenuItemGroup = true; - - static defaultProps = { - disabled: true, - }; - - renderInnerMenuItem = (item: React.ReactElement) => { - const { renderMenuItem, index } = this.props; - return renderMenuItem(item, index, this.props.subMenuKey); - }; - - render() { - const { ...props } = this.props; - const { className = '', rootPrefixCls } = props; - const titleClassName = `${rootPrefixCls}-item-group-title`; - const listClassName = `${rootPrefixCls}-item-group-list`; - const { title, children } = props; - menuAllProps.forEach((key) => delete props[key]); - - delete props.direction; - - return ( -
    • e.stopPropagation()} - className={`${className} ${rootPrefixCls}-item-group`} - > -
      - {title} -
      -
        - {React.Children.map(children, this.renderInnerMenuItem)} -
      -
    • - ); - } -} - -export default MenuItemGroup; diff --git a/src/SubMenu.tsx b/src/SubMenu.tsx deleted file mode 100644 index b44ac2da..00000000 --- a/src/SubMenu.tsx +++ /dev/null @@ -1,710 +0,0 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; -import Trigger from 'rc-trigger'; -import raf from 'rc-util/lib/raf'; -import KeyCode from 'rc-util/lib/KeyCode'; -import type { CSSMotionProps } from 'rc-motion'; -import CSSMotion from 'rc-motion'; -import classNames from 'classnames'; -import { connect } from 'mini-store'; -import type { SubPopupMenuProps } from './SubPopupMenu'; -import SubPopupMenu from './SubPopupMenu'; -import { placements, placementsRtl } from './placements'; -import { - noop, - loopMenuItemRecursively, - getMenuIdFromSubMenuEventKey, - menuAllProps, -} from './util'; -import type { - MiniStore, - RenderIconType, - LegacyFunctionRef, - MenuMode, - OpenEventHandler, - SelectEventHandler, - DestroyEventHandler, - MenuHoverEventHandler, - MenuClickEventHandler, - MenuInfo, - BuiltinPlacements, - TriggerSubMenuAction, - HoverEventHandler, -} from './interface'; -import type { MenuItem } from './MenuItem'; - -let guid = 0; - -const popupPlacementMap = { - horizontal: 'bottomLeft', - vertical: 'rightTop', - 'vertical-left': 'rightTop', - 'vertical-right': 'leftTop', -}; - -const updateDefaultActiveFirst = ( - store: MiniStore, - eventKey: string, - defaultActiveFirst: boolean, -) => { - const menuId = getMenuIdFromSubMenuEventKey(eventKey); - const state = store.getState(); - store.setState({ - defaultActiveFirst: { - ...state.defaultActiveFirst, - [menuId]: defaultActiveFirst, - }, - }); -}; - -export interface SubMenuProps { - parentMenu?: React.ReactElement & { - isRootMenu: boolean; - subMenuInstance: React.ReactInstance; - }; - title?: React.ReactNode; - children?: React.ReactNode; - selectedKeys?: string[]; - openKeys?: string[]; - onClick?: MenuClickEventHandler; - onOpenChange?: OpenEventHandler; - rootPrefixCls?: string; - eventKey?: string; - multiple?: boolean; - active?: boolean; // TODO: remove - onItemHover?: HoverEventHandler; - onSelect?: SelectEventHandler; - triggerSubMenuAction?: TriggerSubMenuAction; - onDeselect?: SelectEventHandler; - onDestroy?: DestroyEventHandler; - onMouseEnter?: MenuHoverEventHandler; - onMouseLeave?: MenuHoverEventHandler; - onTitleMouseEnter?: MenuHoverEventHandler; - onTitleMouseLeave?: MenuHoverEventHandler; - onTitleClick?: (info: { - key: React.Key; - domEvent: React.MouseEvent | React.KeyboardEvent; - }) => void; - popupOffset?: number[]; - isOpen?: boolean; - store?: MiniStore; - mode?: MenuMode; - manualRef?: LegacyFunctionRef; - itemIcon?: RenderIconType; - expandIcon?: RenderIconType; - inlineIndent?: number; - level?: number; - subMenuOpenDelay?: number; - subMenuCloseDelay?: number; - forceSubMenuRender?: boolean; - builtinPlacements?: BuiltinPlacements; - disabled?: boolean; - className?: string; - popupClassName?: string; - - motion?: CSSMotionProps; - direction?: 'ltr' | 'rtl'; -} - -interface SubMenuState { - mode: MenuMode; - isOpen: boolean; -} - -export class SubMenu extends React.Component { - static defaultProps = { - onMouseEnter: noop, - onMouseLeave: noop, - onTitleMouseEnter: noop, - onTitleMouseLeave: noop, - onTitleClick: noop, - manualRef: noop, - mode: 'vertical', - title: '', - }; - - constructor(props: SubMenuProps) { - super(props); - const { store, eventKey } = props; - const { defaultActiveFirst } = store.getState(); - - this.isRootMenu = false; - - let value = false; - - if (defaultActiveFirst) { - value = defaultActiveFirst[eventKey]; - } - - updateDefaultActiveFirst(store, eventKey, value); - - this.state = { - mode: props.mode, - isOpen: props.isOpen, - }; - } - - isRootMenu: boolean; - - menuInstance: MenuItem; - - subMenuTitle: HTMLElement; - - internalMenuId: string; - - haveRendered: boolean; - - haveOpened: boolean; - - updateStateRaf: number; - - /** - * Follow timeout should be `number`. - * Current is only convert code into TS, - * we not use `window.setTimeout` instead of `setTimeout`. - */ - minWidthTimeout: any; - - mouseenterTimeout: any; - - componentDidMount() { - this.componentDidUpdate(); - } - - componentDidUpdate() { - const { mode, parentMenu, manualRef, isOpen } = this.props; - - const updateState = () => { - this.setState({ - mode, - isOpen, - }); - }; - - // Delay sync when mode changed in case openKeys change not sync - const isOpenChanged = isOpen !== this.state.isOpen; - const isModeChanged = mode !== this.state.mode; - if (isModeChanged || isOpenChanged) { - raf.cancel(this.updateStateRaf); - - if (isModeChanged) { - this.updateStateRaf = raf(updateState); - } else { - updateState(); - } - } - - // invoke customized ref to expose component to mixin - if (manualRef) { - manualRef(this); - } - - if (mode !== 'horizontal' || !parentMenu?.isRootMenu || !isOpen) { - return; - } - - this.minWidthTimeout = setTimeout(() => this.adjustWidth(), 0); - } - - componentWillUnmount() { - const { onDestroy, eventKey } = this.props; - if (onDestroy) { - onDestroy(eventKey); - } - - /* istanbul ignore if */ - if (this.minWidthTimeout) { - clearTimeout(this.minWidthTimeout); - } - - /* istanbul ignore if */ - if (this.mouseenterTimeout) { - clearTimeout(this.mouseenterTimeout); - } - - raf.cancel(this.updateStateRaf); - } - - onDestroy = (key: string) => { - this.props.onDestroy(key); - }; - - /** - * note: - * This legacy code that `onKeyDown` is called by parent instead of dom self. - * which need return code to check if this event is handled - */ - onKeyDown: React.KeyboardEventHandler = (e) => { - const { keyCode } = e; - const menu = this.menuInstance; - const { store } = this.props; - const visible = this.getVisible(); - - if (keyCode === KeyCode.ENTER) { - this.onTitleClick(e); - updateDefaultActiveFirst(store, this.props.eventKey, true); - return true; - } - - if (keyCode === KeyCode.RIGHT) { - if (visible) { - menu.onKeyDown(e); - } else { - this.triggerOpenChange(true); - // need to update current menu's defaultActiveFirst value - updateDefaultActiveFirst(store, this.props.eventKey, true); - } - return true; - } - if (keyCode === KeyCode.LEFT) { - let handled: boolean; - if (visible) { - handled = menu.onKeyDown(e); - } else { - return undefined; - } - if (!handled) { - this.triggerOpenChange(false); - handled = true; - } - return handled; - } - - if (visible && (keyCode === KeyCode.UP || keyCode === KeyCode.DOWN)) { - return menu.onKeyDown(e); - } - - return undefined; - }; - - onOpenChange: OpenEventHandler = (e) => { - this.props.onOpenChange(e); - }; - - onPopupVisibleChange = (visible: boolean) => { - this.triggerOpenChange(visible, visible ? 'mouseenter' : 'mouseleave'); - }; - - onMouseEnter: React.MouseEventHandler = (e) => { - const { eventKey: key, onMouseEnter, store } = this.props; - updateDefaultActiveFirst(store, this.props.eventKey, false); - onMouseEnter({ - key, - domEvent: e, - }); - }; - - onMouseLeave: React.MouseEventHandler = (e) => { - const { parentMenu, eventKey, onMouseLeave } = this.props; - parentMenu.subMenuInstance = this; - onMouseLeave({ - key: eventKey, - domEvent: e, - }); - }; - - onTitleMouseEnter: React.MouseEventHandler = (domEvent) => { - const { eventKey: key, onItemHover, onTitleMouseEnter } = this.props; - onItemHover({ - key, - hover: true, - }); - onTitleMouseEnter({ - key, - domEvent, - }); - }; - - onTitleMouseLeave: React.MouseEventHandler = (e) => { - const { parentMenu, eventKey, onItemHover, onTitleMouseLeave } = this.props; - parentMenu.subMenuInstance = this; - onItemHover({ - key: eventKey, - hover: false, - }); - onTitleMouseLeave({ - key: eventKey, - domEvent: e, - }); - }; - - onTitleClick = ( - e: React.MouseEvent | React.KeyboardEvent, - ) => { - const { props } = this; - props.onTitleClick({ - key: props.eventKey, - domEvent: e, - }); - if (props.triggerSubMenuAction === 'hover') { - return; - } - this.triggerOpenChange(!this.getVisible(), 'click'); - updateDefaultActiveFirst(props.store, this.props.eventKey, false); - }; - - onSubMenuClick = (info: MenuInfo) => { - // in the case of overflowed submenu - // onClick is not copied over - if (typeof this.props.onClick === 'function') { - this.props.onClick(this.addKeyPath(info)); - } - }; - - onSelect: SelectEventHandler = (info) => { - this.props.onSelect(info); - }; - - onDeselect: SelectEventHandler = (info) => { - this.props.onDeselect(info); - }; - - getPrefixCls = () => `${this.props.rootPrefixCls}-submenu`; - - getActiveClassName = () => `${this.getPrefixCls()}-active`; - - getDisabledClassName = () => `${this.getPrefixCls()}-disabled`; - - getSelectedClassName = () => `${this.getPrefixCls()}-selected`; - - getOpenClassName = () => `${this.props.rootPrefixCls}-submenu-open`; - - getVisible = () => this.state.isOpen; - - getMode = () => this.state.mode; - - saveMenuInstance = (c: MenuItem) => { - // children menu instance - this.menuInstance = c; - }; - - addKeyPath = (info: MenuInfo) => ({ - ...info, - keyPath: (info.keyPath || []).concat(this.props.eventKey), - }); - - triggerOpenChange = (open: boolean, type?: string) => { - const key = this.props.eventKey; - const openChange = () => { - this.onOpenChange({ - key, - item: this, - trigger: type, - open, - }); - }; - if (type === 'mouseenter') { - // make sure mouseenter happen after other menu item's mouseleave - this.mouseenterTimeout = setTimeout(() => { - openChange(); - }, 0); - } else { - openChange(); - } - }; - - isChildrenSelected = () => { - const ret = { find: false }; - loopMenuItemRecursively(this.props.children, this.props.selectedKeys, ret); - return ret.find; - }; - - isInlineMode = () => this.getMode() === 'inline'; - - adjustWidth = () => { - /* istanbul ignore if */ - if (!this.subMenuTitle || !this.menuInstance) { - return; - } - const popupMenu = ReactDOM.findDOMNode(this.menuInstance) as HTMLElement; - if (popupMenu.offsetWidth >= this.subMenuTitle.offsetWidth) { - return; - } - - /* istanbul ignore next */ - popupMenu.style.minWidth = `${this.subMenuTitle.offsetWidth}px`; - }; - - saveSubMenuTitle = (subMenuTitle: HTMLElement) => { - this.subMenuTitle = subMenuTitle; - }; - - getBaseProps = (): SubPopupMenuProps => { - const { props } = this; - const mergedMode = this.getMode(); - - return { - mode: mergedMode === 'horizontal' ? 'vertical' : mergedMode, - visible: this.getVisible(), - level: props.level + 1, - inlineIndent: props.inlineIndent, - focusable: false, - onClick: this.onSubMenuClick, - onSelect: this.onSelect, - onDeselect: this.onDeselect, - onDestroy: this.onDestroy as any, - selectedKeys: props.selectedKeys, - eventKey: `${props.eventKey}-menu-`, - openKeys: props.openKeys, - motion: props.motion, - onOpenChange: this.onOpenChange, - subMenuOpenDelay: props.subMenuOpenDelay, - parentMenu: this, - subMenuCloseDelay: props.subMenuCloseDelay, - forceSubMenuRender: props.forceSubMenuRender, - triggerSubMenuAction: props.triggerSubMenuAction, - builtinPlacements: props.builtinPlacements, - defaultActiveFirst: props.store.getState().defaultActiveFirst[ - getMenuIdFromSubMenuEventKey(props.eventKey) - ], - multiple: props.multiple, - prefixCls: props.rootPrefixCls, - id: this.internalMenuId, - manualRef: this.saveMenuInstance as any, - itemIcon: props.itemIcon, - expandIcon: props.expandIcon, - direction: props.direction, - }; - }; - - getMotion = (mode: MenuMode, visible: boolean) => { - const { haveRendered } = this; - const { motion, rootPrefixCls } = this.props; - - // don't show transition on first rendering (no animation for opened menu) - // show appear transition if it's not visible (not sure why) - // show appear transition if it's not inline mode - const mergedMotion: CSSMotionProps = { - ...motion, - leavedClassName: `${rootPrefixCls}-hidden`, - removeOnLeave: false, - motionAppear: haveRendered || !visible || mode !== 'inline', - }; - - return mergedMotion; - }; - - renderPopupMenu(className: string, style?: React.CSSProperties) { - const baseProps = this.getBaseProps(); - - /** - * zombiej: Why SubPopupMenu here? - * Seems whatever popup or inline mode both will render SubPopupMenu. - * It's controlled by Trigger for popup or not. - */ - return ( - - {this.props.children} - - ); - } - - renderChildren() { - const baseProps = this.getBaseProps(); - const { mode, visible, forceSubMenuRender, direction } = baseProps; - - // [Legacy] getMotion must be called before `haveRendered` - const mergedMotion = this.getMotion(mode, visible); - - this.haveRendered = true; - - this.haveOpened = this.haveOpened || visible || forceSubMenuRender; - // never rendered not planning to, don't render - if (!this.haveOpened) { - return
      ; - } - - const sharedClassName = classNames(`${baseProps.prefixCls}-sub`, { - [`${baseProps.prefixCls}-rtl`]: direction === 'rtl', - }); - - if (!this.isInlineMode()) { - return this.renderPopupMenu(sharedClassName); - } - - return ( - - {({ className, style }) => { - const mergedClassName = classNames(sharedClassName, className); - - return this.renderPopupMenu(mergedClassName, style); - }} - - ); - } - - render() { - const props = { ...this.props }; - const visible = this.getVisible(); - const prefixCls = this.getPrefixCls(); - const inline = this.isInlineMode(); - const mergedMode = this.getMode(); - const className = classNames(prefixCls, `${prefixCls}-${mergedMode}`, { - [props.className]: !!props.className, - [this.getOpenClassName()]: visible, - [this.getActiveClassName()]: props.active || (visible && !inline), - [this.getDisabledClassName()]: props.disabled, - [this.getSelectedClassName()]: this.isChildrenSelected(), - }); - - if (!this.internalMenuId) { - if (props.eventKey) { - this.internalMenuId = `${props.eventKey}$Menu`; - } else { - guid += 1; - this.internalMenuId = `$__$${guid}$Menu`; - } - } - - let mouseEvents = {}; - let titleClickEvents = {}; - let titleMouseEvents = {}; - if (!props.disabled) { - mouseEvents = { - onMouseLeave: this.onMouseLeave, - onMouseEnter: this.onMouseEnter, - }; - - // only works in title, not outer li - titleClickEvents = { - onClick: this.onTitleClick, - }; - titleMouseEvents = { - onMouseEnter: this.onTitleMouseEnter, - onMouseLeave: this.onTitleMouseLeave, - }; - } - - const style: React.CSSProperties = {}; - - const { direction } = props; - const isRTL = direction === 'rtl'; - - if (inline) { - if (isRTL) { - style.paddingRight = props.inlineIndent * props.level; - } else { - style.paddingLeft = props.inlineIndent * props.level; - } - } - - let ariaOwns = {}; - // only set aria-owns when menu is open - // otherwise it would be an invalid aria-owns value - // since corresponding node cannot be found - if (this.getVisible()) { - ariaOwns = { - 'aria-owns': this.internalMenuId, - }; - } - - // expand custom icon should NOT be displayed in menu with horizontal mode. - let icon = null; - if (mergedMode !== 'horizontal') { - icon = this.props.expandIcon; // ReactNode - if (typeof this.props.expandIcon === 'function') { - icon = React.createElement(this.props.expandIcon as any, { - ...this.props, - }); - } - } - - const title = ( -
      - {props.title} - {icon || } -
      - ); - - const children = this.renderChildren(); - - const getPopupContainer = props.parentMenu?.isRootMenu - ? props.parentMenu.props.getPopupContainer - : (triggerNode: HTMLElement) => triggerNode.parentNode; - const popupPlacement = popupPlacementMap[mergedMode]; - const popupAlign = props.popupOffset ? { offset: props.popupOffset } : {}; - const popupClassName = classNames({ - [props.popupClassName]: props.popupClassName && !inline, - [`${prefixCls}-rtl`]: isRTL, - }); - const { - disabled, - triggerSubMenuAction, - subMenuOpenDelay, - forceSubMenuRender, - subMenuCloseDelay, - builtinPlacements, - } = props; - menuAllProps.forEach((key) => delete props[key]); - // Set onClick to null, to ignore propagated onClick event - delete props.onClick; - const placement = isRTL - ? { ...placementsRtl, ...builtinPlacements } - : { ...placements, ...builtinPlacements }; - delete props.direction; - - // [Legacy] It's a fast fix, - // but we should check if we can refactor this to make code more easy to understand - const baseProps = this.getBaseProps(); - const mergedMotion: CSSMotionProps = inline - ? null - : this.getMotion(baseProps.mode, baseProps.visible); - - return ( -
    • - - {title} - - {inline ? children : null} -
    • - ); - } -} - -const connected = connect( - ({ openKeys, activeKey, selectedKeys }, { eventKey, subMenuKey }) => ({ - isOpen: openKeys.indexOf(eventKey) > -1, - active: activeKey[subMenuKey] === eventKey, - selectedKeys, - }), -)(SubMenu as any); - -connected.isSubMenu = true; - -export default connected; diff --git a/src/SubPopupMenu.tsx b/src/SubPopupMenu.tsx deleted file mode 100644 index 10467584..00000000 --- a/src/SubPopupMenu.tsx +++ /dev/null @@ -1,483 +0,0 @@ -import * as React from 'react'; -import { connect } from 'mini-store'; -import type { CSSMotionProps } from 'rc-motion'; -import KeyCode from 'rc-util/lib/KeyCode'; -import createChainedFunction from 'rc-util/lib/createChainedFunction'; -import toArray from 'rc-util/lib/Children/toArray'; -import shallowEqual from 'shallowequal'; -import classNames from 'classnames'; -import { - getKeyFromChildrenIndex, - loopMenuItem, - noop, - menuAllProps, - isMobileDevice, -} from './util'; -import DOMWrap from './DOMWrap'; -import type { - SelectEventHandler, - OpenEventHandler, - DestroyEventHandler, - MiniStore, - MenuMode, - LegacyFunctionRef, - RenderIconType, - HoverEventHandler, - BuiltinPlacements, - MenuClickEventHandler, - MenuInfo, - TriggerSubMenuAction, -} from './interface'; -import type { MenuItem, MenuItemProps } from './MenuItem'; -import type { MenuItemGroupProps } from './MenuItemGroup'; - -function allDisabled(arr: MenuItem[]) { - if (!arr.length) { - return true; - } - return arr.every((c) => !!c.props.disabled); -} - -function updateActiveKey( - store: MiniStore, - menuId: React.Key, - activeKey: React.Key, -) { - const state = store.getState(); - store.setState({ - activeKey: { - ...state.activeKey, - [menuId]: activeKey, - }, - }); -} - -function getEventKey(props: SubPopupMenuProps): React.Key { - // when eventKey not available ,it's menu and return menu id '0-menu-' - return props.eventKey || '0-menu-'; -} - -export function getActiveKey( - props: { - children?: React.ReactNode; - eventKey?: React.Key; - defaultActiveFirst?: boolean; - }, - originalActiveKey: string, -) { - let activeKey: React.Key = originalActiveKey; - const { children, eventKey } = props; - if (activeKey) { - let found: boolean; - loopMenuItem(children, (c, i) => { - if ( - c && - c.props && - !c.props.disabled && - activeKey === getKeyFromChildrenIndex(c, eventKey, i) - ) { - found = true; - } - }); - if (found) { - return activeKey; - } - } - activeKey = null; - if (props.defaultActiveFirst) { - loopMenuItem(children, (c, i) => { - if (!activeKey && c && !c.props.disabled) { - activeKey = getKeyFromChildrenIndex(c, eventKey, i); - } - }); - return activeKey; - } - return activeKey; -} - -export function saveRef(c: React.ReactInstance) { - if (!c) { - return; - } - /* eslint-disable @typescript-eslint/no-invalid-this */ - const index = this.instanceArray.indexOf(c); - if (index !== -1) { - // update component if it's already inside instanceArray - this.instanceArray[index] = c; - } else { - // add component if it's not in instanceArray yet; - this.instanceArray.push(c); - } - /* eslint-enable @typescript-eslint/no-invalid-this */ -} - -export interface SubPopupMenuProps { - onSelect?: SelectEventHandler; - onClick?: MenuClickEventHandler; - onDeselect?: SelectEventHandler; - onOpenChange?: OpenEventHandler; - onDestroy?: DestroyEventHandler; - openKeys?: string[]; - visible?: boolean; - children?: React.ReactNode; - parentMenu?: React.ReactInstance; - eventKey?: React.Key; - store?: MiniStore; - - // adding in refactor - prefixCls?: string; - focusable?: boolean; - multiple?: boolean; - style?: React.CSSProperties; - className?: string; - defaultActiveFirst?: boolean; - activeKey?: string; - selectedKeys?: string[]; - defaultSelectedKeys?: string[]; - defaultOpenKeys?: string[]; - level?: number; - mode?: MenuMode; - triggerSubMenuAction?: TriggerSubMenuAction; - inlineIndent?: number; - manualRef?: LegacyFunctionRef; - itemIcon?: RenderIconType; - expandIcon?: RenderIconType; - - subMenuOpenDelay?: number; - subMenuCloseDelay?: number; - forceSubMenuRender?: boolean; - builtinPlacements?: BuiltinPlacements; - role?: string; - id?: string; - overflowedIndicator?: React.ReactNode; - theme?: string; - - // [Legacy] - // openTransitionName?: string; - // openAnimation?: OpenAnimation; - motion?: CSSMotionProps; - - direction?: 'ltr' | 'rtl'; -} - -export class SubPopupMenu extends React.Component { - static defaultProps = { - prefixCls: 'rc-menu', - className: '', - mode: 'vertical', - level: 1, - inlineIndent: 24, - visible: true, - focusable: true, - style: {}, - manualRef: noop, - }; - - constructor(props: SubPopupMenuProps) { - super(props); - - props.store.setState({ - activeKey: { - ...props.store.getState().activeKey, - [props.eventKey]: getActiveKey(props, props.activeKey), - }, - }); - - this.instanceArray = []; - } - - instanceArray: MenuItem[]; - - componentDidMount() { - // invoke customized ref to expose component to mixin - if (this.props.manualRef) { - this.props.manualRef(this); - } - } - - shouldComponentUpdate(nextProps: SubPopupMenuProps) { - return ( - this.props.visible || - nextProps.visible || - this.props.className !== nextProps.className || - !shallowEqual(this.props.style, nextProps.style) - ); - } - - componentDidUpdate(prevProps: SubPopupMenuProps) { - const { props } = this; - const originalActiveKey = - 'activeKey' in props - ? props.activeKey - : props.store.getState().activeKey[getEventKey(props)]; - const activeKey = getActiveKey(props, originalActiveKey); - if (activeKey !== originalActiveKey) { - updateActiveKey(props.store, getEventKey(props), activeKey); - } else if ('activeKey' in prevProps) { - // If prev activeKey is not same as current activeKey, - // we should set it. - const prevActiveKey = getActiveKey(prevProps, prevProps.activeKey); - if (activeKey !== prevActiveKey) { - updateActiveKey(props.store, getEventKey(props), activeKey); - } - } - } - - /** - * all keyboard events callbacks run from here at first - * - * note: - * This legacy code that `onKeyDown` is called by parent instead of dom self. - * which need return code to check if this event is handled - */ - onKeyDown = ( - e: React.KeyboardEvent, - callback: (item: MenuItem) => void, - ) => { - const { keyCode } = e; - let handled: boolean; - this.getFlatInstanceArray().forEach((obj: MenuItem) => { - if (obj && obj.props.active && obj.onKeyDown) { - handled = obj.onKeyDown(e); - } - }); - if (handled) { - return 1; - } - let activeItem: MenuItem = null; - if (keyCode === KeyCode.UP || keyCode === KeyCode.DOWN) { - activeItem = this.step(keyCode === KeyCode.UP ? -1 : 1); - } - if (activeItem) { - e.preventDefault(); - updateActiveKey( - this.props.store, - getEventKey(this.props), - activeItem.props.eventKey, - ); - - if (typeof callback === 'function') { - callback(activeItem); - } - - return 1; - } - return undefined; - }; - - onItemHover: HoverEventHandler = (e) => { - const { key, hover } = e; - updateActiveKey( - this.props.store, - getEventKey(this.props), - hover ? key : null, - ); - }; - - onDeselect: SelectEventHandler = (selectInfo) => { - this.props.onDeselect(selectInfo); - }; - - onSelect: SelectEventHandler = (selectInfo) => { - this.props.onSelect(selectInfo); - }; - - onClick: MenuClickEventHandler = (e) => { - this.props.onClick(e); - }; - - onOpenChange: OpenEventHandler = (e) => { - this.props.onOpenChange(e); - }; - - onDestroy: DestroyEventHandler = (key) => { - /* istanbul ignore next */ - this.props.onDestroy(key); - }; - - getFlatInstanceArray = () => this.instanceArray; - - step = (direction: number) => { - let children = this.getFlatInstanceArray(); - const activeKey = this.props.store.getState().activeKey[ - getEventKey(this.props) - ]; - const len = children.length; - if (!len) { - return null; - } - if (direction < 0) { - children = children.concat().reverse(); - } - // find current activeIndex - let activeIndex = -1; - children.every((c, ci) => { - if (c && c.props.eventKey === activeKey) { - activeIndex = ci; - return false; - } - return true; - }); - if ( - !this.props.defaultActiveFirst && - activeIndex !== -1 && - allDisabled(children.slice(activeIndex, len - 1)) - ) { - return undefined; - } - const start = (activeIndex + 1) % len; - let i = start; - - do { - const child = children[i]; - if (!child || child.props.disabled) { - i = (i + 1) % len; - } else { - return child; - } - } while (i !== start); - - return null; - }; - - renderCommonMenuItem = ( - child: React.ReactElement, - i: number, - extraProps: MenuItemProps, - ) => { - const state = this.props.store.getState(); - const { props } = this; - const key = getKeyFromChildrenIndex(child, props.eventKey, i); - const childProps = child.props; - // https://github.com/ant-design/ant-design/issues/11517#issuecomment-477403055 - if (!childProps || typeof child.type === 'string') { - return child; - } - const isActive = key === state.activeKey; - const newChildProps: MenuItemProps & - MenuItemGroupProps & - SubPopupMenuProps = { - mode: childProps.mode || props.mode, - level: props.level, - inlineIndent: props.inlineIndent, - renderMenuItem: this.renderMenuItem, - rootPrefixCls: props.prefixCls, - index: i, - parentMenu: props.parentMenu, - // customized ref function, need to be invoked manually in child's componentDidMount - manualRef: childProps.disabled - ? undefined - : (createChainedFunction( - (child as any).ref, - saveRef.bind(this), - ) as LegacyFunctionRef), - eventKey: key, - active: !childProps.disabled && isActive, - multiple: props.multiple, - onClick: (e: MenuInfo) => { - (childProps.onClick || noop)(e); - this.onClick(e); - }, - onItemHover: this.onItemHover, - motion: props.motion, - subMenuOpenDelay: props.subMenuOpenDelay, - subMenuCloseDelay: props.subMenuCloseDelay, - forceSubMenuRender: props.forceSubMenuRender, - onOpenChange: this.onOpenChange, - onDeselect: this.onDeselect, - onSelect: this.onSelect, - builtinPlacements: props.builtinPlacements, - itemIcon: childProps.itemIcon || this.props.itemIcon, - expandIcon: childProps.expandIcon || this.props.expandIcon, - ...extraProps, - direction: props.direction, - }; - // ref: https://github.com/ant-design/ant-design/issues/13943 - if (props.mode === 'inline' || isMobileDevice()) { - newChildProps.triggerSubMenuAction = 'click'; - } - return React.cloneElement(child, { - ...newChildProps, - key: key || i, - }); - }; - - renderMenuItem = ( - c: React.ReactElement, - i: number, - subMenuKey: React.Key, - ) => { - /* istanbul ignore if */ - if (!c) { - return null; - } - const state = this.props.store.getState(); - const extraProps = { - openKeys: state.openKeys, - selectedKeys: state.selectedKeys, - triggerSubMenuAction: this.props.triggerSubMenuAction, - subMenuKey, - }; - return this.renderCommonMenuItem(c, i, extraProps); - }; - - render() { - const { ...props } = this.props; - this.instanceArray = []; - const className = classNames( - props.prefixCls, - props.className, - `${props.prefixCls}-${props.mode}`, - ); - const domProps: React.HTMLAttributes = { - className, - // role could be 'select' and by default set to menu - role: props.role || 'menu', - }; - if (props.id) { - domProps.id = props.id; - } - if (props.focusable) { - domProps.tabIndex = 0; - domProps.onKeyDown = this.onKeyDown as any; - } - const { - prefixCls, - eventKey, - visible, - level, - mode, - overflowedIndicator, - theme, - } = props; - menuAllProps.forEach((key) => delete props[key]); - - // Otherwise, the propagated click event will trigger another onClick - delete props.onClick; - - return ( - - {toArray(props.children).map((c: React.ReactElement, i) => - this.renderMenuItem(c, i, eventKey || '0-menu-'), - )} - - ); - } -} -const connected = (connect()( - SubPopupMenu as any, -) as unknown) as React.ComponentClass & { - getWrappedInstance: () => SubPopupMenu; -}; - -export default connected; diff --git a/src/dataUtil.ts b/src/dataUtil.ts new file mode 100644 index 00000000..d5c2a418 --- /dev/null +++ b/src/dataUtil.ts @@ -0,0 +1,29 @@ +import type * as React from 'react'; +import toArray from 'rc-util/lib/Children/toArray'; +import type { MenuItemData } from './interface'; + +export function convertToData(children: React.ReactNode): MenuItemData[] { + const childList = toArray(children); + + return childList.map((node: React.ReactElement) => { + const { + key, + props: { children: subChildren, ...restProps }, + } = node; + + console.log(node); + + const subData = convertToData(subChildren); + + const data = { + key, + ...restProps, + }; + + if (subData.length) { + data.children = subData; + } + + return data; + }); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..402dc1b4 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,35 @@ +import Menu, { MenuProps } from './Menu'; +import MenuItem from './sugar/MenuItem'; +import SubMenu from './sugar/SubMenu'; +import MenuItemGroup from './sugar/MenuItemGroup'; +import Divider from './sugar/Divider'; + + +export { + SubMenu, + MenuItem as Item, + MenuItem, + MenuItemGroup, + MenuItemGroup as ItemGroup, + Divider, + MenuProps, + // SubMenuProps, + // MenuItemProps, + // MenuItemGroupProps, +}; + +type MenuType = typeof Menu & { + Item: typeof MenuItem; + SubMenu: typeof SubMenu; + ItemGroup: typeof MenuItemGroup; + Divider: typeof Divider; +}; + +const ExportMenu = Menu as MenuType; + +ExportMenu.Item = MenuItem; +ExportMenu.SubMenu = SubMenu; +ExportMenu.ItemGroup = MenuItemGroup; +ExportMenu.Divider = Divider; + +export default ExportMenu; diff --git a/src/index.tsx b/src/index.tsx deleted file mode 100644 index 6d93f39e..00000000 --- a/src/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import Menu, { MenuProps } from './Menu'; -import SubMenu, { SubMenuProps } from './SubMenu'; -import MenuItem, { MenuItemProps } from './MenuItem'; -import MenuItemGroup, { MenuItemGroupProps } from './MenuItemGroup'; -import Divider from './Divider'; - -export { - SubMenu, - MenuItem as Item, - MenuItem, - MenuItemGroup, - MenuItemGroup as ItemGroup, - Divider, - MenuProps, - SubMenuProps, - MenuItemProps, - MenuItemGroupProps, -}; - -export default Menu; diff --git a/src/interface.ts b/src/interface.ts index feecd216..c8f6ebba 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,43 +1,4 @@ -export type RenderIconType = - | React.ReactNode - | ((props: any) => React.ReactNode); - -export interface MenuInfo { - key: React.Key; - keyPath: React.Key[]; - item: React.ReactInstance; - domEvent: React.MouseEvent; -} -export interface SelectInfo extends MenuInfo { - selectedKeys?: React.Key[]; -} - -export type SelectEventHandler = (info: SelectInfo) => void; - -export type HoverEventHandler = (info: { - key: React.Key; - hover: boolean; -}) => void; - -export type MenuHoverEventHandler = (info: { - key: React.Key; - domEvent: React.MouseEvent; -}) => void; - -export type MenuClickEventHandler = (info: MenuInfo) => void; - -export type DestroyEventHandler = (key: React.Key) => void; - -export type OpenEventHandler = ( - keys: - | React.Key[] - | { - key: React.Key; - item: React.ReactInstance; - trigger: string; - open: boolean; - }, -) => void; +import React from "_@types_react@16.14.5@@types/react"; export type MenuMode = | 'horizontal' @@ -46,16 +7,14 @@ export type MenuMode = | 'vertical-right' | 'inline'; -export type OpenAnimation = string | Record; - -export interface MiniStore { - getState: () => any; - setState: (state: any) => void; - subscribe: (listener: () => void) => () => void; -} - -export type LegacyFunctionRef = (node: React.ReactInstance) => void; - -export type BuiltinPlacements = Record; - -export type TriggerSubMenuAction = 'click' | 'hover'; +export interface MenuItemData { + children?: MenuItemData[]; + disabled?: boolean; + icon?: React.ReactNode; + key: string | number; + popupClassName?: string; + popupOffset?: [number, number]; + title?: React.ReactNode; + // TODO: fill this + onTitleClick?: () => void; +} \ No newline at end of file diff --git a/src/placements.ts b/src/placements.ts deleted file mode 100644 index 7b45acf9..00000000 --- a/src/placements.ts +++ /dev/null @@ -1,52 +0,0 @@ -const autoAdjustOverflow = { - adjustX: 1, - adjustY: 1, -}; - -export const placements = { - topLeft: { - points: ['bl', 'tl'], - overflow: autoAdjustOverflow, - offset: [0, -7], - }, - bottomLeft: { - points: ['tl', 'bl'], - overflow: autoAdjustOverflow, - offset: [0, 7], - }, - leftTop: { - points: ['tr', 'tl'], - overflow: autoAdjustOverflow, - offset: [-4, 0], - }, - rightTop: { - points: ['tl', 'tr'], - overflow: autoAdjustOverflow, - offset: [4, 0], - }, -}; - -export const placementsRtl = { - topLeft: { - points: ['bl', 'tl'], - overflow: autoAdjustOverflow, - offset: [0, -7], - }, - bottomLeft: { - points: ['tl', 'bl'], - overflow: autoAdjustOverflow, - offset: [0, 7], - }, - rightTop: { - points: ['tr', 'tl'], - overflow: autoAdjustOverflow, - offset: [-4, 0], - }, - leftTop: { - points: ['tl', 'tr'], - overflow: autoAdjustOverflow, - offset: [4, 0], - }, -}; - -export default placements; diff --git a/src/sugar/Divider.tsx b/src/sugar/Divider.tsx new file mode 100644 index 00000000..ed2ec755 --- /dev/null +++ b/src/sugar/Divider.tsx @@ -0,0 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export default function Divider(_: any) { + return null; +} diff --git a/src/sugar/MenuItem.tsx b/src/sugar/MenuItem.tsx new file mode 100644 index 00000000..d955d688 --- /dev/null +++ b/src/sugar/MenuItem.tsx @@ -0,0 +1,6 @@ +import type { MenuItemProps } from '../MenuItem'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export default function MenuItem(_: MenuItemProps) { + return null; +} diff --git a/src/sugar/MenuItemGroup.tsx b/src/sugar/MenuItemGroup.tsx new file mode 100644 index 00000000..a827c6c6 --- /dev/null +++ b/src/sugar/MenuItemGroup.tsx @@ -0,0 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export default function MenuItemGroup(_: any) { + return null; +} diff --git a/src/sugar/SubMenu.tsx b/src/sugar/SubMenu.tsx new file mode 100644 index 00000000..9f2b81ae --- /dev/null +++ b/src/sugar/SubMenu.tsx @@ -0,0 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export default function SubMenu(_: any) { + return null; +} diff --git a/src/util.ts b/src/util.ts deleted file mode 100644 index 9f1ab844..00000000 --- a/src/util.ts +++ /dev/null @@ -1,166 +0,0 @@ -import * as React from 'react'; -import isMobile from './utils/isMobile'; -import type MenuItemGroup from './MenuItemGroup'; -import type SubMenu from './SubMenu'; -import type MenuItem from './MenuItem'; - -export function noop() {} - -export function getKeyFromChildrenIndex( - child: React.ReactElement, - menuEventKey: React.Key, - index: number, -): React.Key { - const prefix = menuEventKey || ''; - return child.key || `${prefix}item_${index}`; -} - -export function getMenuIdFromSubMenuEventKey(eventKey: string): React.Key { - return `${eventKey}-menu-`; -} - -export function loopMenuItem( - children: React.ReactNode, - cb: (node: React.ReactElement, index: number) => void, -) { - let index = -1; - React.Children.forEach(children, (c: React.ReactElement) => { - index += 1; - if (c && c.type && (c.type as typeof MenuItemGroup).isMenuItemGroup) { - React.Children.forEach(c.props.children, (c2: React.ReactElement) => { - index += 1; - cb(c2, index); - }); - } else { - cb(c, index); - } - }); -} - -export function loopMenuItemRecursively( - children: React.ReactNode, - keys: string[], - ret: { find: boolean }, -) { - /* istanbul ignore if */ - if (!children || ret.find) { - return; - } - React.Children.forEach(children, (c: React.ReactElement) => { - if (c) { - const construct = c.type as - | typeof MenuItemGroup - | typeof MenuItem - | typeof SubMenu; - if ( - !construct || - !( - construct.isSubMenu || - construct.isMenuItem || - construct.isMenuItemGroup - ) - ) { - return; - } - if (keys.indexOf((c as any).key) !== -1) { - // eslint-disable-next-line no-param-reassign - ret.find = true; - } else if (c.props.children) { - loopMenuItemRecursively(c.props.children, keys, ret); - } - } - }); -} - -export const menuAllProps = [ - 'defaultSelectedKeys', - 'selectedKeys', - 'defaultOpenKeys', - 'openKeys', - 'mode', - 'getPopupContainer', - 'onSelect', - 'onDeselect', - 'onDestroy', - 'openTransitionName', - 'openAnimation', - 'subMenuOpenDelay', - 'subMenuCloseDelay', - 'forceSubMenuRender', - 'triggerSubMenuAction', - 'level', - 'selectable', - 'multiple', - 'onOpenChange', - 'visible', - 'focusable', - 'defaultActiveFirst', - 'prefixCls', - 'inlineIndent', - 'parentMenu', - 'title', - 'rootPrefixCls', - 'eventKey', - 'active', - 'onItemHover', - 'onTitleMouseEnter', - 'onTitleMouseLeave', - 'onTitleClick', - 'popupAlign', - 'popupOffset', - 'isOpen', - 'renderMenuItem', - 'manualRef', - 'subMenuKey', - 'disabled', - 'index', - 'isSelected', - 'store', - 'activeKey', - 'builtinPlacements', - 'overflowedIndicator', - 'motion', - - // the following keys found need to be removed from test regression - 'attribute', - 'value', - 'popupClassName', - 'inlineCollapsed', - 'menu', - 'theme', - 'itemIcon', - 'expandIcon', -]; - -// ref: https://github.com/ant-design/ant-design/issues/14007 -// ref: https://bugs.chromium.org/p/chromium/issues/detail?id=360889 -// getBoundingClientRect return the full precision value, which is -// not the same behavior as on chrome. Set the precision to 6 to -// unify their behavior -export const getWidth = (elem: HTMLElement, includeMargin: boolean = false) => { - let width = - elem && - typeof elem.getBoundingClientRect === 'function' && - elem.getBoundingClientRect().width; - if (width) { - if (includeMargin) { - const { marginLeft, marginRight } = getComputedStyle(elem); - width += +marginLeft.replace('px', '') + +marginRight.replace('px', ''); - } - width = +width.toFixed(6); - } - return width || 0; -}; - -export const setStyle = ( - elem: HTMLElement, - styleProperty: keyof React.CSSProperties, - value: string | number, -) => { - if (elem && typeof elem.style === 'object') { - // eslint-disable-next-line no-param-reassign - elem.style[styleProperty] = value; - } -}; - -export const isMobileDevice = (): boolean => isMobile.any; diff --git a/src/utils/isMobile.ts b/src/utils/isMobile.ts deleted file mode 100644 index d70203ff..00000000 --- a/src/utils/isMobile.ts +++ /dev/null @@ -1,121 +0,0 @@ -// MIT License from https://github.com/kaimallea/isMobile - -const applePhone = /iPhone/i; -const appleIpod = /iPod/i; -const appleTablet = /iPad/i; -const androidPhone = /\bAndroid(?:.+)Mobile\b/i; // Match 'Android' AND 'Mobile' -const androidTablet = /Android/i; -const amazonPhone = /\bAndroid(?:.+)SD4930UR\b/i; -const amazonTablet = /\bAndroid(?:.+)(?:KF[A-Z]{2,4})\b/i; -const windowsPhone = /Windows Phone/i; -const windowsTablet = /\bWindows(?:.+)ARM\b/i; // Match 'Windows' AND 'ARM' -const otherBlackberry = /BlackBerry/i; -const otherBlackberry10 = /BB10/i; -const otherOpera = /Opera Mini/i; -const otherChrome = /\b(CriOS|Chrome)(?:.+)Mobile/i; -const otherFirefox = /Mobile(?:.+)Firefox\b/i; // Match 'Mobile' AND 'Firefox' - -function match(regex, userAgent) { - return regex.test(userAgent); -} - -function isMobile(userAgent?: string) { - let ua = - userAgent || (typeof navigator !== 'undefined' ? navigator.userAgent : ''); - - // Facebook mobile app's integrated browser adds a bunch of strings that - // match everything. Strip it out if it exists. - let tmp = ua.split('[FBAN'); - if (typeof tmp[1] !== 'undefined') { - [ua] = tmp; - } - - // Twitter mobile app's integrated browser on iPad adds a "Twitter for - // iPhone" string. Same probably happens on other tablet platforms. - // This will confuse detection so strip it out if it exists. - tmp = ua.split('Twitter'); - if (typeof tmp[1] !== 'undefined') { - [ua] = tmp; - } - - const result = { - apple: { - phone: match(applePhone, ua) && !match(windowsPhone, ua), - ipod: match(appleIpod, ua), - tablet: - !match(applePhone, ua) && - match(appleTablet, ua) && - !match(windowsPhone, ua), - device: - (match(applePhone, ua) || - match(appleIpod, ua) || - match(appleTablet, ua)) && - !match(windowsPhone, ua), - }, - amazon: { - phone: match(amazonPhone, ua), - tablet: !match(amazonPhone, ua) && match(amazonTablet, ua), - device: match(amazonPhone, ua) || match(amazonTablet, ua), - }, - android: { - phone: - (!match(windowsPhone, ua) && match(amazonPhone, ua)) || - (!match(windowsPhone, ua) && match(androidPhone, ua)), - tablet: - !match(windowsPhone, ua) && - !match(amazonPhone, ua) && - !match(androidPhone, ua) && - (match(amazonTablet, ua) || match(androidTablet, ua)), - device: - (!match(windowsPhone, ua) && - (match(amazonPhone, ua) || - match(amazonTablet, ua) || - match(androidPhone, ua) || - match(androidTablet, ua))) || - match(/\bokhttp\b/i, ua), - }, - windows: { - phone: match(windowsPhone, ua), - tablet: match(windowsTablet, ua), - device: match(windowsPhone, ua) || match(windowsTablet, ua), - }, - other: { - blackberry: match(otherBlackberry, ua), - blackberry10: match(otherBlackberry10, ua), - opera: match(otherOpera, ua), - firefox: match(otherFirefox, ua), - chrome: match(otherChrome, ua), - device: - match(otherBlackberry, ua) || - match(otherBlackberry10, ua) || - match(otherOpera, ua) || - match(otherFirefox, ua) || - match(otherChrome, ua), - }, - - // Additional - any: null, - phone: null, - tablet: null, - }; - result.any = - result.apple.device || - result.android.device || - result.windows.device || - result.other.device; - - // excludes 'other' devices and ipods, targeting touchscreen phones - result.phone = - result.apple.phone || result.android.phone || result.windows.phone; - result.tablet = - result.apple.tablet || result.android.tablet || result.windows.tablet; - - return result; -} - -const defaultResult = { - ...isMobile(), - isMobile, -}; - -export default defaultResult; diff --git a/src/utils/legacyUtil.ts b/src/utils/legacyUtil.ts deleted file mode 100644 index 7e885864..00000000 --- a/src/utils/legacyUtil.ts +++ /dev/null @@ -1,58 +0,0 @@ -import warning from 'rc-util/lib/warning'; -import type { CSSMotionProps } from 'rc-motion'; -import type { OpenAnimation, MenuMode } from '../interface'; - -interface GetMotionProps { - motion?: CSSMotionProps; - defaultMotions?: Partial<{ [key in MenuMode | 'other']: CSSMotionProps }>; - openAnimation?: OpenAnimation; - openTransitionName?: string; - prefixCls?: string; -} - -interface GetMotionState { - switchingModeFromInline: boolean; -} - -export function getMotion( - { - prefixCls, - motion, - defaultMotions = {}, - openAnimation, - openTransitionName, - }: GetMotionProps, - { switchingModeFromInline }: GetMotionState, - menuMode: MenuMode, -): CSSMotionProps { - if (motion) { - return motion; - } - - if (typeof openAnimation === 'object' && openAnimation) { - warning( - false, - 'Object type of `openAnimation` is removed. Please use `motion` instead.', - ); - } else if (typeof openAnimation === 'string') { - return { - motionName: `${prefixCls}-open-${openAnimation}`, - }; - } - - if (openTransitionName) { - return { - motionName: openTransitionName, - }; - } - // Default logic - const defaultMotion = defaultMotions[menuMode]; - - if (defaultMotion) { - return defaultMotion; - } - - // When mode switch from inline - // submenu should hide without animation - return switchingModeFromInline ? null : defaultMotions.other; -} From 4361e5f230aa7b2062be8f18de97233fb130eae4 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 6 Apr 2021 19:13:02 +0800 Subject: [PATCH 02/93] more classNames --- assets/menu.less | 8 ++++-- docs/demo/debug.md | 2 +- docs/examples/debug.tsx | 1 + src/Menu.tsx | 44 ++++++++++++++-------------- src/MenuItem.tsx | 11 ++++--- src/SubMenu.tsx | 64 +++++++++++++++++++++++++++++++++++++++++ src/dataUtil.ts | 29 ------------------- src/index.ts | 5 ++-- src/interface.ts | 21 +------------- src/sugar/SubMenu.tsx | 4 --- 10 files changed, 101 insertions(+), 88 deletions(-) create mode 100644 src/SubMenu.tsx delete mode 100644 src/dataUtil.ts delete mode 100644 src/sugar/SubMenu.tsx diff --git a/assets/menu.less b/assets/menu.less index 4d75a8f5..63e9d26f 100644 --- a/assets/menu.less +++ b/assets/menu.less @@ -1,6 +1,8 @@ -@menuPrefixCls: rc-menu; +@menuPrefixCls: ~'rc-menu'; .@{menuPrefixCls} { - display: flex; - flex-wrap: nowrap; + &-horizontal { + display: flex; + flex-wrap: nowrap; + } } \ No newline at end of file diff --git a/docs/demo/debug.md b/docs/demo/debug.md index 4f7c1fe5..25e02bfb 100644 --- a/docs/demo/debug.md +++ b/docs/demo/debug.md @@ -1,3 +1,3 @@ -## single +## debug \ No newline at end of file diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx index 034b78d3..db35f2d0 100644 --- a/docs/examples/debug.tsx +++ b/docs/examples/debug.tsx @@ -11,6 +11,7 @@ export default () => { return ( Navigation One + Next Item ); }; diff --git a/src/Menu.tsx b/src/Menu.tsx index be5d5b7b..054b3762 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; import { useMemo } from 'react'; import classNames from 'classnames'; +import toArray from 'rc-util/lib/Children/toArray'; import Overflow from 'rc-overflow'; -import type { MenuItemData, MenuMode } from './interface'; -import { convertToData } from './dataUtil'; +import type { MenuMode } from './interface'; import MenuItem from './MenuItem'; export const MenuContext = React.createContext({ @@ -15,9 +15,11 @@ export interface MenuProps prefixCls?: string; mode?: MenuMode; - options?: MenuItemData[]; children?: React.ReactNode; + /** direction of menu */ + direction?: 'ltr' | 'rtl'; + // defaultSelectedKeys?: string[]; // defaultActiveFirst?: boolean; // selectedKeys?: string[]; @@ -53,9 +55,6 @@ export interface MenuProps // /** @deprecated Please use `motion` instead */ // openAnimation?: OpenAnimation; - // /** direction of menu */ - // direction?: 'ltr' | 'rtl'; - // inlineCollapsed?: boolean; // /** SiderContextProps of layout in ant design */ @@ -68,31 +67,32 @@ const Menu: React.FC = ({ style, className, tabIndex = 0, - - // Data - options, + mode = 'vertical', children, + direction, }) => { - const parsedOptions = useMemo(() => { - if (options) { - return options; - } - - return convertToData(children); - }, [options, children]); - const menuContext = useMemo(() => ({ prefixCls }), [prefixCls]); - const node = ( + const childList: React.ReactElement[] = toArray(children); + + const container = ( item.key} + data={childList} + renderRawItem={node => node} /> ); @@ -106,7 +106,7 @@ const Menu: React.FC = ({ // ); return ( - {node} + {container} ); }; diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index 070cc16d..a88f1323 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import Overflow from 'rc-overflow'; import { MenuContext } from './Menu'; export interface MenuItemProps @@ -37,15 +38,13 @@ export interface MenuItemProps // direction?: 'ltr' | 'rtl'; } -const MenuItem: React.FC = ({ children }) => { +export default function MenuItem({ children }: MenuItemProps) { const { prefixCls } = React.useContext(MenuContext); const itemCls = `${prefixCls}-item`; return ( -
    • + {children} -
    • + ); -}; - -export default MenuItem; +} diff --git a/src/SubMenu.tsx b/src/SubMenu.tsx new file mode 100644 index 00000000..79dd420c --- /dev/null +++ b/src/SubMenu.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import Overflow from 'rc-overflow'; +import { MenuContext } from './Menu'; + +export interface SubMenuProps { + children?: React.ReactNode; + + // parentMenu?: React.ReactElement & { + // isRootMenu: boolean; + // subMenuInstance: React.ReactInstance; + // }; + // title?: React.ReactNode; + + // selectedKeys?: string[]; + // openKeys?: string[]; + // onClick?: MenuClickEventHandler; + // onOpenChange?: OpenEventHandler; + // rootPrefixCls?: string; + // eventKey?: string; + // multiple?: boolean; + // active?: boolean; // TODO: remove + // onItemHover?: HoverEventHandler; + // onSelect?: SelectEventHandler; + // triggerSubMenuAction?: TriggerSubMenuAction; + // onDeselect?: SelectEventHandler; + // onDestroy?: DestroyEventHandler; + // onMouseEnter?: MenuHoverEventHandler; + // onMouseLeave?: MenuHoverEventHandler; + // onTitleMouseEnter?: MenuHoverEventHandler; + // onTitleMouseLeave?: MenuHoverEventHandler; + // onTitleClick?: (info: { + // key: React.Key; + // domEvent: React.MouseEvent | React.KeyboardEvent; + // }) => void; + // popupOffset?: number[]; + // isOpen?: boolean; + // store?: MiniStore; + // mode?: MenuMode; + // manualRef?: LegacyFunctionRef; + // itemIcon?: RenderIconType; + // expandIcon?: RenderIconType; + // inlineIndent?: number; + // level?: number; + // subMenuOpenDelay?: number; + // subMenuCloseDelay?: number; + // forceSubMenuRender?: boolean; + // builtinPlacements?: BuiltinPlacements; + // disabled?: boolean; + // className?: string; + // popupClassName?: string; + // motion?: CSSMotionProps; + // direction?: 'ltr' | 'rtl'; +} + +export default function SubMenu({ children }: SubMenuProps) { + const { prefixCls } = React.useContext(MenuContext); + const itemCls = `${prefixCls}-submenu`; + + return ( + + {children} + + ); +} diff --git a/src/dataUtil.ts b/src/dataUtil.ts deleted file mode 100644 index d5c2a418..00000000 --- a/src/dataUtil.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type * as React from 'react'; -import toArray from 'rc-util/lib/Children/toArray'; -import type { MenuItemData } from './interface'; - -export function convertToData(children: React.ReactNode): MenuItemData[] { - const childList = toArray(children); - - return childList.map((node: React.ReactElement) => { - const { - key, - props: { children: subChildren, ...restProps }, - } = node; - - console.log(node); - - const subData = convertToData(subChildren); - - const data = { - key, - ...restProps, - }; - - if (subData.length) { - data.children = subData; - } - - return data; - }); -} diff --git a/src/index.ts b/src/index.ts index 402dc1b4..17375076 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,9 @@ import Menu, { MenuProps } from './Menu'; -import MenuItem from './sugar/MenuItem'; -import SubMenu from './sugar/SubMenu'; +import MenuItem from './MenuItem'; +import SubMenu from './SubMenu'; import MenuItemGroup from './sugar/MenuItemGroup'; import Divider from './sugar/Divider'; - export { SubMenu, MenuItem as Item, diff --git a/src/interface.ts b/src/interface.ts index c8f6ebba..6cbb0f9c 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,20 +1 @@ -import React from "_@types_react@16.14.5@@types/react"; - -export type MenuMode = - | 'horizontal' - | 'vertical' - | 'vertical-left' - | 'vertical-right' - | 'inline'; - -export interface MenuItemData { - children?: MenuItemData[]; - disabled?: boolean; - icon?: React.ReactNode; - key: string | number; - popupClassName?: string; - popupOffset?: [number, number]; - title?: React.ReactNode; - // TODO: fill this - onTitleClick?: () => void; -} \ No newline at end of file +export type MenuMode = 'horizontal' | 'vertical' | 'inline'; diff --git a/src/sugar/SubMenu.tsx b/src/sugar/SubMenu.tsx deleted file mode 100644 index 9f2b81ae..00000000 --- a/src/sugar/SubMenu.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export default function SubMenu(_: any) { - return null; -} From da68ec5c039440c934a8329d82115707a4dda19e Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 7 Apr 2021 10:42:28 +0800 Subject: [PATCH 03/93] add openKeys --- docs/examples/debug.tsx | 27 +++++++++++++++++++++++---- src/Menu.tsx | 33 +++++++++++++++++++++++++++------ src/SubMenu.tsx | 29 +++++++++++++++++++++++------ src/SubMenuList.tsx | 23 +++++++++++++++++++++++ 4 files changed, 96 insertions(+), 16 deletions(-) create mode 100644 src/SubMenuList.tsx diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx index db35f2d0..77fc99fa 100644 --- a/docs/examples/debug.tsx +++ b/docs/examples/debug.tsx @@ -2,16 +2,35 @@ import React from 'react'; import Menu from '../../src'; +import type { MenuProps } from '../../src'; import '../../assets/index.less'; import '../../assets/menu.less'; // const menuOptions = [{ key: 'bamboo' }, { key: 'light', label: 'Light' }]; export default () => { + const [mode, setMode] = React.useState('horizontal'); + return ( - - Navigation One - Next Item - + <> +
      + +
      + + Navigation One + Next Item + + Sub Item 1 + Sub Item 2 + + + ); }; diff --git a/src/Menu.tsx b/src/Menu.tsx index 054b3762..d2021c48 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -2,13 +2,16 @@ import * as React from 'react'; import { useMemo } from 'react'; import classNames from 'classnames'; import toArray from 'rc-util/lib/Children/toArray'; +import useMergedState from 'rc-util/lib/hooks/useMergedState'; import Overflow from 'rc-overflow'; import type { MenuMode } from './interface'; import MenuItem from './MenuItem'; -export const MenuContext = React.createContext({ - prefixCls: '', -}); +export const MenuContext = React.createContext<{ + prefixCls: string; + mode: MenuMode; + openKeys: string[]; +}>(null); export interface MenuProps extends Omit, 'onClick' | 'onSelect'> { @@ -20,11 +23,13 @@ export interface MenuProps /** direction of menu */ direction?: 'ltr' | 'rtl'; + // Open control + defaultOpenKeys?: string[]; + openKeys?: string[]; + // defaultSelectedKeys?: string[]; // defaultActiveFirst?: boolean; // selectedKeys?: string[]; - // defaultOpenKeys?: string[]; - // openKeys?: string[]; // getPopupContainer?: (node: HTMLElement) => HTMLElement; // onClick?: MenuClickEventHandler; @@ -70,8 +75,24 @@ const Menu: React.FC = ({ mode = 'vertical', children, direction, + + // Open + defaultOpenKeys, + openKeys, }) => { - const menuContext = useMemo(() => ({ prefixCls }), [prefixCls]); + // ========================= Open ========================= + const [mergedOpenKeys, setMergedOpenKeys] = useMergedState( + defaultOpenKeys || [], + { + value: openKeys, + }, + ); + + const menuContext = useMemo(() => ({ prefixCls, mode, openKeys }), [ + prefixCls, + mode, + openKeys, + ]); const childList: React.ReactElement[] = toArray(children); diff --git a/src/SubMenu.tsx b/src/SubMenu.tsx index 79dd420c..5540567a 100644 --- a/src/SubMenu.tsx +++ b/src/SubMenu.tsx @@ -1,15 +1,17 @@ import * as React from 'react'; +import classNames from 'classnames'; import Overflow from 'rc-overflow'; import { MenuContext } from './Menu'; +import SubMenuList from './SubMenuList'; export interface SubMenuProps { + title?: React.ReactNode; children?: React.ReactNode; // parentMenu?: React.ReactElement & { // isRootMenu: boolean; // subMenuInstance: React.ReactInstance; // }; - // title?: React.ReactNode; // selectedKeys?: string[]; // openKeys?: string[]; @@ -52,13 +54,28 @@ export interface SubMenuProps { // direction?: 'ltr' | 'rtl'; } -export default function SubMenu({ children }: SubMenuProps) { - const { prefixCls } = React.useContext(MenuContext); - const itemCls = `${prefixCls}-submenu`; +export default function SubMenu({ title, children }: SubMenuProps) { + const { prefixCls, mode } = React.useContext(MenuContext); + const subMenuPrefixCls = `${prefixCls}-submenu`; + + // =============================== Render =============================== + const subListNode = {children}; return ( - - {children} + +
      + {title} +
      + {mode === 'inline' && subListNode}
      ); } diff --git a/src/SubMenuList.tsx b/src/SubMenuList.tsx new file mode 100644 index 00000000..07ec92e8 --- /dev/null +++ b/src/SubMenuList.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import classNames from 'classnames'; +import { MenuContext } from './Menu'; + +export interface SubMenuListProps { + children?: React.ReactNode; +} + +export default function SubMenuList({ children }: SubMenuListProps) { + const { prefixCls } = React.useContext(MenuContext); + + // TODO: props + return ( +
        + {children} +
      + ); +} From 97fe682048934b701302923ed1a9bbb04433414f Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 8 Apr 2021 16:30:06 +0800 Subject: [PATCH 04/93] chore: Back of onClick --- docs/examples/debug.tsx | 20 +++++- src/Menu.tsx | 67 ++++++++++++++------ src/MenuItem.tsx | 77 ++++++++++++++++++----- src/SubMenu.tsx | 114 ++++++++++++++++++++++++++++------- src/SubMenuList.tsx | 24 +++++--- src/context.tsx | 58 ++++++++++++++++++ src/hooks/useMemoCallback.ts | 17 ++++++ src/interface.ts | 17 ++++++ src/sugar/MenuItem.tsx | 6 -- src/utils/nodeUtil.ts | 14 +++++ 10 files changed, 343 insertions(+), 71 deletions(-) create mode 100644 src/context.tsx create mode 100644 src/hooks/useMemoCallback.ts delete mode 100644 src/sugar/MenuItem.tsx create mode 100644 src/utils/nodeUtil.ts diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx index 77fc99fa..8f05f5f1 100644 --- a/docs/examples/debug.tsx +++ b/docs/examples/debug.tsx @@ -5,12 +5,25 @@ import Menu from '../../src'; import type { MenuProps } from '../../src'; import '../../assets/index.less'; import '../../assets/menu.less'; +import type { MenuInfo } from '@/interface'; // const menuOptions = [{ key: 'bamboo' }, { key: 'light', label: 'Light' }]; export default () => { const [mode, setMode] = React.useState('horizontal'); + const onRootClick = (info: MenuInfo) => { + console.log('Root Menu Item Click:', info); + }; + + const onSubMenuClick = (info: MenuInfo) => { + console.log('Sub Menu Item Click:', info); + }; + + const onClick = (info: MenuInfo) => { + console.log('Menu Item Click:', info); + }; + return ( <>
      @@ -23,11 +36,14 @@ export default () => { Navigation One Next Item - - Sub Item 1 + + + Sub Item 1 + Sub Item 2 diff --git a/src/Menu.tsx b/src/Menu.tsx index d2021c48..6657051f 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -1,17 +1,16 @@ import * as React from 'react'; -import { useMemo } from 'react'; +import type { CSSMotionProps } from 'rc-motion'; import classNames from 'classnames'; -import toArray from 'rc-util/lib/Children/toArray'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; import Overflow from 'rc-overflow'; -import type { MenuMode } from './interface'; +import type { MenuClickEventHandler, MenuInfo, MenuMode } from './interface'; import MenuItem from './MenuItem'; +import { parseChildren } from './utils/nodeUtil'; +import MenuContextProvider from './context'; +import useMemoCallback from './hooks/useMemoCallback'; -export const MenuContext = React.createContext<{ - prefixCls: string; - mode: MenuMode; - openKeys: string[]; -}>(null); +// optimize for render +const EMPTY_LIST: string[] = []; export interface MenuProps extends Omit, 'onClick' | 'onSelect'> { @@ -27,14 +26,20 @@ export interface MenuProps defaultOpenKeys?: string[]; openKeys?: string[]; + /** Menu motion define */ + motion?: CSSMotionProps; + + // >>>>> Events + onClick?: MenuClickEventHandler; + onOpenChange?: (openKeys: React.Key[]) => void; + // defaultSelectedKeys?: string[]; // defaultActiveFirst?: boolean; // selectedKeys?: string[]; // getPopupContainer?: (node: HTMLElement) => HTMLElement; - // onClick?: MenuClickEventHandler; + // onSelect?: SelectEventHandler; - // onOpenChange?: (openKeys: React.Key[]) => void; // onDeselect?: SelectEventHandler; // onDestroy?: DestroyEventHandler; // subMenuOpenDelay?: number; @@ -49,8 +54,6 @@ export interface MenuProps // itemIcon?: RenderIconType; // expandIcon?: RenderIconType; // overflowedIndicator?: React.ReactNode; - // /** Menu motion define */ - // motion?: CSSMotionProps; // /** Default menu motion of each mode */ // defaultMotions?: Partial<{ [key in MenuMode | 'other']: CSSMotionProps }>; @@ -79,6 +82,13 @@ const Menu: React.FC = ({ // Open defaultOpenKeys, openKeys, + + // Motion + motion, + + // Events + onClick, + onOpenChange, }) => { // ========================= Open ========================= const [mergedOpenKeys, setMergedOpenKeys] = useMergedState( @@ -88,13 +98,22 @@ const Menu: React.FC = ({ }, ); - const menuContext = useMemo(() => ({ prefixCls, mode, openKeys }), [ - prefixCls, - mode, - openKeys, - ]); + // ======================== Events ======================== + const onInternalClick = useMemoCallback((info: MenuInfo) => { + onClick?.(info); + }); + + const onInternalSubMenuClick = useMemoCallback((key: string) => { + const newOpenKeys = mergedOpenKeys.includes(key) + ? mergedOpenKeys.filter(k => k !== key) + : [...mergedOpenKeys, key]; + + setMergedOpenKeys(newOpenKeys); + onOpenChange?.(newOpenKeys); + }); - const childList: React.ReactElement[] = toArray(children); + // ======================== Render ======================== + const childList: React.ReactElement[] = parseChildren(children); const container = ( = ({ // ); return ( - {container} + + {container} + ); }; diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index a88f1323..e1012c88 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -1,6 +1,9 @@ import * as React from 'react'; +import classNames from 'classnames'; import Overflow from 'rc-overflow'; -import { MenuContext } from './Menu'; +import type { MenuClickEventHandler, MenuInfo } from './interface'; +import type { MenuContextProps } from './context'; +import { MenuContext } from './context'; export interface MenuItemProps extends Omit< @@ -9,19 +12,22 @@ export interface MenuItemProps > { children?: React.ReactNode; + /** @private Internal filled key. Do not set it directly */ + eventKey?: string; + + // >>>>> Events + onClick?: MenuClickEventHandler; + /** @deprecated No place to use this. Should remove */ // attribute?: Record; // rootPrefixCls?: string; - // eventKey?: React.Key; - // className?: string; - // style?: React.CSSProperties; // active?: boolean; // selectedKeys?: string[]; // disabled?: boolean; // title?: string; // onItemHover?: HoverEventHandler; // onSelect?: SelectEventHandler; - // onClick?: MenuClickEventHandler; + // onDeselect?: SelectEventHandler; // parentMenu?: React.ReactInstance; // onDestroy?: DestroyEventHandler; @@ -32,19 +38,62 @@ export interface MenuItemProps // manualRef?: LegacyFunctionRef; // itemIcon?: RenderIconType; // role?: string; - // mode?: MenuMode; // inlineIndent?: number; // level?: number; // direction?: 'ltr' | 'rtl'; + + // No need anymore + // mode?: MenuMode; } -export default function MenuItem({ children }: MenuItemProps) { - const { prefixCls } = React.useContext(MenuContext); - const itemCls = `${prefixCls}-item`; +// Since Menu event provide the `info.item` which point to the MenuItem node instance. +// We have to use class component here. +// This should be removed from doc & api in future. +export default class MenuItem extends React.Component { + context: MenuContextProps; + static contextType = MenuContext; + + getEventInfo = (e: React.MouseEvent): MenuInfo => { + const { parentKeys } = this.context; + // key: React.Key; + // keyPath: React.Key[]; + // /** @deprecated This will not support in future. You should avoid to use this */ + // item: React.ReactInstance; + // domEvent: React.MouseEvent; + const { eventKey } = this.props; + return { + key: eventKey, + keyPath: [...parentKeys, eventKey], + item: this, + domEvent: e, + }; + }; + + onClick: React.MouseEventHandler = e => { + const { onClick } = this.props; + const { onItemClick } = this.context; + + const info = this.getEventInfo(e); + + onClick?.(info); + onItemClick(info); + }; + + render() { + const { children, className, ...restProps } = this.props; + const { prefixCls } = this.context; + const itemCls = `${prefixCls}-item`; - return ( - - {children} - - ); + return ( + + {children} + + ); + } } diff --git a/src/SubMenu.tsx b/src/SubMenu.tsx index 5540567a..574b39ed 100644 --- a/src/SubMenu.tsx +++ b/src/SubMenu.tsx @@ -1,13 +1,28 @@ import * as React from 'react'; import classNames from 'classnames'; +import CSSMotion from 'rc-motion'; import Overflow from 'rc-overflow'; -import { MenuContext } from './Menu'; import SubMenuList from './SubMenuList'; +import { parseChildren } from './utils/nodeUtil'; +import type { + MenuClickEventHandler, + MenuInfo, + MenuTitleInfo, +} from './interface'; +import MenuContextProvider, { MenuContext } from './context'; +import useMemoCallback from './hooks/useMemoCallback'; export interface SubMenuProps { title?: React.ReactNode; children?: React.ReactNode; + /** @private Internal filled key. Do not set it directly */ + eventKey?: string; + + // >>>>> Events + onClick?: MenuClickEventHandler; + onTitleClick?: (info: MenuTitleInfo) => void; + // parentMenu?: React.ReactElement & { // isRootMenu: boolean; // subMenuInstance: React.ReactInstance; @@ -15,10 +30,9 @@ export interface SubMenuProps { // selectedKeys?: string[]; // openKeys?: string[]; - // onClick?: MenuClickEventHandler; + // onOpenChange?: OpenEventHandler; // rootPrefixCls?: string; - // eventKey?: string; // multiple?: boolean; // active?: boolean; // TODO: remove // onItemHover?: HoverEventHandler; @@ -30,10 +44,6 @@ export interface SubMenuProps { // onMouseLeave?: MenuHoverEventHandler; // onTitleMouseEnter?: MenuHoverEventHandler; // onTitleMouseLeave?: MenuHoverEventHandler; - // onTitleClick?: (info: { - // key: React.Key; - // domEvent: React.MouseEvent | React.KeyboardEvent; - // }) => void; // popupOffset?: number[]; // isOpen?: boolean; // store?: MiniStore; @@ -54,28 +64,86 @@ export interface SubMenuProps { // direction?: 'ltr' | 'rtl'; } -export default function SubMenu({ title, children }: SubMenuProps) { - const { prefixCls, mode } = React.useContext(MenuContext); +export default function SubMenu({ + title, + eventKey, + children, + + // Events + onClick, + onTitleClick, +}: SubMenuProps) { + const { + prefixCls, + mode, + openKeys, + motion, + parentKeys, + + // Events + onItemClick, + onSubMenuClick, + } = React.useContext(MenuContext); const subMenuPrefixCls = `${prefixCls}-submenu`; + // ================================ Key ================================= + const connectedKeys = React.useMemo(() => [...parentKeys, eventKey], [ + parentKeys, + eventKey, + ]); + + // ============================== Visible =============================== + const visible = openKeys.includes(eventKey); + + // =============================== Events =============================== + const onInternalTitleClick: React.MouseEventHandler = e => { + onTitleClick?.({ + key: eventKey, + domEvent: e, + }); + + onSubMenuClick(eventKey); + }; + + const onMergedItemClick = useMemoCallback((info: MenuInfo) => { + onClick?.(info); + onItemClick(info); + }); + // =============================== Render =============================== - const subListNode = {children}; + const childList: React.ReactElement[] = parseChildren(children); return ( - -
      - {title} -
      - {mode === 'inline' && subListNode} -
      +
      + {title} +
      + {mode === 'inline' && ( + + {({ className, style }) => { + return ( + + {childList} + + ); + }} + + )} + + ); } diff --git a/src/SubMenuList.tsx b/src/SubMenuList.tsx index 07ec92e8..deba8045 100644 --- a/src/SubMenuList.tsx +++ b/src/SubMenuList.tsx @@ -1,21 +1,31 @@ import * as React from 'react'; import classNames from 'classnames'; -import { MenuContext } from './Menu'; +import { MenuContext } from './context'; -export interface SubMenuListProps { +export interface SubMenuListProps + extends React.HTMLAttributes { children?: React.ReactNode; } -export default function SubMenuList({ children }: SubMenuListProps) { +export default function SubMenuList({ + className, + children, + ...restProps +}: SubMenuListProps) { const { prefixCls } = React.useContext(MenuContext); // TODO: props return (
        {children}
      diff --git a/src/context.tsx b/src/context.tsx new file mode 100644 index 00000000..bd98d835 --- /dev/null +++ b/src/context.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; +import type { CSSMotionProps } from 'rc-motion'; +import useMemo from 'rc-util/lib/hooks/useMemo'; +import shallowEqual from 'shallowequal'; +import type { MenuClickEventHandler, MenuMode } from './interface'; + +export interface MenuContextProps { + prefixCls: string; + mode: MenuMode; + openKeys: string[]; + motion?: CSSMotionProps; + parentKeys: string[]; + + // Events + onItemClick: MenuClickEventHandler; + onSubMenuClick: (key: string) => void; +} + +export const MenuContext = React.createContext(null); + +function mergeProps( + origin: MenuContextProps, + target: Partial, +): MenuContextProps { + const clone = { ...origin }; + + Object.keys(target).forEach(key => { + const value = target[key]; + if (value !== undefined) { + clone[key] = value; + } + }); + + return clone; +} + +export interface InheritableContextProps extends Partial { + children?: React.ReactNode; +} + +export default function InheritableContextProvider({ + children, + ...restProps +}: InheritableContextProps) { + const context = React.useContext(MenuContext); + + const inheritableContext = useMemo( + () => mergeProps(context, restProps), + [context, restProps], + (prev, next) => prev[0] !== next[0] || !shallowEqual(prev[1], next[1]), + ); + + return ( + + {children} + + ); +} diff --git a/src/hooks/useMemoCallback.ts b/src/hooks/useMemoCallback.ts new file mode 100644 index 00000000..c82be24b --- /dev/null +++ b/src/hooks/useMemoCallback.ts @@ -0,0 +1,17 @@ +import * as React from 'react'; + +/** + * Cache callback function that always return same ref instead. + * This is used for context optimization. + */ +export default function useMemoCallback void>( + func: T, +): T { + const funRef = React.useRef(func); + funRef.current = func; + + return React.useCallback( + ((...args: any[]) => funRef.current?.(...args)) as any, + [], + ); +} diff --git a/src/interface.ts b/src/interface.ts index 6cbb0f9c..cd5d8cde 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1 +1,18 @@ +import type * as React from 'react'; + export type MenuMode = 'horizontal' | 'vertical' | 'inline'; + +export interface MenuInfo { + key: string; + keyPath: string[]; + /** @deprecated This will not support in future. You should avoid to use this */ + item: React.ReactInstance; + domEvent: React.MouseEvent; +} + +export interface MenuTitleInfo { + key: string; + domEvent: React.MouseEvent | React.KeyboardEvent; +} + +export type MenuClickEventHandler = (info: MenuInfo) => void; diff --git a/src/sugar/MenuItem.tsx b/src/sugar/MenuItem.tsx deleted file mode 100644 index d955d688..00000000 --- a/src/sugar/MenuItem.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import type { MenuItemProps } from '../MenuItem'; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export default function MenuItem(_: MenuItemProps) { - return null; -} diff --git a/src/utils/nodeUtil.ts b/src/utils/nodeUtil.ts new file mode 100644 index 00000000..f9875214 --- /dev/null +++ b/src/utils/nodeUtil.ts @@ -0,0 +1,14 @@ +import * as React from 'react'; +import toArray from 'rc-util/lib/Children/toArray'; + +export function parseChildren(children: React.ReactNode) { + return toArray(children).map(child => { + if (React.isValidElement(child) && child.key !== undefined) { + return React.cloneElement(child, { + eventKey: child.key, + } as any); + } + + return child; + }); +} From 2331624a4bbeee4cf7158483173b7cd8cbb83526 Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 8 Apr 2021 22:16:55 +0800 Subject: [PATCH 05/93] feat: Motion back --- assets/menu.less | 8 +++- docs/examples/debug.tsx | 35 +++++++++++++++- src/Menu.tsx | 58 +++++++++++++++++++++------ src/MenuItem.tsx | 2 +- src/PopupTrigger.tsx | 78 ++++++++++++++++++++++++++++++++++++ src/SubMenu.tsx | 56 ++++++++++++++++++++------ src/SubMenuList.tsx | 8 ++-- src/context.tsx | 20 +++++++-- src/hooks/useMemoCallback.ts | 4 +- src/interface.ts | 4 ++ src/placements.ts | 52 ++++++++++++++++++++++++ 11 files changed, 288 insertions(+), 37 deletions(-) create mode 100644 src/PopupTrigger.tsx create mode 100644 src/placements.ts diff --git a/assets/menu.less b/assets/menu.less index 63e9d26f..f84c2196 100644 --- a/assets/menu.less +++ b/assets/menu.less @@ -5,4 +5,10 @@ display: flex; flex-wrap: nowrap; } -} \ No newline at end of file + + &-submenu { + &-hidden { + display: none; + } + } +} diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx index 8f05f5f1..35252b9a 100644 --- a/docs/examples/debug.tsx +++ b/docs/examples/debug.tsx @@ -1,13 +1,45 @@ /* eslint no-console:0 */ import React from 'react'; +import type { CSSMotionProps } from 'rc-motion'; import Menu from '../../src'; import type { MenuProps } from '../../src'; import '../../assets/index.less'; import '../../assets/menu.less'; import type { MenuInfo } from '@/interface'; -// const menuOptions = [{ key: 'bamboo' }, { key: 'light', label: 'Light' }]; +const collapseNode = () => ({ height: 0 }); +const expandNode = node => ({ height: node.scrollHeight }); + +const horizontalMotion: CSSMotionProps = { + motionName: 'rc-menu-open-slide-up', + motionAppear: true, + motionEnter: true, + motionLeave: true, +}; + +const verticalMotion: CSSMotionProps = { + motionName: 'rc-menu-open-zoom', + motionAppear: true, + motionEnter: true, + motionLeave: true, +}; + +const inlineMotion: CSSMotionProps = { + motionName: 'rc-menu-collapse', + onAppearStart: collapseNode, + onAppearActive: expandNode, + onEnterStart: collapseNode, + onEnterActive: expandNode, + onLeaveStart: expandNode, + onLeaveActive: collapseNode, +}; + +const motionMap: Record = { + horizontal: horizontalMotion, + inline: inlineMotion, + vertical: verticalMotion, +}; export default () => { const [mode, setMode] = React.useState('horizontal'); @@ -37,6 +69,7 @@ export default () => { mode={mode} style={{ width: mode === 'horizontal' ? undefined : 256 }} onClick={onRootClick} + motion={motionMap[mode]} > Navigation One Next Item diff --git a/src/Menu.tsx b/src/Menu.tsx index 6657051f..7b4082f3 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -3,7 +3,13 @@ import type { CSSMotionProps } from 'rc-motion'; import classNames from 'classnames'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; import Overflow from 'rc-overflow'; -import type { MenuClickEventHandler, MenuInfo, MenuMode } from './interface'; +import type { + BuiltinPlacements, + MenuClickEventHandler, + MenuInfo, + MenuMode, + TriggerSubMenuAction, +} from './interface'; import MenuItem from './MenuItem'; import { parseChildren } from './utils/nodeUtil'; import MenuContextProvider from './context'; @@ -29,6 +35,16 @@ export interface MenuProps /** Menu motion define */ motion?: CSSMotionProps; + // Popup + subMenuOpenDelay?: number; + subMenuCloseDelay?: number; + forceSubMenuRender?: boolean; + triggerSubMenuAction?: TriggerSubMenuAction; + builtinPlacements?: BuiltinPlacements; + + // >>>>> Function + getPopupContainer?: (node: HTMLElement) => HTMLElement; + // >>>>> Events onClick?: MenuClickEventHandler; onOpenChange?: (openKeys: React.Key[]) => void; @@ -37,20 +53,15 @@ export interface MenuProps // defaultActiveFirst?: boolean; // selectedKeys?: string[]; - // getPopupContainer?: (node: HTMLElement) => HTMLElement; - // onSelect?: SelectEventHandler; // onDeselect?: SelectEventHandler; // onDestroy?: DestroyEventHandler; - // subMenuOpenDelay?: number; - // subMenuCloseDelay?: number; - // forceSubMenuRender?: boolean; - // triggerSubMenuAction?: TriggerSubMenuAction; + // level?: number; // selectable?: boolean; // multiple?: boolean; // activeKey?: string; - // builtinPlacements?: BuiltinPlacements; + // itemIcon?: RenderIconType; // expandIcon?: RenderIconType; // overflowedIndicator?: React.ReactNode; @@ -80,12 +91,22 @@ const Menu: React.FC = ({ direction, // Open + subMenuOpenDelay = 0.1, + subMenuCloseDelay = 0.1, + forceSubMenuRender, defaultOpenKeys, openKeys, // Motion motion, + // Popup + triggerSubMenuAction = 'hover', + builtinPlacements, + + // Function + getPopupContainer, + // Events onClick, onOpenChange, @@ -103,15 +124,19 @@ const Menu: React.FC = ({ onClick?.(info); }); - const onInternalSubMenuClick = useMemoCallback((key: string) => { - const newOpenKeys = mergedOpenKeys.includes(key) - ? mergedOpenKeys.filter(k => k !== key) - : [...mergedOpenKeys, key]; + const onInternalOpenChange = useMemoCallback((key: string, open: boolean) => { + const newOpenKeys = mergedOpenKeys.filter(k => k !== key); + + if (open) { + newOpenKeys.push(key); + } setMergedOpenKeys(newOpenKeys); onOpenChange?.(newOpenKeys); }); + const getInternalPopupContainer = useMemoCallback(getPopupContainer); + // ======================== Render ======================== const childList: React.ReactElement[] = parseChildren(children); @@ -152,8 +177,15 @@ const Menu: React.FC = ({ openKeys={mergedOpenKeys} motion={motion} parentKeys={EMPTY_LIST} + rtl={direction === 'rtl'} + subMenuOpenDelay={subMenuOpenDelay} + subMenuCloseDelay={subMenuCloseDelay} + forceSubMenuRender={forceSubMenuRender} + builtinPlacements={builtinPlacements} + triggerSubMenuAction={triggerSubMenuAction} onItemClick={onInternalClick} - onSubMenuClick={onInternalSubMenuClick} + onOpenChange={onInternalOpenChange} + getPopupContainer={getInternalPopupContainer} > {container} diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index e1012c88..4a953cb5 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -80,7 +80,7 @@ export default class MenuItem extends React.Component { }; render() { - const { children, className, ...restProps } = this.props; + const { children, className, eventKey, ...restProps } = this.props; const { prefixCls } = this.context; const itemCls = `${prefixCls}-item`; diff --git a/src/PopupTrigger.tsx b/src/PopupTrigger.tsx new file mode 100644 index 00000000..22bc3e5c --- /dev/null +++ b/src/PopupTrigger.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import Trigger from 'rc-trigger'; +import classNames from 'classnames'; +import type { CSSMotionProps } from 'rc-motion'; +import { MenuContext } from './context'; +import { placements, placementsRtl } from './placements'; + +const popupPlacementMap = { + horizontal: 'bottomLeft', + vertical: 'rightTop', + 'vertical-left': 'rightTop', + 'vertical-right': 'leftTop', +}; + +export interface PopupTriggerProps { + prefixCls: string; + visible: boolean; + children: React.ReactElement; + popup: React.ReactNode; + disabled: boolean; + onVisibleChange: (visible: boolean) => void; +} + +export default function PopupTrigger({ + prefixCls, + visible, + children, + popup, + disabled, + onVisibleChange, +}: PopupTriggerProps) { + const { + getPopupContainer, + rtl, + subMenuOpenDelay, + subMenuCloseDelay, + builtinPlacements, + triggerSubMenuAction, + forceSubMenuRender, + mode, + motion, + } = React.useContext(MenuContext); + + const placement = rtl + ? { ...placementsRtl, ...builtinPlacements } + : { ...placements, ...builtinPlacements }; + + const popupPlacement = popupPlacementMap[mode]; + + const mergedMotion: CSSMotionProps = { + ...motion, + leavedClassName: `${prefixCls}-hidden`, + removeOnLeave: false, + motionAppear: true, + }; + + return ( + + {children} + + ); +} diff --git a/src/SubMenu.tsx b/src/SubMenu.tsx index 574b39ed..af7bc48f 100644 --- a/src/SubMenu.tsx +++ b/src/SubMenu.tsx @@ -11,11 +11,14 @@ import type { } from './interface'; import MenuContextProvider, { MenuContext } from './context'; import useMemoCallback from './hooks/useMemoCallback'; +import PopupTrigger from './PopupTrigger'; export interface SubMenuProps { title?: React.ReactNode; children?: React.ReactNode; + disabled?: boolean; + /** @private Internal filled key. Do not set it directly */ eventKey?: string; @@ -57,7 +60,6 @@ export interface SubMenuProps { // subMenuCloseDelay?: number; // forceSubMenuRender?: boolean; // builtinPlacements?: BuiltinPlacements; - // disabled?: boolean; // className?: string; // popupClassName?: string; // motion?: CSSMotionProps; @@ -67,6 +69,9 @@ export interface SubMenuProps { export default function SubMenu({ title, eventKey, + + disabled, + children, // Events @@ -82,7 +87,7 @@ export default function SubMenu({ // Events onItemClick, - onSubMenuClick, + onOpenChange, } = React.useContext(MenuContext); const subMenuPrefixCls = `${prefixCls}-submenu`; @@ -102,7 +107,10 @@ export default function SubMenu({ domEvent: e, }); - onSubMenuClick(eventKey); + // Trigger open by click when mode is `inline` + if (mode === 'inline') { + onOpenChange(eventKey, !openKeys.includes(eventKey)); + } }; const onMergedItemClick = useMemoCallback((info: MenuInfo) => { @@ -110,9 +118,39 @@ export default function SubMenu({ onItemClick(info); }); + const onPopupVisibleChange = (newVisible: boolean) => { + onOpenChange(eventKey, newVisible); + }; + // =============================== Render =============================== const childList: React.ReactElement[] = parseChildren(children); + let titleNode: React.ReactElement = ( +
      + {title} +
      + ); + + if (mode !== 'inline') { + titleNode = ( + {childList}} + disabled={disabled} + onVisibleChange={onPopupVisibleChange} + > + {titleNode} + + ); + } + return ( -
      - {title} -
      + {titleNode} + + {/* Inline mode */} {mode === 'inline' && ( {({ className, style }) => { diff --git a/src/SubMenuList.tsx b/src/SubMenuList.tsx index deba8045..dd713bac 100644 --- a/src/SubMenuList.tsx +++ b/src/SubMenuList.tsx @@ -12,17 +12,15 @@ export default function SubMenuList({ children, ...restProps }: SubMenuListProps) { - const { prefixCls } = React.useContext(MenuContext); + const { prefixCls, mode } = React.useContext(MenuContext); // TODO: props return (
        void; + onOpenChange: (key: string, open: boolean) => void; + getPopupContainer: (node: HTMLElement) => HTMLElement; } export const MenuContext = React.createContext(null); diff --git a/src/hooks/useMemoCallback.ts b/src/hooks/useMemoCallback.ts index c82be24b..3555dea5 100644 --- a/src/hooks/useMemoCallback.ts +++ b/src/hooks/useMemoCallback.ts @@ -10,8 +10,10 @@ export default function useMemoCallback void>( const funRef = React.useRef(func); funRef.current = func; - return React.useCallback( + const callback = React.useCallback( ((...args: any[]) => funRef.current?.(...args)) as any, [], ); + + return func ? callback : undefined; } diff --git a/src/interface.ts b/src/interface.ts index cd5d8cde..7aa84ba4 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -16,3 +16,7 @@ export interface MenuTitleInfo { } export type MenuClickEventHandler = (info: MenuInfo) => void; + +export type BuiltinPlacements = Record; + +export type TriggerSubMenuAction = 'click' | 'hover'; diff --git a/src/placements.ts b/src/placements.ts new file mode 100644 index 00000000..7b45acf9 --- /dev/null +++ b/src/placements.ts @@ -0,0 +1,52 @@ +const autoAdjustOverflow = { + adjustX: 1, + adjustY: 1, +}; + +export const placements = { + topLeft: { + points: ['bl', 'tl'], + overflow: autoAdjustOverflow, + offset: [0, -7], + }, + bottomLeft: { + points: ['tl', 'bl'], + overflow: autoAdjustOverflow, + offset: [0, 7], + }, + leftTop: { + points: ['tr', 'tl'], + overflow: autoAdjustOverflow, + offset: [-4, 0], + }, + rightTop: { + points: ['tl', 'tr'], + overflow: autoAdjustOverflow, + offset: [4, 0], + }, +}; + +export const placementsRtl = { + topLeft: { + points: ['bl', 'tl'], + overflow: autoAdjustOverflow, + offset: [0, -7], + }, + bottomLeft: { + points: ['tl', 'bl'], + overflow: autoAdjustOverflow, + offset: [0, 7], + }, + rightTop: { + points: ['tr', 'tl'], + overflow: autoAdjustOverflow, + offset: [-4, 0], + }, + leftTop: { + points: ['tl', 'tr'], + overflow: autoAdjustOverflow, + offset: [4, 0], + }, +}; + +export default placements; From 34ff4a98b0569932c987b5ab8e06895d2638a4ea Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 15 Apr 2021 13:10:45 +0800 Subject: [PATCH 06/93] active key support --- docs/examples/debug.tsx | 3 ++ src/Icon.tsx | 25 ++++++++++ src/Menu.tsx | 34 +++++++++++-- src/MenuItem.tsx | 3 +- src/SubMenu.tsx | 105 ++++++++++++++++++++++++++++++---------- src/context.tsx | 5 ++ src/interface.ts | 4 ++ 7 files changed, 150 insertions(+), 29 deletions(-) create mode 100644 src/Icon.tsx diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx index 35252b9a..bf4fbbb0 100644 --- a/docs/examples/debug.tsx +++ b/docs/examples/debug.tsx @@ -79,6 +79,9 @@ export default () => { Sub Item 2 + + Disabled Item +
      ); diff --git a/src/Icon.tsx b/src/Icon.tsx new file mode 100644 index 00000000..889825f2 --- /dev/null +++ b/src/Icon.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; +import type { RenderIconType } from './interface'; +import type { SubMenuProps } from './SubMenu'; + +export interface IconProps { + icon?: RenderIconType; + props: SubMenuProps & { isSubMenu: boolean }; + /** Fallback of icon if provided */ + children?: React.ReactElement; +} + +export default function Icon({ icon, props, children }: IconProps) { + let iconNode: React.ReactElement; + + if (typeof icon === 'function') { + iconNode = React.createElement(this.props.expandIcon as any, { + ...props, + }); + } else { + // Compatible for origin definition + iconNode = icon as React.ReactElement; + } + + return iconNode || children; +} diff --git a/src/Menu.tsx b/src/Menu.tsx index 7b4082f3..0e542a2c 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -32,6 +32,10 @@ export interface MenuProps defaultOpenKeys?: string[]; openKeys?: string[]; + // Active control + activeKey?: string; + defaultActiveFirst?: boolean; + /** Menu motion define */ motion?: CSSMotionProps; @@ -50,7 +54,6 @@ export interface MenuProps onOpenChange?: (openKeys: React.Key[]) => void; // defaultSelectedKeys?: string[]; - // defaultActiveFirst?: boolean; // selectedKeys?: string[]; // onSelect?: SelectEventHandler; @@ -60,7 +63,6 @@ export interface MenuProps // level?: number; // selectable?: boolean; // multiple?: boolean; - // activeKey?: string; // itemIcon?: RenderIconType; // expandIcon?: RenderIconType; @@ -97,6 +99,10 @@ const Menu: React.FC = ({ defaultOpenKeys, openKeys, + // Active + activeKey, + defaultActiveFirst, + // Motion motion, @@ -111,6 +117,8 @@ const Menu: React.FC = ({ onClick, onOpenChange, }) => { + const childList: React.ReactElement[] = parseChildren(children); + // ========================= Open ========================= const [mergedOpenKeys, setMergedOpenKeys] = useMergedState( defaultOpenKeys || [], @@ -119,6 +127,22 @@ const Menu: React.FC = ({ }, ); + // ======================== Active ======================== + const [mergedActiveKey, setMergedActiveKey] = useMergedState( + activeKey || ((defaultActiveFirst && childList[0]?.key) as string), + { + value: activeKey, + }, + ); + + const onActive = useMemoCallback((key: string) => { + setMergedActiveKey(key); + }); + + const onInactive = useMemoCallback(() => { + setMergedActiveKey(undefined); + }); + // ======================== Events ======================== const onInternalClick = useMemoCallback((info: MenuInfo) => { onClick?.(info); @@ -138,7 +162,6 @@ const Menu: React.FC = ({ const getInternalPopupContainer = useMemoCallback(getPopupContainer); // ======================== Render ======================== - const childList: React.ReactElement[] = parseChildren(children); const container = ( = ({ motion={motion} parentKeys={EMPTY_LIST} rtl={direction === 'rtl'} + // Active + activeKey={mergedActiveKey} + onActive={onActive} + onInactive={onInactive} + // Popup subMenuOpenDelay={subMenuOpenDelay} subMenuCloseDelay={subMenuCloseDelay} forceSubMenuRender={forceSubMenuRender} diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index 4a953cb5..00d8ab0c 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -15,6 +15,8 @@ export interface MenuItemProps /** @private Internal filled key. Do not set it directly */ eventKey?: string; + disabled?: boolean; + // >>>>> Events onClick?: MenuClickEventHandler; @@ -23,7 +25,6 @@ export interface MenuItemProps // rootPrefixCls?: string; // active?: boolean; // selectedKeys?: string[]; - // disabled?: boolean; // title?: string; // onItemHover?: HoverEventHandler; // onSelect?: SelectEventHandler; diff --git a/src/SubMenu.tsx b/src/SubMenu.tsx index af7bc48f..d2c22443 100644 --- a/src/SubMenu.tsx +++ b/src/SubMenu.tsx @@ -8,10 +8,12 @@ import type { MenuClickEventHandler, MenuInfo, MenuTitleInfo, + RenderIconType, } from './interface'; import MenuContextProvider, { MenuContext } from './context'; import useMemoCallback from './hooks/useMemoCallback'; import PopupTrigger from './PopupTrigger'; +import Icon from './Icon'; export interface SubMenuProps { title?: React.ReactNode; @@ -22,18 +24,14 @@ export interface SubMenuProps { /** @private Internal filled key. Do not set it directly */ eventKey?: string; + // >>>>> Icon + // itemIcon?: RenderIconType; + expandIcon?: RenderIconType; + // >>>>> Events onClick?: MenuClickEventHandler; onTitleClick?: (info: MenuTitleInfo) => void; - // parentMenu?: React.ReactElement & { - // isRootMenu: boolean; - // subMenuInstance: React.ReactInstance; - // }; - - // selectedKeys?: string[]; - // openKeys?: string[]; - // onOpenChange?: OpenEventHandler; // rootPrefixCls?: string; // multiple?: boolean; @@ -52,8 +50,6 @@ export interface SubMenuProps { // store?: MiniStore; // mode?: MenuMode; // manualRef?: LegacyFunctionRef; - // itemIcon?: RenderIconType; - // expandIcon?: RenderIconType; // inlineIndent?: number; // level?: number; // subMenuOpenDelay?: number; @@ -64,20 +60,35 @@ export interface SubMenuProps { // popupClassName?: string; // motion?: CSSMotionProps; // direction?: 'ltr' | 'rtl'; + + // >>>>>>>>>>>>>>>>>>> Useless content <<<<<<<<<<<<<<<<<<<<< + + // parentMenu?: React.ReactElement & { + // isRootMenu: boolean; + // subMenuInstance: React.ReactInstance; + // }; + + // selectedKeys?: string[]; + // openKeys?: string[]; } -export default function SubMenu({ - title, - eventKey, +export default function SubMenu(props: SubMenuProps) { + const { + title, + eventKey, + + disabled, - disabled, + children, - children, + // Icons + expandIcon, + + // Events + onClick, + onTitleClick, + } = props; - // Events - onClick, - onTitleClick, -}: SubMenuProps) { const { prefixCls, mode, @@ -85,11 +96,18 @@ export default function SubMenu({ motion, parentKeys, + // Active + activeKey, + onActive, + onInactive, + // Events onItemClick, onOpenChange, } = React.useContext(MenuContext); + const subMenuPrefixCls = `${prefixCls}-submenu`; + const childList: React.ReactElement[] = parseChildren(children); // ================================ Key ================================= const connectedKeys = React.useMemo(() => [...parentKeys, eventKey], [ @@ -97,10 +115,11 @@ export default function SubMenu({ eventKey, ]); - // ============================== Visible =============================== - const visible = openKeys.includes(eventKey); + // =========================== Open & Select ============================ + const open = openKeys.includes(eventKey); // =============================== Events =============================== + // >>>> Title click const onInternalTitleClick: React.MouseEventHandler = e => { onTitleClick?.({ key: eventKey, @@ -113,17 +132,34 @@ export default function SubMenu({ } }; + // >>>> Context for children click const onMergedItemClick = useMemoCallback((info: MenuInfo) => { onClick?.(info); onItemClick(info); }); + // >>>>> Visible change const onPopupVisibleChange = (newVisible: boolean) => { onOpenChange(eventKey, newVisible); }; + // >>>>> Title hover + const onTitleMouseEnter: React.MouseEventHandler = () => { + onActive(eventKey); + }; + + const onTitleMouseLeave: React.MouseEventHandler = () => { + onInactive(eventKey); + }; + // =============================== Render =============================== - const childList: React.ReactElement[] = parseChildren(children); + + // >>>>> Title + const titleProps: React.HtmlHTMLAttributes = {}; + if (!disabled) { + titleProps.onMouseEnter = onTitleMouseEnter; + titleProps.onMouseLeave = onTitleMouseLeave; + } let titleNode: React.ReactElement = (
      {title} + + {/* Only non-horizontal mode shows the icon */} + {mode !== 'horizontal' && ( + + + + )}
      ); @@ -141,7 +192,7 @@ export default function SubMenu({ titleNode = ( {childList}} disabled={disabled} onVisibleChange={onPopupVisibleChange} @@ -151,6 +202,7 @@ export default function SubMenu({ ); } + // >>>>> Render return ( {titleNode} {/* Inline mode */} {mode === 'inline' && ( - + {({ className, style }) => { return ( diff --git a/src/context.tsx b/src/context.tsx index 74e5d365..043a206e 100644 --- a/src/context.tsx +++ b/src/context.tsx @@ -17,6 +17,11 @@ export interface MenuContextProps { parentKeys: string[]; rtl?: boolean; + // Active + activeKey: string; + onActive: (key: string) => void; + onInactive: (key: string) => void; + // Popup subMenuOpenDelay: number; subMenuCloseDelay: number; diff --git a/src/interface.ts b/src/interface.ts index 7aa84ba4..049819fe 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -2,6 +2,10 @@ import type * as React from 'react'; export type MenuMode = 'horizontal' | 'vertical' | 'inline'; +export type RenderIconType = + | React.ReactNode + | ((props: any) => React.ReactNode); + export interface MenuInfo { key: string; keyPath: string[]; From 77984229b8756dd57298fe2f16a53145b85602b3 Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 15 Apr 2021 19:06:26 +0800 Subject: [PATCH 07/93] pass active keys --- src/MenuItem.tsx | 110 ++++++++++++++++++++++++++--------------- src/SubMenu.tsx | 44 ++++++++--------- src/hooks/useActive.ts | 47 ++++++++++++++++++ src/interface.ts | 5 ++ 4 files changed, 143 insertions(+), 63 deletions(-) create mode 100644 src/hooks/useActive.ts diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index 00d8ab0c..cb3ffb4c 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -1,9 +1,14 @@ import * as React from 'react'; import classNames from 'classnames'; import Overflow from 'rc-overflow'; -import type { MenuClickEventHandler, MenuInfo } from './interface'; -import type { MenuContextProps } from './context'; +import omit from 'rc-util/lib/omit'; +import type { + MenuClickEventHandler, + MenuInfo, + MenuHoverEventHandler, +} from './interface'; import { MenuContext } from './context'; +import useActive from './hooks/useActive'; export interface MenuItemProps extends Omit< @@ -17,6 +22,10 @@ export interface MenuItemProps disabled?: boolean; + // >>>>> Active + onMouseEnter?: MenuHoverEventHandler; + onMouseLeave?: MenuHoverEventHandler; + // >>>>> Events onClick?: MenuClickEventHandler; @@ -32,8 +41,7 @@ export interface MenuItemProps // onDeselect?: SelectEventHandler; // parentMenu?: React.ReactInstance; // onDestroy?: DestroyEventHandler; - // onMouseEnter?: MenuHoverEventHandler; - // onMouseLeave?: MenuHoverEventHandler; + // multiple?: boolean; // isSelected?: boolean; // manualRef?: LegacyFunctionRef; @@ -50,51 +58,75 @@ export interface MenuItemProps // Since Menu event provide the `info.item` which point to the MenuItem node instance. // We have to use class component here. // This should be removed from doc & api in future. -export default class MenuItem extends React.Component { - context: MenuContextProps; - static contextType = MenuContext; - - getEventInfo = (e: React.MouseEvent): MenuInfo => { - const { parentKeys } = this.context; - // key: React.Key; - // keyPath: React.Key[]; - // /** @deprecated This will not support in future. You should avoid to use this */ - // item: React.ReactInstance; - // domEvent: React.MouseEvent; - const { eventKey } = this.props; +class LegacyMenuItem extends React.Component { + render() { + const passedProps = omit(this.props, ['eventKey']); + return ; + } +} + +/** + * Real Menu Item component + */ +const MenuItem = (props: MenuItemProps) => { + const { + children, + className, + eventKey, + disabled, + // >>>>> Active + onMouseEnter, + onMouseLeave, + + onClick, + } = props; + const { prefixCls, onItemClick, parentKeys } = React.useContext(MenuContext); + const itemCls = `${prefixCls}-item`; + + const legacyMenuItemRef = React.useRef(); + + // ============================= Misc ============================= + const getEventInfo = (e: React.MouseEvent): MenuInfo => { return { key: eventKey, keyPath: [...parentKeys, eventKey], - item: this, + item: legacyMenuItemRef.current, domEvent: e, }; }; - onClick: React.MouseEventHandler = e => { - const { onClick } = this.props; - const { onItemClick } = this.context; + // ============================ Active ============================ + const { active, ...activeProps } = useActive( + eventKey, + disabled, + onMouseEnter, + onMouseLeave, + ); - const info = this.getEventInfo(e); + // ============================ Events ============================ + const onInternalClick: React.MouseEventHandler = e => { + const info = getEventInfo(e); onClick?.(info); onItemClick(info); }; - render() { - const { children, className, eventKey, ...restProps } = this.props; - const { prefixCls } = this.context; - const itemCls = `${prefixCls}-item`; - - return ( - - {children} - - ); - } -} + // ============================ Render ============================ + return ( + + {children} + + ); +}; + +export default MenuItem; diff --git a/src/SubMenu.tsx b/src/SubMenu.tsx index d2c22443..db0dc08e 100644 --- a/src/SubMenu.tsx +++ b/src/SubMenu.tsx @@ -6,6 +6,7 @@ import SubMenuList from './SubMenuList'; import { parseChildren } from './utils/nodeUtil'; import type { MenuClickEventHandler, + MenuHoverEventHandler, MenuInfo, MenuTitleInfo, RenderIconType, @@ -14,6 +15,7 @@ import MenuContextProvider, { MenuContext } from './context'; import useMemoCallback from './hooks/useMemoCallback'; import PopupTrigger from './PopupTrigger'; import Icon from './Icon'; +import useActive from './hooks/useActive'; export interface SubMenuProps { title?: React.ReactNode; @@ -28,6 +30,10 @@ export interface SubMenuProps { // itemIcon?: RenderIconType; expandIcon?: RenderIconType; + // >>>>> Active + onMouseEnter?: MenuHoverEventHandler; + onMouseLeave?: MenuHoverEventHandler; + // >>>>> Events onClick?: MenuClickEventHandler; onTitleClick?: (info: MenuTitleInfo) => void; @@ -41,8 +47,6 @@ export interface SubMenuProps { // triggerSubMenuAction?: TriggerSubMenuAction; // onDeselect?: SelectEventHandler; // onDestroy?: DestroyEventHandler; - // onMouseEnter?: MenuHoverEventHandler; - // onMouseLeave?: MenuHoverEventHandler; // onTitleMouseEnter?: MenuHoverEventHandler; // onTitleMouseLeave?: MenuHoverEventHandler; // popupOffset?: number[]; @@ -84,6 +88,10 @@ export default function SubMenu(props: SubMenuProps) { // Icons expandIcon, + // Active + onMouseEnter, + onMouseLeave, + // Events onClick, onTitleClick, @@ -96,11 +104,6 @@ export default function SubMenu(props: SubMenuProps) { motion, parentKeys, - // Active - activeKey, - onActive, - onInactive, - // Events onItemClick, onOpenChange, @@ -118,6 +121,14 @@ export default function SubMenu(props: SubMenuProps) { // =========================== Open & Select ============================ const open = openKeys.includes(eventKey); + // =============================== Active =============================== + const { active, ...activeProps } = useActive( + eventKey, + disabled, + onMouseEnter, + onMouseLeave, + ); + // =============================== Events =============================== // >>>> Title click const onInternalTitleClick: React.MouseEventHandler = e => { @@ -143,24 +154,9 @@ export default function SubMenu(props: SubMenuProps) { onOpenChange(eventKey, newVisible); }; - // >>>>> Title hover - const onTitleMouseEnter: React.MouseEventHandler = () => { - onActive(eventKey); - }; - - const onTitleMouseLeave: React.MouseEventHandler = () => { - onInactive(eventKey); - }; - // =============================== Render =============================== // >>>>> Title - const titleProps: React.HtmlHTMLAttributes = {}; - if (!disabled) { - titleProps.onMouseEnter = onTitleMouseEnter; - titleProps.onMouseLeave = onTitleMouseLeave; - } - let titleNode: React.ReactElement = (
      {title} @@ -212,7 +208,7 @@ export default function SubMenu(props: SubMenuProps) { component="li" className={classNames(subMenuPrefixCls, `${subMenuPrefixCls}-${mode}`, { [`${subMenuPrefixCls}-open`]: open, - [`${subMenuPrefixCls}-active`]: activeKey === eventKey, + [`${subMenuPrefixCls}-active`]: active, })} role="menuitem" > diff --git a/src/hooks/useActive.ts b/src/hooks/useActive.ts new file mode 100644 index 00000000..99305d70 --- /dev/null +++ b/src/hooks/useActive.ts @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { MenuContext } from '../context'; +import type { MenuHoverEventHandler } from '../interface'; + +interface ActiveObj { + active: boolean; + onMouseEnter?: React.MouseEventHandler; + onMouseLeave?: React.MouseEventHandler; +} + +export default function useActive( + eventKey: string, + disabled: boolean, + onMouseEnter?: MenuHoverEventHandler, + onMouseLeave?: MenuHoverEventHandler, +): ActiveObj { + const { + // Active + activeKey, + onActive, + onInactive, + } = React.useContext(MenuContext); + + const ret: ActiveObj = { + active: activeKey === eventKey, + }; + + // Skip when disabled + if (!disabled) { + ret.onMouseEnter = domEvent => { + onMouseEnter?.({ + key: eventKey, + domEvent, + }); + onActive(eventKey); + }; + ret.onMouseLeave = domEvent => { + onMouseLeave?.({ + key: eventKey, + domEvent, + }); + onInactive(eventKey); + }; + } + + return ret; +} diff --git a/src/interface.ts b/src/interface.ts index 049819fe..72834d75 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -14,6 +14,11 @@ export interface MenuInfo { domEvent: React.MouseEvent; } +export type MenuHoverEventHandler = (info: { + key: React.Key; + domEvent: React.MouseEvent; +}) => void; + export interface MenuTitleInfo { key: string; domEvent: React.MouseEvent | React.KeyboardEvent; From 4fc428aaf88eae4ca9e1a6607eb42c8ab6005664 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 16 Apr 2021 10:24:09 +0800 Subject: [PATCH 08/93] fix: mode pass logic --- docs/examples/debug.tsx | 5 +++++ src/PopupTrigger.tsx | 4 +++- src/SubMenu.tsx | 2 ++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx index bf4fbbb0..af3a27bb 100644 --- a/docs/examples/debug.tsx +++ b/docs/examples/debug.tsx @@ -78,6 +78,11 @@ export default () => { Sub Item 1 Sub Item 2 + + + Nest Sub Item 1 + Nest Sub Item 2 + Disabled Item diff --git a/src/PopupTrigger.tsx b/src/PopupTrigger.tsx index 22bc3e5c..3b4b9f48 100644 --- a/src/PopupTrigger.tsx +++ b/src/PopupTrigger.tsx @@ -4,6 +4,7 @@ import classNames from 'classnames'; import type { CSSMotionProps } from 'rc-motion'; import { MenuContext } from './context'; import { placements, placementsRtl } from './placements'; +import type { MenuMode } from './interface'; const popupPlacementMap = { horizontal: 'bottomLeft', @@ -14,6 +15,7 @@ const popupPlacementMap = { export interface PopupTriggerProps { prefixCls: string; + mode: MenuMode; visible: boolean; children: React.ReactElement; popup: React.ReactNode; @@ -27,6 +29,7 @@ export default function PopupTrigger({ children, popup, disabled, + mode, onVisibleChange, }: PopupTriggerProps) { const { @@ -37,7 +40,6 @@ export default function PopupTrigger({ builtinPlacements, triggerSubMenuAction, forceSubMenuRender, - mode, motion, } = React.useContext(MenuContext); diff --git a/src/SubMenu.tsx b/src/SubMenu.tsx index db0dc08e..70480f88 100644 --- a/src/SubMenu.tsx +++ b/src/SubMenu.tsx @@ -187,6 +187,7 @@ export default function SubMenu(props: SubMenuProps) { if (mode !== 'inline') { titleNode = ( {childList}} @@ -203,6 +204,7 @@ export default function SubMenu(props: SubMenuProps) { Date: Fri, 16 Apr 2021 11:06:46 +0800 Subject: [PATCH 09/93] support menu group --- docs/examples/debug.tsx | 23 ++++++++++++++--- src/MenuItemGroup.tsx | 51 +++++++++++++++++++++++++++++++++++++ src/SubMenu.tsx | 5 ++++ src/index.ts | 2 +- src/sugar/MenuItemGroup.tsx | 4 --- 5 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 src/MenuItemGroup.tsx delete mode 100644 src/sugar/MenuItemGroup.tsx diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx index af3a27bb..fb6de7ff 100644 --- a/docs/examples/debug.tsx +++ b/docs/examples/debug.tsx @@ -2,7 +2,7 @@ import React from 'react'; import type { CSSMotionProps } from 'rc-motion'; -import Menu from '../../src'; +import Menu, { ItemGroup as MenuItemGroup } from '../../src'; import type { MenuProps } from '../../src'; import '../../assets/index.less'; import '../../assets/menu.less'; @@ -80,13 +80,30 @@ export default () => { Sub Item 2 - Nest Sub Item 1 - Nest Sub Item 2 + + 2 + 3 + + + 4 + 5 + Disabled Item + + + + Disabled Sub Item 1 + + ); diff --git a/src/MenuItemGroup.tsx b/src/MenuItemGroup.tsx new file mode 100644 index 00000000..fb17dd13 --- /dev/null +++ b/src/MenuItemGroup.tsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import classNames from 'classnames'; +import { parseChildren } from './utils/nodeUtil'; +import { MenuContext } from './context'; + +export interface MenuItemGroupProps { + className?: string; + title?: React.ReactNode; + children?: React.ReactNode; + + // disabled?: boolean; + // renderMenuItem?: ( + // item: React.ReactElement, + // index: number, + // key: string, + // ) => React.ReactElement; + // index?: number; + // subMenuKey?: string; + // rootPrefixCls?: string; + // onClick?: MenuClickEventHandler; + // direction?: 'ltr' | 'rtl'; +} + +export default function MenuItemGroup({ + className, + title, + children, + ...restProps +}: MenuItemGroupProps) { + const { prefixCls } = React.useContext(MenuContext); + + const groupPrefixCls = `${prefixCls}-item-group`; + + const childList: React.ReactElement[] = parseChildren(children); + + return ( +
    • e.stopPropagation()} + className={classNames(groupPrefixCls, className)} + > +
      + {title} +
      +
        {childList}
      +
    • + ); +} diff --git a/src/SubMenu.tsx b/src/SubMenu.tsx index 70480f88..819ac2e4 100644 --- a/src/SubMenu.tsx +++ b/src/SubMenu.tsx @@ -132,6 +132,11 @@ export default function SubMenu(props: SubMenuProps) { // =============================== Events =============================== // >>>> Title click const onInternalTitleClick: React.MouseEventHandler = e => { + // Skip if disabled + if (disabled) { + return; + } + onTitleClick?.({ key: eventKey, domEvent: e, diff --git a/src/index.ts b/src/index.ts index 17375076..5249fe99 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import Menu, { MenuProps } from './Menu'; import MenuItem from './MenuItem'; import SubMenu from './SubMenu'; -import MenuItemGroup from './sugar/MenuItemGroup'; +import MenuItemGroup from './MenuItemGroup'; import Divider from './sugar/Divider'; export { diff --git a/src/sugar/MenuItemGroup.tsx b/src/sugar/MenuItemGroup.tsx deleted file mode 100644 index a827c6c6..00000000 --- a/src/sugar/MenuItemGroup.tsx +++ /dev/null @@ -1,4 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export default function MenuItemGroup(_: any) { - return null; -} From a5036a15493df5b5ad3a1112c88884d6a3044536 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 16 Apr 2021 11:15:31 +0800 Subject: [PATCH 10/93] docs: Fix definition --- docs/examples/antd.tsx | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/examples/antd.tsx b/docs/examples/antd.tsx index b48e8803..fff46f30 100644 --- a/docs/examples/antd.tsx +++ b/docs/examples/antd.tsx @@ -1,7 +1,7 @@ /* eslint-disable no-console, react/require-default-props, no-param-reassign */ import React from 'react'; -import Menu, { SubMenu, Item as MenuItem, Divider } from 'rc-menu'; +import Menu, { SubMenu, Item as MenuItem, Divider, MenuProps } from '../../src'; import '../../assets/index.less'; function handleClick(info) { @@ -95,8 +95,22 @@ const children2 = [ const customizeIndicator = Add More Items; -export class CommonMenu extends React.Component { - state = { +interface CommonMenuProps { + mode: MenuProps['mode']; + triggerSubMenuAction?: MenuProps['triggerSubMenuAction']; + updateChildrenAndOverflowedIndicator?: boolean; +} + +interface CommonMenuState { + children: React.ReactNode; + overflowedIndicator: React.ReactNode; +} + +export class CommonMenu extends React.Component< + CommonMenuProps, + CommonMenuState +> { + state: CommonMenuState = { children: children1, overflowedIndicator: undefined, }; From 4b26d841d1745231535f8f15a96d30532948be33 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 16 Apr 2021 15:09:33 +0800 Subject: [PATCH 11/93] disabled should not click --- src/Menu.tsx | 88 +++++++++++++++++++++++++++++++++++++++---- src/MenuItem.tsx | 14 ++++++- src/SubMenu.tsx | 7 +++- src/context.tsx | 3 ++ src/interface.ts | 24 ++++++++---- src/utils/nodeUtil.ts | 17 +++++++-- 6 files changed, 133 insertions(+), 20 deletions(-) diff --git a/src/Menu.tsx b/src/Menu.tsx index 0e542a2c..13c2fadf 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -8,7 +8,9 @@ import type { MenuClickEventHandler, MenuInfo, MenuMode, + SelectEventHandler, TriggerSubMenuAction, + SelectInfo, } from './interface'; import MenuItem from './MenuItem'; import { parseChildren } from './utils/nodeUtil'; @@ -36,6 +38,17 @@ export interface MenuProps activeKey?: string; defaultActiveFirst?: boolean; + // Selection + selectable?: boolean; + multiple?: boolean; + + defaultSelectedKeys?: string[]; + selectedKeys?: string[]; + + onSelect?: SelectEventHandler; + onDeselect?: SelectEventHandler; + + // Motion /** Menu motion define */ motion?: CSSMotionProps; @@ -53,16 +66,9 @@ export interface MenuProps onClick?: MenuClickEventHandler; onOpenChange?: (openKeys: React.Key[]) => void; - // defaultSelectedKeys?: string[]; - // selectedKeys?: string[]; - - // onSelect?: SelectEventHandler; - // onDeselect?: SelectEventHandler; // onDestroy?: DestroyEventHandler; // level?: number; - // selectable?: boolean; - // multiple?: boolean; // itemIcon?: RenderIconType; // expandIcon?: RenderIconType; @@ -103,6 +109,14 @@ const Menu: React.FC = ({ activeKey, defaultActiveFirst, + // Selection + selectable = true, + multiple = false, + defaultSelectedKeys, + selectedKeys, + onSelect, + onDeselect, + // Motion motion, @@ -143,9 +157,67 @@ const Menu: React.FC = ({ setMergedActiveKey(undefined); }); + // ======================== Select ======================== + const [mergedSelectKeys, setMergedSelectKeys] = useMergedState( + defaultSelectedKeys || [], + { + value: selectedKeys, + + // Legacy convert key to array + postState: keys => { + if (Array.isArray(keys)) { + return keys; + } + + if (keys === null || keys === undefined) { + return []; + } + + return [keys]; + }, + }, + ); + + const triggerSelection = (info: MenuInfo) => { + if (!selectable) { + return; + } + + // Insert or Remove + const { key: targetKey } = info; + const exist = mergedSelectKeys.includes(targetKey); + let newSelectKeys: string[]; + + if (exist) { + newSelectKeys = mergedSelectKeys.filter(key => key !== targetKey); + } else if (multiple) { + newSelectKeys = [...mergedSelectKeys, targetKey]; + } else { + newSelectKeys = [targetKey]; + } + + setMergedSelectKeys(newSelectKeys); + + // Trigger event + const selectInfo: SelectInfo = { + ...info, + selectedKeys: newSelectKeys, + }; + + if (exist) { + onDeselect?.(selectInfo); + } else { + onSelect?.(selectInfo); + } + }; + // ======================== Events ======================== + /** + * Click for item. SubMenu do not have selection status + */ const onInternalClick = useMemoCallback((info: MenuInfo) => { onClick?.(info); + triggerSelection(info); }); const onInternalOpenChange = useMemoCallback((key: string, open: boolean) => { @@ -205,6 +277,8 @@ const Menu: React.FC = ({ activeKey={mergedActiveKey} onActive={onActive} onInactive={onInactive} + // Selection + selectedKeys={mergedSelectKeys} // Popup subMenuOpenDelay={subMenuOpenDelay} subMenuCloseDelay={subMenuCloseDelay} diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index cb3ffb4c..516527f7 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -74,13 +74,16 @@ const MenuItem = (props: MenuItemProps) => { className, eventKey, disabled, + // >>>>> Active onMouseEnter, onMouseLeave, onClick, } = props; - const { prefixCls, onItemClick, parentKeys } = React.useContext(MenuContext); + const { prefixCls, onItemClick, parentKeys, selectedKeys } = React.useContext( + MenuContext, + ); const itemCls = `${prefixCls}-item`; const legacyMenuItemRef = React.useRef(); @@ -103,8 +106,15 @@ const MenuItem = (props: MenuItemProps) => { onMouseLeave, ); + // ============================ Select ============================ + const selected = selectedKeys.includes(eventKey); + // ============================ Events ============================ const onInternalClick: React.MouseEventHandler = e => { + if (disabled) { + return; + } + const info = getEventInfo(e); onClick?.(info); @@ -120,6 +130,8 @@ const MenuItem = (props: MenuItemProps) => { component="li" className={classNames(itemCls, className, { [`${itemCls}-active`]: active, + [`${itemCls}-selected`]: selected, + [`${itemCls}-disabled`]: disabled, })} role="menuitem" onClick={onInternalClick} diff --git a/src/SubMenu.tsx b/src/SubMenu.tsx index 819ac2e4..6044e649 100644 --- a/src/SubMenu.tsx +++ b/src/SubMenu.tsx @@ -110,7 +110,6 @@ export default function SubMenu(props: SubMenuProps) { } = React.useContext(MenuContext); const subMenuPrefixCls = `${prefixCls}-submenu`; - const childList: React.ReactElement[] = parseChildren(children); // ================================ Key ================================= const connectedKeys = React.useMemo(() => [...parentKeys, eventKey], [ @@ -118,6 +117,12 @@ export default function SubMenu(props: SubMenuProps) { eventKey, ]); + // ============================== Children ============================== + const childList: React.ReactElement[] = parseChildren( + children, + connectedKeys, + ); + // =========================== Open & Select ============================ const open = openKeys.includes(eventKey); diff --git a/src/context.tsx b/src/context.tsx index 043a206e..62bb7f53 100644 --- a/src/context.tsx +++ b/src/context.tsx @@ -22,6 +22,9 @@ export interface MenuContextProps { onActive: (key: string) => void; onInactive: (key: string) => void; + // Selection + selectedKeys: string[]; + // Popup subMenuOpenDelay: number; subMenuCloseDelay: number; diff --git a/src/interface.ts b/src/interface.ts index 72834d75..8d2abd0f 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,7 +1,12 @@ import type * as React from 'react'; +// ========================== Basic ========================== export type MenuMode = 'horizontal' | 'vertical' | 'inline'; +export type BuiltinPlacements = Record; + +export type TriggerSubMenuAction = 'click' | 'hover'; + export type RenderIconType = | React.ReactNode | ((props: any) => React.ReactNode); @@ -14,18 +19,23 @@ export interface MenuInfo { domEvent: React.MouseEvent; } +export interface MenuTitleInfo { + key: string; + domEvent: React.MouseEvent | React.KeyboardEvent; +} + +// ========================== Hover ========================== export type MenuHoverEventHandler = (info: { key: React.Key; domEvent: React.MouseEvent; }) => void; -export interface MenuTitleInfo { - key: string; - domEvent: React.MouseEvent | React.KeyboardEvent; +// ======================== Selection ======================== +export interface SelectInfo extends MenuInfo { + selectedKeys: React.Key[]; } -export type MenuClickEventHandler = (info: MenuInfo) => void; - -export type BuiltinPlacements = Record; +export type SelectEventHandler = (info: SelectInfo) => void; -export type TriggerSubMenuAction = 'click' | 'hover'; +// ========================== Click ========================== +export type MenuClickEventHandler = (info: MenuInfo) => void; diff --git a/src/utils/nodeUtil.ts b/src/utils/nodeUtil.ts index f9875214..8d2df9b7 100644 --- a/src/utils/nodeUtil.ts +++ b/src/utils/nodeUtil.ts @@ -1,11 +1,20 @@ import * as React from 'react'; import toArray from 'rc-util/lib/Children/toArray'; -export function parseChildren(children: React.ReactNode) { - return toArray(children).map(child => { - if (React.isValidElement(child) && child.key !== undefined) { +export function parseChildren( + children: React.ReactNode, + keyPath: string[] = [], +) { + return toArray(children).map((child, index) => { + if (React.isValidElement(child)) { + let { key } = child; + if (key === null || key === undefined) { + key = `tmp_key-${[...keyPath, index].join('-')}`; + } + return React.cloneElement(child, { - eventKey: child.key, + key, + eventKey: key, } as any); } From 1e1cd0d916bbaf1854d52e0e677ff67392cbddd2 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 16 Apr 2021 15:57:20 +0800 Subject: [PATCH 12/93] chore: Add loop check of path --- src/Menu.tsx | 8 ++++++ src/MenuItem.tsx | 33 +++++++++++++++++++---- src/SubMenu.tsx | 22 +++++++++++++++- src/context.tsx | 3 ++- src/hooks/usePathData.ts | 56 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 src/hooks/usePathData.ts diff --git a/src/Menu.tsx b/src/Menu.tsx index 13c2fadf..0e7fcd8e 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -16,6 +16,7 @@ import MenuItem from './MenuItem'; import { parseChildren } from './utils/nodeUtil'; import MenuContextProvider from './context'; import useMemoCallback from './hooks/useMemoCallback'; +import usePathData from './hooks/usePathData'; // optimize for render const EMPTY_LIST: string[] = []; @@ -141,6 +142,9 @@ const Menu: React.FC = ({ }, ); + // ========================= Path ========================= + const pathData = usePathData(); + // ======================== Active ======================== const [mergedActiveKey, setMergedActiveKey] = useMergedState( activeKey || ((defaultActiveFirst && childList[0]?.key) as string), @@ -158,6 +162,7 @@ const Menu: React.FC = ({ }); // ======================== Select ======================== + // >>>>> Select keys const [mergedSelectKeys, setMergedSelectKeys] = useMergedState( defaultSelectedKeys || [], { @@ -178,6 +183,7 @@ const Menu: React.FC = ({ }, ); + // >>>>> Trigger select const triggerSelection = (info: MenuInfo) => { if (!selectable) { return; @@ -273,6 +279,8 @@ const Menu: React.FC = ({ motion={motion} parentKeys={EMPTY_LIST} rtl={direction === 'rtl'} + // Path + {...pathData} // Active activeKey={mergedActiveKey} onActive={onActive} diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index 516527f7..b62c2250 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -81,18 +81,31 @@ const MenuItem = (props: MenuItemProps) => { onClick, } = props; - const { prefixCls, onItemClick, parentKeys, selectedKeys } = React.useContext( - MenuContext, - ); + + const { + prefixCls, + onItemClick, + parentKeys, + selectedKeys, + // Path + registerPath, + unregisterPath, + } = React.useContext(MenuContext); const itemCls = `${prefixCls}-item`; const legacyMenuItemRef = React.useRef(); - // ============================= Misc ============================= + // ============================= Key ============================== + const connectedKeys = React.useMemo(() => [...parentKeys, eventKey], [ + parentKeys, + eventKey, + ]); + + // ============================= Info ============================= const getEventInfo = (e: React.MouseEvent): MenuInfo => { return { key: eventKey, - keyPath: [...parentKeys, eventKey], + keyPath: connectedKeys, item: legacyMenuItemRef.current, domEvent: e, }; @@ -121,6 +134,16 @@ const MenuItem = (props: MenuItemProps) => { onItemClick(info); }; + // ============================ Effect ============================ + // Path register + React.useEffect(() => { + registerPath(eventKey, connectedKeys); + + return () => { + unregisterPath(eventKey, connectedKeys); + }; + }, [eventKey, connectedKeys]); + // ============================ Render ============================ return ( >>> Title click const onInternalTitleClick: React.MouseEventHandler = e => { @@ -164,6 +174,16 @@ export default function SubMenu(props: SubMenuProps) { onOpenChange(eventKey, newVisible); }; + // =============================== Effect =============================== + // Path register + React.useEffect(() => { + registerPath(eventKey, connectedKeys); + + return () => { + unregisterPath(eventKey, connectedKeys); + }; + }, [eventKey, connectedKeys]); + // =============================== Render =============================== // >>>>> Title @@ -220,7 +240,7 @@ export default function SubMenu(props: SubMenuProps) { component="li" className={classNames(subMenuPrefixCls, `${subMenuPrefixCls}-${mode}`, { [`${subMenuPrefixCls}-open`]: open, - [`${subMenuPrefixCls}-active`]: active, + [`${subMenuPrefixCls}-active`]: mergedActive, })} role="menuitem" > diff --git a/src/context.tsx b/src/context.tsx index 62bb7f53..f1d931eb 100644 --- a/src/context.tsx +++ b/src/context.tsx @@ -8,8 +8,9 @@ import type { MenuMode, TriggerSubMenuAction, } from './interface'; +import type { PathHookRet } from './hooks/usePathData'; -export interface MenuContextProps { +export interface MenuContextProps extends PathHookRet { prefixCls: string; mode: MenuMode; openKeys: string[]; diff --git a/src/hooks/usePathData.ts b/src/hooks/usePathData.ts new file mode 100644 index 00000000..3140484c --- /dev/null +++ b/src/hooks/usePathData.ts @@ -0,0 +1,56 @@ +import * as React from 'react'; + +const PATH_SPLIT = '__RC_UTIL_PATH_SPLIT__'; + +const getPathStr = (keyPath: string[]) => keyPath.join(PATH_SPLIT); + +function usePathData() { + const [, forceUpdate] = React.useState({}); + const path2keyRef = React.useRef(new Map()); + const key2pathRef = React.useRef(new Map()); + const updateRef = React.useRef(0); + + function registerPath(key: string, keyPath: string[]) { + const connectedPath = getPathStr(keyPath); + path2keyRef.current.set(connectedPath, key); + key2pathRef.current.set(key, connectedPath); + + updateRef.current += 1; + const id = updateRef.current; + + Promise.resolve().then(() => { + if (id === updateRef.current) { + forceUpdate({}); + } + }); + } + + function unregisterPath(key: string, keyPath: string[]) { + const connectedPath = getPathStr(keyPath); + path2keyRef.current.delete(connectedPath); + key2pathRef.current.delete(key); + } + + function keyInPath(keyList: string[], keyPath: string[]) { + /** + * Generate `path1__SPLIT__path2__SPLIT__` instead of `path1__SPLIT__path2`. + * To avoid full path like `path1__SPLIT__path23__SPLIT__path3` matching. + */ + const connectedPath = getPathStr([...keyPath, '']); + + return keyList.some(key => { + const fullPath = key2pathRef.current.get(key); + return fullPath.startsWith(connectedPath); + }); + } + + return { + registerPath, + unregisterPath, + keyInPath, + }; +} + +export type PathHookRet = ReturnType; + +export default usePathData; From f50eb2b4491f58a101124532db47ff315633e0e5 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 16 Apr 2021 16:02:51 +0800 Subject: [PATCH 13/93] chore: Back of nest selection --- src/SubMenu.tsx | 9 ++++++++- src/hooks/usePathData.ts | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/SubMenu.tsx b/src/SubMenu.tsx index 3f74f9d8..96e7a38c 100644 --- a/src/SubMenu.tsx +++ b/src/SubMenu.tsx @@ -107,6 +107,9 @@ export default function SubMenu(props: SubMenuProps) { // ActiveKey activeKey, + // SelectKey + selectedKeys, + // Path registerPath, unregisterPath, @@ -131,9 +134,12 @@ export default function SubMenu(props: SubMenuProps) { connectedKeys, ); - // =========================== Open & Select ============================ + // ================================ Open ================================ const open = openKeys.includes(eventKey); + // =============================== Select =============================== + const childrenSelected = keyInPath(selectedKeys, connectedKeys); + // =============================== Active =============================== const { active, ...activeProps } = useActive( eventKey, @@ -241,6 +247,7 @@ export default function SubMenu(props: SubMenuProps) { className={classNames(subMenuPrefixCls, `${subMenuPrefixCls}-${mode}`, { [`${subMenuPrefixCls}-open`]: open, [`${subMenuPrefixCls}-active`]: mergedActive, + [`${subMenuPrefixCls}-selected`]: childrenSelected, })} role="menuitem" > diff --git a/src/hooks/usePathData.ts b/src/hooks/usePathData.ts index 3140484c..aa4a94df 100644 --- a/src/hooks/usePathData.ts +++ b/src/hooks/usePathData.ts @@ -39,11 +39,18 @@ function usePathData() { const connectedPath = getPathStr([...keyPath, '']); return keyList.some(key => { - const fullPath = key2pathRef.current.get(key); + const fullPath = key2pathRef.current.get(key) || ''; return fullPath.startsWith(connectedPath); }); } + React.useEffect( + () => () => { + updateRef.current = null; + }, + [], + ); + return { registerPath, unregisterPath, From 8ee63cb874e6ce08f4316827ac26827736b75da3 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 16 Apr 2021 16:15:47 +0800 Subject: [PATCH 14/93] wrap item prop as warning --- src/Menu.tsx | 3 ++- src/MenuItem.tsx | 3 ++- src/SubMenu.tsx | 3 ++- src/utils/warnUtil.ts | 21 +++++++++++++++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 src/utils/warnUtil.ts diff --git a/src/Menu.tsx b/src/Menu.tsx index 0e7fcd8e..08403890 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -17,6 +17,7 @@ import { parseChildren } from './utils/nodeUtil'; import MenuContextProvider from './context'; import useMemoCallback from './hooks/useMemoCallback'; import usePathData from './hooks/usePathData'; +import { warnItemProp } from './utils/warnUtil'; // optimize for render const EMPTY_LIST: string[] = []; @@ -222,7 +223,7 @@ const Menu: React.FC = ({ * Click for item. SubMenu do not have selection status */ const onInternalClick = useMemoCallback((info: MenuInfo) => { - onClick?.(info); + onClick?.(warnItemProp(info)); triggerSelection(info); }); diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index b62c2250..a17443de 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -9,6 +9,7 @@ import type { } from './interface'; import { MenuContext } from './context'; import useActive from './hooks/useActive'; +import { warnItemProp } from './utils/warnUtil'; export interface MenuItemProps extends Omit< @@ -130,7 +131,7 @@ const MenuItem = (props: MenuItemProps) => { const info = getEventInfo(e); - onClick?.(info); + onClick?.(warnItemProp(info)); onItemClick(info); }; diff --git a/src/SubMenu.tsx b/src/SubMenu.tsx index 96e7a38c..b89d0dc3 100644 --- a/src/SubMenu.tsx +++ b/src/SubMenu.tsx @@ -16,6 +16,7 @@ import useMemoCallback from './hooks/useMemoCallback'; import PopupTrigger from './PopupTrigger'; import Icon from './Icon'; import useActive from './hooks/useActive'; +import { warnItemProp } from './utils/warnUtil'; export interface SubMenuProps { title?: React.ReactNode; @@ -171,7 +172,7 @@ export default function SubMenu(props: SubMenuProps) { // >>>> Context for children click const onMergedItemClick = useMemoCallback((info: MenuInfo) => { - onClick?.(info); + onClick?.(warnItemProp(info)); onItemClick(info); }); diff --git a/src/utils/warnUtil.ts b/src/utils/warnUtil.ts new file mode 100644 index 00000000..8a5dc27b --- /dev/null +++ b/src/utils/warnUtil.ts @@ -0,0 +1,21 @@ +import warning from 'rc-util/lib/warning'; +import type { MenuInfo } from '../interface'; + +/** + * `onClick` event return `info.item` which point to react node directly. + * We should warning this since it will not work on FC. + */ +export function warnItemProp({ item, ...restInfo }: MenuInfo): MenuInfo { + Object.defineProperty(restInfo, 'item', { + get: () => { + warning( + false, + '`info.item` is deprecated since we will move to function component that not provides React Node instance in future.', + ); + + return item; + }, + }); + + return restInfo as MenuInfo; +} From 52c4aec910bfd9340c0eeed22621524ba5375b08 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 16 Apr 2021 16:32:36 +0800 Subject: [PATCH 15/93] feat: support onSelect & onDeselect event --- src/Menu.tsx | 59 ++++++++-------------------- src/MenuItem.tsx | 90 +++++++++++++++++++++++++++++++++++++------ src/context.tsx | 5 +++ src/interface.ts | 4 +- src/utils/warnUtil.ts | 8 ++-- 5 files changed, 106 insertions(+), 60 deletions(-) diff --git a/src/Menu.tsx b/src/Menu.tsx index 08403890..5e07aace 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -10,7 +10,6 @@ import type { MenuMode, SelectEventHandler, TriggerSubMenuAction, - SelectInfo, } from './interface'; import MenuItem from './MenuItem'; import { parseChildren } from './utils/nodeUtil'; @@ -184,39 +183,16 @@ const Menu: React.FC = ({ }, ); - // >>>>> Trigger select - const triggerSelection = (info: MenuInfo) => { - if (!selectable) { - return; - } - - // Insert or Remove - const { key: targetKey } = info; - const exist = mergedSelectKeys.includes(targetKey); - let newSelectKeys: string[]; - - if (exist) { - newSelectKeys = mergedSelectKeys.filter(key => key !== targetKey); - } else if (multiple) { - newSelectKeys = [...mergedSelectKeys, targetKey]; - } else { - newSelectKeys = [targetKey]; - } - - setMergedSelectKeys(newSelectKeys); - - // Trigger event - const selectInfo: SelectInfo = { - ...info, - selectedKeys: newSelectKeys, - }; + // >>>>> events + const onInternalSelect: SelectEventHandler = useMemoCallback(selectInfo => { + setMergedSelectKeys(selectInfo.selectedKeys); + onSelect?.(warnItemProp(selectInfo)); + }); - if (exist) { - onDeselect?.(selectInfo); - } else { - onSelect?.(selectInfo); - } - }; + const onInternalDeselect: SelectEventHandler = useMemoCallback(selectInfo => { + setMergedSelectKeys(selectInfo.selectedKeys); + onDeselect?.(warnItemProp(selectInfo)); + }); // ======================== Events ======================== /** @@ -224,7 +200,6 @@ const Menu: React.FC = ({ */ const onInternalClick = useMemoCallback((info: MenuInfo) => { onClick?.(warnItemProp(info)); - triggerSelection(info); }); const onInternalOpenChange = useMemoCallback((key: string, open: boolean) => { @@ -263,15 +238,6 @@ const Menu: React.FC = ({ /> ); - // return ( - //
        - // ); - return ( = ({ onActive={onActive} onInactive={onInactive} // Selection + selectable={selectable} + multiple={multiple} selectedKeys={mergedSelectKeys} + onItemSelect={onInternalSelect} + onItemDeselect={onInternalDeselect} + // Click + onItemClick={onInternalClick} // Popup subMenuOpenDelay={subMenuOpenDelay} subMenuCloseDelay={subMenuCloseDelay} forceSubMenuRender={forceSubMenuRender} builtinPlacements={builtinPlacements} triggerSubMenuAction={triggerSubMenuAction} - onItemClick={onInternalClick} onOpenChange={onInternalOpenChange} getPopupContainer={getInternalPopupContainer} > diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index a17443de..29e3c774 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -1,11 +1,14 @@ import * as React from 'react'; import classNames from 'classnames'; import Overflow from 'rc-overflow'; +import warning from 'rc-util/lib/warning'; import omit from 'rc-util/lib/omit'; import type { MenuClickEventHandler, MenuInfo, MenuHoverEventHandler, + SelectEventHandler, + SelectInfo, } from './interface'; import { MenuContext } from './context'; import useActive from './hooks/useActive'; @@ -23,23 +26,22 @@ export interface MenuItemProps disabled?: boolean; + /** @deprecated No place to use this. Should remove */ + attribute?: Record; + // >>>>> Active onMouseEnter?: MenuHoverEventHandler; onMouseLeave?: MenuHoverEventHandler; + // >>>>> Selection + onSelect?: SelectEventHandler; + onDeselect?: SelectEventHandler; + // >>>>> Events onClick?: MenuClickEventHandler; - /** @deprecated No place to use this. Should remove */ - // attribute?: Record; - // rootPrefixCls?: string; - // active?: boolean; - // selectedKeys?: string[]; - // title?: string; // onItemHover?: HoverEventHandler; - // onSelect?: SelectEventHandler; - // onDeselect?: SelectEventHandler; // parentMenu?: React.ReactInstance; // onDestroy?: DestroyEventHandler; @@ -54,6 +56,11 @@ export interface MenuItemProps // No need anymore // mode?: MenuMode; + + // >>>>>>>>>>>>>> Useless props + // rootPrefixCls?: string; + // active?: boolean; + // selectedKeys?: string[]; } // Since Menu event provide the `info.item` which point to the MenuItem node instance. @@ -61,8 +68,21 @@ export interface MenuItemProps // This should be removed from doc & api in future. class LegacyMenuItem extends React.Component { render() { - const passedProps = omit(this.props, ['eventKey']); - return ; + const { title, attribute, ...restProps } = this.props; + + const passedProps = omit(restProps, ['eventKey']); + warning( + !attribute, + '`attribute` of Menu.Item is deprecated. Please pass attribute directly.', + ); + + return ( + + ); } } @@ -76,10 +96,14 @@ const MenuItem = (props: MenuItemProps) => { eventKey, disabled, - // >>>>> Active + // Active onMouseEnter, onMouseLeave, + // Select + onSelect, + onDeselect, + onClick, } = props; @@ -87,7 +111,14 @@ const MenuItem = (props: MenuItemProps) => { prefixCls, onItemClick, parentKeys, + + // Select + selectable, + multiple, selectedKeys, + onItemSelect, + onItemDeselect, + // Path registerPath, unregisterPath, @@ -123,6 +154,42 @@ const MenuItem = (props: MenuItemProps) => { // ============================ Select ============================ const selected = selectedKeys.includes(eventKey); + // Events + const triggerSelection = (info: MenuInfo) => { + if (!selectable) { + return; + } + + // Insert or Remove + const { key: targetKey } = info; + const exist = selectedKeys.includes(targetKey); + let newSelectKeys: string[]; + + if (exist) { + newSelectKeys = selectedKeys.filter(key => key !== targetKey); + } else if (multiple) { + newSelectKeys = [...selectedKeys, targetKey]; + } else { + newSelectKeys = [targetKey]; + } + + // Trigger event + const selectInfo: SelectInfo = { + ...info, + selectedKeys: newSelectKeys, + }; + + const outerInfo = warnItemProp(selectInfo); + + if (exist) { + onItemDeselect(selectInfo); + onSelect?.(outerInfo); + } else { + onItemSelect(selectInfo); + onDeselect?.(outerInfo); + } + }; + // ============================ Events ============================ const onInternalClick: React.MouseEventHandler = e => { if (disabled) { @@ -133,6 +200,7 @@ const MenuItem = (props: MenuItemProps) => { onClick?.(warnItemProp(info)); onItemClick(info); + triggerSelection(info); }; // ============================ Effect ============================ diff --git a/src/context.tsx b/src/context.tsx index f1d931eb..440d62bd 100644 --- a/src/context.tsx +++ b/src/context.tsx @@ -6,6 +6,7 @@ import type { BuiltinPlacements, MenuClickEventHandler, MenuMode, + SelectEventHandler, TriggerSubMenuAction, } from './interface'; import type { PathHookRet } from './hooks/usePathData'; @@ -24,7 +25,11 @@ export interface MenuContextProps extends PathHookRet { onInactive: (key: string) => void; // Selection + selectable?: boolean; + multiple?: boolean; selectedKeys: string[]; + onItemSelect?: SelectEventHandler; + onItemDeselect?: SelectEventHandler; // Popup subMenuOpenDelay: number; diff --git a/src/interface.ts b/src/interface.ts index 8d2abd0f..aa7cef2f 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -26,13 +26,13 @@ export interface MenuTitleInfo { // ========================== Hover ========================== export type MenuHoverEventHandler = (info: { - key: React.Key; + key: string; domEvent: React.MouseEvent; }) => void; // ======================== Selection ======================== export interface SelectInfo extends MenuInfo { - selectedKeys: React.Key[]; + selectedKeys: string[]; } export type SelectEventHandler = (info: SelectInfo) => void; diff --git a/src/utils/warnUtil.ts b/src/utils/warnUtil.ts index 8a5dc27b..afd5c3cf 100644 --- a/src/utils/warnUtil.ts +++ b/src/utils/warnUtil.ts @@ -1,11 +1,13 @@ import warning from 'rc-util/lib/warning'; -import type { MenuInfo } from '../interface'; /** * `onClick` event return `info.item` which point to react node directly. * We should warning this since it will not work on FC. */ -export function warnItemProp({ item, ...restInfo }: MenuInfo): MenuInfo { +export function warnItemProp({ + item, + ...restInfo +}: T): T { Object.defineProperty(restInfo, 'item', { get: () => { warning( @@ -17,5 +19,5 @@ export function warnItemProp({ item, ...restInfo }: MenuInfo): MenuInfo { }, }); - return restInfo as MenuInfo; + return restInfo as T; } From 3626171adecc1ebebac224388b1410cf2390b3a0 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 16 Apr 2021 17:00:15 +0800 Subject: [PATCH 16/93] revert useless onSelect & support itemIcon --- src/Icon.tsx | 3 +- src/Menu.tsx | 50 ++++++++++++++++++++++++---------- src/MenuItem.tsx | 71 +++++++----------------------------------------- src/SubMenu.tsx | 2 -- src/context.tsx | 5 ---- 5 files changed, 47 insertions(+), 84 deletions(-) diff --git a/src/Icon.tsx b/src/Icon.tsx index 889825f2..2a2bcd52 100644 --- a/src/Icon.tsx +++ b/src/Icon.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; import type { RenderIconType } from './interface'; import type { SubMenuProps } from './SubMenu'; +import type { MenuItemProps } from './MenuItem'; export interface IconProps { icon?: RenderIconType; - props: SubMenuProps & { isSubMenu: boolean }; + props: (SubMenuProps & { isSubMenu: boolean }) | MenuItemProps; /** Fallback of icon if provided */ children?: React.ReactElement; } diff --git a/src/Menu.tsx b/src/Menu.tsx index 5e07aace..ece1f14c 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -10,6 +10,7 @@ import type { MenuMode, SelectEventHandler, TriggerSubMenuAction, + SelectInfo, } from './interface'; import MenuItem from './MenuItem'; import { parseChildren } from './utils/nodeUtil'; @@ -183,16 +184,39 @@ const Menu: React.FC = ({ }, ); - // >>>>> events - const onInternalSelect: SelectEventHandler = useMemoCallback(selectInfo => { - setMergedSelectKeys(selectInfo.selectedKeys); - onSelect?.(warnItemProp(selectInfo)); - }); + // >>>>> Trigger select + const triggerSelection = (info: MenuInfo) => { + if (!selectable) { + return; + } - const onInternalDeselect: SelectEventHandler = useMemoCallback(selectInfo => { - setMergedSelectKeys(selectInfo.selectedKeys); - onDeselect?.(warnItemProp(selectInfo)); - }); + // Insert or Remove + const { key: targetKey } = info; + const exist = mergedSelectKeys.includes(targetKey); + let newSelectKeys: string[]; + + if (exist) { + newSelectKeys = mergedSelectKeys.filter(key => key !== targetKey); + } else if (multiple) { + newSelectKeys = [...mergedSelectKeys, targetKey]; + } else { + newSelectKeys = [targetKey]; + } + + setMergedSelectKeys(newSelectKeys); + + // Trigger event + const selectInfo: SelectInfo = { + ...info, + selectedKeys: newSelectKeys, + }; + + if (exist) { + onDeselect?.(selectInfo); + } else { + onSelect?.(selectInfo); + } + }; // ======================== Events ======================== /** @@ -200,6 +224,7 @@ const Menu: React.FC = ({ */ const onInternalClick = useMemoCallback((info: MenuInfo) => { onClick?.(warnItemProp(info)); + triggerSelection(info); }); const onInternalOpenChange = useMemoCallback((key: string, open: boolean) => { @@ -253,19 +278,14 @@ const Menu: React.FC = ({ onActive={onActive} onInactive={onInactive} // Selection - selectable={selectable} - multiple={multiple} selectedKeys={mergedSelectKeys} - onItemSelect={onInternalSelect} - onItemDeselect={onInternalDeselect} - // Click - onItemClick={onInternalClick} // Popup subMenuOpenDelay={subMenuOpenDelay} subMenuCloseDelay={subMenuCloseDelay} forceSubMenuRender={forceSubMenuRender} builtinPlacements={builtinPlacements} triggerSubMenuAction={triggerSubMenuAction} + onItemClick={onInternalClick} onOpenChange={onInternalOpenChange} getPopupContainer={getInternalPopupContainer} > diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index 29e3c774..4e414d9c 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -7,12 +7,12 @@ import type { MenuClickEventHandler, MenuInfo, MenuHoverEventHandler, - SelectEventHandler, - SelectInfo, + RenderIconType, } from './interface'; import { MenuContext } from './context'; import useActive from './hooks/useActive'; import { warnItemProp } from './utils/warnUtil'; +import Icon from './Icon'; export interface MenuItemProps extends Omit< @@ -26,6 +26,8 @@ export interface MenuItemProps disabled?: boolean; + itemIcon?: RenderIconType; + /** @deprecated No place to use this. Should remove */ attribute?: Record; @@ -33,23 +35,10 @@ export interface MenuItemProps onMouseEnter?: MenuHoverEventHandler; onMouseLeave?: MenuHoverEventHandler; - // >>>>> Selection - onSelect?: SelectEventHandler; - onDeselect?: SelectEventHandler; - // >>>>> Events onClick?: MenuClickEventHandler; - // onItemHover?: HoverEventHandler; - - // parentMenu?: React.ReactInstance; - // onDestroy?: DestroyEventHandler; - - // multiple?: boolean; - // isSelected?: boolean; // manualRef?: LegacyFunctionRef; - // itemIcon?: RenderIconType; - // role?: string; // inlineIndent?: number; // level?: number; // direction?: 'ltr' | 'rtl'; @@ -57,6 +46,9 @@ export interface MenuItemProps // No need anymore // mode?: MenuMode; + // >>>>>>>>>>>>>> Next round + // onDestroy?: DestroyEventHandler; + // >>>>>>>>>>>>>> Useless props // rootPrefixCls?: string; // active?: boolean; @@ -95,15 +87,12 @@ const MenuItem = (props: MenuItemProps) => { className, eventKey, disabled, + itemIcon, // Active onMouseEnter, onMouseLeave, - // Select - onSelect, - onDeselect, - onClick, } = props; @@ -113,11 +102,7 @@ const MenuItem = (props: MenuItemProps) => { parentKeys, // Select - selectable, - multiple, selectedKeys, - onItemSelect, - onItemDeselect, // Path registerPath, @@ -154,42 +139,6 @@ const MenuItem = (props: MenuItemProps) => { // ============================ Select ============================ const selected = selectedKeys.includes(eventKey); - // Events - const triggerSelection = (info: MenuInfo) => { - if (!selectable) { - return; - } - - // Insert or Remove - const { key: targetKey } = info; - const exist = selectedKeys.includes(targetKey); - let newSelectKeys: string[]; - - if (exist) { - newSelectKeys = selectedKeys.filter(key => key !== targetKey); - } else if (multiple) { - newSelectKeys = [...selectedKeys, targetKey]; - } else { - newSelectKeys = [targetKey]; - } - - // Trigger event - const selectInfo: SelectInfo = { - ...info, - selectedKeys: newSelectKeys, - }; - - const outerInfo = warnItemProp(selectInfo); - - if (exist) { - onItemDeselect(selectInfo); - onSelect?.(outerInfo); - } else { - onItemSelect(selectInfo); - onDeselect?.(outerInfo); - } - }; - // ============================ Events ============================ const onInternalClick: React.MouseEventHandler = e => { if (disabled) { @@ -200,7 +149,6 @@ const MenuItem = (props: MenuItemProps) => { onClick?.(warnItemProp(info)); onItemClick(info); - triggerSelection(info); }; // ============================ Effect ============================ @@ -217,6 +165,7 @@ const MenuItem = (props: MenuItemProps) => { return ( { [`${itemCls}-selected`]: selected, [`${itemCls}-disabled`]: disabled, })} - role="menuitem" onClick={onInternalClick} > {children} + ); }; diff --git a/src/SubMenu.tsx b/src/SubMenu.tsx index b89d0dc3..3c595430 100644 --- a/src/SubMenu.tsx +++ b/src/SubMenu.tsx @@ -44,9 +44,7 @@ export interface SubMenuProps { // multiple?: boolean; // active?: boolean; // TODO: remove // onItemHover?: HoverEventHandler; - // onSelect?: SelectEventHandler; // triggerSubMenuAction?: TriggerSubMenuAction; - // onDeselect?: SelectEventHandler; // onDestroy?: DestroyEventHandler; // onTitleMouseEnter?: MenuHoverEventHandler; // onTitleMouseLeave?: MenuHoverEventHandler; diff --git a/src/context.tsx b/src/context.tsx index 440d62bd..f1d931eb 100644 --- a/src/context.tsx +++ b/src/context.tsx @@ -6,7 +6,6 @@ import type { BuiltinPlacements, MenuClickEventHandler, MenuMode, - SelectEventHandler, TriggerSubMenuAction, } from './interface'; import type { PathHookRet } from './hooks/usePathData'; @@ -25,11 +24,7 @@ export interface MenuContextProps extends PathHookRet { onInactive: (key: string) => void; // Selection - selectable?: boolean; - multiple?: boolean; selectedKeys: string[]; - onItemSelect?: SelectEventHandler; - onItemDeselect?: SelectEventHandler; // Popup subMenuOpenDelay: number; From 29f9102a6a504f0b2bd34840f73cd489654df140 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 16 Apr 2021 17:38:28 +0800 Subject: [PATCH 17/93] all rtl inlineIndent --- src/Icon.tsx | 2 +- src/Menu.tsx | 12 +++++++++--- src/MenuItem.tsx | 15 ++++++++++++--- src/MenuItemGroup.tsx | 21 ++++++++------------- src/SubMenu.tsx | 33 ++++++++++++++++++++++++--------- src/context.tsx | 7 ++++++- src/hooks/useDirectionStyle.ts | 17 +++++++++++++++++ src/utils/nodeUtil.ts | 5 +---- 8 files changed, 78 insertions(+), 34 deletions(-) create mode 100644 src/hooks/useDirectionStyle.ts diff --git a/src/Icon.tsx b/src/Icon.tsx index 2a2bcd52..fce0c4f4 100644 --- a/src/Icon.tsx +++ b/src/Icon.tsx @@ -22,5 +22,5 @@ export default function Icon({ icon, props, children }: IconProps) { iconNode = icon as React.ReactElement; } - return iconNode || children; + return iconNode || children || null; } diff --git a/src/Menu.tsx b/src/Menu.tsx index ece1f14c..222694c1 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -50,6 +50,9 @@ export interface MenuProps onSelect?: SelectEventHandler; onDeselect?: SelectEventHandler; + // Level + inlineIndent?: number; + // Motion /** Menu motion define */ motion?: CSSMotionProps; @@ -70,8 +73,6 @@ export interface MenuProps // onDestroy?: DestroyEventHandler; - // level?: number; - // itemIcon?: RenderIconType; // expandIcon?: RenderIconType; // overflowedIndicator?: React.ReactNode; @@ -119,6 +120,9 @@ const Menu: React.FC = ({ onSelect, onDeselect, + // Level + inlineIndent = 24, + // Motion motion, @@ -133,7 +137,7 @@ const Menu: React.FC = ({ onClick, onOpenChange, }) => { - const childList: React.ReactElement[] = parseChildren(children); + const childList: React.ReactElement[] = parseChildren(children, EMPTY_LIST); // ========================= Open ========================= const [mergedOpenKeys, setMergedOpenKeys] = useMergedState( @@ -279,6 +283,8 @@ const Menu: React.FC = ({ onInactive={onInactive} // Selection selectedKeys={mergedSelectKeys} + // Level + inlineIndent={inlineIndent} // Popup subMenuOpenDelay={subMenuOpenDelay} subMenuCloseDelay={subMenuCloseDelay} diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index 4e414d9c..f1eae89f 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -13,6 +13,7 @@ import { MenuContext } from './context'; import useActive from './hooks/useActive'; import { warnItemProp } from './utils/warnUtil'; import Icon from './Icon'; +import useDirectionStyle from './hooks/useDirectionStyle'; export interface MenuItemProps extends Omit< @@ -39,8 +40,7 @@ export interface MenuItemProps onClick?: MenuClickEventHandler; // manualRef?: LegacyFunctionRef; - // inlineIndent?: number; - // level?: number; + // direction?: 'ltr' | 'rtl'; // No need anymore @@ -83,11 +83,13 @@ class LegacyMenuItem extends React.Component { */ const MenuItem = (props: MenuItemProps) => { const { - children, + style, className, + eventKey, disabled, itemIcon, + children, // Active onMouseEnter, @@ -139,6 +141,9 @@ const MenuItem = (props: MenuItemProps) => { // ============================ Select ============================ const selected = selectedKeys.includes(eventKey); + // ======================== DirectionStyle ======================== + const directionStyle = useDirectionStyle(connectedKeys); + // ============================ Events ============================ const onInternalClick: React.MouseEventHandler = e => { if (disabled) { @@ -169,6 +174,10 @@ const MenuItem = (props: MenuItemProps) => { {...props} {...activeProps} component="li" + style={{ + ...directionStyle, + ...style, + }} className={classNames(itemCls, className, { [`${itemCls}-active`]: active, [`${itemCls}-selected`]: selected, diff --git a/src/MenuItemGroup.tsx b/src/MenuItemGroup.tsx index fb17dd13..9960cad9 100644 --- a/src/MenuItemGroup.tsx +++ b/src/MenuItemGroup.tsx @@ -8,30 +8,25 @@ export interface MenuItemGroupProps { title?: React.ReactNode; children?: React.ReactNode; - // disabled?: boolean; - // renderMenuItem?: ( - // item: React.ReactElement, - // index: number, - // key: string, - // ) => React.ReactElement; - // index?: number; - // subMenuKey?: string; - // rootPrefixCls?: string; - // onClick?: MenuClickEventHandler; - // direction?: 'ltr' | 'rtl'; + /** @private Internal filled key. Do not set it directly */ + eventKey?: string; } export default function MenuItemGroup({ className, title, + eventKey, children, ...restProps }: MenuItemGroupProps) { - const { prefixCls } = React.useContext(MenuContext); + const { prefixCls, parentKeys } = React.useContext(MenuContext); const groupPrefixCls = `${prefixCls}-item-group`; - const childList: React.ReactElement[] = parseChildren(children); + const childList: React.ReactElement[] = parseChildren(children, [ + ...parentKeys, + eventKey, + ]); return (
      • >>> Title click const onInternalTitleClick: React.MouseEventHandler = e => { @@ -194,6 +202,7 @@ export default function SubMenu(props: SubMenuProps) { // >>>>> Title let titleNode: React.ReactElement = (
        {titleNode} @@ -255,9 +270,9 @@ export default function SubMenu(props: SubMenuProps) { {/* Inline mode */} {mode === 'inline' && ( - {({ className, style }) => { + {({ className: motionClassName, style: motionStyle }) => { return ( - + {childList} ); diff --git a/src/context.tsx b/src/context.tsx index f1d931eb..a06c371a 100644 --- a/src/context.tsx +++ b/src/context.tsx @@ -14,7 +14,6 @@ export interface MenuContextProps extends PathHookRet { prefixCls: string; mode: MenuMode; openKeys: string[]; - motion?: CSSMotionProps; parentKeys: string[]; rtl?: boolean; @@ -26,6 +25,12 @@ export interface MenuContextProps extends PathHookRet { // Selection selectedKeys: string[]; + // Level + inlineIndent: number; + + // Motion + motion?: CSSMotionProps; + // Popup subMenuOpenDelay: number; subMenuCloseDelay: number; diff --git a/src/hooks/useDirectionStyle.ts b/src/hooks/useDirectionStyle.ts new file mode 100644 index 00000000..35249c99 --- /dev/null +++ b/src/hooks/useDirectionStyle.ts @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { MenuContext } from '../context'; + +export default function useDirectionStyle( + keyPath: string[], +): React.CSSProperties { + const { mode, rtl, inlineIndent } = React.useContext(MenuContext); + + if (mode !== 'inline') { + return null; + } + + const len = keyPath.length; + return rtl + ? { paddingRight: len * inlineIndent } + : { paddingLeft: len * inlineIndent }; +} diff --git a/src/utils/nodeUtil.ts b/src/utils/nodeUtil.ts index 8d2df9b7..d53620c4 100644 --- a/src/utils/nodeUtil.ts +++ b/src/utils/nodeUtil.ts @@ -1,10 +1,7 @@ import * as React from 'react'; import toArray from 'rc-util/lib/Children/toArray'; -export function parseChildren( - children: React.ReactNode, - keyPath: string[] = [], -) { +export function parseChildren(children: React.ReactNode, keyPath: string[]) { return toArray(children).map((child, index) => { if (React.isValidElement(child)) { let { key } = child; From 8ded9aeea49ad84ce362ea8b9b8280e46716541d Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 16 Apr 2021 17:50:15 +0800 Subject: [PATCH 18/93] hover event --- src/MenuItem.tsx | 12 ------------ src/SubMenu.tsx | 49 ++++++++++++++++++++++++++---------------------- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index f1eae89f..ac6e2427 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -39,20 +39,8 @@ export interface MenuItemProps // >>>>> Events onClick?: MenuClickEventHandler; - // manualRef?: LegacyFunctionRef; - - // direction?: 'ltr' | 'rtl'; - - // No need anymore - // mode?: MenuMode; - // >>>>>>>>>>>>>> Next round // onDestroy?: DestroyEventHandler; - - // >>>>>>>>>>>>>> Useless props - // rootPrefixCls?: string; - // active?: boolean; - // selectedKeys?: string[]; } // Since Menu event provide the `info.item` which point to the MenuItem node instance. diff --git a/src/SubMenu.tsx b/src/SubMenu.tsx index 78e37964..6979458c 100644 --- a/src/SubMenu.tsx +++ b/src/SubMenu.tsx @@ -42,30 +42,18 @@ export interface SubMenuProps { // >>>>> Events onClick?: MenuClickEventHandler; onTitleClick?: (info: MenuTitleInfo) => void; + onTitleMouseEnter?: MenuHoverEventHandler; + onTitleMouseLeave?: MenuHoverEventHandler; - // onOpenChange?: OpenEventHandler; - // rootPrefixCls?: string; - // multiple?: boolean; - // active?: boolean; // TODO: remove - // onItemHover?: HoverEventHandler; - // triggerSubMenuAction?: TriggerSubMenuAction; - // onDestroy?: DestroyEventHandler; - // onTitleMouseEnter?: MenuHoverEventHandler; - // onTitleMouseLeave?: MenuHoverEventHandler; // popupOffset?: number[]; - // isOpen?: boolean; - // store?: MiniStore; - // mode?: MenuMode; - // manualRef?: LegacyFunctionRef; - // level?: number; - // subMenuOpenDelay?: number; - // subMenuCloseDelay?: number; // forceSubMenuRender?: boolean; // builtinPlacements?: BuiltinPlacements; // popupClassName?: string; // motion?: CSSMotionProps; // direction?: 'ltr' | 'rtl'; + // >>>>>>>>>>>>>>>>>>>>> Next Round <<<<<<<<<<<<<<<<<<<<<<< + // onDestroy?: DestroyEventHandler; // >>>>>>>>>>>>>>>>>>> Useless content <<<<<<<<<<<<<<<<<<<<< // parentMenu?: React.ReactElement & { @@ -92,13 +80,13 @@ export default function SubMenu(props: SubMenuProps) { // Icons expandIcon, - // Active - onMouseEnter, - onMouseLeave, - // Events onClick, + onMouseEnter, + onMouseLeave, onTitleClick, + onTitleMouseEnter, + onTitleMouseLeave, } = props; const { @@ -148,8 +136,8 @@ export default function SubMenu(props: SubMenuProps) { const { active, ...activeProps } = useActive( eventKey, disabled, - onMouseEnter, - onMouseLeave, + onTitleMouseEnter, + onTitleMouseLeave, ); const mergedActive = active || keyInPath([activeKey], connectedKeys); @@ -187,6 +175,23 @@ export default function SubMenu(props: SubMenuProps) { onOpenChange(eventKey, newVisible); }; + // >>>>> Hover + const hoverProps: React.HTMLAttributes = {}; + if (!disabled) { + hoverProps.onMouseEnter = domEvent => { + onMouseEnter?.({ + key: eventKey, + domEvent, + }); + }; + hoverProps.onMouseLeave = domEvent => { + onMouseLeave?.({ + key: eventKey, + domEvent, + }); + }; + } + // =============================== Effect =============================== // Path register React.useEffect(() => { From 8f169660bda3017a9c6f88c4221c855d9b7c2bac Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 16 Apr 2021 18:02:35 +0800 Subject: [PATCH 19/93] popup data --- src/MenuItem.tsx | 14 +++++++++----- src/PopupTrigger.tsx | 15 ++++++++++++--- src/SubMenu.tsx | 26 ++++++++++---------------- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index ac6e2427..78304216 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -166,11 +166,15 @@ const MenuItem = (props: MenuItemProps) => { ...directionStyle, ...style, }} - className={classNames(itemCls, className, { - [`${itemCls}-active`]: active, - [`${itemCls}-selected`]: selected, - [`${itemCls}-disabled`]: disabled, - })} + className={classNames( + itemCls, + { + [`${itemCls}-active`]: active, + [`${itemCls}-selected`]: selected, + [`${itemCls}-disabled`]: disabled, + }, + className, + )} onClick={onInternalClick} > {children} diff --git a/src/PopupTrigger.tsx b/src/PopupTrigger.tsx index 3b4b9f48..210ed171 100644 --- a/src/PopupTrigger.tsx +++ b/src/PopupTrigger.tsx @@ -19,6 +19,8 @@ export interface PopupTriggerProps { visible: boolean; children: React.ReactElement; popup: React.ReactNode; + popupClassName?: string; + popupOffset?: number[]; disabled: boolean; onVisibleChange: (visible: boolean) => void; } @@ -28,6 +30,8 @@ export default function PopupTrigger({ visible, children, popup, + popupClassName, + popupOffset, disabled, mode, onVisibleChange, @@ -59,14 +63,19 @@ export default function PopupTrigger({ return ( >>>> Popup + popupClassName?: string; + popupOffset?: number[]; + // >>>>> Events onClick?: MenuClickEventHandler; onTitleClick?: (info: MenuTitleInfo) => void; onTitleMouseEnter?: MenuHoverEventHandler; onTitleMouseLeave?: MenuHoverEventHandler; - // popupOffset?: number[]; - // forceSubMenuRender?: boolean; - // builtinPlacements?: BuiltinPlacements; - // popupClassName?: string; - // motion?: CSSMotionProps; - // direction?: 'ltr' | 'rtl'; - // >>>>>>>>>>>>>>>>>>>>> Next Round <<<<<<<<<<<<<<<<<<<<<<< // onDestroy?: DestroyEventHandler; - // >>>>>>>>>>>>>>>>>>> Useless content <<<<<<<<<<<<<<<<<<<<< - - // parentMenu?: React.ReactElement & { - // isRootMenu: boolean; - // subMenuInstance: React.ReactInstance; - // }; - - // selectedKeys?: string[]; - // openKeys?: string[]; } export default function SubMenu(props: SubMenuProps) { @@ -80,6 +68,10 @@ export default function SubMenu(props: SubMenuProps) { // Icons expandIcon, + // Popup + popupClassName, + popupOffset, + // Events onClick, onMouseEnter, @@ -239,6 +231,8 @@ export default function SubMenu(props: SubMenuProps) { mode={mode} prefixCls={subMenuPrefixCls} visible={open} + popupClassName={popupClassName} + popupOffset={popupOffset} popup={{childList}} disabled={disabled} onVisibleChange={onPopupVisibleChange} From 8dde0abcf65db5011042b032c69273122cd6c8f3 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 19 Apr 2021 11:31:11 +0800 Subject: [PATCH 20/93] back of defaultMotions --- docs/examples/debug.tsx | 2 +- src/Menu.tsx | 44 +++++++++++++++++++++++++++-------------- src/PopupTrigger.tsx | 6 +++++- src/SubMenu.tsx | 28 ++++++++++++++++++++++---- src/context.tsx | 6 ++++++ src/utils/motionUtil.ts | 13 ++++++++++++ 6 files changed, 78 insertions(+), 21 deletions(-) create mode 100644 src/utils/motionUtil.ts diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx index fb6de7ff..7496b1ed 100644 --- a/docs/examples/debug.tsx +++ b/docs/examples/debug.tsx @@ -69,7 +69,7 @@ export default () => { mode={mode} style={{ width: mode === 'horizontal' ? undefined : 256 }} onClick={onRootClick} - motion={motionMap[mode]} + defaultMotions={motionMap} > Navigation One Next Item diff --git a/src/Menu.tsx b/src/Menu.tsx index 222694c1..0f69b1df 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -11,6 +11,7 @@ import type { SelectEventHandler, TriggerSubMenuAction, SelectInfo, + RenderIconType, } from './interface'; import MenuItem from './MenuItem'; import { parseChildren } from './utils/nodeUtil'; @@ -19,6 +20,14 @@ import useMemoCallback from './hooks/useMemoCallback'; import usePathData from './hooks/usePathData'; import { warnItemProp } from './utils/warnUtil'; +/** + * Menu modify after refactor: + * ## Remove + * - openTransitionName + * - openAnimation + * - onDestroy + */ + // optimize for render const EMPTY_LIST: string[] = []; @@ -54,8 +63,10 @@ export interface MenuProps inlineIndent?: number; // Motion - /** Menu motion define */ + /** Menu motion define. Use `defaultMotions` if you need config motion of each mode */ motion?: CSSMotionProps; + /** Default menu motion of each mode */ + defaultMotions?: Partial<{ [key in MenuMode | 'other']: CSSMotionProps }>; // Popup subMenuOpenDelay?: number; @@ -64,6 +75,10 @@ export interface MenuProps triggerSubMenuAction?: TriggerSubMenuAction; builtinPlacements?: BuiltinPlacements; + // Icon + itemIcon?: RenderIconType; + expandIcon?: RenderIconType; + // >>>>> Function getPopupContainer?: (node: HTMLElement) => HTMLElement; @@ -71,20 +86,8 @@ export interface MenuProps onClick?: MenuClickEventHandler; onOpenChange?: (openKeys: React.Key[]) => void; - // onDestroy?: DestroyEventHandler; - - // itemIcon?: RenderIconType; - // expandIcon?: RenderIconType; // overflowedIndicator?: React.ReactNode; - // /** Default menu motion of each mode */ - // defaultMotions?: Partial<{ [key in MenuMode | 'other']: CSSMotionProps }>; - - // /** @deprecated Please use `motion` instead */ - // openTransitionName?: string; - // /** @deprecated Please use `motion` instead */ - // openAnimation?: OpenAnimation; - // inlineCollapsed?: boolean; // /** SiderContextProps of layout in ant design */ @@ -125,11 +128,16 @@ const Menu: React.FC = ({ // Motion motion, + defaultMotions, // Popup triggerSubMenuAction = 'hover', builtinPlacements, + // Icon + itemIcon, + expandIcon, + // Function getPopupContainer, @@ -272,9 +280,11 @@ const Menu: React.FC = ({ prefixCls={prefixCls} mode={mode} openKeys={mergedOpenKeys} - motion={motion} parentKeys={EMPTY_LIST} rtl={direction === 'rtl'} + // Motion + motion={motion} + defaultMotions={defaultMotions} // Path {...pathData} // Active @@ -291,9 +301,13 @@ const Menu: React.FC = ({ forceSubMenuRender={forceSubMenuRender} builtinPlacements={builtinPlacements} triggerSubMenuAction={triggerSubMenuAction} + getPopupContainer={getInternalPopupContainer} + // Icon + itemIcon={itemIcon} + expandIcon={expandIcon} + // Events onItemClick={onInternalClick} onOpenChange={onInternalOpenChange} - getPopupContainer={getInternalPopupContainer} > {container} diff --git a/src/PopupTrigger.tsx b/src/PopupTrigger.tsx index 210ed171..91a9a3cf 100644 --- a/src/PopupTrigger.tsx +++ b/src/PopupTrigger.tsx @@ -5,6 +5,7 @@ import type { CSSMotionProps } from 'rc-motion'; import { MenuContext } from './context'; import { placements, placementsRtl } from './placements'; import type { MenuMode } from './interface'; +import { getMotion } from './utils/motionUtil'; const popupPlacementMap = { horizontal: 'bottomLeft', @@ -44,7 +45,10 @@ export default function PopupTrigger({ builtinPlacements, triggerSubMenuAction, forceSubMenuRender, + + // Motion motion, + defaultMotions, } = React.useContext(MenuContext); const placement = rtl @@ -54,7 +58,7 @@ export default function PopupTrigger({ const popupPlacement = popupPlacementMap[mode]; const mergedMotion: CSSMotionProps = { - ...motion, + ...getMotion(mode, motion, defaultMotions), leavedClassName: `${prefixCls}-hidden`, removeOnLeave: false, motionAppear: true, diff --git a/src/SubMenu.tsx b/src/SubMenu.tsx index 20688dc7..7431d0fc 100644 --- a/src/SubMenu.tsx +++ b/src/SubMenu.tsx @@ -18,6 +18,7 @@ import Icon from './Icon'; import useActive from './hooks/useActive'; import { warnItemProp } from './utils/warnUtil'; import useDirectionStyle from './hooks/useDirectionStyle'; +import { getMotion } from './utils/motionUtil'; export interface SubMenuProps { style?: React.CSSProperties; @@ -32,7 +33,7 @@ export interface SubMenuProps { eventKey?: string; // >>>>> Icon - // itemIcon?: RenderIconType; + itemIcon?: RenderIconType; expandIcon?: RenderIconType; // >>>>> Active @@ -66,6 +67,7 @@ export default function SubMenu(props: SubMenuProps) { children, // Icons + itemIcon, expandIcon, // Popup @@ -85,8 +87,12 @@ export default function SubMenu(props: SubMenuProps) { prefixCls, mode, openKeys, - motion, parentKeys, + forceSubMenuRender, + + // Motion + motion, + defaultMotions, // ActiveKey activeKey, @@ -99,6 +105,10 @@ export default function SubMenu(props: SubMenuProps) { unregisterPath, keyInPath, + // Icon + itemIcon: contextItemIcon, + expandIcon: contextExpandIcon, + // Events onItemClick, onOpenChange, @@ -112,6 +122,10 @@ export default function SubMenu(props: SubMenuProps) { eventKey, ]); + // ================================ Icon ================================ + const mergedItemIcon = itemIcon || contextItemIcon; + const mergedExpandIcon = expandIcon || contextExpandIcon; + // ============================== Children ============================== const childList: React.ReactElement[] = parseChildren( children, @@ -212,7 +226,7 @@ export default function SubMenu(props: SubMenuProps) { {/* Only non-horizontal mode shows the icon */} {mode !== 'horizontal' && ( + {({ className: motionClassName, style: motionStyle }) => { return ( diff --git a/src/context.tsx b/src/context.tsx index a06c371a..2c4ef714 100644 --- a/src/context.tsx +++ b/src/context.tsx @@ -6,6 +6,7 @@ import type { BuiltinPlacements, MenuClickEventHandler, MenuMode, + RenderIconType, TriggerSubMenuAction, } from './interface'; import type { PathHookRet } from './hooks/usePathData'; @@ -30,6 +31,7 @@ export interface MenuContextProps extends PathHookRet { // Motion motion?: CSSMotionProps; + defaultMotions?: Partial<{ [key in MenuMode | 'other']: CSSMotionProps }>; // Popup subMenuOpenDelay: number; @@ -38,6 +40,10 @@ export interface MenuContextProps extends PathHookRet { builtinPlacements?: BuiltinPlacements; triggerSubMenuAction?: TriggerSubMenuAction; + // Icon + itemIcon?: RenderIconType; + expandIcon?: RenderIconType; + // Function onItemClick: MenuClickEventHandler; onOpenChange: (key: string, open: boolean) => void; diff --git a/src/utils/motionUtil.ts b/src/utils/motionUtil.ts new file mode 100644 index 00000000..7fbb9ae8 --- /dev/null +++ b/src/utils/motionUtil.ts @@ -0,0 +1,13 @@ +import type { CSSMotionProps } from 'rc-motion'; + +export function getMotion( + mode: string, + motion?: CSSMotionProps, + defaultMotions: Record = {}, +) { + if (motion) { + return motion; + } + + return defaultMotions[mode] || defaultMotions.other || undefined; +} From cbbd6d758ce628ec1549d3b17e5c60dd182fbf56 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 19 Apr 2021 11:42:04 +0800 Subject: [PATCH 21/93] fix: Not lose active when hover disabled region --- src/SubMenu.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/SubMenu.tsx b/src/SubMenu.tsx index 7431d0fc..075e03cc 100644 --- a/src/SubMenu.tsx +++ b/src/SubMenu.tsx @@ -146,7 +146,11 @@ export default function SubMenu(props: SubMenuProps) { onTitleMouseLeave, ); - const mergedActive = active || keyInPath([activeKey], connectedKeys); + // Fallback of active check to avoid hover on menu title or disabled item + const [childrenActive, setChildrenActive] = React.useState(false); + + const mergedActive = + active || childrenActive || keyInPath([activeKey], connectedKeys); // ========================== DirectionStyle ========================== const directionStyle = useDirectionStyle(connectedKeys); @@ -279,6 +283,12 @@ export default function SubMenu(props: SubMenuProps) { }, )} role="menuitem" + onMouseEnter={() => { + setChildrenActive(true); + }} + onMouseLeave={() => { + setChildrenActive(false); + }} > {titleNode} From 72987e7755bb99f21cc4004aebd7b13efb0c141a Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 19 Apr 2021 15:07:15 +0800 Subject: [PATCH 22/93] feat: use rc-overflow --- assets/menu.less | 4 ++ docs/examples/debug.tsx | 85 +++++++++++++++++++++++------------------ package.json | 2 +- src/Menu.tsx | 54 +++++++++++++++++++++++++- src/MenuItem.tsx | 9 +++-- src/SubMenu.tsx | 15 +++++--- src/context.tsx | 5 +++ 7 files changed, 127 insertions(+), 47 deletions(-) diff --git a/assets/menu.less b/assets/menu.less index f84c2196..00c1c2a0 100644 --- a/assets/menu.less +++ b/assets/menu.less @@ -11,4 +11,8 @@ display: none; } } + + &-overflow-item { + flex: none; + } } diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx index 7496b1ed..91edcd23 100644 --- a/docs/examples/debug.tsx +++ b/docs/examples/debug.tsx @@ -43,6 +43,7 @@ const motionMap: Record = { export default () => { const [mode, setMode] = React.useState('horizontal'); + const [narrow, setNarrow] = React.useState(false); const onRootClick = (info: MenuInfo) => { console.log('Root Menu Item Click:', info); @@ -64,47 +65,57 @@ export default () => { +
        - - Navigation One - Next Item - - - Sub Item 1 - - Sub Item 2 - - - 2 - 3 - - - 4 - 5 - - - - - Disabled Item - - - + - - Disabled Sub Item 1 + Navigation One + Next Item + + + Sub Item 1 + + Sub Item 2 + + + + 2 + 3 + + + 4 + 5 + + + + + Disabled Item - - + + + + Disabled Sub Item 1 + + + + ); }; diff --git a/package.json b/package.json index 9ea6e1bd..30f84998 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "classnames": "2.x", "mini-store": "^3.0.1", "rc-motion": "^2.0.1", - "rc-overflow": "^1.2.0-alpha.0", + "rc-overflow": "^1.2.0-alpha.5", "rc-trigger": "^5.1.2", "rc-util": "^5.7.0", "resize-observer-polyfill": "^1.5.0", diff --git a/src/Menu.tsx b/src/Menu.tsx index 0f69b1df..4b17ce34 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -19,9 +19,13 @@ import MenuContextProvider from './context'; import useMemoCallback from './hooks/useMemoCallback'; import usePathData from './hooks/usePathData'; import { warnItemProp } from './utils/warnUtil'; +import SubMenu from './SubMenu'; /** * Menu modify after refactor: + * ## Add + * - disabled + * * ## Remove * - openTransitionName * - openAnimation @@ -38,6 +42,8 @@ export interface MenuProps mode?: MenuMode; children?: React.ReactNode; + disabled?: boolean; + /** direction of menu */ direction?: 'ltr' | 'rtl'; @@ -78,6 +84,7 @@ export interface MenuProps // Icon itemIcon?: RenderIconType; expandIcon?: RenderIconType; + moreIcon?: React.ReactNode; // >>>>> Function getPopupContainer?: (node: HTMLElement) => HTMLElement; @@ -104,6 +111,9 @@ const Menu: React.FC = ({ children, direction, + // Disabled + disabled, + // Open subMenuOpenDelay = 0.1, subMenuCloseDelay = 0.1, @@ -137,6 +147,7 @@ const Menu: React.FC = ({ // Icon itemIcon, expandIcon, + moreIcon = '...', // Function getPopupContainer, @@ -147,6 +158,9 @@ const Menu: React.FC = ({ }) => { const childList: React.ReactElement[] = parseChildren(children, EMPTY_LIST); + // ====================== Responsive======================= + const [visibleCount, setVisibleCount] = React.useState(0); + // ========================= Open ========================= const [mergedOpenKeys, setMergedOpenKeys] = useMergedState( defaultOpenKeys || [], @@ -254,8 +268,27 @@ const Menu: React.FC = ({ // ======================== Render ======================== + // >>>>> Children + const wrappedChildList = + mode !== 'horizontal' + ? childList + : // Need wrap for overflow dropdown that do not response for open + childList.map((child, index) => { + if (index < visibleCount) { + return child; + } + + return ( + + {child} + + ); + }); + + // >>>>> Container const container = ( = ({ style={style} role="menu" tabIndex={tabIndex} - data={childList} + data={wrappedChildList} renderRawItem={node => node} + renderRest={() => moreIcon} + renderRawRest={omitItems => { + // We use origin list since wrapped list use context to prevent open + const len = omitItems.length; + const originOmitItems = childList.slice(-len); + + return ( + + {originOmitItems} + + ); + }} + maxCount={mode === 'horizontal' ? 'responsive' : null} + onVisibleChange={newCount => { + setVisibleCount(newCount); + }} /> ); + // >>>>> Render return ( = ({ openKeys={mergedOpenKeys} parentKeys={EMPTY_LIST} rtl={direction === 'rtl'} + // Disabled + disabled={disabled} // Motion motion={motion} defaultMotions={defaultMotions} diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index 78304216..561589c4 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -91,6 +91,8 @@ const MenuItem = (props: MenuItemProps) => { onItemClick, parentKeys, + disabled: contextDisabled, + // Select selectedKeys, @@ -101,6 +103,7 @@ const MenuItem = (props: MenuItemProps) => { const itemCls = `${prefixCls}-item`; const legacyMenuItemRef = React.useRef(); + const mergedDisabled = contextDisabled || disabled; // ============================= Key ============================== const connectedKeys = React.useMemo(() => [...parentKeys, eventKey], [ @@ -121,7 +124,7 @@ const MenuItem = (props: MenuItemProps) => { // ============================ Active ============================ const { active, ...activeProps } = useActive( eventKey, - disabled, + mergedDisabled, onMouseEnter, onMouseLeave, ); @@ -134,7 +137,7 @@ const MenuItem = (props: MenuItemProps) => { // ============================ Events ============================ const onInternalClick: React.MouseEventHandler = e => { - if (disabled) { + if (mergedDisabled) { return; } @@ -171,7 +174,7 @@ const MenuItem = (props: MenuItemProps) => { { [`${itemCls}-active`]: active, [`${itemCls}-selected`]: selected, - [`${itemCls}-disabled`]: disabled, + [`${itemCls}-disabled`]: mergedDisabled, }, className, )} diff --git a/src/SubMenu.tsx b/src/SubMenu.tsx index 075e03cc..d643591e 100644 --- a/src/SubMenu.tsx +++ b/src/SubMenu.tsx @@ -90,6 +90,10 @@ export default function SubMenu(props: SubMenuProps) { parentKeys, forceSubMenuRender, + // Disabled + disabled: contextDisabled, + openDisabled, + // Motion motion, defaultMotions, @@ -115,6 +119,7 @@ export default function SubMenu(props: SubMenuProps) { } = React.useContext(MenuContext); const subMenuPrefixCls = `${prefixCls}-submenu`; + const mergedDisabled = contextDisabled || disabled; // ================================ Key ================================= const connectedKeys = React.useMemo(() => [...parentKeys, eventKey], [ @@ -133,7 +138,7 @@ export default function SubMenu(props: SubMenuProps) { ); // ================================ Open ================================ - const open = openKeys.includes(eventKey); + const open = !openDisabled && openKeys.includes(eventKey); // =============================== Select =============================== const childrenSelected = keyInPath(selectedKeys, connectedKeys); @@ -141,7 +146,7 @@ export default function SubMenu(props: SubMenuProps) { // =============================== Active =============================== const { active, ...activeProps } = useActive( eventKey, - disabled, + mergedDisabled, onTitleMouseEnter, onTitleMouseLeave, ); @@ -159,7 +164,7 @@ export default function SubMenu(props: SubMenuProps) { // >>>> Title click const onInternalTitleClick: React.MouseEventHandler = e => { // Skip if disabled - if (disabled) { + if (mergedDisabled) { return; } @@ -187,7 +192,7 @@ export default function SubMenu(props: SubMenuProps) { // >>>>> Hover const hoverProps: React.HTMLAttributes = {}; - if (!disabled) { + if (!mergedDisabled) { hoverProps.onMouseEnter = domEvent => { onMouseEnter?.({ key: eventKey, @@ -252,7 +257,7 @@ export default function SubMenu(props: SubMenuProps) { popupClassName={popupClassName} popupOffset={popupOffset} popup={{childList}} - disabled={disabled} + disabled={mergedDisabled} onVisibleChange={onPopupVisibleChange} > {titleNode} diff --git a/src/context.tsx b/src/context.tsx index 2c4ef714..8050c81e 100644 --- a/src/context.tsx +++ b/src/context.tsx @@ -18,6 +18,11 @@ export interface MenuContextProps extends PathHookRet { parentKeys: string[]; rtl?: boolean; + // Disabled + disabled?: boolean; + // Used for overflow only. Prevent hidden node trigger open + openDisabled?: boolean; + // Active activeKey: string; onActive: (key: string) => void; From ab607015b9bcb95b2816e8e6515879c2d4003d18 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 19 Apr 2021 15:51:01 +0800 Subject: [PATCH 23/93] fix: disabled should not trigger children active --- docs/examples/debug.tsx | 2 +- src/Menu.tsx | 2 +- src/SubMenu.tsx | 11 +++++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx index 91edcd23..27d4b62d 100644 --- a/docs/examples/debug.tsx +++ b/docs/examples/debug.tsx @@ -74,7 +74,7 @@ export default () => { -
        +
        = ({ const originOmitItems = childList.slice(-len); return ( - + {originOmitItems} ); diff --git a/src/SubMenu.tsx b/src/SubMenu.tsx index d643591e..58372251 100644 --- a/src/SubMenu.tsx +++ b/src/SubMenu.tsx @@ -154,6 +154,12 @@ export default function SubMenu(props: SubMenuProps) { // Fallback of active check to avoid hover on menu title or disabled item const [childrenActive, setChildrenActive] = React.useState(false); + const triggerChildrenActive = (newActive: boolean) => { + if (!mergedDisabled) { + setChildrenActive(newActive); + } + }; + const mergedActive = active || childrenActive || keyInPath([activeKey], connectedKeys); @@ -285,14 +291,15 @@ export default function SubMenu(props: SubMenuProps) { [`${subMenuPrefixCls}-open`]: open, [`${subMenuPrefixCls}-active`]: mergedActive, [`${subMenuPrefixCls}-selected`]: childrenSelected, + [`${subMenuPrefixCls}-disabled`]: mergedDisabled, }, )} role="menuitem" onMouseEnter={() => { - setChildrenActive(true); + triggerChildrenActive(true); }} onMouseLeave={() => { - setChildrenActive(false); + triggerChildrenActive(false); }} > {titleNode} From c920c064e91ff42c0c22a5af7322321920c474da Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 19 Apr 2021 17:03:18 +0800 Subject: [PATCH 24/93] chore: Cache inline open keys --- docs/examples/debug.tsx | 15 +++++++++- src/Menu.tsx | 63 +++++++++++++++++++++++++++++++---------- src/PopupTrigger.tsx | 19 +++++++++++-- src/context.tsx | 4 ++- 4 files changed, 82 insertions(+), 19 deletions(-) diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx index 27d4b62d..16747782 100644 --- a/docs/examples/debug.tsx +++ b/docs/examples/debug.tsx @@ -44,6 +44,7 @@ const motionMap: Record = { export default () => { const [mode, setMode] = React.useState('horizontal'); const [narrow, setNarrow] = React.useState(false); + const [inlineCollapsed, setInlineCollapsed] = React.useState(false); const onRootClick = (info: MenuInfo) => { console.log('Root Menu Item Click:', info); @@ -65,12 +66,23 @@ export default () => { + + {/* Narrow */} + + {/* InlineCollapsed */} +
        @@ -80,6 +92,7 @@ export default () => { style={{ width: mode === 'horizontal' ? undefined : 256 }} onClick={onRootClick} defaultMotions={motionMap} + inlineCollapsed={inlineCollapsed} > Navigation One Next Item diff --git a/src/Menu.tsx b/src/Menu.tsx index dc3bdb72..4af10057 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -39,7 +39,6 @@ export interface MenuProps extends Omit, 'onClick' | 'onSelect'> { prefixCls?: string; - mode?: MenuMode; children?: React.ReactNode; disabled?: boolean; @@ -47,6 +46,10 @@ export interface MenuProps /** direction of menu */ direction?: 'ltr' | 'rtl'; + // Mode + mode?: MenuMode; + inlineCollapsed?: boolean; + // Open control defaultOpenKeys?: string[]; openKeys?: string[]; @@ -84,7 +87,7 @@ export interface MenuProps // Icon itemIcon?: RenderIconType; expandIcon?: RenderIconType; - moreIcon?: React.ReactNode; + overflowedIndicator?: React.ReactNode; // >>>>> Function getPopupContainer?: (node: HTMLElement) => HTMLElement; @@ -93,10 +96,6 @@ export interface MenuProps onClick?: MenuClickEventHandler; onOpenChange?: (openKeys: React.Key[]) => void; - // overflowedIndicator?: React.ReactNode; - - // inlineCollapsed?: boolean; - // /** SiderContextProps of layout in ant design */ // siderCollapsed?: boolean; // collapsedWidth?: string | number; @@ -107,10 +106,13 @@ const Menu: React.FC = ({ style, className, tabIndex = 0, - mode = 'vertical', children, direction, + // Mode + mode = 'vertical', + inlineCollapsed, + // Disabled disabled, @@ -147,7 +149,7 @@ const Menu: React.FC = ({ // Icon itemIcon, expandIcon, - moreIcon = '...', + overflowedIndicator = '...', // Function getPopupContainer, @@ -158,7 +160,17 @@ const Menu: React.FC = ({ }) => { const childList: React.ReactElement[] = parseChildren(children, EMPTY_LIST); - // ====================== Responsive======================= + // ========================= Mode ========================= + const [mergedMode, mergedInlineCollapsed] = React.useMemo< + [MenuMode, boolean] + >(() => { + if (mode === 'inline' && inlineCollapsed) { + return ['vertical', inlineCollapsed]; + } + return [mode, false]; + }, [mode, inlineCollapsed]); + + // ====================== Responsive ====================== const [visibleCount, setVisibleCount] = React.useState(0); // ========================= Open ========================= @@ -169,6 +181,26 @@ const Menu: React.FC = ({ }, ); + const [inlineCacheOpenKeys, setInlineCacheOpenKeys] = React.useState( + mergedOpenKeys, + ); + + const isInlineMode = mergedMode === 'inline'; + + React.useEffect(() => { + if (isInlineMode) { + setInlineCacheOpenKeys(mergedOpenKeys); + } + }, [mergedOpenKeys, isInlineMode]); + + React.useEffect(() => { + if (isInlineMode) { + setMergedOpenKeys(inlineCacheOpenKeys); + } else { + setMergedOpenKeys([]); + } + }, [isInlineMode]); + // ========================= Path ========================= const pathData = usePathData(); @@ -270,7 +302,7 @@ const Menu: React.FC = ({ // >>>>> Children const wrappedChildList = - mode !== 'horizontal' + mergedMode !== 'horizontal' ? childList : // Need wrap for overflow dropdown that do not response for open childList.map((child, index) => { @@ -294,9 +326,10 @@ const Menu: React.FC = ({ className={classNames( prefixCls, `${prefixCls}-root`, - `${prefixCls}-${mode}`, + `${prefixCls}-${mergedMode}`, className, { + [`${prefixCls}-inline-collapsed`]: mergedInlineCollapsed, [`${prefixCls}-rlt`]: direction === 'rtl', }, )} @@ -305,19 +338,19 @@ const Menu: React.FC = ({ tabIndex={tabIndex} data={wrappedChildList} renderRawItem={node => node} - renderRest={() => moreIcon} + renderRest={() => overflowedIndicator} renderRawRest={omitItems => { // We use origin list since wrapped list use context to prevent open const len = omitItems.length; const originOmitItems = childList.slice(-len); return ( - + {originOmitItems} ); }} - maxCount={mode === 'horizontal' ? 'responsive' : null} + maxCount={mergedMode === 'horizontal' ? 'responsive' : null} onVisibleChange={newCount => { setVisibleCount(newCount); }} @@ -328,7 +361,7 @@ const Menu: React.FC = ({ return ( (); + React.useEffect(() => { + raf.cancel(visibleRef.current); + + visibleRef.current = raf(() => { + setInnerVisible(visible); + }); + }, [visible]); + return (
        diff --git a/src/Menu.tsx b/src/Menu.tsx index 5bdaf386..7de60adb 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -30,6 +30,8 @@ import SubMenu from './SubMenu'; * - openTransitionName * - openAnimation * - onDestroy + * - siderCollapsed: Seems antd do not use this prop + * - collapsedWidth: Seems this logic should be handle by antd Layout.Sider */ // optimize for render @@ -95,10 +97,6 @@ export interface MenuProps // >>>>> Events onClick?: MenuClickEventHandler; onOpenChange?: (openKeys: React.Key[]) => void; - - // /** SiderContextProps of layout in ant design */ - // siderCollapsed?: boolean; - // collapsedWidth?: string | number; } const Menu: React.FC = ({ diff --git a/src/SubMenu/InlineSubMenuList.tsx b/src/SubMenu/InlineSubMenuList.tsx new file mode 100644 index 00000000..053cf2b8 --- /dev/null +++ b/src/SubMenu/InlineSubMenuList.tsx @@ -0,0 +1,79 @@ +import * as React from 'react'; +import CSSMotion from 'rc-motion'; +import { getMotion } from '../utils/motionUtil'; +import MenuContextProvider, { MenuContext } from '../context'; +import SubMenuList from './SubMenuList'; +import type { MenuMode } from '../interface'; + +export interface InlineSubMenuListProps { + open: boolean; + children: React.ReactNode; +} + +export default function InlineSubMenuList({ + open, + children, +}: InlineSubMenuListProps) { + const fixedMode: MenuMode = 'inline'; + + const { + forceSubMenuRender, + motion, + defaultMotions, + parentKeys, + mode, + } = React.useContext(MenuContext); + + const [destroy, setDestroy] = React.useState(false); + + // Always use latest mode check + const sameModeRef = React.useRef(false); + sameModeRef.current = mode === fixedMode; + + // ================================= Effect ================================= + React.useEffect(() => { + if (sameModeRef.current) { + setDestroy(false); + } + }, [mode]); + + // ================================= Render ================================= + const mergedMotion = getMotion(fixedMode, motion, defaultMotions); + + // No need appear since nest inlineCollapse changed + if (parentKeys.length > 1) { + mergedMotion.motionAppear = false; + } + + // Hide inline list when mode changed and motion end + const originOnLeaveEnd = mergedMotion.onLeaveEnd; + mergedMotion.onLeaveEnd = (...args) => { + if (!sameModeRef.current) { + setDestroy(true); + } + + return originOnLeaveEnd?.(...args); + }; + + if (destroy) { + return null; + } + + return ( + + + {({ className: motionClassName, style: motionStyle }) => { + return ( + + {children} + + ); + }} + + + ); +} diff --git a/src/PopupTrigger.tsx b/src/SubMenu/PopupTrigger.tsx similarity index 92% rename from src/PopupTrigger.tsx rename to src/SubMenu/PopupTrigger.tsx index 828d6f45..b34877ad 100644 --- a/src/PopupTrigger.tsx +++ b/src/SubMenu/PopupTrigger.tsx @@ -3,10 +3,10 @@ import Trigger from 'rc-trigger'; import classNames from 'classnames'; import raf from 'rc-util/lib/raf'; import type { CSSMotionProps } from 'rc-motion'; -import { MenuContext } from './context'; -import { placements, placementsRtl } from './placements'; -import type { MenuMode } from './interface'; -import { getMotion } from './utils/motionUtil'; +import { MenuContext } from '../context'; +import { placements, placementsRtl } from '../placements'; +import type { MenuMode } from '../interface'; +import { getMotion } from '../utils/motionUtil'; const popupPlacementMap = { horizontal: 'bottomLeft', diff --git a/src/SubMenuList.tsx b/src/SubMenu/SubMenuList.tsx similarity index 93% rename from src/SubMenuList.tsx rename to src/SubMenu/SubMenuList.tsx index dd713bac..ca04b9ba 100644 --- a/src/SubMenuList.tsx +++ b/src/SubMenu/SubMenuList.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import classNames from 'classnames'; -import { MenuContext } from './context'; +import { MenuContext } from '../context'; export interface SubMenuListProps extends React.HTMLAttributes { diff --git a/src/SubMenu.tsx b/src/SubMenu/index.tsx similarity index 87% rename from src/SubMenu.tsx rename to src/SubMenu/index.tsx index dbbd3e2a..3e675f4f 100644 --- a/src/SubMenu.tsx +++ b/src/SubMenu/index.tsx @@ -1,24 +1,23 @@ import * as React from 'react'; import classNames from 'classnames'; -import CSSMotion from 'rc-motion'; import Overflow from 'rc-overflow'; import SubMenuList from './SubMenuList'; -import { parseChildren } from './utils/nodeUtil'; +import { parseChildren } from '../utils/nodeUtil'; import type { MenuClickEventHandler, MenuHoverEventHandler, MenuInfo, MenuTitleInfo, RenderIconType, -} from './interface'; -import MenuContextProvider, { MenuContext } from './context'; -import useMemoCallback from './hooks/useMemoCallback'; +} from '../interface'; +import MenuContextProvider, { MenuContext } from '../context'; +import useMemoCallback from '../hooks/useMemoCallback'; import PopupTrigger from './PopupTrigger'; -import Icon from './Icon'; -import useActive from './hooks/useActive'; -import { warnItemProp } from './utils/warnUtil'; -import useDirectionStyle from './hooks/useDirectionStyle'; -import { getMotion } from './utils/motionUtil'; +import Icon from '../Icon'; +import useActive from '../hooks/useActive'; +import { warnItemProp } from '../utils/warnUtil'; +import useDirectionStyle from '../hooks/useDirectionStyle'; +import InlineSubMenuList from './InlineSubMenuList'; export interface SubMenuProps { style?: React.CSSProperties; @@ -88,16 +87,11 @@ export default function SubMenu(props: SubMenuProps) { mode, openKeys, parentKeys, - forceSubMenuRender, // Disabled disabled: contextDisabled, openDisabled, - // Motion - motion, - defaultMotions, - // ActiveKey activeKey, @@ -314,21 +308,7 @@ export default function SubMenu(props: SubMenuProps) { {titleNode} {/* Inline mode */} - {mode === 'inline' && ( - - {({ className: motionClassName, style: motionStyle }) => { - return ( - - {childList} - - ); - }} - - )} + {childList}
        ); From f550aae33be4f5d61375ebeaaccb8459540d4f29 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 20 Apr 2021 10:42:30 +0800 Subject: [PATCH 28/93] fix: inline open logic --- src/SubMenu/InlineSubMenuList.tsx | 17 ++++++++++------- src/context.tsx | 5 ++++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/SubMenu/InlineSubMenuList.tsx b/src/SubMenu/InlineSubMenuList.tsx index 053cf2b8..ed84a786 100644 --- a/src/SubMenu/InlineSubMenuList.tsx +++ b/src/SubMenu/InlineSubMenuList.tsx @@ -17,6 +17,7 @@ export default function InlineSubMenuList({ const fixedMode: MenuMode = 'inline'; const { + prefixCls, forceSubMenuRender, motion, defaultMotions, @@ -38,7 +39,7 @@ export default function InlineSubMenuList({ }, [mode]); // ================================= Render ================================= - const mergedMotion = getMotion(fixedMode, motion, defaultMotions); + const mergedMotion = { ...getMotion(fixedMode, motion, defaultMotions) }; // No need appear since nest inlineCollapse changed if (parentKeys.length > 1) { @@ -46,13 +47,13 @@ export default function InlineSubMenuList({ } // Hide inline list when mode changed and motion end - const originOnLeaveEnd = mergedMotion.onLeaveEnd; - mergedMotion.onLeaveEnd = (...args) => { - if (!sameModeRef.current) { + const originOnVisibleChanged = mergedMotion.onVisibleChanged; + mergedMotion.onVisibleChanged = newVisible => { + if (!sameModeRef.current && !newVisible) { setDestroy(true); } - return originOnLeaveEnd?.(...args); + return originOnVisibleChanged?.(newVisible); }; if (destroy) { @@ -60,11 +61,13 @@ export default function InlineSubMenuList({ } return ( - + {({ className: motionClassName, style: motionStyle }) => { return ( diff --git a/src/context.tsx b/src/context.tsx index 20d2b7b7..4c9045fe 100644 --- a/src/context.tsx +++ b/src/context.tsx @@ -77,10 +77,12 @@ function mergeProps( export interface InheritableContextProps extends Partial { children?: React.ReactNode; + locked?: boolean; } export default function InheritableContextProvider({ children, + locked, ...restProps }: InheritableContextProps) { const context = React.useContext(MenuContext); @@ -88,7 +90,8 @@ export default function InheritableContextProvider({ const inheritableContext = useMemo( () => mergeProps(context, restProps), [context, restProps], - (prev, next) => prev[0] !== next[0] || !shallowEqual(prev[1], next[1]), + (prev, next) => + !locked && (prev[0] !== next[0] || !shallowEqual(prev[1], next[1])), ); return ( From 85a0c1d4ed84892cca19039a9bbc2368633ae497 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 20 Apr 2021 10:44:12 +0800 Subject: [PATCH 29/93] chore: Update comment --- src/SubMenu/InlineSubMenuList.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/SubMenu/InlineSubMenuList.tsx b/src/SubMenu/InlineSubMenuList.tsx index ed84a786..a470fa84 100644 --- a/src/SubMenu/InlineSubMenuList.tsx +++ b/src/SubMenu/InlineSubMenuList.tsx @@ -25,6 +25,8 @@ export default function InlineSubMenuList({ mode, } = React.useContext(MenuContext); + // We record `destroy` mark here since when mode change from `inline` to others. + // The inline list should remove when motion end. const [destroy, setDestroy] = React.useState(false); // Always use latest mode check @@ -32,6 +34,7 @@ export default function InlineSubMenuList({ sameModeRef.current = mode === fixedMode; // ================================= Effect ================================= + // Reset destroy state when mode change back React.useEffect(() => { if (sameModeRef.current) { setDestroy(false); From 7e9422cf0a46267f226f0a9eabd0e97b8d04bc28 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 21 Apr 2021 11:46:02 +0800 Subject: [PATCH 30/93] fix open logic --- docs/examples/debug.tsx | 11 +++++++++++ src/SubMenu/InlineSubMenuList.tsx | 12 +++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx index 89791176..d3f8d252 100644 --- a/docs/examples/debug.tsx +++ b/docs/examples/debug.tsx @@ -46,6 +46,7 @@ export default () => { const [mode, setMode] = React.useState('inline'); const [narrow, setNarrow] = React.useState(false); const [inlineCollapsed, setInlineCollapsed] = React.useState(false); + const [forceRender, setForceRender] = React.useState(true); const onRootClick = (info: MenuInfo) => { console.log('Root Menu Item Click:', info); @@ -85,10 +86,20 @@ export default () => { > Inline Collapsed: {String(inlineCollapsed)} + + {/* forceRender */} +
        { @@ -66,7 +68,7 @@ export default function InlineSubMenuList({ return ( Date: Wed, 21 Apr 2021 14:03:53 +0800 Subject: [PATCH 31/93] fix icon --- docs/examples/antd-switch.tsx | 1 - docs/examples/antd.tsx | 3 +-- docs/examples/custom-icon.tsx | 4 ++-- docs/examples/debug.tsx | 7 +++++-- src/Icon.tsx | 8 +++----- src/MenuItem.tsx | 14 +++++++++++++- src/SubMenu/index.tsx | 1 + src/interface.ts | 9 ++++++++- 8 files changed, 33 insertions(+), 14 deletions(-) diff --git a/docs/examples/antd-switch.tsx b/docs/examples/antd-switch.tsx index fdbcc534..2d1c5e2b 100644 --- a/docs/examples/antd-switch.tsx +++ b/docs/examples/antd-switch.tsx @@ -28,7 +28,6 @@ const Demo = () => { { console.error('Open Keys Changed:', keys); setOpenKey(keys); diff --git a/docs/examples/antd.tsx b/docs/examples/antd.tsx index fff46f30..ae81c174 100644 --- a/docs/examples/antd.tsx +++ b/docs/examples/antd.tsx @@ -95,8 +95,7 @@ const children2 = [ const customizeIndicator = Add More Items; -interface CommonMenuProps { - mode: MenuProps['mode']; +interface CommonMenuProps extends MenuProps { triggerSubMenuAction?: MenuProps['triggerSubMenuAction']; updateChildrenAndOverflowedIndicator?: boolean; } diff --git a/docs/examples/custom-icon.tsx b/docs/examples/custom-icon.tsx index 0ca18631..7b1f075b 100644 --- a/docs/examples/custom-icon.tsx +++ b/docs/examples/custom-icon.tsx @@ -1,9 +1,9 @@ /* eslint-disable no-console, no-param-reassign */ import * as React from 'react'; -import Menu, { SubMenu, Item as MenuItem, Divider } from 'rc-menu'; +import Menu, { SubMenu, Item as MenuItem, Divider } from '../../src'; import '../../assets/index.less'; -const getSvgIcon = (style = {}, text) => { +const getSvgIcon = (style = {}, text?: React.ReactNode) => { if (text) { return {text}; } diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx index d3f8d252..ef520858 100644 --- a/docs/examples/debug.tsx +++ b/docs/examples/debug.tsx @@ -47,6 +47,7 @@ export default () => { const [narrow, setNarrow] = React.useState(false); const [inlineCollapsed, setInlineCollapsed] = React.useState(false); const [forceRender, setForceRender] = React.useState(true); + const [openKeys, setOpenKeys] = React.useState([]); const onRootClick = (info: MenuInfo) => { console.log('Root Menu Item Click:', info); @@ -105,6 +106,8 @@ export default () => { onClick={onRootClick} defaultMotions={motionMap} inlineCollapsed={inlineCollapsed} + openKeys={openKeys} + onOpenChange={newOpenKeys => setOpenKeys(newOpenKeys)} > Navigation One Next Item @@ -114,7 +117,7 @@ export default () => { Sub Item 2 - + {/* 2 3 @@ -123,7 +126,7 @@ export default () => { 4 5 - + */} Disabled Item diff --git a/src/Icon.tsx b/src/Icon.tsx index fce0c4f4..8de7b5de 100644 --- a/src/Icon.tsx +++ b/src/Icon.tsx @@ -1,11 +1,9 @@ import * as React from 'react'; -import type { RenderIconType } from './interface'; -import type { SubMenuProps } from './SubMenu'; -import type { MenuItemProps } from './MenuItem'; +import type { RenderIconInfo, RenderIconType } from './interface'; export interface IconProps { icon?: RenderIconType; - props: (SubMenuProps & { isSubMenu: boolean }) | MenuItemProps; + props: RenderIconInfo; /** Fallback of icon if provided */ children?: React.ReactElement; } @@ -14,7 +12,7 @@ export default function Icon({ icon, props, children }: IconProps) { let iconNode: React.ReactElement; if (typeof icon === 'function') { - iconNode = React.createElement(this.props.expandIcon as any, { + iconNode = React.createElement(icon as any, { ...props, }); } else { diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index 561589c4..854f5a73 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -93,6 +93,9 @@ const MenuItem = (props: MenuItemProps) => { disabled: contextDisabled, + // Icon + itemIcon: contextItemIcon, + // Select selectedKeys, @@ -121,6 +124,9 @@ const MenuItem = (props: MenuItemProps) => { }; }; + // ============================= Icon ============================= + const mergedItemIcon = itemIcon || contextItemIcon; + // ============================ Active ============================ const { active, ...activeProps } = useActive( eventKey, @@ -181,7 +187,13 @@ const MenuItem = (props: MenuItemProps) => { onClick={onInternalClick} > {children} - + ); }; diff --git a/src/SubMenu/index.tsx b/src/SubMenu/index.tsx index 3e675f4f..3b46b5af 100644 --- a/src/SubMenu/index.tsx +++ b/src/SubMenu/index.tsx @@ -247,6 +247,7 @@ export default function SubMenu(props: SubMenuProps) { icon={mergedExpandIcon} props={{ ...props, + isOpen: open, // [Legacy] Not sure why need this mark isSubMenu: true, }} diff --git a/src/interface.ts b/src/interface.ts index aa7cef2f..d2682ead 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -7,9 +7,16 @@ export type BuiltinPlacements = Record; export type TriggerSubMenuAction = 'click' | 'hover'; +export interface RenderIconInfo { + isSelected?: boolean; + isOpen?: boolean; + isSubMenu?: boolean; + disabled?: boolean; +} + export type RenderIconType = | React.ReactNode - | ((props: any) => React.ReactNode); + | ((props: RenderIconInfo) => React.ReactNode); export interface MenuInfo { key: string; From 2db29b78698918f092bf96f16cf191d4e8f1f527 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 21 Apr 2021 22:45:15 +0800 Subject: [PATCH 32/93] update test --- docs/examples/debug.tsx | 10 ++- package.json | 2 +- src/Menu.tsx | 16 ++++- src/utils/motionUtil.ts | 8 ++- tests/Collapsed.spec.js | 137 +++++++++++----------------------------- 5 files changed, 65 insertions(+), 108 deletions(-) diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx index ef520858..51111fa0 100644 --- a/docs/examples/debug.tsx +++ b/docs/examples/debug.tsx @@ -8,8 +8,12 @@ import '../../assets/index.less'; import '../../assets/menu.less'; import type { MenuInfo } from '@/interface'; -const collapseNode = () => ({ height: 0 }); -const expandNode = node => ({ height: node.scrollHeight }); +const collapseNode = () => { + return { height: 0 }; +}; +const expandNode = node => { + return { height: node.scrollHeight }; +}; const horizontalMotion: CSSMotionProps = { motionName: 'rc-menu-open-slide-up', @@ -47,7 +51,7 @@ export default () => { const [narrow, setNarrow] = React.useState(false); const [inlineCollapsed, setInlineCollapsed] = React.useState(false); const [forceRender, setForceRender] = React.useState(true); - const [openKeys, setOpenKeys] = React.useState([]); + const [openKeys, setOpenKeys] = React.useState(['sub']); const onRootClick = (info: MenuInfo) => { console.log('Root Menu Item Click:', info); diff --git a/package.json b/package.json index 30f84998..89d94a42 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@babel/runtime": "^7.10.1", "classnames": "2.x", "mini-store": "^3.0.1", - "rc-motion": "^2.0.1", + "rc-motion": "^2.4.2", "rc-overflow": "^1.2.0-alpha.5", "rc-trigger": "^5.1.2", "rc-util": "^5.7.0", diff --git a/src/Menu.tsx b/src/Menu.tsx index 7de60adb..e2ac015f 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -157,6 +157,7 @@ const Menu: React.FC = ({ onOpenChange, }) => { const childList: React.ReactElement[] = parseChildren(children, EMPTY_LIST); + const [mounted, setMounted] = React.useState(false); // ========================= Mode ========================= const [mergedMode, mergedInlineCollapsed] = React.useMemo< @@ -198,7 +199,11 @@ const Menu: React.FC = ({ if (isInlineMode) { setMergedOpenKeys(inlineCacheOpenKeys); } else { - setMergedOpenKeys([]); + const empty = []; + setMergedOpenKeys(empty); + + // Trigger open event in case its in control + onOpenChange?.(empty); } }, [isInlineMode]); @@ -299,6 +304,11 @@ const Menu: React.FC = ({ const getInternalPopupContainer = useMemoCallback(getPopupContainer); + // ======================== Effect ======================== + React.useEffect(() => { + setMounted(true); + }, []); + // ======================== Render ======================== // >>>>> Children @@ -369,8 +379,8 @@ const Menu: React.FC = ({ // Disabled disabled={disabled} // Motion - motion={motion} - defaultMotions={defaultMotions} + motion={mounted ? motion : null} + defaultMotions={mounted ? defaultMotions : null} // Path {...pathData} // Active diff --git a/src/utils/motionUtil.ts b/src/utils/motionUtil.ts index 7fbb9ae8..09042d07 100644 --- a/src/utils/motionUtil.ts +++ b/src/utils/motionUtil.ts @@ -3,11 +3,15 @@ import type { CSSMotionProps } from 'rc-motion'; export function getMotion( mode: string, motion?: CSSMotionProps, - defaultMotions: Record = {}, + defaultMotions?: Record, ) { if (motion) { return motion; } - return defaultMotions[mode] || defaultMotions.other || undefined; + if (defaultMotions) { + return defaultMotions[mode] || defaultMotions.other; + } + + return undefined; } diff --git a/tests/Collapsed.spec.js b/tests/Collapsed.spec.js index a4979bd1..f8146434 100644 --- a/tests/Collapsed.spec.js +++ b/tests/Collapsed.spec.js @@ -14,36 +14,35 @@ describe('Menu.Collapsed', () => { jest.useRealTimers(); }); - it('should always follow openKeys when mode is switched', () => { + it.only('should always follow openKeys when mode is switched', () => { const wrapper = mount( - + {/* Option 1 Option 2 - + */} menu2 , ); - expect( - wrapper - .find('ul.rc-menu-sub') - .at(0) - .hasClass('rc-menu-hidden'), - ).toBe(false); - wrapper.setProps({ mode: 'vertical' }); - expect( - wrapper - .find('ul.rc-menu-sub') - .at(0) - .hasClass('rc-menu-hidden'), - ).toBe(false); - wrapper.setProps({ mode: 'inline' }); - expect( - wrapper - .find('ul.rc-menu-sub') - .at(0) - .hasClass('rc-menu-hidden'), - ).toBe(false); + // expect( + // wrapper.find('ul.rc-menu-sub').at(0).hasClass('rc-menu-hidden'), + // ).toBe(false); + + // wrapper.setProps({ mode: 'vertical' }); + // console.log(wrapper.debug()); + // expect( + // wrapper + // .find('ul.rc-menu-sub') + // .at(0) + // .hasClass('rc-menu-hidden'), + // ).toBe(false); + // wrapper.setProps({ mode: 'inline' }); + // expect( + // wrapper + // .find('ul.rc-menu-sub') + // .at(0) + // .hasClass('rc-menu-hidden'), + // ).toBe(false); }); it('should always follow openKeys when inlineCollapsed is switched', () => { @@ -59,16 +58,10 @@ describe('Menu.Collapsed', () => { , ); expect( - wrapper - .find('ul.rc-menu-sub') - .at(0) - .hasClass('rc-menu-inline'), + wrapper.find('ul.rc-menu-sub').at(0).hasClass('rc-menu-inline'), ).toBe(true); expect( - wrapper - .find('ul.rc-menu-sub') - .at(0) - .hasClass('rc-menu-hidden'), + wrapper.find('ul.rc-menu-sub').at(0).hasClass('rc-menu-hidden'), ).toBe(false); wrapper.setProps({ inlineCollapsed: true }); @@ -84,10 +77,7 @@ describe('Menu.Collapsed', () => { }); expect( - wrapper - .find('ul.rc-menu-root') - .at(0) - .hasClass('rc-menu-vertical'), + wrapper.find('ul.rc-menu-root').at(0).hasClass('rc-menu-vertical'), ).toBe(true); expect(wrapper.find('ul.rc-menu-sub').length).toBe(0); @@ -96,16 +86,10 @@ describe('Menu.Collapsed', () => { wrapper.update(); expect( - wrapper - .find('ul.rc-menu-sub') - .at(0) - .hasClass('rc-menu-inline'), + wrapper.find('ul.rc-menu-sub').at(0).hasClass('rc-menu-inline'), ).toBe(true); expect( - wrapper - .find('ul.rc-menu-sub') - .at(0) - .hasClass('rc-menu-hidden'), + wrapper.find('ul.rc-menu-sub').at(0).hasClass('rc-menu-hidden'), ).toBe(false); }); @@ -138,10 +122,7 @@ describe('Menu.Collapsed', () => { }); // Hover to show - wrapper - .find('.rc-menu-submenu-title') - .at(0) - .simulate('mouseEnter'); + wrapper.find('.rc-menu-submenu-title').at(0).simulate('mouseEnter'); jest.runAllTimers(); wrapper.update(); @@ -153,22 +134,13 @@ describe('Menu.Collapsed', () => { .hasClass('rc-menu-submenu-vertical'), ).toBe(true); expect( - wrapper - .find('.rc-menu-submenu') - .at(0) - .hasClass('rc-menu-submenu-open'), + wrapper.find('.rc-menu-submenu').at(0).hasClass('rc-menu-submenu-open'), ).toBe(true); expect( - wrapper - .find('ul.rc-menu-sub') - .at(0) - .hasClass('rc-menu-vertical'), + wrapper.find('ul.rc-menu-sub').at(0).hasClass('rc-menu-vertical'), ).toBe(true); expect( - wrapper - .find('ul.rc-menu-sub') - .at(0) - .hasClass('rc-menu-hidden'), + wrapper.find('ul.rc-menu-sub').at(0).hasClass('rc-menu-hidden'), ).toBe(false); }); @@ -198,42 +170,12 @@ describe('Menu.Collapsed', () => { , ); - expect( - wrapper - .find(MenuItem) - .at(0) - .getDOMNode().title, - ).toBe(''); - expect( - wrapper - .find(MenuItem) - .at(1) - .getDOMNode().title, - ).toBe('title'); - expect( - wrapper - .find(MenuItem) - .at(2) - .getDOMNode().title, - ).toBe(''); - expect( - wrapper - .find(MenuItem) - .at(3) - .getDOMNode().title, - ).toBe(''); - expect( - wrapper - .find(MenuItem) - .at(4) - .getDOMNode().title, - ).toBe(''); - expect( - wrapper - .find(MenuItem) - .at(4) - .getDOMNode().title, - ).toBe(''); + expect(wrapper.find(MenuItem).at(0).getDOMNode().title).toBe(''); + expect(wrapper.find(MenuItem).at(1).getDOMNode().title).toBe('title'); + expect(wrapper.find(MenuItem).at(2).getDOMNode().title).toBe(''); + expect(wrapper.find(MenuItem).at(3).getDOMNode().title).toBe(''); + expect(wrapper.find(MenuItem).at(4).getDOMNode().title).toBe(''); + expect(wrapper.find(MenuItem).at(4).getDOMNode().title).toBe(''); }); it('should use siderCollapsed when siderCollapsed is passed', () => { @@ -275,10 +217,7 @@ describe('Menu.Collapsed', () => { expect( wrapper.find('.rc-menu-item-selected').getDOMNode().textContent, ).toBe('Option 1'); - wrapper - .find('.rc-menu-item') - .at(1) - .simulate('click'); + wrapper.find('.rc-menu-item').at(1).simulate('click'); expect( wrapper.find('.rc-menu-item-selected').getDOMNode().textContent, ).toBe('Option 2'); From ceca5514df3a99e67461e7e11d92f87a27030d9d Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 21 Apr 2021 23:19:41 +0800 Subject: [PATCH 33/93] test: first test case --- src/hooks/usePathData.ts | 3 ++- src/utils/timeUtil.ts | 14 +++++++++++++ tests/Collapsed.spec.js | 43 ++++++++++++++++++++-------------------- 3 files changed, 38 insertions(+), 22 deletions(-) create mode 100644 src/utils/timeUtil.ts diff --git a/src/hooks/usePathData.ts b/src/hooks/usePathData.ts index aa4a94df..b711958c 100644 --- a/src/hooks/usePathData.ts +++ b/src/hooks/usePathData.ts @@ -1,4 +1,5 @@ import * as React from 'react'; +import { nextSlice } from '../utils/timeUtil'; const PATH_SPLIT = '__RC_UTIL_PATH_SPLIT__'; @@ -18,7 +19,7 @@ function usePathData() { updateRef.current += 1; const id = updateRef.current; - Promise.resolve().then(() => { + nextSlice(() => { if (id === updateRef.current) { forceUpdate({}); } diff --git a/src/utils/timeUtil.ts b/src/utils/timeUtil.ts new file mode 100644 index 00000000..6b381ebb --- /dev/null +++ b/src/utils/timeUtil.ts @@ -0,0 +1,14 @@ +import { act } from 'react-dom/test-utils'; + +export function nextSlice(callback: () => void) { + if (process.env.NODE_ENV === 'test') { + Promise.resolve().then(() => { + act(() => { + callback(); + }); + }); + } else { + /* istanbul ignore next */ + Promise.resolve().then(callback); + } +} diff --git a/tests/Collapsed.spec.js b/tests/Collapsed.spec.js index f8146434..ec6b894a 100644 --- a/tests/Collapsed.spec.js +++ b/tests/Collapsed.spec.js @@ -14,35 +14,36 @@ describe('Menu.Collapsed', () => { jest.useRealTimers(); }); - it.only('should always follow openKeys when mode is switched', () => { + it('should always follow openKeys when mode is switched', () => { const wrapper = mount( - {/* + Option 1 Option 2 - */} + menu2 , ); - // expect( - // wrapper.find('ul.rc-menu-sub').at(0).hasClass('rc-menu-hidden'), - // ).toBe(false); - // wrapper.setProps({ mode: 'vertical' }); - // console.log(wrapper.debug()); - // expect( - // wrapper - // .find('ul.rc-menu-sub') - // .at(0) - // .hasClass('rc-menu-hidden'), - // ).toBe(false); - // wrapper.setProps({ mode: 'inline' }); - // expect( - // wrapper - // .find('ul.rc-menu-sub') - // .at(0) - // .hasClass('rc-menu-hidden'), - // ).toBe(false); + // Inline + wrapper.update(); + expect( + wrapper.find('ul.rc-menu-sub').at(0).hasClass('rc-menu-hidden'), + ).toBe(false); + + // Vertical + wrapper.setProps({ mode: 'vertical' }); + wrapper.update(); + expect( + wrapper.find('ul.rc-menu-sub').at(0).hasClass('rc-menu-hidden'), + ).toBe(false); + + // Inline + wrapper.setProps({ mode: 'inline' }); + wrapper.update(); + expect( + wrapper.find('ul.rc-menu-sub').at(0).hasClass('rc-menu-hidden'), + ).toBe(false); }); it('should always follow openKeys when inlineCollapsed is switched', () => { From 67805d2ab49412aa5046bbf55c978df575425b6d Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 22 Apr 2021 10:03:34 +0800 Subject: [PATCH 34/93] fix: clean up timer logic --- docs/examples/debug.tsx | 4 +- package.json | 2 +- src/SubMenu/PopupTrigger.tsx | 6 +- tests/Collapsed.spec.js | 262 ++++++++++++++++++----------------- 4 files changed, 141 insertions(+), 133 deletions(-) diff --git a/docs/examples/debug.tsx b/docs/examples/debug.tsx index 51111fa0..1aedd61a 100644 --- a/docs/examples/debug.tsx +++ b/docs/examples/debug.tsx @@ -50,8 +50,8 @@ export default () => { const [mode, setMode] = React.useState('inline'); const [narrow, setNarrow] = React.useState(false); const [inlineCollapsed, setInlineCollapsed] = React.useState(false); - const [forceRender, setForceRender] = React.useState(true); - const [openKeys, setOpenKeys] = React.useState(['sub']); + const [forceRender, setForceRender] = React.useState(false); + const [openKeys, setOpenKeys] = React.useState([]); const onRootClick = (info: MenuInfo) => { console.log('Root Menu Item Click:', info); diff --git a/package.json b/package.json index 89d94a42..e76d8ef9 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@babel/runtime": "^7.10.1", "classnames": "2.x", "mini-store": "^3.0.1", - "rc-motion": "^2.4.2", + "rc-motion": "^2.4.3", "rc-overflow": "^1.2.0-alpha.5", "rc-trigger": "^5.1.2", "rc-util": "^5.7.0", diff --git a/src/SubMenu/PopupTrigger.tsx b/src/SubMenu/PopupTrigger.tsx index b34877ad..886b6149 100644 --- a/src/SubMenu/PopupTrigger.tsx +++ b/src/SubMenu/PopupTrigger.tsx @@ -72,11 +72,13 @@ export default function PopupTrigger({ // Delay to change visible const visibleRef = React.useRef(); React.useEffect(() => { - raf.cancel(visibleRef.current); - visibleRef.current = raf(() => { setInnerVisible(visible); }); + + return () => { + raf.cancel(visibleRef.current); + }; }, [visible]); return ( diff --git a/tests/Collapsed.spec.js b/tests/Collapsed.spec.js index ec6b894a..ceb06793 100644 --- a/tests/Collapsed.spec.js +++ b/tests/Collapsed.spec.js @@ -26,14 +26,12 @@ describe('Menu.Collapsed', () => { ); // Inline - wrapper.update(); expect( wrapper.find('ul.rc-menu-sub').at(0).hasClass('rc-menu-hidden'), ).toBe(false); // Vertical wrapper.setProps({ mode: 'vertical' }); - wrapper.update(); expect( wrapper.find('ul.rc-menu-sub').at(0).hasClass('rc-menu-hidden'), ).toBe(false); @@ -67,8 +65,10 @@ describe('Menu.Collapsed', () => { wrapper.setProps({ inlineCollapsed: true }); // 动画结束后套样式; - jest.runAllTimers(); - wrapper.update(); + act(() => { + jest.runAllTimers(); + wrapper.update(); + }); wrapper.simulate('transitionEnd', { propertyName: 'width' }); // Flush SubMenu raf state update @@ -83,8 +83,10 @@ describe('Menu.Collapsed', () => { expect(wrapper.find('ul.rc-menu-sub').length).toBe(0); wrapper.setProps({ inlineCollapsed: false }); - jest.runAllTimers(); - wrapper.update(); + act(() => { + jest.runAllTimers(); + wrapper.update(); + }); expect( wrapper.find('ul.rc-menu-sub').at(0).hasClass('rc-menu-inline'), @@ -111,8 +113,10 @@ describe('Menu.Collapsed', () => { // Do collapsed wrapper.setProps({ inlineCollapsed: true }); - jest.runAllTimers(); - wrapper.update(); + act(() => { + jest.runAllTimers(); + wrapper.update(); + }); wrapper.simulate('transitionEnd', { propertyName: 'width' }); @@ -125,8 +129,10 @@ describe('Menu.Collapsed', () => { // Hover to show wrapper.find('.rc-menu-submenu-title').at(0).simulate('mouseEnter'); - jest.runAllTimers(); - wrapper.update(); + act(() => { + jest.runAllTimers(); + wrapper.update(); + }); expect( wrapper @@ -145,127 +151,127 @@ describe('Menu.Collapsed', () => { ).toBe(false); }); - it('inlineCollapsed MenuItem Tooltip can be removed', () => { - const wrapper = mount( - node.parentNode} - > - item - - item - - - item - - - item - - - item - - - item - - , - ); - expect(wrapper.find(MenuItem).at(0).getDOMNode().title).toBe(''); - expect(wrapper.find(MenuItem).at(1).getDOMNode().title).toBe('title'); - expect(wrapper.find(MenuItem).at(2).getDOMNode().title).toBe(''); - expect(wrapper.find(MenuItem).at(3).getDOMNode().title).toBe(''); - expect(wrapper.find(MenuItem).at(4).getDOMNode().title).toBe(''); - expect(wrapper.find(MenuItem).at(4).getDOMNode().title).toBe(''); - }); + // it('inlineCollapsed MenuItem Tooltip can be removed', () => { + // const wrapper = mount( + // node.parentNode} + // > + // item + // + // item + // + // + // item + // + // + // item + // + // + // item + // + // + // item + // + // , + // ); + // expect(wrapper.find(MenuItem).at(0).getDOMNode().title).toBe(''); + // expect(wrapper.find(MenuItem).at(1).getDOMNode().title).toBe('title'); + // expect(wrapper.find(MenuItem).at(2).getDOMNode().title).toBe(''); + // expect(wrapper.find(MenuItem).at(3).getDOMNode().title).toBe(''); + // expect(wrapper.find(MenuItem).at(4).getDOMNode().title).toBe(''); + // expect(wrapper.find(MenuItem).at(4).getDOMNode().title).toBe(''); + // }); - it('should use siderCollapsed when siderCollapsed is passed', () => { - const wrapper = mount( - - - Option 1 - Option 2 - - menu2 - , - ); - expect(wrapper.instance().getInlineCollapsed()).toBe(false); - }); + // it('should use siderCollapsed when siderCollapsed is passed', () => { + // const wrapper = mount( + // + // + // Option 1 + // Option 2 + // + // menu2 + // , + // ); + // expect(wrapper.instance().getInlineCollapsed()).toBe(false); + // }); - // https://github.com/ant-design/ant-design/issues/18825 - // https://github.com/ant-design/ant-design/issues/8587 - it('should keep selectedKeys in state when collapsed to 0px', () => { - const wrapper = mount( - - Option 1 - Option 2 - - Option 4 - - , - ); - expect( - wrapper.find('.rc-menu-item-selected').getDOMNode().textContent, - ).toBe('Option 1'); - wrapper.find('.rc-menu-item').at(1).simulate('click'); - expect( - wrapper.find('.rc-menu-item-selected').getDOMNode().textContent, - ).toBe('Option 2'); - wrapper.setProps({ inlineCollapsed: true }); - jest.runAllTimers(); - wrapper.update(); - expect( - wrapper - .find('Trigger') - .map(node => node.prop('popupVisible')) - .findIndex(node => !!node), - ).toBe(-1); - wrapper.setProps({ inlineCollapsed: false }); - expect( - wrapper.find('.rc-menu-item-selected').getDOMNode().textContent, - ).toBe('Option 2'); - }); + // // https://github.com/ant-design/ant-design/issues/18825 + // // https://github.com/ant-design/ant-design/issues/8587 + // it('should keep selectedKeys in state when collapsed to 0px', () => { + // const wrapper = mount( + // + // Option 1 + // Option 2 + // + // Option 4 + // + // , + // ); + // expect( + // wrapper.find('.rc-menu-item-selected').getDOMNode().textContent, + // ).toBe('Option 1'); + // wrapper.find('.rc-menu-item').at(1).simulate('click'); + // expect( + // wrapper.find('.rc-menu-item-selected').getDOMNode().textContent, + // ).toBe('Option 2'); + // wrapper.setProps({ inlineCollapsed: true }); + // jest.runAllTimers(); + // wrapper.update(); + // expect( + // wrapper + // .find('Trigger') + // .map(node => node.prop('popupVisible')) + // .findIndex(node => !!node), + // ).toBe(-1); + // wrapper.setProps({ inlineCollapsed: false }); + // expect( + // wrapper.find('.rc-menu-item-selected').getDOMNode().textContent, + // ).toBe('Option 2'); + // }); - it('should hideMenu in initial state when collapsed to 0px', () => { - const wrapper = mount( - - Option 1 - Option 2 - - Option 4 - - , - ); - expect( - wrapper - .find('Trigger') - .map(node => node.prop('popupVisible')) - .findIndex(node => !!node), - ).toBe(-1); - wrapper.setProps({ inlineCollapsed: false }); - jest.runAllTimers(); - wrapper.update(); - expect( - wrapper.find('.rc-menu-item-selected').getDOMNode().textContent, - ).toBe('Option 1'); - }); + // it('should hideMenu in initial state when collapsed to 0px', () => { + // const wrapper = mount( + // + // Option 1 + // Option 2 + // + // Option 4 + // + // , + // ); + // expect( + // wrapper + // .find('Trigger') + // .map(node => node.prop('popupVisible')) + // .findIndex(node => !!node), + // ).toBe(-1); + // wrapper.setProps({ inlineCollapsed: false }); + // jest.runAllTimers(); + // wrapper.update(); + // expect( + // wrapper.find('.rc-menu-item-selected').getDOMNode().textContent, + // ).toBe('Option 1'); + // }); }); }); /* eslint-enable */ From 8bc6c0cee5433f1c514c229cec1fcda85f787f7a Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 22 Apr 2021 11:43:22 +0800 Subject: [PATCH 35/93] test: More test case --- src/Menu.tsx | 2 +- tests/Collapsed.spec.js | 224 +++++++++++++++++++--------------------- 2 files changed, 107 insertions(+), 119 deletions(-) diff --git a/src/Menu.tsx b/src/Menu.tsx index e2ac015f..75de75d2 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -30,7 +30,7 @@ import SubMenu from './SubMenu'; * - openTransitionName * - openAnimation * - onDestroy - * - siderCollapsed: Seems antd do not use this prop + * - siderCollapsed: Seems antd do not use this prop (Need test in antd) * - collapsedWidth: Seems this logic should be handle by antd Layout.Sider */ diff --git a/tests/Collapsed.spec.js b/tests/Collapsed.spec.js index ceb06793..952d533b 100644 --- a/tests/Collapsed.spec.js +++ b/tests/Collapsed.spec.js @@ -151,127 +151,115 @@ describe('Menu.Collapsed', () => { ).toBe(false); }); - // it('inlineCollapsed MenuItem Tooltip can be removed', () => { - // const wrapper = mount( - // node.parentNode} - // > - // item - // - // item - // - // - // item - // - // - // item - // - // - // item - // - // - // item - // - // , - // ); - // expect(wrapper.find(MenuItem).at(0).getDOMNode().title).toBe(''); - // expect(wrapper.find(MenuItem).at(1).getDOMNode().title).toBe('title'); - // expect(wrapper.find(MenuItem).at(2).getDOMNode().title).toBe(''); - // expect(wrapper.find(MenuItem).at(3).getDOMNode().title).toBe(''); - // expect(wrapper.find(MenuItem).at(4).getDOMNode().title).toBe(''); - // expect(wrapper.find(MenuItem).at(4).getDOMNode().title).toBe(''); - // }); + it('inlineCollapsed MenuItem Tooltip can be removed', () => { + const wrapper = mount( + node.parentNode} + > + item + + item + + + item + + + item + + + item + + + item + + , + ); + expect(wrapper.find(MenuItem).at(0).getDOMNode().title).toBe(''); + expect(wrapper.find(MenuItem).at(1).getDOMNode().title).toBe('title'); + expect(wrapper.find(MenuItem).at(2).getDOMNode().title).toBe(''); + expect(wrapper.find(MenuItem).at(3).getDOMNode().title).toBe(''); + expect(wrapper.find(MenuItem).at(4).getDOMNode().title).toBe(''); + expect(wrapper.find(MenuItem).at(4).getDOMNode().title).toBe(''); + }); - // it('should use siderCollapsed when siderCollapsed is passed', () => { - // const wrapper = mount( - // - // - // Option 1 - // Option 2 - // - // menu2 - // , - // ); - // expect(wrapper.instance().getInlineCollapsed()).toBe(false); - // }); + // https://github.com/ant-design/ant-design/issues/18825 + // https://github.com/ant-design/ant-design/issues/8587 + it('should keep selectedKeys in state when collapsed to 0px', () => { + const wrapper = mount( + + Option 1 + Option 2 + + Option 4 + + , + ); - // // https://github.com/ant-design/ant-design/issues/18825 - // // https://github.com/ant-design/ant-design/issues/8587 - // it('should keep selectedKeys in state when collapsed to 0px', () => { - // const wrapper = mount( - // - // Option 1 - // Option 2 - // - // Option 4 - // - // , - // ); - // expect( - // wrapper.find('.rc-menu-item-selected').getDOMNode().textContent, - // ).toBe('Option 1'); - // wrapper.find('.rc-menu-item').at(1).simulate('click'); - // expect( - // wrapper.find('.rc-menu-item-selected').getDOMNode().textContent, - // ).toBe('Option 2'); - // wrapper.setProps({ inlineCollapsed: true }); - // jest.runAllTimers(); - // wrapper.update(); - // expect( - // wrapper - // .find('Trigger') - // .map(node => node.prop('popupVisible')) - // .findIndex(node => !!node), - // ).toBe(-1); - // wrapper.setProps({ inlineCollapsed: false }); - // expect( - // wrapper.find('.rc-menu-item-selected').getDOMNode().textContent, - // ).toBe('Option 2'); - // }); + // Default + expect( + wrapper.find('li.rc-menu-item-selected').getDOMNode().textContent, + ).toBe('Option 1'); - // it('should hideMenu in initial state when collapsed to 0px', () => { - // const wrapper = mount( - // - // Option 1 - // Option 2 - // - // Option 4 - // - // , - // ); - // expect( - // wrapper - // .find('Trigger') - // .map(node => node.prop('popupVisible')) - // .findIndex(node => !!node), - // ).toBe(-1); - // wrapper.setProps({ inlineCollapsed: false }); - // jest.runAllTimers(); - // wrapper.update(); - // expect( - // wrapper.find('.rc-menu-item-selected').getDOMNode().textContent, - // ).toBe('Option 1'); - // }); + // Click to change select + wrapper.find('li.rc-menu-item').at(1).simulate('click'); + expect( + wrapper.find('li.rc-menu-item-selected').getDOMNode().textContent, + ).toBe('Option 2'); + + // Collapse it + wrapper.setProps({ inlineCollapsed: true }); + act(() => { + jest.runAllTimers(); + wrapper.update(); + }); + + // Open since controlled + expect(wrapper.find('Trigger').props().popupVisible).toBeTruthy(); + + // Expand it + wrapper.setProps({ inlineCollapsed: false }); + expect( + wrapper.find('li.rc-menu-item-selected').getDOMNode().textContent, + ).toBe('Option 2'); + }); + + it('should hideMenu in initial state when collapsed to 0px', () => { + const wrapper = mount( + + Option 1 + Option 2 + + Option 4 + + , + ); + expect( + wrapper + .find('Trigger') + .map(node => node.prop('popupVisible')) + .findIndex(node => !!node), + ).toBe(-1); + wrapper.setProps({ inlineCollapsed: false }); + jest.runAllTimers(); + wrapper.update(); + expect( + wrapper.find('.rc-menu-item-selected').getDOMNode().textContent, + ).toBe('Option 1'); + }); }); }); /* eslint-enable */ From cdabf9409e4d5bab4649fe1c06d808d5db43c83c Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 23 Apr 2021 10:39:25 +0800 Subject: [PATCH 36/93] Divider --- package.json | 2 +- src/Divider.tsx | 19 + src/Menu.tsx | 4 +- src/MenuItem.tsx | 5 +- src/SubMenu/index.tsx | 3 +- src/index.ts | 14 +- src/sugar/Divider.tsx | 4 - tests/Collapsed.spec.js | 20 +- tests/Menu.spec.js | 1507 ++++++++++++++++++++------------------- 9 files changed, 800 insertions(+), 778 deletions(-) create mode 100644 src/Divider.tsx delete mode 100644 src/sugar/Divider.tsx diff --git a/package.json b/package.json index e76d8ef9..58d0265b 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "classnames": "2.x", "mini-store": "^3.0.1", "rc-motion": "^2.4.3", - "rc-overflow": "^1.2.0-alpha.5", + "rc-overflow": "^1.2.0-alpha.7", "rc-trigger": "^5.1.2", "rc-util": "^5.7.0", "resize-observer-polyfill": "^1.5.0", diff --git a/src/Divider.tsx b/src/Divider.tsx new file mode 100644 index 00000000..191f4efb --- /dev/null +++ b/src/Divider.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import classNames from 'classnames'; +import { MenuContext } from './context'; + +export interface DividerProps { + className?: string; + style?: React.CSSProperties; +} + +export default function Divider({ className, style }: DividerProps) { + const { prefixCls } = React.useContext(MenuContext); + + return ( +
      • + ); +} diff --git a/src/Menu.tsx b/src/Menu.tsx index 75de75d2..5a1074b4 100644 --- a/src/Menu.tsx +++ b/src/Menu.tsx @@ -361,7 +361,9 @@ const Menu: React.FC = ({ ); }} - maxCount={mergedMode === 'horizontal' ? 'responsive' : null} + maxCount={ + mergedMode === 'horizontal' ? Overflow.RESPONSIVE : Overflow.INVALIDATE + } onVisibleChange={newCount => { setVisibleCount(newCount); }} diff --git a/src/MenuItem.tsx b/src/MenuItem.tsx index 854f5a73..1937a278 100644 --- a/src/MenuItem.tsx +++ b/src/MenuItem.tsx @@ -84,6 +84,8 @@ const MenuItem = (props: MenuItemProps) => { onMouseLeave, onClick, + + ...restProps } = props; const { @@ -168,9 +170,10 @@ const MenuItem = (props: MenuItemProps) => { { ).toBe('Option 2'); }); - it('should hideMenu in initial state when collapsed to 0px', () => { + it('should hideMenu in initial state when collapsed', () => { const wrapper = mount( Option 1 @@ -247,17 +246,16 @@ describe('Menu.Collapsed', () => { , ); - expect( - wrapper - .find('Trigger') - .map(node => node.prop('popupVisible')) - .findIndex(node => !!node), - ).toBe(-1); + + expect(wrapper.find('Trigger').props().popupVisible).toBeFalsy(); + wrapper.setProps({ inlineCollapsed: false }); - jest.runAllTimers(); - wrapper.update(); + act(() => { + jest.runAllTimers(); + wrapper.update(); + }); expect( - wrapper.find('.rc-menu-item-selected').getDOMNode().textContent, + wrapper.find('li.rc-menu-item-selected').getDOMNode().textContent, ).toBe('Option 1'); }); }); diff --git a/tests/Menu.spec.js b/tests/Menu.spec.js index 824686a9..4e9e66af 100644 --- a/tests/Menu.spec.js +++ b/tests/Menu.spec.js @@ -1,11 +1,11 @@ -/* eslint-disable no-undef, react/no-multi-comp, react/jsx-curly-brace-presence */ +/* eslint-disable no-undef, react/no-multi-comp, react/jsx-curly-brace-presence, max-classes-per-file */ import React from 'react'; import { render, mount } from 'enzyme'; import { renderToJson } from 'enzyme-to-json'; import KeyCode from 'rc-util/lib/KeyCode'; import Menu, { MenuItem, MenuItemGroup, SubMenu, Divider } from '../src'; -import * as mockedUtil from '../src/util'; -import { getMotion } from '../src/utils/legacyUtil'; +// import * as mockedUtil from '../src/util'; +// import { getMotion } from '../src/utils/legacyUtil'; describe('Menu', () => { describe('should render', () => { @@ -31,763 +31,766 @@ describe('Menu', () => { ); } - ['vertical', 'horizontal', 'inline'].forEach(mode => { + [ + 'vertical', + // 'horizontal', 'inline' + ].forEach(mode => { it(`${mode} menu correctly`, () => { const wrapper = render(createMenu({ mode })); expect(renderToJson(wrapper)).toMatchSnapshot(); }); - it(`${mode} menu with empty children without error`, () => { - expect(() => render({[]})).not.toThrow(); - }); - - it(`${mode} menu with undefined children without error`, () => { - expect(() => render()).not.toThrow(); - }); - - it(`${mode} menu that has a submenu with undefined children without error`, () => { - expect(() => - render( - - - , - ), - ).not.toThrow(); - }); - - it(`${mode} menu with rtl direction correctly`, () => { - const wrapper = mount(createMenu({ mode, direction: 'rtl' })); - expect(renderToJson(render(wrapper))).toMatchSnapshot(); - - expect( - wrapper - .find('ul') - .first() - .props().className, - ).toContain('-rtl'); - }); - }); - - it('should support Fragment', () => { - const wrapper = mount( - - - 6 - - 6 - <> - - 6 - - 6 - - , - ); - expect(render(wrapper)).toMatchSnapshot(); - }); - }); - - describe('render role listbox', () => { - function createMenu() { - return ( - - - 1 - - - 2 - - - 3 - - - ); - } - - it('renders menu correctly', () => { - const wrapper = render(createMenu()); - expect(renderToJson(wrapper)).toMatchSnapshot(); - }); - }); - - it('set activeKey', () => { - const wrapper = mount( - - 1 - 2 - , - ); - expect( - wrapper - .find('MenuItem') - .first() - .props().active, - ).toBe(true); - expect( - wrapper - .find('MenuItem') - .last() - .props().active, - ).toBe(false); - }); - - it('active first item', () => { - const wrapper = mount( - - 1 - 2 - , - ); - expect( - wrapper - .find('MenuItem') - .first() - .props().active, - ).toBe(true); - }); - - it('should render none menu item children', () => { - expect(() => { - mount( - - 1 - 2 - string - {'string'} - {null} - {undefined} - {12345} -
        - -
        , - ); - }).not.toThrow(); - }); - - it('select multiple items', () => { - const wrapper = mount( - - 1 - 2 - , - ); - wrapper - .find('MenuItem') - .first() - .simulate('click'); - wrapper - .find('MenuItem') - .last() - .simulate('click'); - - expect(wrapper.find('.rc-menu-item-selected').length).toBe(2); - }); - - it('can be controlled by selectedKeys', () => { - const wrapper = mount( - - 1 - 2 - , - ); - expect( - wrapper - .find('li') - .first() - .props().className, - ).toContain('-selected'); - wrapper.setProps({ selectedKeys: ['2'] }); - wrapper.update(); - expect( - wrapper - .find('li') - .last() - .props().className, - ).toContain('-selected'); - }); - - it('select default item', () => { - const wrapper = mount( - - 1 - 2 - , - ); - expect( - wrapper - .find('li') - .first() - .props().className, - ).toContain('-selected'); - }); - - it('issue https://github.com/ant-design/ant-design/issues/29429', () => { - // don't use selectedKeys as string - // it is a compatible feature for https://github.com/ant-design/ant-design/issues/29429 - const wrapper = mount( - - 1 - 2 - , - ); - expect( - wrapper - .find('li') - .at(0) - .props().className, - ).not.toContain('-selected'); - expect( - wrapper - .find('li') - .at(1) - .props().className, - ).toContain('-selected'); - }); - - it('can be controlled by openKeys', () => { - const wrapper = mount( - - - 1 - - - 2 - - , - ); - expect( - wrapper - .find('ul') - .first() - .props().className, - ).not.toContain('-hidden'); - wrapper.setProps({ openKeys: ['g2'] }); - expect( - wrapper - .find('ul') - .last() - .props().className, - ).not.toContain('-hidden'); - }); - - it('openKeys should allow to be empty', () => { - const wrapper = mount( - {}} - onOpenChange={() => {}} - openKeys={undefined} - selectedKeys={['1']} - mode="inline" - > - - - - 123123 - - - - , - ); - expect(wrapper).toBeTruthy(); - }); - - it('open default submenu', () => { - const wrapper = mount( - - - 1 - - - 2 - - , - ); - expect( - wrapper - .find('ul') - .first() - .props().className, - ).not.toContain('-hidden'); - }); - - it('fires select event', () => { - const handleSelect = jest.fn(); - const wrapper = mount( - - 1 - 2 - , - ); - wrapper - .find('MenuItem') - .first() - .simulate('click'); - expect(handleSelect.mock.calls[0][0].key).toBe('1'); - }); - - it('fires click event', () => { - const handleClick = jest.fn(); - const wrapper = mount( - - 1 - 2 - , - ); - wrapper - .find('MenuItem') - .first() - .simulate('click'); - expect(handleClick.mock.calls[0][0].key).toBe('1'); - }); - - it('fires deselect event', () => { - const handleDeselect = jest.fn(); - const wrapper = mount( - - 1 - 2 - , - ); - wrapper - .find('MenuItem') - .first() - .simulate('click') - .simulate('click'); - expect(handleDeselect.mock.calls[0][0].key).toBe('1'); - }); - - it('active by mouse enter', () => { - const wrapper = mount( - - item - disabled - item2 - , - ); - let menuItem = wrapper.find('MenuItem').last(); - menuItem.simulate('mouseEnter'); - menuItem = wrapper.find('MenuItem').last(); - expect(menuItem.props().active).toBe(true); - }); - - it('active by key down', () => { - const wrapper = mount( - - 1 - 2 - , - ); - - wrapper.simulate('keyDown', { keyCode: KeyCode.DOWN }); - expect( - wrapper - .find('MenuItem') - .at(1) - .props().active, - ).toBe(true); - }); - - it('keydown works when children change', () => { - class App extends React.Component { - state = { - items: [1, 2, 3], - }; - - render() { - return ( - - {this.state.items.map(i => ( - {i} - ))} - - ); - } - } - - const wrapper = mount(); - - wrapper.setState({ items: [0, 1] }); - - wrapper.find('Menu').simulate('keyDown', { keyCode: KeyCode.DOWN }); - expect( - wrapper - .find('MenuItem') - .at(0) - .props().active, - ).toBe(true); - - wrapper.find('Menu').simulate('keyDown', { keyCode: KeyCode.DOWN }); - expect( - wrapper - .find('MenuItem') - .at(1) - .props().active, - ).toBe(true); - }); - - it('active first item when children changes', () => { - class App extends React.Component { - state = { - items: ['foo'], - }; - - render() { - return ( - - {this.state.items.map(item => ( - {item} - ))} - - ); - } - } - - const wrapper = mount(); - - wrapper.setState({ items: ['bar', 'foo'] }); - wrapper.update(); - - expect( - wrapper - .find('li') - .first() - .hasClass('rc-menu-item-active'), - ).toBe(true); - }); - - it('should accept builtinPlacements', () => { - const builtinPlacements = { - leftTop: { - points: ['tr', 'tl'], - overflow: { - adjustX: 0, - adjustY: 0, - }, - offset: [0, 0], - }, - }; - - const wrapper = mount( - - menuItem - - menuItem - - , - ); - - expect(wrapper.find('Trigger').prop('builtinPlacements').leftTop).toEqual( - builtinPlacements.leftTop, - ); - }); - - describe('submenu mode', () => { - it('should use menu mode by default', () => { - const wrapper = mount( - - - menuItem - - , - ); - - expect( - wrapper - .find('SubMenu') - .first() - .prop('mode'), - ).toEqual('horizontal'); + // it(`${mode} menu with empty children without error`, () => { + // expect(() => render({[]})).not.toThrow(); + // }); + + // it(`${mode} menu with undefined children without error`, () => { + // expect(() => render()).not.toThrow(); + // }); + + // it(`${mode} menu that has a submenu with undefined children without error`, () => { + // expect(() => + // render( + // + // + // , + // ), + // ).not.toThrow(); + // }); + + // it(`${mode} menu with rtl direction correctly`, () => { + // const wrapper = mount(createMenu({ mode, direction: 'rtl' })); + // expect(renderToJson(render(wrapper))).toMatchSnapshot(); + + // expect( + // wrapper + // .find('ul') + // .first() + // .props().className, + // ).toContain('-rtl'); + // }); }); - it('should be able to customize SubMenu mode', () => { - const wrapper = mount( - - - menuItem - - , - ); - - expect( - wrapper - .find('SubMenu') - .first() - .prop('mode'), - ).toEqual('vertical-right'); - }); + // it('should support Fragment', () => { + // const wrapper = mount( + // + // + // 6 + // + // 6 + // <> + // + // 6 + // + // 6 + // + // , + // ); + // expect(render(wrapper)).toMatchSnapshot(); + // }); }); - describe('DOMWrap Allow Overflow', () => { - const overflowIndicatorSelector = 'SubMenu.rc-menu-overflowed-submenu'; - function createMenu(props) { - return ( - ... - } - {...props} - > - 1 - - 2 - - 3 - 4 - - ); - } - - let wrapper; - - it('getWidth should contain margin when includeMargin is set to true', () => { - const memorizedGetComputedStyle = window.getComputedStyle; - window.getComputedStyle = () => ({ - marginLeft: '10px', - marginRight: '10px', - }); - expect( - mockedUtil.getWidth( - { - getBoundingClientRect() { - return { width: 10 }; - }, - }, - true, - ), - ).toEqual(30); - window.getComputedStyle = memorizedGetComputedStyle; - }); - - it('should not include overflow indicator when having enough width', () => { - const indicatorWidth = 50; // actual width including 40 px padding, which will be 50; - const liWidths = [50, 50, 50, 50]; - const availableWidth = 250; - const widths = [...liWidths, indicatorWidth, availableWidth]; - let i = 0; - mockedUtil.getWidth = () => { - const id = i; - i += 1; - return widths[id]; - }; - wrapper = mount(createMenu()); - - // overflow indicator placeholder - expect( - wrapper - .find(overflowIndicatorSelector) - .at(4) - .prop('style'), - ).toEqual({ - visibility: 'hidden', - position: 'absolute', - }); - - // last overflow indicator should be hidden - expect( - wrapper - .find(overflowIndicatorSelector) - .at(3) - .prop('style'), - ).toEqual({ - display: 'none', - }); - - expect( - wrapper - .find('MenuItem li') - .at(0) - .prop('style'), - ).toEqual({}); - expect( - wrapper - .find('MenuItem li') - .at(1) - .prop('style'), - ).toEqual({}); - expect( - wrapper - .find('MenuItem li') - .at(2) - .prop('style'), - ).toEqual({}); - expect( - wrapper - .find('MenuItem li') - .at(3) - .prop('style'), - ).toEqual({}); - }); - - it('should include overflow indicator when having not enough width', () => { - const indicatorWidth = 5; // actual width including 40 px padding, which will be 45; - const liWidths = [50, 50, 50, 50]; - const availableWidth = 145; - const widths = [...liWidths, indicatorWidth, availableWidth]; - let i = 0; - mockedUtil.getWidth = () => { - const id = i; - i += 1; - return widths[id]; - }; - wrapper = mount(createMenu()); - - expect( - wrapper - .find('MenuItem li') - .at(0) - .prop('style'), - ).toEqual({}); - expect( - wrapper - .find('MenuItem li') - .at(1) - .prop('style'), - ).toEqual({}); - expect( - wrapper - .find('MenuItem li') - .at(2) - .prop('style'), - ).toEqual({ display: 'none' }); - expect( - wrapper - .find('MenuItem li') - .at(3) - .prop('style'), - ).toEqual({ display: 'none' }); - - expect( - wrapper - .find(overflowIndicatorSelector) - .at(2) - .prop('style'), - ).toEqual({}); - expect( - wrapper - .find(overflowIndicatorSelector) - .at(3) - .prop('style'), - ).toEqual({ - display: 'none', - }); - }); - - describe('props changes', () => { - it('should recalculate overflow on children length changes', () => { - const liWidths = [50, 50, 50, 50]; - const availableWidth = 145; - const indicatorWidth = 45; - const widths = [...liWidths, indicatorWidth, availableWidth]; - let i = 0; - - mockedUtil.getWidth = () => { - const id = i; - i += 1; - return widths[id]; - }; - - wrapper = mount(createMenu()); - - expect(wrapper.find(overflowIndicatorSelector).length).toEqual(5); - expect( - wrapper - .find(overflowIndicatorSelector) - .at(1) - .prop('style'), - ).toEqual({ - display: 'none', - }); - expect( - wrapper - .find(overflowIndicatorSelector) - .at(2) - .prop('style'), - ).toEqual({}); - - wrapper.setProps({ children: child }); - wrapper.update(); - - expect(wrapper.find(overflowIndicatorSelector).length).toEqual(2); - expect( - wrapper - .find(overflowIndicatorSelector) - .at(0) - .prop('style'), - ).toEqual({ - display: 'none', - }); - }); - }); - }); - - describe('motion', () => { - it('defaultMotions should work correctly', () => { - const defaultMotions = { - inline: { motionName: 'inlineMotion' }, - horizontal: { motionName: 'horizontalMotion' }, - other: { motionName: 'defaultMotion' }, - }; - const wrapper = mount( - , - ); - expect(getMotion(wrapper.props(), wrapper.state(), 'inline')).toEqual({ - motionName: 'inlineMotion', - }); - expect(getMotion(wrapper.props(), wrapper.state(), 'horizontal')).toEqual( - { - motionName: 'horizontalMotion', - }, - ); - expect(getMotion(wrapper.props(), wrapper.state(), 'vertical')).toEqual({ - motionName: 'defaultMotion', - }); - }); - - it('get correct animation type when switched from inline', () => { - const wrapper = mount(); - wrapper.setProps({ mode: 'horizontal' }); - expect(getMotion(wrapper.props(), wrapper.state())).toEqual(null); - }); - - it('warning if use `openAnimation` as object', () => { - const warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - mount(); - expect(warnSpy).toHaveBeenCalledWith( - 'Warning: Object type of `openAnimation` is removed. Please use `motion` instead.', - ); - warnSpy.mockRestore(); - }); - - it('motion object', () => { - const motion = { test: true }; - const wrapper = mount(); - expect(getMotion(wrapper.props(), wrapper.state())).toEqual(motion); - }); - - it('legacy openTransitionName', () => { - const wrapper = mount(); - expect(getMotion(wrapper.props(), wrapper.state())).toEqual({ - motionName: 'legacy', - }); - }); - }); - - it('onMouseEnter should work', () => { - const onMouseEnter = jest.fn(); - const wrapper = mount( - - Navigation One - Navigation Two - , - ); - wrapper - .find(Menu) - .at(0) - .simulate('mouseenter'); - expect(onMouseEnter).toHaveBeenCalled(); - }); + // describe('render role listbox', () => { + // function createMenu() { + // return ( + // + // + // 1 + // + // + // 2 + // + // + // 3 + // + // + // ); + // } + + // it('renders menu correctly', () => { + // const wrapper = render(createMenu()); + // expect(renderToJson(wrapper)).toMatchSnapshot(); + // }); + // }); + + // it('set activeKey', () => { + // const wrapper = mount( + // + // 1 + // 2 + // , + // ); + // expect( + // wrapper + // .find('MenuItem') + // .first() + // .props().active, + // ).toBe(true); + // expect( + // wrapper + // .find('MenuItem') + // .last() + // .props().active, + // ).toBe(false); + // }); + + // it('active first item', () => { + // const wrapper = mount( + // + // 1 + // 2 + // , + // ); + // expect( + // wrapper + // .find('MenuItem') + // .first() + // .props().active, + // ).toBe(true); + // }); + + // it('should render none menu item children', () => { + // expect(() => { + // mount( + // + // 1 + // 2 + // string + // {'string'} + // {null} + // {undefined} + // {12345} + //
        + // + //
        , + // ); + // }).not.toThrow(); + // }); + + // it('select multiple items', () => { + // const wrapper = mount( + // + // 1 + // 2 + // , + // ); + // wrapper + // .find('MenuItem') + // .first() + // .simulate('click'); + // wrapper + // .find('MenuItem') + // .last() + // .simulate('click'); + + // expect(wrapper.find('.rc-menu-item-selected').length).toBe(2); + // }); + + // it('can be controlled by selectedKeys', () => { + // const wrapper = mount( + // + // 1 + // 2 + // , + // ); + // expect( + // wrapper + // .find('li') + // .first() + // .props().className, + // ).toContain('-selected'); + // wrapper.setProps({ selectedKeys: ['2'] }); + // wrapper.update(); + // expect( + // wrapper + // .find('li') + // .last() + // .props().className, + // ).toContain('-selected'); + // }); + + // it('select default item', () => { + // const wrapper = mount( + // + // 1 + // 2 + // , + // ); + // expect( + // wrapper + // .find('li') + // .first() + // .props().className, + // ).toContain('-selected'); + // }); + + // it('issue https://github.com/ant-design/ant-design/issues/29429', () => { + // // don't use selectedKeys as string + // // it is a compatible feature for https://github.com/ant-design/ant-design/issues/29429 + // const wrapper = mount( + // + // 1 + // 2 + // , + // ); + // expect( + // wrapper + // .find('li') + // .at(0) + // .props().className, + // ).not.toContain('-selected'); + // expect( + // wrapper + // .find('li') + // .at(1) + // .props().className, + // ).toContain('-selected'); + // }); + + // it('can be controlled by openKeys', () => { + // const wrapper = mount( + // + // + // 1 + // + // + // 2 + // + // , + // ); + // expect( + // wrapper + // .find('ul') + // .first() + // .props().className, + // ).not.toContain('-hidden'); + // wrapper.setProps({ openKeys: ['g2'] }); + // expect( + // wrapper + // .find('ul') + // .last() + // .props().className, + // ).not.toContain('-hidden'); + // }); + + // it('openKeys should allow to be empty', () => { + // const wrapper = mount( + // {}} + // onOpenChange={() => {}} + // openKeys={undefined} + // selectedKeys={['1']} + // mode="inline" + // > + // + // + // + // 123123 + // + // + // + // , + // ); + // expect(wrapper).toBeTruthy(); + // }); + + // it('open default submenu', () => { + // const wrapper = mount( + // + // + // 1 + // + // + // 2 + // + // , + // ); + // expect( + // wrapper + // .find('ul') + // .first() + // .props().className, + // ).not.toContain('-hidden'); + // }); + + // it('fires select event', () => { + // const handleSelect = jest.fn(); + // const wrapper = mount( + // + // 1 + // 2 + // , + // ); + // wrapper + // .find('MenuItem') + // .first() + // .simulate('click'); + // expect(handleSelect.mock.calls[0][0].key).toBe('1'); + // }); + + // it('fires click event', () => { + // const handleClick = jest.fn(); + // const wrapper = mount( + // + // 1 + // 2 + // , + // ); + // wrapper + // .find('MenuItem') + // .first() + // .simulate('click'); + // expect(handleClick.mock.calls[0][0].key).toBe('1'); + // }); + + // it('fires deselect event', () => { + // const handleDeselect = jest.fn(); + // const wrapper = mount( + // + // 1 + // 2 + // , + // ); + // wrapper + // .find('MenuItem') + // .first() + // .simulate('click') + // .simulate('click'); + // expect(handleDeselect.mock.calls[0][0].key).toBe('1'); + // }); + + // it('active by mouse enter', () => { + // const wrapper = mount( + // + // item + // disabled + // item2 + // , + // ); + // let menuItem = wrapper.find('MenuItem').last(); + // menuItem.simulate('mouseEnter'); + // menuItem = wrapper.find('MenuItem').last(); + // expect(menuItem.props().active).toBe(true); + // }); + + // it('active by key down', () => { + // const wrapper = mount( + // + // 1 + // 2 + // , + // ); + + // wrapper.simulate('keyDown', { keyCode: KeyCode.DOWN }); + // expect( + // wrapper + // .find('MenuItem') + // .at(1) + // .props().active, + // ).toBe(true); + // }); + + // it('keydown works when children change', () => { + // class App extends React.Component { + // state = { + // items: [1, 2, 3], + // }; + + // render() { + // return ( + // + // {this.state.items.map(i => ( + // {i} + // ))} + // + // ); + // } + // } + + // const wrapper = mount(); + + // wrapper.setState({ items: [0, 1] }); + + // wrapper.find('Menu').simulate('keyDown', { keyCode: KeyCode.DOWN }); + // expect( + // wrapper + // .find('MenuItem') + // .at(0) + // .props().active, + // ).toBe(true); + + // wrapper.find('Menu').simulate('keyDown', { keyCode: KeyCode.DOWN }); + // expect( + // wrapper + // .find('MenuItem') + // .at(1) + // .props().active, + // ).toBe(true); + // }); + + // it('active first item when children changes', () => { + // class App extends React.Component { + // state = { + // items: ['foo'], + // }; + + // render() { + // return ( + // + // {this.state.items.map(item => ( + // {item} + // ))} + // + // ); + // } + // } + + // const wrapper = mount(); + + // wrapper.setState({ items: ['bar', 'foo'] }); + // wrapper.update(); + + // expect( + // wrapper + // .find('li') + // .first() + // .hasClass('rc-menu-item-active'), + // ).toBe(true); + // }); + + // it('should accept builtinPlacements', () => { + // const builtinPlacements = { + // leftTop: { + // points: ['tr', 'tl'], + // overflow: { + // adjustX: 0, + // adjustY: 0, + // }, + // offset: [0, 0], + // }, + // }; + + // const wrapper = mount( + // + // menuItem + // + // menuItem + // + // , + // ); + + // expect(wrapper.find('Trigger').prop('builtinPlacements').leftTop).toEqual( + // builtinPlacements.leftTop, + // ); + // }); + + // describe('submenu mode', () => { + // it('should use menu mode by default', () => { + // const wrapper = mount( + // + // + // menuItem + // + // , + // ); + + // expect( + // wrapper + // .find('SubMenu') + // .first() + // .prop('mode'), + // ).toEqual('horizontal'); + // }); + + // it('should be able to customize SubMenu mode', () => { + // const wrapper = mount( + // + // + // menuItem + // + // , + // ); + + // expect( + // wrapper + // .find('SubMenu') + // .first() + // .prop('mode'), + // ).toEqual('vertical-right'); + // }); + // }); + + // describe('DOMWrap Allow Overflow', () => { + // const overflowIndicatorSelector = 'SubMenu.rc-menu-overflowed-submenu'; + // function createMenu(props) { + // return ( + // ... + // } + // {...props} + // > + // 1 + // + // 2 + // + // 3 + // 4 + // + // ); + // } + + // let wrapper; + + // it('getWidth should contain margin when includeMargin is set to true', () => { + // const memorizedGetComputedStyle = window.getComputedStyle; + // window.getComputedStyle = () => ({ + // marginLeft: '10px', + // marginRight: '10px', + // }); + // expect( + // mockedUtil.getWidth( + // { + // getBoundingClientRect() { + // return { width: 10 }; + // }, + // }, + // true, + // ), + // ).toEqual(30); + // window.getComputedStyle = memorizedGetComputedStyle; + // }); + + // it('should not include overflow indicator when having enough width', () => { + // const indicatorWidth = 50; // actual width including 40 px padding, which will be 50; + // const liWidths = [50, 50, 50, 50]; + // const availableWidth = 250; + // const widths = [...liWidths, indicatorWidth, availableWidth]; + // let i = 0; + // mockedUtil.getWidth = () => { + // const id = i; + // i += 1; + // return widths[id]; + // }; + // wrapper = mount(createMenu()); + + // // overflow indicator placeholder + // expect( + // wrapper + // .find(overflowIndicatorSelector) + // .at(4) + // .prop('style'), + // ).toEqual({ + // visibility: 'hidden', + // position: 'absolute', + // }); + + // // last overflow indicator should be hidden + // expect( + // wrapper + // .find(overflowIndicatorSelector) + // .at(3) + // .prop('style'), + // ).toEqual({ + // display: 'none', + // }); + + // expect( + // wrapper + // .find('MenuItem li') + // .at(0) + // .prop('style'), + // ).toEqual({}); + // expect( + // wrapper + // .find('MenuItem li') + // .at(1) + // .prop('style'), + // ).toEqual({}); + // expect( + // wrapper + // .find('MenuItem li') + // .at(2) + // .prop('style'), + // ).toEqual({}); + // expect( + // wrapper + // .find('MenuItem li') + // .at(3) + // .prop('style'), + // ).toEqual({}); + // }); + + // it('should include overflow indicator when having not enough width', () => { + // const indicatorWidth = 5; // actual width including 40 px padding, which will be 45; + // const liWidths = [50, 50, 50, 50]; + // const availableWidth = 145; + // const widths = [...liWidths, indicatorWidth, availableWidth]; + // let i = 0; + // mockedUtil.getWidth = () => { + // const id = i; + // i += 1; + // return widths[id]; + // }; + // wrapper = mount(createMenu()); + + // expect( + // wrapper + // .find('MenuItem li') + // .at(0) + // .prop('style'), + // ).toEqual({}); + // expect( + // wrapper + // .find('MenuItem li') + // .at(1) + // .prop('style'), + // ).toEqual({}); + // expect( + // wrapper + // .find('MenuItem li') + // .at(2) + // .prop('style'), + // ).toEqual({ display: 'none' }); + // expect( + // wrapper + // .find('MenuItem li') + // .at(3) + // .prop('style'), + // ).toEqual({ display: 'none' }); + + // expect( + // wrapper + // .find(overflowIndicatorSelector) + // .at(2) + // .prop('style'), + // ).toEqual({}); + // expect( + // wrapper + // .find(overflowIndicatorSelector) + // .at(3) + // .prop('style'), + // ).toEqual({ + // display: 'none', + // }); + // }); + + // describe('props changes', () => { + // it('should recalculate overflow on children length changes', () => { + // const liWidths = [50, 50, 50, 50]; + // const availableWidth = 145; + // const indicatorWidth = 45; + // const widths = [...liWidths, indicatorWidth, availableWidth]; + // let i = 0; + + // mockedUtil.getWidth = () => { + // const id = i; + // i += 1; + // return widths[id]; + // }; + + // wrapper = mount(createMenu()); + + // expect(wrapper.find(overflowIndicatorSelector).length).toEqual(5); + // expect( + // wrapper + // .find(overflowIndicatorSelector) + // .at(1) + // .prop('style'), + // ).toEqual({ + // display: 'none', + // }); + // expect( + // wrapper + // .find(overflowIndicatorSelector) + // .at(2) + // .prop('style'), + // ).toEqual({}); + + // wrapper.setProps({ children: child }); + // wrapper.update(); + + // expect(wrapper.find(overflowIndicatorSelector).length).toEqual(2); + // expect( + // wrapper + // .find(overflowIndicatorSelector) + // .at(0) + // .prop('style'), + // ).toEqual({ + // display: 'none', + // }); + // }); + // }); + // }); + + // describe('motion', () => { + // it('defaultMotions should work correctly', () => { + // const defaultMotions = { + // inline: { motionName: 'inlineMotion' }, + // horizontal: { motionName: 'horizontalMotion' }, + // other: { motionName: 'defaultMotion' }, + // }; + // const wrapper = mount( + // , + // ); + // expect(getMotion(wrapper.props(), wrapper.state(), 'inline')).toEqual({ + // motionName: 'inlineMotion', + // }); + // expect(getMotion(wrapper.props(), wrapper.state(), 'horizontal')).toEqual( + // { + // motionName: 'horizontalMotion', + // }, + // ); + // expect(getMotion(wrapper.props(), wrapper.state(), 'vertical')).toEqual({ + // motionName: 'defaultMotion', + // }); + // }); + + // it('get correct animation type when switched from inline', () => { + // const wrapper = mount(); + // wrapper.setProps({ mode: 'horizontal' }); + // expect(getMotion(wrapper.props(), wrapper.state())).toEqual(null); + // }); + + // it('warning if use `openAnimation` as object', () => { + // const warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + // mount(); + // expect(warnSpy).toHaveBeenCalledWith( + // 'Warning: Object type of `openAnimation` is removed. Please use `motion` instead.', + // ); + // warnSpy.mockRestore(); + // }); + + // it('motion object', () => { + // const motion = { test: true }; + // const wrapper = mount(); + // expect(getMotion(wrapper.props(), wrapper.state())).toEqual(motion); + // }); + + // it('legacy openTransitionName', () => { + // const wrapper = mount(); + // expect(getMotion(wrapper.props(), wrapper.state())).toEqual({ + // motionName: 'legacy', + // }); + // }); + // }); + + // it('onMouseEnter should work', () => { + // const onMouseEnter = jest.fn(); + // const wrapper = mount( + // + // Navigation One + // Navigation Two + // , + // ); + // wrapper + // .find(Menu) + // .at(0) + // .simulate('mouseenter'); + // expect(onMouseEnter).toHaveBeenCalled(); + // }); }); /* eslint-enable */ From a0fc7361dfcfae938181402e3691afb7ac9653dd Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 23 Apr 2021 10:41:50 +0800 Subject: [PATCH 37/93] test: update snapshot --- tests/__snapshots__/Menu.spec.js.snap | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/__snapshots__/Menu.spec.js.snap b/tests/__snapshots__/Menu.spec.js.snap index c1a67aeb..9dcf7e9d 100644 --- a/tests/__snapshots__/Menu.spec.js.snap +++ b/tests/__snapshots__/Menu.spec.js.snap @@ -645,12 +645,12 @@ exports[`Menu should render should support Fragment 1`] = ` exports[`Menu should render vertical menu correctly 1`] = ` `; exports[`Menu should render horizontal menu with rtl direction correctly 1`] = `
      • - -
      • - - `; diff --git a/tests/setup.js b/tests/setup.js index 0ae30f37..1477fc27 100644 --- a/tests/setup.js +++ b/tests/setup.js @@ -5,3 +5,5 @@ const Enzyme = require('enzyme'); const Adapter = require('enzyme-adapter-react-16'); Enzyme.configure({ adapter: new Adapter() }); + +Object.assign(Enzyme.ReactWrapper.prototype, {}); From cf7f344195f0072c80512193138ce43e37ec4ef9 Mon Sep 17 00:00:00 2001 From: zombiej Date: Mon, 26 Apr 2021 14:00:18 +0800 Subject: [PATCH 43/93] test: back of fragment test --- tests/Menu.spec.js | 34 +++++++++++++-------------- tests/__snapshots__/Menu.spec.js.snap | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/Menu.spec.js b/tests/Menu.spec.js index f78e1706..9ac35d81 100644 --- a/tests/Menu.spec.js +++ b/tests/Menu.spec.js @@ -68,23 +68,23 @@ describe('Menu', () => { }); }); - // it('should support Fragment', () => { - // const wrapper = mount( - // - // - // 6 - // - // 6 - // <> - // - // 6 - // - // 6 - // - // , - // ); - // expect(render(wrapper)).toMatchSnapshot(); - // }); + it('should support Fragment', () => { + const wrapper = mount( + + + 6 + + 6 + <> + + 6 + + 6 + + , + ); + expect(render(wrapper)).toMatchSnapshot(); + }); }); // describe('render role listbox', () => { diff --git a/tests/__snapshots__/Menu.spec.js.snap b/tests/__snapshots__/Menu.spec.js.snap index c7b74b5c..b1eff0c6 100644 --- a/tests/__snapshots__/Menu.spec.js.snap +++ b/tests/__snapshots__/Menu.spec.js.snap @@ -398,7 +398,7 @@ exports[`Menu should render inline menu with rtl direction correctly 1`] = ` exports[`Menu should render should support Fragment 1`] = `