diff --git a/.changeset/fluffy-countries-beam.md b/.changeset/fluffy-countries-beam.md
new file mode 100644
index 0000000000..f542cfd33b
--- /dev/null
+++ b/.changeset/fluffy-countries-beam.md
@@ -0,0 +1,10 @@
+---
+'@backstage/plugin-catalog': patch
+'@backstage/plugin-cost-insights': patch
+'@backstage/plugin-shortcuts': patch
+'@backstage/plugin-user-settings': patch
+---
+
+**@backstage/plugin-user-settings:** Hide Header on mobile screens to improve the UI & give more space to the content. Furthermore, the "Pin Sidebar" setting is removed on mobile screens, as the mobile sidebar is always pinned to the bottom.
+
+**Other plugins:** Smaller style adjustments across plugins to improve the UI on mobile devices.
diff --git a/.changeset/great-bulldogs-provide.md b/.changeset/great-bulldogs-provide.md
new file mode 100644
index 0000000000..3b51ea9bdf
--- /dev/null
+++ b/.changeset/great-bulldogs-provide.md
@@ -0,0 +1,11 @@
+---
+'@backstage/core-components': patch
+---
+
+The `Bar` component will now render a `MobileSidebar` instead of the current sidebar on smaller screens. The state of the `MobileSidebar` will be treated as always open.
+
+---
+
+**Add MobileSidebar:** A navigation component, which sticks to the bottom. If there is no content in the Sidebar, it won't be rendered. If there are `children ` in the `Sidebar`, but no `SidebarGroup`s as `children`, it will render all `children` into a default overlay menu, which can be displayed by clicking a menu item. If `SidebarGroup`s are provided, it will render them in the bottom navigation. Additionally, a `MobileSidebarContext`, which wraps the component, will save the selected menu item.
+
+**Add SidebarGroup:** Groups items of the `Sidebar` together. On bigger screens, this won't have any effect at the moment. On smaller screens, it will render a given icon into the `MobileSidebar`. If a route is provided, clicking the `SidebarGroup` in the `MobileSidebar` will route to the page. If no route is provided, it will add a provided icon to the `MobileSidebar` as a menu item & will render the children into an overlay menu, which will be displayed when the menu item is clicked.
diff --git a/.changeset/stale-brooms-fry.md b/.changeset/stale-brooms-fry.md
new file mode 100644
index 0000000000..18c847973e
--- /dev/null
+++ b/.changeset/stale-brooms-fry.md
@@ -0,0 +1,51 @@
+---
+'@backstage/create-app': patch
+---
+
+You can now add `SidebarGroup`s to the current `Sidebar`. This will not affect how the current sidebar is displayed, but allows a customization on how the `MobileSidebar` on smaller screens will look like. A `SidebarGroup` will be displayed with the given icon in the `MobileSidebar`.
+
+A `SidebarGroup` can either link to an existing page (e.g. `/search` or `/settings`) or wrap components, which will be displayed in a full-screen overlay menu (e.g. `Menu`).
+
+```diff
+
+
++ } to="/search">
+
++
+
++ }>
+
+
+
+
+
+
++
+
+
++ }
++ to="/settings"
++ >
+
++
+
+```
+
+Additionally, you can order the groups differently in the `MobileSidebar` than in the usual `Sidebar` simply by giving a group a priority. The groups will be displayed in descending order from left to right.
+
+```diff
+}
+ to="/settings"
++ priority={1}
+>
+
+
+```
+
+If you decide against adding `SidebarGroup`s to your `Sidebar` the `MobileSidebar` will contain one default menu item, which will open a full-screen overlay menu displaying all the content of the current `Sidebar`.
+
+More information on the `SidebarGroup` & the `MobileSidebar` component can be found in the changeset for the `core-components`.
diff --git a/docs/getting-started/configure-app-with-plugins.md b/docs/getting-started/configure-app-with-plugins.md
index 746b4bd940..1bfabc335c 100644
--- a/docs/getting-started/configure-app-with-plugins.md
+++ b/docs/getting-started/configure-app-with-plugins.md
@@ -90,3 +90,29 @@ extension to `.icon.svg`. For example:
```ts
import InternalToolIcon from './internal-tool.icon.svg';
```
+
+On mobile devices the `Sidebar` is displayed at the bottom of the screen. For
+customizing the experience you can group `SidebarItems` in a `SidebarGroup`
+(Example 1) or create a `SidebarGroup` with a link (Example 2). All
+`SidebarGroup`s are displayed in the bottom navigation with an icon.
+
+```ts
+// Example 1
+} label="Menu">
+ ...
+
+ ...
+
+```
+
+```ts
+// Example 2
+} to="/search">
+ ...
+
+ ...
+
+```
+
+If no `SidebarGroup` is provided a default menu will display the `Sidebar`
+content.
diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index 2d9b9b97f1..57f1fa536f 100644
--- a/packages/app/src/components/Root/Root.tsx
+++ b/packages/app/src/components/Root/Root.tsx
@@ -23,12 +23,17 @@ 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 SearchIcon from '@material-ui/icons/Search';
+import MenuIcon from '@material-ui/icons/Menu';
import MoneyIcon from '@material-ui/icons/MonetizationOn';
import LogoFull from './LogoFull';
import LogoIcon from './LogoIcon';
import { NavLink } from 'react-router-dom';
import { GraphiQLIcon } from '@backstage/plugin-graphiql';
-import { Settings as SidebarSettings } from '@backstage/plugin-user-settings';
+import {
+ Settings as SidebarSettings,
+ UserSettingsSignInAvatar,
+} from '@backstage/plugin-user-settings';
import {
SidebarSearchModal,
SearchContextProvider,
@@ -36,15 +41,15 @@ import {
import { Shortcuts } from '@backstage/plugin-shortcuts';
import {
Sidebar,
- SidebarPage,
sidebarConfig,
SidebarContext,
- SidebarItem,
SidebarDivider,
- SidebarSpace,
+ SidebarGroup,
+ SidebarItem,
+ SidebarPage,
SidebarScrollWrapper,
+ SidebarSpace,
} from '@backstage/core-components';
-import { AzurePullRequestsIcon } from '@backstage/plugin-azure-devops';
const useSidebarLogoStyles = makeStyles({
root: {
@@ -83,34 +88,43 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
-
-
-
+ } to="/search">
+
+
+
+
- {/* Global nav, not org-specific */}
-
-
-
-
-
- {/* End global nav */}
-
-
-
-
-
-
-
-
-
-
+ }>
+ {/* Global nav, not org-specific */}
+
+
+
+
+
+ {/* End global nav */}
+
+
+
+
+
+
+
+
+
+
-
+ }
+ to="/settings"
+ >
+
+
{children}
diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx
index c99bb78c84..9a8c208103 100644
--- a/packages/app/src/components/search/SearchPage.tsx
+++ b/packages/app/src/components/search/SearchPage.tsx
@@ -21,6 +21,7 @@ import {
Header,
Lifecycle,
Page,
+ SidebarPinStateContext,
} from '@backstage/core-components';
import { CatalogResultListItem } from '@backstage/plugin-catalog';
import {
@@ -33,7 +34,7 @@ import {
} from '@backstage/plugin-search';
import { DocsResultListItem } from '@backstage/plugin-techdocs';
import { Grid, List, makeStyles, Paper, Theme } from '@material-ui/core';
-import React from 'react';
+import React, { useContext } from 'react';
const useStyles = makeStyles((theme: Theme) => ({
bar: {
@@ -52,9 +53,11 @@ const useStyles = makeStyles((theme: Theme) => ({
const SearchPage = () => {
const classes = useStyles();
+ const { isMobile } = useContext(SidebarPinStateContext);
+
return (
- } />
+ {!isMobile && } />}
@@ -62,37 +65,39 @@ const SearchPage = () => {
-
- ,
- },
- {
- value: 'techdocs',
- name: 'Documentation',
- icon: ,
- },
- ]}
- />
-
-
+ ,
+ },
+ {
+ value: 'techdocs',
+ name: 'Documentation',
+ icon: ,
+ },
+ ]}
/>
-
-
-
-
+
+
+
+
+
+ )}
+
{({ results }) => (
diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md
index 29847b079e..8b5d1c61a7 100644
--- a/packages/core-components/api-report.md
+++ b/packages/core-components/api-report.md
@@ -10,6 +10,7 @@ import { BackstageIdentityApi } from '@backstage/core-plugin-api';
import { BackstagePalette } from '@backstage/theme';
import { BackstageTheme } from '@backstage/theme';
import { BackstageUserIdentity } from '@backstage/core-plugin-api';
+import { BottomNavigationActionProps } from '@material-ui/core/BottomNavigationAction';
import { ButtonProps as ButtonProps_2 } from '@material-ui/core/Button';
import { CardHeaderProps } from '@material-ui/core/CardHeader';
import { Column } from '@material-table/core';
@@ -105,7 +106,7 @@ export type BottomLinkProps = {
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
//
// @public
-export function Breadcrumbs(props: Props_20): JSX.Element;
+export function Breadcrumbs(props: Props_19): JSX.Element;
// @public (undocumented)
export type BreadcrumbsClickableTextClassKey = 'root';
@@ -696,6 +697,14 @@ export function MissingAnnotationEmptyState(props: Props_3): JSX.Element;
// @public (undocumented)
export type MissingAnnotationEmptyStateClassKey = 'code';
+// @public
+export const MobileSidebar: (props: MobileSidebarProps) => JSX.Element | null;
+
+// @public
+export type MobileSidebarProps = {
+ children?: React_2.ReactNode;
+};
+
// Warning: (ae-missing-release-tag) "OAuthRequestDialog" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -845,11 +854,8 @@ export type SelectItem = {
value: string | number;
};
-// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
-// Warning: (ae-missing-release-tag) "Sidebar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
-//
-// @public (undocumented)
-export function Sidebar(props: PropsWithChildren): JSX.Element;
+// @public
+export const Sidebar: (props: SidebarProps) => JSX.Element;
// Warning: (ae-missing-release-tag) "SIDEBAR_INTRO_LOCAL_STORAGE" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -857,10 +863,8 @@ export function Sidebar(props: PropsWithChildren): JSX.Element;
export const SIDEBAR_INTRO_LOCAL_STORAGE =
'@backstage/core/sidebar-intro-dismissed';
-// Warning: (ae-missing-release-tag) "SidebarClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
-//
// @public (undocumented)
-export type SidebarClassKey = 'root' | 'drawer' | 'drawerOpen';
+export type SidebarClassKey = 'drawer' | 'drawerOpen';
// Warning: (ae-missing-release-tag) "sidebarConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -878,16 +882,17 @@ export const sidebarConfig: {
selectedIndicatorWidth: number;
userBadgePadding: number;
userBadgeDiameter: number;
+ mobileSidebarHeight: number;
};
// Warning: (ae-missing-release-tag) "SidebarContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
-// @public (undocumented)
+// @public
export const SidebarContext: Context;
// Warning: (ae-missing-release-tag) "SidebarContextType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
-// @public (undocumented)
+// @public
export type SidebarContextType = {
isOpen: boolean;
setOpen: (open: boolean) => void;
@@ -1170,6 +1175,16 @@ export type SidebarDividerClassKey = 'root';
// @public
export const SidebarExpandButton: () => JSX.Element | null;
+// @public
+export const SidebarGroup: (props: SidebarGroupProps) => JSX.Element;
+
+// @public
+export interface SidebarGroupProps extends BottomNavigationActionProps {
+ children?: React_2.ReactNode;
+ priority?: number;
+ to?: string;
+}
+
// 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)
@@ -1186,19 +1201,19 @@ export type SidebarIntroClassKey =
// Warning: (ae-forgotten-export) The symbol "SidebarItemProps" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "SidebarItem" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
-// @public (undocumented)
+// @public
export const SidebarItem: React_2.ForwardRefExoticComponent<
SidebarItemProps & React_2.RefAttributes
>;
-// Warning: (ae-missing-release-tag) "SidebarItemClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
-//
// @public (undocumented)
export type SidebarItemClassKey =
| 'root'
| 'buttonItem'
| 'closed'
| 'open'
+ | 'highlightable'
+ | 'highlighted'
| 'label'
| 'iconContainer'
| 'searchRoot'
@@ -1206,29 +1221,43 @@ export type SidebarItemClassKey =
| 'searchFieldHTMLInput'
| 'searchContainer'
| 'secondaryAction'
+ | 'closedItemIcon'
+ | 'submenuArrow'
+ | 'expandButton'
+ | 'arrows'
| 'selected';
// Warning: (ae-missing-release-tag) "SidebarPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-export function SidebarPage(props: PropsWithChildren<{}>): JSX.Element;
+export function SidebarPage(props: SidebarPageProps): JSX.Element;
// Warning: (ae-missing-release-tag) "SidebarPageClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type SidebarPageClassKey = 'root';
-// Warning: (ae-missing-release-tag) "SidebarPinStateContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
-//
-// @public (undocumented)
+// @public
+export type SidebarPageProps = {
+ children?: React_2.ReactNode;
+};
+
+// @public
export const SidebarPinStateContext: React_2.Context;
-// Warning: (ae-missing-release-tag) "SidebarPinStateContextType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
-//
-// @public (undocumented)
+// @public
export type SidebarPinStateContextType = {
isPinned: boolean;
toggleSidebarPinState: () => any;
+ isMobile?: boolean;
+};
+
+// @public (undocumented)
+export type SidebarProps = {
+ openDelayMs?: number;
+ closeDelayMs?: number;
+ disableExpandOnHover?: boolean;
+ children?: React_2.ReactNode;
};
// Warning: (ae-missing-release-tag) "SidebarScrollWrapper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -2086,7 +2115,7 @@ export type SidebarSubmenuProps = {
// Warning: (ae-missing-release-tag) "SignInPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-export function SignInPage(props: Props_18): JSX.Element;
+export function SignInPage(props: Props_17): JSX.Element;
// Warning: (ae-missing-release-tag) "SignInPageClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -2257,7 +2286,7 @@ export type TabBarClassKey = 'indicator' | 'flexContainer' | 'root';
// Warning: (ae-missing-release-tag) "TabbedCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-export function TabbedCard(props: PropsWithChildren): JSX.Element;
+export function TabbedCard(props: PropsWithChildren): JSX.Element;
// Warning: (ae-missing-release-tag) "TabbedCardClassKey" 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/Page/Page.tsx b/packages/core-components/src/layout/Page/Page.tsx
index 57653d23b5..b013affee3 100644
--- a/packages/core-components/src/layout/Page/Page.tsx
+++ b/packages/core-components/src/layout/Page/Page.tsx
@@ -20,7 +20,7 @@ import { makeStyles, ThemeProvider } from '@material-ui/core/styles';
export type PageClassKey = 'root';
-const useStyles = makeStyles(
+const useStyles = makeStyles(
() => ({
root: {
display: 'grid',
@@ -28,7 +28,7 @@ const useStyles = makeStyles(
"'pageHeader pageHeader pageHeader' 'pageSubheader pageSubheader pageSubheader' 'pageNav pageContent pageSidebar'",
gridTemplateRows: 'max-content auto 1fr',
gridTemplateColumns: 'auto 1fr auto',
- height: '100vh',
+ height: '100%',
overflowY: 'auto',
},
}),
diff --git a/packages/core-components/src/layout/Sidebar/Bar.test.tsx b/packages/core-components/src/layout/Sidebar/Bar.test.tsx
index d288aa94c5..89133997f9 100644
--- a/packages/core-components/src/layout/Sidebar/Bar.test.tsx
+++ b/packages/core-components/src/layout/Sidebar/Bar.test.tsx
@@ -14,24 +14,32 @@
* limitations under the License.
*/
-import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
+import AcUnitIcon from '@material-ui/icons/AcUnit';
+import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
+import BuildRoundedIcon from '@material-ui/icons/BuildRounded';
+import MenuBookIcon from '@material-ui/icons/MenuBook';
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 '.';
+import React from 'react';
+import {
+ Sidebar,
+ SidebarExpandButton,
+ SidebarItem,
+ SidebarSearchField,
+ SidebarPinStateContext,
+ SidebarSubmenu,
+ SidebarSubmenuItem,
+} from '.';
async function renderScalableSidebar() {
await renderInTestApp(
{} }}
+ value={{
+ isPinned: false,
+ isMobile: false,
+ toggleSidebarPinState: () => {},
+ }}
>
{}} to="/search" />
diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx
index bada68a028..ff907cdda3 100644
--- a/packages/core-components/src/layout/Sidebar/Bar.tsx
+++ b/packages/core-components/src/layout/Sidebar/Bar.tsx
@@ -17,23 +17,17 @@
import { makeStyles } from '@material-ui/core/styles';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import classnames from 'classnames';
-import React, { useState, useContext, PropsWithChildren, useRef } from 'react';
+import React, { useState, useContext, 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';
+import { MobileSidebar } from './MobileSidebar';
-export type SidebarClassKey = 'root' | 'drawer' | 'drawerOpen';
+/** @public */
+export type SidebarClassKey = 'drawer' | 'drawerOpen';
const useStyles = makeStyles(
theme => ({
- root: {
- zIndex: 1000,
- position: 'relative',
- overflow: 'visible',
- width: theme.spacing(7) + 1,
- },
drawer: {
display: 'flex',
flexFlow: 'column nowrap',
@@ -42,7 +36,7 @@ const useStyles = makeStyles(
left: 0,
top: 0,
bottom: 0,
- padding: 0,
+ zIndex: theme.zIndex.appBar,
background: theme.palette.navigation.background,
overflowX: 'hidden',
msOverflowStyle: 'none',
@@ -59,19 +53,6 @@ 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', {
@@ -89,29 +70,44 @@ enum State {
Open,
}
-type Props = {
+/** @public */
+export type SidebarProps = {
openDelayMs?: number;
closeDelayMs?: number;
disableExpandOnHover?: boolean;
+ children?: React.ReactNode;
};
-export function Sidebar(props: PropsWithChildren) {
+/**
+ * Places the Sidebar & wraps the children providing context weather the `Sidebar` is open or not.
+ *
+ * Handles & delays hover events for expanding the `Sidebar`
+ *
+ * @param props `disableExpandOnHover` disables the default hover behaviour;
+ * `openDelayMs` & `closeDelayMs` set delay until sidebar will open/close on hover
+ * @returns
+ * @internal
+ */
+const DesktopSidebar = (props: SidebarProps) => {
const {
- disableExpandOnHover = false,
openDelayMs = sidebarConfig.defaultOpenDelayMs,
closeDelayMs = sidebarConfig.defaultCloseDelayMs,
+ disableExpandOnHover,
children,
} = props;
const classes = useStyles();
- const isSmallScreen = useMediaQuery(theme =>
- theme.breakpoints.down('md'),
+ const isSmallScreen = useMediaQuery(
+ theme => theme.breakpoints.down('md'),
+ { noSsr: true },
);
const [state, setState] = useState(State.Closed);
const hoverTimerRef = useRef();
- const { isPinned } = useContext(SidebarPinStateContext);
+ const { isPinned, toggleSidebarPinState } = useContext(
+ SidebarPinStateContext,
+ );
const handleOpen = () => {
- if (isPinned) {
+ if (isPinned || disableExpandOnHover) {
return;
}
if (hoverTimerRef.current) {
@@ -129,7 +125,7 @@ export function Sidebar(props: PropsWithChildren) {
};
const handleClose = () => {
- if (isPinned) {
+ if (isPinned || disableExpandOnHover) {
return;
}
if (hoverTimerRef.current) {
@@ -148,73 +144,60 @@ export function Sidebar(props: PropsWithChildren) {
const isOpen = (state === State.Open && !isSmallScreen) || isPinned;
+ /**
+ * Close/Open Sidebar directily without delays. Also toggles `SidebarPinState` to avoid hidden content behind Sidebar.
+ */
const setOpen = (open: boolean) => {
if (open) {
- handleOpen();
+ setState(State.Open);
+ toggleSidebarPinState();
} else {
- handleClose();
+ setState(State.Closed);
+ toggleSidebarPinState();
}
};
return (
-