DockedMegaMenu: Keep undock button (#78461)

* dock undock smoothly

* handle keyboard focus

* use ref instead of state

* run i18n:extract

* undo this change

* make dock/undock first button to focus

* only focus when going to docked, add comment

* minor tweaks
pull/78589/head
Ashley Harrison 2 years ago committed by GitHub
parent b5d1c8874b
commit 4247696402
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      public/app/core/components/AppChrome/AppChrome.tsx
  2. 13
      public/app/core/components/AppChrome/AppChromeMenu.tsx
  3. 28
      public/app/core/components/AppChrome/DockedMegaMenu/MegaMenu.tsx
  4. 20
      public/app/core/components/AppChrome/NavToolbar/NavToolbar.tsx
  5. 3
      public/locales/de-DE/grafana.json
  6. 3
      public/locales/en-US/grafana.json
  7. 3
      public/locales/es-ES/grafana.json
  8. 3
      public/locales/fr-FR/grafana.json
  9. 3
      public/locales/pseudo-LOCALE/grafana.json
  10. 3
      public/locales/zh-Hans/grafana.json

@ -93,7 +93,7 @@ export function AppChrome({ children }: Props) {
</main> </main>
{!state.chromeless && ( {!state.chromeless && (
<> <>
{config.featureToggles.dockedMegaMenu ? ( {config.featureToggles.dockedMegaMenu && state.megaMenu !== 'docked' ? (
<AppChromeMenu /> <AppChromeMenu />
) : ( ) : (
<MegaMenu searchBarHidden={searchBarHidden} onClose={() => chrome.setMegaMenu('closed')} /> <MegaMenu searchBarHidden={searchBarHidden} onClose={() => chrome.setMegaMenu('closed')} />

@ -2,7 +2,7 @@ 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, { useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import CSSTransition from 'react-transition-group/CSSTransition'; import CSSTransition from 'react-transition-group/CSSTransition';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
@ -20,11 +20,18 @@ export function AppChromeMenu({}: Props) {
const theme = useTheme2(); const theme = useTheme2();
const { chrome } = useGrafana(); const { chrome } = useGrafana();
const state = chrome.useState(); const state = chrome.useState();
const prevMegaMenuState = useRef(state.megaMenu);
const searchBarHidden = state.searchBarHidden || state.kioskMode === KioskMode.TV; const searchBarHidden = state.searchBarHidden || state.kioskMode === KioskMode.TV;
useEffect(() => {
prevMegaMenuState.current = state.megaMenu;
}, [state.megaMenu]);
const ref = useRef(null); const ref = useRef(null);
const backdropRef = useRef(null); const backdropRef = useRef(null);
const animationSpeed = theme.transitions.duration.shortest; // we don't want to show the opening animation when transitioning between docked + open
const animationSpeed =
prevMegaMenuState.current === 'docked' && state.megaMenu === 'open' ? 0 : theme.transitions.duration.shortest;
const animationStyles = useStyles2(getAnimStyles, animationSpeed); const animationStyles = useStyles2(getAnimStyles, animationSpeed);
const isOpen = state.megaMenu === 'open'; const isOpen = state.megaMenu === 'open';
@ -57,7 +64,7 @@ export function AppChromeMenu({}: Props) {
classNames={animationStyles.overlay} classNames={animationStyles.overlay}
timeout={{ enter: animationSpeed, exit: 0 }} timeout={{ enter: animationSpeed, exit: 0 }}
> >
<FocusScope contain autoFocus> <FocusScope contain autoFocus restoreFocus>
<MegaMenu className={styles.menu} onClose={onClose} ref={ref} {...overlayProps} {...dialogProps} /> <MegaMenu className={styles.menu} onClose={onClose} ref={ref} {...overlayProps} {...dialogProps} />
</FocusScope> </FocusScope>
</CSSTransition> </CSSTransition>

@ -35,7 +35,12 @@ export const MegaMenu = React.memo(
const activeItem = getActiveItem(navItems, location.pathname); const activeItem = getActiveItem(navItems, location.pathname);
const handleDockedMenu = () => { const handleDockedMenu = () => {
chrome.setMegaMenu(state.megaMenu === 'docked' ? 'closed' : 'docked'); chrome.setMegaMenu(state.megaMenu === 'docked' ? 'open' : 'docked');
// refocus on dock/undock button when changing state
setTimeout(() => {
document.getElementById('dock-menu-button')?.focus();
});
}; };
return ( return (
@ -54,21 +59,26 @@ export const MegaMenu = React.memo(
<CustomScrollbar showScrollIndicators hideHorizontalTrack> <CustomScrollbar showScrollIndicators hideHorizontalTrack>
<ul className={styles.itemList}> <ul className={styles.itemList}>
{navItems.map((link, index) => ( {navItems.map((link, index) => (
<Stack key={link.text} direction="row" alignItems="center"> <Stack key={link.text} direction={index === 0 ? 'row-reverse' : 'row'} alignItems="center">
<MegaMenuItem {index === 0 && (
link={link}
onClick={state.megaMenu === 'open' ? onClose : undefined}
activeItem={activeItem}
/>
{index === 0 && Boolean(state.megaMenu === 'open') && (
<IconButton <IconButton
id="dock-menu-button"
className={styles.dockMenuButton} className={styles.dockMenuButton}
tooltip={t('navigation.megamenu.dock', 'Dock menu')} tooltip={
state.megaMenu === 'docked'
? t('navigation.megamenu.undock', 'Undock menu')
: t('navigation.megamenu.dock', 'Dock menu')
}
name="web-section-alt" name="web-section-alt"
onClick={handleDockedMenu} onClick={handleDockedMenu}
variant="secondary" variant="secondary"
/> />
)} )}
<MegaMenuItem
link={link}
onClick={state.megaMenu === 'open' ? onClose : undefined}
activeItem={activeItem}
/>
</Stack> </Stack>
))} ))}
</ul> </ul>

@ -1,10 +1,11 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React from 'react'; import React, { useState } from 'react';
import { GrafanaTheme2, NavModelItem } from '@grafana/data'; import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { Components } from '@grafana/e2e-selectors'; import { Components } from '@grafana/e2e-selectors';
import { Icon, IconButton, ToolbarButton, useStyles2 } from '@grafana/ui'; import { Icon, IconButton, ToolbarButton, useStyles2, useTheme2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext'; import { useGrafana } from 'app/core/context/GrafanaContext';
import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange';
import { t } from 'app/core/internationalization'; import { t } from 'app/core/internationalization';
import { HOME_NAV_ID } from 'app/core/reducers/navModel'; import { HOME_NAV_ID } from 'app/core/reducers/navModel';
import { useSelector } from 'app/types'; import { useSelector } from 'app/types';
@ -39,9 +40,22 @@ export function NavToolbar({
const { chrome } = useGrafana(); const { chrome } = useGrafana();
const state = chrome.useState(); const state = chrome.useState();
const homeNav = useSelector((state) => state.navIndex)[HOME_NAV_ID]; const homeNav = useSelector((state) => state.navIndex)[HOME_NAV_ID];
const theme = useTheme2();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const breadcrumbs = buildBreadcrumbs(sectionNav, pageNav, homeNav); const breadcrumbs = buildBreadcrumbs(sectionNav, pageNav, homeNav);
const dockMenuBreakpoint = theme.breakpoints.values.xl;
const [isTooSmallForDockedMenu, setIsTooSmallForDockedMenu] = useState(
!window.matchMedia(`(min-width: ${dockMenuBreakpoint}px)`).matches
);
useMediaQueryChange({
breakpoint: dockMenuBreakpoint,
onChange: (e) => {
setIsTooSmallForDockedMenu(!e.matches);
},
});
return ( return (
<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}>
@ -49,7 +63,7 @@ export function NavToolbar({
id={TOGGLE_BUTTON_ID} id={TOGGLE_BUTTON_ID}
name="bars" name="bars"
tooltip={ tooltip={
state.megaMenu === 'closed' state.megaMenu === 'closed' || (state.megaMenu === 'docked' && isTooSmallForDockedMenu)
? t('navigation.toolbar.open-menu', 'Open menu') ? t('navigation.toolbar.open-menu', 'Open menu')
: t('navigation.toolbar.close-menu', 'Close menu') : t('navigation.toolbar.close-menu', 'Close menu')
} }

@ -935,7 +935,8 @@
}, },
"megamenu": { "megamenu": {
"close": "Menü schließen", "close": "Menü schließen",
"dock": "Menü andocken" "dock": "Menü andocken",
"undock": ""
}, },
"toolbar": { "toolbar": {
"close-menu": "Menü schließen", "close-menu": "Menü schließen",

@ -935,7 +935,8 @@
}, },
"megamenu": { "megamenu": {
"close": "Close menu", "close": "Close menu",
"dock": "Dock menu" "dock": "Dock menu",
"undock": "Undock menu"
}, },
"toolbar": { "toolbar": {
"close-menu": "Close menu", "close-menu": "Close menu",

@ -941,7 +941,8 @@
}, },
"megamenu": { "megamenu": {
"close": "Cerrar menú", "close": "Cerrar menú",
"dock": "Menú base" "dock": "Menú base",
"undock": ""
}, },
"toolbar": { "toolbar": {
"close-menu": "Cerrar menú", "close-menu": "Cerrar menú",

@ -941,7 +941,8 @@
}, },
"megamenu": { "megamenu": {
"close": "Fermer le menu", "close": "Fermer le menu",
"dock": "Ancrer le menu" "dock": "Ancrer le menu",
"undock": ""
}, },
"toolbar": { "toolbar": {
"close-menu": "Fermer le menu", "close-menu": "Fermer le menu",

@ -935,7 +935,8 @@
}, },
"megamenu": { "megamenu": {
"close": "Cľőşę męʼnū", "close": "Cľőşę męʼnū",
"dock": "Đőčĸ męʼnū" "dock": "Đőčĸ męʼnū",
"undock": "Ůʼnđőčĸ męʼnū"
}, },
"toolbar": { "toolbar": {
"close-menu": "Cľőşę męʼnū", "close-menu": "Cľőşę męʼnū",

@ -929,7 +929,8 @@
}, },
"megamenu": { "megamenu": {
"close": "关闭菜单", "close": "关闭菜单",
"dock": "停靠菜单" "dock": "停靠菜单",
"undock": ""
}, },
"toolbar": { "toolbar": {
"close-menu": "关闭菜单", "close-menu": "关闭菜单",

Loading…
Cancel
Save