Navigation: implement underlay (#47311)

* Navigation: implement underlay

* abstract out animation duration and add box-shadow

* rename underlay to backdrop, better syntax for composing css
This commit is contained in:
Ashley Harrison
2022-04-05 14:21:22 +01:00
committed by GitHub
parent 2fe6eca7a0
commit 8ede6161d4
2 changed files with 111 additions and 64 deletions
@@ -1,9 +1,10 @@
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import CSSTransition from 'react-transition-group/CSSTransition';
import { GrafanaTheme2, NavModelItem } from '@grafana/data'; 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 { FocusScope } from '@react-aria/focus';
import { useDialog } from '@react-aria/dialog'; 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 { css, cx } from '@emotion/css';
import { NavBarMenuItem } from './NavBarMenuItem'; import { NavBarMenuItem } from './NavBarMenuItem';
import { NavBarItemWithoutMenu } from './NavBarItemWithoutMenu'; import { NavBarItemWithoutMenu } from './NavBarItemWithoutMenu';
@@ -15,14 +16,18 @@ export interface Props {
activeItem?: NavModelItem; activeItem?: NavModelItem;
isOpen: boolean; isOpen: boolean;
navItems: NavModelItem[]; navItems: NavModelItem[];
setMenuAnimationInProgress: (isInProgress: boolean) => void;
onClose: () => void; onClose: () => void;
} }
export function NavBarMenu({ activeItem, isOpen, navItems, onClose }: Props) { export function NavBarMenu({ activeItem, isOpen, navItems, onClose, setMenuAnimationInProgress }: Props) {
const styles = useStyles2(getStyles); 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 ref = useRef(null);
const { dialogProps } = useDialog({}, ref); const { dialogProps } = useDialog({}, ref);
const { overlayProps } = useOverlay( const { overlayProps, underlayProps } = useOverlay(
{ {
isDismissable: true, isDismissable: true,
isOpen, isOpen,
@@ -32,26 +37,50 @@ export function NavBarMenu({ activeItem, isOpen, navItems, onClose }: Props) {
); );
return ( return (
<div data-testid="navbarmenu" className={styles.container}> <OverlayContainer>
<FocusScope contain restoreFocus autoFocus> <FocusScope contain restoreFocus autoFocus>
<nav className={styles.content} ref={ref} {...overlayProps} {...dialogProps}> <CSSTransition
<NavBarToggle className={styles.menuCollapseIcon} isExpanded={isOpen} onClick={onClose} /> onEnter={() => setMenuAnimationInProgress(true)}
<CustomScrollbar hideHorizontalTrack> onExited={() => setMenuAnimationInProgress(false)}
<ul className={styles.itemList}> appear={isOpen}
{navItems.map((link) => ( in={isOpen}
<NavItem link={link} onClose={onClose} activeItem={activeItem} key={link.text} /> classNames={animStyles.overlay}
))} timeout={ANIMATION_DURATION}
</ul> >
</CustomScrollbar> <div data-testid="navbarmenu" ref={ref} {...overlayProps} {...dialogProps} className={styles.container}>
</nav> <NavBarToggle className={styles.menuCollapseIcon} isExpanded={isOpen} onClick={onClose} />
<nav className={styles.content}>
<CustomScrollbar hideHorizontalTrack>
<ul className={styles.itemList}>
{navItems.map((link) => (
<NavItem link={link} onClose={onClose} activeItem={activeItem} key={link.text} />
))}
</ul>
</CustomScrollbar>
</nav>
</div>
</CSSTransition>
</FocusScope> </FocusScope>
</div> <CSSTransition appear={isOpen} in={isOpen} classNames={animStyles.backdrop} timeout={ANIMATION_DURATION}>
<div className={styles.backdrop} {...underlayProps} />
</CSSTransition>
</OverlayContainer>
); );
} }
NavBarMenu.displayName = 'NavBarMenu'; NavBarMenu.displayName = 'NavBarMenu';
const getStyles = (theme: GrafanaTheme2) => ({ 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({ container: css({
bottom: 0, bottom: 0,
display: 'flex', display: 'flex',
@@ -60,9 +89,9 @@ const getStyles = (theme: GrafanaTheme2) => ({
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
paddingTop: theme.spacing(1), paddingTop: theme.spacing(1),
marginRight: theme.spacing(1.5), marginRight: theme.spacing(1.5),
overflow: 'hidden',
right: 0, right: 0,
zIndex: theme.zIndex.sidemenu, zIndex: theme.zIndex.modal,
position: 'fixed',
top: 0, top: 0,
boxSizing: 'content-box', boxSizing: 'content-box',
[theme.breakpoints.up('md')]: { [theme.breakpoints.up('md')]: {
@@ -83,9 +112,65 @@ const getStyles = (theme: GrafanaTheme2) => ({
position: 'absolute', position: 'absolute',
top: '43px', top: '43px',
right: '0px', 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({ function NavItem({
link, link,
activeItem, activeItem,
@@ -1,10 +1,9 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import CSSTransition from 'react-transition-group/CSSTransition';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { GrafanaTheme2, NavModelItem, NavSection } from '@grafana/data'; 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 { config, locationService } from '@grafana/runtime';
import { getKioskMode } from 'app/core/navigation/kiosk'; import { getKioskMode } from 'app/core/navigation/kiosk';
import { KioskMode, StoreState } from 'app/types'; import { KioskMode, StoreState } from 'app/types';
@@ -41,7 +40,6 @@ export const NavBarNext = React.memo(() => {
const navBarTree = useSelector((state: StoreState) => state.navBarTree); const navBarTree = useSelector((state: StoreState) => state.navBarTree);
const theme = useTheme2(); const theme = useTheme2();
const styles = getStyles(theme); const styles = getStyles(theme);
const animStyles = useStyles2(getAnimStyles);
const location = useLocation(); const location = useLocation();
const kiosk = getKioskMode(); const kiosk = getKioskMode();
const [showSwitcherModal, setShowSwitcherModal] = useState(false); const [showSwitcherModal, setShowSwitcherModal] = useState(false);
@@ -60,6 +58,7 @@ export const NavBarNext = React.memo(() => {
); );
const activeItem = isSearchActive(location) ? searchItem : getActiveItem(navTree, location.pathname); const activeItem = isSearchActive(location) ? searchItem : getActiveItem(navTree, location.pathname);
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const [menuAnimationInProgress, setMenuAnimationInProgress] = useState(false);
const [menuIdOpen, setMenuIdOpen] = useState<string | null>(null); const [menuIdOpen, setMenuIdOpen] = useState<string | null>(null);
if (kiosk !== KioskMode.Off) { if (kiosk !== KioskMode.Off) {
@@ -135,16 +134,17 @@ export const NavBarNext = React.memo(() => {
</NavBarContext.Provider> </NavBarContext.Provider>
</nav> </nav>
{showSwitcherModal && <OrgSwitcher onDismiss={toggleSwitcherModal} />} {showSwitcherModal && <OrgSwitcher onDismiss={toggleSwitcherModal} />}
<div className={styles.menuWrapper}> {(menuOpen || menuAnimationInProgress) && (
<CSSTransition in={menuOpen} classNames={animStyles} timeout={150} unmountOnExit> <div className={styles.menuWrapper}>
<NavBarMenu <NavBarMenu
activeItem={activeItem} activeItem={activeItem}
isOpen={menuOpen} isOpen={menuOpen}
setMenuAnimationInProgress={setMenuAnimationInProgress}
navItems={[homeItem, searchItem, ...coreItems, ...pluginItems, ...configItems]} navItems={[homeItem, searchItem, ...coreItems, ...pluginItems, ...configItems]}
onClose={() => setMenuOpen(false)} onClose={() => setMenuOpen(false)}
/> />
</CSSTransition> </div>
</div> )}
</div> </div>
); );
}); });
@@ -242,41 +242,3 @@ const getStyles = (theme: GrafanaTheme2) => ({
transform: `translateX(50%)`, 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,
}),
};
};