Skip to content

Commit b352afd

Browse files
authored
feat: change the interaction of MenuBar in horizontal mode (#636)
1 parent ecdb312 commit b352afd

File tree

3 files changed

+167
-37
lines changed

3 files changed

+167
-37
lines changed

src/workbench/menuBar/__tests__/menubar.test.tsx

+59-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22
import renderer from 'react-test-renderer';
3-
import { cleanup, fireEvent, render } from '@testing-library/react';
3+
import { cleanup, fireEvent, render, waitFor } from '@testing-library/react';
44
import '@testing-library/jest-dom';
55

66
import MenuBar, { actionClassName } from '../menuBar';
@@ -123,4 +123,62 @@ describe('Test MenuBar Component', () => {
123123
fireEvent.click(elem);
124124
expect(mockFn).toBeCalled();
125125
});
126+
127+
test('Should support to execute the handleClickMenuBar method in HorizontalView', async () => {
128+
const { getByText } = render(
129+
<MenuBar
130+
data={menuData}
131+
onClick={TEST_FN}
132+
mode={MenuBarMode.horizontal}
133+
/>
134+
);
135+
const elem = getByText(TEST_ID);
136+
const liElem = elem.closest('li');
137+
const elemArr = liElem ? [liElem] : [];
138+
const spanElem = getByText(TEST_DATA);
139+
const ulElem = spanElem.closest('ul');
140+
const originalFunc = document.elementsFromPoint;
141+
document.elementsFromPoint = jest.fn(() => elemArr);
142+
143+
fireEvent.click(elem);
144+
await waitFor(() => {
145+
expect(ulElem?.style.opacity).toBe('1');
146+
});
147+
148+
fireEvent.click(elem);
149+
await waitFor(() => {
150+
expect(ulElem?.style.opacity).toBe('0');
151+
});
152+
153+
document.elementsFromPoint = originalFunc;
154+
});
155+
156+
test('Should support to execute the clearAutoDisplay method in HorizontalView', async () => {
157+
const { getByText } = render(
158+
<MenuBar
159+
data={menuData}
160+
onClick={TEST_FN}
161+
mode={MenuBarMode.horizontal}
162+
/>
163+
);
164+
const elem = getByText(TEST_ID);
165+
const liElem = elem.closest('li');
166+
const elemArr = liElem ? [liElem] : [];
167+
const spanElem = getByText(TEST_DATA);
168+
const ulElem = spanElem.closest('ul');
169+
const originalFunc = document.elementsFromPoint;
170+
document.elementsFromPoint = jest.fn(() => elemArr);
171+
172+
fireEvent.click(elem);
173+
await waitFor(() => {
174+
expect(ulElem?.style.opacity).toBe('1');
175+
});
176+
177+
fireEvent.click(document.body);
178+
await waitFor(() => {
179+
expect(ulElem?.style.opacity).toBe('0');
180+
});
181+
182+
document.elementsFromPoint = originalFunc;
183+
});
126184
});
+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import React, { useState, useRef, useEffect } from 'react';
2+
import {
3+
getBEMElement,
4+
prefixClaName,
5+
getBEMModifier,
6+
} from 'mo/common/className';
7+
import { IMenuBarItem } from 'mo/model/workbench/menuBar';
8+
import { Menu, MenuMode, MenuRef, IMenuProps } from 'mo/components/menu';
9+
import Logo from './logo';
10+
11+
export const defaultClassName = prefixClaName('menuBar');
12+
export const actionClassName = getBEMElement(defaultClassName, 'action');
13+
export const horizontalClassName = getBEMModifier(
14+
defaultClassName,
15+
'horizontal'
16+
);
17+
export const logoClassName = getBEMElement(horizontalClassName, 'logo');
18+
export const logoContentClassName = getBEMElement(logoClassName, 'content');
19+
20+
export interface IHorizontalViewProps {
21+
data?: IMenuProps[];
22+
onClick?: (event: React.MouseEvent<any, any>, item: IMenuBarItem) => void;
23+
logo?: React.ReactNode;
24+
}
25+
26+
export function HorizontalView(props: IHorizontalViewProps) {
27+
const { data, onClick, logo } = props;
28+
const menuRef = useRef<MenuRef>(null);
29+
const [autoDisplayMenu, setAutoDisplayMenu] = useState(false);
30+
31+
const checkIsRootLiElem = (e: MouseEvent) => {
32+
const target = e.target as HTMLElement;
33+
const liElem = target.closest('li');
34+
const menuBarElem = liElem?.parentElement?.parentElement;
35+
const isRootLiElem =
36+
!!menuBarElem &&
37+
menuBarElem.classList.contains(horizontalClassName);
38+
return isRootLiElem;
39+
};
40+
41+
useEffect(() => {
42+
const menuBarElem = document.getElementsByClassName(
43+
horizontalClassName
44+
)[0] as HTMLElement;
45+
46+
const handleClickMenuBar = (e: MouseEvent) => {
47+
const isRootLiElem = checkIsRootLiElem(e);
48+
if (!isRootLiElem) return;
49+
if (autoDisplayMenu) {
50+
e.preventDefault();
51+
e.stopPropagation();
52+
menuRef.current?.dispose?.();
53+
}
54+
// Delay the execution of setAutoDisplayMenu to ensure that the menu can be displayed.
55+
setTimeout(() => setAutoDisplayMenu(!autoDisplayMenu));
56+
};
57+
58+
const clearAutoDisplay = (e: MouseEvent) => {
59+
if (!autoDisplayMenu) return;
60+
const isRootLiElem = checkIsRootLiElem(e);
61+
const target = e.target as HTMLElement;
62+
const liElem = target.closest('li');
63+
if (!isRootLiElem && !liElem?.dataset.submenu) {
64+
setAutoDisplayMenu(false);
65+
}
66+
};
67+
68+
document.addEventListener('click', clearAutoDisplay);
69+
menuBarElem?.addEventListener('click', handleClickMenuBar);
70+
71+
return () => {
72+
document.removeEventListener('click', clearAutoDisplay);
73+
menuBarElem?.removeEventListener('click', handleClickMenuBar);
74+
};
75+
}, [autoDisplayMenu]);
76+
77+
const trigger = autoDisplayMenu ? 'hover' : 'click';
78+
79+
const handleClickMenu = (e: React.MouseEvent, item: IMenuBarItem) => {
80+
onClick?.(e, item);
81+
menuRef.current!.dispose();
82+
};
83+
84+
return (
85+
<div className={horizontalClassName}>
86+
<div className={logoClassName}>
87+
{logo || <Logo className={logoContentClassName} />}
88+
</div>
89+
<Menu
90+
ref={menuRef}
91+
role="menu"
92+
mode={MenuMode.Horizontal}
93+
trigger={trigger}
94+
onClick={handleClickMenu}
95+
style={{ width: '100%' }}
96+
data={data}
97+
/>
98+
</div>
99+
);
100+
}

