Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: optimize the interaction of MenuBar in horizontal mode #636

Merged
merged 1 commit into from
Jan 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion src/workbench/menuBar/__tests__/menubar.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { cleanup, fireEvent, render } from '@testing-library/react';
import { cleanup, fireEvent, render, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';

import MenuBar, { actionClassName } from '../menuBar';
Expand Down Expand Up @@ -123,4 +123,62 @@ describe('Test MenuBar Component', () => {
fireEvent.click(elem);
expect(mockFn).toBeCalled();
});

test('Should support to execute the handleClickMenuBar method in HorizontalView', async () => {
const { getByText } = render(
<MenuBar
data={menuData}
onClick={TEST_FN}
mode={MenuBarMode.horizontal}
/>
);
const elem = getByText(TEST_ID);
const liElem = elem.closest('li');
const elemArr = liElem ? [liElem] : [];
const spanElem = getByText(TEST_DATA);
const ulElem = spanElem.closest('ul');
const originalFunc = document.elementsFromPoint;
document.elementsFromPoint = jest.fn(() => elemArr);

fireEvent.click(elem);
await waitFor(() => {
expect(ulElem?.style.opacity).toBe('1');
});

fireEvent.click(elem);
await waitFor(() => {
expect(ulElem?.style.opacity).toBe('0');
});

document.elementsFromPoint = originalFunc;
});

test('Should support to execute the clearAutoDisplay method in HorizontalView', async () => {
const { getByText } = render(
<MenuBar
data={menuData}
onClick={TEST_FN}
mode={MenuBarMode.horizontal}
/>
);
const elem = getByText(TEST_ID);
const liElem = elem.closest('li');
const elemArr = liElem ? [liElem] : [];
const spanElem = getByText(TEST_DATA);
const ulElem = spanElem.closest('ul');
const originalFunc = document.elementsFromPoint;
document.elementsFromPoint = jest.fn(() => elemArr);

fireEvent.click(elem);
await waitFor(() => {
expect(ulElem?.style.opacity).toBe('1');
});

fireEvent.click(document.body);
await waitFor(() => {
expect(ulElem?.style.opacity).toBe('0');
});

document.elementsFromPoint = originalFunc;
});
});
100 changes: 100 additions & 0 deletions src/workbench/menuBar/horizontalView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React, { useState, useRef, useEffect } from 'react';
import {
getBEMElement,
prefixClaName,
getBEMModifier,
} from 'mo/common/className';
import { IMenuBarItem } from 'mo/model/workbench/menuBar';
import { Menu, MenuMode, MenuRef, IMenuProps } from 'mo/components/menu';
import Logo from './logo';

export const defaultClassName = prefixClaName('menuBar');
export const actionClassName = getBEMElement(defaultClassName, 'action');
export const horizontalClassName = getBEMModifier(
defaultClassName,
'horizontal'
);
export const logoClassName = getBEMElement(horizontalClassName, 'logo');
export const logoContentClassName = getBEMElement(logoClassName, 'content');

export interface IHorizontalViewProps {
data?: IMenuProps[];
onClick?: (event: React.MouseEvent<any, any>, item: IMenuBarItem) => void;
logo?: React.ReactNode;
}

