Skip to content

Commit

Permalink
Navigation: Collapsible section nav implementation (#55995)
Browse files Browse the repository at this point in the history
* initial collapsible section nav implementation

* fix unit tests

* automatically collapse sectionnav when below lg size

* fix unit tests

* only register 1 event listener each time

* fix display name for SectionNavToggle
  • Loading branch information
ashharrison90 committed Oct 4, 2022
1 parent 4087ad4 commit 317b353
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 24 deletions.
16 changes: 0 additions & 16 deletions public/app/core/components/MegaMenu/NavBarMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@ import React, { useEffect, useRef, useState } from 'react';
import CSSTransition from 'react-transition-group/CSSTransition';

import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { CustomScrollbar, Icon, IconButton, useTheme2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext';

import { TOP_BAR_LEVEL_HEIGHT } from '../AppChrome/types';
import { NavBarToggle } from '../NavBar/NavBarToggle';

import { NavBarMenuItemWrapper } from './NavBarMenuItemWrapper';

Expand Down Expand Up @@ -75,14 +73,6 @@ export function NavBarMenu({ activeItem, navItems, searchBarHidden, onClose }: P
variant="secondary"
/>
</div>
<NavBarToggle
className={styles.menuCollapseIcon}
isExpanded={true}
onClick={() => {
reportInteraction('grafana_navigation_collapsed');
onMenuClose();
}}
/>
<nav className={styles.content}>
<CustomScrollbar showScrollIndicators hideHorizontalTrack>
<ul className={styles.itemList}>
Expand Down Expand Up @@ -162,12 +152,6 @@ const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => {
gridTemplateColumns: `minmax(${MENU_WIDTH}, auto)`,
minWidth: MENU_WIDTH,
}),
menuCollapseIcon: css({
position: 'absolute',
top: '20px',
right: '0px',
transform: `translateX(50%)`,
}),
};
};

Expand Down
49 changes: 46 additions & 3 deletions public/app/core/components/PageNew/Page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// Libraries
import { css, cx } from '@emotion/css';
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useLocalStorage } from 'react-use';