src/workbench/menuBar/menuBar.tsx

+8-36
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,16 @@
11
import React, { useCallback, useEffect, useRef } from 'react';
2-
import {
3-
getBEMElement,
4-
prefixClaName,
5-
getBEMModifier,
6-
} from 'mo/common/className';
2+
import { getBEMElement, prefixClaName } from 'mo/common/className';
73
import { IMenuBar, IMenuBarItem } from 'mo/model/workbench/menuBar';
84
import { IMenuBarController } from 'mo/controller/menuBar';
95
import { DropDown, DropDownRef } from 'mo/components/dropdown';
10-
import { IMenuProps, Menu, MenuMode, MenuRef } from 'mo/components/menu';
6+
import { IMenuProps, Menu } from 'mo/components/menu';
117
import { Icon } from 'mo/components/icon';
128
import { KeybindingHelper } from 'mo/services/keybinding';
139
import { MenuBarMode } from 'mo/model/workbench/layout';
14-
import Logo from './logo';
10+
import { HorizontalView } from './horizontalView';
1511

1612
export const defaultClassName = prefixClaName('menuBar');
1713
export const actionClassName = getBEMElement(defaultClassName, 'action');
18-
export const horizontalClassName = getBEMModifier(
19-
defaultClassName,
20-
'horizontal'
21-
);
22-
export const logoClassName = getBEMElement(horizontalClassName, 'logo');
23-
export const logoContentClassName = getBEMElement(logoClassName, 'content');
2414

2515
export function MenuBar(props: IMenuBar & IMenuBarController) {
2616
const {
@@ -31,7 +21,6 @@ export function MenuBar(props: IMenuBar & IMenuBarController) {
3121
logo,
3222
} = props;
3323
const childRef = useRef<DropDownRef>(null);
34-
const menuRef = useRef<MenuRef>(null);
3524

3625
const addKeybindingForData = (
3726
rawData: IMenuBarItem[] = []
@@ -62,14 +51,6 @@ export function MenuBar(props: IMenuBar & IMenuBarController) {
6251
childRef.current!.dispose();
6352
};
6453

65-
const handleClickHorizontalMenu = (
66-
e: React.MouseEvent,
67-
item: IMenuBarItem
68-
) => {
69-
onClick?.(e, item);
70-
menuRef.current!.dispose();
71-
};
72-
7354
const overlay = (
7455
<Menu
7556
role="menu"
@@ -92,20 +73,11 @@ export function MenuBar(props: IMenuBar & IMenuBarController) {
9273

9374
if (mode === MenuBarMode.horizontal) {
9475
return (
95-
<div className={horizontalClassName}>
96-
<div className={logoClassName}>
97-
{logo || <Logo className={logoContentClassName} />}
98-
</div>
99-
<Menu
100-
ref={menuRef}
101-
role="menu"
102-
mode={MenuMode.Horizontal}
103-
trigger="click"
104-
onClick={handleClickHorizontalMenu}
105-
style={{ width: '100%' }}
106-
data={addKeybindingForData(data)}
107-
/>
108-
</div>
76+
<HorizontalView
77+
data={addKeybindingForData(data)}
78+
onClick={onClick}
79+
logo={logo}
80+
/>
10981
);
11082
}
11183

0 commit comments

Comments
 (0)