export function HorizontalView(props: IHorizontalViewProps) {
const { data, onClick, logo } = props;
const menuRef = useRef<MenuRef>(null);
const [autoDisplayMenu, setAutoDisplayMenu] = useState(false);

const checkIsRootLiElem = (e: MouseEvent) => {
const target = e.target as HTMLElement;
const liElem = target.closest('li');
const menuBarElem = liElem?.parentElement?.parentElement;
const isRootLiElem =
!!menuBarElem &&
menuBarElem.classList.contains(horizontalClassName);
return isRootLiElem;
};

useEffect(() => {
const menuBarElem = document.getElementsByClassName(
horizontalClassName
)[0] as HTMLElement;

const handleClickMenuBar = (e: MouseEvent) => {
const isRootLiElem = checkIsRootLiElem(e);
if (!isRootLiElem) return;
if (autoDisplayMenu) {
e.preventDefault();
e.stopPropagation();
menuRef.current?.dispose?.();
}
// Delay the execution of setAutoDisplayMenu to ensure that the menu can be displayed.
setTimeout(() => setAutoDisplayMenu(!autoDisplayMenu));
};

const clearAutoDisplay = (e: MouseEvent) => {
if (!autoDisplayMenu) return;
const isRootLiElem = checkIsRootLiElem(e);
const target = e.target as HTMLElement;
const liElem = target.closest('li');
if (!isRootLiElem && !liElem?.dataset.submenu) {
setAutoDisplayMenu(false);
}
};

document.addEventListener('click', clearAutoDisplay);
menuBarElem?.addEventListener('click', handleClickMenuBar);

return () => {
document.removeEventListener('click', clearAutoDisplay);
menuBarElem?.removeEventListener('click', handleClickMenuBar);
};
}, [autoDisplayMenu]);

const trigger = autoDisplayMenu ? 'hover' : 'click';

const handleClickMenu = (e: React.MouseEvent, item: IMenuBarItem) => {
onClick?.(e, item);
menuRef.current!.dispose();
};

return (
<div className={horizontalClassName}>
<div className={logoClassName}>
{logo || <Logo className={logoContentClassName} />}
</div>
<Menu
ref={menuRef}
role="menu"
mode={MenuMode.Horizontal}
trigger={trigger}
onClick={handleClickMenu}
style={{ width: '100%' }}
data={data}
/>
</div>
);
}
44 changes: 8 additions & 36 deletions src/workbench/menuBar/menuBar.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
import React, { useCallback, useEffect, useRef } from 'react';
import {
getBEMElement,
prefixClaName,
getBEMModifier,
} from 'mo/common/className';
import { getBEMElement, prefixClaName } from 'mo/common/className';
import { IMenuBar, IMenuBarItem } from 'mo/model/workbench/menuBar';
import { IMenuBarController } from 'mo/controller/menuBar';
import { DropDown, DropDownRef } from 'mo/components/dropdown';
import { IMenuProps, Menu, MenuMode, MenuRef } from 'mo/components/menu';
import { IMenuProps, Menu } from 'mo/components/menu';
import { Icon } from 'mo/components/icon';
import { KeybindingHelper } from 'mo/services/keybinding';
import { MenuBarMode } from 'mo/model/workbench/layout';
import Logo from './logo';
import { HorizontalView } from './horizontalView';

export const defaultClassName = prefixClaName('menuBar');
export const actionClassName = getBEMElement(defaultClassName, 'action');
export const horizontalClassName = getBEMModifier(
defaultClassName,
'horizontal'
);
export const logoClassName = getBEMElement(horizontalClassName, 'logo');
export const logoContentClassName = getBEMElement(logoClassName, 'content');

export function MenuBar(props: IMenuBar & IMenuBarController) {
const {
Expand All @@ -31,7 +21,6 @@ export function MenuBar(props: IMenuBar & IMenuBarController) {
logo,
} = props;
const childRef = useRef<DropDownRef>(null);
const menuRef = useRef<MenuRef>(null);

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

const handleClickHorizontalMenu = (
e: React.MouseEvent,
item: IMenuBarItem
) => {
onClick?.(e, item);
menuRef.current!.dispose();
};

const overlay = (
<Menu
role="menu"
Expand All @@ -92,20 +73,11 @@ export function MenuBar(props: IMenuBar & IMenuBarController) {

if (mode === MenuBarMode.horizontal) {
return (
<div className={horizontalClassName}>
<div className={logoClassName}>
{logo || <Logo className={logoContentClassName} />}
</div>
<Menu
ref={menuRef}
role="menu"
mode={MenuMode.Horizontal}
trigger="click"
onClick={handleClickHorizontalMenu}
style={{ width: '100%' }}
data={addKeybindingForData(data)}
/>
</div>
<HorizontalView
data={addKeybindingForData(data)}
onClick={onClick}
logo={logo}
/>
);
}

Expand Down