diff --git a/public/app/core/components/NavBar/Next/NavBarMenu.tsx b/public/app/core/components/NavBar/Next/NavBarMenu.tsx index 3c7bfe2b442..bad0b3fe435 100644 --- a/public/app/core/components/NavBar/Next/NavBarMenu.tsx +++ b/public/app/core/components/NavBar/Next/NavBarMenu.tsx @@ -1,9 +1,10 @@ import React, { useRef } from 'react'; +import CSSTransition from 'react-transition-group/CSSTransition'; import { GrafanaTheme2, NavModelItem } from '@grafana/data'; -import { CollapsableSection, CustomScrollbar, Icon, IconName, useStyles2 } from '@grafana/ui'; +import { CollapsableSection, CustomScrollbar, Icon, IconName, useStyles2, useTheme2 } from '@grafana/ui'; import { FocusScope } from '@react-aria/focus'; import { useDialog } from '@react-aria/dialog'; -import { useOverlay } from '@react-aria/overlays'; +import { OverlayContainer, useOverlay } from '@react-aria/overlays'; import { css, cx } from '@emotion/css'; import { NavBarMenuItem } from './NavBarMenuItem'; import { NavBarItemWithoutMenu } from './NavBarItemWithoutMenu'; @@ -15,14 +16,18 @@ export interface Props { activeItem?: NavModelItem; isOpen: boolean; navItems: NavModelItem[]; + setMenuAnimationInProgress: (isInProgress: boolean) => void; onClose: () => void; } -export function NavBarMenu({ activeItem, isOpen, navItems, onClose }: Props) { - const styles = useStyles2(getStyles); +export function NavBarMenu({ activeItem, isOpen, navItems, onClose, setMenuAnimationInProgress }: Props) { + const theme = useTheme2(); + const styles = getStyles(theme); + const ANIMATION_DURATION = theme.transitions.duration.standard; + const animStyles = getAnimStyles(theme, ANIMATION_DURATION); const ref = useRef(null); const { dialogProps } = useDialog({}, ref); - const { overlayProps } = useOverlay( + const { overlayProps, underlayProps } = useOverlay( { isDismissable: true, isOpen, @@ -32,26 +37,50 @@ export function NavBarMenu({ activeItem, isOpen, navItems, onClose }: Props) { ); return ( -
+ - + setMenuAnimationInProgress(true)} + onExited={() => setMenuAnimationInProgress(false)} + appear={isOpen} + in={isOpen} + classNames={animStyles.overlay} + timeout={ANIMATION_DURATION} + > +
+ + +
+
-
+ +
+ + ); } NavBarMenu.displayName = 'NavBarMenu'; const getStyles = (theme: GrafanaTheme2) => ({ + backdrop: css({ + backdropFilter: 'blur(1px)', + backgroundColor: theme.components.overlay.background, + bottom: 0, + left: 0, + position: 'fixed', + right: 0, + top: 0, + zIndex: theme.zIndex.modalBackdrop, + }), container: css({ bottom: 0, display: 'flex', @@ -60,9 +89,9 @@ const getStyles = (theme: GrafanaTheme2) => ({ whiteSpace: 'nowrap', paddingTop: theme.spacing(1), marginRight: theme.spacing(1.5), - overflow: 'hidden', right: 0, - zIndex: theme.zIndex.sidemenu, + zIndex: theme.zIndex.modal, + position: 'fixed', top: 0, boxSizing: 'content-box', [theme.breakpoints.up('md')]: { @@ -83,9 +112,65 @@ const getStyles = (theme: GrafanaTheme2) => ({ position: 'absolute', top: '43px', right: '0px', + transform: `translateX(50%)`, }), }); +const getAnimStyles = (theme: GrafanaTheme2, animationDuration: number) => { + const commonTransition = { + transitionProperty: 'width, background-color, opacity', + transitionDuration: `${animationDuration}ms`, + transitionTimingFunction: theme.transitions.easing.easeInOut, + }; + + const overlayTransition = { + ...commonTransition, + transitionProperty: 'width, background-color, box-shadow', + }; + + const backdropTransition = { + ...commonTransition, + transitionProperty: 'opacity', + }; + + const overlayOpen = { + backgroundColor: theme.colors.background.canvas, + boxShadow: theme.shadows.z3, + width: '300px', + }; + + const overlayClosed = { + backgroundColor: theme.colors.background.primary, + boxShadow: 'none', + width: theme.spacing(7), + }; + + const backdropOpen = { + opacity: 1, + }; + + const backdropClosed = { + opacity: 0, + }; + + return { + backdrop: { + appear: css(backdropClosed), + appearActive: css(backdropTransition, backdropOpen), + appearDone: css(backdropOpen), + exit: css(backdropOpen), + exitActive: css(backdropTransition, backdropClosed), + }, + overlay: { + appear: css(overlayClosed), + appearActive: css(overlayTransition, overlayOpen), + appearDone: css(overlayOpen), + exit: css(overlayOpen), + exitActive: css(overlayTransition, overlayClosed), + }, + }; +}; + function NavItem({ link, activeItem, diff --git a/public/app/core/components/NavBar/Next/NavBarNext.tsx b/public/app/core/components/NavBar/Next/NavBarNext.tsx index 73ea07636ef..63cf25fcb27 100644 --- a/public/app/core/components/NavBar/Next/NavBarNext.tsx +++ b/public/app/core/components/NavBar/Next/NavBarNext.tsx @@ -1,10 +1,9 @@ import React, { useState } from 'react'; import { useLocation } from 'react-router-dom'; -import CSSTransition from 'react-transition-group/CSSTransition'; import { css, cx } from '@emotion/css'; import { cloneDeep } from 'lodash'; import { GrafanaTheme2, NavModelItem, NavSection } from '@grafana/data'; -import { Icon, IconName, useStyles2, useTheme2 } from '@grafana/ui'; +import { Icon, IconName, useTheme2 } from '@grafana/ui'; import { config, locationService } from '@grafana/runtime'; import { getKioskMode } from 'app/core/navigation/kiosk'; import { KioskMode, StoreState } from 'app/types'; @@ -41,7 +40,6 @@ export const NavBarNext = React.memo(() => { const navBarTree = useSelector((state: StoreState) => state.navBarTree); const theme = useTheme2(); const styles = getStyles(theme); - const animStyles = useStyles2(getAnimStyles); const location = useLocation(); const kiosk = getKioskMode(); const [showSwitcherModal, setShowSwitcherModal] = useState(false); @@ -60,6 +58,7 @@ export const NavBarNext = React.memo(() => { ); const activeItem = isSearchActive(location) ? searchItem : getActiveItem(navTree, location.pathname); const [menuOpen, setMenuOpen] = useState(false); + const [menuAnimationInProgress, setMenuAnimationInProgress] = useState(false); const [menuIdOpen, setMenuIdOpen] = useState(null); if (kiosk !== KioskMode.Off) { @@ -135,16 +134,17 @@ export const NavBarNext = React.memo(() => { {showSwitcherModal && } -
- + {(menuOpen || menuAnimationInProgress) && ( +
setMenuOpen(false)} /> - -
+
+ )}
); }); @@ -242,41 +242,3 @@ const getStyles = (theme: GrafanaTheme2) => ({ transform: `translateX(50%)`, }), }); - -const getAnimStyles = (theme: GrafanaTheme2) => { - const transitionProps = { - transitionProperty: 'width, background-color', - transitionDuration: '150ms', - transitionTimingFunction: 'ease-in-out', - }; - - const openStyles = { - backgroundColor: theme.colors.background.canvas, - width: '300px', - }; - - const closedStyles = { - backgroundColor: theme.colors.background.primary, - width: theme.spacing(7), - }; - - return { - enter: css({ - ...closedStyles, - }), - enterActive: css({ - ...transitionProps, - ...openStyles, - }), - enterDone: css({ - ...openStyles, - }), - exit: css({ - ...openStyles, - }), - exitActive: css({ - ...transitionProps, - ...closedStyles, - }), - }; -};