refactor: menu

feat-new-menu
tanjinzhou 2021-05-14 16:46:43 +08:00
parent ab4478c9f4
commit 994df6ff6f
29 changed files with 1039 additions and 763 deletions

View File

@ -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<ExtractPropTypes<typeof menuProps>>;
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<SiderContextProps>('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) {
// openKeysreactopenKeysvue便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 <Menu style={{ width: '100%' }} />, 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 <VcMenu {...menuProps} class={menuClassName} v-slots={this.$slots} />;
},
});
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;

View File

@ -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 (
<li onClick={e => e.stopPropagation()} class={groupPrefixCls.value}>
<div
title={typeof props.title === 'string' ? props.title : undefined}
class={`${groupPrefixCls.value}-title`}
>
{getPropsSlot(slots, props, 'title')}
</div>
<ul class={`${groupPrefixCls.value}-list`}>{slots.default?.()}</ul>
</li>
);
};
},
});

View File

@ -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<ExtractPropTypes<typeof menuProps>>;
export default defineComponent({
name: 'AMenu',
props: menuProps,
setup(props, { slots }) {
const prefixCls = usePrefixCls('menu', props);
useProvideMenu({ prefixCls });
return () => {
return <div>{slots.default?.()}</div>;
};
},
});

View File

@ -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 <li>{slots.default?.()}</li>;
};
},
});

View File

@ -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<string>;
// 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<MenuContextProps> = Symbol('menuContextKey');
const useProvideMenu = (props: MenuContextProps) => {
provide(MenuContextKey, props);
};
const useInjectMenu = () => {
return inject(MenuContextKey, {
prefixCls: computed(() => 'ant'),
});
};
export { useProvideMenu, MenuContextKey, useInjectMenu };
export default useProvideMenu;

View File

@ -0,0 +1,39 @@
// ========================== Basic ==========================
export type MenuMode = 'horizontal' | 'vertical' | 'inline';
export type BuiltinPlacements = Record<string, any>;
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;

View File

@ -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;

View File

@ -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;
}

View File

