Merge pull request #9539 from jonathan-ash/sidebar-custom-config

Added ability to override properties of Sidebar config and Sidebar Submenu config
This commit is contained in:
Ben Lambert
2022-03-31 13:08:38 +02:00
committed by GitHub
11 changed files with 401 additions and 277 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
`<Sidebar />` now accepts additional props `sidebarOptions` and `submenuOptions` to allow further customization
+14
View File
@@ -972,6 +972,12 @@ export type SidebarItemClassKey =
| 'arrows'
| 'selected';
// @public (undocumented)
export type SidebarOptions = {
drawerWidthClosed?: number;
drawerWidthOpen?: number;
};
// Warning: (ae-missing-release-tag) "SidebarPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -1001,6 +1007,8 @@ export type SidebarPinStateContextType = {
export type SidebarProps = {
openDelayMs?: number;
closeDelayMs?: number;
sidebarOptions?: SidebarOptions;
submenuOptions?: SubmenuOptions;
disableExpandOnHover?: boolean;
children?: React_2.ReactNode;
};
@@ -1181,6 +1189,12 @@ export type StructuredMetadataTableListClassKey = 'root';
// @public (undocumented)
export type StructuredMetadataTableNestedListClassKey = 'root';
// @public (undocumented)
export type SubmenuOptions = {
drawerWidthClosed?: number;
drawerWidthOpen?: number;
};
// Warning: (ae-forgotten-export) The symbol "SubvalueCellProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "SubvalueCell" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -21,17 +21,25 @@ import classnames from 'classnames';
import React, { useState, useContext, useRef } from 'react';
import Button from '@material-ui/core/Button';
import { sidebarConfig, SidebarContext } from './config';
import {
makeSidebarConfig,
makeSidebarSubmenuConfig,
SidebarConfig,
SidebarContext,
SidebarConfigContext,
SubmenuConfig,
SidebarOptions,
SubmenuOptions,
} from './config';
import { BackstageTheme } from '@backstage/theme';
import { SidebarPinStateContext, useContent } from './Page';
import { MobileSidebar } from './MobileSidebar';
/** @public */
export type SidebarClassKey = 'drawer' | 'drawerOpen';
const useStyles = makeStyles<BackstageTheme>(
const useStyles = makeStyles<BackstageTheme, { sidebarConfig: SidebarConfig }>(
theme => ({
drawer: {
drawer: props => ({
display: 'flex',
flexFlow: 'column nowrap',
alignItems: 'flex-start',
@@ -44,7 +52,7 @@ const useStyles = makeStyles<BackstageTheme>(
overflowX: 'hidden',
msOverflowStyle: 'none',
scrollbarWidth: 'none',
width: sidebarConfig.drawerWidthClosed,
width: props.sidebarConfig.drawerWidthClosed,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.shortest,
@@ -55,14 +63,14 @@ const useStyles = makeStyles<BackstageTheme>(
'&::-webkit-scrollbar': {
display: 'none',
},
},
drawerOpen: {
width: sidebarConfig.drawerWidthOpen,
}),
drawerOpen: props => ({
width: props.sidebarConfig.drawerWidthOpen,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.shorter,
}),
},
}),
visuallyHidden: {
top: 0,
position: 'absolute',
@@ -84,6 +92,15 @@ enum State {
/** @public */
export type SidebarProps = {
openDelayMs?: number;
closeDelayMs?: number;
sidebarOptions?: SidebarOptions;
submenuOptions?: SubmenuOptions;
disableExpandOnHover?: boolean;
children?: React.ReactNode;
};
export type DesktopSidebarProps = {
openDelayMs?: number;
closeDelayMs?: number;
disableExpandOnHover?: boolean;
@@ -100,14 +117,16 @@ export type SidebarProps = {
* @returns
* @internal
*/
const DesktopSidebar = (props: SidebarProps) => {
const DesktopSidebar = (props: DesktopSidebarProps) => {
const { sidebarConfig } = useContext(SidebarConfigContext);
const {
openDelayMs = sidebarConfig.defaultOpenDelayMs,
closeDelayMs = sidebarConfig.defaultCloseDelayMs,
disableExpandOnHover,
children,
} = props;
const classes = useStyles();
const classes = useStyles({ sidebarConfig });
const isSmallScreen = useMediaQuery<BackstageTheme>(
theme => theme.breakpoints.down('md'),
{ noSsr: true },
@@ -172,12 +191,7 @@ const DesktopSidebar = (props: SidebarProps) => {
return (
<div style={{}}>
<A11ySkipSidebar />
<SidebarContext.Provider
value={{
isOpen,
setOpen,
}}
>
<SidebarContext.Provider value={{ isOpen, setOpen }}>
<div
className={classes.root}
data-testid="sidebar-root"
@@ -205,25 +219,34 @@ const DesktopSidebar = (props: SidebarProps) => {
* @public
*/
export const Sidebar = (props: SidebarProps) => {
const { children, openDelayMs, closeDelayMs, disableExpandOnHover } = props;
const sidebarConfig: SidebarConfig = makeSidebarConfig(
props.sidebarOptions ?? {},
);
const submenuConfig: SubmenuConfig = makeSidebarSubmenuConfig(
props.submenuOptions ?? {},
);
const { children, disableExpandOnHover, openDelayMs, closeDelayMs } = props;
const { isMobile } = useContext(SidebarPinStateContext);
return isMobile ? (
<MobileSidebar>{children}</MobileSidebar>
) : (
<DesktopSidebar
openDelayMs={openDelayMs}
closeDelayMs={closeDelayMs}
disableExpandOnHover={disableExpandOnHover}
>
{children}
</DesktopSidebar>
<SidebarConfigContext.Provider value={{ sidebarConfig, submenuConfig }}>
<DesktopSidebar
openDelayMs={openDelayMs}
closeDelayMs={closeDelayMs}
disableExpandOnHover={disableExpandOnHover}
>
{children}
</DesktopSidebar>
</SidebarConfigContext.Provider>
);
};
function A11ySkipSidebar() {
const { sidebarConfig } = useContext(SidebarConfigContext);
const { focusContent, contentRef } = useContent();
const classes = useStyles();
const classes = useStyles({ sidebarConfig });
if (!contentRef?.current) {
return null;
@@ -23,7 +23,8 @@ import CloseIcon from '@material-ui/icons/Close';
import React, { useContext, useState } from 'react';
import { useLocalStorageValue } from '@react-hookz/web';
import {
sidebarConfig,
SidebarConfigContext,
SidebarConfig,
SidebarContext,
SIDEBAR_INTRO_LOCAL_STORAGE,
} from './config';
@@ -37,18 +38,18 @@ export type SidebarIntroClassKey =
| 'introDismissText'
| 'introDismissIcon';
const useStyles = makeStyles<BackstageTheme>(
const useStyles = makeStyles<BackstageTheme, { sidebarConfig: SidebarConfig }>(
theme => ({
introCard: {
introCard: props => ({
color: '#b5b5b5',
// XXX (@koroeskohr): should I be using a Mui theme variable?
fontSize: 12,
width: sidebarConfig.drawerWidthOpen,
width: props.sidebarConfig.drawerWidthOpen,
marginTop: 18,
marginBottom: 12,
paddingLeft: sidebarConfig.iconPadding,
paddingRight: sidebarConfig.iconPadding,
},
paddingLeft: props.sidebarConfig.iconPadding,
paddingRight: props.sidebarConfig.iconPadding,
}),
introDismiss: {
display: 'flex',
justifyContent: 'flex-end',
@@ -96,7 +97,8 @@ type IntroCardProps = {
*/
export function IntroCard(props: IntroCardProps) {
const classes = useStyles();
const { sidebarConfig } = useContext(SidebarConfigContext);
const classes = useStyles({ sidebarConfig });
const { text, onClose } = props;
const handleClose = () => onClose();
@@ -47,9 +47,10 @@ import {
useResolvedPath,
} from 'react-router-dom';
import {
sidebarConfig,
SidebarContext,
SidebarConfigContext,
SidebarItemWithSubmenuContext,
SidebarConfig,
} from './config';
import {
SidebarSubmenuItemProps,
@@ -82,126 +83,120 @@ export type SidebarItemClassKey =
| 'arrows'
| 'selected';
const useStyles = makeStyles<BackstageTheme>(
theme => {
const {
selectedIndicatorWidth,
drawerWidthClosed,
drawerWidthOpen,
iconContainerWidth,
} = sidebarConfig;
return {
root: {
color: theme.palette.navigation.color,
display: 'flex',
flexFlow: 'row nowrap',
alignItems: 'center',
height: 48,
cursor: 'pointer',
const useStyles = makeStyles<BackstageTheme, { sidebarConfig: SidebarConfig }>(
theme => ({
root: {
color: theme.palette.navigation.color,
display: 'flex',
flexFlow: 'row nowrap',
alignItems: 'center',
height: 48,
cursor: 'pointer',
},
buttonItem: {
background: 'none',
border: 'none',
width: '100%',
margin: 0,
padding: 0,
textAlign: 'inherit',
font: 'inherit',
},
closed: props => ({
width: props.sidebarConfig.drawerWidthClosed,
justifyContent: 'center',
}),
open: props => ({
[theme.breakpoints.up('sm')]: {
width: props.sidebarConfig.drawerWidthOpen,
},
buttonItem: {
background: 'none',
border: 'none',
width: '100%',
margin: 0,
padding: 0,
textAlign: 'inherit',
font: 'inherit',
},
closed: {
width: drawerWidthClosed,
justifyContent: 'center',
},
open: {
[theme.breakpoints.up('sm')]: {
width: drawerWidthOpen,
},
},
highlightable: {
'&:hover': {
background:
theme.palette.navigation.navItem?.hoverBackground ?? '#404040',
},
},
highlighted: {
}),
highlightable: {
'&:hover': {
background:
theme.palette.navigation.navItem?.hoverBackground ?? '#404040',
},
label: {
// XXX (@koroeskohr): I can't seem to achieve the desired font-weight from the designs
fontWeight: 'bold',
whiteSpace: 'nowrap',
lineHeight: 'auto',
flex: '3 1 auto',
width: '110px',
overflow: 'hidden',
'text-overflow': 'ellipsis',
},
highlighted: {
background:
theme.palette.navigation.navItem?.hoverBackground ?? '#404040',
},
label: {
// XXX (@koroeskohr): I can't seem to achieve the desired font-weight from the designs
fontWeight: 'bold',
whiteSpace: 'nowrap',
lineHeight: 'auto',
flex: '3 1 auto',
width: '110px',
overflow: 'hidden',
'text-overflow': 'ellipsis',
},
iconContainer: props => ({
boxSizing: 'border-box',
height: '100%',
width: props.sidebarConfig.iconContainerWidth,
marginRight: -theme.spacing(2),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}),
searchRoot: {
marginBottom: 12,
},
searchField: {
color: '#b5b5b5',
fontWeight: 'bold',
fontSize: theme.typography.fontSize,
},
searchFieldHTMLInput: {
padding: theme.spacing(2, 0, 2),
},
searchContainer: props => ({
width:
props.sidebarConfig.drawerWidthOpen -
props.sidebarConfig.iconContainerWidth,
}),
secondaryAction: {
width: theme.spacing(6),
textAlign: 'center',
marginRight: theme.spacing(1),
},
closedItemIcon: {
width: '100%',
justifyContent: 'center',
},
submenuArrow: {
display: 'flex',
},
expandButton: {
background: 'none',
border: 'none',
color: theme.palette.navigation.color,
width: '100%',
cursor: 'pointer',
position: 'relative',
height: 48,
},
arrows: {
position: 'absolute',
right: 10,
},
selected: props => ({
'&$root': {
borderLeft: `solid ${props.sidebarConfig.selectedIndicatorWidth}px ${theme.palette.navigation.indicator}`,
color: theme.palette.navigation.selectedColor,
},
iconContainer: {
boxSizing: 'border-box',
height: '100%',
width: iconContainerWidth,
marginRight: -theme.spacing(2),
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
'&$closed': {
width: props.sidebarConfig.drawerWidthClosed,
},
searchRoot: {
marginBottom: 12,
'& $closedItemIcon': {
paddingRight: props.sidebarConfig.selectedIndicatorWidth,
},
searchField: {
color: '#b5b5b5',
fontWeight: 'bold',
fontSize: theme.typography.fontSize,
'& $iconContainer': {
marginLeft: -props.sidebarConfig.selectedIndicatorWidth,
},
searchFieldHTMLInput: {
padding: theme.spacing(2, 0, 2),
},
searchContainer: {
width: drawerWidthOpen - iconContainerWidth,
},
secondaryAction: {
width: theme.spacing(6),
textAlign: 'center',
marginRight: theme.spacing(1),
},
closedItemIcon: {
width: '100%',
justifyContent: 'center',
},
submenuArrow: {
display: 'flex',
},
expandButton: {
background: 'none',
border: 'none',
color: theme.palette.navigation.color,
width: '100%',
cursor: 'pointer',
position: 'relative',
height: 48,
},
arrows: {
position: 'absolute',
right: 10,
},
selected: {
'&$root': {
borderLeft: `solid ${selectedIndicatorWidth}px ${theme.palette.navigation.indicator}`,
color: theme.palette.navigation.selectedColor,
},
'&$closed': {
width: drawerWidthClosed,
},
'& $closedItemIcon': {
paddingRight: selectedIndicatorWidth,
},
'& $iconContainer': {
marginLeft: -selectedIndicatorWidth,
},
},
};
},
}),
}),
{ name: 'BackstageSidebarItem' },
);
@@ -356,7 +351,8 @@ const SidebarItemBase = forwardRef<any, SidebarItemProps>((props, ref) => {
className,
...navLinkProps
} = props;
const classes = useStyles();
const { sidebarConfig } = useContext(SidebarConfigContext);
const classes = useStyles({ sidebarConfig });
// XXX (@koroeskohr): unsure this is optimal. But I just really didn't want to have the item component
// depend on the current location, and at least have it being optionally forced to selected.
// Still waiting on a Q answered to fine tune the implementation
@@ -429,7 +425,8 @@ const SidebarItemWithSubmenu = ({
}: SidebarItemBaseProps & {
children: React.ReactElement<SidebarSubmenuProps>;
}) => {
const classes = useStyles();
const { sidebarConfig } = useContext(SidebarConfigContext);
const classes = useStyles({ sidebarConfig });
const [isHoveredOn, setIsHoveredOn] = useState(false);
const location = useLocation();
const isActive = useLocationMatch(children, location);
@@ -518,8 +515,9 @@ type SidebarSearchFieldProps = {
};
export function SidebarSearchField(props: SidebarSearchFieldProps) {
const { sidebarConfig } = useContext(SidebarConfigContext);
const [input, setInput] = useState('');
const classes = useStyles();
const classes = useStyles({ sidebarConfig });
const Icon = props.icon ? props.icon : SearchIcon;
const search = () => {
@@ -647,7 +645,8 @@ export const SidebarScrollWrapper = styled('div')(({ theme }) => {
* @public
*/
export const SidebarExpandButton = () => {
const classes = useStyles();
const { sidebarConfig } = useContext(SidebarConfigContext);
const classes = useStyles({ sidebarConfig });
const { isOpen, setOpen } = useContext(SidebarContext);
const isSmallScreen = useMediaQuery<BackstageTheme>(
theme => theme.breakpoints.down('md'),
@@ -25,11 +25,10 @@ import Typography from '@material-ui/core/Typography';
import CloseIcon from '@material-ui/icons/Close';
import MenuIcon from '@material-ui/icons/Menu';
import { orderBy } from 'lodash';
import React, { createContext, useEffect, useState } from 'react';
import React, { createContext, useEffect, useState, useContext } from 'react';
import { useLocation } from 'react-router';
import { SidebarContext } from '.';
import { sidebarConfig } from './config';
import { SidebarGroup } from './SidebarGroup';
import { SidebarConfigContext, SidebarContext, SidebarConfig } from './config';
/**
* Type of `MobileSidebarContext`
@@ -60,44 +59,46 @@ type OverlayMenuProps = {
children?: React.ReactNode;
};
const useStyles = makeStyles<BackstageTheme>(theme => ({
root: {
position: 'fixed',
backgroundColor: theme.palette.navigation.background,
color: theme.palette.navigation.color,
bottom: 0,
left: 0,
right: 0,
zIndex: theme.zIndex.snackbar,
// SidebarDivider color
borderTop: '1px solid #383838',
},
const useStyles = makeStyles<BackstageTheme, { sidebarConfig: SidebarConfig }>(
theme => ({
root: {
position: 'fixed',
backgroundColor: theme.palette.navigation.background,
color: theme.palette.navigation.color,
bottom: 0,
left: 0,
right: 0,
zIndex: theme.zIndex.snackbar,
// SidebarDivider color
borderTop: '1px solid #383838',
},
overlay: {
background: theme.palette.navigation.background,
width: '100%',
bottom: `${sidebarConfig.mobileSidebarHeight}px`,
height: `calc(100% - ${sidebarConfig.mobileSidebarHeight}px)`,
flex: '0 1 auto',
overflow: 'auto',
},
overlay: props => ({
background: theme.palette.navigation.background,
width: '100%',
bottom: `${props.sidebarConfig.mobileSidebarHeight}px`,
height: `calc(100% - ${props.sidebarConfig.mobileSidebarHeight}px)`,
flex: '0 1 auto',
overflow: 'auto',
}),
overlayHeader: {
display: 'flex',
color: theme.palette.bursts.fontColor,
alignItems: 'center',
justifyContent: 'space-between',
padding: theme.spacing(2, 3),
},
overlayHeader: {
display: 'flex',
color: theme.palette.bursts.fontColor,
alignItems: 'center',
justifyContent: 'space-between',
padding: theme.spacing(2, 3),
},
overlayHeaderClose: {
color: theme.palette.bursts.fontColor,
},
overlayHeaderClose: {
color: theme.palette.bursts.fontColor,
},
marginMobileSidebar: {
marginBottom: `${sidebarConfig.mobileSidebarHeight}px`,
},
}));
marginMobileSidebar: props => ({
marginBottom: `${props.sidebarConfig.mobileSidebarHeight}px`,
}),
}),
);
const sortSidebarGroupsForPriority = (children: React.ReactElement[]) =>
orderBy(
@@ -114,7 +115,8 @@ const OverlayMenu = ({
open,
onClose,
}: OverlayMenuProps) => {
const classes = useStyles();
const { sidebarConfig } = useContext(SidebarConfigContext);
const classes = useStyles({ sidebarConfig });
return (
<Drawer
@@ -163,8 +165,9 @@ export const MobileSidebarContext = createContext<MobileSidebarContextType>({
* @public
*/
export const MobileSidebar = (props: MobileSidebarProps) => {
const { sidebarConfig } = useContext(SidebarConfigContext);
const { children } = props;
const classes = useStyles();
const classes = useStyles({ sidebarConfig });
const location = useLocation();
const [selectedMenuItemIndex, setSelectedMenuItemIndex] =
useState<number>(-1);
@@ -25,29 +25,32 @@ import React, {
useRef,
useState,
} from 'react';
import { sidebarConfig } from './config';
import { SidebarConfigContext, SidebarConfig } from './config';
import { BackstageTheme } from '@backstage/theme';
import { LocalStorage } from './localStorage';
import useMediaQuery from '@material-ui/core/useMediaQuery';
export type SidebarPageClassKey = 'root';
const useStyles = makeStyles<BackstageTheme, { isPinned: boolean }>(
const useStyles = makeStyles<
BackstageTheme,
{ sidebarConfig: SidebarConfig; isPinned: boolean }
>(
theme => ({
root: {
root: props => ({
width: '100%',
transition: 'padding-left 0.1s ease-out',
isolation: 'isolate',
[theme.breakpoints.up('sm')]: {
paddingLeft: ({ isPinned }) =>
isPinned
? sidebarConfig.drawerWidthOpen
: sidebarConfig.drawerWidthClosed,
paddingLeft: () =>
props.isPinned
? props.sidebarConfig.drawerWidthOpen
: props.sidebarConfig.drawerWidthClosed,
},
[theme.breakpoints.down('xs')]: {
paddingBottom: sidebarConfig.mobileSidebarHeight,
paddingBottom: props.sidebarConfig.mobileSidebarHeight,
},
},
}),
content: {
zIndex: 0,
isolation: 'isolate',
@@ -107,6 +110,7 @@ export function SidebarPage(props: SidebarPageProps) {
const [isPinned, setIsPinned] = useState(() =>
LocalStorage.getSidebarPinState(),
);
const { sidebarConfig } = useContext(SidebarConfigContext);
const contentRef = useRef(null);
@@ -130,7 +134,7 @@ export function SidebarPage(props: SidebarPageProps) {
const toggleSidebarPinState = () => setIsPinned(!isPinned);
const classes = useStyles({ isPinned });
const classes = useStyles({ isPinned, sidebarConfig });
return (
<SidebarPinStateContext.Provider
@@ -24,7 +24,7 @@ import React, { useContext } from 'react';
import { useLocation } from 'react-router-dom';
import { SidebarPinStateContext } from '.';
import { Link } from '../../components';
import { sidebarConfig } from './config';
import { SidebarConfigContext, SidebarConfig } from './config';
import { MobileSidebarContext } from './MobileSidebar';
/**
@@ -48,23 +48,25 @@ export interface SidebarGroupProps extends BottomNavigationActionProps {
children?: React.ReactNode;
}
const useStyles = makeStyles<BackstageTheme>(theme => ({
root: {
flexGrow: 0,
margin: theme.spacing(0, 2),
color: theme.palette.navigation.color,
},
const useStyles = makeStyles<BackstageTheme, { sidebarConfig: SidebarConfig }>(
theme => ({
root: {
flexGrow: 0,
margin: theme.spacing(0, 2),
color: theme.palette.navigation.color,
},
selected: {
color: `${theme.palette.navigation.selectedColor}!important`,
borderTop: `solid ${sidebarConfig.selectedIndicatorWidth}px ${theme.palette.navigation.indicator}`,
marginTop: '-1px',
},
selected: props => ({
color: `${theme.palette.navigation.selectedColor}!important`,
borderTop: `solid ${props.sidebarConfig.selectedIndicatorWidth}px ${theme.palette.navigation.indicator}`,
marginTop: '-1px',
}),
label: {
display: 'none',
},
}));
label: {
display: 'none',
},
}),
);
/**
* Returns a MUI `BottomNavigationAction`, which is aware of the current location & the selected item in the `BottomNavigation`,
@@ -75,7 +77,8 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
*/
const MobileSidebarGroup = (props: SidebarGroupProps) => {
const { to, label, icon, value } = props;
const classes = useStyles();
const { sidebarConfig } = useContext(SidebarConfigContext);
const classes = useStyles({ sidebarConfig });
const location = useLocation();
const { selectedMenuItemIndex, setSelectedMenuItemIndex } =
useContext(MobileSidebarContext);
@@ -19,72 +19,74 @@ import classnames from 'classnames';
import React, { ReactNode, useContext, useEffect, useState } from 'react';
import {
SidebarItemWithSubmenuContext,
sidebarConfig,
SidebarContext,
submenuConfig,
SidebarConfigContext,
SubmenuConfig,
} from './config';
import { BackstageTheme } from '@backstage/theme';
const useStyles = (props: { left: number }) =>
makeStyles<BackstageTheme>(
theme => ({
root: {
zIndex: 1000,
position: 'relative',
overflow: 'visible',
width: theme.spacing(7) + 1,
const useStyles = makeStyles<
BackstageTheme,
{ submenuConfig: SubmenuConfig; left: number }
>(
theme => ({
root: {
zIndex: 1000,
position: 'relative',
overflow: 'visible',
width: theme.spacing(7) + 1,
},
drawer: props => ({
display: 'flex',
flexFlow: 'column nowrap',
alignItems: 'flex-start',
position: 'fixed',
[theme.breakpoints.up('sm')]: {
marginLeft: props.left,
transition: theme.transitions.create('margin-left', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.shortest,
}),
},
drawer: {
display: 'flex',
flexFlow: 'column nowrap',
alignItems: 'flex-start',
position: 'fixed',
[theme.breakpoints.up('sm')]: {
marginLeft: props.left,
transition: theme.transitions.create('margin-left', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.shortest,
}),
},
top: 0,
bottom: 0,
padding: 0,
background: theme.palette.navigation.submenu?.background ?? '#404040',
overflowX: 'hidden',
msOverflowStyle: 'none',
scrollbarWidth: 'none',
cursor: 'default',
width: submenuConfig.drawerWidthClosed,
transitionDelay: `${submenuConfig.defaultOpenDelayMs}ms`,
'& > *': {
flexShrink: 0,
},
'&::-webkit-scrollbar': {
display: 'none',
},
top: 0,
bottom: 0,
padding: 0,
background: theme.palette.navigation.submenu?.background ?? '#404040',
overflowX: 'hidden',
msOverflowStyle: 'none',
scrollbarWidth: 'none',
cursor: 'default',
width: props.submenuConfig.drawerWidthClosed,
transitionDelay: `${props.submenuConfig.defaultOpenDelayMs}ms`,
'& > *': {
flexShrink: 0,
},
drawerOpen: {
width: submenuConfig.drawerWidthOpen,
[theme.breakpoints.down('xs')]: {
width: '100%',
position: 'relative',
paddingLeft: theme.spacing(3),
left: 0,
top: 0,
},
},
title: {
fontSize: 24,
fontWeight: 500,
color: '#FFF',
padding: 20,
[theme.breakpoints.down('xs')]: {
display: 'none',
},
'&::-webkit-scrollbar': {
display: 'none',
},
}),
{ name: 'BackstageSidebarSubmenu' },
);
drawerOpen: props => ({
width: props.submenuConfig.drawerWidthOpen,
[theme.breakpoints.down('xs')]: {
width: '100%',
position: 'relative',
paddingLeft: theme.spacing(3),
left: 0,
top: 0,
},
}),
title: {
fontSize: 24,
fontWeight: 500,
color: '#FFF',
padding: 20,
[theme.breakpoints.down('xs')]: {
display: 'none',
},
},
}),
{ name: 'BackstageSidebarSubmenu' },
);
/**
* Holds a title for text Header of a sidebar submenu and children
@@ -104,10 +106,11 @@ export type SidebarSubmenuProps = {
*/
export const SidebarSubmenu = (props: SidebarSubmenuProps) => {
const { isOpen } = useContext(SidebarContext);
const { sidebarConfig, submenuConfig } = useContext(SidebarConfigContext);
const left = isOpen
? sidebarConfig.drawerWidthOpen
: sidebarConfig.drawerWidthClosed;
const classes = useStyles({ left })();
const classes = useStyles({ left, submenuConfig });
const { isHoveredOn } = useContext(SidebarItemWithSubmenuContext);
const [isSubmenuOpen, setIsSubmenuOpen] = useState(false);
@@ -20,6 +20,35 @@ const drawerWidthClosed = 72;
const iconPadding = 24;
const userBadgePadding = 18;
/** @public **/
export type SidebarOptions = {
drawerWidthClosed?: number;
drawerWidthOpen?: number;
};
/** @public **/
export type SubmenuOptions = {
drawerWidthClosed?: number;
drawerWidthOpen?: number;
};
/** @internal **/
export type SidebarConfig = {
drawerWidthClosed: number;
drawerWidthOpen: number;
defaultOpenDelayMs: number;
defaultCloseDelayMs: number;
defaultFadeDuration: number;
logoHeight: number;
iconContainerWidth: number;
iconSize: number;
iconPadding: number;
selectedIndicatorWidth: number;
userBadgePadding: number;
userBadgeDiameter: number;
mobileSidebarHeight: number;
};
export const sidebarConfig = {
drawerWidthClosed,
drawerWidthOpen: 224,
@@ -38,12 +67,37 @@ export const sidebarConfig = {
mobileSidebarHeight: 56,
};
export const makeSidebarConfig = (
customSidebarConfig: Partial<SidebarOptions>,
) => ({
...sidebarConfig,
...customSidebarConfig,
iconContainerWidth: sidebarConfig.drawerWidthClosed,
iconSize: sidebarConfig.drawerWidthClosed - sidebarConfig.iconPadding * 2,
userBadgeDiameter:
sidebarConfig.drawerWidthClosed - sidebarConfig.userBadgePadding * 2,
});
/** @internal **/
export type SubmenuConfig = {
drawerWidthClosed: number;
drawerWidthOpen: number;
defaultOpenDelayMs: number;
};
export const submenuConfig = {
drawerWidthClosed: 0,
drawerWidthOpen: 202,
defaultOpenDelayMs: sidebarConfig.defaultOpenDelayMs + 200,
};
export const makeSidebarSubmenuConfig = (
customSubmenuConfig: Partial<SubmenuOptions>,
) => ({
...submenuConfig,
...customSubmenuConfig,
});
export const SIDEBAR_INTRO_LOCAL_STORAGE =
'@backstage/core/sidebar-intro-dismissed';
@@ -63,6 +117,16 @@ export const SidebarContext = createContext<SidebarContextType>({
setOpen: () => {},
});
export type SidebarConfigContextType = {
sidebarConfig: SidebarConfig;
submenuConfig: SubmenuConfig;
};
export const SidebarConfigContext = createContext<SidebarConfigContextType>({
sidebarConfig,
submenuConfig,
});
export type SidebarItemWithSubmenuContextType = {
isHoveredOn: boolean;
setIsHoveredOn: Dispatch<SetStateAction<boolean>>;
@@ -59,4 +59,8 @@ export {
SidebarContext,
sidebarConfig,
} from './config';
export type { SidebarContextType } from './config';
export type {
SidebarContextType,
SidebarOptions,
SubmenuOptions,
} from './config';