Replace SidebarContext with versioned provider and hook.

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2022-05-20 13:00:51 +02:00
parent 685b44414a
commit 3533075dfd
16 changed files with 190 additions and 60 deletions
+3 -3
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { useContext, PropsWithChildren } from 'react';
import React, { PropsWithChildren } from 'react';
import { Link, makeStyles } from '@material-ui/core';
import HomeIcon from '@material-ui/icons/Home';
import ExtensionIcon from '@material-ui/icons/Extension';
@@ -39,13 +39,13 @@ import { Shortcuts } from '@backstage/plugin-shortcuts';
import {
Sidebar,
sidebarConfig,
SidebarContext,
SidebarDivider,
SidebarGroup,
SidebarItem,
SidebarPage,
SidebarScrollWrapper,
SidebarSpace,
useSidebar,
} from '@backstage/core-components';
import { MyGroupsSidebarItem } from '@backstage/plugin-org';
import GroupIcon from '@material-ui/icons/People';
@@ -68,7 +68,7 @@ const useSidebarLogoStyles = makeStyles({
const SidebarLogo = () => {
const classes = useSidebarLogoStyles();
const { isOpen } = useContext(SidebarContext);
const { isOpen } = useSidebar();
return (
<div className={classes.root}>
+10 -6
View File
@@ -16,7 +16,6 @@ import { CardHeaderProps } from '@material-ui/core/CardHeader';
import { Column } from '@material-table/core';
import { ComponentClass } from 'react';
import { ComponentProps } from 'react';
import { Context } from 'react';
import { default as CSS_2 } from 'csstype';
import { CSSProperties } from 'react';
import { ElementType } from 'react';
@@ -903,13 +902,15 @@ export const sidebarConfig: {
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
export const SidebarContext: Context<SidebarContextType>;
export const SidebarContextProvider: ({
children,
value,
}: {
children: ReactNode;
value: SidebarContextType;
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "SidebarContextType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type SidebarContextType = {
isOpen: boolean;
@@ -1447,6 +1448,9 @@ export class UserIdentity implements IdentityApi {
signOut(): Promise<void>;
}
// @public
export const useSidebar: () => SidebarContextType;
// Warning: (ae-missing-release-tag) "useSupportConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
+1
View File
@@ -37,6 +37,7 @@
"@backstage/core-plugin-api": "^1.0.2",
"@backstage/errors": "^1.0.0",
"@backstage/theme": "^0.2.15",
"@backstage/version-bridge": "^1.0.1",
"@material-table/core": "^3.1.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
@@ -25,7 +25,6 @@ import {
makeSidebarConfig,
makeSidebarSubmenuConfig,
SidebarConfig,
SidebarContext,
SidebarConfigContext,
SubmenuConfig,
SidebarOptions,
@@ -33,6 +32,7 @@ import {
} from './config';
import { BackstageTheme } from '@backstage/theme';
import { SidebarPinStateContext, useContent } from './Page';
import { SidebarContextProvider } from './SidebarContext';
import { MobileSidebar } from './MobileSidebar';
/** @public */
@@ -191,7 +191,7 @@ const DesktopSidebar = (props: DesktopSidebarProps) => {
return (
<nav style={{}} aria-label="sidebar nav">
<A11ySkipSidebar />
<SidebarContext.Provider value={{ isOpen, setOpen }}>
<SidebarContextProvider value={{ isOpen, setOpen }}>
<div
className={classes.root}
data-testid="sidebar-root"
@@ -208,7 +208,7 @@ const DesktopSidebar = (props: DesktopSidebarProps) => {
{children}
</div>
</div>
</SidebarContext.Provider>
</SidebarContextProvider>
</nav>
);
};
@@ -25,10 +25,10 @@ import { useLocalStorageValue } from '@react-hookz/web';
import {
SidebarConfigContext,
SidebarConfig,
SidebarContext,
SIDEBAR_INTRO_LOCAL_STORAGE,
} from './config';
import { SidebarDivider } from './Items';
import { useSidebar } from './SidebarContext';
/** @public */
export type SidebarIntroClassKey =
@@ -151,7 +151,7 @@ const recentlyViewedIntroText =
'And your recently viewed plugins will pop up here!';
export function SidebarIntro(_props: {}) {
const { isOpen } = useContext(SidebarContext);
const { isOpen } = useSidebar();
const defaultValue = {
starredItemsDismissed: false,
recentlyViewedItemsDismissed: false,
@@ -48,7 +48,6 @@ import {
useResolvedPath,
} from 'react-router-dom';
import {
SidebarContext,
SidebarConfigContext,
SidebarItemWithSubmenuContext,
SidebarConfig,
@@ -62,6 +61,7 @@ import DoubleArrowLeft from './icons/DoubleArrowLeft';
import DoubleArrowRight from './icons/DoubleArrowRight';
import { isLocationMatch } from './utils';
import { Location } from 'history';
import { useSidebar } from './SidebarContext';
/** @public */
export type SidebarItemClassKey =
@@ -369,7 +369,7 @@ const SidebarItemBase = forwardRef<any, SidebarItemProps>((props, ref) => {
// XXX (@koroeskohr): unsure this is optimal. But I just really didn't want to have the item component
// depend on the current location, and at least have it being optionally forced to selected.
// Still waiting on a Q answered to fine tune the implementation
const { isOpen } = useContext(SidebarContext);
const { isOpen } = useSidebar();
const divStyle =
!isOpen && hasSubmenu ? { display: 'flex', marginLeft: '24px' } : {};
@@ -671,7 +671,7 @@ export const SidebarScrollWrapper = styled('div')(({ theme }) => {
export const SidebarExpandButton = () => {
const { sidebarConfig } = useContext(SidebarConfigContext);
const classes = useMemoStyles(sidebarConfig);
const { isOpen, setOpen } = useContext(SidebarContext);
const { isOpen, setOpen } = useSidebar();
const isSmallScreen = useMediaQuery<BackstageTheme>(
theme => theme.breakpoints.down('md'),
{ noSsr: true },
@@ -27,8 +27,9 @@ import MenuIcon from '@material-ui/icons/Menu';
import { orderBy } from 'lodash';
import React, { createContext, useEffect, useState, useContext } from 'react';
import { useLocation } from 'react-router';
import { SidebarContextProvider } from './SidebarContext';
import { SidebarGroup } from './SidebarGroup';
import { SidebarConfigContext, SidebarContext, SidebarConfig } from './config';
import { SidebarConfigContext, SidebarConfig } from './config';
/**
* Type of `MobileSidebarContext`
@@ -207,7 +208,7 @@ export const MobileSidebar = (props: MobileSidebarProps) => {
!sidebarGroups[selectedMenuItemIndex].props.to;
return (
<SidebarContext.Provider value={{ isOpen: true, setOpen: () => {} }}>
<SidebarContextProvider value={{ isOpen: true, setOpen: () => {} }}>
<MobileSidebarContext.Provider
value={{ selectedMenuItemIndex, setSelectedMenuItemIndex }}
>
@@ -231,6 +232,6 @@ export const MobileSidebar = (props: MobileSidebarProps) => {
{sidebarGroups}
</BottomNavigation>
</MobileSidebarContext.Provider>
</SidebarContext.Provider>
</SidebarContextProvider>
);
};
@@ -0,0 +1,68 @@
/*
* Copyright 2022 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, { ReactNode } from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import { screen, waitFor } from '@testing-library/react';
import { renderHook, act } from '@testing-library/react-hooks';
import { SidebarContextProvider, useSidebar } from './SidebarContext';
describe('SidebarContext', () => {
describe('SidebarContextProvider', () => {
it('should render children', async () => {
await renderInTestApp(
<SidebarContextProvider value={{ isOpen: false, setOpen: () => {} }}>
Child
</SidebarContextProvider>,
);
expect(await screen.findByText('Child')).toBeInTheDocument();
});
});
describe('useSidebar', () => {
it('does not need to be invoked within provider', () => {
const { result } = renderHook(() => useSidebar());
expect(result.current.isOpen).toBe(false);
expect(typeof result.current.setOpen).toBe('function');
});
it('should read and update state', async () => {
let actualValue = true;
const wrapper = ({ children }: { children: ReactNode }) => (
<SidebarContextProvider
value={{
isOpen: actualValue,
setOpen: value => {
actualValue = value;
},
}}
>
{children}
</SidebarContextProvider>
);
const { result } = renderHook(() => useSidebar(), { wrapper });
expect(result.current.isOpen).toBe(true);
act(() => {
result.current.setOpen(false);
});
waitFor(() => {
expect(result.current.isOpen).toBe(false);
});
});
});
});
@@ -0,0 +1,78 @@
/*
* Copyright 2022 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, { ReactNode, useContext } from 'react';
import {
createVersionedContext,
createVersionedValueMap,
} from '@backstage/version-bridge';
/**
* Types for the `SidebarContext`
*
* @public
*/
export type SidebarContextType = {
isOpen: boolean;
setOpen: (open: boolean) => void;
};
const VersionedSidebarContext = createVersionedContext<{
1: SidebarContextType;
}>('sidebar-context');
/**
* Provides context for reading and updating sidebar state.
*
* @public
*/
export const SidebarContextProvider = ({
children,
value,
}: {
children: ReactNode;
value: SidebarContextType;
}) => (
<VersionedSidebarContext.Provider
value={createVersionedValueMap({ 1: value })}
>
{children}
</VersionedSidebarContext.Provider>
);
/**
* Hook to read and update sidebar state.
*
* @public
*/
export const useSidebar = (): SidebarContextType => {
const versionedSidebarContext = useContext(VersionedSidebarContext);
// Invoked from outside a SidbarContextProvider, return a default value.
if (versionedSidebarContext === undefined) {
return {
isOpen: false,
setOpen: () => {},
};
}
const sidebarContext = versionedSidebarContext.atVersion(1);
if (sidebarContext === undefined) {
throw new Error('No context found for version 1.');
}
return sidebarContext;
};
@@ -19,10 +19,10 @@ import classnames from 'classnames';
import React, { ReactNode, useContext, useEffect, useState } from 'react';
import {
SidebarItemWithSubmenuContext,
SidebarContext,
SidebarConfigContext,
SubmenuConfig,
} from './config';
import { useSidebar } from './SidebarContext';
import { BackstageTheme } from '@backstage/theme';
const useStyles = makeStyles<
@@ -105,7 +105,7 @@ export type SidebarSubmenuProps = {
* @public
*/
export const SidebarSubmenu = (props: SidebarSubmenuProps) => {
const { isOpen } = useContext(SidebarContext);
const { isOpen } = useSidebar();
const { sidebarConfig, submenuConfig } = useContext(SidebarConfigContext);
const left = isOpen
? sidebarConfig.drawerWidthOpen
@@ -101,22 +101,6 @@ export const makeSidebarSubmenuConfig = (
export const SIDEBAR_INTRO_LOCAL_STORAGE =
'@backstage/core/sidebar-intro-dismissed';
/**
* Types for the `SidebarContext`
*/
export type SidebarContextType = {
isOpen: boolean;
setOpen: (open: boolean) => void;
};
/**
* Context whether the `Sidebar` is open
*/
export const SidebarContext = createContext<SidebarContextType>({
isOpen: false,
setOpen: () => {},
});
export type SidebarConfigContextType = {
sidebarConfig: SidebarConfig;
submenuConfig: SubmenuConfig;
@@ -54,13 +54,7 @@ export type {
} from './Items';
export { IntroCard, SidebarIntro } from './Intro';
export type { SidebarIntroClassKey } from './Intro';
export {
SIDEBAR_INTRO_LOCAL_STORAGE,
SidebarContext,
sidebarConfig,
} from './config';
export type {
SidebarContextType,
SidebarOptions,
SubmenuOptions,
} from './config';
export { SIDEBAR_INTRO_LOCAL_STORAGE, sidebarConfig } from './config';
export type { SidebarOptions, SubmenuOptions } from './config';
export { SidebarContextProvider, useSidebar } from './SidebarContext';
export type { SidebarContextType } from './SidebarContext';
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { useContext, PropsWithChildren } from 'react';
import React, { PropsWithChildren } from 'react';
import { Link, makeStyles } from '@material-ui/core';
import HomeIcon from '@material-ui/icons/Home';
import ExtensionIcon from '@material-ui/icons/Extension';
@@ -32,13 +32,13 @@ import { SidebarSearchModal } from '@backstage/plugin-search';
import {
Sidebar,
sidebarConfig,
SidebarContext,
SidebarDivider,
SidebarGroup,
SidebarItem,
SidebarPage,
SidebarScrollWrapper,
SidebarSpace,
useSidebar,
} from '@backstage/core-components';
import MenuIcon from '@material-ui/icons/Menu';
import SearchIcon from '@material-ui/icons/Search';
@@ -60,7 +60,7 @@ const useSidebarLogoStyles = makeStyles({
const SidebarLogo = () => {
const classes = useSidebarLogoStyles();
const { isOpen } = useContext(SidebarContext);
const { isOpen } = useSidebar();
return (
<div className={classes.root}>
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { PropsWithChildren, useContext } from 'react';
import React, { PropsWithChildren } from 'react';
import { Link, makeStyles } from '@material-ui/core';
import LibraryBooks from '@material-ui/icons/LibraryBooks';
@@ -27,7 +27,7 @@ import {
SidebarPage,
sidebarConfig,
SidebarDivider,
SidebarContext,
useSidebar,
} from '@backstage/core-components';
import { NavLink } from 'react-router-dom';
@@ -48,7 +48,7 @@ const useSidebarLogoStyles = makeStyles({
const SidebarLogo = () => {
const classes = useSidebarLogoStyles();
const { isOpen } = useContext(SidebarContext);
const { isOpen } = useSidebar();
return (
<div className={classes.root}>
+3 -3
View File
@@ -20,7 +20,7 @@ import { ShortcutItem } from './ShortcutItem';
import { Shortcut } from './types';
import { LocalStoredShortcuts } from './api';
import { MockStorageApi, renderInTestApp } from '@backstage/test-utils';
import { SidebarContext } from '@backstage/core-components';
import { SidebarContextProvider } from '@backstage/core-components';
describe('ShortcutItem', () => {
const shortcut: Shortcut = {
@@ -32,9 +32,9 @@ describe('ShortcutItem', () => {
it('displays the shortcut', async () => {
await renderInTestApp(
<SidebarContext.Provider value={{ isOpen: true, setOpen: _open => {} }}>
<SidebarContextProvider value={{ isOpen: true, setOpen: _open => {} }}>
<ShortcutItem api={api} shortcut={shortcut} />
</SidebarContext.Provider>,
</SidebarContextProvider>,
);
expect(screen.getByText('ST')).toBeInTheDocument();
expect(screen.getByText('some title')).toBeInTheDocument();
+3 -3
View File
@@ -24,12 +24,12 @@ import { screen, waitFor } from '@testing-library/react';
import { Shortcuts } from './Shortcuts';
import { LocalStoredShortcuts, shortcutsApiRef } from './api';
import { SidebarContext } from '@backstage/core-components';
import { SidebarContextProvider } from '@backstage/core-components';
describe('Shortcuts', () => {
it('displays an add button', async () => {
await renderInTestApp(
<SidebarContext.Provider value={{ isOpen: true, setOpen: _open => {} }}>
<SidebarContextProvider value={{ isOpen: true, setOpen: _open => {} }}>
<TestApiProvider
apis={[
[
@@ -40,7 +40,7 @@ describe('Shortcuts', () => {
>
<Shortcuts />
</TestApiProvider>
</SidebarContext.Provider>,
</SidebarContextProvider>,
);
await waitFor(() => !screen.queryByTestId('progress'));
expect(screen.getByText('Add Shortcuts')).toBeInTheDocument();