From efaa779c2e14147791ad1dae209dcaadb09ed65e Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Wed, 27 Sep 2023 17:05:40 +0100 Subject: [PATCH] Navigation: Refactor MegaMenu to separate out overlay/animation logic (#75365) * user essentials mob! :trident: lastFile:public/app/core/components/AppChrome/DockedMegaMenu/DockedMegaMenu.tsx * mob start [ci-skip] [ci skip] [skip ci] * user essentials mob! :trident: lastFile:public/app/core/components/AppChrome/DockedMegaMenu/DockedMegaMenu.tsx * user essentials mob! :trident: lastFile:public/app/core/components/AppChrome/AppChrome.tsx * fix border css + scroll * import width from DockedMegaMenu * extract logic out into AppChromeMenu * fix bug with hamburger icon * rename some styles * one more rename * remove janky state logic * prevent react-aria closing the overlay when interacting with the toggle button * don't need boolean --------- Co-authored-by: eledobleefe --- .../core/components/AppChrome/AppChrome.tsx | 14 +- .../NavBarMenu.tsx => AppChromeMenu.tsx} | 152 +++++++----------- .../DockedMegaMenu/DockedMegaMenu.tsx | 91 +++++++---- .../AppChrome/NavToolbar/NavToolbar.tsx | 3 + 4 files changed, 129 insertions(+), 131 deletions(-) rename public/app/core/components/AppChrome/{DockedMegaMenu/NavBarMenu.tsx => AppChromeMenu.tsx} (50%) diff --git a/public/app/core/components/AppChrome/AppChrome.tsx b/public/app/core/components/AppChrome/AppChrome.tsx index f1abdfc2a68..5c2f458f9c1 100644 --- a/public/app/core/components/AppChrome/AppChrome.tsx +++ b/public/app/core/components/AppChrome/AppChrome.tsx @@ -9,7 +9,7 @@ import { useGrafana } from 'app/core/context/GrafanaContext'; import { CommandPalette } from 'app/features/commandPalette/CommandPalette'; import { KioskMode } from 'app/types'; -import { DockedMegaMenu } from './DockedMegaMenu/DockedMegaMenu'; +import { AppChromeMenu } from './AppChromeMenu'; import { MegaMenu } from './MegaMenu/MegaMenu'; import { NavToolbar } from './NavToolbar/NavToolbar'; import { SectionNav } from './SectionNav/SectionNav'; @@ -19,11 +19,10 @@ import { TOP_BAR_LEVEL_HEIGHT } from './types'; export interface Props extends PropsWithChildren<{}> {} export function AppChrome({ children }: Props) { - const styles = useStyles2(getStyles); const { chrome } = useGrafana(); const state = chrome.useState(); - const searchBarHidden = state.searchBarHidden || state.kioskMode === KioskMode.TV; + const styles = useStyles2(getStyles); const contentClass = cx({ [styles.content]: true, @@ -34,7 +33,6 @@ export function AppChrome({ children }: Props) { // Chromeless routes are without topNav, mega menu, search & command palette // We check chromeless twice here instead of having a separate path so {children} // doesn't get re-mounted when chromeless goes from true to false. - return (
)} -
+
{state.layout === PageLayoutType.Standard && state.sectionNav && !config.featureToggles.dockedMegaMenu && ( )} -
{children}
+
+ {children} +
{!state.chromeless && ( <> {config.featureToggles.dockedMegaMenu ? ( - chrome.setMegaMenu(false)} /> + ) : ( chrome.setMegaMenu(false)} /> )} diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/NavBarMenu.tsx b/public/app/core/components/AppChrome/AppChromeMenu.tsx similarity index 50% rename from public/app/core/components/AppChrome/DockedMegaMenu/NavBarMenu.tsx rename to public/app/core/components/AppChrome/AppChromeMenu.tsx index 6879e4f1cda..e8c0404f366 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/NavBarMenu.tsx +++ b/public/app/core/components/AppChrome/AppChromeMenu.tsx @@ -2,105 +2,79 @@ import { css } from '@emotion/css'; import { useDialog } from '@react-aria/dialog'; import { FocusScope } from '@react-aria/focus'; import { OverlayContainer, useOverlay } from '@react-aria/overlays'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useRef } from 'react'; import CSSTransition from 'react-transition-group/CSSTransition'; -import { GrafanaTheme2, NavModelItem } from '@grafana/data'; -import { CustomScrollbar, Icon, IconButton, useTheme2 } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data'; +import { useStyles2, useTheme2 } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; +import { KioskMode } from 'app/types'; -import { TOP_BAR_LEVEL_HEIGHT } from '../types'; +import { DockedMegaMenu, MENU_WIDTH } from './DockedMegaMenu/DockedMegaMenu'; +import { TOGGLE_BUTTON_ID } from './NavToolbar/NavToolbar'; +import { TOP_BAR_LEVEL_HEIGHT } from './types'; -import { NavBarMenuItemWrapper } from './NavBarMenuItemWrapper'; +interface Props {} -const MENU_WIDTH = '350px'; - -export interface Props { - activeItem?: NavModelItem; - navItems: NavModelItem[]; - searchBarHidden?: boolean; - onClose: () => void; -} - -export function NavBarMenu({ activeItem, navItems, searchBarHidden, onClose }: Props) { +export function AppChromeMenu({}: Props) { const theme = useTheme2(); - const styles = getStyles(theme, searchBarHidden); - const animationSpeed = theme.transitions.duration.shortest; - const animStyles = getAnimStyles(theme, animationSpeed); const { chrome } = useGrafana(); const state = chrome.useState(); + const searchBarHidden = state.searchBarHidden || state.kioskMode === KioskMode.TV; + const ref = useRef(null); const backdropRef = useRef(null); - const { dialogProps } = useDialog({}, ref); - const [isOpen, setIsOpen] = useState(false); + const animationSpeed = theme.transitions.duration.shortest; + const animationStyles = useStyles2(getAnimStyles, animationSpeed); - const onMenuClose = () => setIsOpen(false); + const isOpen = state.megaMenuOpen; + const onClose = () => chrome.setMegaMenu(false); const { overlayProps, underlayProps } = useOverlay( { isDismissable: true, isOpen: true, - onClose: onMenuClose, + onClose, + shouldCloseOnInteractOutside: (element) => { + // don't close when clicking on the menu toggle, let the toggle button handle that + // this prevents some nasty flickering when the menu is open and the toggle button is clicked + const isMenuToggle = document.getElementById(TOGGLE_BUTTON_ID)?.contains(element); + return !isMenuToggle; + }, }, ref ); - - useEffect(() => { - if (state.megaMenuOpen) { - setIsOpen(true); - } - }, [state.megaMenuOpen]); + const { dialogProps } = useDialog({}, ref); + const styles = useStyles2(getStyles, searchBarHidden); return ( - - - -
-
- - -
- -
-
-
- -
- - +
+ + + + + + + +
+ + +
); } -NavBarMenu.displayName = 'NavBarMenu'; - const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => { const topPosition = (searchBarHidden ? TOP_BAR_LEVEL_HEIGHT : TOP_BAR_LEVEL_HEIGHT * 2) + 1; @@ -119,12 +93,11 @@ const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => { top: topPosition, }, }), - container: css({ + menu: css({ display: 'flex', bottom: 0, flexDirection: 'column', left: 0, - marginRight: theme.spacing(1.5), right: 0, // Needs to below navbar should we change the navbarFixed? add add a new level? zIndex: theme.zIndex.modal, @@ -135,32 +108,17 @@ const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => { flex: '1 1 0', [theme.breakpoints.up('md')]: { - borderRight: `1px solid ${theme.colors.border.weak}`, right: 'unset', + borderRight: `1px solid ${theme.colors.border.weak}`, top: topPosition, }, }), - content: css({ - display: 'flex', - flexDirection: 'column', - flexGrow: 1, - minHeight: 0, - }), - mobileHeader: css({ - display: 'flex', - justifyContent: 'space-between', - padding: theme.spacing(1, 1, 1, 2), - borderBottom: `1px solid ${theme.colors.border.weak}`, - - [theme.breakpoints.up('md')]: { - display: 'none', - }, - }), - itemList: css({ + wrapper: css({ + position: 'fixed', display: 'grid', - gridAutoRows: `minmax(${theme.spacing(6)}, auto)`, - gridTemplateColumns: `minmax(${MENU_WIDTH}, auto)`, - minWidth: MENU_WIDTH, + gridAutoFlow: 'column', + height: '100%', + zIndex: theme.zIndex.sidemenu, }), }; }; diff --git a/public/app/core/components/AppChrome/DockedMegaMenu/DockedMegaMenu.tsx b/public/app/core/components/AppChrome/DockedMegaMenu/DockedMegaMenu.tsx index 71fbdf52e3f..30e6e30aa2e 100644 --- a/public/app/core/components/AppChrome/DockedMegaMenu/DockedMegaMenu.tsx +++ b/public/app/core/components/AppChrome/DockedMegaMenu/DockedMegaMenu.tsx @@ -1,50 +1,87 @@ import { css } from '@emotion/css'; +import { DOMAttributes } from '@react-types/shared'; import { cloneDeep } from 'lodash'; -import React from 'react'; +import React, { forwardRef } from 'react'; import { useLocation } from 'react-router-dom'; import { GrafanaTheme2 } from '@grafana/data'; -import { useTheme2 } from '@grafana/ui'; +import { CustomScrollbar, Icon, IconButton, useStyles2 } from '@grafana/ui'; import { useSelector } from 'app/types'; -import { NavBarMenu } from './NavBarMenu'; +import { NavBarMenuItemWrapper } from './NavBarMenuItemWrapper'; import { enrichWithInteractionTracking, getActiveItem } from './utils'; -export interface Props { +export const MENU_WIDTH = '350px'; + +export interface Props extends DOMAttributes { onClose: () => void; - searchBarHidden?: boolean; } -export const DockedMegaMenu = React.memo(({ onClose, searchBarHidden }) => { - const navBarTree = useSelector((state) => state.navBarTree); - const theme = useTheme2(); - const styles = getStyles(theme); - const location = useLocation(); +export const DockedMegaMenu = React.memo( + forwardRef(({ onClose, ...restProps }, ref) => { + const navBarTree = useSelector((state) => state.navBarTree); + const styles = useStyles2(getStyles); + const location = useLocation(); - const navTree = cloneDeep(navBarTree); + const navTree = cloneDeep(navBarTree); - // Remove profile + help from tree - const navItems = navTree - .filter((item) => item.id !== 'profile' && item.id !== 'help') - .map((item) => enrichWithInteractionTracking(item, true)); + // Remove profile + help from tree + const navItems = navTree + .filter((item) => item.id !== 'profile' && item.id !== 'help') + .map((item) => enrichWithInteractionTracking(item, true)); - const activeItem = getActiveItem(navItems, location.pathname); + const activeItem = getActiveItem(navItems, location.pathname); - return ( -
- -
- ); -}); + return ( +
+
+ + +
+ +
+ ); + }) +); DockedMegaMenu.displayName = 'DockedMegaMenu'; const getStyles = (theme: GrafanaTheme2) => ({ - menuWrapper: css({ - position: 'fixed', + content: css({ + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + minHeight: 0, + }), + mobileHeader: css({ + display: 'flex', + justifyContent: 'space-between', + padding: theme.spacing(1, 1, 1, 2), + borderBottom: `1px solid ${theme.colors.border.weak}`, + + [theme.breakpoints.up('md')]: { + display: 'none', + }, + }), + itemList: css({ display: 'grid', - gridAutoFlow: 'column', - height: '100%', - zIndex: theme.zIndex.sidemenu, + gridAutoRows: `minmax(${theme.spacing(6)}, auto)`, + gridTemplateColumns: `minmax(${MENU_WIDTH}, auto)`, + minWidth: MENU_WIDTH, }), }); diff --git a/public/app/core/components/AppChrome/NavToolbar/NavToolbar.tsx b/public/app/core/components/AppChrome/NavToolbar/NavToolbar.tsx index c422fa4f4bf..ec8738a9af4 100644 --- a/public/app/core/components/AppChrome/NavToolbar/NavToolbar.tsx +++ b/public/app/core/components/AppChrome/NavToolbar/NavToolbar.tsx @@ -14,6 +14,8 @@ import { TOP_BAR_LEVEL_HEIGHT } from '../types'; import { NavToolbarSeparator } from './NavToolbarSeparator'; +export const TOGGLE_BUTTON_ID = 'mega-menu-toggle'; + export interface Props { onToggleSearchBar(): void; onToggleMegaMenu(): void; @@ -41,6 +43,7 @@ export function NavToolbar({