diff --git a/.changeset/thin-deers-turn.md b/.changeset/thin-deers-turn.md new file mode 100644 index 0000000000..cec95355d4 --- /dev/null +++ b/.changeset/thin-deers-turn.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + + now accepts custom configuration options by supplying props sidebarConfig and submenuConfig diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index 74a57ea6ee..246878567d 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -21,7 +21,14 @@ 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, +} from './config'; import { BackstageTheme } from '@backstage/theme'; import { SidebarPinStateContext, useContent } from './Page'; import { MobileSidebar } from './MobileSidebar'; @@ -29,52 +36,55 @@ import { MobileSidebar } from './MobileSidebar'; /** @public */ export type SidebarClassKey = 'drawer' | 'drawerOpen'; -const useStyles = makeStyles( - theme => ({ - drawer: { - display: 'flex', - flexFlow: 'column nowrap', - alignItems: 'flex-start', - position: 'fixed', - left: 0, - top: 0, - bottom: 0, - zIndex: theme.zIndex.appBar, - background: theme.palette.navigation.background, - overflowX: 'hidden', - msOverflowStyle: 'none', - scrollbarWidth: 'none', - width: sidebarConfig.drawerWidthClosed, - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.shortest, - }), - '& > *': { - flexShrink: 0, - }, - '&::-webkit-scrollbar': { - display: 'none', - }, +const useStyles = ({ sidebarConfig }: { sidebarConfig: SidebarConfig }) => + makeStyles( + theme => { + return { + drawer: { + display: 'flex', + flexFlow: 'column nowrap', + alignItems: 'flex-start', + position: 'fixed', + left: 0, + top: 0, + bottom: 0, + zIndex: theme.zIndex.appBar, + background: theme.palette.navigation.background, + overflowX: 'hidden', + msOverflowStyle: 'none', + scrollbarWidth: 'none', + width: sidebarConfig.drawerWidthClosed, + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.shortest, + }), + '& > *': { + flexShrink: 0, + }, + '&::-webkit-scrollbar': { + display: 'none', + }, + }, + drawerOpen: { + width: sidebarConfig.drawerWidthOpen, + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.shorter, + }), + }, + visuallyHidden: { + top: 0, + position: 'absolute', + zIndex: 1000, + transform: 'translateY(-200%)', + '&:focus': { + transform: 'translateY(5px)', + }, + }, + }; }, - drawerOpen: { - width: sidebarConfig.drawerWidthOpen, - transition: theme.transitions.create('width', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.shorter, - }), - }, - visuallyHidden: { - top: 0, - position: 'absolute', - zIndex: 1000, - transform: 'translateY(-200%)', - '&:focus': { - transform: 'translateY(5px)', - }, - }, - }), - { name: 'BackstageSidebar' }, -); + { name: 'BackstageSidebar' }, + ); enum State { Closed, @@ -84,8 +94,13 @@ enum State { /** @public */ export type SidebarProps = { - openDelayMs?: number; - closeDelayMs?: number; + sidebarConfig?: Partial; + submenuConfig?: Partial; + disableExpandOnHover?: boolean; + children?: React.ReactNode; +}; + +export type DesktopSidebarProps = { disableExpandOnHover?: boolean; children?: React.ReactNode; }; @@ -100,14 +115,14 @@ export type SidebarProps = { * @returns * @internal */ -const DesktopSidebar = (props: SidebarProps) => { - const { - openDelayMs = sidebarConfig.defaultOpenDelayMs, - closeDelayMs = sidebarConfig.defaultCloseDelayMs, - disableExpandOnHover, - children, - } = props; - const classes = useStyles(); +const DesktopSidebar = (props: DesktopSidebarProps) => { + const { sidebarConfig } = useContext(SidebarConfigContext); + const { defaultOpenDelayMs: openDelayMs, defaultCloseDelayMs: closeDelayMs } = + sidebarConfig; + + const { disableExpandOnHover, children } = props; + + const classes = useStyles({ sidebarConfig })(); const isSmallScreen = useMediaQuery( theme => theme.breakpoints.down('md'), { noSsr: true }, @@ -172,12 +187,7 @@ const DesktopSidebar = (props: SidebarProps) => { return (
- +
{ * @public */ export const Sidebar = (props: SidebarProps) => { - const { children, openDelayMs, closeDelayMs, disableExpandOnHover } = props; + const sidebarConfig: SidebarConfig = makeSidebarConfig( + props.sidebarConfig ?? {}, + ); + const submenuConfig: SubmenuConfig = makeSidebarSubmenuConfig( + props.submenuConfig ?? {}, + sidebarConfig, + ); + const { children, disableExpandOnHover } = 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..5abe0112a2 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,51 +38,54 @@ export type SidebarIntroClassKey = | 'introDismissText' | 'introDismissIcon'; -const useStyles = makeStyles( - theme => ({ - introCard: { - color: '#b5b5b5', - // XXX (@koroeskohr): should I be using a Mui theme variable? - fontSize: 12, - width: sidebarConfig.drawerWidthOpen, - marginTop: 18, - marginBottom: 12, - paddingLeft: sidebarConfig.iconPadding, - paddingRight: sidebarConfig.iconPadding, +const useStyles = ({sidebarConfig}: { sidebarConfig: SidebarConfig }) => + makeStyles( + theme => { + return { + introCard: { + color: '#b5b5b5', + // XXX (@koroeskohr): should I be using a Mui theme variable? + fontSize: 12, + width: sidebarConfig.drawerWidthOpen, + marginTop: 18, + marginBottom: 12, + paddingLeft: sidebarConfig.iconPadding, + paddingRight: sidebarConfig.iconPadding, + }, + introDismiss: { + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'center', + marginTop: 12, + }, + introDismissLink: { + color: '#dddddd', + display: 'flex', + alignItems: 'center', + marginBottom: 4, + '&:hover': { + color: theme.palette.linkHover, + transition: theme.transitions.create('color', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.shortest, + }), + }, + }, + introDismissText: { + fontSize: '0.7rem', + fontWeight: 'bold', + textTransform: 'uppercase', + letterSpacing: 1, + }, + introDismissIcon: { + width: 18, + height: 18, + marginRight: 12, + }, + }; }, - introDismiss: { - display: 'flex', - justifyContent: 'flex-end', - alignItems: 'center', - marginTop: 12, - }, - introDismissLink: { - color: '#dddddd', - display: 'flex', - alignItems: 'center', - marginBottom: 4, - '&:hover': { - color: theme.palette.linkHover, - transition: theme.transitions.create('color', { - easing: theme.transitions.easing.sharp, - duration: theme.transitions.duration.shortest, - }), - }, - }, - introDismissText: { - fontSize: '0.7rem', - fontWeight: 'bold', - textTransform: 'uppercase', - letterSpacing: 1, - }, - introDismissIcon: { - width: 18, - height: 18, - marginRight: 12, - }, - }), - { name: 'BackstageSidebarIntro' }, -); + { name: 'BackstageSidebarIntro' }, + ); type IntroCardProps = { text: string; @@ -96,7 +100,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..79ab925d93 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,128 +83,124 @@ 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', - }, - 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, +const useStyles = ({ sidebarConfig }: { sidebarConfig: SidebarConfig }) => + makeStyles( + theme => { + return { + root: { + color: theme.palette.navigation.color, + display: 'flex', + flexFlow: 'row nowrap', + alignItems: 'center', + height: 48, + cursor: 'pointer', }, - }, - highlightable: { - '&:hover': { + buttonItem: { + background: 'none', + border: 'none', + width: '100%', + margin: 0, + padding: 0, + textAlign: 'inherit', + font: 'inherit', + }, + closed: { + width: sidebarConfig.drawerWidthClosed, + justifyContent: 'center', + }, + open: { + [theme.breakpoints.up('sm')]: { + width: sidebarConfig.drawerWidthOpen, + }, + }, + highlightable: { + '&:hover': { + background: + theme.palette.navigation.navItem?.hoverBackground ?? '#404040', + }, + }, + highlighted: { background: theme.palette.navigation.navItem?.hoverBackground ?? '#404040', }, - }, - 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: { - boxSizing: 'border-box', - height: '100%', - width: 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: { - 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, + 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', }, - '&$closed': { - width: drawerWidthClosed, + iconContainer: { + boxSizing: 'border-box', + height: '100%', + width: sidebarConfig.iconContainerWidth, + marginRight: -theme.spacing(2), + display: 'flex', + alignItems: 'center', + justifyContent: 'center', }, - '& $closedItemIcon': { - paddingRight: selectedIndicatorWidth, + searchRoot: { + marginBottom: 12, }, - '& $iconContainer': { - marginLeft: -selectedIndicatorWidth, + searchField: { + color: '#b5b5b5', + fontWeight: 'bold', + fontSize: theme.typography.fontSize, }, - }, - }; - }, - { name: 'BackstageSidebarItem' }, -); + searchFieldHTMLInput: { + padding: theme.spacing(2, 0, 2), + }, + searchContainer: { + width: + sidebarConfig.drawerWidthOpen - 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: { + '&$root': { + borderLeft: `solid ${sidebarConfig.selectedIndicatorWidth}px ${theme.palette.navigation.indicator}`, + color: theme.palette.navigation.selectedColor, + }, + '&$closed': { + width: sidebarConfig.drawerWidthClosed, + }, + '& $closedItemIcon': { + paddingRight: sidebarConfig.selectedIndicatorWidth, + }, + '& $iconContainer': { + marginLeft: -sidebarConfig.selectedIndicatorWidth, + }, + }, + }; + }, + { name: 'BackstageSidebarItem' }, + ); /** * Evaluates the routes of the SubmenuItems & nested DropdownItems. @@ -356,7 +353,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 +427,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 +517,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 +647,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..8bf5198ea8 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,47 @@ 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 = ({ sidebarConfig }: { sidebarConfig: SidebarConfig }) => + makeStyles(theme => { + return { + 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: { + background: theme.palette.navigation.background, + width: '100%', + bottom: `${sidebarConfig.mobileSidebarHeight}px`, + height: `calc(100% - ${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: { + marginBottom: `${sidebarConfig.mobileSidebarHeight}px`, + }, + }; + }) const sortSidebarGroupsForPriority = (children: React.ReactElement[]) => orderBy( @@ -114,7 +116,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 3b0efb62c1..86d990e7f8 100644 --- a/packages/core-components/src/layout/Sidebar/Page.tsx +++ b/packages/core-components/src/layout/Sidebar/Page.tsx @@ -25,39 +25,45 @@ import React, { useRef, useState, } from 'react'; -import { sidebarConfig } from './config'; +import { SidebarConfigContext } 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( - theme => ({ - root: { - width: '100%', - transition: 'padding-left 0.1s ease-out', - isolation: 'isolate', - [theme.breakpoints.up('sm')]: { - paddingLeft: ({ isPinned }) => - isPinned - ? sidebarConfig.drawerWidthOpen - : sidebarConfig.drawerWidthClosed, - }, - [theme.breakpoints.down('xs')]: { - paddingBottom: sidebarConfig.mobileSidebarHeight, - }, +const useStyles = ({ isPinned }: { isPinned: boolean }) => { + const { sidebarConfig } = useContext(SidebarConfigContext); + + return makeStyles( + theme => { + return { + root: { + width: '100%', + transition: 'padding-left 0.1s ease-out', + isolation: 'isolate', + [theme.breakpoints.up('sm')]: { + paddingLeft: () => + isPinned + ? sidebarConfig.drawerWidthOpen + : sidebarConfig.drawerWidthClosed, + }, + [theme.breakpoints.down('xs')]: { + paddingBottom: sidebarConfig.mobileSidebarHeight, + }, + }, + content: { + zIndex: 0, + isolation: 'isolate', + '&:focus': { + outline: 0, + }, + }, + }; }, - content: { - zIndex: 0, - isolation: 'isolate', - '&:focus': { - outline: 0, - }, - }, - }), - { name: 'BackstageSidebarPage' }, -); + { name: 'BackstageSidebarPage' }, + )(); +}; /** * Type of `SidebarPinStateContext` diff --git a/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx b/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx index 16395d86f2..cc43963112 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx @@ -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,26 @@ export interface SidebarGroupProps extends BottomNavigationActionProps { children?: React.ReactNode; } -const useStyles = makeStyles(theme => ({ - root: { - flexGrow: 0, - margin: theme.spacing(0, 2), - color: theme.palette.navigation.color, - }, +const useStyles = ({ sidebarConfig }: { sidebarConfig: SidebarConfig }) => + makeStyles(theme => { + return { + 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: { + color: `${theme.palette.navigation.selectedColor}!important`, + borderTop: `solid ${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 +78,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..ff0086bffb 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarSubmenu.tsx @@ -19,13 +19,13 @@ 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 }) => +const useStyles = (props: { left: number, submenuConfig: SubmenuConfig }) => makeStyles( theme => ({ root: { @@ -54,8 +54,8 @@ const useStyles = (props: { left: number }) => msOverflowStyle: 'none', scrollbarWidth: 'none', cursor: 'default', - width: submenuConfig.drawerWidthClosed, - transitionDelay: `${submenuConfig.defaultOpenDelayMs}ms`, + width: props.submenuConfig.drawerWidthClosed, + transitionDelay: `${props.submenuConfig.defaultOpenDelayMs}ms`, '& > *': { flexShrink: 0, }, @@ -64,7 +64,7 @@ const useStyles = (props: { left: number }) => }, }, drawerOpen: { - width: submenuConfig.drawerWidthOpen, + width: props.submenuConfig.drawerWidthOpen, [theme.breakpoints.down('xs')]: { width: '100%', position: 'relative', @@ -104,10 +104,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 55b9787b13..2021b4ed69 100644 --- a/packages/core-components/src/layout/Sidebar/config.ts +++ b/packages/core-components/src/layout/Sidebar/config.ts @@ -20,6 +20,22 @@ const drawerWidthClosed = 72; const iconPadding = 24; const userBadgePadding = 18; +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 +54,38 @@ 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, +}); + +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, + customSidebarConfig: SidebarConfig, +) => ({ + ...submenuConfig, + ...customSubmenuConfig, + defaultOpenDelayMs: customSidebarConfig.defaultOpenDelayMs + 200, +}); + export const SIDEBAR_INTRO_LOCAL_STORAGE = '@backstage/core/sidebar-intro-dismissed'; @@ -63,6 +105,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>;