import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
import { CustomScrollbar, useStyles2, useTheme2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext';

import { Footer } from '../Footer/Footer';
Expand All @@ -15,6 +16,7 @@ import { PageContents } from './PageContents';
import { PageHeader } from './PageHeader';
import { PageTabs } from './PageTabs';
import { SectionNav } from './SectionNav';
import { SectionNavToggle } from './SectionNavToggle';

export const Page: PageType = ({
navId,
Expand All @@ -29,14 +31,29 @@ export const Page: PageType = ({
scrollRef,
...otherProps
}) => {
const theme = useTheme2();
const styles = useStyles2(getStyles);
const navModel = usePageNav(navId, oldNavProp);
const { chrome } = useGrafana();

const isSmallScreen = window.matchMedia(`(max-width: ${theme.breakpoints.values.lg}px)`).matches;
const [navExpandedPreference, setNavExpandedPreference] = useLocalStorage<boolean>(
'grafana.sectionNav.expanded',
!isSmallScreen
);
const [isNavExpanded, setNavExpanded] = useState(!isSmallScreen && navExpandedPreference);

usePageTitle(navModel, pageNav);

const pageHeaderNav = pageNav ?? navModel?.node;

useEffect(() => {
const mediaQuery = window.matchMedia(`(max-width: ${theme.breakpoints.values.lg}px)`);
const onMediaQueryChange = (e: MediaQueryListEvent) => setNavExpanded(e.matches ? false : navExpandedPreference);
mediaQuery.addEventListener('change', onMediaQueryChange);
return () => mediaQuery.removeEventListener('change', onMediaQueryChange);
}, [navExpandedPreference, theme.breakpoints.values.lg]);

useEffect(() => {
if (navModel) {
chrome.update({
Expand All @@ -46,11 +63,25 @@ export const Page: PageType = ({
}
}, [navModel, pageNav, chrome]);

const onToggleSectionNav = () => {
setNavExpandedPreference(!isNavExpanded);
setNavExpanded(!isNavExpanded);
};

return (
<div className={cx(styles.wrapper, className)} {...otherProps}>
{layout === PageLayoutType.Standard && (
<div className={styles.panes}>
{navModel && <SectionNav model={navModel} />}
{navModel && (
<>
<SectionNav model={navModel} isExpanded={Boolean(isNavExpanded)} />
<SectionNavToggle
className={styles.collapseIcon}
isExpanded={Boolean(isNavExpanded)}
onClick={onToggleSectionNav}
/>
</>
)}
<div className={styles.pageContent}>
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
<div className={styles.pageInner}>
Expand Down Expand Up @@ -94,6 +125,18 @@ const getStyles = (theme: GrafanaTheme2) => {
: '0 0.6px 1.5px -1px rgb(0 0 0 / 8%),0 2px 4px rgb(0 0 0 / 6%),0 5px 10px -1px rgb(0 0 0 / 5%)';

return {
collapseIcon: css({
border: `1px solid ${theme.colors.border.weak}`,
transform: 'translateX(50%)',
top: theme.spacing(8),
right: theme.spacing(-1),

[theme.breakpoints.down('md')]: {
left: '50%',
transform: 'translate(-50%, 50%) rotate(90deg)',
top: theme.spacing(2),
},
}),
wrapper: css`
height: 100%;
display: flex;
Expand Down
24 changes: 21 additions & 3 deletions public/app/core/components/PageNew/SectionNav.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { css } from '@emotion/css';
import { css, cx } from '@emotion/css';
import React from 'react';

import { NavModel, GrafanaTheme2 } from '@grafana/data';
Expand All @@ -8,17 +8,22 @@ import { SectionNavItem } from './SectionNavItem';

export interface Props {
model: NavModel;
isExpanded: boolean;
}

export function SectionNav({ model }: Props) {
export function SectionNav({ model, isExpanded }: Props) {
const styles = useStyles2(getStyles);

if (!Boolean(model.main?.children?.length)) {
return null;
}

return (
<nav className={styles.nav}>
<nav
className={cx(styles.nav, {
[styles.navExpanded]: isExpanded,
})}
>
<CustomScrollbar showScrollIndicators>
<div className={styles.items} role="tablist">
<SectionNavItem item={model.main} />
Expand All @@ -35,14 +40,27 @@ const getStyles = (theme: GrafanaTheme2) => {
flexDirection: 'column',
background: theme.colors.background.canvas,
flexShrink: 0,
transition: theme.transitions.create(['width', 'max-height']),
[theme.breakpoints.up('md')]: {
width: 0,
},
[theme.breakpoints.down('md')]: {
maxHeight: 0,
},
}),
navExpanded: css({
[theme.breakpoints.up('md')]: {
width: '250px',
},
[theme.breakpoints.down('md')]: {
maxHeight: '50vh',
},
}),
items: css({
display: 'flex',
flexDirection: 'column',
padding: theme.spacing(4.5, 1, 2, 2),
minWidth: '250px',
}),
};
};
39 changes: 39 additions & 0 deletions public/app/core/components/PageNew/SectionNavToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { css } from '@emotion/css';
import classnames from 'classnames';
import React from 'react';

import { GrafanaTheme2 } from '@grafana/data';
import { IconButton, useTheme2 } from '@grafana/ui';

export interface Props {
className?: string;
isExpanded: boolean;
onClick: () => void;
}

export const SectionNavToggle = ({ className, isExpanded, onClick }: Props) => {
const theme = useTheme2();
const styles = getStyles(theme);

return (
<IconButton
aria-label={isExpanded ? 'Close section navigation' : 'Open section navigation'}
name={isExpanded ? 'angle-left' : 'angle-right'}
className={classnames(className, styles.icon)}
size="xl"
onClick={onClick}
/>
);
};

SectionNavToggle.displayName = 'SectionNavToggle';

const getStyles = (theme: GrafanaTheme2) => ({
icon: css({
backgroundColor: theme.colors.background.secondary,
border: `1px solid ${theme.colors.border.weak}`,
borderRadius: '50%',
marginRight: 0,
zIndex: 1,
}),
});
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('VersionSettings', () => {
// Need to use delay: null here to work with fakeTimers
// see https://github.com/testing-library/user-event/issues/833
user = userEvent.setup({ delay: null });
jest.resetAllMocks();
jest.clearAllMocks();
jest.useFakeTimers();
});

Expand Down Expand Up @@ -103,7 +103,7 @@ describe('VersionSettings', () => {
historySrv.getHistoryList.mockResolvedValue(versions.slice(0, VERSIONS_FETCH_LIMIT - 5));
setup();

expect(screen.queryByRole('button', { name: /show more versions|/i })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /show more versions/i })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /compare versions/i })).not.toBeInTheDocument();

await waitFor(() => expect(screen.getByRole('table')).toBeInTheDocument());
Expand Down

0 comments on commit 317b353

Please sign in to comment.