From 4311c157550cfaf9e6c31ded7c378988a76948c8 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Tue, 16 Jun 2020 22:55:02 +0800 Subject: [PATCH] feat: update menu --- breakChange-2.x.md | 6 ++ components/_util/openAnimation.js | 4 +- components/_util/props-util.js | 38 ++++--- components/_util/proxyComponent.jsx | 37 +++---- components/_util/store/Provider.jsx | 10 +- components/_util/store/connect.jsx | 34 +++--- components/menu/MenuItem.jsx | 34 +++--- components/menu/SubMenu.jsx | 34 +++--- components/menu/index.jsx | 95 ++++++++-------- components/vc-menu/DOMWrap.jsx | 41 +++---- components/vc-menu/Menu.jsx | 43 +++----- components/vc-menu/MenuItem.jsx | 36 +++---- components/vc-menu/MenuItemGroup.jsx | 21 ++-- components/vc-menu/SubMenu.jsx | 150 +++++++++++++------------- components/vc-menu/SubPopupMenu.jsx | 143 ++++++++++-------------- components/vc-menu/commonPropsType.js | 1 + components/vc-menu/util.js | 119 ++++++++++---------- examples/index.js | 2 + 18 files changed, 388 insertions(+), 460 deletions(-) diff --git a/breakChange-2.x.md b/breakChange-2.x.md index 894ab469a..fb0ead71e 100644 --- a/breakChange-2.x.md +++ b/breakChange-2.x.md @@ -29,3 +29,9 @@ v-model -> v-model:visible ## Tooltip v-model -> v-model:visible + +## Modal + +v-model -> v-model:visible + +okButtonProps、cancelButtonProps 扁平化处理 diff --git a/components/_util/openAnimation.js b/components/_util/openAnimation.js index 29d98f0bf..0cd76cdc4 100644 --- a/components/_util/openAnimation.js +++ b/components/_util/openAnimation.js @@ -54,12 +54,12 @@ function animate(node, show, done) { } const animation = { - enter(node, done) { + onEnter(node, done) { nextTick(() => { animate(node, true, done); }); }, - leave(node, done) { + onLeave(node, done) { return animate(node, false, done); }, }; diff --git a/components/_util/props-util.js b/components/_util/props-util.js index f0d503255..85ac8b81f 100644 --- a/components/_util/props-util.js +++ b/components/_util/props-util.js @@ -118,14 +118,26 @@ const getOptionProps = instance => { return res; }; const getComponent = (instance, prop, options = instance, execute = true) => { - const temp = instance[prop]; - if (temp !== undefined) { - return typeof temp === 'function' && execute ? temp(options) : temp; - } else { - let com = instance.$slots[prop]; - com = execute && com ? com(options) : com; - return Array.isArray(com) && com.length === 1 ? com[0] : com; + if (instance.$) { + const temp = instance[prop]; + if (temp !== undefined) { + return typeof temp === 'function' && execute ? temp(options) : temp; + } else { + let com = instance.$slots[prop]; + com = execute && com ? com(options) : com; + return Array.isArray(com) && com.length === 1 ? com[0] : com; + } + } else if (isVNode(instance)) { + const temp = instance.props && instance.props[prop]; + if (temp !== undefined) { + return typeof temp === 'function' && execute ? temp(options) : temp; + } else if (instance.children && instance.children[name]) { + let com = instance.children[prop]; + com = execute && com ? com(options) : com; + return Array.isArray(com) && com.length === 1 ? com[0] : com; + } } + return undefined; }; const getComponentFromProp = (instance, prop, options = instance, execute = true) => { if (instance.$createElement) { @@ -169,13 +181,13 @@ const getComponentFromProp = (instance, prop, options = instance, execute = true }; const getAllProps = ele => { - let data = ele.data || {}; - let componentOptions = ele.componentOptions || {}; - if (ele.$vnode) { - data = ele.$vnode.data || {}; - componentOptions = ele.$vnode.componentOptions || {}; + let props = getOptionProps(ele); + if (ele.$) { + props = { ...props, ...this.$attrs }; + } else { + props = { ...props, ...ele.props }; } - return { ...data.props, ...data.attrs, ...componentOptions.propsData }; + return props; }; // 使用 getOptionProps 替换 ,待测试 diff --git a/components/_util/proxyComponent.jsx b/components/_util/proxyComponent.jsx index 5a13daa8a..3adb4db2d 100644 --- a/components/_util/proxyComponent.jsx +++ b/components/_util/proxyComponent.jsx @@ -1,5 +1,6 @@ +import { createVNode } from 'vue'; import PropTypes from './vue-types'; -import { getOptionProps, getListeners } from './props-util'; +import { getOptionProps } from './props-util'; function getDisplayName(WrappedComponent) { return WrappedComponent.name || 'Component'; @@ -15,6 +16,7 @@ export default function wrapWithConnect(WrappedComponent) { WrappedComponent.props.children = PropTypes.array.def([]); const ProxyWrappedComponent = { props, + inheritAttrs: false, model: WrappedComponent.model, name: `Proxy_${getDisplayName(WrappedComponent)}`, methods: { @@ -23,30 +25,21 @@ export default function wrapWithConnect(WrappedComponent) { }, }, render() { - const { $slots = {}, $scopedSlots } = this; + const { $slots = {} } = this; const props = getOptionProps(this); const wrapProps = { - props: { - ...props, - __propsSymbol__: Symbol(), - componentWillReceiveProps: { ...props }, - children: $slots.default || props.children || [], - }, - on: getListeners(this), + ...props, + __propsSymbol__: Symbol(), + componentWillReceiveProps: { ...props }, + children: props.children || $slots?.default() || [], + slots: $slots, + ref: 'wrappedInstance', }; - if (Object.keys($scopedSlots).length) { - wrapProps.scopedSlots = $scopedSlots; - } - const slotsKey = Object.keys($slots); - return ( - - {slotsKey.length - ? slotsKey.map(name => { - return ; - }) - : null} - - ); + return createVNode(WrappedComponent, wrapProps); + // return ( + // + // + // ); }, }; Object.keys(methods).map(m => { diff --git a/components/_util/store/Provider.jsx b/components/_util/store/Provider.jsx index c8f3148a2..d7636b82b 100644 --- a/components/_util/store/Provider.jsx +++ b/components/_util/store/Provider.jsx @@ -1,15 +1,15 @@ +import { provide } from 'vue'; import { storeShape } from './PropTypes'; +import { getSlot } from '../props-util'; export default { name: 'StoreProvider', props: { store: storeShape.isRequired, }, - provide() { - return { - storeContext: this.$props, - }; + created() { + provide('storeContext', this.$props); }, render() { - return this.$slots.default[0]; + return getSlot(this); }, }; diff --git a/components/_util/store/connect.jsx b/components/_util/store/connect.jsx index b58f1670b..0891d6daa 100644 --- a/components/_util/store/connect.jsx +++ b/components/_util/store/connect.jsx @@ -1,6 +1,7 @@ import shallowEqual from 'shallowequal'; +import { inject, createVNode } from 'vue'; import omit from 'omit.js'; -import { getOptionProps, getListeners } from '../props-util'; +import { getOptionProps } from '../props-util'; import PropTypes from '../vue-types'; import proxyComponent from '../proxyComponent'; @@ -22,9 +23,12 @@ export default function connect(mapStateToProps) { }); const Connect = { name: `Connect_${getDisplayName(WrappedComponent)}`, + inheritAttrs: false, props, - inject: { - storeContext: { default: () => ({}) }, + setup() { + return { + storeContext: inject('storeContext', {}), + }; }, data() { this.store = this.storeContext.store; @@ -80,25 +84,19 @@ export default function connect(mapStateToProps) { }, }, render() { - const { $slots = {}, $scopedSlots, subscribed, store } = this; + const { $slots = {}, subscribed, store, $attrs } = this; const props = getOptionProps(this); this.preProps = { ...omit(props, ['__propsSymbol__']) }; const wrapProps = { - props: { - ...props, - ...subscribed, - store, - }, - on: getListeners(this), - scopedSlots: $scopedSlots, + ...props, + ...subscribed, + ...$attrs, + store, + slots: $slots, + ref: 'wrappedInstance', }; - return ( - - {Object.keys($slots).map(name => { - return ; - })} - - ); + return createVNode(WrappedComponent, wrapProps); + // return ; }, }; return proxyComponent(Connect); diff --git a/components/menu/MenuItem.jsx b/components/menu/MenuItem.jsx index c9196d999..63148cde0 100644 --- a/components/menu/MenuItem.jsx +++ b/components/menu/MenuItem.jsx @@ -1,16 +1,20 @@ +import { inject } from 'vue'; import { Item, itemProps } from '../vc-menu'; -import { getOptionProps, getListeners } from '../_util/props-util'; +import { getOptionProps, getSlot } from '../_util/props-util'; import Tooltip from '../tooltip'; function noop() {} export default { name: 'MenuItem', inheritAttrs: false, props: itemProps, - inject: { - getInlineCollapsed: { default: () => noop }, - layoutSiderContext: { default: () => ({}) }, - }, + isMenuItem: true, + setup() { + return { + getInlineCollapsed: inject('getInlineCollapsed', noop), + layoutSiderContext: inject('layoutSiderContext', {}), + }; + }, methods: { onKeyDown(e) { this.$refs.menuItem.onKeyDown(e); @@ -33,24 +37,20 @@ export default { } const itemProps = { - props: { - ...props, - title, - }, - attrs, - on: getListeners(this), + ...props, + title, + ...attrs, }; const toolTipProps = { - props: { - ...tooltipProps, - placement: 'right', - overlayClassName: `${rootPrefixCls}-inline-collapsed-tooltip`, - }, + ...tooltipProps, + placement: 'right', + overlayClassName: `${rootPrefixCls}-inline-collapsed-tooltip`, }; + // return
ddd
; return ( - {$slots.default} + {getSlot(this)} ); diff --git a/components/menu/SubMenu.jsx b/components/menu/SubMenu.jsx index 302084bee..b9b41dec7 100644 --- a/components/menu/SubMenu.jsx +++ b/components/menu/SubMenu.jsx @@ -1,13 +1,18 @@ +import { inject } from 'vue'; import { SubMenu as VcSubMenu } from '../vc-menu'; -import { getListeners } from '../_util/props-util'; import classNames from 'classnames'; +import Omit from 'omit.js'; +import { getSlot } from '../_util/props-util'; export default { name: 'ASubMenu', isSubMenu: true, + inheritAttrs: false, props: { ...VcSubMenu.props }, - inject: { - menuPropsContext: { default: () => ({}) }, + setup() { + return { + menuPropsContext: inject('menuPropsContext', {}), + }; }, methods: { onKeyDown(e) { @@ -16,27 +21,16 @@ export default { }, render() { - const { $slots, $scopedSlots } = this; + const { $slots, $attrs } = this; const { rootPrefixCls, popupClassName } = this.$props; const { theme: antdMenuTheme } = this.menuPropsContext; const props = { - props: { - ...this.$props, - popupClassName: classNames(`${rootPrefixCls}-${antdMenuTheme}`, popupClassName), - }, + ...this.$props, + popupClassName: classNames(`${rootPrefixCls}-${antdMenuTheme}`, popupClassName), ref: 'subMenu', - on: getListeners(this), - scopedSlots: $scopedSlots, + ...$attrs, + ...Omit($slots, ['default']), }; - const slotsKey = Object.keys($slots); - return ( - - {slotsKey.length - ? slotsKey.map(name => { - return ; - }) - : null} - - ); + return {getSlot(this)}; }, }; diff --git a/components/menu/index.jsx b/components/menu/index.jsx index c5e2578ce..56e3258c4 100644 --- a/components/menu/index.jsx +++ b/components/menu/index.jsx @@ -1,3 +1,4 @@ +import { inject, provide } from 'vue'; import omit from 'omit.js'; import VcMenu, { Divider, ItemGroup } from '../vc-menu'; import SubMenu from './SubMenu'; @@ -5,11 +6,10 @@ import PropTypes from '../_util/vue-types'; import animation from '../_util/openAnimation'; import warning from '../_util/warning'; import Item from './MenuItem'; -import { hasProp, getListeners, getOptionProps } from '../_util/props-util'; +import { hasProp, getOptionProps, getSlot } from '../_util/props-util'; import BaseMixin from '../_util/BaseMixin'; import commonPropsType from '../vc-menu/commonPropsType'; import { ConfigConsumerProps } from '../config-provider'; -import Base from '../base'; // import raf from '../_util/raf'; export const MenuMode = PropTypes.oneOf([ @@ -41,26 +41,27 @@ export const menuProps = { const Menu = { name: 'AMenu', + inheritAttrs: false, props: menuProps, Divider: { ...Divider, name: 'AMenuDivider' }, Item: { ...Item, name: 'AMenuItem' }, SubMenu: { ...SubMenu, name: 'ASubMenu' }, ItemGroup: { ...ItemGroup, name: 'AMenuItemGroup' }, - provide() { - return { - getInlineCollapsed: this.getInlineCollapsed, - menuPropsContext: this.$props, - }; - }, mixins: [BaseMixin], - inject: { - layoutSiderContext: { default: () => ({}) }, - configProvider: { default: () => ConfigConsumerProps }, + created() { + provide('getInlineCollapsed', this.getInlineCollapsed); + provide('menuPropsContext', this.$props); }, - model: { - prop: 'selectedKeys', - event: 'selectChange', + setup() { + return { + configProvider: inject('configProvider', ConfigConsumerProps), + layoutSiderContext: inject('layoutSiderContext', {}), + }; }, + // model: { + // prop: 'selectedKeys', + // event: 'selectChange', + // }, updated() { this.propsUpdating = false; }, @@ -165,10 +166,12 @@ const Menu = { }, handleSelect(info) { this.$emit('select', info); + this.$emit('update:selectedKeys', info.selectedKeys); this.$emit('selectChange', info.selectedKeys); }, handleDeselect(info) { this.$emit('deselect', info); + this.$emit('update:selectedKeys', info.selectedKeys); this.$emit('selectChange', info.selectedKeys); }, handleOpenChange(openKeys) { @@ -203,7 +206,7 @@ const Menu = { if (menuMode === 'horizontal') { menuOpenAnimation = 'slide-up'; } else if (menuMode === 'inline') { - menuOpenAnimation = { on: animation }; + menuOpenAnimation = animation; } else { // When mode switch from inline // submenu should hide without animation @@ -219,7 +222,7 @@ const Menu = { }, }, render() { - const { layoutSiderContext, $slots } = this; + const { layoutSiderContext } = this; const { collapsedWidth } = layoutSiderContext; const { getPopupContainer: getContextPopupContainer } = this.configProvider; const props = getOptionProps(this); @@ -235,37 +238,32 @@ const Menu = { }; const menuProps = { - props: { - ...omit(props, ['inlineCollapsed']), - getPopupContainer: getPopupContainer || getContextPopupContainer, - openKeys: this.sOpenKeys, - mode: menuMode, - prefixCls, - }, - on: { - ...getListeners(this), - select: this.handleSelect, - deselect: this.handleDeselect, - openChange: this.handleOpenChange, - mouseenter: this.handleMouseEnter, - }, - nativeOn: { - transitionend: this.handleTransitionEnd, - }, + ...omit(props, ['inlineCollapsed']), + getPopupContainer: getPopupContainer || getContextPopupContainer, + openKeys: this.sOpenKeys, + mode: menuMode, + prefixCls, + ...this.$attrs, + onSelect: this.handleSelect, + onDeselect: this.handleDeselect, + onOpenChange: this.handleOpenChange, + onMouseenter: this.handleMouseEnter, + onTransitionend: this.handleTransitionEnd, + children: getSlot(this), }; if (!hasProp(this, 'selectedKeys')) { - delete menuProps.props.selectedKeys; + delete menuProps.selectedKeys; } if (menuMode !== 'inline') { // closing vertical popup submenu after click it - menuProps.on.click = this.handleClick; - menuProps.props.openTransitionName = menuOpenAnimation; + menuProps.onClick = this.handleClick; + menuProps.openTransitionName = menuOpenAnimation; } else { - menuProps.on.click = e => { + menuProps.onClick = e => { this.$emit('click', e); }; - menuProps.props.openAnimation = menuOpenAnimation; + menuProps.openAnimation = menuOpenAnimation; } // https://github.com/ant-design/ant-design/issues/8587 @@ -273,24 +271,19 @@ const Menu = { this.getInlineCollapsed() && (collapsedWidth === 0 || collapsedWidth === '0' || collapsedWidth === '0px'); if (hideMenu) { - menuProps.props.openKeys = []; + menuProps.openKeys = []; } - return ( - - {$slots.default} - - ); + return ; }, }; /* istanbul ignore next */ -Menu.install = function(Vue) { - Vue.use(Base); - Vue.component(Menu.name, Menu); - Vue.component(Menu.Item.name, Menu.Item); - Vue.component(Menu.SubMenu.name, Menu.SubMenu); - Vue.component(Menu.Divider.name, Menu.Divider); - Vue.component(Menu.ItemGroup.name, Menu.ItemGroup); +Menu.install = function(app) { + app.component(Menu.name, Menu); + app.component(Menu.Item.name, Menu.Item); + app.component(Menu.SubMenu.name, Menu.SubMenu); + app.component(Menu.Divider.name, Menu.Divider); + app.component(Menu.ItemGroup.name, Menu.ItemGroup); }; export default Menu; diff --git a/components/vc-menu/DOMWrap.jsx b/components/vc-menu/DOMWrap.jsx index d956e5788..651a09129 100644 --- a/components/vc-menu/DOMWrap.jsx +++ b/components/vc-menu/DOMWrap.jsx @@ -4,7 +4,7 @@ import SubMenu from './SubMenu'; import BaseMixin from '../_util/BaseMixin'; import { getWidth, setStyle, menuAllProps } from './util'; import { cloneElement } from '../_util/vnode'; -import { getClass, getPropsData, getEvents, getListeners } from '../_util/props-util'; +import { getPropsData, getSlot, getAllProps } from '../_util/props-util'; const canUseDOM = !!( typeof window !== 'undefined' && @@ -110,9 +110,8 @@ const DOMWrap = { } // put all the overflowed item inside a submenu // with a title of overflow indicator ('...') - const copy = this.$slots.default[0]; - const { title, ...rest } = getPropsData(copy); // eslint-disable-line no-unused-vars - const events = getEvents(copy); + const copy = getSlot(this)[0]; + const { title, ...rest } = getAllProps(copy); // eslint-disable-line no-unused-vars let style = {}; let key = `${keyPrefix}-overflowed-indicator`; let eventKey = `${keyPrefix}-overflowed-indicator`; @@ -133,29 +132,20 @@ const DOMWrap = { const popupClassName = theme ? `${prefixCls}-${theme}` : ''; const props = {}; - const on = {}; - menuAllProps.props.forEach(k => { + menuAllProps.forEach(k => { if (rest[k] !== undefined) { props[k] = rest[k]; } }); - menuAllProps.on.forEach(k => { - if (events[k] !== undefined) { - on[k] = events[k]; - } - }); const subMenuProps = { - props: { - title: overflowedIndicator, - popupClassName, - ...props, - eventKey, - disabled: false, - }, + title: overflowedIndicator, + popupClassName, + ...props, + eventKey, + disabled: false, class: `${prefixCls}-overflowed-submenu`, key, style, - on, }; return {overflowedItems}; @@ -245,7 +235,7 @@ const DOMWrap = { renderChildren(children) { // need to take care of overflowed items in horizontal mode const { lastVisibleIndex } = this.$data; - const className = getClass(this); + const className = this.$attrs.class || ''; return (children || []).reduce((acc, childNode, index) => { let item = childNode; const eventKey = getPropsData(childNode).eventKey; @@ -258,7 +248,7 @@ const DOMWrap = { // 这里修改 eventKey 是为了防止隐藏状态下还会触发 openkeys 事件 { style: { display: 'none' }, - props: { eventKey: `${eventKey}-hidden` }, + eventKey: `${eventKey}-hidden`, class: MENUITEM_OVERFLOWED_CLASSNAME, }, ); @@ -271,7 +261,7 @@ const DOMWrap = { // we have to overwrite with the correct key explicitly { key: getPropsData(c).eventKey, - props: { mode: 'vertical-left' }, + mode: 'vertical-left', }, ); }); @@ -295,10 +285,8 @@ const DOMWrap = { render() { const Tag = this.$props.tag; - const tagProps = { - on: getListeners(this), - }; - return {this.renderChildren(this.$slots.default)}; + + return {this.renderChildren(getSlot(this))}; }, }; @@ -311,6 +299,7 @@ DOMWrap.props = { visible: PropTypes.bool, hiddenClassName: PropTypes.string, tag: PropTypes.string.def('div'), + children: PropTypes.any, }; export default DOMWrap; diff --git a/components/vc-menu/Menu.jsx b/components/vc-menu/Menu.jsx index 6e044e770..ea412119d 100644 --- a/components/vc-menu/Menu.jsx +++ b/components/vc-menu/Menu.jsx @@ -2,16 +2,12 @@ import PropTypes from '../_util/vue-types'; import { Provider, create } from '../_util/store'; import { default as SubPopupMenu, getActiveKey } from './SubPopupMenu'; import BaseMixin from '../_util/BaseMixin'; -import hasProp, { - getOptionProps, - getComponentFromProp, - filterEmpty, - getListeners, -} from '../_util/props-util'; +import hasProp, { getOptionProps, getComponent, filterEmpty } from '../_util/props-util'; import commonPropsType from './commonPropsType'; const Menu = { name: 'Menu', + inheritAttrs: false, props: { ...commonPropsType, selectable: PropTypes.bool.def(true), @@ -33,7 +29,7 @@ const Menu = { selectedKeys, openKeys, activeKey: { - '0-menu-': getActiveKey({ ...props, children: this.$slots.default || [] }, props.activeKey), + '0-menu-': getActiveKey({ ...props, children: props.children || [] }, props.activeKey), }, }); @@ -158,27 +154,20 @@ const Menu = { }, render() { - const props = getOptionProps(this); + const props = { ...getOptionProps(this), ...this.$attrs }; + props.class += ` ${props.prefixCls}-root`; const subPopupMenuProps = { - props: { - ...props, - itemIcon: getComponentFromProp(this, 'itemIcon', props), - expandIcon: getComponentFromProp(this, 'expandIcon', props), - overflowedIndicator: getComponentFromProp(this, 'overflowedIndicator', props) || ( - ··· - ), - openTransitionName: this.getOpenTransitionName(), - parentMenu: this, - children: filterEmpty(this.$slots.default || []), - }, - class: `${props.prefixCls}-root`, - on: { - ...getListeners(this), - click: this.onClick, - openChange: this.onOpenChange, - deselect: this.onDeselect, - select: this.onSelect, - }, + ...props, + itemIcon: getComponent(this, 'itemIcon', props), + expandIcon: getComponent(this, 'expandIcon', props), + overflowedIndicator: getComponent(this, 'overflowedIndicator', props) || ···, + openTransitionName: this.getOpenTransitionName(), + parentMenu: this, + children: filterEmpty(props.children), + onClick: this.onClick, + onOpenChange: this.onOpenChange, + onDeselect: this.onDeselect, + onSelect: this.onSelect, ref: 'innerMenu', }; return ( diff --git a/components/vc-menu/MenuItem.jsx b/components/vc-menu/MenuItem.jsx index 6dc53ca44..9910bbb5d 100644 --- a/components/vc-menu/MenuItem.jsx +++ b/components/vc-menu/MenuItem.jsx @@ -4,7 +4,7 @@ import BaseMixin from '../_util/BaseMixin'; import scrollIntoView from 'dom-scroll-into-view'; import { connect } from '../_util/store'; import { noop, menuAllProps } from './util'; -import { getComponentFromProp, getListeners } from '../_util/props-util'; +import { getComponent, getSlot } from '../_util/props-util'; const props = { attribute: PropTypes.object, @@ -36,6 +36,7 @@ const props = { }; const MenuItem = { name: 'MenuItem', + inheritAttrs: false, props, mixins: [BaseMixin], isMenuItem: true, @@ -141,8 +142,10 @@ const MenuItem = { }, render() { - const props = { ...this.$props }; + const props = { ...this.$props, ...this.$attrs }; + const className = { + [props.class]: props.class, [this.getPrefixCls()]: true, [this.getActiveClassName()]: !props.disabled && props.active, [this.getSelectedClassName()]: props.isSelected, @@ -171,32 +174,27 @@ const MenuItem = { } // In case that onClick/onMouseLeave/onMouseEnter is passed down from owner const mouseEvent = { - click: props.disabled ? noop : this.onClick, - mouseleave: props.disabled ? noop : this.onMouseLeave, - mouseenter: props.disabled ? noop : this.onMouseEnter, + onClick: props.disabled ? noop : this.onClick, + onMouseleave: props.disabled ? noop : this.onMouseLeave, + onMouseenter: props.disabled ? noop : this.onMouseEnter, }; - const style = {}; + const style = { ...(props.style || {}) }; if (props.mode === 'inline') { style.paddingLeft = `${props.inlineIndent * props.level}px`; } - const listeners = { ...getListeners(this) }; - menuAllProps.props.forEach(key => delete props[key]); - menuAllProps.on.forEach(key => delete listeners[key]); + [...menuAllProps, 'children', 'slots', '__propsSymbol__', 'componentWillReceiveProps'].forEach( + key => delete props[key], + ); const liProps = { - attrs: { - ...props, - ...attrs, - }, - on: { - ...listeners, - ...mouseEvent, - }, + ...props, + ...attrs, + ...mouseEvent, }; return (
  • - {this.$slots.default} - {getComponentFromProp(this, 'itemIcon', props)} + {getSlot(this)} + {getComponent(this, 'itemIcon', props)}
  • ); }, diff --git a/components/vc-menu/MenuItemGroup.jsx b/components/vc-menu/MenuItemGroup.jsx index d8525dbd3..56c430a93 100644 --- a/components/vc-menu/MenuItemGroup.jsx +++ b/components/vc-menu/MenuItemGroup.jsx @@ -1,11 +1,11 @@ import PropTypes from '../_util/vue-types'; -import { getComponentFromProp, getListeners } from '../_util/props-util'; +import { getComponent, getSlot } from '../_util/props-util'; // import { menuAllProps } from './util' const MenuItemGroup = { name: 'MenuItemGroup', - + inheritAttrs: false, props: { renderMenuItem: PropTypes.func, index: PropTypes.number, @@ -23,22 +23,19 @@ const MenuItemGroup = { }, }, render() { - const props = { ...this.$props }; - const { rootPrefixCls, title } = props; + const props = { ...this.$props, ...this.$attrs }; + const { class: cls = '', rootPrefixCls, title } = props; const titleClassName = `${rootPrefixCls}-item-group-title`; const listClassName = `${rootPrefixCls}-item-group-list`; // menuAllProps.props.forEach(key => delete props[key]) - const listeners = { ...getListeners(this) }; - delete listeners.click; - + delete props.onClick; + const children = getSlot(this); return ( -
  • +
  • - {getComponentFromProp(this, 'title')} + {getComponent(this, 'title')}
    - +
  • ); }, diff --git a/components/vc-menu/SubMenu.jsx b/components/vc-menu/SubMenu.jsx index 771accf5b..125e6a4b3 100644 --- a/components/vc-menu/SubMenu.jsx +++ b/components/vc-menu/SubMenu.jsx @@ -6,7 +6,7 @@ import { connect } from '../_util/store'; import SubPopupMenu from './SubPopupMenu'; import placements from './placements'; import BaseMixin from '../_util/BaseMixin'; -import { getComponentFromProp, filterEmpty, getListeners } from '../_util/props-util'; +import { getComponent, filterEmpty, getSlot, splitAttrs } from '../_util/props-util'; import { requestAnimationTimeout, cancelAnimationTimeout } from '../_util/requestAnimationTimeout'; import { noop, loopMenuItemRecursively, getMenuIdFromSubMenuEventKey } from './util'; import getTransitionProps from '../_util/getTransitionProps'; @@ -33,6 +33,7 @@ const updateDefaultActiveFirst = (store, eventKey, defaultActiveFirst) => { const SubMenu = { name: 'SubMenu', + inheritAttrs: false, props: { parentMenu: PropTypes.object, title: PropTypes.any, @@ -86,6 +87,9 @@ const SubMenu = { } updateDefaultActiveFirst(store, eventKey, value); + this.internalMenuId = undefined; + this.haveRendered = undefined; + this.haveOpened = undefined; return { // defaultActiveFirst: false, }; @@ -312,7 +316,7 @@ const SubMenu = { isChildrenSelected() { const ret = { find: false }; - loopMenuItemRecursively(this.$slots.default, this.$props.selectedKeys, ret); + loopMenuItemRecursively(getSlot(this), this.$props.selectedKeys, ret); return ret.find; }, // isOpen () { @@ -334,49 +338,44 @@ const SubMenu = { }, renderChildren(children) { - const props = this.$props; - const { select, deselect, openChange } = getListeners(this); + const props = { ...this.$props, ...this.$attrs }; const subPopupMenuProps = { - props: { - mode: props.mode === 'horizontal' ? 'vertical' : props.mode, - visible: props.isOpen, - level: props.level + 1, - inlineIndent: props.inlineIndent, - focusable: false, - selectedKeys: props.selectedKeys, - eventKey: `${props.eventKey}-menu-`, - openKeys: props.openKeys, - openTransitionName: props.openTransitionName, - openAnimation: props.openAnimation, - 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, - manualRef: this.saveMenuInstance, - itemIcon: getComponentFromProp(this, 'itemIcon'), - expandIcon: getComponentFromProp(this, 'expandIcon'), - children, - }, - on: { - click: this.onSubMenuClick, - select, - deselect, - openChange, - }, + mode: props.mode === 'horizontal' ? 'vertical' : props.mode, + visible: props.isOpen, + level: props.level + 1, + inlineIndent: props.inlineIndent, + focusable: false, + selectedKeys: props.selectedKeys, + eventKey: `${props.eventKey}-menu-`, + openKeys: props.openKeys, + openTransitionName: props.openTransitionName, + openAnimation: props.openAnimation, + 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, + manualRef: this.saveMenuInstance, + itemIcon: getComponent(this, 'itemIcon'), + expandIcon: getComponent(this, 'expandIcon'), + children, + click: this.onSubMenuClick, + onSelect: props.onSelect || noop, + onDeselect: props.onDeselect || noop, + onOpenChange: props.onOpenChange || noop, id: this.internalMenuId, }; - const baseProps = subPopupMenuProps.props; const haveRendered = this.haveRendered; this.haveRendered = true; - this.haveOpened = this.haveOpened || baseProps.visible || baseProps.forceSubMenuRender; + this.haveOpened = + this.haveOpened || subPopupMenuProps.visible || subPopupMenuProps.forceSubMenuRender; // never rendered not planning to, don't render if (!this.haveOpened) { return
    ; @@ -385,28 +384,24 @@ const SubMenu = { // 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 transitionAppear = haveRendered || !baseProps.visible || !baseProps.mode === 'inline'; - subPopupMenuProps.class = ` ${baseProps.prefixCls}-sub`; - let animProps = { appear: transitionAppear, css: false }; - let transitionProps = { - props: animProps, - on: {}, - }; - if (baseProps.openTransitionName) { - transitionProps = getTransitionProps(baseProps.openTransitionName, { + const transitionAppear = + haveRendered || !subPopupMenuProps.visible || !subPopupMenuProps.mode === 'inline'; + subPopupMenuProps.class = ` ${subPopupMenuProps.prefixCls}-sub`; + let transitionProps = { appear: transitionAppear, css: false }; + + if (subPopupMenuProps.openTransitionName) { + transitionProps = getTransitionProps(subPopupMenuProps.openTransitionName, { appear: transitionAppear, }); - } else if (typeof baseProps.openAnimation === 'object') { - animProps = { ...animProps, ...(baseProps.openAnimation.props || {}) }; + } else if (typeof subPopupMenuProps.openAnimation === 'object') { + transitionProps = { ...transitionProps, ...(subPopupMenuProps.openAnimation || {}) }; if (!transitionAppear) { - animProps.appear = false; + transitionProps.appear = false; } - } else if (typeof baseProps.openAnimation === 'string') { - transitionProps = getTransitionProps(baseProps.openAnimation, { appear: transitionAppear }); - } - - if (typeof baseProps.openAnimation === 'object' && baseProps.openAnimation.on) { - transitionProps.on = baseProps.openAnimation.on; + } else if (typeof subPopupMenuProps.openAnimation === 'string') { + transitionProps = getTransitionProps(subPopupMenuProps.openAnimation, { + appear: transitionAppear, + }); } return ( @@ -417,7 +412,8 @@ const SubMenu = { }, render() { - const props = this.$props; + const props = { ...this.$props, ...this.$attrs }; + const { onEvents } = splitAttrs(props); const { rootPrefixCls, parentMenu } = this; const isOpen = props.isOpen; const prefixCls = this.getPrefixCls(); @@ -425,6 +421,7 @@ const SubMenu = { const className = { [prefixCls]: true, [`${prefixCls}-${props.mode}`]: true, + [props.className]: !!props.className, [this.getOpenClassName()]: isOpen, [this.getActiveClassName()]: props.active || (isOpen && !isInlineMode), [this.getDisabledClassName()]: props.disabled, @@ -444,17 +441,17 @@ const SubMenu = { let titleMouseEvents = {}; if (!props.disabled) { mouseEvents = { - mouseleave: this.onMouseLeave, - mouseenter: this.onMouseEnter, + onMouseleave: this.onMouseLeave, + onMouseenter: this.onMouseEnter, }; // only works in title, not outer li titleClickEvents = { - click: this.onTitleClick, + onClick: this.onTitleClick, }; titleMouseEvents = { - mouseenter: this.onTitleMouseEnter, - mouseleave: this.onTitleMouseLeave, + onMouseenter: this.onTitleMouseEnter, + onMouseleave: this.onTitleMouseLeave, }; } @@ -472,16 +469,12 @@ const SubMenu = { }; } const titleProps = { - attrs: { - 'aria-expanded': isOpen, - ...ariaOwns, - 'aria-haspopup': 'true', - title: typeof props.title === 'string' ? props.title : undefined, - }, - on: { - ...titleMouseEvents, - ...titleClickEvents, - }, + 'aria-expanded': isOpen, + ...ariaOwns, + 'aria-haspopup': 'true', + title: typeof props.title === 'string' ? props.title : undefined, + ...titleMouseEvents, + ...titleClickEvents, style, class: `${prefixCls}-title`, ref: 'subMenuTitle', @@ -489,15 +482,15 @@ const SubMenu = { // expand custom icon should NOT be displayed in menu with horizontal mode. let icon = null; if (props.mode !== 'horizontal') { - icon = getComponentFromProp(this, 'expandIcon', props); + icon = getComponent(this, 'expandIcon', props); } const title = (
    - {getComponentFromProp(this, 'title')} + {getComponent(this, 'title')} {icon || }
    ); - const children = this.renderChildren(filterEmpty(this.$slots.default)); + const children = this.renderChildren(filterEmpty(getSlot(this))); const getPopupContainer = this.parentMenu.isRootMenu ? this.parentMenu.getPopupContainer @@ -506,7 +499,8 @@ const SubMenu = { const popupAlign = props.popupOffset ? { offset: props.popupOffset } : {}; const popupClassName = props.mode === 'inline' ? '' : props.popupClassName; const liProps = { - on: { ...omit(getListeners(this), ['click']), ...mouseEvents }, + ...omit(onEvents, ['onClick']), + ...mouseEvents, class: className, }; @@ -533,8 +527,8 @@ const SubMenu = { forceRender={props.forceSubMenuRender} // popupTransitionName='rc-menu-open-slide-up' // popupAnimation={transitionProps} + popup={children} > - {title} )} diff --git a/components/vc-menu/SubPopupMenu.jsx b/components/vc-menu/SubPopupMenu.jsx index 1194ab264..7c4d338d1 100644 --- a/components/vc-menu/SubPopupMenu.jsx +++ b/components/vc-menu/SubPopupMenu.jsx @@ -1,4 +1,4 @@ -import omit from 'omit.js'; +import { Comment } from 'vue'; import PropTypes from '../_util/vue-types'; import { connect } from '../_util/store'; import BaseMixin from '../_util/BaseMixin'; @@ -7,14 +7,7 @@ import classNames from 'classnames'; import { getKeyFromChildrenIndex, loopMenuItem, noop, isMobileDevice } from './util'; import DOMWrap from './DOMWrap'; import { cloneElement } from '../_util/vnode'; -import { - initDefaultProps, - getOptionProps, - getPropsData, - getEvents, - getComponentFromProp, - getListeners, -} from '../_util/props-util'; +import { initDefaultProps, getOptionProps, getComponent, splitAttrs } from '../_util/props-util'; function allDisabled(arr) { if (!arr.length) { @@ -77,6 +70,7 @@ export function getActiveKey(props, originalActiveKey) { const SubPopupMenu = { name: 'SubPopupMenu', + inheritAttrs: false, props: initDefaultProps( { // onSelect: PropTypes.func, @@ -271,85 +265,57 @@ const SubPopupMenu = { return null; }, getIcon(instance, name) { - if (instance.$createElement) { - const temp = instance[name]; - if (temp !== undefined) { - return temp; - } - return instance.$slots[name] || instance.$scopedSlots[name]; - } else { - const temp = getPropsData(instance)[name]; - if (temp !== undefined) { - return temp; - } - const slotsProp = []; - const componentOptions = instance.componentOptions || {}; - (componentOptions.children || []).forEach(child => { - if (child.data && child.data.slot === name) { - if (child.tag === 'template') { - slotsProp.push(child.children); - } else { - slotsProp.push(child); - } - } - }); - return slotsProp.length ? slotsProp : undefined; - } + return getComponent(instance, name); }, renderCommonMenuItem(child, i, extraProps) { - if (child.tag === undefined) { + if (child.type === Comment) { return child; } const state = this.$props.store.getState(); const props = this.$props; const key = getKeyFromChildrenIndex(child, props.eventKey, i); - const childProps = child.componentOptions.propsData || {}; + const childProps = { ...getOptionProps(child), ...child.props }; // child.props 包含事件 const isActive = key === state.activeKey[getEventKey(this.$props)]; if (!childProps.disabled) { // manualRef的执行顺序不能保证,使用key映射ref在this.instanceArray中的位置 this.instanceArrayKeyIndexMap[key] = Object.keys(this.instanceArrayKeyIndexMap).length; } - const childListeners = getEvents(child); const newChildProps = { - props: { - 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 ? noop : saveRef.bind(this, key), - eventKey: key, - active: !childProps.disabled && isActive, - multiple: props.multiple, - openTransitionName: this.getOpenTransitionName(), - openAnimation: props.openAnimation, - subMenuOpenDelay: props.subMenuOpenDelay, - subMenuCloseDelay: props.subMenuCloseDelay, - forceSubMenuRender: props.forceSubMenuRender, - builtinPlacements: props.builtinPlacements, - itemIcon: this.getIcon(child, 'itemIcon') || this.getIcon(this, 'itemIcon'), - expandIcon: this.getIcon(child, 'expandIcon') || this.getIcon(this, 'expandIcon'), - ...extraProps, - }, - on: { - click: e => { - (childListeners.click || noop)(e); - this.onClick(e); - }, - itemHover: this.onItemHover, - openChange: this.onOpenChange, - deselect: this.onDeselect, - // destroy: this.onDestroy, - select: this.onSelect, + 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 ? noop : saveRef.bind(this, key), + eventKey: key, + active: !childProps.disabled && isActive, + multiple: props.multiple, + openTransitionName: this.getOpenTransitionName(), + openAnimation: props.openAnimation, + subMenuOpenDelay: props.subMenuOpenDelay, + subMenuCloseDelay: props.subMenuCloseDelay, + forceSubMenuRender: props.forceSubMenuRender, + builtinPlacements: props.builtinPlacements, + itemIcon: this.getIcon(child, 'itemIcon') || this.getIcon(this, 'itemIcon'), + expandIcon: this.getIcon(child, 'expandIcon') || this.getIcon(this, 'expandIcon'), + ...extraProps, + onClick: e => { + (childProps.onClick || noop)(e); + this.onClick(e); }, + onItemHover: this.onItemHover, + onOpenChange: this.onOpenChange, + onDeselect: this.onDeselect, + // destroy: this.onDestroy, + onSelect: this.onSelect, }; // ref: https://github.com/ant-design/ant-design/issues/13943 if (props.mode === 'inline' || isMobileDevice()) { - newChildProps.props.triggerSubMenuAction = 'click'; + newChildProps.triggerSubMenuAction = 'click'; } return cloneElement(child, newChildProps); }, @@ -370,35 +336,34 @@ const SubPopupMenu = { }, }, render() { - const { ...props } = this.$props; + const props = { ...this.$props }; + const { onEvents } = splitAttrs(this.$attrs); const { eventKey, prefixCls, visible, level, mode, theme } = props; this.instanceArray = []; this.instanceArrayKeyIndexMap = {}; - const className = classNames(props.prefixCls, `${props.prefixCls}-${props.mode}`); + const className = classNames(props.class, props.prefixCls, `${props.prefixCls}-${props.mode}`); + // Otherwise, the propagated click event will trigger another onClick + delete onEvents.onClick; const domWrapProps = { - props: { - tag: 'ul', - // hiddenClassName: `${prefixCls}-hidden`, - visible, - prefixCls, - level, - mode, - theme, - overflowedIndicator: getComponentFromProp(this, 'overflowedIndicator'), - }, - attrs: { - role: props.role || 'menu', - }, + ...props, + tag: 'ul', + // hiddenClassName: `${prefixCls}-hidden`, + visible, + prefixCls, + level, + mode, + theme, + overflowedIndicator: getComponent(this, 'overflowedIndicator'), + role: props.role || 'menu', class: className, - // Otherwise, the propagated click event will trigger another onClick - on: omit(getListeners(this), ['click']), + ...onEvents, }; // if (props.id) { // domProps.id = props.id // } if (props.focusable) { - domWrapProps.attrs.tabIndex = '0'; - domWrapProps.on.keydown = this.onKeyDown; + domWrapProps.tabIndex = '0'; + domWrapProps.onKeydown = this.onKeyDown; } return ( // ESLint is not smart enough to know that the type of `children` was checked. diff --git a/components/vc-menu/commonPropsType.js b/components/vc-menu/commonPropsType.js index f7bab2c2b..d2a97a1e0 100644 --- a/components/vc-menu/commonPropsType.js +++ b/components/vc-menu/commonPropsType.js @@ -37,4 +37,5 @@ export default { itemIcon: PropTypes.any, expandIcon: PropTypes.any, overflowedIndicator: PropTypes.any, + children: PropTypes.any, }; diff --git a/components/vc-menu/util.js b/components/vc-menu/util.js index c48a4c690..353c46e12 100644 --- a/components/vc-menu/util.js +++ b/components/vc-menu/util.js @@ -51,68 +51,65 @@ export function loopMenuItemRecursively(children, keys, ret) { }); } -export const menuAllProps = { - props: [ - 'defaultSelectedKeys', - 'selectedKeys', - 'defaultOpenKeys', - 'openKeys', - 'mode', - 'getPopupContainer', - 'openTransitionName', - 'openAnimation', - 'subMenuOpenDelay', - 'subMenuCloseDelay', - 'forceSubMenuRender', - 'triggerSubMenuAction', - 'level', - 'selectable', - 'multiple', - 'visible', - 'focusable', - 'defaultActiveFirst', - 'prefixCls', - 'inlineIndent', - 'parentMenu', - 'title', - 'rootPrefixCls', - 'eventKey', - 'active', - 'popupAlign', - 'popupOffset', - 'isOpen', - 'renderMenuItem', - 'manualRef', - 'subMenuKey', - 'disabled', - 'index', - 'isSelected', - 'store', - 'activeKey', - 'builtinPlacements', - 'overflowedIndicator', +export const menuAllProps = [ + 'defaultSelectedKeys', + 'selectedKeys', + 'defaultOpenKeys', + 'openKeys', + 'mode', + 'getPopupContainer', + 'openTransitionName', + 'openAnimation', + 'subMenuOpenDelay', + 'subMenuCloseDelay', + 'forceSubMenuRender', + 'triggerSubMenuAction', + 'level', + 'selectable', + 'multiple', + 'visible', + 'focusable', + 'defaultActiveFirst', + 'prefixCls', + 'inlineIndent', + 'parentMenu', + 'title', + 'rootPrefixCls', + 'eventKey', + 'active', + 'popupAlign', + 'popupOffset', + 'isOpen', + 'renderMenuItem', + 'manualRef', + 'subMenuKey', + 'disabled', + 'index', + 'isSelected', + 'store', + 'activeKey', + 'builtinPlacements', + 'overflowedIndicator', - // the following keys found need to be removed from test regression - 'attribute', - 'value', - 'popupClassName', - 'inlineCollapsed', - 'menu', - 'theme', - 'itemIcon', - 'expandIcon', - ], - on: [ - 'select', - 'deselect', - 'destroy', - 'openChange', - 'itemHover', - 'titleMouseenter', - 'titleMouseleave', - 'titleClick', - ], -}; + // the following keys found need to be removed from test regression + 'attribute', + 'value', + 'popupClassName', + 'inlineCollapsed', + 'menu', + 'theme', + 'itemIcon', + 'expandIcon', + + 'onSelect', + 'onDeselect', + 'onDestroy', + 'onOpenChange', + 'onItemHover', + 'onTitleMouseenter', + 'onTitleMouseleave', + 'onTitleClick', +]; // ref: https://github.com/ant-design/ant-design/issues/14007 // ref: https://bugs.chromium.org/p/chromium/issues/detail?id=360889 diff --git a/examples/index.js b/examples/index.js index ec941d450..7a22247ba 100644 --- a/examples/index.js +++ b/examples/index.js @@ -29,6 +29,7 @@ import Popover from 'ant-design-vue/popover'; import notification from 'ant-design-vue/notification'; import message from 'ant-design-vue/message'; import Modal from 'ant-design-vue/modal'; +import Menu from 'ant-design-vue/menu'; import 'ant-design-vue/style.js'; const app = createApp(App); @@ -61,4 +62,5 @@ app .use(Popconfirm) .use(Popover) .use(Modal) + .use(Menu) .mount('#app');