Merge pull request #7521 from hiba-aldalaty/scalable-sidebar-new-implementation
New Scalable Side Nav design Implementation
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
---
|
||||
'@backstage/core-components': patch
|
||||
'@backstage/theme': patch
|
||||
---
|
||||
|
||||
Added `<SidebarSubmenu>` and `<SidebarSubmenuItem>` 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`.
|
||||
@@ -832,6 +832,7 @@ export const SidebarContext: Context<SidebarContextType>;
|
||||
// @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<SidebarSubmenuProps>) => 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)
|
||||
//
|
||||
|
||||
@@ -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(
|
||||
<SidebarPinStateContext.Provider
|
||||
value={{ isPinned: false, toggleSidebarPinState: () => {} }}
|
||||
>
|
||||
<Sidebar disableExpandOnHover>
|
||||
<SidebarSearchField onSearch={() => {}} to="/search" />
|
||||
<SidebarItem icon={MenuBookIcon} onClick={() => {}} text="Catalog">
|
||||
<SidebarSubmenu title="Catalog">
|
||||
<SidebarSubmenuItem title="Tools" to="/1" icon={BuildRoundedIcon} />
|
||||
<SidebarSubmenuItem
|
||||
title="Misc"
|
||||
to="/6"
|
||||
icon={AcUnitIcon}
|
||||
dropdownItems={[
|
||||
{
|
||||
title: 'dropdown item 1',
|
||||
to: '/dropdownitemlink',
|
||||
},
|
||||
{
|
||||
title: 'dropdown item 2',
|
||||
to: '/dropdownitemlink2',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SidebarSubmenu>
|
||||
</SidebarItem>
|
||||
<SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />
|
||||
<SidebarExpandButton />
|
||||
</Sidebar>
|
||||
</SidebarPinStateContext.Provider>,
|
||||
);
|
||||
}
|
||||
|
||||
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',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<BackstageTheme>(
|
||||
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<BackstageTheme>(
|
||||
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<Props>) {
|
||||
const {
|
||||
disableExpandOnHover = false,
|
||||
openDelayMs = sidebarConfig.defaultOpenDelayMs,
|
||||
closeDelayMs = sidebarConfig.defaultCloseDelayMs,
|
||||
children,
|
||||
@@ -132,18 +148,27 @@ export function Sidebar(props: PropsWithChildren<Props>) {
|
||||
|
||||
const isOpen = (state === State.Open && !isSmallScreen) || isPinned;
|
||||
|
||||
const setOpen = (open: boolean) => {
|
||||
if (open) {
|
||||
handleOpen();
|
||||
} else {
|
||||
handleClose();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classes.root}
|
||||
onMouseEnter={handleOpen}
|
||||
onFocus={handleOpen}
|
||||
onMouseLeave={handleClose}
|
||||
onBlur={handleClose}
|
||||
data-testid="sidebar-root"
|
||||
onMouseEnter={disableExpandOnHover ? () => {} : handleOpen}
|
||||
onFocus={disableExpandOnHover ? () => {} : handleOpen}
|
||||
onMouseLeave={disableExpandOnHover ? () => {} : handleClose}
|
||||
onBlur={disableExpandOnHover ? () => {} : handleClose}
|
||||
>
|
||||
<SidebarContext.Provider
|
||||
value={{
|
||||
isOpen,
|
||||
setOpen,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -157,3 +182,39 @@ export function Sidebar(props: PropsWithChildren<Props>) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<BackstageTheme>(theme =>
|
||||
theme.breakpoints.down('md'),
|
||||
);
|
||||
|
||||
const handleClick = () => {
|
||||
setOpen(!isOpen);
|
||||
};
|
||||
|
||||
if (isPinned || isSmallScreen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className={classes.expandButton}
|
||||
aria-label="Expand Sidebar"
|
||||
data-testid="sidebar-expand-button"
|
||||
>
|
||||
<div className={classes.arrows}>
|
||||
{isOpen ? <DoubleArrowLeft /> : <DoubleArrowRight />}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
<SidebarExpandButton />
|
||||
</Sidebar>,
|
||||
);
|
||||
userEvent.hover(screen.getByTestId('sidebar-root'));
|
||||
|
||||
@@ -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<BackstageTheme>(
|
||||
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<BackstageTheme>(
|
||||
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<BackstageTheme>(
|
||||
{ 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<SidebarItemWithSubmenuProps>) => {
|
||||
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 = (
|
||||
<Badge
|
||||
color="secondary"
|
||||
variant="dot"
|
||||
overlap="circular"
|
||||
className={isOpen ? '' : classes.closedItemIcon}
|
||||
invisible={!hasNotifications}
|
||||
>
|
||||
<Icon fontSize="small" />
|
||||
</Badge>
|
||||
);
|
||||
const openContent = (
|
||||
<>
|
||||
<div data-testid="login-button" className={classes.iconContainer}>
|
||||
{itemIcon}
|
||||
</div>
|
||||
{text && (
|
||||
<Typography variant="subtitle2" className={classes.text}>
|
||||
{text}
|
||||
</Typography>
|
||||
)}
|
||||
<div className={classes.secondaryAction}>{}</div>
|
||||
</>
|
||||
);
|
||||
const closedContent = itemIcon;
|
||||
|
||||
return (
|
||||
<SidebarItemWithSubmenuContext.Provider
|
||||
value={{
|
||||
isHoveredOn,
|
||||
setIsHoveredOn,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onMouseLeave={handleMouseLeave}
|
||||
className={clsx(isHoveredOn && classes.highlighted)}
|
||||
>
|
||||
<div
|
||||
onMouseEnter={handleMouseEnter}
|
||||
data-testid="item-with-submenu"
|
||||
className={clsx(
|
||||
classes.root,
|
||||
isOpen ? classes.open : classes.closed,
|
||||
isActive && classes.selected,
|
||||
classes.highlightable,
|
||||
isHoveredOn && classes.highlighted,
|
||||
)}
|
||||
>
|
||||
{isOpen ? openContent : closedContent}
|
||||
{!isHoveredOn && (
|
||||
<ArrowRightIcon fontSize="small" className={classes.submenuArrow} />
|
||||
)}
|
||||
</div>
|
||||
{isHoveredOn && children}
|
||||
</div>
|
||||
</SidebarItemWithSubmenuContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
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<any, SidebarItemProps>((props, ref) => {
|
||||
icon: Icon,
|
||||
text,
|
||||
hasNotifications = false,
|
||||
disableHighlight = false,
|
||||
onClick,
|
||||
children,
|
||||
className,
|
||||
@@ -235,6 +388,7 @@ export const SidebarItem = forwardRef<any, SidebarItemProps>((props, ref) => {
|
||||
variant="dot"
|
||||
overlap="circular"
|
||||
invisible={!hasNotifications}
|
||||
className={clsx({ [classes.closedItemIcon]: !isOpen })}
|
||||
>
|
||||
<Icon fontSize="small" />
|
||||
</Badge>
|
||||
@@ -248,7 +402,7 @@ export const SidebarItem = forwardRef<any, SidebarItemProps>((props, ref) => {
|
||||
{itemIcon}
|
||||
</div>
|
||||
{text && (
|
||||
<Typography variant="subtitle2" className={classes.label}>
|
||||
<Typography variant="subtitle2" className={classes.text}>
|
||||
{text}
|
||||
</Typography>
|
||||
)}
|
||||
@@ -265,9 +419,43 @@ export const SidebarItem = forwardRef<any, SidebarItemProps>((props, ref) => {
|
||||
classes.root,
|
||||
isOpen ? classes.open : classes.closed,
|
||||
isButtonItem(props) && classes.buttonItem,
|
||||
{ [classes.highlightable]: !disableHighlight },
|
||||
),
|
||||
};
|
||||
|
||||
let hasSubmenu = false;
|
||||
let submenu: ReactNode;
|
||||
const componentType = (
|
||||
<SidebarSubmenu>
|
||||
<></>
|
||||
</SidebarSubmenu>
|
||||
).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 (
|
||||
<SidebarItemWithSubmenu
|
||||
text={text}
|
||||
icon={Icon}
|
||||
hasNotifications={hasNotifications}
|
||||
>
|
||||
{submenu}
|
||||
</SidebarItemWithSubmenu>
|
||||
);
|
||||
}
|
||||
|
||||
if (isButtonItem(props)) {
|
||||
return (
|
||||
<button aria-label={text} {...childProps} ref={ref}>
|
||||
@@ -280,7 +468,7 @@ export const SidebarItem = forwardRef<any, SidebarItemProps>((props, ref) => {
|
||||
<WorkaroundNavLink
|
||||
{...childProps}
|
||||
activeClassName={classes.selected}
|
||||
to={props.to}
|
||||
to={props.to ? props.to : ''}
|
||||
ref={ref}
|
||||
aria-label={text ? text : props.to}
|
||||
{...navLinkProps}
|
||||
@@ -331,7 +519,12 @@ export function SidebarSearchField(props: SidebarSearchFieldProps) {
|
||||
|
||||
return (
|
||||
<div className={classes.searchRoot}>
|
||||
<SidebarItem icon={Icon} to={props.to} onClick={handleItemClick}>
|
||||
<SidebarItem
|
||||
icon={Icon}
|
||||
to={props.to}
|
||||
onClick={handleItemClick}
|
||||
disableHighlight
|
||||
>
|
||||
<TextField
|
||||
placeholder="Search"
|
||||
value={input}
|
||||
|
||||
@@ -16,16 +16,27 @@
|
||||
|
||||
import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline';
|
||||
import HomeOutlinedIcon from '@material-ui/icons/HomeOutlined';
|
||||
import BuildRoundedIcon from '@material-ui/icons/BuildRounded';
|
||||
import LibraryBooksOutlinedIcon from '@material-ui/icons/LibraryBooksOutlined';
|
||||
import WebOutlinedIcon from '@material-ui/icons/WebOutlined';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarDivider,
|
||||
SidebarExpandButton,
|
||||
SidebarIntro,
|
||||
SidebarItem,
|
||||
SidebarPage,
|
||||
SidebarSearchField,
|
||||
SidebarSpace,
|
||||
} from '.';
|
||||
import { SidebarSubmenuItem } from './SidebarSubmenuItem';
|
||||
import MenuBookIcon from '@material-ui/icons/MenuBook';
|
||||
import CloudQueueIcon from '@material-ui/icons/CloudQueue';
|
||||
import SettingsApplications from '@material-ui/icons/SettingsApplications';
|
||||
import AcUnitIcon from '@material-ui/icons/AcUnit';
|
||||
import { SidebarSubmenu } from './SidebarSubmenu';
|
||||
|
||||
export default {
|
||||
title: 'Layout/Sidebar',
|
||||
@@ -43,14 +54,62 @@ const handleSearch = (input: string) => {
|
||||
};
|
||||
|
||||
export const SampleSidebar = () => (
|
||||
<Sidebar>
|
||||
<SidebarSearchField onSearch={handleSearch} to="/search" />
|
||||
<SidebarDivider />
|
||||
<SidebarItem icon={HomeOutlinedIcon} to="#" text="Home" />
|
||||
<SidebarItem icon={HomeOutlinedIcon} to="#" text="Plugins" />
|
||||
<SidebarItem icon={AddCircleOutlineIcon} to="#" text="Create..." />
|
||||
<SidebarDivider />
|
||||
<SidebarIntro />
|
||||
<SidebarSpace />
|
||||
</Sidebar>
|
||||
<SidebarPage>
|
||||
<Sidebar>
|
||||
<SidebarSearchField onSearch={handleSearch} to="/search" />
|
||||
<SidebarDivider />
|
||||
<SidebarItem icon={HomeOutlinedIcon} to="#" text="Plugins" />
|
||||
<SidebarItem icon={AddCircleOutlineIcon} to="#" text="Create..." />
|
||||
<SidebarDivider />
|
||||
<SidebarIntro />
|
||||
<SidebarSpace />
|
||||
</Sidebar>
|
||||
</SidebarPage>
|
||||
);
|
||||
|
||||
export const SampleScalableSidebar = () => (
|
||||
<SidebarPage>
|
||||
<Sidebar disableExpandOnHover>
|
||||
<SidebarSearchField onSearch={handleSearch} to="/search" />
|
||||
<SidebarDivider />
|
||||
<SidebarItem icon={MenuBookIcon} text="Catalog">
|
||||
<SidebarSubmenu title="Catalog">
|
||||
<SidebarSubmenuItem title="Tools" to="/1" icon={BuildRoundedIcon} />
|
||||
<SidebarSubmenuItem title="APIs" to="/2" icon={CloudQueueIcon} />
|
||||
<SidebarSubmenuItem
|
||||
title="Services"
|
||||
to="/3"
|
||||
icon={SettingsApplications}
|
||||
/>
|
||||
<SidebarSubmenuItem
|
||||
title="Libraries"
|
||||
to="/4"
|
||||
icon={LibraryBooksOutlinedIcon}
|
||||
/>
|
||||
<SidebarSubmenuItem title="Websites" to="/5" icon={WebOutlinedIcon} />
|
||||
<SidebarSubmenuItem
|
||||
title="Misc"
|
||||
to="/6"
|
||||
icon={AcUnitIcon}
|
||||
dropdownItems={[
|
||||
{
|
||||
title: 'Lorem Ipsum',
|
||||
to: '/7',
|
||||
},
|
||||
{
|
||||
title: 'Lorem Ipsum',
|
||||
to: '/8',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</SidebarSubmenu>
|
||||
</SidebarItem>
|
||||
<SidebarItem icon={HomeOutlinedIcon} to="#" text="Plugins" />
|
||||
<SidebarItem icon={AddCircleOutlineIcon} to="#" text="Create..." />
|
||||
<SidebarDivider />
|
||||
<SidebarIntro />
|
||||
<SidebarSpace />
|
||||
<SidebarExpandButton />
|
||||
</Sidebar>
|
||||
</SidebarPage>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2021 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 { makeStyles } from '@material-ui/core/styles';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import clsx from 'clsx';
|
||||
import React, { PropsWithChildren, ReactNode, useContext } from 'react';
|
||||
import {
|
||||
SidebarItemWithSubmenuContext,
|
||||
sidebarConfig,
|
||||
SidebarContext,
|
||||
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,
|
||||
},
|
||||
drawer: {
|
||||
display: 'flex',
|
||||
flexFlow: 'column nowrap',
|
||||
alignItems: 'flex-start',
|
||||
position: 'fixed',
|
||||
left: props.left,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
padding: 0,
|
||||
background: theme.palette.navigation.submenu?.background ?? '#404040',
|
||||
overflowX: 'hidden',
|
||||
msOverflowStyle: 'none',
|
||||
scrollbarWidth: 'none',
|
||||
cursor: 'default',
|
||||
width: submenuConfig.drawerWidthClosed,
|
||||
borderRight: `1px solid #383838`,
|
||||
'& > *': {
|
||||
flexShrink: 0,
|
||||
},
|
||||
'&::-webkit-scrollbar': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
drawerOpen: {
|
||||
width: submenuConfig.drawerWidthOpen,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 500,
|
||||
color: '#FFF',
|
||||
padding: 20,
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* Holds a title for text Header of a sidebar submenu and children
|
||||
* components to be rendered inside SidebarSubmenu
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SidebarSubmenuProps = {
|
||||
title?: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* Used inside SidebarItem to display an expandable Submenu
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const SidebarSubmenu = ({
|
||||
title,
|
||||
children,
|
||||
}: PropsWithChildren<SidebarSubmenuProps>) => {
|
||||
const { isOpen } = useContext(SidebarContext);
|
||||
const left = isOpen
|
||||
? sidebarConfig.drawerWidthOpen
|
||||
: sidebarConfig.drawerWidthClosed;
|
||||
const props = { left: left };
|
||||
const classes = useStyles(props)();
|
||||
|
||||
const { isHoveredOn } = useContext(SidebarItemWithSubmenuContext);
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes.drawer, {
|
||||
[classes.drawerOpen]: isHoveredOn,
|
||||
})}
|
||||
>
|
||||
<Typography variant="h5" className={classes.title}>
|
||||
{title}
|
||||
</Typography>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright 2021 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, { useContext, useState } from 'react';
|
||||
import {
|
||||
NavLink,
|
||||
resolvePath,
|
||||
useLocation,
|
||||
useResolvedPath,
|
||||
} from 'react-router-dom';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Link from '@material-ui/core/Link';
|
||||
import { IconComponent } from '@backstage/core-plugin-api';
|
||||
import clsx from 'clsx';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
|
||||
import ArrowDropUpIcon from '@material-ui/icons/ArrowDropUp';
|
||||
import { SidebarItemWithSubmenuContext } from './config';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
item: {
|
||||
height: 48,
|
||||
width: '100%',
|
||||
'&:hover': {
|
||||
background: '#6f6f6f',
|
||||
color: theme.palette.navigation.selectedColor,
|
||||
},
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: theme.palette.navigation.color,
|
||||
padding: 20,
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
},
|
||||
itemContainer: {
|
||||
width: '100%',
|
||||
},
|
||||
selected: {
|
||||
background: '#6f6f6f',
|
||||
color: '#FFF',
|
||||
},
|
||||
label: {
|
||||
margin: 14,
|
||||
marginLeft: 7,
|
||||
fontSize: 14,
|
||||
},
|
||||
dropdownArrow: {
|
||||
position: 'absolute',
|
||||
right: 21,
|
||||
},
|
||||
dropdown: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'end',
|
||||
},
|
||||
dropdownItem: {
|
||||
width: '100%',
|
||||
padding: '10px 0 10px 0',
|
||||
},
|
||||
textContent: {
|
||||
color: theme.palette.navigation.color,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
fontSize: '14px',
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* Clickable item displayed when submenu item is clicked.
|
||||
* title: Text content of item
|
||||
* to: Path to navigate to when item is clicked
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SidebarSubmenuItemDropdownItem = {
|
||||
title: string;
|
||||
to: string;
|
||||
};
|
||||
/**
|
||||
* Holds submenu item content.
|
||||
*
|
||||
* title: Text content of submenu item
|
||||
* to: Path to navigate to when item is clicked
|
||||
* icon: Icon displayed on the left of text content
|
||||
* dropdownItems: Optional array of dropdown items displayed when submenu item is clicked.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type SidebarSubmenuItemProps = {
|
||||
title: string;
|
||||
to: string;
|
||||
icon: IconComponent;
|
||||
dropdownItems?: SidebarSubmenuItemDropdownItem[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Item used inside a submenu within the sidebar.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => {
|
||||
const { title, to, icon: Icon, dropdownItems } = props;
|
||||
const classes = useStyles();
|
||||
const { pathname: locationPathname } = useLocation();
|
||||
const { pathname: toPathname } = useResolvedPath(to);
|
||||
const { setIsHoveredOn } = useContext(SidebarItemWithSubmenuContext);
|
||||
const closeSubmenu = () => {
|
||||
setIsHoveredOn(false);
|
||||
};
|
||||
|
||||
let isActive = locationPathname === toPathname;
|
||||
|
||||
const [showDropDown, setShowDropDown] = useState(false);
|
||||
const handleClickDropdown = () => {
|
||||
setShowDropDown(!showDropDown);
|
||||
};
|
||||
if (dropdownItems !== undefined) {
|
||||
dropdownItems.some(item => {
|
||||
const resolvedPath = resolvePath(item.to);
|
||||
isActive = locationPathname === resolvedPath.pathname;
|
||||
});
|
||||
return (
|
||||
<div className={classes.itemContainer}>
|
||||
<button
|
||||
onClick={handleClickDropdown}
|
||||
className={clsx(
|
||||
classes.item,
|
||||
isActive ? classes.selected : undefined,
|
||||
)}
|
||||
>
|
||||
<Icon fontSize="small" />
|
||||
<Typography variant="subtitle1" className={classes.label}>
|
||||
{title}
|
||||
</Typography>
|
||||
{showDropDown ? (
|
||||
<ArrowDropUpIcon className={classes.dropdownArrow} />
|
||||
) : (
|
||||
<ArrowDropDownIcon className={classes.dropdownArrow} />
|
||||
)}
|
||||
</button>
|
||||
{dropdownItems && showDropDown && (
|
||||
<div className={classes.dropdown}>
|
||||
{dropdownItems.map((object, key) => (
|
||||
<Link
|
||||
component={NavLink}
|
||||
to={object.to}
|
||||
underline="none"
|
||||
className={classes.dropdownItem}
|
||||
onClick={closeSubmenu}
|
||||
key={key}
|
||||
>
|
||||
<Typography className={classes.textContent}>
|
||||
{object.title}
|
||||
</Typography>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.itemContainer}>
|
||||
<Link
|
||||
component={NavLink}
|
||||
to={to}
|
||||
underline="none"
|
||||
className={clsx(classes.item, isActive ? classes.selected : undefined)}
|
||||
onClick={closeSubmenu}
|
||||
>
|
||||
<Icon fontSize="small" />
|
||||
<Typography variant="subtitle1" className={classes.label}>
|
||||
{title}
|
||||
</Typography>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<SidebarContextType>({
|
||||
isOpen: false,
|
||||
setOpen: _open => {},
|
||||
});
|
||||
|
||||
export type SidebarItemWithSubmenuContextType = {
|
||||
isHoveredOn: boolean;
|
||||
setIsHoveredOn: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const SidebarItemWithSubmenuContext =
|
||||
createContext<SidebarItemWithSubmenuContextType>({
|
||||
isHoveredOn: false,
|
||||
setIsHoveredOn: () => {},
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<div className={classes.iconContainer}>
|
||||
<div className={classes.arrow1}>
|
||||
<ArrowBackIosIcon style={{ fontSize: '12px' }} />
|
||||
</div>
|
||||
<div>
|
||||
<ArrowBackIosIcon style={{ fontSize: '12px' }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DoubleArrowLeft;
|
||||
@@ -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 (
|
||||
<div className={classes.iconContainer}>
|
||||
<div className={classes.arrow1}>
|
||||
<ArrowForwardIosIcon style={{ fontSize: '12px' }} />
|
||||
</div>
|
||||
<div>
|
||||
<ArrowForwardIosIcon style={{ fontSize: '12px' }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DoubleArrowRight;
|
||||
@@ -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';
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -41,6 +41,12 @@ export type BackstagePaletteAdditions = {
|
||||
indicator: string;
|
||||
color: string;
|
||||
selectedColor: string;
|
||||
navItem?: {
|
||||
hoverBackground: string;
|
||||
};
|
||||
submenu?: {
|
||||
background: string;
|
||||
};
|
||||
};
|
||||
tabbar: {
|
||||
indicator: string;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -53,6 +53,12 @@ export type BackstagePaletteAdditions = {
|
||||
indicator: string;
|
||||
color: string;
|
||||
selectedColor: string;
|
||||
navItem?: {
|
||||
hoverBackground: string;
|
||||
};
|
||||
submenu?: {
|
||||
background: string;
|
||||
};
|
||||
};
|
||||
tabbar: {
|
||||
indicator: string;
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('ShortcutItem', () => {
|
||||
|
||||
it('displays the shortcut', async () => {
|
||||
await renderInTestApp(
|
||||
<SidebarContext.Provider value={{ isOpen: true }}>
|
||||
<SidebarContext.Provider value={{ isOpen: true, setOpen: _open => {} }}>
|
||||
<ShortcutItem api={api} shortcut={shortcut} />
|
||||
</SidebarContext.Provider>,
|
||||
);
|
||||
|
||||
@@ -29,7 +29,7 @@ import { SidebarContext } from '@backstage/core-components';
|
||||
describe('Shortcuts', () => {
|
||||
it('displays an add button', async () => {
|
||||
await renderInTestApp(
|
||||
<SidebarContext.Provider value={{ isOpen: true }}>
|
||||
<SidebarContext.Provider value={{ isOpen: true, setOpen: _open => {} }}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user