mirror of
https://github.com/grafana/grafana.git
synced 2025-01-02 04:07:15 -06:00
Navigation: Refactor MegaMenu to separate out overlay/animation logic (#75365)
* user essentials mob! 🔱 lastFile:public/app/core/components/AppChrome/DockedMegaMenu/DockedMegaMenu.tsx * mob start [ci-skip] [ci skip] [skip ci] * user essentials mob! 🔱 lastFile:public/app/core/components/AppChrome/DockedMegaMenu/DockedMegaMenu.tsx * user essentials mob! 🔱 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 <laura.fernandez@grafana.com>
This commit is contained in:
parent
bbdd1fc3b1
commit
efaa779c2e
@ -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 (
|
||||
<div
|
||||
className={classNames('main-view', {
|
||||
@ -61,18 +59,20 @@ export function AppChrome({ children }: Props) {
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<main className={contentClass} id="pageContent">
|
||||
<main className={contentClass}>
|
||||
<div className={styles.panes}>
|
||||
{state.layout === PageLayoutType.Standard && state.sectionNav && !config.featureToggles.dockedMegaMenu && (
|
||||
<SectionNav model={state.sectionNav} />
|
||||
)}
|
||||
<div className={styles.pageContainer}>{children}</div>
|
||||
<div className={styles.pageContainer} id="pageContent">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{!state.chromeless && (
|
||||
<>
|
||||
{config.featureToggles.dockedMegaMenu ? (
|
||||
<DockedMegaMenu searchBarHidden={searchBarHidden} onClose={() => chrome.setMegaMenu(false)} />
|
||||
<AppChromeMenu />
|
||||
) : (
|
||||
<MegaMenu searchBarHidden={searchBarHidden} onClose={() => chrome.setMegaMenu(false)} />
|
||||
)}
|
||||
|
@ -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 (
|
||||
<OverlayContainer>
|
||||
<CSSTransition
|
||||
nodeRef={ref}
|
||||
in={isOpen}
|
||||
unmountOnExit={true}
|
||||
classNames={animStyles.overlay}
|
||||
timeout={{ enter: animationSpeed, exit: 0 }}
|
||||
onExited={onClose}
|
||||
>
|
||||
<FocusScope contain autoFocus>
|
||||
<div data-testid="navbarmenu" ref={ref} {...overlayProps} {...dialogProps} className={styles.container}>
|
||||
<div className={styles.mobileHeader}>
|
||||
<Icon name="bars" size="xl" />
|
||||
<IconButton
|
||||
aria-label="Close navigation menu"
|
||||
tooltip="Close menu"
|
||||
name="times"
|
||||
onClick={onMenuClose}
|
||||
size="xl"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
<nav className={styles.content}>
|
||||
<CustomScrollbar showScrollIndicators hideHorizontalTrack>
|
||||
<ul className={styles.itemList}>
|
||||
{navItems.map((link) => (
|
||||
<NavBarMenuItemWrapper link={link} onClose={onMenuClose} activeItem={activeItem} key={link.text} />
|
||||
))}
|
||||
</ul>
|
||||
</CustomScrollbar>
|
||||
</nav>
|
||||
</div>
|
||||
</FocusScope>
|
||||
</CSSTransition>
|
||||
<CSSTransition
|
||||
nodeRef={backdropRef}
|
||||
in={isOpen}
|
||||
unmountOnExit={true}
|
||||
classNames={animStyles.backdrop}
|
||||
timeout={{ enter: animationSpeed, exit: 0 }}
|
||||
>
|
||||
<div ref={backdropRef} className={styles.backdrop} {...underlayProps} />
|
||||
</CSSTransition>
|
||||
</OverlayContainer>
|
||||
<div className={styles.wrapper}>
|
||||
<OverlayContainer>
|
||||
<CSSTransition
|
||||
nodeRef={ref}
|
||||
in={isOpen}
|
||||
unmountOnExit={true}
|
||||
classNames={animationStyles.overlay}
|
||||
timeout={{ enter: animationSpeed, exit: 0 }}
|
||||
>
|
||||
<FocusScope contain autoFocus>
|
||||
<DockedMegaMenu className={styles.menu} onClose={onClose} ref={ref} {...overlayProps} {...dialogProps} />
|
||||
</FocusScope>
|
||||
</CSSTransition>
|
||||
<CSSTransition
|
||||
nodeRef={backdropRef}
|
||||
in={isOpen}
|
||||
unmountOnExit={true}
|
||||
classNames={animationStyles.backdrop}
|
||||
timeout={{ enter: animationSpeed, exit: 0 }}
|
||||
>
|
||||
<div ref={backdropRef} className={styles.backdrop} {...underlayProps} />
|
||||
</CSSTransition>
|
||||
</OverlayContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
}),
|
||||
};
|
||||
};
|
@ -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<Props>(({ onClose, searchBarHidden }) => {
|
||||
const navBarTree = useSelector((state) => state.navBarTree);
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme);
|
||||
const location = useLocation();
|
||||
export const DockedMegaMenu = React.memo(
|
||||
forwardRef<HTMLDivElement, Props>(({ 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 (
|
||||
<div className={styles.menuWrapper}>
|
||||
<NavBarMenu activeItem={activeItem} navItems={navItems} onClose={onClose} searchBarHidden={searchBarHidden} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div data-testid="navbarmenu" ref={ref} {...restProps}>
|
||||
<div className={styles.mobileHeader}>
|
||||
<Icon name="bars" size="xl" />
|
||||
<IconButton
|
||||
aria-label="Close navigation menu"
|
||||
tooltip="Close menu"
|
||||
name="times"
|
||||
onClick={onClose}
|
||||
size="xl"
|
||||
variant="secondary"
|
||||
/>
|
||||
</div>
|
||||
<nav className={styles.content}>
|
||||
<CustomScrollbar showScrollIndicators hideHorizontalTrack>
|
||||
<ul className={styles.itemList}>
|
||||
{navItems.map((link) => (
|
||||
<NavBarMenuItemWrapper link={link} onClose={onClose} activeItem={activeItem} key={link.text} />
|
||||
))}
|
||||
</ul>
|
||||
</CustomScrollbar>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
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,
|
||||
}),
|
||||
});
|
||||
|
@ -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({
|
||||
<div data-testid={Components.NavToolbar.container} className={styles.pageToolbar}>
|
||||
<div className={styles.menuButton}>
|
||||
<IconButton
|
||||
id={TOGGLE_BUTTON_ID}
|
||||
name="bars"
|
||||
tooltip={t('navigation.toolbar.toggle-menu', 'Toggle menu')}
|
||||
tooltipPlacement="bottom"
|
||||
|
Loading…
Reference in New Issue
Block a user