From ab4478c9f4aab3e20fe3b5e31e6dcd08f12df5f6 Mon Sep 17 00:00:00 2001 From: tanjinzhou <415800467@qq.com> Date: Thu, 13 May 2021 18:30:55 +0800 Subject: [PATCH 01/20] feat: new-menu --- components/new-menu/index.tsx | 0 components/new-menu/src/Divider.tsx | 13 + components/new-menu/src/ItemGroup.tsx | 16 + components/new-menu/src/Menu.tsx | 10 + components/new-menu/src/MenuItem.tsx | 10 + components/new-menu/src/SubMenu.tsx | 10 + components/new-menu/style/dark.less | 156 ++++++ components/new-menu/style/index.less | 651 ++++++++++++++++++++++++++ components/new-menu/style/index.tsx | 6 + components/new-menu/style/rtl.less | 164 +++++++ components/new-menu/style/status.less | 47 ++ v2-doc | 2 +- 12 files changed, 1084 insertions(+), 1 deletion(-) create mode 100644 components/new-menu/index.tsx create mode 100644 components/new-menu/src/Divider.tsx create mode 100644 components/new-menu/src/ItemGroup.tsx create mode 100644 components/new-menu/src/Menu.tsx create mode 100644 components/new-menu/src/MenuItem.tsx create mode 100644 components/new-menu/src/SubMenu.tsx create mode 100644 components/new-menu/style/dark.less create mode 100644 components/new-menu/style/index.less create mode 100644 components/new-menu/style/index.tsx create mode 100644 components/new-menu/style/rtl.less create mode 100644 components/new-menu/style/status.less diff --git a/components/new-menu/index.tsx b/components/new-menu/index.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/components/new-menu/src/Divider.tsx b/components/new-menu/src/Divider.tsx new file mode 100644 index 0000000000..8e9d99ad5a --- /dev/null +++ b/components/new-menu/src/Divider.tsx @@ -0,0 +1,13 @@ +import { defineComponent } from 'vue'; + +export default defineComponent({ + name: 'Divider', + props: { + prefixCls: String, + }, + setup(props) { + return () => { + return
  • ; + }; + }, +}); diff --git a/components/new-menu/src/ItemGroup.tsx b/components/new-menu/src/ItemGroup.tsx new file mode 100644 index 0000000000..191c164472 --- /dev/null +++ b/components/new-menu/src/ItemGroup.tsx @@ -0,0 +1,16 @@ +import { getPropsSlot } from '../../_util/props-util'; +import { defineComponent } from 'vue'; + +export default defineComponent({ + name: 'AMenuItemGroup', + setup(props, { slots }) { + return () => { + return ( +
  • + {getPropsSlot(slots, props, 'title')} + +
  • + ); + }; + }, +}); diff --git a/components/new-menu/src/Menu.tsx b/components/new-menu/src/Menu.tsx new file mode 100644 index 0000000000..0bf679ac74 --- /dev/null +++ b/components/new-menu/src/Menu.tsx @@ -0,0 +1,10 @@ +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 new file mode 100644 index 0000000000..20268d09a9 --- /dev/null +++ b/components/new-menu/src/MenuItem.tsx @@ -0,0 +1,10 @@ +import { defineComponent } from 'vue'; + +export default defineComponent({ + name: 'AMenuItem', + setup(props, { slots }) { + return () => { + return
  • {slots.default?.()}
  • ; + }; + }, +}); diff --git a/components/new-menu/src/SubMenu.tsx b/components/new-menu/src/SubMenu.tsx new file mode 100644 index 0000000000..b88004338c --- /dev/null +++ b/components/new-menu/src/SubMenu.tsx @@ -0,0 +1,10 @@ +import { defineComponent } from 'vue'; + +export default defineComponent({ + name: 'ASubMenu', + setup(props, { slots }) { + return () => { + return ; + }; + }, +}); diff --git a/components/new-menu/style/dark.less b/components/new-menu/style/dark.less new file mode 100644 index 0000000000..1ad2abf991 --- /dev/null +++ b/components/new-menu/style/dark.less @@ -0,0 +1,156 @@ +.@{menu-prefix-cls} { + // dark theme + &&-dark, + &-dark &-sub, + &&-dark &-sub { + color: @menu-dark-color; + background: @menu-dark-bg; + .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow { + opacity: 0.45; + transition: all 0.3s; + &::after, + &::before { + background: @menu-dark-arrow-color; + } + } + } + + &-dark&-submenu-popup { + background: transparent; + } + + &-dark &-inline&-sub { + background: @menu-dark-inline-submenu-bg; + } + + &-dark&-horizontal { + border-bottom: 0; + } + + &-dark&-horizontal > &-item, + &-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 { + color: @menu-dark-color; + } + + &-dark&-inline, + &-dark&-vertical, + &-dark&-vertical-left, + &-dark&-vertical-right { + border-right: 0; + } + + &-dark&-inline &-item, + &-dark&-vertical &-item, + &-dark&-vertical-left &-item, + &-dark&-vertical-right &-item { + left: 0; + margin-left: 0; + border-right: 0; + &::after { + border-right: 0; + } + } + + &-dark&-inline &-item, + &-dark&-inline &-submenu-title { + width: 100%; + } + + &-dark &-item:hover, + &-dark &-item-active, + &-dark &-submenu-active, + &-dark &-submenu-open, + &-dark &-submenu-selected, + &-dark &-submenu-title:hover { + color: @menu-dark-highlight-color; + background-color: transparent; + > a, + > span > a { + color: @menu-dark-highlight-color; + } + > .@{menu-prefix-cls}-submenu-title, + > .@{menu-prefix-cls}-submenu-title:hover { + > .@{menu-prefix-cls}-submenu-arrow { + opacity: 1; + &::after, + &::before { + background: @menu-dark-highlight-color; + } + } + } + } + &-dark &-item:hover { + 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; + &::after { + border-right: 0; + } + > a, + > 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; + + + span { + color: @menu-dark-selected-item-text-color; + } + } + } + + &&-dark &-item-selected, + &-submenu-popup&-dark &-item-selected { + background-color: @menu-dark-item-active-bg; + } + + // Disabled state sets text to dark gray and nukes hover/tab effects + &-dark &-item-disabled, + &-dark &-submenu-disabled { + &, + > a, + > span > a { + color: @disabled-color-dark !important; + opacity: 0.8; + } + > .@{menu-prefix-cls}-submenu-title { + color: @disabled-color-dark !important; + > .@{menu-prefix-cls}-submenu-arrow { + &::before, + &::after { + background: @disabled-color-dark !important; + } + } + } + } +} diff --git a/components/new-menu/style/index.less b/components/new-menu/style/index.less new file mode 100644 index 0000000000..b8956d2fc0 --- /dev/null +++ b/components/new-menu/style/index.less @@ -0,0 +1,651 @@ +@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} { + .reset-component(); + + 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; + .clearfix(); + + &&-root:focus-visible { + .accessibility-focus(); + } + + ul, + ol { + margin: 0; + padding: 0; + list-style: none; + } + + &-hidden, + &-submenu-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; + } + + &-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; + } + + &-submenu-selected { + color: @menu-highlight-color; + } + + &-item:active, + &-submenu-title:active { + background: @menu-item-active-bg; + } + + &-submenu &-sub { + cursor: initial; + transition: background @animation-duration-slow @ease-in-out, + padding @animation-duration-slow @ease-in-out; + } + + &-item a { + color: @menu-item-color; + &:hover { + color: @menu-highlight-color; + } + &::before { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: transparent; + content: ''; + } + } + + // https://github.com/ant-design/ant-design/issues/19809 + &-item > .@{ant-prefix}-badge a { + color: @menu-item-color; + &:hover { + color: @menu-highlight-color; + } + } + + &-item-divider { + height: 1px; + overflow: hidden; + line-height: 0; + background-color: @border-color-split; + } + + &-item:hover, + &-item-active, + &:not(&-inline) &-submenu-open, + &-submenu-active, + &-submenu-title:hover { + color: @menu-highlight-color; + } + + &-horizontal &-item, + &-horizontal &-submenu { + margin-top: -1px; + } + + &-horizontal > &-item:hover, + &-horizontal > &-item-active, + &-horizontal > &-submenu &-submenu-title:hover { + background-color: transparent; + } + + &-item-selected { + color: @menu-highlight-color; + a, + a:hover { + color: @menu-highlight-color; + } + } + + &:not(&-horizontal) &-item-selected { + background-color: @menu-item-active-bg; + } + + &-inline, + &-vertical, + &-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; + } + + &-vertical&-sub, + &-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; + } + + .@{menu-prefix-cls}-item { + left: 0; + margin-left: 0; + border-right: 0; + &::after { + border-right: 0; + } + } + > .@{menu-prefix-cls}-item, + > .@{menu-prefix-cls}-submenu { + transform-origin: 0 0; + } + } + + &-horizontal&-sub { + 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; + 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, + .@{iconfont-css-prefix} { + min-width: 14px; + 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; + + 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; + } + } + + &.@{menu-prefix-cls}-item-only-child { + > .@{iconfont-css-prefix}, + > .@{menu-prefix-cls}-item-icon { + margin-right: 0; + } + } + + &:focus-visible { + .accessibility-focus(); + } + } + + & > &-item-divider { + height: 1px; + margin: 1px 0; + padding: 0; + overflow: hidden; + line-height: 0; + background-color: @border-color-split; + } + + &-submenu { + &-popup { + position: absolute; + z-index: @zindex-dropdown; + background: transparent; + border-radius: @border-radius-base; + box-shadow: none; + transform-origin: 0 0; + + // 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 @animation-duration-slow @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 { + 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: ''; + } + &::before { + transform: rotate(45deg) translateY(-2.5px); + } + &::after { + transform: rotate(-45deg) translateY(2.5px); + } + } + + &: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); + } + } + } + + &-vertical &-submenu-selected, + &-vertical-left &-submenu-selected, + &-vertical-right &-submenu-selected { + color: @menu-highlight-color; + } + + &-horizontal { + 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; + top: 1px; + display: inline-block; + vertical-align: bottom; + border-bottom: 2px solid transparent; + } + + > .@{menu-prefix-cls}-submenu > .@{menu-prefix-cls}-submenu-title { + padding: 0; + } + + > .@{menu-prefix-cls}-item { + a { + color: @menu-item-color; + &:hover { + color: @menu-highlight-color; + } + &::before { + bottom: -2px; + } + } + &-selected a { + color: @menu-highlight-color; + } + } + + &::after { + display: block; + clear: both; + height: 0; + content: '\20'; + } + } + + &-vertical, + &-vertical-left, + &-vertical-right, + &-inline { + .@{menu-prefix-cls}-item { + position: relative; + &::after { + position: absolute; + top: 0; + right: 0; + bottom: 0; + 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; + content: ''; + } + } + + .@{menu-prefix-cls}-item, + .@{menu-prefix-cls}-submenu-title { + height: @menu-item-height; + margin-top: @menu-item-vertical-margin; + margin-bottom: @menu-item-vertical-margin; + padding: 0 16px; + overflow: hidden; + line-height: @menu-item-height; + text-overflow: ellipsis; + } + + // disable margin collapsed + .@{menu-prefix-cls}-submenu { + padding-bottom: 0.02px; + } + + .@{menu-prefix-cls}-item:not(:last-child) { + margin-bottom: @menu-item-boundary-margin; + } + + > .@{menu-prefix-cls}-item, + > .@{menu-prefix-cls}-submenu > .@{menu-prefix-cls}-submenu-title { + height: @menu-inline-toplevel-item-height; + line-height: @menu-inline-toplevel-item-height; + } + } + + &-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, + .@{menu-prefix-cls}-item-selected { + &::after { + transform: scaleY(1); + opacity: 1; + transition: transform @menu-animation-duration-normal @ease-in-out, + opacity @menu-animation-duration-normal @ease-in-out; + } + } + + .@{menu-prefix-cls}-item, + .@{menu-prefix-cls}-submenu-title { + 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 { + width: @menu-collapsed-width; + + > .@{menu-prefix-cls}-item, + > .@{menu-prefix-cls}-item-group + > .@{menu-prefix-cls}-item-group-list + > .@{menu-prefix-cls}-item, + > .@{menu-prefix-cls}-item-group + > .@{menu-prefix-cls}-item-group-list + > .@{menu-prefix-cls}-submenu + > .@{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)'; + text-overflow: clip; + + .@{menu-prefix-cls}-submenu-arrow { + 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; + 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; + } + a { + color: @text-color-dark; + } + } + + .@{menu-prefix-cls}-item-group-title { + padding-right: 4px; + padding-left: 4px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + + &-item-group-list { + margin: 0; + padding: 0; + .@{menu-prefix-cls}-item, + .@{menu-prefix-cls}-submenu-title { + padding: 0 16px 0 28px; + } + } + + &-root&-vertical, + &-root&-vertical-left, + &-root&-vertical-right, + &-root&-inline { + 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; + & > .@{menu-prefix-cls}-item, + & > .@{menu-prefix-cls}-submenu > .@{menu-prefix-cls}-submenu-title { + height: @menu-item-height; + line-height: @menu-item-height; + list-style-position: inside; + list-style-type: disc; + } + + & .@{menu-prefix-cls}-item-group-title { + padding-left: 32px; + } + } + + // Disabled state sets text to gray and nukes hover/tab effects + &-item-disabled, + &-submenu-disabled { + color: @disabled-color !important; + background: none; + border-color: transparent !important; + cursor: not-allowed; + a { + color: @disabled-color !important; + pointer-events: none; + } + > .@{menu-prefix-cls}-submenu-title { + color: @disabled-color !important; + cursor: not-allowed; + > .@{menu-prefix-cls}-submenu-arrow { + &::before, + &::after { + background: @disabled-color !important; + } + } + } + } +} + +// 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/new-menu/style/index.tsx new file mode 100644 index 0000000000..be49986340 --- /dev/null +++ b/components/new-menu/style/index.tsx @@ -0,0 +1,6 @@ +import '../../style/index.less'; +import './index.less'; + +// style dependencies +// deps-lint-skip: layout +import '../../tooltip/style'; diff --git a/components/new-menu/style/rtl.less b/components/new-menu/style/rtl.less new file mode 100644 index 0000000000..a7edba5bbf --- /dev/null +++ b/components/new-menu/style/rtl.less @@ -0,0 +1,164 @@ +@import '../../style/themes/index'; +@import '../../style/mixins/index'; + +@menu-prefix-cls: ~'@{ant-prefix}-menu'; + +.@{menu-prefix-cls} { + &&-rtl { + direction: rtl; + text-align: right; + } + + &-item-group-title { + .@{menu-prefix-cls}-rtl & { + text-align: right; + } + } + + &-inline, + &-vertical { + .@{menu-prefix-cls}-rtl& { + border-right: none; + border-left: @border-width-base @border-style-base @border-color-split; + } + } + + &-dark&-inline, + &-dark&-vertical { + .@{menu-prefix-cls}-rtl& { + border-left: none; + } + } + + &-vertical&-sub, + &-vertical-left&-sub, + &-vertical-right&-sub { + > .@{menu-prefix-cls}-item, + > .@{menu-prefix-cls}-submenu { + .@{menu-prefix-cls}-rtl& { + transform-origin: top right; + } + } + } + + &-item, + &-submenu-title { + .@{menu-prefix-cls}-item-icon, + .@{iconfont-css-prefix} { + .@{menu-prefix-cls}-rtl & { + margin-right: auto; + margin-left: @menu-icon-margin-right; + } + } + + &.@{menu-prefix-cls}-item-only-child { + > .@{menu-prefix-cls}-item-icon, + > .@{iconfont-css-prefix} { + .@{menu-prefix-cls}-rtl & { + margin-left: 0; + } + } + } + } + + &-submenu { + &-rtl.@{menu-prefix-cls}-submenu-popup { + transform-origin: 100% 0; + } + + &-vertical, + &-vertical-left, + &-vertical-right, + &-inline { + > .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow { + .@{menu-prefix-cls}-rtl & { + right: auto; + left: 16px; + } + } + } + + &-vertical, + &-vertical-left, + &-vertical-right { + > .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow { + &::before { + .@{menu-prefix-cls}-rtl & { + transform: rotate(-45deg) translateY(-2px); + } + } + &::after { + .@{menu-prefix-cls}-rtl & { + transform: rotate(45deg) translateY(2px); + } + } + } + } + } + + &-vertical, + &-vertical-left, + &-vertical-right, + &-inline { + .@{menu-prefix-cls}-item { + &::after { + .@{menu-prefix-cls}-rtl& { + right: auto; + left: 0; + } + } + } + + .@{menu-prefix-cls}-item, + .@{menu-prefix-cls}-submenu-title { + .@{menu-prefix-cls}-rtl& { + text-align: right; + } + } + } + + &-inline { + .@{menu-prefix-cls}-submenu-title { + .@{menu-prefix-cls}-rtl& { + padding-right: 0; + padding-left: 34px; + } + } + } + + &-vertical { + .@{menu-prefix-cls}-submenu-title { + .@{menu-prefix-cls}-rtl& { + padding-right: 16px; + padding-left: 34px; + } + } + } + + &-inline-collapsed&-vertical { + .@{menu-prefix-cls}-submenu-title { + .@{menu-prefix-cls}-rtl& { + padding: 0 ~'calc(50% - @{menu-icon-size-lg} / 2)'; + } + } + } + + &-item-group-list { + .@{menu-prefix-cls}-item, + .@{menu-prefix-cls}-submenu-title { + .@{menu-prefix-cls}-rtl & { + padding: 0 28px 0 16px; + } + } + } + + &-sub&-inline { + border: 0; + & .@{menu-prefix-cls}-item-group-title { + .@{menu-prefix-cls}-rtl& { + padding-right: 32px; + padding-left: 0; + } + } + } +} diff --git a/components/new-menu/style/status.less b/components/new-menu/style/status.less new file mode 100644 index 0000000000..5e5d66c057 --- /dev/null +++ b/components/new-menu/style/status.less @@ -0,0 +1,47 @@ +@import './index'; + +.@{menu-prefix-cls} { + // Danger + &-item-danger&-item { + color: @menu-highlight-danger-color; + + &:hover, + &-active { + color: @menu-highlight-danger-color; + } + + &:active { + background: @menu-item-active-danger-bg; + } + + &-selected { + color: @menu-highlight-danger-color; + > a, + > a:hover { + color: @menu-highlight-danger-color; + } + } + + .@{menu-prefix-cls}:not(.@{menu-prefix-cls}-horizontal) &-selected { + background-color: @menu-item-active-danger-bg; + } + + .@{menu-prefix-cls}-inline &::after { + border-right-color: @menu-highlight-danger-color; + } + } + + // ==================== Dark ==================== + &-dark &-item-danger&-item { + &, + &:hover, + & > a { + color: @menu-dark-danger-color; + } + } + + &-dark&-dark:not(&-horizontal) &-item-danger&-item-selected { + color: @menu-dark-highlight-color; + background-color: @menu-dark-item-active-danger-bg; + } +} diff --git a/v2-doc b/v2-doc index eacad021cf..a7013ae87f 160000 --- a/v2-doc +++ b/v2-doc @@ -1 +1 @@ -Subproject commit eacad021cf9e4d7d18fe4c4b9a38cbd7e3378d49 +Subproject commit a7013ae87f69dcbcf547f4b023255b8a7a775557 From 994df6ff6f7cc72f8656478324d7245a123b7dbf Mon Sep 17 00:00:00 2001 From: tanjinzhou <415800467@qq.com> Date: Fri, 14 May 2021 16:46:43 +0800 Subject: [PATCH 02/20] refactor: menu --- components/menu/index.tsx | 322 +---------------- components/{new-menu => menu}/src/Divider.tsx | 0 components/menu/src/ItemGroup.tsx | 29 ++ components/menu/src/Menu.tsx | 21 ++ components/menu/src/MenuItem.tsx | 16 + components/{new-menu => menu}/src/SubMenu.tsx | 0 components/menu/src/hooks/useMenuContext.ts | 70 ++++ components/menu/src/interface.ts | 39 ++ components/menu/src/placements.ts | 52 +++ components/menu/style/dark.less | 39 +- components/menu/style/index.less | 336 ++++++++++++------ components/{new-menu => menu}/style/index.tsx | 0 components/{new-menu => menu}/style/rtl.less | 0 .../{new-menu => menu}/style/status.less | 0 components/new-menu/index.tsx | 0 components/new-menu/src/ItemGroup.tsx | 16 - components/new-menu/src/Menu.tsx | 10 - components/new-menu/src/MenuItem.tsx | 10 - components/{menu => old-menu}/MenuItem.tsx | 0 components/{menu => old-menu}/SubMenu.tsx | 0 .../__tests__/__snapshots__/demo.test.js.snap | 0 .../{menu => old-menu}/__tests__/demo.test.js | 0 .../__tests__/index.test.js | 0 components/old-menu/index.tsx | 323 +++++++++++++++++ .../{new-menu => old-menu}/style/dark.less | 39 +- .../{new-menu => old-menu}/style/index.less | 336 ++++++------------ components/{menu => old-menu}/style/index.ts | 0 components/style/themes/default.less | 19 +- examples/App.vue | 111 ++++-- 29 files changed, 1032 insertions(+), 756 deletions(-) rename components/{new-menu => menu}/src/Divider.tsx (100%) create mode 100644 components/menu/src/ItemGroup.tsx create mode 100644 components/menu/src/Menu.tsx create mode 100644 components/menu/src/MenuItem.tsx rename components/{new-menu => menu}/src/SubMenu.tsx (100%) create mode 100644 components/menu/src/hooks/useMenuContext.ts create mode 100644 components/menu/src/interface.ts create mode 100644 components/menu/src/placements.ts rename components/{new-menu => menu}/style/index.tsx (100%) rename components/{new-menu => menu}/style/rtl.less (100%) rename components/{new-menu => menu}/style/status.less (100%) delete mode 100644 components/new-menu/index.tsx delete mode 100644 components/new-menu/src/ItemGroup.tsx delete mode 100644 components/new-menu/src/Menu.tsx delete mode 100644 components/new-menu/src/MenuItem.tsx rename components/{menu => old-menu}/MenuItem.tsx (100%) rename components/{menu => old-menu}/SubMenu.tsx (100%) rename components/{menu => old-menu}/__tests__/__snapshots__/demo.test.js.snap (100%) rename components/{menu => old-menu}/__tests__/demo.test.js (100%) rename components/{menu => old-menu}/__tests__/index.test.js (100%) create mode 100644 components/old-menu/index.tsx rename components/{new-menu => old-menu}/style/dark.less (81%) rename components/{new-menu => old-menu}/style/index.less (55%) rename components/{menu => old-menu}/style/index.ts (100%) diff --git a/components/menu/index.tsx b/components/menu/index.tsx index 842ace832f..120eed0742 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 0000000000..b44604b478 --- /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 0000000000..918adb0bc4 --- /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 0000000000..ecdb8916c1 --- /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 0000000000..0864a82d8d --- /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 0000000000..e1db277395 --- /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 0000000000..7b45acf921 --- /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 03d837a6aa..1ad2abf991 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 53ac605993..b8956d2fc0 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); - } + &::before { + transform: rotate(45deg) translateY(-2.5px); + } + &::after { + transform: rotate(-45deg) translateY(2.5px); } } - &-inline > .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow { + &:hover > &-title > &-expand-icon, + &:hover > &-title > &-arrow { + color: @menu-highlight-color; + } + + .@{menu-prefix-cls}-inline-collapsed &-arrow, + &-inline &-arrow { + // ↓ &::before { - transform: rotate(-45deg) translateX(2px); + transform: rotate(-45deg) translateX(2.5px); } &::after { - transform: rotate(45deg) translateX(-2px); + 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); - } + &-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 e69de29bb2..0000000000 diff --git a/components/new-menu/src/ItemGroup.tsx b/components/new-menu/src/ItemGroup.tsx deleted file mode 100644 index 191c164472..0000000000 --- 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 0bf679ac74..0000000000 --- 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 20268d09a9..0000000000 --- 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 0000000000..842ace832f --- /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 1ad2abf991..03d837a6aa 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 b8956d2fc0..53ac605993 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: ''; - } - &::before { - transform: rotate(45deg) translateY(-2.5px); + 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); + } } - &::after { - transform: rotate(-45deg) translateY(2.5px); + > .@{menu-prefix-cls}-submenu-title:hover .@{menu-prefix-cls}-submenu-arrow { + &::after, + &::before { + background: linear-gradient(to right, @menu-highlight-color, @menu-highlight-color); + } } } - &:hover > &-title > &-expand-icon, - &:hover > &-title > &-arrow { - color: @menu-highlight-color; - } - - .@{menu-prefix-cls}-inline-collapsed &-arrow, - &-inline &-arrow { - // ↓ + &-inline > .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow { &::before { - transform: rotate(-45deg) translateX(2.5px); + transform: rotate(-45deg) translateX(2px); } &::after { - transform: rotate(45deg) translateX(-2.5px); + transform: rotate(45deg) translateX(-2px); } } - &-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 666c69872b..dcb8fa8cb5 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 e15daac464..e845326581 100644 --- a/examples/App.vue +++ b/examples/App.vue @@ -1,39 +1,92 @@ - From 2ab77978f2eb4ce634753e8b00fe5f24b5112ac4 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Sat, 15 May 2021 23:05:54 +0800 Subject: [PATCH 03/20] refactor: menu --- components/menu/src/Menu.tsx | 35 +++++++++-- components/menu/src/MenuItem.tsx | 66 ++++++++++++++++++++- components/menu/src/SubMenu.tsx | 2 + components/menu/src/hooks/useKeyPath.ts | 23 +++++++ components/menu/src/hooks/useMenuContext.ts | 15 ++--- components/menu/src/interface.ts | 2 + examples/App.vue | 1 + v2-doc | 2 +- 8 files changed, 130 insertions(+), 16 deletions(-) create mode 100644 components/menu/src/hooks/useKeyPath.ts diff --git a/components/menu/src/Menu.tsx b/components/menu/src/Menu.tsx index 918adb0bc4..cd2bf72be6 100644 --- a/components/menu/src/Menu.tsx +++ b/components/menu/src/Menu.tsx @@ -1,9 +1,14 @@ -import usePrefixCls from 'ant-design-vue/es/_util/hooks/usePrefixCls'; -import { defineComponent, ExtractPropTypes } from 'vue'; +import { Key } from '../../_util/type'; +import { computed, defineComponent, ExtractPropTypes, ref, PropType } from 'vue'; import useProvideMenu from './hooks/useMenuContext'; +import useConfigInject from '../../_util/hooks/useConfigInject'; +import { MenuTheme, MenuMode } from './interface'; export const menuProps = { prefixCls: String, + disabled: Boolean, + theme: { type: String as PropType, default: 'light' }, + mode: { type: String as PropType, default: 'vertical' }, }; export type MenuProps = Partial>; @@ -12,10 +17,30 @@ export default defineComponent({ name: 'AMenu', props: menuProps, setup(props, { slots }) { - const prefixCls = usePrefixCls('menu', props); - useProvideMenu({ prefixCls }); + const { prefixCls, direction } = useConfigInject('menu', props); + const activeKeys = ref([]); + const openKeys = ref([]); + const selectedKeys = ref([]); + const changeActiveKeys = (keys: Key[]) => { + activeKeys.value = keys; + }; + const disabled = computed(() => !!props.disabled); + useProvideMenu({ prefixCls, activeKeys, openKeys, selectedKeys, changeActiveKeys, disabled }); + const isRtl = computed(() => direction.value === 'rtl'); + const mergedMode = ref('vertical'); + const mergedInlineCollapsed = ref(false); + const className = computed(() => { + return { + [`${prefixCls.value}`]: true, + [`${prefixCls.value}-root`]: true, + [`${prefixCls.value}-${mergedMode.value}`]: true, + [`${prefixCls.value}-inline-collapsed`]: mergedInlineCollapsed.value, + [`${prefixCls.value}-rtl`]: isRtl.value, + [`${prefixCls.value}-${props.theme}`]: true, + }; + }); return () => { - return
    {slots.default?.()}
    ; + return
      {slots.default?.()}
    ; }; }, }); diff --git a/components/menu/src/MenuItem.tsx b/components/menu/src/MenuItem.tsx index ecdb8916c1..415b32b1d2 100644 --- a/components/menu/src/MenuItem.tsx +++ b/components/menu/src/MenuItem.tsx @@ -1,16 +1,76 @@ -import { defineComponent, getCurrentInstance } from 'vue'; +import { computed, defineComponent, getCurrentInstance, ref, watch } from 'vue'; +import { useInjectKeyPath } from './hooks/useKeyPath'; +import { useInjectMenu } from './hooks/useMenuContext'; let indexGuid = 0; export default defineComponent({ name: 'AMenuItem', - setup(props, { slots }) { + props: { + role: String, + disabled: Boolean, + }, + emits: ['mouseenter', 'mouseleave'], + setup(props, { slots, emit }) { const instance = getCurrentInstance(); const key = instance.vnode.key; const uniKey = `menu_item_${++indexGuid}`; + const parentKeys = useInjectKeyPath(); + console.log(parentKeys.value); + const { prefixCls, activeKeys, disabled, changeActiveKeys } = useInjectMenu(); + const isActive = ref(false); + watch( + activeKeys, + () => { + isActive.value = !!activeKeys.value.find(val => val === key); + }, + { immediate: true }, + ); + const mergedDisabled = computed(() => disabled.value || props.disabled); + const selected = computed(() => false); + const classNames = computed(() => { + const itemCls = `${prefixCls.value}-item`; + return { + [`${itemCls}`]: true, + [`${itemCls}-active`]: isActive.value, + [`${itemCls}-selected`]: selected.value, + [`${itemCls}-disabled`]: mergedDisabled.value, + }; + }); + const onMouseEnter = (event: MouseEvent) => { + if (!mergedDisabled.value) { + changeActiveKeys([...parentKeys.value, key]); + emit('mouseenter', event); + } + }; + const onMouseLeave = (event: MouseEvent) => { + if (!mergedDisabled.value) { + changeActiveKeys([]); + emit('mouseleave', event); + } + }; return () => { - return
  • {slots.default?.()}
  • ; + // ============================ Render ============================ + const optionRoleProps = {}; + + if (props.role === 'option') { + optionRoleProps['aria-selected'] = selected.value; + } + return ( +
  • + {slots.default?.()} +
  • + ); }; }, }); diff --git a/components/menu/src/SubMenu.tsx b/components/menu/src/SubMenu.tsx index b88004338c..51e186a944 100644 --- a/components/menu/src/SubMenu.tsx +++ b/components/menu/src/SubMenu.tsx @@ -1,8 +1,10 @@ import { defineComponent } from 'vue'; +import useProvideKeyPath from './hooks/useKeyPath'; export default defineComponent({ name: 'ASubMenu', setup(props, { slots }) { + useProvideKeyPath(); return () => { return
      {slots.default?.()}
    ; }; diff --git a/components/menu/src/hooks/useKeyPath.ts b/components/menu/src/hooks/useKeyPath.ts new file mode 100644 index 0000000000..a9ecfcee79 --- /dev/null +++ b/components/menu/src/hooks/useKeyPath.ts @@ -0,0 +1,23 @@ +import { Key } from '../../../_util/type'; +import { computed, ComputedRef, getCurrentInstance, inject, InjectionKey, provide } from 'vue'; + +const KeyPathContext: InjectionKey> = Symbol('KeyPathContext'); + +const useInjectKeyPath = () => { + return inject( + KeyPathContext, + computed(() => []), + ); +}; + +const useProvideKeyPath = () => { + const parentKeys = useInjectKeyPath(); + const key = getCurrentInstance().vnode.key; + const keys = computed(() => [...parentKeys.value, key]); + provide(KeyPathContext, keys); + return keys; +}; + +export { useProvideKeyPath, useInjectKeyPath, KeyPathContext }; + +export default useProvideKeyPath; diff --git a/components/menu/src/hooks/useMenuContext.ts b/components/menu/src/hooks/useMenuContext.ts index 0864a82d8d..76c9596311 100644 --- a/components/menu/src/hooks/useMenuContext.ts +++ b/components/menu/src/hooks/useMenuContext.ts @@ -1,4 +1,5 @@ -import { computed, ComputedRef, inject, InjectionKey, provide } from 'vue'; +import { Key } from '../../../_util/type'; +import { ComputedRef, inject, InjectionKey, provide, Ref } from 'vue'; // import { // BuiltinPlacements, @@ -10,19 +11,21 @@ import { computed, ComputedRef, inject, InjectionKey, provide } from 'vue'; export interface MenuContextProps { prefixCls: ComputedRef; - // openKeys: string[]; + openKeys: Ref; + selectedKeys: Ref; // rtl?: boolean; // // Mode // mode: MenuMode; // // Disabled - // disabled?: boolean; + disabled?: ComputedRef; // // Used for overflow only. Prevent hidden node trigger open // overflowDisabled?: boolean; // // Active - // activeKey: string; + activeKeys: Ref; + changeActiveKeys: (keys: Key[]) => void; // onActive: (key: string) => void; // onInactive: (key: string) => void; @@ -60,9 +63,7 @@ const useProvideMenu = (props: MenuContextProps) => { }; const useInjectMenu = () => { - return inject(MenuContextKey, { - prefixCls: computed(() => 'ant'), - }); + return inject(MenuContextKey); }; export { useProvideMenu, MenuContextKey, useInjectMenu }; diff --git a/components/menu/src/interface.ts b/components/menu/src/interface.ts index e1db277395..d0f9c377d9 100644 --- a/components/menu/src/interface.ts +++ b/components/menu/src/interface.ts @@ -1,3 +1,5 @@ +export type MenuTheme = 'light' | 'dark'; + // ========================== Basic ========================== export type MenuMode = 'horizontal' | 'vertical' | 'inline'; diff --git a/examples/App.vue b/examples/App.vue index e845326581..3caf71cb9c 100644 --- a/examples/App.vue +++ b/examples/App.vue @@ -7,6 +7,7 @@ mode="inline" @click="handleClick" > + Option 0 - - - Option 1 - Option 2 - + + + Option 1 + + Option 2 Option 3 Option 4 From aa8dc1a0c271832906be8e0412c50ef61936de08 Mon Sep 17 00:00:00 2001 From: tanjinzhou <415800467@qq.com> Date: Mon, 17 May 2021 18:15:50 +0800 Subject: [PATCH 05/20] refactor: menu --- components/menu/src/SubMenu.tsx | 21 ++++++- components/vc-overflow/Item.tsx | 105 ++++++++++++++++++++++++++++++++ components/vc-overflow/index.ts | 5 ++ v2-doc | 2 +- 4 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 components/vc-overflow/Item.tsx create mode 100644 components/vc-overflow/index.ts diff --git a/components/menu/src/SubMenu.tsx b/components/menu/src/SubMenu.tsx index 09ac845f26..84ee6e242a 100644 --- a/components/menu/src/SubMenu.tsx +++ b/components/menu/src/SubMenu.tsx @@ -1,5 +1,5 @@ import PropTypes from '../../_util/vue-types'; -import { computed, defineComponent } from 'vue'; +import { computed, defineComponent, getCurrentInstance, ref } from 'vue'; import useProvideKeyPath, { useInjectKeyPath } from './hooks/useKeyPath'; import { useInjectMenu, useProvideFirstLevel } from './hooks/useMenuContext'; import { getPropsSlot, isValidElement } from 'ant-design-vue/es/_util/props-util'; @@ -20,18 +20,35 @@ export default defineComponent({ setup(props, { slots, attrs }) { useProvideKeyPath(); useProvideFirstLevel(false); + const instance = getCurrentInstance(); + const key = instance.vnode.key; const keyPath = useInjectKeyPath(); const { prefixCls, activeKeys, - disabled, + disabled: contextDisabled, changeActiveKeys, rtl, mode, inlineCollapsed, antdMenuTheme, + openKeys, + overflowDisabled, } = useInjectMenu(); + const subMenuPrefixCls = computed(() => `${prefixCls}-submenu`); + const mergedDisabled = computed(() => contextDisabled.value || props.disabled); + const elementRef = ref(); + const popupRef = ref(); + + // // ================================ Icon ================================ + // const mergedItemIcon = itemIcon || contextItemIcon; + // const mergedExpandIcon = expandIcon || contextExpandIcon; + + // ================================ Open ================================ + const originOpen = computed(() => openKeys.value.includes(key)); + const open = computed(() => !overflowDisabled.value && originOpen.value); + const popupClassName = computed(() => classNames(prefixCls, `${prefixCls.value}-${antdMenuTheme.value}`, props.popupClassName), ); diff --git a/components/vc-overflow/Item.tsx b/components/vc-overflow/Item.tsx new file mode 100644 index 0000000000..e8045ccb8f --- /dev/null +++ b/components/vc-overflow/Item.tsx @@ -0,0 +1,105 @@ +import { + computed, + CSSProperties, + defineComponent, + HTMLAttributes, + onUnmounted, + PropType, + ref, +} from 'vue'; +import ResizeObserver from '../vc-resize-observer'; +import classNames from '../_util/classNames'; +import { Key, VueNode } from '../_util/type'; +import PropTypes from '../_util/vue-types'; + +export default defineComponent({ + name: 'InternalItem', + props: { + prefixCls: String, + item: PropTypes.any, + renderItem: Function as PropType<(item: any) => VueNode>, + responsive: Boolean, + itemKey: [String, Number], + registerSize: Function as PropType<(key: Key, width: number | null) => void>, + display: Boolean, + order: Number, + component: PropTypes.any, + invalidate: Boolean, + }, + setup(props, { slots, expose }) { + const mergedHidden = computed(() => props.responsive && !props.display); + const itemNodeRef = ref(); + + expose({ itemNodeRef }); + + // ================================ Effect ================================ + function internalRegisterSize(width: number | null) { + props.registerSize(props.itemKey!, width); + } + + onUnmounted(() => { + internalRegisterSize(null); + }); + + return () => { + const { + prefixCls, + invalidate, + item, + renderItem, + responsive, + registerSize, + itemKey, + display, + order, + component: Component = 'div', + ...restProps + } = props; + const children = slots.default?.(); + // ================================ Render ================================ + const childNode = renderItem && item !== undefined ? renderItem(item) : children; + + let overflowStyle: CSSProperties | undefined; + if (!invalidate) { + overflowStyle = { + opacity: mergedHidden.value ? 0 : 1, + height: mergedHidden.value ? 0 : undefined, + overflowY: mergedHidden.value ? 'hidden' : undefined, + order: responsive ? order : undefined, + pointerEvents: mergedHidden.value ? 'none' : undefined, + }; + } + + const overflowProps: HTMLAttributes = {}; + if (mergedHidden.value) { + overflowProps['aria-hidden'] = true; + } + + let itemNode = ( + + {childNode} + + ); + + if (responsive) { + itemNode = ( + { + internalRegisterSize(offsetWidth); + }} + > + {itemNode} + + ); + } + + return itemNode; + }; + }, +}); diff --git a/components/vc-overflow/index.ts b/components/vc-overflow/index.ts new file mode 100644 index 0000000000..0d37249788 --- /dev/null +++ b/components/vc-overflow/index.ts @@ -0,0 +1,5 @@ +// import Overflow, { OverflowProps } from './Overflow'; + +// export { OverflowProps }; + +// export default Overflow; diff --git a/v2-doc b/v2-doc index d197053285..a7013ae87f 160000 --- a/v2-doc +++ b/v2-doc @@ -1 +1 @@ -Subproject commit d197053285b81e77718621c0b5b94cb3b21831a2 +Subproject commit a7013ae87f69dcbcf547f4b023255b8a7a775557 From f0baa6118bac6eed68b99199123020591d419528 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Mon, 17 May 2021 23:17:27 +0800 Subject: [PATCH 06/20] refactor: menu --- components/menu/src/Menu.tsx | 11 +- components/menu/src/SubMenu.tsx | 183 ++++++++++++++++++-- components/menu/src/SubMenuList.tsx | 6 +- components/menu/src/hooks/useMenuContext.ts | 27 +-- v2-doc | 2 +- 5 files changed, 200 insertions(+), 29 deletions(-) diff --git a/components/menu/src/Menu.tsx b/components/menu/src/Menu.tsx index 49cbd8ff3d..0ed4d8f3ef 100644 --- a/components/menu/src/Menu.tsx +++ b/components/menu/src/Menu.tsx @@ -18,6 +18,7 @@ export const menuProps = { prefixCls: String, disabled: Boolean, inlineCollapsed: Boolean, + overflowDisabled: Boolean, theme: { type: String as PropType, default: 'light' }, mode: { type: String as PropType, default: 'vertical' }, @@ -38,7 +39,8 @@ export type MenuProps = Partial>; export default defineComponent({ name: 'AMenu', props: menuProps, - setup(props, { slots }) { + emits: ['update:openKeys', 'openChange'], + setup(props, { slots, emit }) { const { prefixCls, direction } = useConfigInject('menu', props); const siderCollapsed = inject( @@ -105,6 +107,11 @@ export default defineComponent({ useProvideFirstLevel(true); + const onOpenChange = (key: Key, open: boolean) => { + // emit('update:openKeys', openKeys); + emit('openChange', open); + }; + useProvideMenu({ prefixCls, activeKeys, @@ -124,6 +131,8 @@ export default defineComponent({ antdMenuTheme: computed(() => props.theme), siderCollapsed, defaultMotions, + overflowDisabled: computed(() => props.overflowDisabled), + onOpenChange, }); return () => { return
      {slots.default?.()}
    ; diff --git a/components/menu/src/SubMenu.tsx b/components/menu/src/SubMenu.tsx index 84ee6e242a..98627c5bd2 100644 --- a/components/menu/src/SubMenu.tsx +++ b/components/menu/src/SubMenu.tsx @@ -1,9 +1,13 @@ import PropTypes from '../../_util/vue-types'; -import { computed, defineComponent, getCurrentInstance, ref } from 'vue'; +import { computed, defineComponent, getCurrentInstance, ref, watch, PropType } from 'vue'; import useProvideKeyPath, { useInjectKeyPath } from './hooks/useKeyPath'; -import { useInjectMenu, useProvideFirstLevel } from './hooks/useMenuContext'; +import { useInjectMenu, useProvideFirstLevel, MenuContextProvider } from './hooks/useMenuContext'; import { getPropsSlot, isValidElement } from 'ant-design-vue/es/_util/props-util'; import classNames from 'ant-design-vue/es/_util/classNames'; +import useDirectionStyle from './hooks/useDirectionStyle'; +import PopupTrigger from './PopupTrigger'; +import SubMenuList from './SubMenuList'; +import InlineSubMenuList from './InlineSubMenuList'; export default defineComponent({ name: 'ASubMenu', @@ -13,30 +17,32 @@ export default defineComponent({ disabled: Boolean, level: Number, popupClassName: String, - popupOffset: [Number, Number], + popupOffset: Array as PropType, + internalPopupClose: Boolean, }, slots: ['icon', 'title'], + emits: ['titleClick', 'titleMouseenter', 'titleMouseleave'], inheritAttrs: false, - setup(props, { slots, attrs }) { + setup(props, { slots, attrs, emit }) { useProvideKeyPath(); useProvideFirstLevel(false); const instance = getCurrentInstance(); const key = instance.vnode.key; - const keyPath = useInjectKeyPath(); + const parentKeys = useInjectKeyPath(); const { prefixCls, activeKeys, disabled: contextDisabled, changeActiveKeys, - rtl, mode, inlineCollapsed, antdMenuTheme, openKeys, overflowDisabled, + onOpenChange, } = useInjectMenu(); - const subMenuPrefixCls = computed(() => `${prefixCls}-submenu`); + const subMenuPrefixCls = computed(() => `${prefixCls.value}-submenu`); const mergedDisabled = computed(() => contextDisabled.value || props.disabled); const elementRef = ref(); const popupRef = ref(); @@ -49,8 +55,73 @@ export default defineComponent({ const originOpen = computed(() => openKeys.value.includes(key)); const open = computed(() => !overflowDisabled.value && originOpen.value); + // =============================== Select =============================== + const childrenSelected = ref(true); // isSubPathKey(selectedKeys, eventKey); + + const isActive = ref(false); + watch( + activeKeys, + () => { + isActive.value = !!activeKeys.value.find(val => val === key); + }, + { immediate: true }, + ); + + // =============================== Events =============================== + // >>>> Title click + const onInternalTitleClick = (e: Event) => { + // Skip if disabled + if (mergedDisabled) { + return; + } + emit('titleClick', e, key); + + // Trigger open by click when mode is `inline` + if (mode.value === 'inline') { + onOpenChange(key, !originOpen); + } + }; + + const onMouseEnter = (event: MouseEvent) => { + if (!mergedDisabled.value) { + changeActiveKeys([...parentKeys.value, key]); + emit('titleMouseenter', event); + } + }; + const onMouseLeave = (event: MouseEvent) => { + if (!mergedDisabled.value) { + changeActiveKeys([]); + emit('titleMouseleave', event); + } + }; + + // ========================== DirectionStyle ========================== + const directionStyle = useDirectionStyle(computed(() => parentKeys.value.length)); + + // >>>>> Visible change + const onPopupVisibleChange = (newVisible: boolean) => { + if (mode.value !== 'inline') { + onOpenChange(key, newVisible); + } + }; + + /** + * Used for accessibility. Helper will focus element without key board. + * We should manually trigger an active + */ + const onInternalFocus = () => { + changeActiveKeys([...parentKeys.value, key]); + }; + + // =============================== Render =============================== + const popupId = key && `${key}-popup`; + const popupClassName = computed(() => - classNames(prefixCls, `${prefixCls.value}-${antdMenuTheme.value}`, props.popupClassName), + classNames( + prefixCls.value, + `${prefixCls.value}-${antdMenuTheme.value}`, + props.popupClassName, + ), ); const renderTitle = (title: any, icon: any) => { if (!icon) { @@ -78,13 +149,103 @@ export default defineComponent({ `${prefixCls.value}-${mode.value === 'inline' ? 'inline' : 'vertical'}`, ), ); + + // Cache mode if it change to `inline` which do not have popup motion + const triggerModeRef = computed(() => { + return mode.value !== 'inline' && parentKeys.value.length > 1 ? 'vertical' : mode.value; + }); + + const renderMode = computed(() => (mode.value === 'horizontal' ? 'vertical' : mode.value)); + return () => { const icon = getPropsSlot(slots, props, 'icon'); const title = renderTitle(getPropsSlot(slots, props, 'title'), icon); + const subMenuPrefixClsValue = subMenuPrefixCls.value; + let titleNode = ( + + ); + + if (!overflowDisabled.value) { + const triggerMode = triggerModeRef.value; + + // Still wrap with Trigger here since we need avoid react re-mount dom node + // Which makes motion failed + titleNode = ( + ( + + + {slots.default?.()} + + + ), + }} + > + {titleNode} + + ); + } return ( -
      - {slots.default?.()} -
    + +
  • + {titleNode} + + {/* Inline mode */} + {!overflowDisabled.value && ( + + {slots.default?.()} + + )} +
  • +
    ); }; }, diff --git a/components/menu/src/SubMenuList.tsx b/components/menu/src/SubMenuList.tsx index d98343d6f1..a5d9040091 100644 --- a/components/menu/src/SubMenuList.tsx +++ b/components/menu/src/SubMenuList.tsx @@ -7,9 +7,9 @@ const InternalSubMenuList: FunctionalComponent = (_props, { slots, attrs })
      diff --git a/components/menu/src/hooks/useMenuContext.ts b/components/menu/src/hooks/useMenuContext.ts index 060d70e598..097aea7a5e 100644 --- a/components/menu/src/hooks/useMenuContext.ts +++ b/components/menu/src/hooks/useMenuContext.ts @@ -1,5 +1,5 @@ import { Key } from '../../../_util/type'; -import { ComputedRef, FunctionalComponent, inject, InjectionKey, provide, Ref } from 'vue'; +import { ComputedRef, defineComponent, inject, InjectionKey, provide, Ref } from 'vue'; import { BuiltinPlacements, MenuMode, MenuTheme, TriggerSubMenuAction } from '../interface'; export interface MenuContextProps { @@ -21,7 +21,7 @@ export interface MenuContextProps { // // Disabled disabled?: ComputedRef; // // Used for overflow only. Prevent hidden node trigger open - // overflowDisabled?: boolean; + overflowDisabled?: ComputedRef; // // Active activeKeys: Ref; @@ -52,7 +52,7 @@ export interface MenuContextProps { // // Function // onItemClick: MenuClickEventHandler; - // onOpenChange: (key: string, open: boolean) => void; + onOpenChange: (key: Key, open: boolean) => void; getPopupContainer: ComputedRef<(node: HTMLElement) => HTMLElement>; } @@ -75,16 +75,17 @@ const useInjectFirstLevel = () => { return inject(MenuFirstLevelContextKey, true); }; -const MenuContextProvider: FunctionalComponent<{ props: Record }> = ( - props, - { slots }, -) => { - useProvideMenu({ ...useInjectMenu(), ...props }); - return slots.default?.(); -}; -MenuContextProvider.props = { props: Object }; -MenuContextProvider.inheritAttrs = false; -MenuContextProvider.displayName = 'MenuContextProvider'; +const MenuContextProvider = defineComponent({ + name: 'MenuContextProvider', + inheritAttrs: false, + props: { + props: Object, + }, + setup(props, { slots }) { + useProvideMenu({ ...useInjectMenu(), ...props }); + return () => slots.default?.(); + }, +}); export { useProvideMenu, diff --git a/v2-doc b/v2-doc index a7013ae87f..d197053285 160000 --- a/v2-doc +++ b/v2-doc @@ -1 +1 @@ -Subproject commit a7013ae87f69dcbcf547f4b023255b8a7a775557 +Subproject commit d197053285b81e77718621c0b5b94cb3b21831a2 From 8dd74a99771bf5273787d026444ed9ceec632274 Mon Sep 17 00:00:00 2001 From: tanjinzhou <415800467@qq.com> Date: Tue, 18 May 2021 18:56:33 +0800 Subject: [PATCH 07/20] refactor: menu --- components/menu/src/Menu.tsx | 64 ++++++++++++++++++--- components/menu/src/MenuItem.tsx | 8 +-- components/menu/src/SubMenu.tsx | 64 +++++++++++++++++---- components/menu/src/hooks/useKeyPath.ts | 25 ++++---- components/menu/src/hooks/useMenuContext.ts | 12 +++- v2-doc | 2 +- 6 files changed, 138 insertions(+), 37 deletions(-) diff --git a/components/menu/src/Menu.tsx b/components/menu/src/Menu.tsx index 0ed4d8f3ef..9e412969f7 100644 --- a/components/menu/src/Menu.tsx +++ b/components/menu/src/Menu.tsx @@ -7,8 +7,11 @@ import { PropType, inject, watchEffect, + watch, + reactive, } from 'vue'; -import useProvideMenu, { useProvideFirstLevel } from './hooks/useMenuContext'; +import shallowEqual from '../../_util/shallowequal'; +import useProvideMenu, { StoreMenuInfo, useProvideFirstLevel } from './hooks/useMenuContext'; import useConfigInject from '../../_util/hooks/useConfigInject'; import { MenuTheme, MenuMode, BuiltinPlacements, TriggerSubMenuAction } from './interface'; import devWarning from 'ant-design-vue/es/vc-util/devWarning'; @@ -19,6 +22,7 @@ export const menuProps = { disabled: Boolean, inlineCollapsed: Boolean, overflowDisabled: Boolean, + openKeys: Array, theme: { type: String as PropType, default: 'light' }, mode: { type: String as PropType, default: 'vertical' }, @@ -42,7 +46,7 @@ export default defineComponent({ emits: ['update:openKeys', 'openChange'], setup(props, { slots, emit }) { const { prefixCls, direction } = useConfigInject('menu', props); - + const store = reactive>({}); const siderCollapsed = inject( 'layoutSiderCollapsed', computed(() => undefined), @@ -70,8 +74,19 @@ export default defineComponent({ }); const activeKeys = ref([]); - const openKeys = ref([]); const selectedKeys = ref([]); + + const mergedOpenKeys = ref([]); + + watch( + () => props.openKeys, + (openKeys = mergedOpenKeys.value) => { + console.log('mergedOpenKeys', openKeys); + mergedOpenKeys.value = openKeys; + }, + { immediate: true }, + ); + const changeActiveKeys = (keys: Key[]) => { activeKeys.value = keys; }; @@ -107,15 +122,46 @@ export default defineComponent({ useProvideFirstLevel(true); - const onOpenChange = (key: Key, open: boolean) => { - // emit('update:openKeys', openKeys); - emit('openChange', open); + const getChildrenKeys = (eventKeys: string[]): Key[] => { + const keys = []; + eventKeys.forEach(eventKey => { + const { key, childrenEventKeys } = store[eventKey] as any; + keys.push(key, ...getChildrenKeys(childrenEventKeys.value)); + }); + return keys; + }; + + const onInternalOpenChange = (eventKey: Key, open: boolean) => { + const { key, childrenEventKeys } = store[eventKey] as any; + let newOpenKeys = mergedOpenKeys.value.filter(k => k !== key); + + if (open) { + newOpenKeys.push(key); + } else if (mergedMode.value !== 'inline') { + // We need find all related popup to close + const subPathKeys = getChildrenKeys(childrenEventKeys.value); + newOpenKeys = newOpenKeys.filter(k => !subPathKeys.includes(k)); + } + + if (!shallowEqual(mergedOpenKeys, newOpenKeys)) { + mergedOpenKeys.value = newOpenKeys; + emit('update:openKeys', newOpenKeys); + emit('openChange', key, open); + } + }; + + const registerMenuInfo = (key: string, info: StoreMenuInfo) => { + store[key] = info as any; + }; + const unRegisterMenuInfo = (key: string) => { + delete store[key]; }; useProvideMenu({ + store, prefixCls, activeKeys, - openKeys, + openKeys: mergedOpenKeys, selectedKeys, changeActiveKeys, disabled, @@ -132,7 +178,9 @@ export default defineComponent({ siderCollapsed, defaultMotions, overflowDisabled: computed(() => props.overflowDisabled), - onOpenChange, + onOpenChange: onInternalOpenChange, + registerMenuInfo, + unRegisterMenuInfo, }); return () => { return
        {slots.default?.()}
      ; diff --git a/components/menu/src/MenuItem.tsx b/components/menu/src/MenuItem.tsx index 3c1bf23c11..5fbaeef493 100644 --- a/components/menu/src/MenuItem.tsx +++ b/components/menu/src/MenuItem.tsx @@ -23,9 +23,9 @@ export default defineComponent({ setup(props, { slots, emit, attrs }) { const instance = getCurrentInstance(); const key = instance.vnode.key; - const uniKey = `menu_item_${++indexGuid}`; - const parentKeys = useInjectKeyPath(); - console.log(parentKeys.value); + const eventKey = `menu_item_${++indexGuid}_$$_${key}`; + const { parentEventKeys } = useInjectKeyPath(); + console.log(parentEventKeys.value); const { prefixCls, activeKeys, @@ -58,7 +58,7 @@ export default defineComponent({ }); const onMouseEnter = (event: MouseEvent) => { if (!mergedDisabled.value) { - changeActiveKeys([...parentKeys.value, key]); + changeActiveKeys([...parentEventKeys.value, key]); emit('mouseenter', event); } }; diff --git a/components/menu/src/SubMenu.tsx b/components/menu/src/SubMenu.tsx index 98627c5bd2..ea5ed37634 100644 --- a/components/menu/src/SubMenu.tsx +++ b/components/menu/src/SubMenu.tsx @@ -1,5 +1,13 @@ import PropTypes from '../../_util/vue-types'; -import { computed, defineComponent, getCurrentInstance, ref, watch, PropType } from 'vue'; +import { + computed, + defineComponent, + getCurrentInstance, + ref, + watch, + PropType, + onBeforeUnmount, +} from 'vue'; import useProvideKeyPath, { useInjectKeyPath } from './hooks/useKeyPath'; import { useInjectMenu, useProvideFirstLevel, MenuContextProvider } from './hooks/useMenuContext'; import { getPropsSlot, isValidElement } from 'ant-design-vue/es/_util/props-util'; @@ -9,6 +17,7 @@ import PopupTrigger from './PopupTrigger'; import SubMenuList from './SubMenuList'; import InlineSubMenuList from './InlineSubMenuList'; +let indexGuid = 0; export default defineComponent({ name: 'ASubMenu', props: { @@ -24,11 +33,34 @@ export default defineComponent({ emits: ['titleClick', 'titleMouseenter', 'titleMouseleave'], inheritAttrs: false, setup(props, { slots, attrs, emit }) { - useProvideKeyPath(); useProvideFirstLevel(false); + const instance = getCurrentInstance(); const key = instance.vnode.key; - const parentKeys = useInjectKeyPath(); + + const eventKey = `sub_menu_${++indexGuid}_$$_${key}`; + const { parentEventKeys, parentInfo } = useInjectKeyPath(); + const keysPath = computed(() => [...parentEventKeys.value, eventKey]); + + const childrenEventKeys = ref([]); + const menuInfo = { + eventKey, + key, + parentEventKeys, + childrenEventKeys, + }; + + parentInfo.childrenEventKeys?.value.push(eventKey); + onBeforeUnmount(() => { + if (parentInfo.childrenEventKeys) { + parentInfo.childrenEventKeys.value = parentInfo.childrenEventKeys?.value.filter( + k => k != eventKey, + ); + } + }); + + useProvideKeyPath(eventKey, menuInfo); + const { prefixCls, activeKeys, @@ -40,8 +72,16 @@ export default defineComponent({ openKeys, overflowDisabled, onOpenChange, + registerMenuInfo, + unRegisterMenuInfo, } = useInjectMenu(); + registerMenuInfo(eventKey, menuInfo); + + onBeforeUnmount(() => { + unRegisterMenuInfo(eventKey); + }); + const subMenuPrefixCls = computed(() => `${prefixCls.value}-submenu`); const mergedDisabled = computed(() => contextDisabled.value || props.disabled); const elementRef = ref(); @@ -71,20 +111,20 @@ export default defineComponent({ // >>>> Title click const onInternalTitleClick = (e: Event) => { // Skip if disabled - if (mergedDisabled) { + if (mergedDisabled.value) { return; } emit('titleClick', e, key); // Trigger open by click when mode is `inline` if (mode.value === 'inline') { - onOpenChange(key, !originOpen); + onOpenChange(eventKey, !originOpen.value); } }; const onMouseEnter = (event: MouseEvent) => { if (!mergedDisabled.value) { - changeActiveKeys([...parentKeys.value, key]); + changeActiveKeys(keysPath.value); emit('titleMouseenter', event); } }; @@ -96,12 +136,12 @@ export default defineComponent({ }; // ========================== DirectionStyle ========================== - const directionStyle = useDirectionStyle(computed(() => parentKeys.value.length)); + const directionStyle = useDirectionStyle(computed(() => keysPath.value.length)); // >>>>> Visible change const onPopupVisibleChange = (newVisible: boolean) => { if (mode.value !== 'inline') { - onOpenChange(key, newVisible); + onOpenChange(eventKey, newVisible); } }; @@ -110,11 +150,11 @@ export default defineComponent({ * We should manually trigger an active */ const onInternalFocus = () => { - changeActiveKeys([...parentKeys.value, key]); + changeActiveKeys(keysPath.value); }; // =============================== Render =============================== - const popupId = key && `${key}-popup`; + const popupId = eventKey && `${eventKey}-popup`; const popupClassName = computed(() => classNames( @@ -152,7 +192,7 @@ export default defineComponent({ // Cache mode if it change to `inline` which do not have popup motion const triggerModeRef = computed(() => { - return mode.value !== 'inline' && parentKeys.value.length > 1 ? 'vertical' : mode.value; + return mode.value !== 'inline' && keysPath.value.length > 1 ? 'vertical' : mode.value; }); const renderMode = computed(() => (mode.value === 'horizontal' ? 'vertical' : mode.value)); @@ -240,7 +280,7 @@ export default defineComponent({ {/* Inline mode */} {!overflowDisabled.value && ( - + {slots.default?.()} )} diff --git a/components/menu/src/hooks/useKeyPath.ts b/components/menu/src/hooks/useKeyPath.ts index a9ecfcee79..35f30a1ec8 100644 --- a/components/menu/src/hooks/useKeyPath.ts +++ b/components/menu/src/hooks/useKeyPath.ts @@ -1,20 +1,23 @@ import { Key } from '../../../_util/type'; -import { computed, ComputedRef, getCurrentInstance, inject, InjectionKey, provide } from 'vue'; +import { computed, ComputedRef, inject, InjectionKey, provide } from 'vue'; +import { StoreMenuInfo } from './useMenuContext'; -const KeyPathContext: InjectionKey> = Symbol('KeyPathContext'); +const KeyPathContext: InjectionKey<{ + parentEventKeys: ComputedRef; + parentInfo: StoreMenuInfo; +}> = Symbol('KeyPathContext'); const useInjectKeyPath = () => { - return inject( - KeyPathContext, - computed(() => []), - ); + return inject(KeyPathContext, { + parentEventKeys: computed(() => []), + parentInfo: {} as StoreMenuInfo, + }); }; -const useProvideKeyPath = () => { - const parentKeys = useInjectKeyPath(); - const key = getCurrentInstance().vnode.key; - const keys = computed(() => [...parentKeys.value, key]); - provide(KeyPathContext, keys); +const useProvideKeyPath = (eventKey: string, menuInfo: StoreMenuInfo) => { + const { parentEventKeys } = useInjectKeyPath(); + const keys = computed(() => [...parentEventKeys.value, eventKey]); + provide(KeyPathContext, { parentEventKeys: keys, parentInfo: menuInfo }); return keys; }; diff --git a/components/menu/src/hooks/useMenuContext.ts b/components/menu/src/hooks/useMenuContext.ts index 097aea7a5e..0b4fea4f50 100644 --- a/components/menu/src/hooks/useMenuContext.ts +++ b/components/menu/src/hooks/useMenuContext.ts @@ -1,8 +1,18 @@ import { Key } from '../../../_util/type'; -import { ComputedRef, defineComponent, inject, InjectionKey, provide, Ref } from 'vue'; +import { ComputedRef, defineComponent, inject, InjectionKey, provide, Ref, UnwrapRef } from 'vue'; import { BuiltinPlacements, MenuMode, MenuTheme, TriggerSubMenuAction } from '../interface'; +export interface StoreMenuInfo { + eventKey: string; + key: Key; + parentEventKeys: ComputedRef; + childrenEventKeys: Ref; + isLeaf?: boolean; +} export interface MenuContextProps { + store: UnwrapRef>; + registerMenuInfo: (key: string, info: StoreMenuInfo) => void; + unRegisterMenuInfo: (key: string) => void; prefixCls: ComputedRef; openKeys: Ref; selectedKeys: Ref; diff --git a/v2-doc b/v2-doc index d197053285..a7013ae87f 160000 --- a/v2-doc +++ b/v2-doc @@ -1 +1 @@ -Subproject commit d197053285b81e77718621c0b5b94cb3b21831a2 +Subproject commit a7013ae87f69dcbcf547f4b023255b8a7a775557 From 0d318a324ae4eaf6d245c860170a9aac772acebb Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Tue, 18 May 2021 22:44:59 +0800 Subject: [PATCH 08/20] refactor: menu --- components/_util/transition.tsx | 10 ++++++++-- components/menu/src/Menu.tsx | 16 ++++++++++++---- components/menu/src/SubMenu.tsx | 9 --------- components/menu/src/hooks/useMenuContext.ts | 5 +++-- v2-doc | 2 +- 5 files changed, 24 insertions(+), 18 deletions(-) diff --git a/components/_util/transition.tsx b/components/_util/transition.tsx index 275d1cef72..d6c3c238c8 100644 --- a/components/_util/transition.tsx +++ b/components/_util/transition.tsx @@ -108,9 +108,15 @@ const getCurrentHeight: MotionEventHandler = node => ({ height: node.offsetHeigh // const skipOpacityTransition: MotionEndEventHandler = (_, event) => // (event as TransitionEvent).propertyName === 'height'; -const collapseMotion: BaseTransitionProps = { - // motionName: 'ant-motion-collapse', +export interface CSSMotionProps extends Partial> { + name?: string; + css?: boolean; +} + +const collapseMotion: CSSMotionProps = { + name: 'ant-motion-collapse', appear: true, + css: false, // onAppearStart: getCollapsedHeight, onBeforeEnter: getCollapsedHeight, onEnter: getRealHeight, diff --git a/components/menu/src/Menu.tsx b/components/menu/src/Menu.tsx index 9e412969f7..81410d59f8 100644 --- a/components/menu/src/Menu.tsx +++ b/components/menu/src/Menu.tsx @@ -9,13 +9,14 @@ import { watchEffect, watch, reactive, + onMounted, } from 'vue'; import shallowEqual from '../../_util/shallowequal'; import useProvideMenu, { StoreMenuInfo, useProvideFirstLevel } from './hooks/useMenuContext'; import useConfigInject from '../../_util/hooks/useConfigInject'; import { MenuTheme, MenuMode, BuiltinPlacements, TriggerSubMenuAction } from './interface'; import devWarning from 'ant-design-vue/es/vc-util/devWarning'; -import { collapseMotion } from 'ant-design-vue/es/_util/transition'; +import { collapseMotion, CSSMotionProps } from 'ant-design-vue/es/_util/transition'; export const menuProps = { prefixCls: String, @@ -24,6 +25,8 @@ export const menuProps = { overflowDisabled: Boolean, openKeys: Array, + motion: Object as PropType, + theme: { type: String as PropType, default: 'light' }, mode: { type: String as PropType, default: 'vertical' }, @@ -59,6 +62,10 @@ export default defineComponent({ return inlineCollapsed; }); + const isMounted = ref(false); + onMounted(() => { + isMounted.value = true; + }); watchEffect(() => { devWarning( !('inlineCollapsed' in props && props.mode !== 'inline'), @@ -115,9 +122,9 @@ export default defineComponent({ }); const defaultMotions = { - horizontal: { motionName: `ant-slide-up` }, + horizontal: { name: `ant-slide-up` }, inline: collapseMotion, - other: { motionName: `ant-zoom-big` }, + other: { name: `ant-zoom-big` }, }; useProvideFirstLevel(true); @@ -176,7 +183,8 @@ export default defineComponent({ inlineCollapsed: mergedInlineCollapsed, antdMenuTheme: computed(() => props.theme), siderCollapsed, - defaultMotions, + defaultMotions: computed(() => (isMounted.value ? defaultMotions : null)), + motion: computed(() => (isMounted.value ? props.motion : null)), overflowDisabled: computed(() => props.overflowDisabled), onOpenChange: onInternalOpenChange, registerMenuInfo, diff --git a/components/menu/src/SubMenu.tsx b/components/menu/src/SubMenu.tsx index ea5ed37634..b99caeec1b 100644 --- a/components/menu/src/SubMenu.tsx +++ b/components/menu/src/SubMenu.tsx @@ -182,14 +182,6 @@ export default defineComponent({ ); }; - const className = computed(() => - classNames( - prefixCls.value, - `${prefixCls.value}-sub`, - `${prefixCls.value}-${mode.value === 'inline' ? 'inline' : 'vertical'}`, - ), - ); - // Cache mode if it change to `inline` which do not have popup motion const triggerModeRef = computed(() => { return mode.value !== 'inline' && keysPath.value.length > 1 ? 'vertical' : mode.value; @@ -266,7 +258,6 @@ export default defineComponent({ class={classNames( subMenuPrefixClsValue, `${subMenuPrefixClsValue}-${mode.value}`, - className.value, attrs.class, { [`${subMenuPrefixClsValue}-open`]: open.value, diff --git a/components/menu/src/hooks/useMenuContext.ts b/components/menu/src/hooks/useMenuContext.ts index 0b4fea4f50..a2eeae83b8 100644 --- a/components/menu/src/hooks/useMenuContext.ts +++ b/components/menu/src/hooks/useMenuContext.ts @@ -1,6 +1,7 @@ import { Key } from '../../../_util/type'; import { ComputedRef, defineComponent, inject, InjectionKey, provide, Ref, UnwrapRef } from 'vue'; import { BuiltinPlacements, MenuMode, MenuTheme, TriggerSubMenuAction } from '../interface'; +import { CSSMotionProps } from '../../../_util/transition'; export interface StoreMenuInfo { eventKey: string; @@ -46,8 +47,8 @@ export interface MenuContextProps { inlineIndent: ComputedRef; // // Motion - motion?: any; - defaultMotions?: Partial<{ [key in MenuMode | 'other']: any }>; + motion?: ComputedRef; + defaultMotions?: ComputedRef | null>; // // Popup subMenuOpenDelay: ComputedRef; diff --git a/v2-doc b/v2-doc index a7013ae87f..d197053285 160000 --- a/v2-doc +++ b/v2-doc @@ -1 +1 @@ -Subproject commit a7013ae87f69dcbcf547f4b023255b8a7a775557 +Subproject commit d197053285b81e77718621c0b5b94cb3b21831a2 From a887ec5b8fd7e5699fe707097221241c90eddbcd Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Tue, 18 May 2021 23:39:47 +0800 Subject: [PATCH 09/20] refactor: menu --- components/_util/transition.tsx | 12 ++++-------- components/menu/src/InlineSubMenuList.tsx | 20 +++++++++++++++----- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/components/_util/transition.tsx b/components/_util/transition.tsx index d6c3c238c8..1e49b0babd 100644 --- a/components/_util/transition.tsx +++ b/components/_util/transition.tsx @@ -92,23 +92,20 @@ export declare type MotionEvent = (TransitionEvent | AnimationEvent) & { }; export declare type MotionEventHandler = ( - element: HTMLElement, + element: Element, done?: () => void, ) => CSSProperties | void; -export declare type MotionEndEventHandler = ( - element: HTMLElement, - done?: () => void, -) => boolean | void; +export declare type MotionEndEventHandler = (element: Element, done?: () => void) => boolean | void; // ================== Collapse Motion ================== const getCollapsedHeight: MotionEventHandler = () => ({ height: 0, opacity: 0 }); const getRealHeight: MotionEventHandler = node => ({ height: node.scrollHeight, opacity: 1 }); -const getCurrentHeight: MotionEventHandler = node => ({ height: node.offsetHeight }); +const getCurrentHeight: MotionEventHandler = (node: any) => ({ height: node.offsetHeight }); // const skipOpacityTransition: MotionEndEventHandler = (_, event) => // (event as TransitionEvent).propertyName === 'height'; -export interface CSSMotionProps extends Partial> { +export interface CSSMotionProps extends Partial> { name?: string; css?: boolean; } @@ -116,7 +113,6 @@ export interface CSSMotionProps extends Partial const collapseMotion: CSSMotionProps = { name: 'ant-motion-collapse', appear: true, - css: false, // onAppearStart: getCollapsedHeight, onBeforeEnter: getCollapsedHeight, onEnter: getRealHeight, diff --git a/components/menu/src/InlineSubMenuList.tsx b/components/menu/src/InlineSubMenuList.tsx index 5bf53b5a98..71cdd1e1b4 100644 --- a/components/menu/src/InlineSubMenuList.tsx +++ b/components/menu/src/InlineSubMenuList.tsx @@ -1,7 +1,9 @@ import { computed, defineComponent, ref, watch } from '@vue/runtime-core'; +import Transition from 'ant-design-vue/es/_util/transition'; import { useInjectMenu, MenuContextProvider } from './hooks/useMenuContext'; import { MenuMode } from './interface'; import SubMenuList from './SubMenuList'; + export default defineComponent({ name: 'InlineSubMenuList', inheritAttrs: false, @@ -12,10 +14,12 @@ export default defineComponent({ }, setup(props, { slots }) { const fixedMode: MenuMode = 'inline'; - const { prefixCls, forceSubMenuRender, motion, mode } = useInjectMenu(); + const { prefixCls, forceSubMenuRender, motion, mode, defaultMotions } = useInjectMenu(); const sameModeRef = computed(() => mode.value === fixedMode); const destroy = ref(!sameModeRef.value); + const mergedOpen = computed(() => (sameModeRef.value ? props.open : false)); + // ================================= Effect ================================= // Reset destroy state when mode change back watch( @@ -27,9 +31,11 @@ export default defineComponent({ }, { flush: 'post' }, ); - let transitionProps = computed(() => { - return { appear: props.keyPath.length > 1, css: false }; - }); + + const mergedMotion = computed(() => ({ + ...(motion.value || defaultMotions.value?.[fixedMode]), + appear: props.keyPath.length <= 1, + })); return () => { if (destroy.value) { @@ -42,7 +48,11 @@ export default defineComponent({ locked: !sameModeRef.value, }} > - {slots.default?.()} + + + {slots.default?.()} + + ); }; From 9bb16977a99d7cb698f3aeca85e7130b0b528114 Mon Sep 17 00:00:00 2001 From: tanjinzhou <415800467@qq.com> Date: Wed, 19 May 2021 15:34:25 +0800 Subject: [PATCH 10/20] refactor: menu --- components/_util/transition.tsx | 53 +++++++++++++++------ components/menu/src/InlineSubMenuList.tsx | 22 +++++---- components/menu/src/Menu.tsx | 1 - components/menu/src/hooks/useMenuContext.ts | 19 +++++++- components/style/core/global.less | 1 - components/style/core/motion.less | 1 - components/style/core/motion/fade.less | 9 ++-- components/style/core/motion/move.less | 9 ++-- components/style/core/motion/other.less | 14 ++++-- components/style/core/motion/slide.less | 9 ++-- components/style/core/motion/swing.less | 34 ------------- components/style/core/motion/zoom.less | 12 +++-- examples/App.vue | 6 +-- v2-doc | 2 +- 14 files changed, 106 insertions(+), 86 deletions(-) delete mode 100644 components/style/core/motion/swing.less diff --git a/components/_util/transition.tsx b/components/_util/transition.tsx index 1e49b0babd..3fc0478629 100644 --- a/components/_util/transition.tsx +++ b/components/_util/transition.tsx @@ -3,6 +3,7 @@ import { CSSProperties, defineComponent, nextTick, + Ref, Transition as T, TransitionGroup as TG, } from 'vue'; @@ -91,17 +92,17 @@ export declare type MotionEvent = (TransitionEvent | AnimationEvent) & { deadline?: boolean; }; -export declare type MotionEventHandler = ( - element: Element, - done?: () => void, -) => CSSProperties | void; +export declare type MotionEventHandler = (element: Element, done?: () => void) => CSSProperties; export declare type MotionEndEventHandler = (element: Element, done?: () => void) => boolean | void; // ================== Collapse Motion ================== const getCollapsedHeight: MotionEventHandler = () => ({ height: 0, opacity: 0 }); -const getRealHeight: MotionEventHandler = node => ({ height: node.scrollHeight, opacity: 1 }); -const getCurrentHeight: MotionEventHandler = (node: any) => ({ height: node.offsetHeight }); +const getRealHeight: MotionEventHandler = node => ({ + height: `${node.scrollHeight}px`, + opacity: 1, +}); +const getCurrentHeight: MotionEventHandler = (node: any) => ({ height: `${node.offsetHeight}px` }); // const skipOpacityTransition: MotionEndEventHandler = (_, event) => // (event as TransitionEvent).propertyName === 'height'; @@ -110,14 +111,38 @@ export interface CSSMotionProps extends Partial> { css?: boolean; } -const collapseMotion: CSSMotionProps = { - name: 'ant-motion-collapse', - appear: true, - // onAppearStart: getCollapsedHeight, - onBeforeEnter: getCollapsedHeight, - onEnter: getRealHeight, - onBeforeLeave: getCurrentHeight, - onLeave: getCollapsedHeight, +const collapseMotion = (style: Ref, className: Ref): CSSMotionProps => { + return { + name: 'ant-motion-collapse', + appear: true, + css: true, + onBeforeEnter: node => { + className.value = 'ant-motion-collapse'; + style.value = getCollapsedHeight(node); + }, + onEnter: node => { + nextTick(() => { + style.value = getRealHeight(node); + }); + }, + onAfterEnter: () => { + className.value = ''; + style.value = {}; + }, + onBeforeLeave: node => { + className.value = 'ant-motion-collapse'; + style.value = getCurrentHeight(node); + }, + onLeave: node => { + window.setTimeout(() => { + style.value = getCollapsedHeight(node); + }); + }, + onAfterLeave: () => { + className.value = ''; + style.value = {}; + }, + }; }; export { Transition, TransitionGroup, collapseMotion }; diff --git a/components/menu/src/InlineSubMenuList.tsx b/components/menu/src/InlineSubMenuList.tsx index 71cdd1e1b4..2208ffd84e 100644 --- a/components/menu/src/InlineSubMenuList.tsx +++ b/components/menu/src/InlineSubMenuList.tsx @@ -1,4 +1,4 @@ -import { computed, defineComponent, ref, watch } from '@vue/runtime-core'; +import { computed, defineComponent, reactive, ref, watch } from '@vue/runtime-core'; import Transition from 'ant-design-vue/es/_util/transition'; import { useInjectMenu, MenuContextProvider } from './hooks/useMenuContext'; import { MenuMode } from './interface'; @@ -31,12 +31,13 @@ export default defineComponent({ }, { flush: 'post' }, ); - - const mergedMotion = computed(() => ({ - ...(motion.value || defaultMotions.value?.[fixedMode]), - appear: props.keyPath.length <= 1, - })); - + const style = ref({}); + const className = ref(''); + const mergedMotion = computed(() => { + const m = motion.value || defaultMotions.value?.[fixedMode]; + const res = typeof m === 'function' ? m(style, className) : m; + return { ...res, appear: props.keyPath.length <= 1 }; + }); return () => { if (destroy.value) { return null; @@ -49,7 +50,12 @@ export default defineComponent({ }} > - + {slots.default?.()} diff --git a/components/menu/src/Menu.tsx b/components/menu/src/Menu.tsx index 81410d59f8..63166ba00e 100644 --- a/components/menu/src/Menu.tsx +++ b/components/menu/src/Menu.tsx @@ -88,7 +88,6 @@ export default defineComponent({ watch( () => props.openKeys, (openKeys = mergedOpenKeys.value) => { - console.log('mergedOpenKeys', openKeys); mergedOpenKeys.value = openKeys; }, { immediate: true }, diff --git a/components/menu/src/hooks/useMenuContext.ts b/components/menu/src/hooks/useMenuContext.ts index a2eeae83b8..3c6c82921f 100644 --- a/components/menu/src/hooks/useMenuContext.ts +++ b/components/menu/src/hooks/useMenuContext.ts @@ -1,5 +1,14 @@ import { Key } from '../../../_util/type'; -import { ComputedRef, defineComponent, inject, InjectionKey, provide, Ref, UnwrapRef } from 'vue'; +import { + ComputedRef, + CSSProperties, + defineComponent, + inject, + InjectionKey, + provide, + Ref, + UnwrapRef, +} from 'vue'; import { BuiltinPlacements, MenuMode, MenuTheme, TriggerSubMenuAction } from '../interface'; import { CSSMotionProps } from '../../../_util/transition'; @@ -48,7 +57,13 @@ export interface MenuContextProps { // // Motion motion?: ComputedRef; - defaultMotions?: ComputedRef | null>; + defaultMotions?: ComputedRef, className: Ref) => CSSMotionProps); + } + > | null>; // // Popup subMenuOpenDelay: ComputedRef; diff --git a/components/style/core/global.less b/components/style/core/global.less index 1b3e8d2849..d4f53103fb 100644 --- a/components/style/core/global.less +++ b/components/style/core/global.less @@ -239,7 +239,6 @@ a { &[disabled] { color: @disabled-color; cursor: not-allowed; - pointer-events: none; } } diff --git a/components/style/core/motion.less b/components/style/core/motion.less index cce881b779..730c693687 100644 --- a/components/style/core/motion.less +++ b/components/style/core/motion.less @@ -3,7 +3,6 @@ @import 'motion/move'; @import 'motion/other'; @import 'motion/slide'; -@import 'motion/swing'; @import 'motion/zoom'; // For common/openAnimation diff --git a/components/style/core/motion/fade.less b/components/style/core/motion/fade.less index fd9d621cba..c703b5973a 100644 --- a/components/style/core/motion/fade.less +++ b/components/style/core/motion/fade.less @@ -1,11 +1,12 @@ .fade-motion(@className, @keyframeName) { - .make-motion(@className, @keyframeName); - .@{className}-enter, - .@{className}-appear { + @name: ~'@{ant-prefix}-@{className}'; + .make-motion(@name, @keyframeName); + .@{name}-enter, + .@{name}-appear { opacity: 0; animation-timing-function: linear; } - .@{className}-leave { + .@{name}-leave { animation-timing-function: linear; } } diff --git a/components/style/core/motion/move.less b/components/style/core/motion/move.less index a11b911b3c..e7972d77af 100644 --- a/components/style/core/motion/move.less +++ b/components/style/core/motion/move.less @@ -1,11 +1,12 @@ .move-motion(@className, @keyframeName) { - .make-motion(@className, @keyframeName); - .@{className}-enter, - .@{className}-appear { + @name: ~'@{ant-prefix}-@{className}'; + .make-motion(@name, @keyframeName); + .@{name}-enter, + .@{name}-appear { opacity: 0; animation-timing-function: @ease-out-circ; } - .@{className}-leave { + .@{name}-leave { animation-timing-function: @ease-in-circ; } } diff --git a/components/style/core/motion/other.less b/components/style/core/motion/other.less index 80887427a6..d1a25494e7 100644 --- a/components/style/core/motion/other.less +++ b/components/style/core/motion/other.less @@ -4,17 +4,23 @@ } } -[ant-click-animating='true'], -[ant-click-animating-without-extra-node='true'] { +@click-animating-true: ~"[@{ant-prefix}-click-animating='true']"; +@click-animating-with-extra-node-true: ~"[@{ant-prefix}-click-animating-without-extra-node='true']"; + +@{click-animating-true}, +@{click-animating-with-extra-node-true} { position: relative; } html { --antd-wave-shadow-color: @primary-color; + --scroll-bar: 0; } -[ant-click-animating-without-extra-node='true']::after, -.ant-click-animating-node { +@click-animating-with-extra-node-true-after: ~'@{click-animating-with-extra-node-true}::after'; + +@{click-animating-with-extra-node-true-after}, +.@{ant-prefix}-click-animating-node { position: absolute; top: 0; right: 0; diff --git a/components/style/core/motion/slide.less b/components/style/core/motion/slide.less index b5032c799d..f838c6e4ac 100644 --- a/components/style/core/motion/slide.less +++ b/components/style/core/motion/slide.less @@ -1,11 +1,12 @@ .slide-motion(@className, @keyframeName) { - .make-motion(@className, @keyframeName); - .@{className}-enter, - .@{className}-appear { + @name: ~'@{ant-prefix}-@{className}'; + .make-motion(@name, @keyframeName); + .@{name}-enter, + .@{name}-appear { opacity: 0; animation-timing-function: @ease-out-quint; } - .@{className}-leave { + .@{name}-leave { animation-timing-function: @ease-in-quint; } } diff --git a/components/style/core/motion/swing.less b/components/style/core/motion/swing.less deleted file mode 100644 index 138a942d47..0000000000 --- a/components/style/core/motion/swing.less +++ /dev/null @@ -1,34 +0,0 @@ -.swing-motion(@className, @keyframeName) { - .@{className}-enter, - .@{className}-appear { - .motion-common(); - - animation-play-state: paused; - } - .@{className}-enter.@{className}-enter-active, - .@{className}-appear.@{className}-appear-active { - animation-name: ~'@{keyframeName}In'; - animation-play-state: running; - } -} - -.swing-motion(swing, antSwing); - -@keyframes antSwingIn { - 0%, - 100% { - transform: translateX(0); - } - 20% { - transform: translateX(-10px); - } - 40% { - transform: translateX(10px); - } - 60% { - transform: translateX(-5px); - } - 80% { - transform: translateX(5px); - } -} diff --git a/components/style/core/motion/zoom.less b/components/style/core/motion/zoom.less index f633274291..8c2c57acac 100644 --- a/components/style/core/motion/zoom.less +++ b/components/style/core/motion/zoom.less @@ -1,15 +1,17 @@ .zoom-motion(@className, @keyframeName, @duration: @animation-duration-base) { - .make-motion(@className, @keyframeName, @duration); - .@{className}-enter, - .@{className}-appear { + @name: ~'@{ant-prefix}-@{className}'; + .make-motion(@name, @keyframeName, @duration); + .@{name}-enter, + .@{name}-appear { transform: scale(0); // need this by yiminghe opacity: 0; animation-timing-function: @ease-out-circ; + &-prepare { transform: none; } } - .@{className}-leave { + .@{name}-leave { animation-timing-function: @ease-in-out-circ; } } @@ -54,7 +56,7 @@ opacity: 0; } 5% { - transform: scale(0.2); + transform: scale(0.8); opacity: 0; } 100% { diff --git a/examples/App.vue b/examples/App.vue index cd9ce1dfc6..dceeed41a7 100644 --- a/examples/App.vue +++ b/examples/App.vue @@ -67,15 +67,15 @@ export default defineComponent({ const selectedKeys = ref(['1']); const openKeys = ref(['sub1']); const handleClick = (e: Event) => { - console.log('click', e); + // console.log('click', e); }; const titleClick = (e: Event) => { - console.log('titleClick', e); + // console.log('titleClick', e); }; watch( () => openKeys, val => { - console.log('openKeys', val); + // console.log('openKeys', val); }, ); return { diff --git a/v2-doc b/v2-doc index d197053285..a7013ae87f 160000 --- a/v2-doc +++ b/v2-doc @@ -1 +1 @@ -Subproject commit d197053285b81e77718621c0b5b94cb3b21831a2 +Subproject commit a7013ae87f69dcbcf547f4b023255b8a7a775557 From f3db7548b5105306d9f368fff17e901c810edc8b Mon Sep 17 00:00:00 2001 From: tanjinzhou <415800467@qq.com> Date: Wed, 19 May 2021 18:41:02 +0800 Subject: [PATCH 11/20] refactor: menu --- components/menu/src/Menu.tsx | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/components/menu/src/Menu.tsx b/components/menu/src/Menu.tsx index 63166ba00e..9978db3abd 100644 --- a/components/menu/src/Menu.tsx +++ b/components/menu/src/Menu.tsx @@ -100,6 +100,39 @@ export default defineComponent({ const isRtl = computed(() => direction.value === 'rtl'); const mergedMode = ref('vertical'); const mergedInlineCollapsed = ref(false); + + const isInlineMode = computed(() => mergedMode.value === 'inline'); + + // >>>>> Cache & Reset open keys when inlineCollapsed changed + const inlineCacheOpenKeys = ref([]); + + // Cache + watchEffect(() => { + if (isInlineMode.value) { + inlineCacheOpenKeys.value = mergedOpenKeys.value; + } + }); + + const mountRef = ref(false); + + // Restore + watch(isInlineMode, () => { + if (!mountRef.value) { + mountRef.value = true; + return; + } + + if (isInlineMode.value) { + mergedOpenKeys.value = inlineCacheOpenKeys.value; + } else { + const empty = []; + mergedOpenKeys.value = empty; + // Trigger open event in case its in control + emit('update:openKeys', empty); + emit('openChange', empty); + } + }); + watchEffect(() => { if (props.mode === 'inline' && inlineCollapsed.value) { mergedMode.value = 'vertical'; @@ -152,7 +185,7 @@ export default defineComponent({ if (!shallowEqual(mergedOpenKeys, newOpenKeys)) { mergedOpenKeys.value = newOpenKeys; emit('update:openKeys', newOpenKeys); - emit('openChange', key, open); + emit('openChange', newOpenKeys); } }; From 24efefe16d56905df10f7c0969fd51fe86149ed7 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Wed, 19 May 2021 23:38:56 +0800 Subject: [PATCH 12/20] refactor: menu --- components/menu/src/InlineSubMenuList.tsx | 4 +- components/menu/src/Menu.tsx | 60 +++++++++++++++++++-- components/menu/src/MenuItem.tsx | 26 ++++++++- components/menu/src/hooks/useMenuContext.ts | 10 +++- components/menu/src/interface.ts | 14 +++-- v2-doc | 2 +- 6 files changed, 101 insertions(+), 15 deletions(-) diff --git a/components/menu/src/InlineSubMenuList.tsx b/components/menu/src/InlineSubMenuList.tsx index 2208ffd84e..659e4978d2 100644 --- a/components/menu/src/InlineSubMenuList.tsx +++ b/components/menu/src/InlineSubMenuList.tsx @@ -1,4 +1,4 @@ -import { computed, defineComponent, reactive, ref, watch } from '@vue/runtime-core'; +import { computed, defineComponent, ref, watch } from '@vue/runtime-core'; import Transition from 'ant-design-vue/es/_util/transition'; import { useInjectMenu, MenuContextProvider } from './hooks/useMenuContext'; import { MenuMode } from './interface'; @@ -14,7 +14,7 @@ export default defineComponent({ }, setup(props, { slots }) { const fixedMode: MenuMode = 'inline'; - const { prefixCls, forceSubMenuRender, motion, mode, defaultMotions } = useInjectMenu(); + const { forceSubMenuRender, motion, mode, defaultMotions } = useInjectMenu(); const sameModeRef = computed(() => mode.value === fixedMode); const destroy = ref(!sameModeRef.value); diff --git a/components/menu/src/Menu.tsx b/components/menu/src/Menu.tsx index 9978db3abd..8897be8bc6 100644 --- a/components/menu/src/Menu.tsx +++ b/components/menu/src/Menu.tsx @@ -14,7 +14,14 @@ import { import shallowEqual from '../../_util/shallowequal'; import useProvideMenu, { StoreMenuInfo, useProvideFirstLevel } from './hooks/useMenuContext'; import useConfigInject from '../../_util/hooks/useConfigInject'; -import { MenuTheme, MenuMode, BuiltinPlacements, TriggerSubMenuAction } from './interface'; +import { + MenuTheme, + MenuMode, + BuiltinPlacements, + TriggerSubMenuAction, + MenuInfo, + SelectInfo, +} from './interface'; import devWarning from 'ant-design-vue/es/vc-util/devWarning'; import { collapseMotion, CSSMotionProps } from 'ant-design-vue/es/_util/transition'; @@ -24,6 +31,9 @@ export const menuProps = { inlineCollapsed: Boolean, overflowDisabled: Boolean, openKeys: Array, + selectedKeys: Array, + selectable: Boolean, + multiple: Boolean, motion: Object as PropType, @@ -46,7 +56,7 @@ export type MenuProps = Partial>; export default defineComponent({ name: 'AMenu', props: menuProps, - emits: ['update:openKeys', 'openChange'], + emits: ['update:openKeys', 'openChange', 'select', 'deselect', 'update:selectedKeys'], setup(props, { slots, emit }) { const { prefixCls, direction } = useConfigInject('menu', props); const store = reactive>({}); @@ -81,7 +91,48 @@ export default defineComponent({ }); const activeKeys = ref([]); - const selectedKeys = ref([]); + const mergedSelectedKeys = ref([]); + + watch( + () => props.selectedKeys, + (selectedKeys = mergedSelectedKeys.value) => { + mergedSelectedKeys.value = selectedKeys; + }, + { immediate: true }, + ); + + // >>>>> Trigger select + const triggerSelection = (info: MenuInfo) => { + if (!props.selectable) { + return; + } + + // Insert or Remove + const { key: targetKey } = info; + const exist = mergedSelectedKeys.value.includes(targetKey); + let newSelectedKeys: Key[]; + + if (exist) { + newSelectedKeys = mergedSelectedKeys.value.filter(key => key !== targetKey); + } else if (props.multiple) { + newSelectedKeys = [...mergedSelectedKeys.value, targetKey]; + } else { + newSelectedKeys = [targetKey]; + } + + mergedSelectedKeys.value = newSelectedKeys; + // Trigger event + const selectInfo: SelectInfo = { + ...info, + selectedKeys: newSelectedKeys, + }; + + if (exist) { + emit('deselect', selectInfo); + } else { + emit('select', selectInfo); + } + }; const mergedOpenKeys = ref([]); @@ -201,7 +252,7 @@ export default defineComponent({ prefixCls, activeKeys, openKeys: mergedOpenKeys, - selectedKeys, + selectedKeys: mergedSelectedKeys, changeActiveKeys, disabled, rtl: isRtl, @@ -219,6 +270,7 @@ export default defineComponent({ motion: computed(() => (isMounted.value ? props.motion : null)), overflowDisabled: computed(() => props.overflowDisabled), onOpenChange: onInternalOpenChange, + onItemClick: triggerSelection, registerMenuInfo, unRegisterMenuInfo, }); diff --git a/components/menu/src/MenuItem.tsx b/components/menu/src/MenuItem.tsx index 5fbaeef493..22e85679fc 100644 --- a/components/menu/src/MenuItem.tsx +++ b/components/menu/src/MenuItem.tsx @@ -5,6 +5,7 @@ import { useInjectKeyPath } from './hooks/useKeyPath'; import { useInjectFirstLevel, useInjectMenu } from './hooks/useMenuContext'; import { cloneElement } from '../../_util/vnode'; import Tooltip from '../../tooltip'; +import { MenuInfo } from './interface'; let indexGuid = 0; @@ -17,7 +18,7 @@ export default defineComponent({ title: { type: [String, Boolean] }, icon: PropTypes.VNodeChild, }, - emits: ['mouseenter', 'mouseleave'], + emits: ['mouseenter', 'mouseleave', 'click'], slots: ['icon'], inheritAttrs: false, setup(props, { slots, emit, attrs }) { @@ -34,6 +35,7 @@ export default defineComponent({ rtl, inlineCollapsed, siderCollapsed, + onItemClick, } = useInjectMenu(); const firstLevel = useInjectFirstLevel(); const isActive = ref(false); @@ -56,6 +58,27 @@ export default defineComponent({ [`${itemCls}-disabled`]: mergedDisabled.value, }; }); + + const getEventInfo = (e: MouseEvent): MenuInfo => { + return { + key: key, + eventKey: eventKey, + eventKeyPath: [...parentEventKeys.value, key], + domEvent: e, + }; + }; + + // ============================ Events ============================ + const onInternalClick = (e: MouseEvent) => { + if (mergedDisabled.value) { + return; + } + + const info = getEventInfo(e); + emit('click', e); + onItemClick(info); + }; + const onMouseEnter = (event: MouseEvent) => { if (!mergedDisabled.value) { changeActiveKeys([...parentEventKeys.value, key]); @@ -135,6 +158,7 @@ export default defineComponent({ {...optionRoleProps} onMouseenter={onMouseEnter} onMouseleave={onMouseLeave} + onClick={onInternalClick} title={typeof title === 'string' ? title : undefined} > {cloneElement(icon, { diff --git a/components/menu/src/hooks/useMenuContext.ts b/components/menu/src/hooks/useMenuContext.ts index 3c6c82921f..7d2e9cd5d0 100644 --- a/components/menu/src/hooks/useMenuContext.ts +++ b/components/menu/src/hooks/useMenuContext.ts @@ -9,7 +9,13 @@ import { Ref, UnwrapRef, } from 'vue'; -import { BuiltinPlacements, MenuMode, MenuTheme, TriggerSubMenuAction } from '../interface'; +import { + BuiltinPlacements, + MenuClickEventHandler, + MenuMode, + MenuTheme, + TriggerSubMenuAction, +} from '../interface'; import { CSSMotionProps } from '../../../_util/transition'; export interface StoreMenuInfo { @@ -77,7 +83,7 @@ export interface MenuContextProps { // expandIcon?: RenderIconType; // // Function - // onItemClick: MenuClickEventHandler; + onItemClick: MenuClickEventHandler; onOpenChange: (key: Key, open: boolean) => void; getPopupContainer: ComputedRef<(node: HTMLElement) => HTMLElement>; } diff --git a/components/menu/src/interface.ts b/components/menu/src/interface.ts index d0f9c377d9..92c99d63ed 100644 --- a/components/menu/src/interface.ts +++ b/components/menu/src/interface.ts @@ -1,3 +1,5 @@ +import { Key } from 'ant-design-vue/es/_util/type'; + export type MenuTheme = 'light' | 'dark'; // ========================== Basic ========================== @@ -17,22 +19,24 @@ export interface RenderIconInfo { export type RenderIconType = (props: RenderIconInfo) => any; export interface MenuInfo { - key: string; - keyPath: string[]; + key: Key; + eventKey: string; + keyPath?: string[]; + eventKeyPath: Key[]; domEvent: MouseEvent | KeyboardEvent; } export interface MenuTitleInfo { - key: string; + key: Key; domEvent: MouseEvent | KeyboardEvent; } // ========================== Hover ========================== -export type MenuHoverEventHandler = (info: { key: string; domEvent: MouseEvent }) => void; +export type MenuHoverEventHandler = (info: { key: Key; domEvent: MouseEvent }) => void; // ======================== Selection ======================== export interface SelectInfo extends MenuInfo { - selectedKeys: string[]; + selectedKeys: Key[]; } export type SelectEventHandler = (info: SelectInfo) => void; diff --git a/v2-doc b/v2-doc index a7013ae87f..d197053285 160000 --- a/v2-doc +++ b/v2-doc @@ -1 +1 @@ -Subproject commit a7013ae87f69dcbcf547f4b023255b8a7a775557 +Subproject commit d197053285b81e77718621c0b5b94cb3b21831a2 From 8862f604e235fedecfe62c69fa773f7b00c0c679 Mon Sep 17 00:00:00 2001 From: tanjinzhou <415800467@qq.com> Date: Thu, 20 May 2021 15:24:32 +0800 Subject: [PATCH 13/20] refactor: menu --- components/menu/src/Menu.tsx | 65 ++++++++++--- components/menu/src/MenuItem.tsx | 29 +++++- components/menu/src/SubMenu.tsx | 7 +- components/menu/src/hooks/useMenuContext.ts | 4 +- components/menu/src/interface.ts | 2 +- examples/App.vue | 100 ++++++-------------- v2-doc | 2 +- 7 files changed, 114 insertions(+), 95 deletions(-) diff --git a/components/menu/src/Menu.tsx b/components/menu/src/Menu.tsx index 8897be8bc6..e836ae702f 100644 --- a/components/menu/src/Menu.tsx +++ b/components/menu/src/Menu.tsx @@ -10,6 +10,7 @@ import { watch, reactive, onMounted, + toRaw, } from 'vue'; import shallowEqual from '../../_util/shallowequal'; import useProvideMenu, { StoreMenuInfo, useProvideFirstLevel } from './hooks/useMenuContext'; @@ -24,6 +25,7 @@ import { } from './interface'; import devWarning from 'ant-design-vue/es/vc-util/devWarning'; import { collapseMotion, CSSMotionProps } from 'ant-design-vue/es/_util/transition'; +import uniq from 'lodash-es/uniq'; export const menuProps = { prefixCls: String, @@ -32,8 +34,8 @@ export const menuProps = { overflowDisabled: Boolean, openKeys: Array, selectedKeys: Array, - selectable: Boolean, - multiple: Boolean, + selectable: { type: Boolean, default: true }, + multiple: { type: Boolean, default: false }, motion: Object as PropType, @@ -56,7 +58,7 @@ export type MenuProps = Partial>; export default defineComponent({ name: 'AMenu', props: menuProps, - emits: ['update:openKeys', 'openChange', 'select', 'deselect', 'update:selectedKeys'], + emits: ['update:openKeys', 'openChange', 'select', 'deselect', 'update:selectedKeys', 'click'], setup(props, { slots, emit }) { const { prefixCls, direction } = useConfigInject('menu', props); const store = reactive>({}); @@ -78,13 +80,13 @@ export default defineComponent({ }); watchEffect(() => { devWarning( - !('inlineCollapsed' in props && props.mode !== 'inline'), + !(props.inlineCollapsed === true && props.mode !== 'inline'), 'Menu', '`inlineCollapsed` should only be used when `mode` is inline.', ); devWarning( - !(siderCollapsed.value !== undefined && 'inlineCollapsed' in props), + !(siderCollapsed.value !== undefined && props.inlineCollapsed === true), 'Menu', '`inlineCollapsed` not control Menu under Sider. Should set `collapsed` on Sider instead.', ); @@ -101,18 +103,37 @@ export default defineComponent({ { immediate: true }, ); + const selectedSubMenuEventKeys = ref([]); + + watch( + [store, mergedSelectedKeys], + () => { + let subMenuParentEventKeys = []; + (Object.values(toRaw(store)) as any).forEach((menuInfo: StoreMenuInfo) => { + if (mergedSelectedKeys.value.includes(menuInfo.key)) { + subMenuParentEventKeys.push(...menuInfo.parentEventKeys.value); + } + }); + + subMenuParentEventKeys = uniq(subMenuParentEventKeys); + if (!shallowEqual(selectedSubMenuEventKeys.value, subMenuParentEventKeys)) { + selectedSubMenuEventKeys.value = subMenuParentEventKeys; + } + }, + { immediate: true }, + ); + // >>>>> Trigger select const triggerSelection = (info: MenuInfo) => { if (!props.selectable) { return; } - // Insert or Remove const { key: targetKey } = info; const exist = mergedSelectedKeys.value.includes(targetKey); let newSelectedKeys: Key[]; - if (exist) { + if (exist && props.multiple) { newSelectedKeys = mergedSelectedKeys.value.filter(key => key !== targetKey); } else if (props.multiple) { newSelectedKeys = [...mergedSelectedKeys.value, targetKey]; @@ -120,17 +141,21 @@ export default defineComponent({ newSelectedKeys = [targetKey]; } - mergedSelectedKeys.value = newSelectedKeys; // Trigger event const selectInfo: SelectInfo = { ...info, selectedKeys: newSelectedKeys, }; - - if (exist) { - emit('deselect', selectInfo); - } else { - emit('select', selectInfo); + if (!('selectedKeys' in props)) { + mergedSelectedKeys.value = newSelectedKeys; + } + if (!shallowEqual(newSelectedKeys, mergedSelectedKeys.value)) { + emit('update:selectedKeys', newSelectedKeys); + if (exist && props.multiple) { + emit('deselect', selectInfo); + } else { + emit('select', selectInfo); + } } }; @@ -212,7 +237,7 @@ export default defineComponent({ useProvideFirstLevel(true); - const getChildrenKeys = (eventKeys: string[]): Key[] => { + const getChildrenKeys = (eventKeys: string[] = []): Key[] => { const keys = []; eventKeys.forEach(eventKey => { const { key, childrenEventKeys } = store[eventKey] as any; @@ -221,6 +246,15 @@ export default defineComponent({ return keys; }; + // ========================= Open ========================= + /** + * Click for item. SubMenu do not have selection status + */ + const onInternalClick = (info: MenuInfo) => { + emit('click', info); + triggerSelection(info); + }; + const onInternalOpenChange = (eventKey: Key, open: boolean) => { const { key, childrenEventKeys } = store[eventKey] as any; let newOpenKeys = mergedOpenKeys.value.filter(k => k !== key); @@ -270,9 +304,10 @@ export default defineComponent({ motion: computed(() => (isMounted.value ? props.motion : null)), overflowDisabled: computed(() => props.overflowDisabled), onOpenChange: onInternalOpenChange, - onItemClick: triggerSelection, + onItemClick: onInternalClick, registerMenuInfo, unRegisterMenuInfo, + selectedSubMenuEventKeys, }); return () => { return
        {slots.default?.()}
      ; diff --git a/components/menu/src/MenuItem.tsx b/components/menu/src/MenuItem.tsx index 22e85679fc..9eee8f2dc3 100644 --- a/components/menu/src/MenuItem.tsx +++ b/components/menu/src/MenuItem.tsx @@ -1,6 +1,6 @@ import { flattenChildren, getPropsSlot, isValidElement } from '../../_util/props-util'; import PropTypes from '../../_util/vue-types'; -import { computed, defineComponent, getCurrentInstance, ref, watch } from 'vue'; +import { computed, defineComponent, getCurrentInstance, onBeforeUnmount, ref, watch } from 'vue'; import { useInjectKeyPath } from './hooks/useKeyPath'; import { useInjectFirstLevel, useInjectMenu } from './hooks/useMenuContext'; import { cloneElement } from '../../_util/vnode'; @@ -26,7 +26,6 @@ export default defineComponent({ const key = instance.vnode.key; const eventKey = `menu_item_${++indexGuid}_$$_${key}`; const { parentEventKeys } = useInjectKeyPath(); - console.log(parentEventKeys.value); const { prefixCls, activeKeys, @@ -36,9 +35,30 @@ export default defineComponent({ inlineCollapsed, siderCollapsed, onItemClick, + selectedKeys, + store, + registerMenuInfo, + unRegisterMenuInfo, } = useInjectMenu(); const firstLevel = useInjectFirstLevel(); const isActive = ref(false); + const keyPath = computed(() => { + return [...parentEventKeys.value.map(eK => store[eK].key), key]; + }); + + const menuInfo = { + eventKey, + key, + parentEventKeys, + isLeaf: true, + }; + + registerMenuInfo(eventKey, menuInfo); + + onBeforeUnmount(() => { + unRegisterMenuInfo(eventKey); + }); + watch( activeKeys, () => { @@ -47,7 +67,7 @@ export default defineComponent({ { immediate: true }, ); const mergedDisabled = computed(() => disabled.value || props.disabled); - const selected = computed(() => false); + const selected = computed(() => selectedKeys.value.includes(key)); const classNames = computed(() => { const itemCls = `${prefixCls.value}-item`; return { @@ -63,7 +83,8 @@ export default defineComponent({ return { key: key, eventKey: eventKey, - eventKeyPath: [...parentEventKeys.value, key], + keyPath: keyPath.value, + eventKeyPath: [...parentEventKeys.value, eventKey], domEvent: e, }; }; diff --git a/components/menu/src/SubMenu.tsx b/components/menu/src/SubMenu.tsx index b99caeec1b..b46d2c5ef8 100644 --- a/components/menu/src/SubMenu.tsx +++ b/components/menu/src/SubMenu.tsx @@ -74,6 +74,7 @@ export default defineComponent({ onOpenChange, registerMenuInfo, unRegisterMenuInfo, + selectedSubMenuEventKeys, } = useInjectMenu(); registerMenuInfo(eventKey, menuInfo); @@ -96,7 +97,9 @@ export default defineComponent({ const open = computed(() => !overflowDisabled.value && originOpen.value); // =============================== Select =============================== - const childrenSelected = ref(true); // isSubPathKey(selectedKeys, eventKey); + const childrenSelected = computed(() => { + return selectedSubMenuEventKeys.value.includes(eventKey); + }); const isActive = ref(false); watch( @@ -225,8 +228,6 @@ export default defineComponent({ if (!overflowDisabled.value) { const triggerMode = triggerModeRef.value; - // Still wrap with Trigger here since we need avoid react re-mount dom node - // Which makes motion failed titleNode = ( ; - childrenEventKeys: Ref; + childrenEventKeys?: Ref; isLeaf?: boolean; } export interface MenuContextProps { @@ -32,6 +32,8 @@ export interface MenuContextProps { prefixCls: ComputedRef; openKeys: Ref; selectedKeys: Ref; + + selectedSubMenuEventKeys: Ref; rtl?: ComputedRef; locked?: Ref; diff --git a/components/menu/src/interface.ts b/components/menu/src/interface.ts index 92c99d63ed..02c2f992e2 100644 --- a/components/menu/src/interface.ts +++ b/components/menu/src/interface.ts @@ -21,7 +21,7 @@ export type RenderIconType = (props: RenderIconInfo) => any; export interface MenuInfo { key: Key; eventKey: string; - keyPath?: string[]; + keyPath?: Key[]; eventKeyPath: Key[]; domEvent: MouseEvent | KeyboardEvent; } diff --git a/examples/App.vue b/examples/App.vue index dceeed41a7..b402f06657 100644 --- a/examples/App.vue +++ b/examples/App.vue @@ -1,89 +1,49 @@ -