Skip to content

Commit bc01c59

Browse files
authored
fix(menu): 修复组件部分错误 (DevCloudFE#1334)
1 parent 17999c2 commit bc01c59

File tree

14 files changed

+346
-196
lines changed

14 files changed

+346
-196
lines changed

packages/devui-vue/devui/menu/__tests__/menu.spec.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Menu, SubMenu, MenuItem } from '../index';
44
import { useNamespace } from '../../shared/hooks/use-namespace';
55

66
const ns = useNamespace('menu');
7+
const SubNs = useNamespace('submenu');
78
const dotNs = useNamespace('menu', true);
89
const dotSubNs = useNamespace('submenu', true);
910

@@ -12,6 +13,9 @@ const menuHorizontal = ns.b() + '-horizontal';
1213
const dotMenuItem = dotNs.b() + '-item';
1314
const dotMenuItemVerticalWrapper = dotNs.b() + '-item-vertical-wrapper';
1415
const dotSubMenu = dotSubNs.b();
16+
const submenuDisabled = SubNs.b() + '-disabled';
17+
const menuitemDisabled = ns.b() + '-item-disabled';
18+
1519

1620
describe('menu test', () => {
1721
let wrapper: VueWrapper<ComponentPublicInstance>;
@@ -136,7 +140,6 @@ describe('menu test', () => {
136140
expect(wrapper.findAll('i')[0].classes().includes('is-opened')).toBe(true);
137141
expect(wrapper.findAll('i')[1].classes().includes('is-opened')).toBe(false);
138142
});
139-
140143
it.todo('props mode(vertical/horizontal) work well.');
141144

142145
it.todo('props multiple work well.');
@@ -148,4 +151,30 @@ describe('menu test', () => {
148151
it.todo('props router work well.');
149152

150153
it.todo('slot icon work well.');
154+
it('menu - disabled', async ()=>{
155+
wrapper = wrapper = mount({
156+
components: {
157+
'd-menu': Menu,
158+
'd-sub-menu': SubMenu,
159+
'd-menu-item': MenuItem,
160+
},
161+
template: `
162+
<d-menu>
163+
<d-menu-item key="home">首页</d-menu-item>
164+
<d-sub-menu title="课程" key="course" class="course" disabled>
165+
<d-menu-item key="c"> C </d-menu-item>
166+
<d-sub-menu title="Python" key="python">
167+
<d-menu-item key="basic"> 基础 </d-menu-item>
168+
<d-menu-item key="advanced"> 进阶 </d-menu-item>
169+
</d-sub-menu>
170+
</d-sub-menu>
171+
<d-menu-item key="person">个人</d-menu-item>
172+
<d-menu-item key="custom" href="https://www.baidu.com" disabled> Link To Baidu </d-menu-item>
173+
</d-menu>
174+
`,
175+
});
176+
await nextTick();
177+
expect(wrapper.findAll(dotMenuItem).at(-1)?.classes().includes(menuitemDisabled)).toBe(true);
178+
expect(wrapper.find('.course').classes().includes(submenuDisabled)).toBe(true);
179+
});
151180
});

packages/devui-vue/devui/menu/src/components/menu-item/menu-item.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ export default defineComponent({
3434
const rootMenuEmit = inject('rootMenuEmit') as (eventName: string, ...args: unknown[]) => void;
3535
const useRouter = inject('useRouter') as boolean;
3636
const router = instance?.appContext.config.globalProperties.$router as Router;
37-
3837
const classObject = computed(()=>({
3938
[`${ns.b()}-item`]: true,
4039
[`${ns.b()}-item-isCollapsed`]: isCollapsed.value,
@@ -48,6 +47,7 @@ export default defineComponent({
4847
e.stopPropagation();
4948
const ele = e.currentTarget as HTMLElement;
5049
let changeRouteResult = undefined;
50+
props.disabled && e.preventDefault();
5151
if (!props.disabled) {
5252
if (!multiple) {
5353
menuStore.emit('menuItem:clear:select');
@@ -85,7 +85,9 @@ export default defineComponent({
8585
const icons = <span class={`${ns.b()}-icon`}>{ctx.slots.icon?.()}</span>;
8686
const menuItems = ref(null);
8787
watch(disabled, () => {
88-
classObject.value[menuItemSelect] = false;
88+
if (!multiple){
89+
classObject.value[menuItemSelect] = false;
90+
}
8991
});
9092
watch(
9193
() => defaultSelectKey,

packages/devui-vue/devui/menu/src/components/menu-item/use-menu-item.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,15 @@ export function changeRoute(props: MenuItemProps, router: Router, useRouter: boo
4444
}
4545
return undefined;
4646
}
47+
48+
export function changeSelect(isMultiple: boolean, defaultSelectKeys: string[], key: string): string[]{
49+
if (isMultiple){
50+
if (!defaultSelectKeys.indexOf(key)){
51+
defaultSelectKeys.push(key);
52+
}
53+
} else{
54+
defaultSelectKeys = [];
55+
defaultSelectKeys.push(key);
56+
}
57+
return defaultSelectKeys;
58+
}

packages/devui-vue/devui/menu/src/components/sub-menu/sub-menu.tsx

Lines changed: 59 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
1-
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import { defineComponent, getCurrentInstance, inject, onMounted, ref, watchEffect, watch } from 'vue';
1+
import { randomId } from '../../../../shared/utils/random-id';
32
import type { ComponentInternalInstance, Ref } from 'vue';
4-
import { addLayer, pushElement, clearSelect, getLayer } from '../../composables/use-layer-operate';
3+
import {
4+
defineComponent,
5+
getCurrentInstance,
6+
inject,
7+
onMounted,
8+
ref,
9+
watch,
10+
watchEffect
11+
} from 'vue';
12+
import { useNamespace } from '../../../../shared/hooks/use-namespace';
513
import { useClick } from '../../composables/use-click';
6-
import { useShowSubMenu } from './use-sub-menu';
7-
import { SubMenuProps, subMenuProps } from './sub-menu-types';
14+
import { addLayer, clearSelect, getLayer, pushElement } from '../../composables/use-layer-operate';
15+
import { useNearestMenuElement } from '../../composables/use-nearest-menu-element';
816
import MenuTransition from '../menu-transition/menu-transition';
9-
import { useNamespace } from '../../../../shared/hooks/use-namespace';
17+
import { SubMenuProps, subMenuProps } from './sub-menu-types';
18+
import { useShowSubMenu } from './use-sub-menu';
1019

1120
const ns = useNamespace('menu');
1221
const subNs = useNamespace('submenu');
@@ -24,29 +33,23 @@ export default defineComponent({
2433
const {
2534
vnode: { key }
2635
} = getCurrentInstance() as ComponentInternalInstance;
27-
const key_ = String(key);
28-
const isOpen = ref(false);
36+
let key_ = String(key);
2937
const defaultOpenKeys = inject('openKeys') as Ref<string[]>;
38+
const isOpen = ref(defaultOpenKeys.value.includes(key_));
3039
const indent = inject('defaultIndent');
3140
const isCollapsed = inject('isCollapsed') as Ref<boolean>;
3241
const mode = inject('mode') as Ref<string>;
3342
const subMenuItemContainer = ref(null) as Ref<null>;
43+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3444
const parentEmit = inject('rootMenuEmit') as (eventName: 'submenu-change', ...args: any[]) => void;
3545
const isHorizontal = mode.value === 'horizontal';
3646
if (key_ === 'null') {
3747
console.warn(`[devui][menu]: Key can not be null`);
38-
} else {
39-
if (defaultOpenKeys.value.includes(key_)) {
40-
isOpen.value = true;
41-
} else {
42-
isOpen.value = false;
43-
}
48+
key_ = `randomKey-${randomId(16)}`;
4449
}
4550
const clickHandle = (e: MouseEvent) => {
46-
e.preventDefault();
4751
e.stopPropagation();
48-
const ele = e.currentTarget as HTMLElement;
49-
52+
const ele = useNearestMenuElement(e.target as HTMLElement);
5053
if (ele.classList.contains(subMenuClass) && isHorizontal) {
5154
return;
5255
}
@@ -55,26 +58,22 @@ export default defineComponent({
5558
useClick(e as clickEvent);
5659
}
5760
if (!props.disabled && mode.value !== 'horizontal') {
58-
const target = e.target as HTMLElement;
59-
let cur = e.target as HTMLElement;
60-
if (target.tagName === 'UL') {
61-
if (target.classList.contains(`${subMenuClass}-open`)) {
62-
isOpen.value = !isOpen.value;
63-
} else {
64-
isOpen.value = isOpen.value;
65-
}
61+
const cur = useNearestMenuElement(e.target as HTMLElement);
62+
const idx = defaultOpenKeys.value.indexOf(key_);
63+
if (idx >= 0 && cur.tagName === 'UL') {
64+
defaultOpenKeys.value.splice(idx, 1);
6665
} else {
67-
while (cur && cur.tagName !== 'UL') {
68-
if (cur.tagName === 'LI') {
69-
break;
70-
}
71-
cur = cur.parentElement as HTMLElement;
72-
}
73-
if (cur.tagName === 'UL') {
74-
isOpen.value = !isOpen.value;
66+
if (cur.tagName === 'UL'){
67+
defaultOpenKeys.value.push(key_);
7568
}
7669
}
77-
parentEmit('submenu-change', { type: 'submenu-change', state: isOpen.value, key: key_, el: cur });
70+
isOpen.value = defaultOpenKeys.value.indexOf(key_) >= 0;
71+
parentEmit('submenu-change', {
72+
type: 'submenu-change',
73+
state: isOpen.value,
74+
key: key_,
75+
el: ele
76+
});
7877
}
7978
};
8079
const wrapper = ref(null);
@@ -86,26 +85,27 @@ export default defineComponent({
8685
watchEffect(
8786
() => {
8887
wrapperDom = wrapper.value as unknown as HTMLElement;
88+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8989
pushElement({ el: subMenu.value } as any);
9090
},
9191
{ flush: 'post' }
9292
);
9393
watch(
9494
() => defaultOpenKeys,
9595
(n) => {
96-
if (n.value.includes(key_)) {
96+
if (n.value.includes(key_)){
9797
isOpen.value = true;
9898
} else {
9999
isOpen.value = false;
100100
}
101101
},{deep: true}
102102
);
103103
onMounted(() => {
104-
const el = title.value as unknown as HTMLElement;
105-
const e = subMenu.value as unknown as HTMLElement;
104+
const subMenuTitle = title.value as unknown as HTMLElement;
105+
const subMenuWrapper = subMenu.value as unknown as HTMLElement;
106106
addLayer();
107-
class_layer.value = `layer_${Array.from(e.classList).at(-1)?.replace('layer_', '')}`;
108-
if (isHorizontal) {
107+
class_layer.value = `layer_${Array.from(subMenuWrapper.classList).at(-1)?.replace('layer_', '')}`;
108+
if (isHorizontal && !props.disabled) {
109109
(subMenu.value as unknown as Element as HTMLElement).addEventListener('mouseenter', (ev: MouseEvent) => {
110110
ev.stopPropagation();
111111
useShowSubMenu('mouseenter', ev, wrapperDom);
@@ -116,30 +116,34 @@ export default defineComponent({
116116
});
117117
}
118118
watch(isCollapsed, (newValue) => {
119-
const layer = Number(getLayer(e));
119+
const layer = Number(getLayer(subMenuWrapper));
120120
if (!Number.isNaN(layer)) {
121121
layer > 2 && (isShow.value = !isCollapsed.value);
122122
}
123123
if (newValue) {
124-
el.style.padding !== '0' && (oldPadding = el.style.padding);
124+
subMenuTitle.style.padding !== '0' && (oldPadding = subMenuTitle.style.padding);
125125
setTimeout(() => {
126-
el.style.padding = '0';
127-
el.style.width = '';
128-
el.style.textAlign = `center`;
126+
subMenuTitle.style.padding = '0';
127+
subMenuTitle.style.width = '';
128+
subMenuTitle.style.textAlign = `center`;
129129
}, 300);
130-
el.style.display = `block`;
130+
subMenuTitle.style.display = `block`;
131131
} else {
132-
el.style.padding = `${oldPadding}`;
133-
el.style.textAlign = ``;
134-
el.style.display = `flex`;
132+
subMenuTitle.style.padding = `${oldPadding}`;
133+
subMenuTitle.style.textAlign = ``;
134+
subMenuTitle.style.display = `flex`;
135135
}
136136
});
137137
});
138138
return () => {
139139
return (
140-
<ul v-show={isShow.value} onClick={clickHandle} class={[subMenuClass, class_layer.value]} ref={subMenu}>
140+
<ul
141+
v-show={isShow.value}
142+
onClick={clickHandle}
143+
class={[subMenuClass, class_layer.value, props['disabled'] && `${subMenuClass}-disabled`]}
144+
ref={subMenu}>
141145
<div
142-
class={[`${subMenuClass}-title`, props['disabled'] && `${subMenuClass}-disabled`]}
146+
class={[`${subMenuClass}-title`]}
143147
style={`padding: 0 ${indent}px`}
144148
ref={title}>
145149
<span class={`${ns.b()}-icon`}>{ctx.slots?.icon?.()}</span>
@@ -155,7 +159,11 @@ export default defineComponent({
155159
}}></i>
156160
</div>
157161
{isHorizontal ? (
158-
<div class={`${ns.b()}-item-horizontal-wrapper ${ns.b()}-item-horizontal-wrapper-hidden`} ref={wrapper}>
162+
<div
163+
class={`${ns.b()}-item-horizontal-wrapper ${ns.b()}-item-horizontal-wrapper-hidden`}
164+
ref={wrapper}
165+
v-show={!props.disabled}
166+
>
159167
{ctx.slots.default?.()}
160168
</div>
161169
) : (
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export function useNearestMenuElement(ele: HTMLElement): HTMLElement{
2+
while (ele && ele.tagName !== 'LI' && ele.tagName !== 'UL'){
3+
ele = ele.parentElement as HTMLElement;
4+
}
5+
return ele;
6+
}

packages/devui-vue/devui/menu/src/composables/use-store.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,15 @@ export class Store{
2222
emit(eventName: string, ...args: any[]): void{
2323
recordTable[this.rootMenuName][eventName].forEach((fn)=>fn(...args));
2424
}
25-
off(eventName: string, fn: (...args: []) => void): void {
25+
off(eventName: string, fn: (...args: []) => void): void{
2626
const idx = recordTable[this.rootMenuName][eventName].indexOf(fn);
2727
if (idx >= 0) {
2828
recordTable[this.rootMenuName][eventName].splice(idx, 1);
2929
}
3030
}
3131
}
3232

33-
export function useStore(rootName: string): Store {
33+
export function useStore(rootName: string): Store{
3434
if (!recordTable[rootName]){
3535
Reflect.set(recordTable, rootName, {});
3636
}

packages/devui-vue/devui/menu/src/menu.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ $devui-menu-item: var(--devui-menu-item);
88
$devui-menu-item-sub: var(--devui-menu-item-sub);
99
$devui-menu-item-disabled: var(--devui-menu-disabled);
1010
$devui-menu-item-select: var(--devui-menu-item-hover);
11+
$devui-menu-item-hover: var(--devui-menu-item-hover);
1112

1213
$devui-menu-item-selectBar: var(--devui-primary-hover, #5e7ce0);
1314
$devui-menu-active-parent: var(--devui-icon-fill-active);

0 commit comments

Comments
 (0)