From bcd1e59b664e39b9bead154b065c60eca8c28de5 Mon Sep 17 00:00:00 2001 From: jiming Date: Thu, 20 Jan 2022 11:21:55 +0800 Subject: [PATCH] feat: change the interaction of MenuBar in horizontal mode --- .../menuBar/__tests__/menubar.test.tsx | 60 ++++++++++- src/workbench/menuBar/horizontalView.tsx | 100 ++++++++++++++++++ src/workbench/menuBar/menuBar.tsx | 44 ++------ 3 files changed, 167 insertions(+), 37 deletions(-) create mode 100644 src/workbench/menuBar/horizontalView.tsx diff --git a/src/workbench/menuBar/__tests__/menubar.test.tsx b/src/workbench/menuBar/__tests__/menubar.test.tsx index 9c84b0a42..42c41554c 100644 --- a/src/workbench/menuBar/__tests__/menubar.test.tsx +++ b/src/workbench/menuBar/__tests__/menubar.test.tsx @@ -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'; @@ -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( + + ); + 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( + + ); + 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; + }); }); diff --git a/src/workbench/menuBar/horizontalView.tsx b/src/workbench/menuBar/horizontalView.tsx new file mode 100644 index 000000000..a9b560abf --- /dev/null +++ b/src/workbench/menuBar/horizontalView.tsx @@ -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, item: IMenuBarItem) => void; + logo?: React.ReactNode; +} + +export function HorizontalView(props: IHorizontalViewProps) { + const { data, onClick, logo } = props; + const menuRef = useRef(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 ( +
+
+ {logo || } +
+ +
+ ); +} diff --git a/src/workbench/menuBar/menuBar.tsx b/src/workbench/menuBar/menuBar.tsx index 3bd3813e3..a11eeaf2a 100644 --- a/src/workbench/menuBar/menuBar.tsx +++ b/src/workbench/menuBar/menuBar.tsx @@ -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 { @@ -31,7 +21,6 @@ export function MenuBar(props: IMenuBar & IMenuBarController) { logo, } = props; const childRef = useRef(null); - const menuRef = useRef(null); const addKeybindingForData = ( rawData: IMenuBarItem[] = [] @@ -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 = ( -
- {logo || } -
- - + ); }