From a6cf7598a15f8a409bb3dd8d7cc65555b09b4363 Mon Sep 17 00:00:00 2001 From: hiba-aldalaty Date: Thu, 4 Nov 2021 13:59:55 +0000 Subject: [PATCH] Make sidebar expand on hover by default to prevent adopter impact Signed-off-by: hiba-aldalaty --- packages/app/src/components/Root/Root.tsx | 2 - .../src/layout/Sidebar/Bar.test.tsx | 6 +- .../src/layout/Sidebar/Bar.tsx | 103 +++++++++++++----- .../src/layout/Sidebar/Items.test.tsx | 2 +- .../src/layout/Sidebar/Sidebar.stories.tsx | 12 ++ .../src/layout/Sidebar/config.ts | 6 +- .../Sidebar/icons/CatalogSidebarLogo.tsx | 1 + packages/theme/src/themes.ts | 6 - packages/theme/src/types.ts | 3 - 9 files changed, 100 insertions(+), 41 deletions(-) diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index b0318cdaf8..7a666a020a 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -40,7 +40,6 @@ import { SidebarDivider, SidebarSpace, SidebarScrollWrapper, - SidebarExpandButton, } from '@backstage/core-components'; const useSidebarLogoStyles = makeStyles({ @@ -99,7 +98,6 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( - diff --git a/packages/core-components/src/layout/Sidebar/Bar.test.tsx b/packages/core-components/src/layout/Sidebar/Bar.test.tsx index 74a4fb468e..2039210c2d 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.test.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.test.tsx @@ -28,7 +28,7 @@ import { SubmenuItem } from './SubmenuItem'; async function renderScalableSidebar() { await renderInTestApp( - + {}} to="/search" /> { userEvent.click(screen.getByTestId('sidebar-expand-button')); expect(await screen.findByText('Create...')).toBeInTheDocument(); }); + it('Sidebar should not show expanded items when hovered on', async () => { + userEvent.hover(screen.getByTestId('sidebar-root')); + expect(await screen.queryByText('Create...')).not.toBeInTheDocument(); + }); }); describe('Submenu Items', () => { it('Extended sidebar with submenu content hidden by default', async () => { diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index 23a4ae1286..1fb8dc5fc6 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -17,12 +17,7 @@ import { makeStyles } from '@material-ui/core/styles'; import useMediaQuery from '@material-ui/core/useMediaQuery'; import clsx from 'clsx'; -import React, { - useState, - useContext, - PropsWithChildren, - useEffect, -} from 'react'; +import React, { useState, useContext, PropsWithChildren, useRef } from 'react'; import { sidebarConfig, SidebarContext } from './config'; import { BackstageTheme } from '@backstage/theme'; import { SidebarPinStateContext } from './Page'; @@ -88,24 +83,85 @@ const useStyles = makeStyles( { name: 'BackstageSidebar' }, ); -export function Sidebar({ children }: PropsWithChildren<{}>) { +enum State { + Closed, + Idle, + Open, +} + +type Props = { + openDelayMs?: number; + closeDelayMs?: number; + disableExpandOnHover?: boolean; +}; + +export function Sidebar(props: PropsWithChildren) { + const { + disableExpandOnHover = false, + openDelayMs = sidebarConfig.defaultOpenDelayMs, + closeDelayMs = sidebarConfig.defaultCloseDelayMs, + children, + } = props; const classes = useStyles(); - + const isSmallScreen = useMediaQuery(theme => + theme.breakpoints.down('md'), + ); + const [state, setState] = useState(State.Closed); + const hoverTimerRef = useRef(); const { isPinned } = useContext(SidebarPinStateContext); - const [isOpen, setIsOpen] = useState(isPinned); - useEffect(() => { + const handleOpen = () => { if (isPinned) { - setIsOpen(true); + return; } - }, [isPinned]); + if (hoverTimerRef.current) { + clearTimeout(hoverTimerRef.current); + hoverTimerRef.current = undefined; + } + if (state !== State.Open && !isSmallScreen) { + hoverTimerRef.current = window.setTimeout(() => { + hoverTimerRef.current = undefined; + setState(State.Open); + }, openDelayMs); + + setState(State.Idle); + } + }; + + const handleClose = () => { + if (isPinned) { + return; + } + if (hoverTimerRef.current) { + clearTimeout(hoverTimerRef.current); + hoverTimerRef.current = undefined; + } + if (state === State.Idle) { + setState(State.Closed); + } else if (state === State.Open) { + hoverTimerRef.current = window.setTimeout(() => { + hoverTimerRef.current = undefined; + setState(State.Closed); + }, closeDelayMs); + } + }; + + const isOpen = (state === State.Open && !isSmallScreen) || isPinned; return ( -
+
{} : handleOpen} + onFocus={disableExpandOnHover ? () => {} : handleOpen} + onMouseLeave={disableExpandOnHover ? () => {} : handleClose} + onBlur={disableExpandOnHover ? () => {} : handleClose} + >
) { export const SidebarExpandButton = () => { const classes = useStyles(); + const { isOpen, handleOpen, handleClose } = useContext(SidebarContext); + const { isPinned } = useContext(SidebarPinStateContext); const isSmallScreen = useMediaQuery(theme => theme.breakpoints.down('md'), ); - const { isPinned } = useContext(SidebarPinStateContext); - const { isOpen, setIsOpen } = useContext(SidebarContext); - - const openDelayMs = sidebarConfig.defaultOpenDelayMs; - const closeDelayMs = sidebarConfig.defaultCloseDelayMs; const handleClick = () => { - if (isPinned || isSmallScreen) { - return; + if (isOpen) { + handleClose(); + } else { + handleOpen(); } - const delayMs = isOpen ? openDelayMs : closeDelayMs; - setTimeout(async () => { - setIsOpen(!isOpen); - }, delayMs); }; - if (isSmallScreen || isPinned) { + if (isPinned || isSmallScreen) { return null; } diff --git a/packages/core-components/src/layout/Sidebar/Items.test.tsx b/packages/core-components/src/layout/Sidebar/Items.test.tsx index 34bc454fec..f4d699d09c 100644 --- a/packages/core-components/src/layout/Sidebar/Items.test.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.test.tsx @@ -47,7 +47,7 @@ async function renderSidebar() { , ); - userEvent.click(screen.getByTestId('sidebar-expand-button')); + userEvent.hover(screen.getByTestId('sidebar-root')); } describe('Items', () => { diff --git a/packages/core-components/src/layout/Sidebar/Sidebar.stories.tsx b/packages/core-components/src/layout/Sidebar/Sidebar.stories.tsx index 18bfb120e3..c95460553c 100644 --- a/packages/core-components/src/layout/Sidebar/Sidebar.stories.tsx +++ b/packages/core-components/src/layout/Sidebar/Sidebar.stories.tsx @@ -53,6 +53,18 @@ const handleSearch = (input: string) => { export const SampleSidebar = () => ( + + + + + + + + +); + +export const SampleScalableSidebar = () => ( + >; + handleOpen: () => any; + handleClose: () => any; }; export const SidebarContext = createContext({ isOpen: false, - setIsOpen: () => {}, + handleOpen: () => {}, + handleClose: () => {}, }); export type ItemWithSubmenuContextType = { diff --git a/packages/core-components/src/layout/Sidebar/icons/CatalogSidebarLogo.tsx b/packages/core-components/src/layout/Sidebar/icons/CatalogSidebarLogo.tsx index 02415d6fbc..fe37c9c74f 100644 --- a/packages/core-components/src/layout/Sidebar/icons/CatalogSidebarLogo.tsx +++ b/packages/core-components/src/layout/Sidebar/icons/CatalogSidebarLogo.tsx @@ -24,6 +24,7 @@ export const CatalogSidebarLogo = () => { height="21" fill="none" viewBox="0 0 20 21" + fontSize="small" >