Replace SidebarContext with versioned provider and hook.
Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
@@ -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}>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user