Replace SidebarPinStateContext with versioned provider and hook.

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2022-05-20 15:23:39 +02:00
parent 3533075dfd
commit da72da5dae
15 changed files with 208 additions and 70 deletions
@@ -21,7 +21,7 @@ import {
Header,
Lifecycle,
Page,
SidebarPinStateContext,
useSidebarPinState,
} from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { CatalogSearchResultListItem } from '@backstage/plugin-catalog';
@@ -40,7 +40,7 @@ import {
import { useSearch } from '@backstage/plugin-search-react';
import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs';
import { Grid, List, makeStyles, Paper, Theme } from '@material-ui/core';
import React, { useContext } from 'react';
import React from 'react';
const useStyles = makeStyles((theme: Theme) => ({
bar: {
@@ -59,7 +59,7 @@ const useStyles = makeStyles((theme: Theme) => ({
const SearchPage = () => {
const classes = useStyles();
const { isMobile } = useContext(SidebarPinStateContext);
const { isMobile } = useSidebarPinState();
const { types } = useSearch();
const catalogApi = useApi(catalogApiRef);
+10 -1
View File
@@ -1006,7 +1006,13 @@ export type SidebarPageProps = {
};
// @public
export const SidebarPinStateContext: React_2.Context<SidebarPinStateContextType>;
export const SidebarPinStateContextProvider: ({
children,
value,
}: {
children: ReactNode;
value: SidebarPinStateContextType;
}) => JSX.Element;
// @public
export type SidebarPinStateContextType = {
@@ -1451,6 +1457,9 @@ export class UserIdentity implements IdentityApi {
// @public
export const useSidebar: () => SidebarContextType;
// @public
export const useSidebarPinState: () => SidebarPinStateContextType;
// 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)
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import React, { useContext } from 'react';
import React from 'react';
import { BackstageTheme } from '@backstage/theme';
import { makeStyles, ThemeProvider } from '@material-ui/core/styles';
import { SidebarPinStateContext } from '../Sidebar/Page';
import { useSidebarPinState } from '../Sidebar/SidebarPinStateContext';
export type PageClassKey = 'root';
@@ -43,7 +43,7 @@ type Props = {
export function Page(props: Props) {
const { themeId, children } = props;
const { isMobile } = useContext(SidebarPinStateContext);
const { isMobile } = useSidebarPinState();
const classes = useStyles({ isMobile });
return (
<ThemeProvider
@@ -27,14 +27,14 @@ import {
SidebarExpandButton,
SidebarItem,
SidebarSearchField,
SidebarPinStateContext,
SidebarPinStateContextProvider,
SidebarSubmenu,
SidebarSubmenuItem,
} from '.';
async function renderScalableSidebar() {
await renderInTestApp(
<SidebarPinStateContext.Provider
<SidebarPinStateContextProvider
value={{
isPinned: false,
isMobile: false,
@@ -75,7 +75,7 @@ async function renderScalableSidebar() {
<SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />
<SidebarExpandButton />
</Sidebar>
</SidebarPinStateContext.Provider>,
</SidebarPinStateContextProvider>,
);
}
@@ -31,8 +31,9 @@ import {
SubmenuOptions,
} from './config';
import { BackstageTheme } from '@backstage/theme';
import { SidebarPinStateContext, useContent } from './Page';
import { useContent } from './Page';
import { SidebarContextProvider } from './SidebarContext';
import { useSidebarPinState } from './SidebarPinStateContext';
import { MobileSidebar } from './MobileSidebar';
/** @public */
@@ -133,9 +134,7 @@ const DesktopSidebar = (props: DesktopSidebarProps) => {
);
const [state, setState] = useState(State.Closed);
const hoverTimerRef = useRef<number>();
const { isPinned, toggleSidebarPinState } = useContext(
SidebarPinStateContext,
);
const { isPinned, toggleSidebarPinState } = useSidebarPinState();
const handleOpen = () => {
if (isPinned || disableExpandOnHover) {
@@ -226,7 +225,7 @@ export const Sidebar = (props: SidebarProps) => {
props.submenuOptions ?? {},
);
const { children, disableExpandOnHover, openDelayMs, closeDelayMs } = props;
const { isMobile } = useContext(SidebarPinStateContext);
const { isMobile } = useSidebarPinState();
return isMobile ? (
<MobileSidebar>{children}</MobileSidebar>
@@ -29,6 +29,7 @@ import { SidebarConfigContext, SidebarConfig } from './config';
import { BackstageTheme } from '@backstage/theme';
import { LocalStorage } from './localStorage';
import useMediaQuery from '@material-ui/core/useMediaQuery';
import { SidebarPinStateContextProvider } from './SidebarPinStateContext';
export type SidebarPageClassKey = 'root';
@@ -62,17 +63,6 @@ const useStyles = makeStyles<
{ name: 'BackstageSidebarPage' },
);
/**
* Type of `SidebarPinStateContext`
*
* @public
*/
export type SidebarPinStateContextType = {
isPinned: boolean;
toggleSidebarPinState: () => any;
isMobile?: boolean;
};
/**
* Props for SidebarPage
*
@@ -82,19 +72,6 @@ export type SidebarPageProps = {
children?: React.ReactNode;
};
/**
* Contains the state on how the `Sidebar` is rendered
*
* @public
*/
export const SidebarPinStateContext = createContext<SidebarPinStateContextType>(
{
isPinned: true,
toggleSidebarPinState: () => {},
isMobile: false,
},
);
type PageContextType = {
content: {
contentRef?: React.MutableRefObject<HTMLElement | null>;
@@ -137,7 +114,7 @@ export function SidebarPage(props: SidebarPageProps) {
const classes = useStyles({ isPinned, sidebarConfig });
return (
<SidebarPinStateContext.Provider
<SidebarPinStateContextProvider
value={{
isPinned,
toggleSidebarPinState,
@@ -147,7 +124,7 @@ export function SidebarPage(props: SidebarPageProps) {
<PageContext.Provider value={pageContext}>
<div className={classes.root}>{props.children}</div>
</PageContext.Provider>
</SidebarPinStateContext.Provider>
</SidebarPinStateContextProvider>
);
}
@@ -22,7 +22,7 @@ import BottomNavigationAction, {
import { makeStyles } from '@material-ui/core/styles';
import React, { useContext } from 'react';
import { useLocation } from 'react-router-dom';
import { SidebarPinStateContext } from '.';
import { useSidebarPinState } from '.';
import { Link } from '../../components';
import { SidebarConfigContext, SidebarConfig } from './config';
import { MobileSidebarContext } from './MobileSidebar';
@@ -122,7 +122,7 @@ const MobileSidebarGroup = (props: SidebarGroupProps) => {
*/
export const SidebarGroup = (props: SidebarGroupProps) => {
const { children, to, label, icon, value } = props;
const { isMobile } = useContext(SidebarPinStateContext);
const { isMobile } = useSidebarPinState();
return isMobile ? (
<MobileSidebarGroup to={to} label={label} icon={icon} value={value} />
@@ -0,0 +1,79 @@
/*
* 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 {
SidebarPinStateContextProvider,
useSidebarPinState,
} from './SidebarPinStateContext';
describe('SidebarContext', () => {
describe('SidebarContextProvider', () => {
it('should render children', async () => {
await renderInTestApp(
<SidebarPinStateContextProvider
value={{
isPinned: true,
isMobile: false,
toggleSidebarPinState: () => {},
}}
>
Child
</SidebarPinStateContextProvider>,
);
expect(await screen.findByText('Child')).toBeInTheDocument();
});
});
describe('useSidebar', () => {
it('does not need to be invoked within provider', () => {
const { result } = renderHook(() => useSidebarPinState());
expect(result.current.isPinned).toBe(true);
expect(result.current.isMobile).toBe(false);
expect(typeof result.current.toggleSidebarPinState).toBe('function');
});
it('should read and update state', async () => {
let actualValue = true;
const wrapper = ({ children }: { children: ReactNode }) => (
<SidebarPinStateContextProvider
value={{
isPinned: actualValue,
isMobile: false,
toggleSidebarPinState: () => {
actualValue = !actualValue;
},
}}
>
{children}
</SidebarPinStateContextProvider>
);
const { result } = renderHook(() => useSidebarPinState(), { wrapper });
expect(result.current.isPinned).toBe(true);
act(() => {
result.current.toggleSidebarPinState();
});
waitFor(() => {
expect(result.current.isPinned).toBe(false);
});
});
});
});
@@ -0,0 +1,79 @@
/*
* 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 {
createVersionedContext,
createVersionedValueMap,
} from '@backstage/version-bridge';
import React, { ReactNode, useContext } from 'react';
/**
* Type of `SidebarPinStateContext`
*
* @public
*/
export type SidebarPinStateContextType = {
isPinned: boolean;
toggleSidebarPinState: () => any;
isMobile?: boolean;
};
const VersionedSidebarPinStateContext = createVersionedContext<{
1: SidebarPinStateContextType;
}>('sidebar-pin-state-context');
/**
* Provides state for how the `Sidebar` is rendered
*
* @public
*/
export const SidebarPinStateContextProvider = ({
children,
value,
}: {
children: ReactNode;
value: SidebarPinStateContextType;
}) => (
<VersionedSidebarPinStateContext.Provider
value={createVersionedValueMap({ 1: value })}
>
{children}
</VersionedSidebarPinStateContext.Provider>
);
/**
* Hook to read and update sidebar pin state.
*
* @public
*/
export const useSidebarPinState = (): SidebarPinStateContextType => {
const versionedSidebarContext = useContext(VersionedSidebarPinStateContext);
// Invoked from outside a SidebarPinStateContextProvider: default value.
if (versionedSidebarContext === undefined) {
return {
isPinned: true,
toggleSidebarPinState: () => {},
isMobile: false,
};
}
const sidebarContext = versionedSidebarContext.atVersion(1);
if (sidebarContext === undefined) {
throw new Error('No context found for version 1.');
}
return sidebarContext;
};
@@ -27,16 +27,8 @@ export type {
SidebarSubmenuItemDropdownItem,
} from './SidebarSubmenuItem';
export type { SidebarClassKey, SidebarProps } from './Bar';
export {
SidebarPage,
SidebarPinStateContext as SidebarPinStateContext,
useContent,
} from './Page';
export type {
SidebarPinStateContextType as SidebarPinStateContextType,
SidebarPageClassKey,
SidebarPageProps,
} from './Page';
export { SidebarPage, useContent } from './Page';
export type { SidebarPageClassKey, SidebarPageProps } from './Page';
export {
SidebarDivider,
SidebarItem,
@@ -58,3 +50,8 @@ export { SIDEBAR_INTRO_LOCAL_STORAGE, sidebarConfig } from './config';
export type { SidebarOptions, SubmenuOptions } from './config';
export { SidebarContextProvider, useSidebar } from './SidebarContext';
export type { SidebarContextType } from './SidebarContext';
export {
SidebarPinStateContextProvider,
useSidebarPinState,
} from './SidebarPinStateContext';
export type { SidebarPinStateContextType } from './SidebarPinStateContext';
@@ -14,11 +14,11 @@
* limitations under the License.
*/
import { useCallback, useContext, useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { useTheme } from '@material-ui/core';
import { SidebarPinStateContext } from '@backstage/core-components';
import { useSidebarPinState } from '@backstage/core-components';
import { BackstageTheme } from '@backstage/theme';
import { Transformer } from '../transformer';
@@ -27,7 +27,7 @@ import { rules } from './rules';
/**
* Sidebar pinned state to be used in computing style injections.
*/
const useSidebar = () => useContext(SidebarPinStateContext);
const useSidebar = () => useSidebarPinState();
/**
* Process all rules and concatenate their definitions into a single style.
@@ -13,14 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InfoCard, SidebarPinStateContext } from '@backstage/core-components';
import { InfoCard, useSidebarPinState } from '@backstage/core-components';
import { List } from '@material-ui/core';
import React, { useContext } from 'react';
import React from 'react';
import { UserSettingsPinToggle } from './UserSettingsPinToggle';
import { UserSettingsThemeToggle } from './UserSettingsThemeToggle';
export const UserSettingsAppearanceCard = () => {
const { isMobile } = useContext(SidebarPinStateContext);
const { isMobile } = useSidebarPinState();
return (
<InfoCard title="Appearance" variant="gridItem">
@@ -18,14 +18,14 @@ import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils';
import { fireEvent } from '@testing-library/react';
import React from 'react';
import { UserSettingsPinToggle } from './UserSettingsPinToggle';
import { SidebarPinStateContext } from '@backstage/core-components';
import { SidebarPinStateContextProvider } from '@backstage/core-components';
describe('<UserSettingsPinToggle />', () => {
it('toggles the pin sidebar button', async () => {
const mockToggleFn = jest.fn();
const rendered = await renderWithEffects(
wrapInTestApp(
<SidebarPinStateContext.Provider
<SidebarPinStateContextProvider
value={{
isPinned: false,
isMobile: false,
@@ -33,7 +33,7 @@ describe('<UserSettingsPinToggle />', () => {
}}
>
<UserSettingsPinToggle />
</SidebarPinStateContext.Provider>,
</SidebarPinStateContextProvider>,
),
);
expect(rendered.getByText('Pin Sidebar')).toBeInTheDocument();
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { useContext } from 'react';
import React from 'react';
import {
ListItem,
ListItemSecondaryAction,
@@ -22,12 +22,10 @@ import {
Switch,
Tooltip,
} from '@material-ui/core';
import { SidebarPinStateContext } from '@backstage/core-components';
import { useSidebarPinState } from '@backstage/core-components';
export const UserSettingsPinToggle = () => {
const { isPinned, toggleSidebarPinState } = useContext(
SidebarPinStateContext,
);
const { isPinned, toggleSidebarPinState } = useSidebarPinState();
return (
<ListItem>
@@ -17,10 +17,10 @@
import {
Header,
Page,
SidebarPinStateContext,
TabbedLayout,
useSidebarPinState,
} from '@backstage/core-components';
import React, { useContext } from 'react';
import React from 'react';
import { useOutlet } from 'react-router';
import { useElementFilter } from '@backstage/core-plugin-api';
import { UserSettingsAuthProviders } from './AuthProviders';
@@ -33,7 +33,7 @@ type Props = {
};
export const SettingsPage = ({ providerSettings }: Props) => {
const { isMobile } = useContext(SidebarPinStateContext);
const { isMobile } = useSidebarPinState();
const outlet = useOutlet();
const tabs = useElementFilter(outlet, elements =>