Implement a grouping for mobile sidebar
Signed-off-by: Philipp Hugenroth <philipph@spotify.com>
This commit is contained in:
@@ -14,35 +14,44 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { useContext, PropsWithChildren } from 'react';
|
||||
import { Link, makeStyles } from '@material-ui/core';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import ExtensionIcon from '@material-ui/icons/Extension';
|
||||
import RuleIcon from '@material-ui/icons/AssignmentTurnedIn';
|
||||
import MapIcon from '@material-ui/icons/MyLocation';
|
||||
import LayersIcon from '@material-ui/icons/Layers';
|
||||
import LibraryBooks from '@material-ui/icons/LibraryBooks';
|
||||
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
|
||||
import MoneyIcon from '@material-ui/icons/MonetizationOn';
|
||||
import LogoFull from './LogoFull';
|
||||
import LogoIcon from './LogoIcon';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import {
|
||||
MobileSidebar,
|
||||
sidebarConfig,
|
||||
SidebarContext,
|
||||
SidebarDivider,
|
||||
SidebarGroup,
|
||||
SidebarItem,
|
||||
SidebarPage,
|
||||
SidebarScrollWrapper,
|
||||
SidebarSpace,
|
||||
} from '@backstage/core-components';
|
||||
import { GraphiQLIcon } from '@backstage/plugin-graphiql';
|
||||
import { Settings as SidebarSettings } from '@backstage/plugin-user-settings';
|
||||
import { SidebarSearch } from '@backstage/plugin-search';
|
||||
import { Shortcuts } from '@backstage/plugin-shortcuts';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarPage,
|
||||
sidebarConfig,
|
||||
SidebarContext,
|
||||
SidebarItem,
|
||||
SidebarDivider,
|
||||
SidebarSpace,
|
||||
SidebarScrollWrapper,
|
||||
Settings as SidebarSettings,
|
||||
UserSettingsSignInAvatar,
|
||||
} from '@backstage/plugin-user-settings';
|
||||
import {
|
||||
BottomNavigation,
|
||||
SidebarGroup,
|
||||
} from '@backstage/core-components';
|
||||
BottomNavigationAction,
|
||||
Link,
|
||||
makeStyles,
|
||||
} from '@material-ui/core';
|
||||
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
|
||||
import RuleIcon from '@material-ui/icons/AssignmentTurnedIn';
|
||||
import ExtensionIcon from '@material-ui/icons/Extension';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import LayersIcon from '@material-ui/icons/Layers';
|
||||
import LibraryBooks from '@material-ui/icons/LibraryBooks';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
import MoneyIcon from '@material-ui/icons/MonetizationOn';
|
||||
import MapIcon from '@material-ui/icons/MyLocation';
|
||||
import SearchIcon from '@material-ui/icons/Search';
|
||||
import React, { PropsWithChildren, useContext, useState } from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import LogoFull from './LogoFull';
|
||||
import LogoIcon from './LogoIcon';
|
||||
|
||||
const useSidebarLogoStyles = makeStyles({
|
||||
root: {
|
||||
@@ -77,37 +86,52 @@ const SidebarLogo = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Root = ({ children }: PropsWithChildren<{}>) => (
|
||||
<SidebarPage>
|
||||
<Sidebar>
|
||||
<SidebarLogo />
|
||||
<SidebarSearch />
|
||||
<SidebarDivider />
|
||||
{/* Global nav, not org-specific */}
|
||||
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
|
||||
<SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />
|
||||
<SidebarItem icon={LibraryBooks} to="docs" text="Docs" />
|
||||
<SidebarItem icon={LayersIcon} to="explore" text="Explore" />
|
||||
<SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />
|
||||
{/* End global nav */}
|
||||
<SidebarDivider />
|
||||
<SidebarScrollWrapper>
|
||||
<SidebarItem icon={MapIcon} to="tech-radar" text="Tech Radar" />
|
||||
<SidebarItem icon={RuleIcon} to="lighthouse" text="Lighthouse" />
|
||||
<SidebarItem icon={MoneyIcon} to="cost-insights" text="Cost Insights" />
|
||||
<SidebarItem icon={GraphiQLIcon} to="graphiql" text="GraphiQL" />
|
||||
</SidebarScrollWrapper>
|
||||
<SidebarDivider />
|
||||
<Shortcuts />
|
||||
<SidebarSpace />
|
||||
<SidebarDivider />
|
||||
<SidebarSettings />
|
||||
</Sidebar>
|
||||
<BottomNavigation>
|
||||
<SidebarGroup />
|
||||
<SidebarGroup />
|
||||
<SidebarGroup />
|
||||
</BottomNavigation>
|
||||
{children}
|
||||
</SidebarPage>
|
||||
);
|
||||
export const Root = ({ children }: PropsWithChildren<{}>) => {
|
||||
return (
|
||||
<SidebarPage>
|
||||
<MobileSidebar>
|
||||
<SidebarLogo />
|
||||
<SidebarGroup label="Search" icon={<SearchIcon />} to="/search">
|
||||
<SidebarSearch />
|
||||
</SidebarGroup>
|
||||
<SidebarDivider />
|
||||
{/* Global nav, not org-specific */}
|
||||
<SidebarGroup label="Menu" icon={<MenuIcon />}>
|
||||
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
|
||||
<SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />
|
||||
<SidebarItem icon={LibraryBooks} to="docs" text="Docs" />
|
||||
<SidebarItem icon={LayersIcon} to="explore" text="Explore" />
|
||||
<SidebarItem
|
||||
icon={CreateComponentIcon}
|
||||
to="create"
|
||||
text="Create..."
|
||||
/>
|
||||
{/* End global nav */}
|
||||
<SidebarDivider />
|
||||
<SidebarScrollWrapper>
|
||||
<SidebarItem icon={MapIcon} to="tech-radar" text="Tech Radar" />
|
||||
<SidebarItem icon={RuleIcon} to="lighthouse" text="Lighthouse" />
|
||||
<SidebarItem
|
||||
icon={MoneyIcon}
|
||||
to="cost-insights"
|
||||
text="Cost Insights"
|
||||
/>
|
||||
<SidebarItem icon={GraphiQLIcon} to="graphiql" text="GraphiQL" />
|
||||
</SidebarScrollWrapper>
|
||||
<SidebarDivider />
|
||||
<Shortcuts />
|
||||
</SidebarGroup>
|
||||
<SidebarSpace />
|
||||
<SidebarDivider />
|
||||
<SidebarGroup
|
||||
label="Settings"
|
||||
icon={<UserSettingsSignInAvatar />}
|
||||
to="/settings"
|
||||
>
|
||||
<SidebarSettings />
|
||||
</SidebarGroup>
|
||||
</MobileSidebar>
|
||||
{children}
|
||||
</SidebarPage>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* 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 {
|
||||
BottomNavigation as MUIBottomNavigation,
|
||||
makeStyles,
|
||||
} from '@material-ui/core';
|
||||
import React from 'react';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Filters for sidebar groups and reorders them to create a custom BottomNavigation
|
||||
*/
|
||||
export const BottomNavigation = ({ children }: React.PropsWithChildren<{}>) => {
|
||||
const classes = useStyles();
|
||||
const [value, setValue] = React.useState('recents');
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<{}>, newValue: string) => {
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<MUIBottomNavigation
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
className={classes.root}
|
||||
>
|
||||
{children}
|
||||
</MUIBottomNavigation>
|
||||
);
|
||||
};
|
||||
@@ -14,15 +14,50 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { BottomNavigationAction } from '@material-ui/core';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
import {
|
||||
BottomNavigationAction,
|
||||
BottomNavigationActionProps,
|
||||
makeStyles,
|
||||
} from '@material-ui/core';
|
||||
import React from 'react';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
|
||||
interface SidebarGroupProps extends BottomNavigationActionProps {
|
||||
to?: string;
|
||||
injectedSelected?: boolean;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles({
|
||||
label: {
|
||||
display: 'none',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* If the page is mobile it should be BottomNavigationAction - otherwise just a fragment
|
||||
* Links to page, which will be displayed
|
||||
* - If 'to' Prop is not defined it will render a Menu page out of the children (if children given)
|
||||
*/
|
||||
export const SidebarGroup = ({ children }: React.PropsWithChildren<{}>) => {
|
||||
return <BottomNavigationAction label="Recents" icon={<MenuIcon />} />;
|
||||
export const SidebarGroup = ({
|
||||
to,
|
||||
label,
|
||||
icon,
|
||||
onClick,
|
||||
injectedSelected,
|
||||
}: React.PropsWithChildren<SidebarGroupProps>) => {
|
||||
const classes = useStyles();
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<BottomNavigationAction
|
||||
label={label}
|
||||
icon={icon}
|
||||
component={NavLink}
|
||||
to={to ? to : location.pathname}
|
||||
onClick={onClick}
|
||||
value={to}
|
||||
selected={injectedSelected}
|
||||
classes={classes}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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 { BottomNavigation, Box, makeStyles } from '@material-ui/core';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router';
|
||||
import { SidebarGroup } from './Group';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
root: {
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
const OverlayMenu = ({ children }: React.PropsWithChildren<{}>) => (
|
||||
<Box
|
||||
style={{
|
||||
background: 'black',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'fixed',
|
||||
zIndex: 500,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
|
||||
let currentLocation: string;
|
||||
|
||||
/**
|
||||
* Filters for sidebar groups and reorders them to create a custom BottomNavigation
|
||||
*/
|
||||
export const MobileSidebar = ({ children }: React.PropsWithChildren<{}>) => {
|
||||
const classes = useStyles();
|
||||
const [value, setValue] = useState<number | undefined>();
|
||||
const [menu, setMenu] = useState<React.ReactNode | undefined>();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (menu && currentLocation !== location.pathname) {
|
||||
setMenu(undefined);
|
||||
setValue(undefined);
|
||||
}
|
||||
currentLocation = location.pathname;
|
||||
}, [location, menu]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{value && menu && <OverlayMenu children={menu} />}
|
||||
<BottomNavigation className={classes.root}>
|
||||
{React.Children.map(children, (child, index) => {
|
||||
if (
|
||||
React.isValidElement(child) &&
|
||||
(child as React.ReactElement).type === SidebarGroup
|
||||
) {
|
||||
if (index === value && !menu && !child.props.to) {
|
||||
setMenu(child.props.children);
|
||||
}
|
||||
return React.cloneElement(child, {
|
||||
injectedSelected: value
|
||||
? index === value
|
||||
: location.pathname === child.props.to,
|
||||
onClick: () => {
|
||||
if (index === value && menu) {
|
||||
setValue(undefined);
|
||||
setMenu(undefined);
|
||||
} else {
|
||||
setValue(index);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</BottomNavigation>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
@@ -25,19 +25,11 @@ import { sidebarConfig } from './config';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import { LocalStorage } from './localStorage';
|
||||
|
||||
export type SidebarPageClassKey = 'root';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme, { isPinned: boolean }>(
|
||||
{
|
||||
root: {
|
||||
width: '100%',
|
||||
minHeight: '100%',
|
||||
transition: 'padding-left 0.1s ease-out',
|
||||
paddingLeft: ({ isPinned }) =>
|
||||
isPinned
|
||||
? sidebarConfig.drawerWidthOpen
|
||||
: sidebarConfig.drawerWidthClosed,
|
||||
},
|
||||
const useStyles = makeStyles<BackstageTheme, { isPinned: boolean }>({
|
||||
root: {
|
||||
width: '100%',
|
||||
minHeight: '100%',
|
||||
transition: 'padding-left 0.1s ease-out',
|
||||
},
|
||||
{ name: 'BackstageSidebarPage' },
|
||||
);
|
||||
|
||||
@@ -45,5 +45,5 @@ export type SidebarContextType = {
|
||||
};
|
||||
|
||||
export const SidebarContext = createContext<SidebarContextType>({
|
||||
isOpen: false,
|
||||
isOpen: true,
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
export { Sidebar } from './Bar';
|
||||
export { BottomNavigation } from './BottomNavigation';
|
||||
export { MobileSidebar } from './MobileSidebar';
|
||||
export { SidebarGroup } from './Group';
|
||||
export { SidebarPage, SidebarPinStateContext } from './Page';
|
||||
export type { SidebarPinStateContextType, SidebarPageClassKey } from './Page';
|
||||
|
||||
Reference in New Issue
Block a user