@ -1,7 +1,15 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@import './status';
@menu-prefix-cls: ~'@{ant-prefix}-menu';
@menu-animation-duration-normal: 0.15s;
.accessibility-focus() {
box-shadow: 0 0 0 2px fade(@primary-color, 20%);
}
// TODO: Should remove icon style compatible in v5
// default theme
.@{menu-prefix-cls} {
@ -10,14 +18,21 @@
margin-bottom: 0;
padding-left: 0; // Override default ul/ol
color: @menu-item-color;
font-size: @menu-item-font-size;
line-height: 0; // Fix display inline-block gap
text-align: left;
list-style: none;
background: @menu-bg;
outline: none;
box-shadow: @box-shadow-base;
transition: background 0.3s, width 0.3s cubic-bezier(0.2, 0, 0, 1) 0s;
transition: background @animation-duration-slow,
width @animation-duration-slow cubic-bezier(0.2, 0, 0, 1) 0s;
.clearfix();
&&-root:focus-visible {
.accessibility-focus();
}
ul,
ol {
margin: 0;
@ -25,22 +40,29 @@
list-style: none;
}
&-hidden {
&-hidden,
&-submenu-hidden {
display: none;
}
&-item-group-title {
height: @menu-item-group-height;
padding: 8px 16px;
color: @menu-item-group-title-color;
font-size: @font-size-base;
line-height: @line-height-base;
transition: all 0.3s;
font-size: @menu-item-group-title-font-size;
line-height: @menu-item-group-height;
transition: all @animation-duration-slow;
}
&-horizontal &-submenu {
transition: border-color @animation-duration-slow @ease-in-out,
background @animation-duration-slow @ease-in-out;
}
&-submenu,
&-submenu-inline {
transition: border-color 0.3s @ease-in-out, background 0.3s @ease-in-out,
padding 0.15s @ease-in-out;
transition: border-color @animation-duration-slow @ease-in-out,
background @animation-duration-slow @ease-in-out,
padding @menu-animation-duration-normal @ease-in-out;
}
&-submenu-selected {
@ -54,11 +76,11 @@
&-submenu &-sub {
cursor: initial;
transition: background 0.3s @ease-in-out, padding 0.3s @ease-in-out;
transition: background @animation-duration-slow @ease-in-out,
padding @animation-duration-slow @ease-in-out;
}
&-item > a {
display: block;
&-item a {
color: @menu-item-color;
&:hover {
color: @menu-highlight-color;
@ -75,7 +97,7 @@
}
// https://github.com/ant-design/ant-design/issues/19809
&-item > .@{ant-prefix}-badge > a {
&-item > .@{ant-prefix}-badge a {
color: @menu-item-color;
&:hover {
color: @menu-highlight-color;
@ -110,8 +132,8 @@
&-item-selected {
color: @menu-highlight-color;
> a,
> a:hover {
a,
a:hover {
color: @menu-highlight-color;
}
}
@ -125,6 +147,7 @@
&-vertical-left {
border-right: @border-width-base @border-style-base @border-color-split;
}
&-vertical-right {
border-left: @border-width-base @border-style-base @border-color-split;
}
@ -133,9 +156,17 @@
&-vertical-left&-sub,
&-vertical-right&-sub {
min-width: 160px;
max-height: calc(100vh - 100px);
padding: 0;
overflow: hidden;
border-right: 0;
transform-origin: 0 0;
// https://github.com/ant-design/ant-design/issues/22244
// https://github.com/ant-design/ant-design/issues/26812
&:not([class*='-active']) {
overflow-x: hidden;
overflow-y: auto;
}
.@{menu-prefix-cls}-item {
left: 0;
@ -155,26 +186,48 @@
min-width: 114px; // in case of submenu width is too big: https://codesandbox.io/s/qvpwm6mk66
}
&-horizontal &-item,
&-horizontal &-submenu-title {
transition: border-color @animation-duration-slow, background @animation-duration-slow;
}
&-item,
&-submenu-title {
position: relative;
display: block;
margin: 0;
padding: 0 20px;
padding: @menu-item-padding;
white-space: nowrap;
cursor: pointer;
transition: color 0.3s @ease-in-out, border-color 0.3s @ease-in-out,
background 0.3s @ease-in-out, padding 0.15s @ease-in-out;
transition: border-color @animation-duration-slow, background @animation-duration-slow,
padding @animation-duration-slow @ease-in-out;
.@{menu-prefix-cls}-item-icon,
.@{iconfont-css-prefix} {
min-width: 14px;
margin-right: 10px;
font-size: @menu-icon-size;
transition: font-size 0.15s @ease-out, margin 0.3s @ease-in-out;
transition: font-size @menu-animation-duration-normal @ease-out,
margin @animation-duration-slow @ease-in-out, color @animation-duration-slow;
+ span {
margin-left: @menu-icon-margin-right;
opacity: 1;
transition: opacity 0.3s @ease-in-out, width 0.3s @ease-in-out;
// transition: opacity @animation-duration-slow @ease-in-out,
// width @animation-duration-slow @ease-in-out, color @animation-duration-slow;
transition: opacity @animation-duration-slow @ease-in-out, margin @animation-duration-slow,
color @animation-duration-slow;
}
}
&.@{menu-prefix-cls}-item-only-child {
> .@{iconfont-css-prefix},
> .@{menu-prefix-cls}-item-icon {
margin-right: 0;
}
}
&:focus-visible {
.accessibility-focus();
}
}
& > &-item-divider {
@ -190,94 +243,105 @@
&-popup {
position: absolute;
z-index: @zindex-dropdown;
// background: @menu-popup-bg;
background: transparent;
border-radius: @border-radius-base;
box-shadow: none;
transform-origin: 0 0;
.submenu-title-wrapper {
padding-right: 20px;
}
// https://github.com/ant-design/ant-design/issues/13955
&::before {
position: absolute;
top: -7px;
right: 0;
bottom: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100%;
opacity: 0.0001;
content: ' ';
}
}
// https://github.com/ant-design/ant-design/issues/13955
&-placement-rightTop::before {
top: 0;
left: -7px;
}
> .@{menu-prefix-cls} {
background-color: @menu-bg;
border-radius: @border-radius-base;
&-submenu-title::after {
transition: transform 0.3s @ease-in-out;
transition: transform @animation-duration-slow @ease-in-out;
}
}
&-vertical,
&-vertical-left,
&-vertical-right,
&-inline {
> .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow {
&-popup > .@{menu-prefix-cls} {
background-color: @menu-popup-bg;
}
&-expand-icon,
&-arrow {
position: absolute;
top: 50%;
right: 16px;
width: 10px;
color: @menu-item-color;
transform: translateY(-50%);
transition: transform @animation-duration-slow @ease-in-out;
}
&-arrow {
// →
&::before,
&::after {
position: absolute;
top: 50%;
right: 16px;
width: 10px;
transition: transform 0.3s @ease-in-out;
&::before,
&::after {
position: absolute;
width: 6px;
height: 1.5px;
// background + background-image to makes before & after cross have same color.
// Since `linear-gradient` not work on IE9, we should hack it.
// ref: https://github.com/ant-design/ant-design/issues/15910
background: @menu-bg;
background: ~'@{menu-item-color} \9';
background-image: linear-gradient(to right, @menu-item-color, @menu-item-color);
background-image: ~'none \9';
border-radius: 2px;
transition: background 0.3s @ease-in-out, transform 0.3s @ease-in-out,
top 0.3s @ease-in-out;
content: '';
}
&::before {
transform: rotate(45deg) translateY(-2px);
}
&::after {
transform: rotate(-45deg) translateY(2px);
}
width: 6px;
height: 1.5px;
background-color: currentColor;
border-radius: 2px;
transition: background @animation-duration-slow @ease-in-out,
transform @animation-duration-slow @ease-in-out, top @animation-duration-slow @ease-in-out,
color @animation-duration-slow @ease-in-out;
content: '';
}
> .@{menu-prefix-cls}-submenu-title:hover .@{menu-prefix-cls}-submenu-arrow {
&::after,
&::before {
background: linear-gradient(to right, @menu-highlight-color, @menu-highlight-color);
}
}
}
&-inline > .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow {
&::before {
transform: rotate(-45deg) translateX(2px);
transform: rotate(45deg) translateY(-2.5px);
}
&::after {
transform: rotate(45deg) translateX(-2px);
transform: rotate(-45deg) translateY(2.5px);
}
}
&-open {
&.@{menu-prefix-cls}-submenu-inline
> .@{menu-prefix-cls}-submenu-title
.@{menu-prefix-cls}-submenu-arrow {
transform: translateY(-2px);
&::after {
transform: rotate(-45deg) translateX(-2px);
}
&::before {
transform: rotate(45deg) translateX(2px);
}
&:hover > &-title > &-expand-icon,
&:hover > &-title > &-arrow {
color: @menu-highlight-color;
}
.@{menu-prefix-cls}-inline-collapsed &-arrow,
&-inline &-arrow {
// ↓
&::before {
transform: rotate(-45deg) translateX(2.5px);
}
&::after {
transform: rotate(45deg) translateX(-2.5px);
}
}
&-horizontal &-arrow {
display: none;
}
&-open&-inline > &-title > &-arrow {
// ↑
transform: translateY(-2px);
&::after {
transform: rotate(-45deg) translateX(-2.5px);
}
&::before {
transform: rotate(45deg) translateX(2.5px);
}
}
}
@ -286,18 +350,34 @@
&-vertical-left &-submenu-selected,
&-vertical-right &-submenu-selected {
color: @menu-highlight-color;
> a {
color: @menu-highlight-color;
}
}
&-horizontal {
line-height: 46px;
white-space: nowrap;
line-height: @menu-horizontal-line-height;
border: 0;
border-bottom: @border-width-base @border-style-base @border-color-split;
box-shadow: none;
&:not(.@{menu-prefix-cls}-dark) {
> .@{menu-prefix-cls}-item,
> .@{menu-prefix-cls}-submenu {
margin: @menu-item-padding;
margin-top: -1px;
margin-bottom: 0;
padding: @menu-item-padding;
padding-right: 0;
padding-left: 0;
&:hover,
&-active,
&-open,
&-selected {
color: @menu-highlight-color;
border-bottom: 2px solid @menu-highlight-color;
}
}
}
> .@{menu-prefix-cls}-item,
> .@{menu-prefix-cls}-submenu {
position: relative;
@ -305,19 +385,14 @@
display: inline-block;
vertical-align: bottom;
border-bottom: 2px solid transparent;
}
&:hover,
&-active,
&-open,
&-selected {
color: @menu-highlight-color;
border-bottom: 2px solid @menu-highlight-color;
}
> .@{menu-prefix-cls}-submenu > .@{menu-prefix-cls}-submenu-title {
padding: 0;
}
> .@{menu-prefix-cls}-item {
> a {
display: block;
a {
color: @menu-item-color;
&:hover {
color: @menu-highlight-color;
@ -326,7 +401,7 @@
bottom: -2px;
}
}
&-selected > a {
&-selected a {
color: @menu-highlight-color;
}
}
@ -353,7 +428,8 @@
border-right: @menu-item-active-border-width solid @menu-highlight-color;
transform: scaleY(0.0001);
opacity: 0;
transition: transform 0.15s @ease-out, opacity 0.15s @ease-out;
transition: transform @menu-animation-duration-normal @ease-out,
opacity @menu-animation-duration-normal @ease-out;
content: '';
}
}
@ -365,7 +441,6 @@
margin-bottom: @menu-item-vertical-margin;
padding: 0 16px;
overflow: hidden;
font-size: @menu-item-font-size;
line-height: @menu-item-height;
text-overflow: ellipsis;
}
@ -386,6 +461,13 @@
}
}
&-vertical {
.@{menu-prefix-cls}-item-group-list .@{menu-prefix-cls}-submenu-title,
.@{menu-prefix-cls}-submenu-title {
padding-right: 34px;
}
}
&-inline {
width: 100%;
.@{menu-prefix-cls}-selected,
@ -393,7 +475,8 @@
&::after {
transform: scaleY(1);
opacity: 1;
transition: transform 0.15s @ease-in-out, opacity 0.15s @ease-in-out;
transition: transform @menu-animation-duration-normal @ease-in-out,
opacity @menu-animation-duration-normal @ease-in-out;
}
}
@ -402,13 +485,37 @@
width: ~'calc(100% + 1px)';
}
.@{menu-prefix-cls}-item-group-list .@{menu-prefix-cls}-submenu-title,
.@{menu-prefix-cls}-submenu-title {
padding-right: 34px;
}
// Motion enhance for first level
&.@{menu-prefix-cls}-root {
.@{menu-prefix-cls}-item,
.@{menu-prefix-cls}-submenu-title {
display: flex;
align-items: center;
transition: border-color @animation-duration-slow, background @animation-duration-slow,
padding 0.1s @ease-out;
> .@{menu-prefix-cls}-title-content {
flex: auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
> * {
flex: none;
}
}
}
}
&-inline-collapsed {
&&-inline-collapsed {
width: @menu-collapsed-width;
> .@{menu-prefix-cls}-item,
> .@{menu-prefix-cls}-item-group
> .@{menu-prefix-cls}-item-group-list
@ -419,24 +526,34 @@
> .@{menu-prefix-cls}-submenu-title,
> .@{menu-prefix-cls}-submenu > .@{menu-prefix-cls}-submenu-title {
left: 0;
padding: 0 ((@menu-collapsed-width - @menu-icon-size-lg) / 2) !important;
padding: 0 ~'calc(50% - @{menu-icon-size-lg} / 2)';
text-overflow: clip;
.@{menu-prefix-cls}-submenu-arrow {
display: none;
opacity: 0;
}
.@{menu-prefix-cls}-item-icon,
.@{iconfont-css-prefix} {
margin: 0;
font-size: @menu-icon-size-lg;
line-height: @menu-item-height;
+ span {
display: inline-block;
max-width: 0;
opacity: 0;
}
}
}
.@{menu-prefix-cls}-item-icon,
.@{iconfont-css-prefix} {
display: inline-block;
}
&-tooltip {
pointer-events: none;
.@{menu-prefix-cls}-item-icon,
.@{iconfont-css-prefix} {
display: none;
}
@ -470,8 +587,19 @@
box-shadow: none;
}
&-root&-inline-collapsed {
.@{menu-prefix-cls}-item,
.@{menu-prefix-cls}-submenu .@{menu-prefix-cls}-submenu-title {
> .@{menu-prefix-cls}-inline-collapsed-noicon {
font-size: @menu-icon-size-lg;
text-align: center;
}
}
}
&-sub&-inline {
padding: 0;
background: @menu-inline-submenu-bg;
border: 0;
border-radius: 0;
box-shadow: none;
@ -495,7 +623,7 @@
background: none;
border-color: transparent !important;
cursor: not-allowed;
> a {
a {
color: @disabled-color !important;
pointer-events: none;
}
@ -512,4 +640,12 @@
}
}
// Integration with header element so menu items have the same height
.@{ant-prefix}-layout-header {
.@{menu-prefix-cls} {
line-height: inherit;
}
}
@import './dark';
@import './rtl';

View File

@ -1,16 +0,0 @@
import { getPropsSlot } from '../../_util/props-util';
import { defineComponent } from 'vue';
export default defineComponent({
name: 'AMenuItemGroup',
setup(props, { slots }) {
return () => {
return (
<li>
{getPropsSlot(slots, props, 'title')}
<ul>{slots.default?.()}</ul>
</li>
);
};
},
});

View File

@ -1,10 +0,0 @@
import { defineComponent } from 'vue';
export default defineComponent({
name: 'AMenu',
setup(props, { slots }) {
return () => {
return <div>{slots.default?.()}</div>;
};
},
});

View File

@ -1,10 +0,0 @@
import { defineComponent } from 'vue';
export default defineComponent({
name: 'AMenuItem',
setup(props, { slots }) {
return () => {
return <li>{slots.default?.()}</li>;
};
},
});

View File

@ -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<ExtractPropTypes<typeof menuProps>>;
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<SiderContextProps>('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) {
// openKeysreactopenKeysvue便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 <Menu style={{ width: '100%' }} />, 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 <VcMenu {...menuProps} class={menuClassName} v-slots={this.$slots} />;
},
});
/* 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;
};

View File

@ -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;
}

View File

@ -1,15 +1,7 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@import './status';
@menu-prefix-cls: ~'@{ant-prefix}-menu';
@menu-animation-duration-normal: 0.15s;
.accessibility-focus() {
box-shadow: 0 0 0 2px fade(@primary-color, 20%);
}
// TODO: Should remove icon style compatible in v5
// default theme
.@{menu-prefix-cls} {
@ -18,21 +10,14 @@
margin-bottom: 0;
padding-left: 0; // Override default ul/ol
color: @menu-item-color;
font-size: @menu-item-font-size;
line-height: 0; // Fix display inline-block gap
text-align: left;
list-style: none;
background: @menu-bg;
outline: none;
box-shadow: @box-shadow-base;
transition: background @animation-duration-slow,
width @animation-duration-slow cubic-bezier(0.2, 0, 0, 1) 0s;
transition: background 0.3s, width 0.3s cubic-bezier(0.2, 0, 0, 1) 0s;
.clearfix();
&&-root:focus-visible {
.accessibility-focus();
}
ul,
ol {
margin: 0;
@ -40,29 +25,22 @@
list-style: none;
}
&-hidden,
&-submenu-hidden {
&-hidden {
display: none;
}
&-item-group-title {
height: @menu-item-group-height;
padding: 8px 16px;
color: @menu-item-group-title-color;
font-size: @menu-item-group-title-font-size;
line-height: @menu-item-group-height;
transition: all @animation-duration-slow;
font-size: @font-size-base;
line-height: @line-height-base;
transition: all 0.3s;
}
&-horizontal &-submenu {
transition: border-color @animation-duration-slow @ease-in-out,
background @animation-duration-slow @ease-in-out;
}
&-submenu,
&-submenu-inline {
transition: border-color @animation-duration-slow @ease-in-out,
background @animation-duration-slow @ease-in-out,
padding @menu-animation-duration-normal @ease-in-out;
transition: border-color 0.3s @ease-in-out, background 0.3s @ease-in-out,
padding 0.15s @ease-in-out;
}
&-submenu-selected {
@ -76,11 +54,11 @@
&-submenu &-sub {
cursor: initial;
transition: background @animation-duration-slow @ease-in-out,
padding @animation-duration-slow @ease-in-out;
transition: background 0.3s @ease-in-out, padding 0.3s @ease-in-out;
}
&-item a {
&-item > a {
display: block;
color: @menu-item-color;
&:hover {
color: @menu-highlight-color;
@ -97,7 +75,7 @@
}
// https://github.com/ant-design/ant-design/issues/19809
&-item > .@{ant-prefix}-badge a {
&-item > .@{ant-prefix}-badge > a {
color: @menu-item-color;
&:hover {
color: @menu-highlight-color;
@ -132,8 +110,8 @@
&-item-selected {
color: @menu-highlight-color;
a,
a:hover {
> a,
> a:hover {
color: @menu-highlight-color;
}
}
@ -147,7 +125,6 @@
&-vertical-left {
border-right: @border-width-base @border-style-base @border-color-split;
}
&-vertical-right {
border-left: @border-width-base @border-style-base @border-color-split;
}
@ -156,17 +133,9 @@
&-vertical-left&-sub,
&-vertical-right&-sub {
min-width: 160px;
max-height: calc(100vh - 100px);
padding: 0;
overflow: hidden;
border-right: 0;
// https://github.com/ant-design/ant-design/issues/22244
// https://github.com/ant-design/ant-design/issues/26812
&:not([class*='-active']) {
overflow-x: hidden;
overflow-y: auto;
}
transform-origin: 0 0;
.@{menu-prefix-cls}-item {
left: 0;
@ -186,48 +155,26 @@
min-width: 114px; // in case of submenu width is too big: https://codesandbox.io/s/qvpwm6mk66
}
&-horizontal &-item,
&-horizontal &-submenu-title {
transition: border-color @animation-duration-slow, background @animation-duration-slow;
}
&-item,
&-submenu-title {
position: relative;
display: block;
margin: 0;
padding: @menu-item-padding;
padding: 0 20px;
white-space: nowrap;
cursor: pointer;
transition: border-color @animation-duration-slow, background @animation-duration-slow,
padding @animation-duration-slow @ease-in-out;
.@{menu-prefix-cls}-item-icon,
transition: color 0.3s @ease-in-out, border-color 0.3s @ease-in-out,
background 0.3s @ease-in-out, padding 0.15s @ease-in-out;
.@{iconfont-css-prefix} {
min-width: 14px;
margin-right: 10px;
font-size: @menu-icon-size;
transition: font-size @menu-animation-duration-normal @ease-out,
margin @animation-duration-slow @ease-in-out, color @animation-duration-slow;
transition: font-size 0.15s @ease-out, margin 0.3s @ease-in-out;
+ span {
margin-left: @menu-icon-margin-right;
opacity: 1;
// transition: opacity @animation-duration-slow @ease-in-out,
// width @animation-duration-slow @ease-in-out, color @animation-duration-slow;
transition: opacity @animation-duration-slow @ease-in-out, margin @animation-duration-slow,
color @animation-duration-slow;
transition: opacity 0.3s @ease-in-out, width 0.3s @ease-in-out;
}
}
&.@{menu-prefix-cls}-item-only-child {
> .@{iconfont-css-prefix},
> .@{menu-prefix-cls}-item-icon {
margin-right: 0;
}
}
&:focus-visible {
.accessibility-focus();
}
}
& > &-item-divider {
@ -243,105 +190,94 @@
&-popup {
position: absolute;
z-index: @zindex-dropdown;
background: transparent;
// background: @menu-popup-bg;
border-radius: @border-radius-base;
box-shadow: none;
transform-origin: 0 0;
// https://github.com/ant-design/ant-design/issues/13955
.submenu-title-wrapper {
padding-right: 20px;
}
&::before {
position: absolute;
top: -7px;
right: 0;
bottom: 0;
left: 0;
z-index: -1;
width: 100%;
height: 100%;
opacity: 0.0001;
content: ' ';
}
}
// https://github.com/ant-design/ant-design/issues/13955
&-placement-rightTop::before {
top: 0;
left: -7px;
}
> .@{menu-prefix-cls} {
background-color: @menu-bg;
border-radius: @border-radius-base;
&-submenu-title::after {
transition: transform @animation-duration-slow @ease-in-out;
transition: transform 0.3s @ease-in-out;
}
}
&-popup > .@{menu-prefix-cls} {
background-color: @menu-popup-bg;
}
&-expand-icon,
&-arrow {
position: absolute;
top: 50%;
right: 16px;
width: 10px;
color: @menu-item-color;
transform: translateY(-50%);
transition: transform @animation-duration-slow @ease-in-out;
}
&-arrow {
// →
&::before,
&::after {
&-vertical,
&-vertical-left,
&-vertical-right,
&-inline {
> .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow {
position: absolute;
width: 6px;
height: 1.5px;
background-color: currentColor;
border-radius: 2px;
transition: background @animation-duration-slow @ease-in-out,
transform @animation-duration-slow @ease-in-out, top @animation-duration-slow @ease-in-out,
color @animation-duration-slow @ease-in-out;
content: '';
top: 50%;
right: 16px;
width: 10px;
transition: transform 0.3s @ease-in-out;
&::before,
&::after {
position: absolute;
width: 6px;
height: 1.5px;
// background + background-image to makes before & after cross have same color.
// Since `linear-gradient` not work on IE9, we should hack it.
// ref: https://github.com/ant-design/ant-design/issues/15910
background: @menu-bg;
background: ~'@{menu-item-color} \9';
background-image: linear-gradient(to right, @menu-item-color, @menu-item-color);
background-image: ~'none \9';
border-radius: 2px;
transition: background 0.3s @ease-in-out, transform 0.3s @ease-in-out,
top 0.3s @ease-in-out;
content: '';
}
&::before {
transform: rotate(45deg) translateY(-2px);
}
&::after {
transform: rotate(-45deg) translateY(2px);
}
}
> .@{menu-prefix-cls}-submenu-title:hover .@{menu-prefix-cls}-submenu-arrow {
&::after,
&::before {
background: linear-gradient(to right, @menu-highlight-color, @menu-highlight-color);
}
}
}
&-inline > .@{menu-prefix-cls}-submenu-title .@{menu-prefix-cls}-submenu-arrow {
&::before {
transform: rotate(45deg) translateY(-2.5px);
transform: rotate(-45deg) translateX(2px);
}
&::after {
transform: rotate(-45deg) translateY(2.5px);
transform: rotate(45deg) translateX(-2px);
}
}
&:hover > &-title > &-expand-icon,
&:hover > &-title > &-arrow {
color: @menu-highlight-color;
}
.@{menu-prefix-cls}-inline-collapsed &-arrow,
&-inline &-arrow {
// ↓
&::before {
transform: rotate(-45deg) translateX(2.5px);
}
&::after {
transform: rotate(45deg) translateX(-2.5px);
}
}
&-horizontal &-arrow {
display: none;
}
&-open&-inline > &-title > &-arrow {
// ↑
transform: translateY(-2px);
&::after {
transform: rotate(-45deg) translateX(-2.5px);
}
&::before {
transform: rotate(45deg) translateX(2.5px);
&-open {
&.@{menu-prefix-cls}-submenu-inline
> .@{menu-prefix-cls}-submenu-title
.@{menu-prefix-cls}-submenu-arrow {
transform: translateY(-2px);
&::after {
transform: rotate(-45deg) translateX(-2px);
}
&::before {
transform: rotate(45deg) translateX(2px);
}
}
}
}
@ -350,34 +286,18 @@
&-vertical-left &-submenu-selected,
&-vertical-right &-submenu-selected {
color: @menu-highlight-color;
> a {
color: @menu-highlight-color;
}
}
&-horizontal {
line-height: @menu-horizontal-line-height;
line-height: 46px;
white-space: nowrap;
border: 0;
border-bottom: @border-width-base @border-style-base @border-color-split;
box-shadow: none;
&:not(.@{menu-prefix-cls}-dark) {
> .@{menu-prefix-cls}-item,
> .@{menu-prefix-cls}-submenu {
margin: @menu-item-padding;
margin-top: -1px;
margin-bottom: 0;
padding: @menu-item-padding;
padding-right: 0;
padding-left: 0;
&:hover,
&-active,
&-open,
&-selected {
color: @menu-highlight-color;
border-bottom: 2px solid @menu-highlight-color;
}
}
}
> .@{menu-prefix-cls}-item,
> .@{menu-prefix-cls}-submenu {
position: relative;
@ -385,14 +305,19 @@
display: inline-block;
vertical-align: bottom;
border-bottom: 2px solid transparent;
}
> .@{menu-prefix-cls}-submenu > .@{menu-prefix-cls}-submenu-title {
padding: 0;
&:hover,
&-active,
&-open,
&-selected {
color: @menu-highlight-color;
border-bottom: 2px solid @menu-highlight-color;
}
}
> .@{menu-prefix-cls}-item {
a {
> a {
display: block;
color: @menu-item-color;
&:hover {
color: @menu-highlight-color;
@ -401,7 +326,7 @@
bottom: -2px;
}
}
&-selected a {
&-selected > a {
color: @menu-highlight-color;
}
}
@ -428,8 +353,7 @@
border-right: @menu-item-active-border-width solid @menu-highlight-color;
transform: scaleY(0.0001);
opacity: 0;
transition: transform @menu-animation-duration-normal @ease-out,
opacity @menu-animation-duration-normal @ease-out;
transition: transform 0.15s @ease-out, opacity 0.15s @ease-out;
content: '';
}
}
@ -441,6 +365,7 @@
margin-bottom: @menu-item-vertical-margin;
padding: 0 16px;
overflow: hidden;
font-size: @menu-item-font-size;
line-height: @menu-item-height;
text-overflow: ellipsis;
}
@ -461,13 +386,6 @@
}
}
&-vertical {
.@{menu-prefix-cls}-item-group-list .@{menu-prefix-cls}-submenu-title,
.@{menu-prefix-cls}-submenu-title {
padding-right: 34px;
}
}
&-inline {
width: 100%;
.@{menu-prefix-cls}-selected,
@ -475,8 +393,7 @@
&::after {
transform: scaleY(1);
opacity: 1;
transition: transform @menu-animation-duration-normal @ease-in-out,
opacity @menu-animation-duration-normal @ease-in-out;
transition: transform 0.15s @ease-in-out, opacity 0.15s @ease-in-out;
}
}
@ -485,37 +402,13 @@
width: ~'calc(100% + 1px)';
}
.@{menu-prefix-cls}-item-group-list .@{menu-prefix-cls}-submenu-title,
.@{menu-prefix-cls}-submenu-title {
padding-right: 34px;
}
// Motion enhance for first level
&.@{menu-prefix-cls}-root {
.@{menu-prefix-cls}-item,
.@{menu-prefix-cls}-submenu-title {
display: flex;
align-items: center;
transition: border-color @animation-duration-slow, background @animation-duration-slow,
padding 0.1s @ease-out;
> .@{menu-prefix-cls}-title-content {
flex: auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
> * {
flex: none;
}
}
}
}
&&-inline-collapsed {
&-inline-collapsed {
width: @menu-collapsed-width;
> .@{menu-prefix-cls}-item,
> .@{menu-prefix-cls}-item-group
> .@{menu-prefix-cls}-item-group-list
@ -526,34 +419,24 @@
> .@{menu-prefix-cls}-submenu-title,
> .@{menu-prefix-cls}-submenu > .@{menu-prefix-cls}-submenu-title {
left: 0;
padding: 0 ~'calc(50% - @{menu-icon-size-lg} / 2)';
padding: 0 ((@menu-collapsed-width - @menu-icon-size-lg) / 2) !important;
text-overflow: clip;
.@{menu-prefix-cls}-submenu-arrow {
opacity: 0;
display: none;
}
.@{menu-prefix-cls}-item-icon,
.@{iconfont-css-prefix} {
margin: 0;
font-size: @menu-icon-size-lg;
line-height: @menu-item-height;
+ span {
display: inline-block;
max-width: 0;
opacity: 0;
}
}
}
.@{menu-prefix-cls}-item-icon,
.@{iconfont-css-prefix} {
display: inline-block;
}
&-tooltip {
pointer-events: none;
.@{menu-prefix-cls}-item-icon,
.@{iconfont-css-prefix} {
display: none;
}
@ -587,19 +470,8 @@
box-shadow: none;
}
&-root&-inline-collapsed {
.@{menu-prefix-cls}-item,
.@{menu-prefix-cls}-submenu .@{menu-prefix-cls}-submenu-title {
> .@{menu-prefix-cls}-inline-collapsed-noicon {
font-size: @menu-icon-size-lg;
text-align: center;
}
}
}
&-sub&-inline {
padding: 0;
background: @menu-inline-submenu-bg;
border: 0;
border-radius: 0;
box-shadow: none;
@ -623,7 +495,7 @@
background: none;
border-color: transparent !important;
cursor: not-allowed;
a {
> a {
color: @disabled-color !important;
pointer-events: none;
}
@ -640,12 +512,4 @@
}
}
// Integration with header element so menu items have the same height
.@{ant-prefix}-layout-header {
.@{menu-prefix-cls} {
line-height: inherit;
}
}
@import './dark';
@import './rtl';

View File

@ -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;

View File

@ -1,39 +1,92 @@
<template>
<div>
<demo />
</div>
<a-menu
id="dddddd"
v-model:openKeys="openKeys"
v-model:selectedKeys="selectedKeys"
style="width: 256px"
mode="inline"
@click="handleClick"
>
<a-sub-menu key="sub1" @titleClick="titleClick">
<template #title>
<span>
<MailOutlined />
<span>Navigation One</span>
</span>
</template>
<a-menu-item-group key="g1">
<template #title>
<QqOutlined />
<span>Item 1</span>
</template>
<a-menu-item key="1">Option 1</a-menu-item>
<a-menu-item key="2">Option 2</a-menu-item>
</a-menu-item-group>
<a-menu-item-group key="g2" title="Item 2">
<a-menu-item key="3">Option 3</a-menu-item>
<a-menu-item key="4">Option 4</a-menu-item>
</a-menu-item-group>
</a-sub-menu>
<!-- <a-sub-menu key="sub2" @titleClick="titleClick">
<template #title>
<span>
<AppstoreOutlined />
<span>Navigation Two</span>
</span>
</template>
<a-menu-item key="5">Option 5</a-menu-item>
<a-menu-item key="6">Option 6</a-menu-item>
<a-sub-menu key="sub3" title="Submenu">
<a-menu-item key="7">Option 7</a-menu-item>
<a-menu-item key="8">Option 8</a-menu-item>
</a-sub-menu>
</a-sub-menu>
<a-sub-menu key="sub4">
<template #title>
<span>
<SettingOutlined />
<span>Navigation Three</span>
</span>
</template>
<a-menu-item key="9">Option 9</a-menu-item>
<a-menu-item key="10">Option 10</a-menu-item>
<a-menu-item key="11">Option 11</a-menu-item>
<a-menu-item key="12">Option 12</a-menu-item>
</a-sub-menu> -->
</a-menu>
</template>
<script>
import { defineComponent } from 'vue';
import demo from '../v2-doc/src/docs/tooltip/demo/index.vue';
// import Affix from '../components/affix';
<script lang="ts">
import { defineComponent, ref, watch } from 'vue';
import { MailOutlined, QqOutlined, AppstoreOutlined, SettingOutlined } from '@ant-design/icons-vue';
export default defineComponent({
components: {
demo,
// Affix,
MailOutlined,
QqOutlined,
AppstoreOutlined,
SettingOutlined,
},
data() {
return {
visible: false,
pStyle: {
fontSize: '16px',
color: 'rgba(0,0,0,0.85)',
lineHeight: '24px',
display: 'block',
marginBottom: '16px',
},
pStyle2: {
marginBottom: '24px',
},
setup() {
const selectedKeys = ref<string[]>(['1']);
const openKeys = ref<string[]>(['sub1']);
const handleClick = (e: Event) => {
console.log('click', e);
};
const titleClick = (e: Event) => {
console.log('titleClick', e);
};
watch(
() => openKeys,
val => {
console.log('openKeys', val);
},
);
return {
selectedKeys,
openKeys,
handleClick,
titleClick,
};
},
methods: {
showDrawer() {
this.visible = true;
},
onClose() {
this.visible = false;
},
},
});
</script>