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')}
+
+
+
+ );
+ };
+ },
+});
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')}
-
-
- );
- };
- },
-});
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 @@
-
-
-
+
+
+
+
+
+ Navigation One
+
+
+
+
+
+ Item 1
+
+ Option 1
+ Option 2
+
+
+ Option 3
+ Option 4
+
+
+
+
-
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 ;
};
},
});
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 ;
};
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
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 16f051d593f50db4876b425732ef798b388bd485 Mon Sep 17 00:00:00 2001
From: tangjinzhou <415800467@qq.com>
Date: Sun, 16 May 2021 23:09:47 +0800
Subject: [PATCH 04/20] refactor: menu
---
components/_util/transition.tsx | 42 +++++++-
components/menu/src/InlineSubMenuList.tsx | 50 +++++++++
components/menu/src/ItemGroup.tsx | 5 +-
components/menu/src/Menu.tsx | 96 ++++++++++++++++-
components/menu/src/MenuItem.tsx | 101 +++++++++++++++---
components/menu/src/PopupTrigger.tsx | 96 +++++++++++++++++
components/menu/src/SubMenu.tsx | 70 +++++++++++-
components/menu/src/SubMenuList.tsx | 23 ++++
.../menu/src/hooks/useDirectionStyle.ts | 14 +++
components/menu/src/hooks/useMenuContext.ts | 68 ++++++++----
components/vc-trigger/Trigger.jsx | 6 +-
components/vc-util/devWarning.ts | 7 ++
examples/App.vue | 13 +--
13 files changed, 533 insertions(+), 58 deletions(-)
create mode 100644 components/menu/src/InlineSubMenuList.tsx
create mode 100644 components/menu/src/PopupTrigger.tsx
create mode 100644 components/menu/src/SubMenuList.tsx
create mode 100644 components/menu/src/hooks/useDirectionStyle.ts
create mode 100644 components/vc-util/devWarning.ts
diff --git a/components/_util/transition.tsx b/components/_util/transition.tsx
index 0281d680eb..275d1cef72 100644
--- a/components/_util/transition.tsx
+++ b/components/_util/transition.tsx
@@ -1,4 +1,11 @@
-import { defineComponent, nextTick, Transition as T, TransitionGroup as TG } from 'vue';
+import {
+ BaseTransitionProps,
+ CSSProperties,
+ defineComponent,
+ nextTick,
+ Transition as T,
+ TransitionGroup as TG,
+} from 'vue';
import { findDOMNode } from './props-util';
export const getTransitionProps = (transitionName: string, opt: object = {}) => {
@@ -80,6 +87,37 @@ if (process.env.NODE_ENV === 'test') {
});
}
-export { Transition, TransitionGroup };
+export declare type MotionEvent = (TransitionEvent | AnimationEvent) & {
+ deadline?: boolean;
+};
+
+export declare type MotionEventHandler = (
+ element: HTMLElement,
+ done?: () => void,
+) => CSSProperties | void;
+
+export declare type MotionEndEventHandler = (
+ element: HTMLElement,
+ 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 skipOpacityTransition: MotionEndEventHandler = (_, event) =>
+// (event as TransitionEvent).propertyName === 'height';
+
+const collapseMotion: BaseTransitionProps = {
+ // motionName: 'ant-motion-collapse',
+ appear: true,
+ // onAppearStart: getCollapsedHeight,
+ onBeforeEnter: getCollapsedHeight,
+ onEnter: getRealHeight,
+ onBeforeLeave: getCurrentHeight,
+ onLeave: getCollapsedHeight,
+};
+
+export { Transition, TransitionGroup, collapseMotion };
export default Transition;
diff --git a/components/menu/src/InlineSubMenuList.tsx b/components/menu/src/InlineSubMenuList.tsx
new file mode 100644
index 0000000000..5bf53b5a98
--- /dev/null
+++ b/components/menu/src/InlineSubMenuList.tsx
@@ -0,0 +1,50 @@
+import { computed, defineComponent, ref, watch } from '@vue/runtime-core';
+import { useInjectMenu, MenuContextProvider } from './hooks/useMenuContext';
+import { MenuMode } from './interface';
+import SubMenuList from './SubMenuList';
+export default defineComponent({
+ name: 'InlineSubMenuList',
+ inheritAttrs: false,
+ props: {
+ id: String,
+ open: Boolean,
+ keyPath: Array,
+ },
+ setup(props, { slots }) {
+ const fixedMode: MenuMode = 'inline';
+ const { prefixCls, forceSubMenuRender, motion, mode } = useInjectMenu();
+ const sameModeRef = computed(() => mode.value === fixedMode);
+ const destroy = ref(!sameModeRef.value);
+
+ // ================================= Effect =================================
+ // Reset destroy state when mode change back
+ watch(
+ mode,
+ () => {
+ if (sameModeRef.value) {
+ destroy.value = false;
+ }
+ },
+ { flush: 'post' },
+ );
+ let transitionProps = computed(() => {
+ return { appear: props.keyPath.length > 1, css: false };
+ });
+
+ return () => {
+ if (destroy.value) {
+ return null;
+ }
+ return (
+
+ {slots.default?.()}
+
+ );
+ };
+ },
+});
diff --git a/components/menu/src/ItemGroup.tsx b/components/menu/src/ItemGroup.tsx
index b44604b478..993f0cb4de 100644
--- a/components/menu/src/ItemGroup.tsx
+++ b/components/menu/src/ItemGroup.tsx
@@ -8,13 +8,14 @@ export default defineComponent({
props: {
title: PropTypes.VNodeChild,
},
+ inheritAttrs: false,
slots: ['title'],
- setup(props, { slots }) {
+ setup(props, { slots, attrs }) {
const { prefixCls } = useInjectMenu();
const groupPrefixCls = computed(() => `${prefixCls.value}-item-group`);
return () => {
return (
- e.stopPropagation()} class={groupPrefixCls.value}>
+ e.stopPropagation()} class={groupPrefixCls.value}>
, default: 'light' },
mode: { type: String as PropType
, default: 'vertical' },
+
+ inlineIndent: { type: Number, default: 24 },
+ subMenuOpenDelay: { type: Number, default: 0.1 },
+ subMenuCloseDelay: { type: Number, default: 0.1 },
+
+ builtinPlacements: { type: Object as PropType },
+
+ triggerSubMenuAction: { type: String as PropType, default: 'hover' },
+
+ getPopupContainer: Function as PropType<(node: HTMLElement) => HTMLElement>,
};
export type MenuProps = Partial>;
@@ -18,6 +40,33 @@ export default defineComponent({
props: menuProps,
setup(props, { slots }) {
const { prefixCls, direction } = useConfigInject('menu', props);
+
+ const siderCollapsed = inject(
+ 'layoutSiderCollapsed',
+ computed(() => undefined),
+ );
+ const inlineCollapsed = computed(() => {
+ const { inlineCollapsed } = props;
+ if (siderCollapsed.value !== undefined) {
+ return siderCollapsed.value;
+ }
+ return inlineCollapsed;
+ });
+
+ watchEffect(() => {
+ devWarning(
+ !('inlineCollapsed' in props && props.mode !== 'inline'),
+ 'Menu',
+ '`inlineCollapsed` should only be used when `mode` is inline.',
+ );
+
+ devWarning(
+ !(siderCollapsed.value !== undefined && 'inlineCollapsed' in props),
+ 'Menu',
+ '`inlineCollapsed` not control Menu under Sider. Should set `collapsed` on Sider instead.',
+ );
+ });
+
const activeKeys = ref([]);
const openKeys = ref([]);
const selectedKeys = ref([]);
@@ -25,10 +74,18 @@ export default defineComponent({
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 mergedMode = ref('vertical');
const mergedInlineCollapsed = ref(false);
+ watchEffect(() => {
+ if (props.mode === 'inline' && inlineCollapsed.value) {
+ mergedMode.value = 'vertical';
+ mergedInlineCollapsed.value = inlineCollapsed.value;
+ }
+ mergedMode.value = props.mode;
+ mergedInlineCollapsed.value = false;
+ });
+
const className = computed(() => {
return {
[`${prefixCls.value}`]: true,
@@ -39,6 +96,35 @@ export default defineComponent({
[`${prefixCls.value}-${props.theme}`]: true,
};
});
+
+ const defaultMotions = {
+ horizontal: { motionName: `ant-slide-up` },
+ inline: collapseMotion,
+ other: { motionName: `ant-zoom-big` },
+ };
+
+ useProvideFirstLevel(true);
+
+ useProvideMenu({
+ prefixCls,
+ activeKeys,
+ openKeys,
+ selectedKeys,
+ changeActiveKeys,
+ disabled,
+ rtl: isRtl,
+ mode: mergedMode,
+ inlineIndent: computed(() => props.inlineIndent),
+ subMenuCloseDelay: computed(() => props.subMenuCloseDelay),
+ subMenuOpenDelay: computed(() => props.subMenuOpenDelay),
+ builtinPlacements: computed(() => props.builtinPlacements),
+ triggerSubMenuAction: computed(() => props.triggerSubMenuAction),
+ getPopupContainer: computed(() => props.getPopupContainer),
+ inlineCollapsed: mergedInlineCollapsed,
+ antdMenuTheme: computed(() => props.theme),
+ siderCollapsed,
+ defaultMotions,
+ });
return () => {
return ;
};
diff --git a/components/menu/src/MenuItem.tsx b/components/menu/src/MenuItem.tsx
index 415b32b1d2..3c1bf23c11 100644
--- a/components/menu/src/MenuItem.tsx
+++ b/components/menu/src/MenuItem.tsx
@@ -1,6 +1,10 @@
+import { flattenChildren, getPropsSlot, isValidElement } from '../../_util/props-util';
+import PropTypes from '../../_util/vue-types';
import { computed, defineComponent, getCurrentInstance, ref, watch } from 'vue';
import { useInjectKeyPath } from './hooks/useKeyPath';
-import { useInjectMenu } from './hooks/useMenuContext';
+import { useInjectFirstLevel, useInjectMenu } from './hooks/useMenuContext';
+import { cloneElement } from '../../_util/vnode';
+import Tooltip from '../../tooltip';
let indexGuid = 0;
@@ -9,15 +13,29 @@ export default defineComponent({
props: {
role: String,
disabled: Boolean,
+ danger: Boolean,
+ title: { type: [String, Boolean] },
+ icon: PropTypes.VNodeChild,
},
emits: ['mouseenter', 'mouseleave'],
- setup(props, { slots, emit }) {
+ slots: ['icon'],
+ inheritAttrs: false,
+ 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 { prefixCls, activeKeys, disabled, changeActiveKeys } = useInjectMenu();
+ const {
+ prefixCls,
+ activeKeys,
+ disabled,
+ changeActiveKeys,
+ rtl,
+ inlineCollapsed,
+ siderCollapsed,
+ } = useInjectMenu();
+ const firstLevel = useInjectFirstLevel();
const isActive = ref(false);
watch(
activeKeys,
@@ -32,6 +50,7 @@ export default defineComponent({
const itemCls = `${prefixCls.value}-item`;
return {
[`${itemCls}`]: true,
+ [`${itemCls}-danger`]: props.danger,
[`${itemCls}-active`]: isActive.value,
[`${itemCls}-selected`]: selected.value,
[`${itemCls}-disabled`]: mergedDisabled.value,
@@ -50,26 +69,80 @@ export default defineComponent({
}
};
+ const renderItemChildren = (icon: any, children: any) => {
+ // inline-collapsed.md demo 依赖 span 来隐藏文字,有 icon 属性,则内部包裹一个 span
+ // ref: https://github.com/ant-design/ant-design/pull/23456
+ if (!icon || (isValidElement(children) && children.type === 'span')) {
+ if (children && inlineCollapsed.value && firstLevel && typeof children === 'string') {
+ return (
+ {children.charAt(0)}
+ );
+ }
+ return children;
+ }
+ return {children};
+ };
+
return () => {
+ const { title } = props;
+ const children = flattenChildren(slots.default?.());
+ const childrenLength = children.length;
+ let tooltipTitle: any = title;
+ if (typeof title === 'undefined') {
+ tooltipTitle = firstLevel ? children : '';
+ } else if (title === false) {
+ tooltipTitle = '';
+ }
+ const tooltipProps: any = {
+ title: tooltipTitle,
+ };
+
+ if (!siderCollapsed.value && !inlineCollapsed.value) {
+ tooltipProps.title = null;
+ // Reset `visible` to fix control mode tooltip display not correct
+ // ref: https://github.com/ant-design/ant-design/issues/16742
+ tooltipProps.visible = false;
+ }
+
// ============================ Render ============================
const optionRoleProps = {};
if (props.role === 'option') {
optionRoleProps['aria-selected'] = selected.value;
}
+
+ const icon = getPropsSlot(slots, props, 'icon');
return (
-
- {slots.default?.()}
-
+
+ {cloneElement(icon, {
+ class: `${prefixCls.value}-item-icon`,
+ })}
+ {renderItemChildren(icon, children)}
+
+
);
};
},
diff --git a/components/menu/src/PopupTrigger.tsx b/components/menu/src/PopupTrigger.tsx
new file mode 100644
index 0000000000..ac291d5c1a
--- /dev/null
+++ b/components/menu/src/PopupTrigger.tsx
@@ -0,0 +1,96 @@
+import Trigger from '../../vc-trigger';
+import { computed, defineComponent, onBeforeUnmount, PropType, ref, watch } from 'vue';
+import { MenuMode } from './interface';
+import { useInjectMenu } from './hooks/useMenuContext';
+import { placements, placementsRtl } from './placements';
+import raf from '../../_util/raf';
+import classNames from '../../_util/classNames';
+
+const popupPlacementMap = {
+ horizontal: 'bottomLeft',
+ vertical: 'rightTop',
+ 'vertical-left': 'rightTop',
+ 'vertical-right': 'leftTop',
+};
+export default defineComponent({
+ name: 'PopupTrigger',
+ props: {
+ prefixCls: String,
+ mode: String as PropType,
+ visible: Boolean,
+ // popup: React.ReactNode;
+ popupClassName: String,
+ popupOffset: Array as PropType,
+ disabled: Boolean,
+ onVisibleChange: Function as PropType<(visible: boolean) => void>,
+ },
+ slots: ['popup'],
+ emits: ['visibleChange'],
+ inheritAttrs: false,
+ setup(props, { slots, emit }) {
+ const innerVisible = ref(false);
+ const {
+ getPopupContainer,
+ rtl,
+ subMenuOpenDelay,
+ subMenuCloseDelay,
+ builtinPlacements,
+ triggerSubMenuAction,
+ } = useInjectMenu();
+
+ const placement = computed(() =>
+ rtl
+ ? { ...placementsRtl, ...builtinPlacements.value }
+ : { ...placements, ...builtinPlacements.value },
+ );
+
+ const popupPlacement = computed(() => popupPlacementMap[props.mode]);
+
+ const visibleRef = ref();
+ watch(
+ () => props.visible,
+ visible => {
+ raf.cancel(visibleRef.value);
+ visibleRef.value = raf(() => {
+ innerVisible.value = visible;
+ });
+ },
+ { immediate: true },
+ );
+ onBeforeUnmount(() => {
+ raf.cancel(visibleRef.value);
+ });
+
+ const onVisibleChange = (visible: boolean) => {
+ emit('visibleChange', visible);
+ };
+ return () => {
+ const { prefixCls, popupClassName, mode, popupOffset, disabled } = props;
+ return (
+
+ );
+ };
+ },
+});
diff --git a/components/menu/src/SubMenu.tsx b/components/menu/src/SubMenu.tsx
index 51e186a944..09ac845f26 100644
--- a/components/menu/src/SubMenu.tsx
+++ b/components/menu/src/SubMenu.tsx
@@ -1,12 +1,74 @@
-import { defineComponent } from 'vue';
-import useProvideKeyPath from './hooks/useKeyPath';
+import PropTypes from '../../_util/vue-types';
+import { computed, defineComponent } 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';
+import classNames from 'ant-design-vue/es/_util/classNames';
export default defineComponent({
name: 'ASubMenu',
- setup(props, { slots }) {
+ props: {
+ icon: PropTypes.VNodeChild,
+ title: PropTypes.VNodeChild,
+ disabled: Boolean,
+ level: Number,
+ popupClassName: String,
+ popupOffset: [Number, Number],
+ },
+ slots: ['icon', 'title'],
+ inheritAttrs: false,
+ setup(props, { slots, attrs }) {
useProvideKeyPath();
+ useProvideFirstLevel(false);
+ const keyPath = useInjectKeyPath();
+ const {
+ prefixCls,
+ activeKeys,
+ disabled,
+ changeActiveKeys,
+ rtl,
+ mode,
+ inlineCollapsed,
+ antdMenuTheme,
+ } = useInjectMenu();
+
+ const popupClassName = computed(() =>
+ classNames(prefixCls, `${prefixCls.value}-${antdMenuTheme.value}`, props.popupClassName),
+ );
+ const renderTitle = (title: any, icon: any) => {
+ if (!icon) {
+ return inlineCollapsed.value && props.level === 1 && title && typeof title === 'string' ? (
+ {title.charAt(0)}
+ ) : (
+ title
+ );
+ }
+ // inline-collapsed.md demo 依赖 span 来隐藏文字,有 icon 属性,则内部包裹一个 span
+ // ref: https://github.com/ant-design/ant-design/pull/23456
+ const titleIsSpan = isValidElement(title) && title.type === 'span';
+ return (
+ <>
+ {icon}
+ {titleIsSpan ? title : {title}}
+ >
+ );
+ };
+
+ const className = computed(() =>
+ classNames(
+ prefixCls.value,
+ `${prefixCls.value}-sub`,
+ `${prefixCls.value}-${mode.value === 'inline' ? 'inline' : 'vertical'}`,
+ ),
+ );
return () => {
- return ;
+ const icon = getPropsSlot(slots, props, 'icon');
+ const title = renderTitle(getPropsSlot(slots, props, 'title'), icon);
+ return (
+
+ );
};
},
});
diff --git a/components/menu/src/SubMenuList.tsx b/components/menu/src/SubMenuList.tsx
new file mode 100644
index 0000000000..d98343d6f1
--- /dev/null
+++ b/components/menu/src/SubMenuList.tsx
@@ -0,0 +1,23 @@
+import classNames from '../../_util/classNames';
+import { FunctionalComponent, provide } from 'vue';
+import { useInjectMenu } from './hooks/useMenuContext';
+const InternalSubMenuList: FunctionalComponent = (_props, { slots, attrs }) => {
+ const { prefixCls, mode } = useInjectMenu();
+ return (
+
+ );
+};
+
+InternalSubMenuList.displayName = 'SubMenuList';
+
+export default InternalSubMenuList;
diff --git a/components/menu/src/hooks/useDirectionStyle.ts b/components/menu/src/hooks/useDirectionStyle.ts
new file mode 100644
index 0000000000..9721fc16d0
--- /dev/null
+++ b/components/menu/src/hooks/useDirectionStyle.ts
@@ -0,0 +1,14 @@
+import { computed, ComputedRef, CSSProperties } from 'vue';
+import { useInjectMenu } from './useMenuContext';
+
+export default function useDirectionStyle(level: ComputedRef): ComputedRef {
+ const { mode, rtl, inlineIndent } = useInjectMenu();
+
+ return computed(() =>
+ mode.value !== 'inline'
+ ? null
+ : rtl.value
+ ? { paddingRight: level.value * inlineIndent.value }
+ : { paddingLeft: level.value * inlineIndent.value },
+ );
+}
diff --git a/components/menu/src/hooks/useMenuContext.ts b/components/menu/src/hooks/useMenuContext.ts
index 76c9596311..060d70e598 100644
--- a/components/menu/src/hooks/useMenuContext.ts
+++ b/components/menu/src/hooks/useMenuContext.ts
@@ -1,22 +1,22 @@
import { Key } from '../../../_util/type';
-import { ComputedRef, inject, InjectionKey, provide, Ref } from 'vue';
-
-// import {
-// BuiltinPlacements,
-// MenuClickEventHandler,
-// MenuMode,
-// RenderIconType,
-// TriggerSubMenuAction,
-// } from '../interface';
+import { ComputedRef, FunctionalComponent, inject, InjectionKey, provide, Ref } from 'vue';
+import { BuiltinPlacements, MenuMode, MenuTheme, TriggerSubMenuAction } from '../interface';
export interface MenuContextProps {
prefixCls: ComputedRef;
openKeys: Ref;
selectedKeys: Ref;
- // rtl?: boolean;
+ rtl?: ComputedRef;
+
+ locked?: Ref;
+
+ inlineCollapsed: Ref;
+ antdMenuTheme?: ComputedRef;
+
+ siderCollapsed?: ComputedRef;
// // Mode
- // mode: MenuMode;
+ mode: Ref;
// // Disabled
disabled?: ComputedRef;
@@ -33,18 +33,18 @@ export interface MenuContextProps {
// selectedKeys: string[];
// // Level
- // inlineIndent: number;
+ inlineIndent: ComputedRef;
// // Motion
- // // motion?: CSSMotionProps;
- // // defaultMotions?: Partial<{ [key in MenuMode | 'other']: CSSMotionProps }>;
+ motion?: any;
+ defaultMotions?: Partial<{ [key in MenuMode | 'other']: any }>;
// // Popup
- // subMenuOpenDelay: number;
- // subMenuCloseDelay: number;
+ subMenuOpenDelay: ComputedRef;
+ subMenuCloseDelay: ComputedRef;
// forceSubMenuRender?: boolean;
- // builtinPlacements?: BuiltinPlacements;
- // triggerSubMenuAction?: TriggerSubMenuAction;
+ builtinPlacements?: ComputedRef;
+ triggerSubMenuAction?: ComputedRef;
// // Icon
// itemIcon?: RenderIconType;
@@ -53,7 +53,7 @@ export interface MenuContextProps {
// // Function
// onItemClick: MenuClickEventHandler;
// onOpenChange: (key: string, open: boolean) => void;
- // getPopupContainer: (node: HTMLElement) => HTMLElement;
+ getPopupContainer: ComputedRef<(node: HTMLElement) => HTMLElement>;
}
const MenuContextKey: InjectionKey = Symbol('menuContextKey');
@@ -66,6 +66,34 @@ const useInjectMenu = () => {
return inject(MenuContextKey);
};
-export { useProvideMenu, MenuContextKey, useInjectMenu };
+const MenuFirstLevelContextKey: InjectionKey = Symbol('menuFirstLevelContextKey');
+const useProvideFirstLevel = (firstLevel: Boolean) => {
+ provide(MenuFirstLevelContextKey, firstLevel);
+};
+
+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';
+
+export {
+ useProvideMenu,
+ MenuContextKey,
+ useInjectMenu,
+ MenuFirstLevelContextKey,
+ useProvideFirstLevel,
+ useInjectFirstLevel,
+ MenuContextProvider,
+};
export default useProvideMenu;
diff --git a/components/vc-trigger/Trigger.jsx b/components/vc-trigger/Trigger.jsx
index d97f417aa4..1fcfbedd21 100644
--- a/components/vc-trigger/Trigger.jsx
+++ b/components/vc-trigger/Trigger.jsx
@@ -47,7 +47,7 @@ export default defineComponent({
showAction: PropTypes.any.def([]),
hideAction: PropTypes.any.def([]),
getPopupClassNameFromAlign: PropTypes.any.def(returnEmptyString),
- // onPopupVisibleChange: PropTypes.func.def(noop),
+ onPopupVisibleChange: PropTypes.func.def(noop),
afterPopupVisibleChange: PropTypes.func.def(noop),
popup: PropTypes.any,
popupStyle: PropTypes.object.def(() => ({})),
@@ -443,7 +443,7 @@ export default defineComponent({
},
setPopupVisible(sPopupVisible, event) {
- const { alignPoint, sPopupVisible: prevPopupVisible, $attrs } = this;
+ const { alignPoint, sPopupVisible: prevPopupVisible, onPopupVisibleChange } = this;
this.clearDelayTimer();
if (prevPopupVisible !== sPopupVisible) {
if (!hasProp(this, 'popupVisible')) {
@@ -452,7 +452,7 @@ export default defineComponent({
prevPopupVisible,
});
}
- $attrs.onPopupVisibleChange && $attrs.onPopupVisibleChange(sPopupVisible);
+ onPopupVisibleChange && onPopupVisibleChange(sPopupVisible);
}
// Always record the point position since mouseEnterDelay will delay the show
if (alignPoint && event) {
diff --git a/components/vc-util/devWarning.ts b/components/vc-util/devWarning.ts
new file mode 100644
index 0000000000..17f72748b5
--- /dev/null
+++ b/components/vc-util/devWarning.ts
@@ -0,0 +1,7 @@
+import devWarning, { resetWarned } from './warning';
+
+export { resetWarned };
+
+export default (valid: boolean, component: string, message: string): void => {
+ devWarning(valid, `[ant-design-vue: ${component}] ${message}`);
+};
diff --git a/examples/App.vue b/examples/App.vue
index 3caf71cb9c..cd9ce1dfc6 100644
--- a/examples/App.vue
+++ b/examples/App.vue
@@ -15,14 +15,11 @@
Navigation One
-
-
-
- Item 1
-
- 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 ;
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 = (
+ (
+
+
+
+ ),
+ }}
+ >
+ {titleNode}
+
+ );
+ }
return (
-
+
+
+ {titleNode}
+
+ {/* Inline mode */}
+ {!overflowDisabled.value && (
+
+ )}
+
+
);
};
},
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 ;
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 && (
-