diff --git a/components/menu/index.tsx b/components/menu/index.tsx index 842ace832..120eed074 100644 --- a/components/menu/index.tsx +++ b/components/menu/index.tsx @@ -1,322 +1,22 @@ -import { defineComponent, inject, provide, toRef, App, ExtractPropTypes, Plugin } from 'vue'; -import omit from 'omit.js'; -import VcMenu, { Divider, ItemGroup } from '../vc-menu'; -import SubMenu from './SubMenu'; -import PropTypes from '../_util/vue-types'; -import animation from '../_util/openAnimation'; -import warning from '../_util/warning'; -import Item from './MenuItem'; -import { hasProp, getOptionProps } from '../_util/props-util'; -import BaseMixin from '../_util/BaseMixin'; -import commonPropsType from '../vc-menu/commonPropsType'; -import { defaultConfigProvider } from '../config-provider'; -import { SiderContextProps } from '../layout/Sider'; -import { tuple } from '../_util/type'; -// import raf from '../_util/raf'; - -export const MenuMode = PropTypes.oneOf([ - 'vertical', - 'vertical-left', - 'vertical-right', - 'horizontal', - 'inline', -]); - -export const menuProps = { - ...commonPropsType, - theme: PropTypes.oneOf(tuple('light', 'dark')).def('light'), - mode: MenuMode.def('vertical'), - selectable: PropTypes.looseBool, - selectedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), - defaultSelectedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), - openKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), - defaultOpenKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), - openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - openTransitionName: PropTypes.string, - prefixCls: PropTypes.string, - multiple: PropTypes.looseBool, - inlineIndent: PropTypes.number.def(24), - inlineCollapsed: PropTypes.looseBool, - isRootMenu: PropTypes.looseBool.def(true), - focusable: PropTypes.looseBool.def(false), - onOpenChange: PropTypes.func, - onSelect: PropTypes.func, - onDeselect: PropTypes.func, - onClick: PropTypes.func, - onMouseenter: PropTypes.func, - onSelectChange: PropTypes.func, -}; - -export type MenuProps = Partial>; - -const Menu = defineComponent({ - name: 'AMenu', - mixins: [BaseMixin], - inheritAttrs: false, - props: menuProps, - Divider: { ...Divider, name: 'AMenuDivider' }, - Item: { ...Item, name: 'AMenuItem' }, - SubMenu: { ...SubMenu, name: 'ASubMenu' }, - ItemGroup: { ...ItemGroup, name: 'AMenuItemGroup' }, - emits: [ - 'update:selectedKeys', - 'update:openKeys', - 'mouseenter', - 'openChange', - 'click', - 'selectChange', - 'select', - 'deselect', - ], - setup() { - const layoutSiderContext = inject('layoutSiderContext', {}); - const layoutSiderCollapsed = toRef(layoutSiderContext, 'sCollapsed'); - return { - configProvider: inject('configProvider', defaultConfigProvider), - layoutSiderContext, - layoutSiderCollapsed, - propsUpdating: false, - switchingModeFromInline: false, - leaveAnimationExecutedWhenInlineCollapsed: false, - inlineOpenKeys: [], - }; - }, - data() { - const props: MenuProps = getOptionProps(this); - warning( - !('inlineCollapsed' in props && props.mode !== 'inline'), - 'Menu', - "`inlineCollapsed` should only be used when Menu's `mode` is inline.", - ); - let sOpenKeys: (number | string)[]; - - if ('openKeys' in props) { - sOpenKeys = props.openKeys; - } else if ('defaultOpenKeys' in props) { - sOpenKeys = props.defaultOpenKeys; - } - return { - sOpenKeys, - }; - }, - // beforeUnmount() { - // raf.cancel(this.mountRafId); - // }, - watch: { - mode(val, oldVal) { - if (oldVal === 'inline' && val !== 'inline') { - this.switchingModeFromInline = true; - } - }, - openKeys(val) { - this.setState({ sOpenKeys: val }); - }, - inlineCollapsed(val) { - this.collapsedChange(val); - }, - layoutSiderCollapsed(val) { - this.collapsedChange(val); - }, - }, - created() { - provide('getInlineCollapsed', this.getInlineCollapsed); - provide('menuPropsContext', this.$props); - }, - updated() { - this.propsUpdating = false; - }, - methods: { - collapsedChange(val: unknown) { - if (this.propsUpdating) { - return; - } - this.propsUpdating = true; - if (!hasProp(this, 'openKeys')) { - if (val) { - this.switchingModeFromInline = true; - this.inlineOpenKeys = this.sOpenKeys; - this.setState({ sOpenKeys: [] }); - } else { - this.setState({ sOpenKeys: this.inlineOpenKeys }); - this.inlineOpenKeys = []; - } - } else if (val) { - // 缩起时,openKeys置为空的动画会闪动,react可以通过是否传递openKeys避免闪动,vue不是很方便动态传递openKeys - this.switchingModeFromInline = true; - } - }, - restoreModeVerticalFromInline() { - if (this.switchingModeFromInline) { - this.switchingModeFromInline = false; - this.$forceUpdate(); - } - }, - // 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 - handleMouseEnter(e: Event) { - this.restoreModeVerticalFromInline(); - this.$emit('mouseenter', e); - }, - handleTransitionEnd(e: 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 SVGAnimationElement | HTMLElement; - // 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(); - } - }, - handleClick(e: Event) { - this.handleOpenChange([]); - this.$emit('click', e); - }, - handleSelect(info) { - this.$emit('update:selectedKeys', info.selectedKeys); - this.$emit('select', info); - this.$emit('selectChange', info.selectedKeys); - }, - handleDeselect(info) { - this.$emit('update:selectedKeys', info.selectedKeys); - this.$emit('deselect', info); - this.$emit('selectChange', info.selectedKeys); - }, - handleOpenChange(openKeys: (number | string)[]) { - this.setOpenKeys(openKeys); - this.$emit('update:openKeys', openKeys); - this.$emit('openChange', openKeys); - }, - setOpenKeys(openKeys: (number | string)[]) { - if (!hasProp(this, 'openKeys')) { - this.setState({ sOpenKeys: openKeys }); - } - }, - getRealMenuMode() { - const inlineCollapsed = this.getInlineCollapsed(); - if (this.switchingModeFromInline && inlineCollapsed) { - return 'inline'; - } - const { mode } = this.$props; - return inlineCollapsed ? 'vertical' : mode; - }, - getInlineCollapsed() { - const { inlineCollapsed } = this.$props; - if (this.layoutSiderContext.sCollapsed !== undefined) { - return this.layoutSiderContext.sCollapsed; - } - return inlineCollapsed; - }, - getMenuOpenAnimation(menuMode: string) { - const { openAnimation, openTransitionName } = this.$props; - let menuOpenAnimation = openAnimation || openTransitionName; - if (openAnimation === undefined && openTransitionName === undefined) { - if (menuMode === 'horizontal') { - menuOpenAnimation = 'slide-up'; - } else if (menuMode === 'inline') { - menuOpenAnimation = animation; - } else { - // When mode switch from inline - // submenu should hide without animation - if (this.switchingModeFromInline) { - menuOpenAnimation = ''; - this.switchingModeFromInline = false; - } else { - menuOpenAnimation = 'zoom-big'; - } - } - } - return menuOpenAnimation; - }, - }, - render() { - const { layoutSiderContext } = this; - const { collapsedWidth } = layoutSiderContext; - const { getPopupContainer: getContextPopupContainer } = this.configProvider; - const props = getOptionProps(this); - const { prefixCls: customizePrefixCls, theme, getPopupContainer } = props; - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('menu', customizePrefixCls); - const menuMode = this.getRealMenuMode(); - const menuOpenAnimation = this.getMenuOpenAnimation(menuMode); - const { class: className, ...otherAttrs } = this.$attrs; - const menuClassName = { - [className as string]: className, - [`${prefixCls}-${theme}`]: true, - [`${prefixCls}-inline-collapsed`]: this.getInlineCollapsed(), - }; - - const menuProps = { - ...omit(props, [ - 'inlineCollapsed', - 'onUpdate:selectedKeys', - 'onUpdate:openKeys', - 'onSelectChange', - ]), - getPopupContainer: getPopupContainer || getContextPopupContainer, - openKeys: this.sOpenKeys, - mode: menuMode, - prefixCls, - ...otherAttrs, - onSelect: this.handleSelect, - onDeselect: this.handleDeselect, - onOpenChange: this.handleOpenChange, - onMouseenter: this.handleMouseEnter, - onTransitionend: this.handleTransitionEnd, - // children: getSlot(this), - }; - if (!hasProp(this, 'selectedKeys')) { - delete menuProps.selectedKeys; - } - - if (menuMode !== 'inline') { - // closing vertical popup submenu after click it - menuProps.onClick = this.handleClick; - menuProps.openTransitionName = menuOpenAnimation; - } else { - menuProps.onClick = (e: Event) => { - this.$emit('click', e); - }; - menuProps.openAnimation = menuOpenAnimation; - } - - // https://github.com/ant-design/ant-design/issues/8587 - const hideMenu = - this.getInlineCollapsed() && - (collapsedWidth === 0 || collapsedWidth === '0' || collapsedWidth === '0px'); - if (hideMenu) { - menuProps.openKeys = []; - } - - return ; - }, -}); - +import Menu from './src/Menu'; +import MenuItem from './src/MenuItem'; +import SubMenu from './src/SubMenu'; +import ItemGroup from './src/ItemGroup'; +import Divider from './src/Divider'; +import { App } from 'vue'; /* istanbul ignore next */ Menu.install = function(app: 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); + app.component(MenuItem.name, MenuItem); + app.component(SubMenu.name, SubMenu); + app.component(Divider.name, Divider); + app.component(ItemGroup.name, ItemGroup); return app; }; export default Menu as typeof Menu & Plugin & { - readonly Item: typeof Item; + readonly Item: typeof MenuItem; readonly SubMenu: typeof SubMenu; readonly Divider: typeof Divider; readonly ItemGroup: typeof ItemGroup; diff --git a/components/new-menu/src/Divider.tsx b/components/menu/src/Divider.tsx similarity index 100% rename from components/new-menu/src/Divider.tsx rename to components/menu/src/Divider.tsx diff --git a/components/menu/src/ItemGroup.tsx b/components/menu/src/ItemGroup.tsx new file mode 100644 index 000000000..b44604b47 --- /dev/null +++ b/components/menu/src/ItemGroup.tsx @@ -0,0 +1,29 @@ +import { getPropsSlot } from '../../_util/props-util'; +import { computed, defineComponent } from 'vue'; +import PropTypes from '../../_util/vue-types'; +import { useInjectMenu } from './hooks/useMenuContext'; + +export default defineComponent({ + name: 'AMenuItemGroup', + props: { + title: PropTypes.VNodeChild, + }, + slots: ['title'], + setup(props, { slots }) { + const { prefixCls } = useInjectMenu(); + const groupPrefixCls = computed(() => `${prefixCls.value}-item-group`); + return () => { + return ( +
  • e.stopPropagation()} class={groupPrefixCls.value}> +
    + {getPropsSlot(slots, props, 'title')} +
    +
      {slots.default?.()}
    +
  • + ); + }; + }, +}); diff --git a/components/menu/src/Menu.tsx b/components/menu/src/Menu.tsx new file mode 100644 index 000000000..918adb0bc --- /dev/null +++ b/components/menu/src/Menu.tsx @@ -0,0 +1,21 @@ +import usePrefixCls from 'ant-design-vue/es/_util/hooks/usePrefixCls'; +import { defineComponent, ExtractPropTypes } from 'vue'; +import useProvideMenu from './hooks/useMenuContext'; + +export const menuProps = { + prefixCls: String, +}; + +export type MenuProps = Partial>; + +export default defineComponent({ + name: 'AMenu', + props: menuProps, + setup(props, { slots }) { + const prefixCls = usePrefixCls('menu', props); + useProvideMenu({ prefixCls }); + return () => { + return
    {slots.default?.()}
    ; + }; + }, +}); diff --git a/components/menu/src/MenuItem.tsx b/components/menu/src/MenuItem.tsx new file mode 100644 index 000000000..ecdb8916c --- /dev/null +++ b/components/menu/src/MenuItem.tsx @@ -0,0 +1,16 @@ +import { defineComponent, getCurrentInstance } from 'vue'; + +let indexGuid = 0; + +export default defineComponent({ + name: 'AMenuItem', + setup(props, { slots }) { + const instance = getCurrentInstance(); + const key = instance.vnode.key; + const uniKey = `menu_item_${++indexGuid}`; + + return () => { + return
  • {slots.default?.()}
  • ; + }; + }, +}); diff --git a/components/new-menu/src/SubMenu.tsx b/components/menu/src/SubMenu.tsx similarity index 100% rename from components/new-menu/src/SubMenu.tsx rename to components/menu/src/SubMenu.tsx diff --git a/components/menu/src/hooks/useMenuContext.ts b/components/menu/src/hooks/useMenuContext.ts new file mode 100644 index 000000000..0864a82d8 --- /dev/null +++ b/components/menu/src/hooks/useMenuContext.ts @@ -0,0 +1,70 @@ +import { computed, ComputedRef, inject, InjectionKey, provide } from 'vue'; + +// import { +// BuiltinPlacements, +// MenuClickEventHandler, +// MenuMode, +// RenderIconType, +// TriggerSubMenuAction, +// } from '../interface'; + +export interface MenuContextProps { + prefixCls: ComputedRef; + // openKeys: string[]; + // rtl?: boolean; + + // // Mode + // mode: MenuMode; + + // // Disabled + // disabled?: boolean; + // // Used for overflow only. Prevent hidden node trigger open + // overflowDisabled?: boolean; + + // // Active + // activeKey: string; + // onActive: (key: string) => void; + // onInactive: (key: string) => void; + + // // Selection + // selectedKeys: string[]; + + // // Level + // inlineIndent: number; + + // // Motion + // // motion?: CSSMotionProps; + // // defaultMotions?: Partial<{ [key in MenuMode | 'other']: CSSMotionProps }>; + + // // Popup + // subMenuOpenDelay: number; + // subMenuCloseDelay: number; + // forceSubMenuRender?: boolean; + // builtinPlacements?: BuiltinPlacements; + // triggerSubMenuAction?: TriggerSubMenuAction; + + // // Icon + // itemIcon?: RenderIconType; + // expandIcon?: RenderIconType; + + // // Function + // onItemClick: MenuClickEventHandler; + // onOpenChange: (key: string, open: boolean) => void; + // getPopupContainer: (node: HTMLElement) => HTMLElement; +} + +const MenuContextKey: InjectionKey = Symbol('menuContextKey'); + +const useProvideMenu = (props: MenuContextProps) => { + provide(MenuContextKey, props); +}; + +const useInjectMenu = () => { + return inject(MenuContextKey, { + prefixCls: computed(() => 'ant'), + }); +}; + +export { useProvideMenu, MenuContextKey, useInjectMenu }; + +export default useProvideMenu; diff --git a/components/menu/src/interface.ts b/components/menu/src/interface.ts new file mode 100644 index 000000000..e1db27739 --- /dev/null +++ b/components/menu/src/interface.ts @@ -0,0 +1,39 @@ +// ========================== Basic ========================== +export type MenuMode = 'horizontal' | 'vertical' | 'inline'; + +export type BuiltinPlacements = Record; + +export type TriggerSubMenuAction = 'click' | 'hover'; + +export interface RenderIconInfo { + isSelected?: boolean; + isOpen?: boolean; + isSubMenu?: boolean; + disabled?: boolean; +} + +export type RenderIconType = (props: RenderIconInfo) => any; + +export interface MenuInfo { + key: string; + keyPath: string[]; + domEvent: MouseEvent | KeyboardEvent; +} + +export interface MenuTitleInfo { + key: string; + domEvent: MouseEvent | KeyboardEvent; +} + +// ========================== Hover ========================== +export type MenuHoverEventHandler = (info: { key: string; domEvent: MouseEvent }) => void; + +// ======================== Selection ======================== +export interface SelectInfo extends MenuInfo { + selectedKeys: string[]; +} + +export type SelectEventHandler = (info: SelectInfo) => void; + +// ========================== Click ========================== +export type MenuClickEventHandler = (info: MenuInfo) => void; diff --git a/components/menu/src/placements.ts b/components/menu/src/placements.ts new file mode 100644 index 000000000..7b45acf92 --- /dev/null +++ b/components/menu/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; diff --git a/components/menu/style/dark.less b/components/menu/style/dark.less index 03d837a6a..1ad2abf99 100644 --- a/components/menu/style/dark.less +++ b/components/menu/style/dark.less @@ -1,7 +1,8 @@ .@{menu-prefix-cls} { // dark theme - &-dark, - &-dark &-sub { + &&-dark, + &-dark &-sub, + &&-dark &-sub { color: @menu-dark-color; background: @menu-dark-bg; .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow { @@ -19,8 +20,7 @@ } &-dark &-inline&-sub { - background: @menu-dark-submenu-bg; - box-shadow: 0 2px 8px fade(@black, 45%) inset; + background: @menu-dark-inline-submenu-bg; } &-dark&-horizontal { @@ -31,17 +31,23 @@ &-dark&-horizontal > &-submenu { top: 0; margin-top: 0; + padding: @menu-item-padding; border-color: @menu-dark-bg; border-bottom: 0; } + &-dark&-horizontal > &-item:hover { + background-color: @menu-dark-item-active-bg; + } + &-dark&-horizontal > &-item > a::before { bottom: 0; } &-dark &-item, &-dark &-item-group-title, - &-dark &-item > a { + &-dark &-item > a, + &-dark &-item > span > a { color: @menu-dark-color; } @@ -77,7 +83,8 @@ &-dark &-submenu-title:hover { color: @menu-dark-highlight-color; background-color: transparent; - > a { + > a, + > span > a { color: @menu-dark-highlight-color; } > .@{menu-prefix-cls}-submenu-title, @@ -95,6 +102,10 @@ background-color: @menu-dark-item-hover-bg; } + &-dark&-dark:not(&-horizontal) &-item-selected { + background-color: @menu-dark-item-active-bg; + } + &-dark &-item-selected { color: @menu-dark-highlight-color; border-right: 0; @@ -102,14 +113,19 @@ border-right: 0; } > a, - > a:hover { + > span > a, + > a:hover, + > span > a:hover { color: @menu-dark-highlight-color; } + + .@{menu-prefix-cls}-item-icon, .@{iconfont-css-prefix} { color: @menu-dark-selected-item-icon-color; - } - .@{iconfont-css-prefix} + span { - color: @menu-dark-selected-item-text-color; + + + span { + color: @menu-dark-selected-item-text-color; + } } } @@ -122,7 +138,8 @@ &-dark &-item-disabled, &-dark &-submenu-disabled { &, - > a { + > a, + > span > a { color: @disabled-color-dark !important; opacity: 0.8; } diff --git a/components/menu/style/index.less b/components/menu/style/index.less index 53ac60599..b8956d2fc 100644 --- a/components/menu/style/index.less +++ b/components/menu/style/index.less @@ -1,7 +1,15 @@ @import '../../style/themes/index'; @import '../../style/mixins/index'; +@import './status'; @menu-prefix-cls: ~'@{ant-prefix}-menu'; +@menu-animation-duration-normal: 0.15s; + +.accessibility-focus() { + box-shadow: 0 0 0 2px fade(@primary-color, 20%); +} + +// TODO: Should remove icon style compatible in v5 // default theme .@{menu-prefix-cls} { @@ -10,14 +18,21 @@ margin-bottom: 0; padding-left: 0; // Override default ul/ol color: @menu-item-color; + font-size: @menu-item-font-size; line-height: 0; // Fix display inline-block gap + text-align: left; list-style: none; background: @menu-bg; outline: none; box-shadow: @box-shadow-base; - transition: background 0.3s, width 0.3s cubic-bezier(0.2, 0, 0, 1) 0s; + transition: background @animation-duration-slow, + width @animation-duration-slow cubic-bezier(0.2, 0, 0, 1) 0s; .clearfix(); + &&-root:focus-visible { + .accessibility-focus(); + } + ul, ol { margin: 0; @@ -25,22 +40,29 @@ list-style: none; } - &-hidden { + &-hidden, + &-submenu-hidden { display: none; } &-item-group-title { + height: @menu-item-group-height; padding: 8px 16px; color: @menu-item-group-title-color; - font-size: @font-size-base; - line-height: @line-height-base; - transition: all 0.3s; + font-size: @menu-item-group-title-font-size; + line-height: @menu-item-group-height; + transition: all @animation-duration-slow; } + &-horizontal &-submenu { + transition: border-color @animation-duration-slow @ease-in-out, + background @animation-duration-slow @ease-in-out; + } &-submenu, &-submenu-inline { - transition: border-color 0.3s @ease-in-out, background 0.3s @ease-in-out, - padding 0.15s @ease-in-out; + transition: border-color @animation-duration-slow @ease-in-out, + background @animation-duration-slow @ease-in-out, + padding @menu-animation-duration-normal @ease-in-out; } &-submenu-selected { @@ -54,11 +76,11 @@ &-submenu &-sub { cursor: initial; - transition: background 0.3s @ease-in-out, padding 0.3s @ease-in-out; + transition: background @animation-duration-slow @ease-in-out, + padding @animation-duration-slow @ease-in-out; } - &-item > a { - display: block; + &-item a { color: @menu-item-color; &:hover { color: @menu-highlight-color; @@ -75,7 +97,7 @@ } // https://github.com/ant-design/ant-design/issues/19809 - &-item > .@{ant-prefix}-badge > a { + &-item > .@{ant-prefix}-badge a { color: @menu-item-color; &:hover { color: @menu-highlight-color; @@ -110,8 +132,8 @@ &-item-selected { color: @menu-highlight-color; - > a, - > a:hover { + a, + a:hover { color: @menu-highlight-color; } } @@ -125,6 +147,7 @@ &-vertical-left { border-right: @border-width-base @border-style-base @border-color-split; } + &-vertical-right { border-left: @border-width-base @border-style-base @border-color-split; } @@ -133,9 +156,17 @@ &-vertical-left&-sub, &-vertical-right&-sub { min-width: 160px; + max-height: calc(100vh - 100px); padding: 0; + overflow: hidden; border-right: 0; - transform-origin: 0 0; + + // https://github.com/ant-design/ant-design/issues/22244 + // https://github.com/ant-design/ant-design/issues/26812 + &:not([class*='-active']) { + overflow-x: hidden; + overflow-y: auto; + } .@{menu-prefix-cls}-item { left: 0; @@ -155,26 +186,48 @@ min-width: 114px; // in case of submenu width is too big: https://codesandbox.io/s/qvpwm6mk66 } + &-horizontal &-item, + &-horizontal &-submenu-title { + transition: border-color @animation-duration-slow, background @animation-duration-slow; + } + &-item, &-submenu-title { position: relative; display: block; margin: 0; - padding: 0 20px; + padding: @menu-item-padding; white-space: nowrap; cursor: pointer; - transition: color 0.3s @ease-in-out, border-color 0.3s @ease-in-out, - background 0.3s @ease-in-out, padding 0.15s @ease-in-out; + transition: border-color @animation-duration-slow, background @animation-duration-slow, + padding @animation-duration-slow @ease-in-out; + + .@{menu-prefix-cls}-item-icon, .@{iconfont-css-prefix} { min-width: 14px; - margin-right: 10px; font-size: @menu-icon-size; - transition: font-size 0.15s @ease-out, margin 0.3s @ease-in-out; + transition: font-size @menu-animation-duration-normal @ease-out, + margin @animation-duration-slow @ease-in-out, color @animation-duration-slow; + span { + margin-left: @menu-icon-margin-right; opacity: 1; - transition: opacity 0.3s @ease-in-out, width 0.3s @ease-in-out; + // transition: opacity @animation-duration-slow @ease-in-out, + // width @animation-duration-slow @ease-in-out, color @animation-duration-slow; + transition: opacity @animation-duration-slow @ease-in-out, margin @animation-duration-slow, + color @animation-duration-slow; } } + + &.@{menu-prefix-cls}-item-only-child { + > .@{iconfont-css-prefix}, + > .@{menu-prefix-cls}-item-icon { + margin-right: 0; + } + } + + &:focus-visible { + .accessibility-focus(); + } } & > &-item-divider { @@ -190,94 +243,105 @@ &-popup { position: absolute; z-index: @zindex-dropdown; - // background: @menu-popup-bg; + background: transparent; border-radius: @border-radius-base; + box-shadow: none; + transform-origin: 0 0; - .submenu-title-wrapper { - padding-right: 20px; - } - + // https://github.com/ant-design/ant-design/issues/13955 &::before { position: absolute; top: -7px; right: 0; bottom: 0; left: 0; + z-index: -1; + width: 100%; + height: 100%; opacity: 0.0001; content: ' '; } } + // https://github.com/ant-design/ant-design/issues/13955 + &-placement-rightTop::before { + top: 0; + left: -7px; + } + > .@{menu-prefix-cls} { background-color: @menu-bg; border-radius: @border-radius-base; &-submenu-title::after { - transition: transform 0.3s @ease-in-out; + transition: transform @animation-duration-slow @ease-in-out; } } - &-vertical, - &-vertical-left, - &-vertical-right, - &-inline { - > .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow { + &-popup > .@{menu-prefix-cls} { + background-color: @menu-popup-bg; + } + + &-expand-icon, + &-arrow { + position: absolute; + top: 50%; + right: 16px; + width: 10px; + color: @menu-item-color; + transform: translateY(-50%); + transition: transform @animation-duration-slow @ease-in-out; + } + + &-arrow { + // → + &::before, + &::after { position: absolute; - top: 50%; - right: 16px; - width: 10px; - transition: transform 0.3s @ease-in-out; - &::before, - &::after { - position: absolute; - width: 6px; - height: 1.5px; - // background + background-image to makes before & after cross have same color. - // Since `linear-gradient` not work on IE9, we should hack it. - // ref: https://github.com/ant-design/ant-design/issues/15910 - background: @menu-bg; - background: ~'@{menu-item-color} \9'; - background-image: linear-gradient(to right, @menu-item-color, @menu-item-color); - background-image: ~'none \9'; - border-radius: 2px; - transition: background 0.3s @ease-in-out, transform 0.3s @ease-in-out, - top 0.3s @ease-in-out; - content: ''; - } - &::before { - transform: rotate(45deg) translateY(-2px); - } - &::after { - transform: rotate(-45deg) translateY(2px); - } + width: 6px; + height: 1.5px; + background-color: currentColor; + border-radius: 2px; + transition: background @animation-duration-slow @ease-in-out, + transform @animation-duration-slow @ease-in-out, top @animation-duration-slow @ease-in-out, + color @animation-duration-slow @ease-in-out; + content: ''; } - > .@{menu-prefix-cls}-submenu-title:hover .@{menu-prefix-cls}-submenu-arrow { - &::after, - &::before { - background: linear-gradient(to right, @menu-highlight-color, @menu-highlight-color); - } - } - } - - &-inline > .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow { &::before { - transform: rotate(-45deg) translateX(2px); + transform: rotate(45deg) translateY(-2.5px); } &::after { - transform: rotate(45deg) translateX(-2px); + transform: rotate(-45deg) translateY(2.5px); } } - &-open { - &.@{menu-prefix-cls}-submenu-inline - > .@{menu-prefix-cls}-submenu-title - .@{menu-prefix-cls}-submenu-arrow { - transform: translateY(-2px); - &::after { - transform: rotate(-45deg) translateX(-2px); - } - &::before { - transform: rotate(45deg) translateX(2px); - } + &:hover > &-title > &-expand-icon, + &:hover > &-title > &-arrow { + color: @menu-highlight-color; + } + + .@{menu-prefix-cls}-inline-collapsed &-arrow, + &-inline &-arrow { + // ↓ + &::before { + transform: rotate(-45deg) translateX(2.5px); + } + &::after { + transform: rotate(45deg) translateX(-2.5px); + } + } + + &-horizontal &-arrow { + display: none; + } + + &-open&-inline > &-title > &-arrow { + // ↑ + transform: translateY(-2px); + &::after { + transform: rotate(-45deg) translateX(-2.5px); + } + &::before { + transform: rotate(45deg) translateX(2.5px); } } } @@ -286,18 +350,34 @@ &-vertical-left &-submenu-selected, &-vertical-right &-submenu-selected { color: @menu-highlight-color; - > a { - color: @menu-highlight-color; - } } &-horizontal { - line-height: 46px; - white-space: nowrap; + line-height: @menu-horizontal-line-height; border: 0; border-bottom: @border-width-base @border-style-base @border-color-split; box-shadow: none; + &:not(.@{menu-prefix-cls}-dark) { + > .@{menu-prefix-cls}-item, + > .@{menu-prefix-cls}-submenu { + margin: @menu-item-padding; + margin-top: -1px; + margin-bottom: 0; + padding: @menu-item-padding; + padding-right: 0; + padding-left: 0; + + &:hover, + &-active, + &-open, + &-selected { + color: @menu-highlight-color; + border-bottom: 2px solid @menu-highlight-color; + } + } + } + > .@{menu-prefix-cls}-item, > .@{menu-prefix-cls}-submenu { position: relative; @@ -305,19 +385,14 @@ display: inline-block; vertical-align: bottom; border-bottom: 2px solid transparent; + } - &:hover, - &-active, - &-open, - &-selected { - color: @menu-highlight-color; - border-bottom: 2px solid @menu-highlight-color; - } + > .@{menu-prefix-cls}-submenu > .@{menu-prefix-cls}-submenu-title { + padding: 0; } > .@{menu-prefix-cls}-item { - > a { - display: block; + a { color: @menu-item-color; &:hover { color: @menu-highlight-color; @@ -326,7 +401,7 @@ bottom: -2px; } } - &-selected > a { + &-selected a { color: @menu-highlight-color; } } @@ -353,7 +428,8 @@ border-right: @menu-item-active-border-width solid @menu-highlight-color; transform: scaleY(0.0001); opacity: 0; - transition: transform 0.15s @ease-out, opacity 0.15s @ease-out; + transition: transform @menu-animation-duration-normal @ease-out, + opacity @menu-animation-duration-normal @ease-out; content: ''; } } @@ -365,7 +441,6 @@ margin-bottom: @menu-item-vertical-margin; padding: 0 16px; overflow: hidden; - font-size: @menu-item-font-size; line-height: @menu-item-height; text-overflow: ellipsis; } @@ -386,6 +461,13 @@ } } + &-vertical { + .@{menu-prefix-cls}-item-group-list .@{menu-prefix-cls}-submenu-title, + .@{menu-prefix-cls}-submenu-title { + padding-right: 34px; + } + } + &-inline { width: 100%; .@{menu-prefix-cls}-selected, @@ -393,7 +475,8 @@ &::after { transform: scaleY(1); opacity: 1; - transition: transform 0.15s @ease-in-out, opacity 0.15s @ease-in-out; + transition: transform @menu-animation-duration-normal @ease-in-out, + opacity @menu-animation-duration-normal @ease-in-out; } } @@ -402,13 +485,37 @@ width: ~'calc(100% + 1px)'; } + .@{menu-prefix-cls}-item-group-list .@{menu-prefix-cls}-submenu-title, .@{menu-prefix-cls}-submenu-title { padding-right: 34px; } + + // Motion enhance for first level + &.@{menu-prefix-cls}-root { + .@{menu-prefix-cls}-item, + .@{menu-prefix-cls}-submenu-title { + display: flex; + align-items: center; + transition: border-color @animation-duration-slow, background @animation-duration-slow, + padding 0.1s @ease-out; + + > .@{menu-prefix-cls}-title-content { + flex: auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + } + + > * { + flex: none; + } + } + } } - &-inline-collapsed { + &&-inline-collapsed { width: @menu-collapsed-width; + > .@{menu-prefix-cls}-item, > .@{menu-prefix-cls}-item-group > .@{menu-prefix-cls}-item-group-list @@ -419,24 +526,34 @@ > .@{menu-prefix-cls}-submenu-title, > .@{menu-prefix-cls}-submenu > .@{menu-prefix-cls}-submenu-title { left: 0; - padding: 0 ((@menu-collapsed-width - @menu-icon-size-lg) / 2) !important; + padding: 0 ~'calc(50% - @{menu-icon-size-lg} / 2)'; text-overflow: clip; + .@{menu-prefix-cls}-submenu-arrow { - display: none; + opacity: 0; } + + .@{menu-prefix-cls}-item-icon, .@{iconfont-css-prefix} { margin: 0; font-size: @menu-icon-size-lg; line-height: @menu-item-height; + span { display: inline-block; - max-width: 0; opacity: 0; } } } + + .@{menu-prefix-cls}-item-icon, + .@{iconfont-css-prefix} { + display: inline-block; + } + &-tooltip { pointer-events: none; + + .@{menu-prefix-cls}-item-icon, .@{iconfont-css-prefix} { display: none; } @@ -470,8 +587,19 @@ box-shadow: none; } + &-root&-inline-collapsed { + .@{menu-prefix-cls}-item, + .@{menu-prefix-cls}-submenu .@{menu-prefix-cls}-submenu-title { + > .@{menu-prefix-cls}-inline-collapsed-noicon { + font-size: @menu-icon-size-lg; + text-align: center; + } + } + } + &-sub&-inline { padding: 0; + background: @menu-inline-submenu-bg; border: 0; border-radius: 0; box-shadow: none; @@ -495,7 +623,7 @@ background: none; border-color: transparent !important; cursor: not-allowed; - > a { + a { color: @disabled-color !important; pointer-events: none; } @@ -512,4 +640,12 @@ } } +// Integration with header element so menu items have the same height +.@{ant-prefix}-layout-header { + .@{menu-prefix-cls} { + line-height: inherit; + } +} + @import './dark'; +@import './rtl'; diff --git a/components/new-menu/style/index.tsx b/components/menu/style/index.tsx similarity index 100% rename from components/new-menu/style/index.tsx rename to components/menu/style/index.tsx diff --git a/components/new-menu/style/rtl.less b/components/menu/style/rtl.less similarity index 100% rename from components/new-menu/style/rtl.less rename to components/menu/style/rtl.less diff --git a/components/new-menu/style/status.less b/components/menu/style/status.less similarity index 100% rename from components/new-menu/style/status.less rename to components/menu/style/status.less diff --git a/components/new-menu/index.tsx b/components/new-menu/index.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/components/new-menu/src/ItemGroup.tsx b/components/new-menu/src/ItemGroup.tsx deleted file mode 100644 index 191c16447..000000000 --- a/components/new-menu/src/ItemGroup.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { getPropsSlot } from '../../_util/props-util'; -import { defineComponent } from 'vue'; - -export default defineComponent({ - name: 'AMenuItemGroup', - setup(props, { slots }) { - return () => { - return ( -
  • - {getPropsSlot(slots, props, 'title')} -
      {slots.default?.()}
    -
  • - ); - }; - }, -}); diff --git a/components/new-menu/src/Menu.tsx b/components/new-menu/src/Menu.tsx deleted file mode 100644 index 0bf679ac7..000000000 --- a/components/new-menu/src/Menu.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineComponent } from 'vue'; - -export default defineComponent({ - name: 'AMenu', - setup(props, { slots }) { - return () => { - return
    {slots.default?.()}
    ; - }; - }, -}); diff --git a/components/new-menu/src/MenuItem.tsx b/components/new-menu/src/MenuItem.tsx deleted file mode 100644 index 20268d09a..000000000 --- a/components/new-menu/src/MenuItem.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { defineComponent } from 'vue'; - -export default defineComponent({ - name: 'AMenuItem', - setup(props, { slots }) { - return () => { - return
  • {slots.default?.()}
  • ; - }; - }, -}); diff --git a/components/menu/MenuItem.tsx b/components/old-menu/MenuItem.tsx similarity index 100% rename from components/menu/MenuItem.tsx rename to components/old-menu/MenuItem.tsx diff --git a/components/menu/SubMenu.tsx b/components/old-menu/SubMenu.tsx similarity index 100% rename from components/menu/SubMenu.tsx rename to components/old-menu/SubMenu.tsx diff --git a/components/menu/__tests__/__snapshots__/demo.test.js.snap b/components/old-menu/__tests__/__snapshots__/demo.test.js.snap similarity index 100% rename from components/menu/__tests__/__snapshots__/demo.test.js.snap rename to components/old-menu/__tests__/__snapshots__/demo.test.js.snap diff --git a/components/menu/__tests__/demo.test.js b/components/old-menu/__tests__/demo.test.js similarity index 100% rename from components/menu/__tests__/demo.test.js rename to components/old-menu/__tests__/demo.test.js diff --git a/components/menu/__tests__/index.test.js b/components/old-menu/__tests__/index.test.js similarity index 100% rename from components/menu/__tests__/index.test.js rename to components/old-menu/__tests__/index.test.js diff --git a/components/old-menu/index.tsx b/components/old-menu/index.tsx new file mode 100644 index 000000000..842ace832 --- /dev/null +++ b/components/old-menu/index.tsx @@ -0,0 +1,323 @@ +import { defineComponent, inject, provide, toRef, App, ExtractPropTypes, Plugin } from 'vue'; +import omit from 'omit.js'; +import VcMenu, { Divider, ItemGroup } from '../vc-menu'; +import SubMenu from './SubMenu'; +import PropTypes from '../_util/vue-types'; +import animation from '../_util/openAnimation'; +import warning from '../_util/warning'; +import Item from './MenuItem'; +import { hasProp, getOptionProps } from '../_util/props-util'; +import BaseMixin from '../_util/BaseMixin'; +import commonPropsType from '../vc-menu/commonPropsType'; +import { defaultConfigProvider } from '../config-provider'; +import { SiderContextProps } from '../layout/Sider'; +import { tuple } from '../_util/type'; +// import raf from '../_util/raf'; + +export const MenuMode = PropTypes.oneOf([ + 'vertical', + 'vertical-left', + 'vertical-right', + 'horizontal', + 'inline', +]); + +export const menuProps = { + ...commonPropsType, + theme: PropTypes.oneOf(tuple('light', 'dark')).def('light'), + mode: MenuMode.def('vertical'), + selectable: PropTypes.looseBool, + selectedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), + defaultSelectedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), + openKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), + defaultOpenKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), + openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), + openTransitionName: PropTypes.string, + prefixCls: PropTypes.string, + multiple: PropTypes.looseBool, + inlineIndent: PropTypes.number.def(24), + inlineCollapsed: PropTypes.looseBool, + isRootMenu: PropTypes.looseBool.def(true), + focusable: PropTypes.looseBool.def(false), + onOpenChange: PropTypes.func, + onSelect: PropTypes.func, + onDeselect: PropTypes.func, + onClick: PropTypes.func, + onMouseenter: PropTypes.func, + onSelectChange: PropTypes.func, +}; + +export type MenuProps = Partial>; + +const Menu = defineComponent({ + name: 'AMenu', + mixins: [BaseMixin], + inheritAttrs: false, + props: menuProps, + Divider: { ...Divider, name: 'AMenuDivider' }, + Item: { ...Item, name: 'AMenuItem' }, + SubMenu: { ...SubMenu, name: 'ASubMenu' }, + ItemGroup: { ...ItemGroup, name: 'AMenuItemGroup' }, + emits: [ + 'update:selectedKeys', + 'update:openKeys', + 'mouseenter', + 'openChange', + 'click', + 'selectChange', + 'select', + 'deselect', + ], + setup() { + const layoutSiderContext = inject('layoutSiderContext', {}); + const layoutSiderCollapsed = toRef(layoutSiderContext, 'sCollapsed'); + return { + configProvider: inject('configProvider', defaultConfigProvider), + layoutSiderContext, + layoutSiderCollapsed, + propsUpdating: false, + switchingModeFromInline: false, + leaveAnimationExecutedWhenInlineCollapsed: false, + inlineOpenKeys: [], + }; + }, + data() { + const props: MenuProps = getOptionProps(this); + warning( + !('inlineCollapsed' in props && props.mode !== 'inline'), + 'Menu', + "`inlineCollapsed` should only be used when Menu's `mode` is inline.", + ); + let sOpenKeys: (number | string)[]; + + if ('openKeys' in props) { + sOpenKeys = props.openKeys; + } else if ('defaultOpenKeys' in props) { + sOpenKeys = props.defaultOpenKeys; + } + return { + sOpenKeys, + }; + }, + // beforeUnmount() { + // raf.cancel(this.mountRafId); + // }, + watch: { + mode(val, oldVal) { + if (oldVal === 'inline' && val !== 'inline') { + this.switchingModeFromInline = true; + } + }, + openKeys(val) { + this.setState({ sOpenKeys: val }); + }, + inlineCollapsed(val) { + this.collapsedChange(val); + }, + layoutSiderCollapsed(val) { + this.collapsedChange(val); + }, + }, + created() { + provide('getInlineCollapsed', this.getInlineCollapsed); + provide('menuPropsContext', this.$props); + }, + updated() { + this.propsUpdating = false; + }, + methods: { + collapsedChange(val: unknown) { + if (this.propsUpdating) { + return; + } + this.propsUpdating = true; + if (!hasProp(this, 'openKeys')) { + if (val) { + this.switchingModeFromInline = true; + this.inlineOpenKeys = this.sOpenKeys; + this.setState({ sOpenKeys: [] }); + } else { + this.setState({ sOpenKeys: this.inlineOpenKeys }); + this.inlineOpenKeys = []; + } + } else if (val) { + // 缩起时,openKeys置为空的动画会闪动,react可以通过是否传递openKeys避免闪动,vue不是很方便动态传递openKeys + this.switchingModeFromInline = true; + } + }, + restoreModeVerticalFromInline() { + if (this.switchingModeFromInline) { + this.switchingModeFromInline = false; + this.$forceUpdate(); + } + }, + // 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 + handleMouseEnter(e: Event) { + this.restoreModeVerticalFromInline(); + this.$emit('mouseenter', e); + }, + handleTransitionEnd(e: 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 SVGAnimationElement | HTMLElement; + // 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(); + } + }, + handleClick(e: Event) { + this.handleOpenChange([]); + this.$emit('click', e); + }, + handleSelect(info) { + this.$emit('update:selectedKeys', info.selectedKeys); + this.$emit('select', info); + this.$emit('selectChange', info.selectedKeys); + }, + handleDeselect(info) { + this.$emit('update:selectedKeys', info.selectedKeys); + this.$emit('deselect', info); + this.$emit('selectChange', info.selectedKeys); + }, + handleOpenChange(openKeys: (number | string)[]) { + this.setOpenKeys(openKeys); + this.$emit('update:openKeys', openKeys); + this.$emit('openChange', openKeys); + }, + setOpenKeys(openKeys: (number | string)[]) { + if (!hasProp(this, 'openKeys')) { + this.setState({ sOpenKeys: openKeys }); + } + }, + getRealMenuMode() { + const inlineCollapsed = this.getInlineCollapsed(); + if (this.switchingModeFromInline && inlineCollapsed) { + return 'inline'; + } + const { mode } = this.$props; + return inlineCollapsed ? 'vertical' : mode; + }, + getInlineCollapsed() { + const { inlineCollapsed } = this.$props; + if (this.layoutSiderContext.sCollapsed !== undefined) { + return this.layoutSiderContext.sCollapsed; + } + return inlineCollapsed; + }, + getMenuOpenAnimation(menuMode: string) { + const { openAnimation, openTransitionName } = this.$props; + let menuOpenAnimation = openAnimation || openTransitionName; + if (openAnimation === undefined && openTransitionName === undefined) { + if (menuMode === 'horizontal') { + menuOpenAnimation = 'slide-up'; + } else if (menuMode === 'inline') { + menuOpenAnimation = animation; + } else { + // When mode switch from inline + // submenu should hide without animation + if (this.switchingModeFromInline) { + menuOpenAnimation = ''; + this.switchingModeFromInline = false; + } else { + menuOpenAnimation = 'zoom-big'; + } + } + } + return menuOpenAnimation; + }, + }, + render() { + const { layoutSiderContext } = this; + const { collapsedWidth } = layoutSiderContext; + const { getPopupContainer: getContextPopupContainer } = this.configProvider; + const props = getOptionProps(this); + const { prefixCls: customizePrefixCls, theme, getPopupContainer } = props; + const getPrefixCls = this.configProvider.getPrefixCls; + const prefixCls = getPrefixCls('menu', customizePrefixCls); + const menuMode = this.getRealMenuMode(); + const menuOpenAnimation = this.getMenuOpenAnimation(menuMode); + const { class: className, ...otherAttrs } = this.$attrs; + const menuClassName = { + [className as string]: className, + [`${prefixCls}-${theme}`]: true, + [`${prefixCls}-inline-collapsed`]: this.getInlineCollapsed(), + }; + + const menuProps = { + ...omit(props, [ + 'inlineCollapsed', + 'onUpdate:selectedKeys', + 'onUpdate:openKeys', + 'onSelectChange', + ]), + getPopupContainer: getPopupContainer || getContextPopupContainer, + openKeys: this.sOpenKeys, + mode: menuMode, + prefixCls, + ...otherAttrs, + onSelect: this.handleSelect, + onDeselect: this.handleDeselect, + onOpenChange: this.handleOpenChange, + onMouseenter: this.handleMouseEnter, + onTransitionend: this.handleTransitionEnd, + // children: getSlot(this), + }; + if (!hasProp(this, 'selectedKeys')) { + delete menuProps.selectedKeys; + } + + if (menuMode !== 'inline') { + // closing vertical popup submenu after click it + menuProps.onClick = this.handleClick; + menuProps.openTransitionName = menuOpenAnimation; + } else { + menuProps.onClick = (e: Event) => { + this.$emit('click', e); + }; + menuProps.openAnimation = menuOpenAnimation; + } + + // https://github.com/ant-design/ant-design/issues/8587 + const hideMenu = + this.getInlineCollapsed() && + (collapsedWidth === 0 || collapsedWidth === '0' || collapsedWidth === '0px'); + if (hideMenu) { + menuProps.openKeys = []; + } + + return ; + }, +}); + +/* istanbul ignore next */ +Menu.install = function(app: 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); + return app; +}; + +export default Menu as typeof Menu & + Plugin & { + readonly Item: typeof Item; + readonly SubMenu: typeof SubMenu; + readonly Divider: typeof Divider; + readonly ItemGroup: typeof ItemGroup; + }; diff --git a/components/new-menu/style/dark.less b/components/old-menu/style/dark.less similarity index 81% rename from components/new-menu/style/dark.less rename to components/old-menu/style/dark.less index 1ad2abf99..03d837a6a 100644 --- a/components/new-menu/style/dark.less +++ b/components/old-menu/style/dark.less @@ -1,8 +1,7 @@ .@{menu-prefix-cls} { // dark theme - &&-dark, - &-dark &-sub, - &&-dark &-sub { + &-dark, + &-dark &-sub { color: @menu-dark-color; background: @menu-dark-bg; .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow { @@ -20,7 +19,8 @@ } &-dark &-inline&-sub { - background: @menu-dark-inline-submenu-bg; + background: @menu-dark-submenu-bg; + box-shadow: 0 2px 8px fade(@black, 45%) inset; } &-dark&-horizontal { @@ -31,23 +31,17 @@ &-dark&-horizontal > &-submenu { top: 0; margin-top: 0; - padding: @menu-item-padding; border-color: @menu-dark-bg; border-bottom: 0; } - &-dark&-horizontal > &-item:hover { - background-color: @menu-dark-item-active-bg; - } - &-dark&-horizontal > &-item > a::before { bottom: 0; } &-dark &-item, &-dark &-item-group-title, - &-dark &-item > a, - &-dark &-item > span > a { + &-dark &-item > a { color: @menu-dark-color; } @@ -83,8 +77,7 @@ &-dark &-submenu-title:hover { color: @menu-dark-highlight-color; background-color: transparent; - > a, - > span > a { + > a { color: @menu-dark-highlight-color; } > .@{menu-prefix-cls}-submenu-title, @@ -102,10 +95,6 @@ background-color: @menu-dark-item-hover-bg; } - &-dark&-dark:not(&-horizontal) &-item-selected { - background-color: @menu-dark-item-active-bg; - } - &-dark &-item-selected { color: @menu-dark-highlight-color; border-right: 0; @@ -113,19 +102,14 @@ border-right: 0; } > a, - > span > a, - > a:hover, - > span > a:hover { + > a:hover { color: @menu-dark-highlight-color; } - - .@{menu-prefix-cls}-item-icon, .@{iconfont-css-prefix} { color: @menu-dark-selected-item-icon-color; - - + span { - color: @menu-dark-selected-item-text-color; - } + } + .@{iconfont-css-prefix} + span { + color: @menu-dark-selected-item-text-color; } } @@ -138,8 +122,7 @@ &-dark &-item-disabled, &-dark &-submenu-disabled { &, - > a, - > span > a { + > a { color: @disabled-color-dark !important; opacity: 0.8; } diff --git a/components/new-menu/style/index.less b/components/old-menu/style/index.less similarity index 55% rename from components/new-menu/style/index.less rename to components/old-menu/style/index.less index b8956d2fc..53ac60599 100644 --- a/components/new-menu/style/index.less +++ b/components/old-menu/style/index.less @@ -1,15 +1,7 @@ @import '../../style/themes/index'; @import '../../style/mixins/index'; -@import './status'; @menu-prefix-cls: ~'@{ant-prefix}-menu'; -@menu-animation-duration-normal: 0.15s; - -.accessibility-focus() { - box-shadow: 0 0 0 2px fade(@primary-color, 20%); -} - -// TODO: Should remove icon style compatible in v5 // default theme .@{menu-prefix-cls} { @@ -18,21 +10,14 @@ margin-bottom: 0; padding-left: 0; // Override default ul/ol color: @menu-item-color; - font-size: @menu-item-font-size; line-height: 0; // Fix display inline-block gap - text-align: left; list-style: none; background: @menu-bg; outline: none; box-shadow: @box-shadow-base; - transition: background @animation-duration-slow, - width @animation-duration-slow cubic-bezier(0.2, 0, 0, 1) 0s; + transition: background 0.3s, width 0.3s cubic-bezier(0.2, 0, 0, 1) 0s; .clearfix(); - &&-root:focus-visible { - .accessibility-focus(); - } - ul, ol { margin: 0; @@ -40,29 +25,22 @@ list-style: none; } - &-hidden, - &-submenu-hidden { + &-hidden { display: none; } &-item-group-title { - height: @menu-item-group-height; padding: 8px 16px; color: @menu-item-group-title-color; - font-size: @menu-item-group-title-font-size; - line-height: @menu-item-group-height; - transition: all @animation-duration-slow; + font-size: @font-size-base; + line-height: @line-height-base; + transition: all 0.3s; } - &-horizontal &-submenu { - transition: border-color @animation-duration-slow @ease-in-out, - background @animation-duration-slow @ease-in-out; - } &-submenu, &-submenu-inline { - transition: border-color @animation-duration-slow @ease-in-out, - background @animation-duration-slow @ease-in-out, - padding @menu-animation-duration-normal @ease-in-out; + transition: border-color 0.3s @ease-in-out, background 0.3s @ease-in-out, + padding 0.15s @ease-in-out; } &-submenu-selected { @@ -76,11 +54,11 @@ &-submenu &-sub { cursor: initial; - transition: background @animation-duration-slow @ease-in-out, - padding @animation-duration-slow @ease-in-out; + transition: background 0.3s @ease-in-out, padding 0.3s @ease-in-out; } - &-item a { + &-item > a { + display: block; color: @menu-item-color; &:hover { color: @menu-highlight-color; @@ -97,7 +75,7 @@ } // https://github.com/ant-design/ant-design/issues/19809 - &-item > .@{ant-prefix}-badge a { + &-item > .@{ant-prefix}-badge > a { color: @menu-item-color; &:hover { color: @menu-highlight-color; @@ -132,8 +110,8 @@ &-item-selected { color: @menu-highlight-color; - a, - a:hover { + > a, + > a:hover { color: @menu-highlight-color; } } @@ -147,7 +125,6 @@ &-vertical-left { border-right: @border-width-base @border-style-base @border-color-split; } - &-vertical-right { border-left: @border-width-base @border-style-base @border-color-split; } @@ -156,17 +133,9 @@ &-vertical-left&-sub, &-vertical-right&-sub { min-width: 160px; - max-height: calc(100vh - 100px); padding: 0; - overflow: hidden; border-right: 0; - - // https://github.com/ant-design/ant-design/issues/22244 - // https://github.com/ant-design/ant-design/issues/26812 - &:not([class*='-active']) { - overflow-x: hidden; - overflow-y: auto; - } + transform-origin: 0 0; .@{menu-prefix-cls}-item { left: 0; @@ -186,48 +155,26 @@ min-width: 114px; // in case of submenu width is too big: https://codesandbox.io/s/qvpwm6mk66 } - &-horizontal &-item, - &-horizontal &-submenu-title { - transition: border-color @animation-duration-slow, background @animation-duration-slow; - } - &-item, &-submenu-title { position: relative; display: block; margin: 0; - padding: @menu-item-padding; + padding: 0 20px; white-space: nowrap; cursor: pointer; - transition: border-color @animation-duration-slow, background @animation-duration-slow, - padding @animation-duration-slow @ease-in-out; - - .@{menu-prefix-cls}-item-icon, + transition: color 0.3s @ease-in-out, border-color 0.3s @ease-in-out, + background 0.3s @ease-in-out, padding 0.15s @ease-in-out; .@{iconfont-css-prefix} { min-width: 14px; + margin-right: 10px; font-size: @menu-icon-size; - transition: font-size @menu-animation-duration-normal @ease-out, - margin @animation-duration-slow @ease-in-out, color @animation-duration-slow; + transition: font-size 0.15s @ease-out, margin 0.3s @ease-in-out; + span { - margin-left: @menu-icon-margin-right; opacity: 1; - // transition: opacity @animation-duration-slow @ease-in-out, - // width @animation-duration-slow @ease-in-out, color @animation-duration-slow; - transition: opacity @animation-duration-slow @ease-in-out, margin @animation-duration-slow, - color @animation-duration-slow; + transition: opacity 0.3s @ease-in-out, width 0.3s @ease-in-out; } } - - &.@{menu-prefix-cls}-item-only-child { - > .@{iconfont-css-prefix}, - > .@{menu-prefix-cls}-item-icon { - margin-right: 0; - } - } - - &:focus-visible { - .accessibility-focus(); - } } & > &-item-divider { @@ -243,105 +190,94 @@ &-popup { position: absolute; z-index: @zindex-dropdown; - background: transparent; + // background: @menu-popup-bg; border-radius: @border-radius-base; - box-shadow: none; - transform-origin: 0 0; - // https://github.com/ant-design/ant-design/issues/13955 + .submenu-title-wrapper { + padding-right: 20px; + } + &::before { position: absolute; top: -7px; right: 0; bottom: 0; left: 0; - z-index: -1; - width: 100%; - height: 100%; opacity: 0.0001; content: ' '; } } - // https://github.com/ant-design/ant-design/issues/13955 - &-placement-rightTop::before { - top: 0; - left: -7px; - } - > .@{menu-prefix-cls} { background-color: @menu-bg; border-radius: @border-radius-base; &-submenu-title::after { - transition: transform @animation-duration-slow @ease-in-out; + transition: transform 0.3s @ease-in-out; } } - &-popup > .@{menu-prefix-cls} { - background-color: @menu-popup-bg; - } - - &-expand-icon, - &-arrow { - position: absolute; - top: 50%; - right: 16px; - width: 10px; - color: @menu-item-color; - transform: translateY(-50%); - transition: transform @animation-duration-slow @ease-in-out; - } - - &-arrow { - // → - &::before, - &::after { + &-vertical, + &-vertical-left, + &-vertical-right, + &-inline { + > .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow { position: absolute; - width: 6px; - height: 1.5px; - background-color: currentColor; - border-radius: 2px; - transition: background @animation-duration-slow @ease-in-out, - transform @animation-duration-slow @ease-in-out, top @animation-duration-slow @ease-in-out, - color @animation-duration-slow @ease-in-out; - content: ''; + top: 50%; + right: 16px; + width: 10px; + transition: transform 0.3s @ease-in-out; + &::before, + &::after { + position: absolute; + width: 6px; + height: 1.5px; + // background + background-image to makes before & after cross have same color. + // Since `linear-gradient` not work on IE9, we should hack it. + // ref: https://github.com/ant-design/ant-design/issues/15910 + background: @menu-bg; + background: ~'@{menu-item-color} \9'; + background-image: linear-gradient(to right, @menu-item-color, @menu-item-color); + background-image: ~'none \9'; + border-radius: 2px; + transition: background 0.3s @ease-in-out, transform 0.3s @ease-in-out, + top 0.3s @ease-in-out; + content: ''; + } + &::before { + transform: rotate(45deg) translateY(-2px); + } + &::after { + transform: rotate(-45deg) translateY(2px); + } } + > .@{menu-prefix-cls}-submenu-title:hover .@{menu-prefix-cls}-submenu-arrow { + &::after, + &::before { + background: linear-gradient(to right, @menu-highlight-color, @menu-highlight-color); + } + } + } + + &-inline > .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow { &::before { - transform: rotate(45deg) translateY(-2.5px); + transform: rotate(-45deg) translateX(2px); } &::after { - transform: rotate(-45deg) translateY(2.5px); + transform: rotate(45deg) translateX(-2px); } } - &:hover > &-title > &-expand-icon, - &:hover > &-title > &-arrow { - color: @menu-highlight-color; - } - - .@{menu-prefix-cls}-inline-collapsed &-arrow, - &-inline &-arrow { - // ↓ - &::before { - transform: rotate(-45deg) translateX(2.5px); - } - &::after { - transform: rotate(45deg) translateX(-2.5px); - } - } - - &-horizontal &-arrow { - display: none; - } - - &-open&-inline > &-title > &-arrow { - // ↑ - transform: translateY(-2px); - &::after { - transform: rotate(-45deg) translateX(-2.5px); - } - &::before { - transform: rotate(45deg) translateX(2.5px); + &-open { + &.@{menu-prefix-cls}-submenu-inline + > .@{menu-prefix-cls}-submenu-title + .@{menu-prefix-cls}-submenu-arrow { + transform: translateY(-2px); + &::after { + transform: rotate(-45deg) translateX(-2px); + } + &::before { + transform: rotate(45deg) translateX(2px); + } } } } @@ -350,34 +286,18 @@ &-vertical-left &-submenu-selected, &-vertical-right &-submenu-selected { color: @menu-highlight-color; + > a { + color: @menu-highlight-color; + } } &-horizontal { - line-height: @menu-horizontal-line-height; + line-height: 46px; + white-space: nowrap; border: 0; border-bottom: @border-width-base @border-style-base @border-color-split; box-shadow: none; - &:not(.@{menu-prefix-cls}-dark) { - > .@{menu-prefix-cls}-item, - > .@{menu-prefix-cls}-submenu { - margin: @menu-item-padding; - margin-top: -1px; - margin-bottom: 0; - padding: @menu-item-padding; - padding-right: 0; - padding-left: 0; - - &:hover, - &-active, - &-open, - &-selected { - color: @menu-highlight-color; - border-bottom: 2px solid @menu-highlight-color; - } - } - } - > .@{menu-prefix-cls}-item, > .@{menu-prefix-cls}-submenu { position: relative; @@ -385,14 +305,19 @@ display: inline-block; vertical-align: bottom; border-bottom: 2px solid transparent; - } - > .@{menu-prefix-cls}-submenu > .@{menu-prefix-cls}-submenu-title { - padding: 0; + &:hover, + &-active, + &-open, + &-selected { + color: @menu-highlight-color; + border-bottom: 2px solid @menu-highlight-color; + } } > .@{menu-prefix-cls}-item { - a { + > a { + display: block; color: @menu-item-color; &:hover { color: @menu-highlight-color; @@ -401,7 +326,7 @@ bottom: -2px; } } - &-selected a { + &-selected > a { color: @menu-highlight-color; } } @@ -428,8 +353,7 @@ border-right: @menu-item-active-border-width solid @menu-highlight-color; transform: scaleY(0.0001); opacity: 0; - transition: transform @menu-animation-duration-normal @ease-out, - opacity @menu-animation-duration-normal @ease-out; + transition: transform 0.15s @ease-out, opacity 0.15s @ease-out; content: ''; } } @@ -441,6 +365,7 @@ margin-bottom: @menu-item-vertical-margin; padding: 0 16px; overflow: hidden; + font-size: @menu-item-font-size; line-height: @menu-item-height; text-overflow: ellipsis; } @@ -461,13 +386,6 @@ } } - &-vertical { - .@{menu-prefix-cls}-item-group-list .@{menu-prefix-cls}-submenu-title, - .@{menu-prefix-cls}-submenu-title { - padding-right: 34px; - } - } - &-inline { width: 100%; .@{menu-prefix-cls}-selected, @@ -475,8 +393,7 @@ &::after { transform: scaleY(1); opacity: 1; - transition: transform @menu-animation-duration-normal @ease-in-out, - opacity @menu-animation-duration-normal @ease-in-out; + transition: transform 0.15s @ease-in-out, opacity 0.15s @ease-in-out; } } @@ -485,37 +402,13 @@ width: ~'calc(100% + 1px)'; } - .@{menu-prefix-cls}-item-group-list .@{menu-prefix-cls}-submenu-title, .@{menu-prefix-cls}-submenu-title { padding-right: 34px; } - - // Motion enhance for first level - &.@{menu-prefix-cls}-root { - .@{menu-prefix-cls}-item, - .@{menu-prefix-cls}-submenu-title { - display: flex; - align-items: center; - transition: border-color @animation-duration-slow, background @animation-duration-slow, - padding 0.1s @ease-out; - - > .@{menu-prefix-cls}-title-content { - flex: auto; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - } - - > * { - flex: none; - } - } - } } - &&-inline-collapsed { + &-inline-collapsed { width: @menu-collapsed-width; - > .@{menu-prefix-cls}-item, > .@{menu-prefix-cls}-item-group > .@{menu-prefix-cls}-item-group-list @@ -526,34 +419,24 @@ > .@{menu-prefix-cls}-submenu-title, > .@{menu-prefix-cls}-submenu > .@{menu-prefix-cls}-submenu-title { left: 0; - padding: 0 ~'calc(50% - @{menu-icon-size-lg} / 2)'; + padding: 0 ((@menu-collapsed-width - @menu-icon-size-lg) / 2) !important; text-overflow: clip; - .@{menu-prefix-cls}-submenu-arrow { - opacity: 0; + display: none; } - - .@{menu-prefix-cls}-item-icon, .@{iconfont-css-prefix} { margin: 0; font-size: @menu-icon-size-lg; line-height: @menu-item-height; + span { display: inline-block; + max-width: 0; opacity: 0; } } } - - .@{menu-prefix-cls}-item-icon, - .@{iconfont-css-prefix} { - display: inline-block; - } - &-tooltip { pointer-events: none; - - .@{menu-prefix-cls}-item-icon, .@{iconfont-css-prefix} { display: none; } @@ -587,19 +470,8 @@ box-shadow: none; } - &-root&-inline-collapsed { - .@{menu-prefix-cls}-item, - .@{menu-prefix-cls}-submenu .@{menu-prefix-cls}-submenu-title { - > .@{menu-prefix-cls}-inline-collapsed-noicon { - font-size: @menu-icon-size-lg; - text-align: center; - } - } - } - &-sub&-inline { padding: 0; - background: @menu-inline-submenu-bg; border: 0; border-radius: 0; box-shadow: none; @@ -623,7 +495,7 @@ background: none; border-color: transparent !important; cursor: not-allowed; - a { + > a { color: @disabled-color !important; pointer-events: none; } @@ -640,12 +512,4 @@ } } -// Integration with header element so menu items have the same height -.@{ant-prefix}-layout-header { - .@{menu-prefix-cls} { - line-height: inherit; - } -} - @import './dark'; -@import './rtl'; diff --git a/components/menu/style/index.ts b/components/old-menu/style/index.ts similarity index 100% rename from components/menu/style/index.ts rename to components/old-menu/style/index.ts diff --git a/components/style/themes/default.less b/components/style/themes/default.less index 666c69872..dcb8fa8cb 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -490,28 +490,37 @@ // --- @menu-inline-toplevel-item-height: 40px; @menu-item-height: 40px; +@menu-item-group-height: @line-height-base; @menu-collapsed-width: 80px; @menu-bg: @component-background; @menu-popup-bg: @component-background; @menu-item-color: @text-color; +@menu-inline-submenu-bg: @background-color-light; @menu-highlight-color: @primary-color; -@menu-item-active-bg: @item-active-bg; +@menu-highlight-danger-color: @error-color; +@menu-item-active-bg: @primary-1; +@menu-item-active-danger-bg: @red-1; @menu-item-active-border-width: 3px; @menu-item-group-title-color: @text-color-secondary; -@menu-icon-size: @font-size-base; -@menu-icon-size-lg: @font-size-lg; - @menu-item-vertical-margin: 4px; @menu-item-font-size: @font-size-base; @menu-item-boundary-margin: 8px; +@menu-item-padding: 0 20px; +@menu-horizontal-line-height: 46px; +@menu-icon-margin-right: 10px; +@menu-icon-size: @menu-item-font-size; +@menu-icon-size-lg: @font-size-lg; +@menu-item-group-title-font-size: @menu-item-font-size; // dark theme @menu-dark-color: @text-color-secondary-dark; +@menu-dark-danger-color: @error-color; @menu-dark-bg: @layout-header-background; @menu-dark-arrow-color: #fff; -@menu-dark-submenu-bg: #000c17; +@menu-dark-inline-submenu-bg: #000c17; @menu-dark-highlight-color: #fff; @menu-dark-item-active-bg: @primary-color; +@menu-dark-item-active-danger-bg: @error-color; @menu-dark-selected-item-icon-color: @white; @menu-dark-selected-item-text-color: @white; @menu-dark-item-hover-bg: transparent; diff --git a/examples/App.vue b/examples/App.vue index e15daac46..e84532658 100644 --- a/examples/App.vue +++ b/examples/App.vue @@ -1,39 +1,92 @@ -