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:
@@ -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,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user