Make sidebar expand on hover by default to prevent adopter impact
Signed-off-by: hiba-aldalaty <hibaaldalaty@gmail.com>
This commit is contained in:
@@ -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<{}>) => (
|
||||
<SidebarDivider />
|
||||
<Shortcuts />
|
||||
<SidebarSpace />
|
||||
<SidebarExpandButton />
|
||||
<SidebarDivider />
|
||||
<SidebarSettings />
|
||||
</Sidebar>
|
||||
|
||||
@@ -28,7 +28,7 @@ import { SubmenuItem } from './SubmenuItem';
|
||||
|
||||
async function renderScalableSidebar() {
|
||||
await renderInTestApp(
|
||||
<Sidebar>
|
||||
<Sidebar disableExpandOnHover>
|
||||
<SidebarSearchField onSearch={() => {}} to="/search" />
|
||||
<SidebarItem
|
||||
icon={CatalogSidebarLogo}
|
||||
@@ -71,6 +71,10 @@ describe('Sidebar', () => {
|
||||
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 () => {
|
||||
|
||||
@@ -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<BackstageTheme>(
|
||||
{ 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<Props>) {
|
||||
const {
|
||||
disableExpandOnHover = false,
|
||||
openDelayMs = sidebarConfig.defaultOpenDelayMs,
|
||||
closeDelayMs = sidebarConfig.defaultCloseDelayMs,
|
||||
children,
|
||||
} = props;
|
||||
const classes = useStyles();
|
||||
|
||||
const isSmallScreen = useMediaQuery<BackstageTheme>(theme =>
|
||||
theme.breakpoints.down('md'),
|
||||
);
|
||||
const [state, setState] = useState(State.Closed);
|
||||
const hoverTimerRef = useRef<number>();
|
||||
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 (
|
||||
<div className={classes.root} data-testid="sidebar-root">
|
||||
<div
|
||||
className={classes.root}
|
||||
data-testid="sidebar-root"
|
||||
onMouseEnter={disableExpandOnHover ? () => {} : handleOpen}
|
||||
onFocus={disableExpandOnHover ? () => {} : handleOpen}
|
||||
onMouseLeave={disableExpandOnHover ? () => {} : handleClose}
|
||||
onBlur={disableExpandOnHover ? () => {} : handleClose}
|
||||
>
|
||||
<SidebarContext.Provider
|
||||
value={{
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
handleOpen,
|
||||
handleClose,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -122,26 +178,21 @@ export function Sidebar({ children }: PropsWithChildren<{}>) {
|
||||
|
||||
export const SidebarExpandButton = () => {
|
||||
const classes = useStyles();
|
||||
const { isOpen, handleOpen, handleClose } = useContext(SidebarContext);
|
||||
const { isPinned } = useContext(SidebarPinStateContext);
|
||||
const isSmallScreen = useMediaQuery<BackstageTheme>(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;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ async function renderSidebar() {
|
||||
<SidebarExpandButton />
|
||||
</Sidebar>,
|
||||
);
|
||||
userEvent.click(screen.getByTestId('sidebar-expand-button'));
|
||||
userEvent.hover(screen.getByTestId('sidebar-root'));
|
||||
}
|
||||
|
||||
describe('Items', () => {
|
||||
|
||||
@@ -53,6 +53,18 @@ const handleSearch = (input: string) => {
|
||||
|
||||
export const SampleSidebar = () => (
|
||||
<Sidebar>
|
||||
<SidebarSearchField onSearch={handleSearch} to="/search" />
|
||||
<SidebarDivider />
|
||||
<SidebarItem icon={HomeOutlinedIcon} to="#" text="Plugins" />
|
||||
<SidebarItem icon={AddCircleOutlineIcon} to="#" text="Create..." />
|
||||
<SidebarDivider />
|
||||
<SidebarIntro />
|
||||
<SidebarSpace />
|
||||
</Sidebar>
|
||||
);
|
||||
|
||||
export const SampleScalableSidebar = () => (
|
||||
<Sidebar disableExpandOnHover>
|
||||
<SidebarSearchField onSearch={handleSearch} to="/search" />
|
||||
<SidebarDivider />
|
||||
<SidebarItem
|
||||
|
||||
@@ -59,12 +59,14 @@ export const SIDEBAR_INTRO_LOCAL_STORAGE =
|
||||
|
||||
export type SidebarContextType = {
|
||||
isOpen: boolean;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
handleOpen: () => any;
|
||||
handleClose: () => any;
|
||||
};
|
||||
|
||||
export const SidebarContext = createContext<SidebarContextType>({
|
||||
isOpen: false,
|
||||
setIsOpen: () => {},
|
||||
handleOpen: () => {},
|
||||
handleClose: () => {},
|
||||
});
|
||||
|
||||
export type ItemWithSubmenuContextType = {
|
||||
|
||||
@@ -24,6 +24,7 @@ export const CatalogSidebarLogo = () => {
|
||||
height="21"
|
||||
fill="none"
|
||||
viewBox="0 0 20 21"
|
||||
fontSize="small"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
|
||||
@@ -81,9 +81,6 @@ export const lightTheme = createTheme({
|
||||
},
|
||||
submenu: {
|
||||
background: '#404040',
|
||||
indicator: '#9BF0E1',
|
||||
color: '#b5b5b5',
|
||||
selectedColor: '#FFF',
|
||||
},
|
||||
},
|
||||
pinSidebarButton: {
|
||||
@@ -165,9 +162,6 @@ export const darkTheme = createTheme({
|
||||
},
|
||||
submenu: {
|
||||
background: '#404040',
|
||||
indicator: '#9BF0E1',
|
||||
color: '#b5b5b5',
|
||||
selectedColor: '#FFF',
|
||||
},
|
||||
},
|
||||
pinSidebarButton: {
|
||||
|
||||
@@ -58,9 +58,6 @@ export type BackstagePaletteAdditions = {
|
||||
};
|
||||
submenu: {
|
||||
background: string;
|
||||
indicator: string;
|
||||
color: string;
|
||||
selectedColor: string;
|
||||
};
|
||||
};
|
||||
tabbar: {
|
||||
|
||||
Reference in New Issue
Block a user