mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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 { CommandPalette } from 'app/features/commandPalette/CommandPalette';
|
||||||
import { KioskMode } from 'app/types';
|
import { KioskMode } from 'app/types';
|
||||||
|
|
||||||
import { DockedMegaMenu } from './DockedMegaMenu/DockedMegaMenu';
|
import { AppChromeMenu } from './AppChromeMenu';
|
||||||
import { MegaMenu } from './MegaMenu/MegaMenu';
|
import { MegaMenu } from './MegaMenu/MegaMenu';
|
||||||
import { NavToolbar } from './NavToolbar/NavToolbar';
|
import { NavToolbar } from './NavToolbar/NavToolbar';
|
||||||
import { SectionNav } from './SectionNav/SectionNav';
|
import { SectionNav } from './SectionNav/SectionNav';
|
||||||
@ -19,11 +19,10 @@ import { TOP_BAR_LEVEL_HEIGHT } from './types';
|
|||||||
export interface Props extends PropsWithChildren<{}> {}
|
export interface Props extends PropsWithChildren<{}> {}
|
||||||
|
|
||||||
export function AppChrome({ children }: Props) {
|
export function AppChrome({ children }: Props) {
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
const { chrome } = useGrafana();
|
const { chrome } = useGrafana();
|
||||||
const state = chrome.useState();
|
const state = chrome.useState();
|
||||||
|
|
||||||
const searchBarHidden = state.searchBarHidden || state.kioskMode === KioskMode.TV;
|
const searchBarHidden = state.searchBarHidden || state.kioskMode === KioskMode.TV;
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const contentClass = cx({
|
const contentClass = cx({
|
||||||
[styles.content]: true,
|
[styles.content]: true,
|
||||||
@ -34,7 +33,6 @@ export function AppChrome({ children }: Props) {
|
|||||||
// Chromeless routes are without topNav, mega menu, search & command palette
|
// Chromeless routes are without topNav, mega menu, search & command palette
|
||||||
// We check chromeless twice here instead of having a separate path so {children}
|
// 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.
|
// doesn't get re-mounted when chromeless goes from true to false.
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames('main-view', {
|
className={classNames('main-view', {
|
||||||
@ -61,18 +59,20 @@ export function AppChrome({ children }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<main className={contentClass} id="pageContent">
|
<main className={contentClass}>
|
||||||
<div className={styles.panes}>
|
<div className={styles.panes}>
|
||||||
{state.layout === PageLayoutType.Standard && state.sectionNav && !config.featureToggles.dockedMegaMenu && (
|
{state.layout === PageLayoutType.Standard && state.sectionNav && !config.featureToggles.dockedMegaMenu && (
|
||||||
<SectionNav model={state.sectionNav} />
|
<SectionNav model={state.sectionNav} />
|
||||||
)}
|
)}
|
||||||
<div className={styles.pageContainer}>{children}</div>
|
<div className={styles.pageContainer} id="pageContent">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{!state.chromeless && (
|
{!state.chromeless && (
|
||||||
<>
|
<>
|
||||||
{config.featureToggles.dockedMegaMenu ? (
|
{config.featureToggles.dockedMegaMenu ? (
|
||||||
<DockedMegaMenu searchBarHidden={searchBarHidden} onClose={() => chrome.setMegaMenu(false)} />
|
<AppChromeMenu />
|
||||||
) : (
|
) : (
|
||||||
<MegaMenu searchBarHidden={searchBarHidden} onClose={() => chrome.setMegaMenu(false)} />
|
<MegaMenu searchBarHidden={searchBarHidden} onClose={() => chrome.setMegaMenu(false)} />
|
||||||
)}
|
)}
|
||||||
|
@ -2,105 +2,79 @@ import { css } from '@emotion/css';
|
|||||||
import { useDialog } from '@react-aria/dialog';
|
import { useDialog } from '@react-aria/dialog';
|
||||||
import { FocusScope } from '@react-aria/focus';
|
import { FocusScope } from '@react-aria/focus';
|
||||||
import { OverlayContainer, useOverlay } from '@react-aria/overlays';
|
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 CSSTransition from 'react-transition-group/CSSTransition';
|
||||||
|
|
||||||
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { CustomScrollbar, Icon, IconButton, useTheme2 } from '@grafana/ui';
|
import { useStyles2, useTheme2 } from '@grafana/ui';
|
||||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
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 function AppChromeMenu({}: Props) {
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
activeItem?: NavModelItem;
|
|
||||||
navItems: NavModelItem[];
|
|
||||||
searchBarHidden?: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function NavBarMenu({ activeItem, navItems, searchBarHidden, onClose }: Props) {
|
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme, searchBarHidden);
|
|
||||||
const animationSpeed = theme.transitions.duration.shortest;
|
|
||||||
const animStyles = getAnimStyles(theme, animationSpeed);
|
|
||||||
const { chrome } = useGrafana();
|
const { chrome } = useGrafana();
|
||||||
const state = chrome.useState();
|
const state = chrome.useState();
|
||||||
|
const searchBarHidden = state.searchBarHidden || state.kioskMode === KioskMode.TV;
|
||||||
|
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const backdropRef = useRef(null);
|
const backdropRef = useRef(null);
|
||||||
const { dialogProps } = useDialog({}, ref);
|
const animationSpeed = theme.transitions.duration.shortest;
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const animationStyles = useStyles2(getAnimStyles, animationSpeed);
|
||||||
|
|
||||||
const onMenuClose = () => setIsOpen(false);
|
const isOpen = state.megaMenuOpen;
|
||||||
|
const onClose = () => chrome.setMegaMenu(false);
|
||||||
|
|
||||||
const { overlayProps, underlayProps } = useOverlay(
|
const { overlayProps, underlayProps } = useOverlay(
|
||||||
{
|
{
|
||||||
isDismissable: true,
|
isDismissable: true,
|
||||||
isOpen: 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
|
ref
|
||||||
);
|
);
|
||||||
|
const { dialogProps } = useDialog({}, ref);
|
||||||
useEffect(() => {
|
const styles = useStyles2(getStyles, searchBarHidden);
|
||||||
if (state.megaMenuOpen) {
|
|
||||||
setIsOpen(true);
|
|
||||||
}
|
|
||||||
}, [state.megaMenuOpen]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
<OverlayContainer>
|
<OverlayContainer>
|
||||||
<CSSTransition
|
<CSSTransition
|
||||||
nodeRef={ref}
|
nodeRef={ref}
|
||||||
in={isOpen}
|
in={isOpen}
|
||||||
unmountOnExit={true}
|
unmountOnExit={true}
|
||||||
classNames={animStyles.overlay}
|
classNames={animationStyles.overlay}
|
||||||
timeout={{ enter: animationSpeed, exit: 0 }}
|
timeout={{ enter: animationSpeed, exit: 0 }}
|
||||||
onExited={onClose}
|
|
||||||
>
|
>
|
||||||
<FocusScope contain autoFocus>
|
<FocusScope contain autoFocus>
|
||||||
<div data-testid="navbarmenu" ref={ref} {...overlayProps} {...dialogProps} className={styles.container}>
|
<DockedMegaMenu className={styles.menu} onClose={onClose} ref={ref} {...overlayProps} {...dialogProps} />
|
||||||
<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>
|
</FocusScope>
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
<CSSTransition
|
<CSSTransition
|
||||||
nodeRef={backdropRef}
|
nodeRef={backdropRef}
|
||||||
in={isOpen}
|
in={isOpen}
|
||||||
unmountOnExit={true}
|
unmountOnExit={true}
|
||||||
classNames={animStyles.backdrop}
|
classNames={animationStyles.backdrop}
|
||||||
timeout={{ enter: animationSpeed, exit: 0 }}
|
timeout={{ enter: animationSpeed, exit: 0 }}
|
||||||
>
|
>
|
||||||
<div ref={backdropRef} className={styles.backdrop} {...underlayProps} />
|
<div ref={backdropRef} className={styles.backdrop} {...underlayProps} />
|
||||||
</CSSTransition>
|
</CSSTransition>
|
||||||
</OverlayContainer>
|
</OverlayContainer>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
NavBarMenu.displayName = 'NavBarMenu';
|
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => {
|
const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => {
|
||||||
const topPosition = (searchBarHidden ? TOP_BAR_LEVEL_HEIGHT : TOP_BAR_LEVEL_HEIGHT * 2) + 1;
|
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,
|
top: topPosition,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
container: css({
|
menu: css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
left: 0,
|
left: 0,
|
||||||
marginRight: theme.spacing(1.5),
|
|
||||||
right: 0,
|
right: 0,
|
||||||
// Needs to below navbar should we change the navbarFixed? add add a new level?
|
// Needs to below navbar should we change the navbarFixed? add add a new level?
|
||||||
zIndex: theme.zIndex.modal,
|
zIndex: theme.zIndex.modal,
|
||||||
@ -135,32 +108,17 @@ const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => {
|
|||||||
flex: '1 1 0',
|
flex: '1 1 0',
|
||||||
|
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up('md')]: {
|
||||||
borderRight: `1px solid ${theme.colors.border.weak}`,
|
|
||||||
right: 'unset',
|
right: 'unset',
|
||||||
|
borderRight: `1px solid ${theme.colors.border.weak}`,
|
||||||
top: topPosition,
|
top: topPosition,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
content: css({
|
wrapper: css({
|
||||||
display: 'flex',
|
position: 'fixed',
|
||||||
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',
|
display: 'grid',
|
||||||
gridAutoRows: `minmax(${theme.spacing(6)}, auto)`,
|
gridAutoFlow: 'column',
|
||||||
gridTemplateColumns: `minmax(${MENU_WIDTH}, auto)`,
|
height: '100%',
|
||||||
minWidth: MENU_WIDTH,
|
zIndex: theme.zIndex.sidemenu,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
@ -1,24 +1,26 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
|
import { DOMAttributes } from '@react-types/shared';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
import React from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { useTheme2 } from '@grafana/ui';
|
import { CustomScrollbar, Icon, IconButton, useStyles2 } from '@grafana/ui';
|
||||||
import { useSelector } from 'app/types';
|
import { useSelector } from 'app/types';
|
||||||
|
|
||||||
import { NavBarMenu } from './NavBarMenu';
|
import { NavBarMenuItemWrapper } from './NavBarMenuItemWrapper';
|
||||||
import { enrichWithInteractionTracking, getActiveItem } from './utils';
|
import { enrichWithInteractionTracking, getActiveItem } from './utils';
|
||||||
|
|
||||||
export interface Props {
|
export const MENU_WIDTH = '350px';
|
||||||
|
|
||||||
|
export interface Props extends DOMAttributes {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
searchBarHidden?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DockedMegaMenu = React.memo<Props>(({ onClose, searchBarHidden }) => {
|
export const DockedMegaMenu = React.memo(
|
||||||
|
forwardRef<HTMLDivElement, Props>(({ onClose, ...restProps }, ref) => {
|
||||||
const navBarTree = useSelector((state) => state.navBarTree);
|
const navBarTree = useSelector((state) => state.navBarTree);
|
||||||
const theme = useTheme2();
|
const styles = useStyles2(getStyles);
|
||||||
const styles = getStyles(theme);
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
const navTree = cloneDeep(navBarTree);
|
const navTree = cloneDeep(navBarTree);
|
||||||
@ -31,20 +33,55 @@ export const DockedMegaMenu = React.memo<Props>(({ onClose, searchBarHidden }) =
|
|||||||
const activeItem = getActiveItem(navItems, location.pathname);
|
const activeItem = getActiveItem(navItems, location.pathname);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.menuWrapper}>
|
<div data-testid="navbarmenu" ref={ref} {...restProps}>
|
||||||
<NavBarMenu activeItem={activeItem} navItems={navItems} onClose={onClose} searchBarHidden={searchBarHidden} />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
})
|
||||||
|
);
|
||||||
|
|
||||||
DockedMegaMenu.displayName = 'DockedMegaMenu';
|
DockedMegaMenu.displayName = 'DockedMegaMenu';
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
menuWrapper: css({
|
content: css({
|
||||||
position: 'fixed',
|
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',
|
display: 'grid',
|
||||||
gridAutoFlow: 'column',
|
gridAutoRows: `minmax(${theme.spacing(6)}, auto)`,
|
||||||
height: '100%',
|
gridTemplateColumns: `minmax(${MENU_WIDTH}, auto)`,
|
||||||
zIndex: theme.zIndex.sidemenu,
|
minWidth: MENU_WIDTH,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
@ -14,6 +14,8 @@ import { TOP_BAR_LEVEL_HEIGHT } from '../types';
|
|||||||
|
|
||||||
import { NavToolbarSeparator } from './NavToolbarSeparator';
|
import { NavToolbarSeparator } from './NavToolbarSeparator';
|
||||||
|
|
||||||
|
export const TOGGLE_BUTTON_ID = 'mega-menu-toggle';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
onToggleSearchBar(): void;
|
onToggleSearchBar(): void;
|
||||||
onToggleMegaMenu(): void;
|
onToggleMegaMenu(): void;
|
||||||
@ -41,6 +43,7 @@ export function NavToolbar({
|
|||||||
<div data-testid={Components.NavToolbar.container} className={styles.pageToolbar}>
|
<div data-testid={Components.NavToolbar.container} className={styles.pageToolbar}>
|
||||||
<div className={styles.menuButton}>
|
<div className={styles.menuButton}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
id={TOGGLE_BUTTON_ID}
|
||||||
name="bars"
|
name="bars"
|
||||||
tooltip={t('navigation.toolbar.toggle-menu', 'Toggle menu')}
|
tooltip={t('navigation.toolbar.toggle-menu', 'Toggle menu')}
|
||||||
tooltipPlacement="bottom"
|
tooltipPlacement="bottom"
|
||||||
|
Loading…
Reference in New Issue
Block a user