diff --git a/.changeset/cool-clocks-cheer.md b/.changeset/cool-clocks-cheer.md new file mode 100644 index 0000000000..4d5e5b76bd --- /dev/null +++ b/.changeset/cool-clocks-cheer.md @@ -0,0 +1,8 @@ +--- +'@backstage/core-components': patch +'@backstage/theme': patch +--- + +Added `` and `` to enable building better sidebars. You can check out the storybook for more inspiration and how to get started. + +Added two new theme props for styling the sidebar too, `navItem.hoverBackground` and `submenu.background`. diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 0f1e3510ae..c1e021391e 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -832,6 +832,7 @@ export const SidebarContext: Context; // @public (undocumented) export type SidebarContextType = { isOpen: boolean; + setOpen: (open: boolean) => void; }; // Warning: (ae-missing-release-tag) "SidebarDivider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1103,6 +1104,9 @@ export const SidebarDivider: React_2.ComponentType< } >; +// @public +export const SidebarExpandButton: () => JSX.Element | null; + // Warning: (ae-missing-release-tag) "SidebarIntro" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1977,6 +1981,37 @@ export const SidebarSpacer: React_2.ComponentType< } >; +// @public +export const SidebarSubmenu: ({ + title, + children, +}: PropsWithChildren) => JSX.Element; + +// @public +export const SidebarSubmenuItem: ( + props: SidebarSubmenuItemProps, +) => JSX.Element; + +// @public +export type SidebarSubmenuItemDropdownItem = { + title: string; + to: string; +}; + +// @public +export type SidebarSubmenuItemProps = { + title: string; + to: string; + icon: IconComponent; + dropdownItems?: SidebarSubmenuItemDropdownItem[]; +}; + +// @public +export type SidebarSubmenuProps = { + title?: string; + children: ReactNode; +}; + // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "SignInPage" 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.test.tsx b/packages/core-components/src/layout/Sidebar/Bar.test.tsx new file mode 100644 index 0000000000..d288aa94c5 --- /dev/null +++ b/packages/core-components/src/layout/Sidebar/Bar.test.tsx @@ -0,0 +1,108 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import BuildRoundedIcon from '@material-ui/icons/BuildRounded'; +import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; +import MenuBookIcon from '@material-ui/icons/MenuBook'; +import AcUnitIcon from '@material-ui/icons/AcUnit'; +import { Sidebar, SidebarExpandButton } from './Bar'; +import { SidebarItem, SidebarSearchField } from './Items'; +import { SidebarSubmenuItem } from './SidebarSubmenuItem'; +import { SidebarSubmenu } from './SidebarSubmenu'; +import { SidebarPinStateContext } from '.'; + +async function renderScalableSidebar() { + await renderInTestApp( + {} }} + > + + {}} to="/search" /> + {}} text="Catalog"> + + + + + + + + + , + ); +} + +describe('Sidebar', () => { + beforeEach(async () => { + await renderScalableSidebar(); + }); + + describe('Click to Expand', () => { + it('Sidebar should show expanded items when expand button is clicked', async () => { + 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(screen.queryByText('Create...')).not.toBeInTheDocument(); + }); + }); + describe('Submenu Items', () => { + it('Extended sidebar with submenu content hidden by default', async () => { + expect(screen.queryByText('Tools')).not.toBeInTheDocument(); + expect(screen.queryByText('Misc')).not.toBeInTheDocument(); + }); + + it('Extended sidebar with submenu content visible when hover over submenu items', async () => { + userEvent.hover(screen.getByTestId('item-with-submenu')); + expect(await screen.findByText('Tools')).toBeInTheDocument(); + expect(await screen.findByText('Misc')).toBeInTheDocument(); + }); + + it('Multicategory item in submenu shows drop down on click', async () => { + userEvent.hover(screen.getByTestId('item-with-submenu')); + userEvent.click(screen.getByText('Misc')); + expect(screen.getByText('dropdown item 1')).toBeInTheDocument(); + expect(screen.getByText('dropdown item 2')).toBeInTheDocument(); + }); + + it('Dropdown item in submenu renders a link when `to` value is provided', async () => { + userEvent.hover(screen.getByTestId('item-with-submenu')); + userEvent.click(screen.getByText('Misc')); + expect(screen.getByText('dropdown item 1').closest('a')).toHaveAttribute( + 'href', + '/dropdownitemlink', + ); + }); + }); +}); diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index e6fa3820ad..775c537695 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -17,10 +17,12 @@ import { makeStyles } from '@material-ui/core/styles'; import useMediaQuery from '@material-ui/core/useMediaQuery'; import clsx from 'clsx'; -import React, { useRef, useState, useContext, PropsWithChildren } from 'react'; +import React, { useState, useContext, PropsWithChildren, useRef } from 'react'; import { sidebarConfig, SidebarContext } from './config'; import { BackstageTheme } from '@backstage/theme'; import { SidebarPinStateContext } from './Page'; +import DoubleArrowRight from './icons/DoubleArrowRight'; +import DoubleArrowLeft from './icons/DoubleArrowLeft'; export type SidebarClassKey = 'root' | 'drawer' | 'drawerOpen'; @@ -46,7 +48,6 @@ const useStyles = makeStyles( msOverflowStyle: 'none', scrollbarWidth: 'none', width: sidebarConfig.drawerWidthClosed, - borderRight: `1px solid #383838`, transition: theme.transitions.create('width', { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.shortest, @@ -58,6 +59,19 @@ const useStyles = makeStyles( display: 'none', }, }, + expandButton: { + background: 'none', + border: 'none', + color: theme.palette.navigation.color, + width: '100%', + cursor: 'pointer', + position: 'relative', + height: 48, + }, + arrows: { + position: 'absolute', + right: 10, + }, drawerOpen: { width: sidebarConfig.drawerWidthOpen, transition: theme.transitions.create('width', { @@ -78,10 +92,12 @@ enum State { type Props = { openDelayMs?: number; closeDelayMs?: number; + disableExpandOnHover?: boolean; }; export function Sidebar(props: PropsWithChildren) { const { + disableExpandOnHover = false, openDelayMs = sidebarConfig.defaultOpenDelayMs, closeDelayMs = sidebarConfig.defaultCloseDelayMs, children, @@ -132,18 +148,27 @@ export function Sidebar(props: PropsWithChildren) { const isOpen = (state === State.Open && !isSmallScreen) || isPinned; + const setOpen = (open: boolean) => { + if (open) { + handleOpen(); + } else { + handleClose(); + } + }; + return (
{} : handleOpen} + onFocus={disableExpandOnHover ? () => {} : handleOpen} + onMouseLeave={disableExpandOnHover ? () => {} : handleClose} + onBlur={disableExpandOnHover ? () => {} : handleClose} >
) {
); } + +/** + * A button which allows you to expand the sidebar when clicked. + * Use optionally to replace sidebar's expand-on-hover feature with expand-on-click. + * + * @public + */ +export const SidebarExpandButton = () => { + const classes = useStyles(); + const { isOpen, setOpen } = useContext(SidebarContext); + const { isPinned } = useContext(SidebarPinStateContext); + const isSmallScreen = useMediaQuery(theme => + theme.breakpoints.down('md'), + ); + + const handleClick = () => { + setOpen(!isOpen); + }; + + if (isPinned || isSmallScreen) { + return null; + } + + return ( + + ); +}; diff --git a/packages/core-components/src/layout/Sidebar/Items.test.tsx b/packages/core-components/src/layout/Sidebar/Items.test.tsx index 123c30ceae..f4d699d09c 100644 --- a/packages/core-components/src/layout/Sidebar/Items.test.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.test.tsx @@ -20,7 +20,7 @@ import { createEvent, fireEvent, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import HomeIcon from '@material-ui/icons/Home'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; -import { Sidebar } from './Bar'; +import { Sidebar, SidebarExpandButton } from './Bar'; import { SidebarItem, SidebarSearchField } from './Items'; import { renderHook } from '@testing-library/react-hooks'; import { hexToRgb, makeStyles } from '@material-ui/core/styles'; @@ -44,6 +44,7 @@ async function renderSidebar() { text="Create..." className={result.current.spotlight} /> + , ); userEvent.hover(screen.getByTestId('sidebar-root')); diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index 3f15ae4ea6..a41c3e4bdd 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -14,18 +14,21 @@ * limitations under the License. */ -import { IconComponent } from '@backstage/core-plugin-api'; +import { IconComponent, useElementFilter } from '@backstage/core-plugin-api'; import { BackstageTheme } from '@backstage/theme'; import { makeStyles, styled, Theme } from '@material-ui/core/styles'; import Badge from '@material-ui/core/Badge'; import TextField from '@material-ui/core/TextField'; import Typography from '@material-ui/core/Typography'; import { CreateCSSProperties } from '@material-ui/core/styles/withStyles'; +import ArrowRightIcon from '@material-ui/icons/ArrowRight'; import SearchIcon from '@material-ui/icons/Search'; import clsx from 'clsx'; import React, { + Children, forwardRef, KeyboardEventHandler, + PropsWithChildren, ReactNode, useContext, useState, @@ -33,10 +36,16 @@ import React, { import { Link, NavLinkProps, + resolvePath, useLocation, useResolvedPath, } from 'react-router-dom'; -import { sidebarConfig, SidebarContext } from './config'; +import { + sidebarConfig, + SidebarContext, + SidebarItemWithSubmenuContext, +} from './config'; +import { SidebarSubmenu } from './SidebarSubmenu'; export type SidebarItemClassKey = | 'root' @@ -85,6 +94,16 @@ const useStyles = makeStyles( open: { width: drawerWidthOpen, }, + highlightable: { + '&:hover': { + 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', @@ -123,13 +142,24 @@ const useStyles = makeStyles( textAlign: 'center', marginRight: theme.spacing(1), }, + closedItemIcon: { + width: '100%', + justifyContent: 'center', + }, + submenuArrow: { + position: 'absolute', + right: 0, + }, selected: { '&$root': { borderLeft: `solid ${selectedIndicatorWidth}px ${theme.palette.navigation.indicator}`, color: theme.palette.navigation.selectedColor, }, '&$closed': { - width: drawerWidthClosed - selectedIndicatorWidth, + width: drawerWidthClosed, + }, + '& $closedItemIcon': { + paddingRight: selectedIndicatorWidth, }, '& $iconContainer': { marginLeft: -selectedIndicatorWidth, @@ -140,16 +170,124 @@ const useStyles = makeStyles( { name: 'BackstageSidebarItem' }, ); +function isSidebarItemWithSubmenuActive( + submenu: ReactNode, + locationPathname: string, +) { + // Item is active if any of submenu items have active paths + const toPathnames: string[] = []; + let isActive = false; + let submenuItems: ReactNode; + Children.forEach(submenu, element => { + if (!React.isValidElement(element)) return; + submenuItems = element.props.children; + }); + Children.forEach(submenuItems, element => { + if (!React.isValidElement(element)) return; + if (element.props.dropdownItems) { + element.props.dropdownItems.map((item: { to: string }) => + toPathnames.push(item.to), + ); + } else if (element.props.to) { + toPathnames.push(element.props.to); + } + }); + isActive = toPathnames.some(to => { + const toPathname = resolvePath(to); + return locationPathname === toPathname.pathname; + }); + return isActive; +} + +const SidebarItemWithSubmenu = ({ + text, + hasNotifications = false, + icon: Icon, + children, +}: PropsWithChildren) => { + const classes = useStyles(); + const [isHoveredOn, setIsHoveredOn] = useState(false); + const { pathname: locationPathname } = useLocation(); + const isActive = isSidebarItemWithSubmenuActive(children, locationPathname); + + const handleMouseEnter = () => { + setIsHoveredOn(true); + }; + const handleMouseLeave = () => { + setIsHoveredOn(false); + }; + + const { isOpen } = useContext(SidebarContext); + const itemIcon = ( + + + + ); + const openContent = ( + <> +
+ {itemIcon} +
+ {text && ( + + {text} + + )} +
{}
+ + ); + const closedContent = itemIcon; + + return ( + +
+
+ {isOpen ? openContent : closedContent} + {!isHoveredOn && ( + + )} +
+ {isHoveredOn && children} +
+
+ ); +}; + type SidebarItemBaseProps = { icon: IconComponent; text?: string; hasNotifications?: boolean; - children?: ReactNode; + disableHighlight?: boolean; className?: string; }; type SidebarItemButtonProps = SidebarItemBaseProps & { onClick: (ev: React.MouseEvent) => void; + children?: ReactNode; }; type SidebarItemLinkProps = SidebarItemBaseProps & { @@ -157,7 +295,21 @@ type SidebarItemLinkProps = SidebarItemBaseProps & { onClick?: (ev: React.MouseEvent) => void; } & NavLinkProps; -type SidebarItemProps = SidebarItemButtonProps | SidebarItemLinkProps; +type SidebarItemWithSubmenuProps = SidebarItemBaseProps & { + to?: string; + onClick?: (ev: React.MouseEvent) => void; + children: ReactNode; +}; + +/** + * SidebarItem with 'to' property will be a clickable link. + * SidebarItem with 'onClick' property and without 'to' property will be a clickable button. + * SidebarItem which wraps a SidebarSubmenu will be a clickable button which opens a submenu. + */ +type SidebarItemProps = + | SidebarItemLinkProps + | SidebarItemButtonProps + | SidebarItemWithSubmenuProps; function isButtonItem( props: SidebarItemProps, @@ -218,6 +370,7 @@ export const SidebarItem = forwardRef((props, ref) => { icon: Icon, text, hasNotifications = false, + disableHighlight = false, onClick, children, className, @@ -235,6 +388,7 @@ export const SidebarItem = forwardRef((props, ref) => { variant="dot" overlap="circular" invisible={!hasNotifications} + className={clsx({ [classes.closedItemIcon]: !isOpen })} > @@ -248,7 +402,7 @@ export const SidebarItem = forwardRef((props, ref) => { {itemIcon}
{text && ( - + {text} )} @@ -265,9 +419,43 @@ export const SidebarItem = forwardRef((props, ref) => { classes.root, isOpen ? classes.open : classes.closed, isButtonItem(props) && classes.buttonItem, + { [classes.highlightable]: !disableHighlight }, ), }; + let hasSubmenu = false; + let submenu: ReactNode; + const componentType = ( + + <> + + ).type; + // Filter children for SidebarSubmenu components + const submenus = useElementFilter(children, elements => + elements.getElements().filter(child => child.type === componentType), + ); + // Error thrown if more than one SidebarSubmenu in a SidebarItem + if (submenus.length > 1) { + throw new Error( + 'Cannot render more than one SidebarSubmenu inside a SidebarItem', + ); + } else if (submenus.length === 1) { + hasSubmenu = true; + submenu = submenus[0]; + } + + if (hasSubmenu) { + return ( + + {submenu} + + ); + } + if (isButtonItem(props)) { return ( + {dropdownItems && showDropDown && ( +
+ {dropdownItems.map((object, key) => ( + + + {object.title} + + + ))} +
+ )} + + ); + } + + return ( +
+ + + + {title} + + +
+ ); +}; diff --git a/packages/core-components/src/layout/Sidebar/config.ts b/packages/core-components/src/layout/Sidebar/config.ts index d2a690e38f..dbcdb7d788 100644 --- a/packages/core-components/src/layout/Sidebar/config.ts +++ b/packages/core-components/src/layout/Sidebar/config.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createContext } from 'react'; +import { createContext, Dispatch, SetStateAction } from 'react'; const drawerWidthClosed = 72; const iconPadding = 24; @@ -37,13 +37,43 @@ export const sidebarConfig = { userBadgeDiameter: drawerWidthClosed - userBadgePadding * 2, }; +export const submenuConfig = { + drawerWidthClosed: 0, + drawerWidthOpen: 202, + // As per NN/g's guidance on timing for exposing hidden content + // See https://www.nngroup.com/articles/timing-exposing-content/ + defaultOpenDelayMs: 100, + defaultCloseDelayMs: 0, + defaultFadeDuration: 200, + logoHeight: 32, + iconContainerWidth: drawerWidthClosed, + iconSize: drawerWidthClosed - iconPadding * 2, + iconPadding, + selectedIndicatorWidth: 3, + userBadgePadding, + userBadgeDiameter: drawerWidthClosed - userBadgePadding * 2, +}; + export const SIDEBAR_INTRO_LOCAL_STORAGE = '@backstage/core/sidebar-intro-dismissed'; export type SidebarContextType = { isOpen: boolean; + setOpen: (open: boolean) => void; }; export const SidebarContext = createContext({ isOpen: false, + setOpen: _open => {}, }); + +export type SidebarItemWithSubmenuContextType = { + isHoveredOn: boolean; + setIsHoveredOn: Dispatch>; +}; + +export const SidebarItemWithSubmenuContext = + createContext({ + isHoveredOn: false, + setIsHoveredOn: () => {}, + }); diff --git a/packages/core-components/src/layout/Sidebar/icons/DoubleArrowLeft.tsx b/packages/core-components/src/layout/Sidebar/icons/DoubleArrowLeft.tsx new file mode 100644 index 0000000000..eb487940a8 --- /dev/null +++ b/packages/core-components/src/layout/Sidebar/icons/DoubleArrowLeft.tsx @@ -0,0 +1,48 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos'; + +const useStyles = makeStyles({ + iconContainer: { + display: 'flex', + position: 'relative', + width: '100%', + }, + arrow1: { + right: '6px', + position: 'absolute', + }, +}); + +const DoubleArrowLeft = () => { + const classes = useStyles(); + + return ( +
+
+ +
+
+ +
+
+ ); +}; + +export default DoubleArrowLeft; diff --git a/packages/core-components/src/layout/Sidebar/icons/DoubleArrowRight.tsx b/packages/core-components/src/layout/Sidebar/icons/DoubleArrowRight.tsx new file mode 100644 index 0000000000..e1e9b9476d --- /dev/null +++ b/packages/core-components/src/layout/Sidebar/icons/DoubleArrowRight.tsx @@ -0,0 +1,48 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { makeStyles } from '@material-ui/core/styles'; +import ArrowForwardIosIcon from '@material-ui/icons/ArrowForwardIos'; + +const useStyles = makeStyles({ + iconContainer: { + display: 'flex', + position: 'relative', + width: '100%', + }, + arrow1: { + right: '6px', + position: 'absolute', + }, +}); + +const DoubleArrowRight = () => { + const classes = useStyles(); + + return ( +
+
+ +
+
+ +
+
+ ); +}; + +export default DoubleArrowRight; diff --git a/packages/core-components/src/layout/Sidebar/index.ts b/packages/core-components/src/layout/Sidebar/index.ts index 067a9bf2eb..79a86023ee 100644 --- a/packages/core-components/src/layout/Sidebar/index.ts +++ b/packages/core-components/src/layout/Sidebar/index.ts @@ -14,7 +14,14 @@ * limitations under the License. */ -export { Sidebar } from './Bar'; +export { Sidebar, SidebarExpandButton } from './Bar'; +export { SidebarSubmenuItem } from './SidebarSubmenuItem'; +export { SidebarSubmenu } from './SidebarSubmenu'; +export type { SidebarSubmenuProps } from './SidebarSubmenu'; +export type { + SidebarSubmenuItemProps, + SidebarSubmenuItemDropdownItem, +} from './SidebarSubmenuItem'; export type { SidebarClassKey } from './Bar'; export { SidebarPage, SidebarPinStateContext } from './Page'; export type { SidebarPinStateContextType, SidebarPageClassKey } from './Page'; diff --git a/packages/storybook/.storybook/apis.js b/packages/storybook/.storybook/apis.js index 2750324c17..4c0813a8e9 100644 --- a/packages/storybook/.storybook/apis.js +++ b/packages/storybook/.storybook/apis.js @@ -10,6 +10,7 @@ import { OktaAuth, Auth0Auth, ConfigReader, + LocalStorageFeatureFlags, } from '@backstage/core-app-api'; import { @@ -24,9 +25,11 @@ import { oktaAuthApiRef, auth0AuthApiRef, configApiRef, + featureFlagsApiRef, } from '@backstage/core-plugin-api'; const configApi = new ConfigReader({}); +const featureFlagsApi = new LocalStorageFeatureFlags(); const alertApi = new AlertApiForwarder(); const errorApi = new ErrorAlerter(alertApi, new ErrorApiForwarder()); const identityApi = { @@ -69,6 +72,7 @@ const oauth2Api = OAuth2.create({ export const apis = [ [configApiRef, configApi], + [featureFlagsApiRef, featureFlagsApi], [alertApiRef, alertApi], [errorApiRef, errorApi], [identityApiRef, identityApi], diff --git a/packages/theme/api-report.md b/packages/theme/api-report.md index 2dc6ab7b55..f650636710 100644 --- a/packages/theme/api-report.md +++ b/packages/theme/api-report.md @@ -41,6 +41,12 @@ export type BackstagePaletteAdditions = { indicator: string; color: string; selectedColor: string; + navItem?: { + hoverBackground: string; + }; + submenu?: { + background: string; + }; }; tabbar: { indicator: string; diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index 7e99ce8e8a..b048c70fec 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -76,6 +76,12 @@ export const lightTheme = createTheme({ indicator: '#9BF0E1', color: '#b5b5b5', selectedColor: '#FFF', + navItem: { + hoverBackground: '#404040', + }, + submenu: { + background: '#404040', + }, }, pinSidebarButton: { icon: '#181818', @@ -151,6 +157,12 @@ export const darkTheme = createTheme({ indicator: '#9BF0E1', color: '#b5b5b5', selectedColor: '#FFF', + navItem: { + hoverBackground: '#404040', + }, + submenu: { + background: '#404040', + }, }, pinSidebarButton: { icon: '#404040', diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index e1acba3a01..3d1a64c38f 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -53,6 +53,12 @@ export type BackstagePaletteAdditions = { indicator: string; color: string; selectedColor: string; + navItem?: { + hoverBackground: string; + }; + submenu?: { + background: string; + }; }; tabbar: { indicator: string; diff --git a/plugins/shortcuts/src/ShortcutItem.test.tsx b/plugins/shortcuts/src/ShortcutItem.test.tsx index 0b5c5fc919..e080026f00 100644 --- a/plugins/shortcuts/src/ShortcutItem.test.tsx +++ b/plugins/shortcuts/src/ShortcutItem.test.tsx @@ -37,7 +37,7 @@ describe('ShortcutItem', () => { it('displays the shortcut', async () => { await renderInTestApp( - + {} }}> , ); diff --git a/plugins/shortcuts/src/Shortcuts.test.tsx b/plugins/shortcuts/src/Shortcuts.test.tsx index e0a70d6069..8cee9a6622 100644 --- a/plugins/shortcuts/src/Shortcuts.test.tsx +++ b/plugins/shortcuts/src/Shortcuts.test.tsx @@ -29,7 +29,7 @@ import { SidebarContext } from '@backstage/core-components'; describe('Shortcuts', () => { it('displays an add button', async () => { await renderInTestApp( - + {} }}>