diff --git a/.changeset/thin-deers-turn.md b/.changeset/thin-deers-turn.md new file mode 100644 index 0000000000..d148c3fcdf --- /dev/null +++ b/.changeset/thin-deers-turn.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +`` now accepts additional props `sidebarOptions` and `submenuOptions` to allow further customization diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index a8c51b6913..2058fe2122 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -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) // diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index 74a57ea6ee..7a0b3f5b76 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -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( +const useStyles = makeStyles( theme => ({ - drawer: { + drawer: props => ({ display: 'flex', flexFlow: 'column nowrap', alignItems: 'flex-start', @@ -44,7 +52,7 @@ const useStyles = makeStyles( 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( '&::-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( theme => theme.breakpoints.down('md'), { noSsr: true }, @@ -172,12 +191,7 @@ const DesktopSidebar = (props: SidebarProps) => { return (
- +
{ * @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 ? ( {children} ) : ( - - {children} - + + + {children} + + ); }; function A11ySkipSidebar() { + const { sidebarConfig } = useContext(SidebarConfigContext); const { focusContent, contentRef } = useContent(); - const classes = useStyles(); + const classes = useStyles({ sidebarConfig }); if (!contentRef?.current) { return null; diff --git a/packages/core-components/src/layout/Sidebar/Intro.tsx b/packages/core-components/src/layout/Sidebar/Intro.tsx index 1fcc54188f..d1d8f4b2a2 100644 --- a/packages/core-components/src/layout/Sidebar/Intro.tsx +++ b/packages/core-components/src/layout/Sidebar/Intro.tsx @@ -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( +const useStyles = makeStyles( 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(); diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index 7eb86772f9..b3beed9147 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -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( - 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( + 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((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; }) => { - 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( theme => theme.breakpoints.down('md'), diff --git a/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx b/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx index 6734d4b53a..aefc58bc45 100644 --- a/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx +++ b/packages/core-components/src/layout/Sidebar/MobileSidebar.tsx @@ -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(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( + 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 ( ({ * @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(-1); diff --git a/packages/core-components/src/layout/Sidebar/Page.tsx b/packages/core-components/src/layout/Sidebar/Page.tsx index 464d0a893e..9ce4a9f997 100644 --- a/packages/core-components/src/layout/Sidebar/Page.tsx +++ b/packages/core-components/src/layout/Sidebar/Page.tsx @@ -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( +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 ( (theme => ({ - root: { - flexGrow: 0, - margin: theme.spacing(0, 2), - color: theme.palette.navigation.color, - }, +const useStyles = makeStyles( + 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(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); diff --git a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx index 5a511b2094..d1b79263eb 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx @@ -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( - 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); diff --git a/packages/core-components/src/layout/Sidebar/config.ts b/packages/core-components/src/layout/Sidebar/config.ts index b72e6f4601..08d469d5a3 100644 --- a/packages/core-components/src/layout/Sidebar/config.ts +++ b/packages/core-components/src/layout/Sidebar/config.ts @@ -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, +) => ({ + ...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, +) => ({ + ...submenuConfig, + ...customSubmenuConfig, +}); + export const SIDEBAR_INTRO_LOCAL_STORAGE = '@backstage/core/sidebar-intro-dismissed'; @@ -63,6 +117,16 @@ export const SidebarContext = createContext({ setOpen: () => {}, }); +export type SidebarConfigContextType = { + sidebarConfig: SidebarConfig; + submenuConfig: SubmenuConfig; +}; + +export const SidebarConfigContext = createContext({ + sidebarConfig, + submenuConfig, +}); + export type SidebarItemWithSubmenuContextType = { isHoveredOn: boolean; setIsHoveredOn: Dispatch>; diff --git a/packages/core-components/src/layout/Sidebar/index.ts b/packages/core-components/src/layout/Sidebar/index.ts index 96fedaa75a..ecc573fbda 100644 --- a/packages/core-components/src/layout/Sidebar/index.ts +++ b/packages/core-components/src/layout/Sidebar/index.ts @@ -59,4 +59,8 @@ export { SidebarContext, sidebarConfig, } from './config'; -export type { SidebarContextType } from './config'; +export type { + SidebarContextType, + SidebarOptions, + SubmenuOptions, +} from './config';