diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index d1b56bbaf0..d23ad957d7 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -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); diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index e7ccc8e72b..d162e7a714 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -1006,7 +1006,13 @@ export type SidebarPageProps = { }; // @public -export const SidebarPinStateContext: React_2.Context; +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) diff --git a/packages/core-components/src/layout/Page/Page.tsx b/packages/core-components/src/layout/Page/Page.tsx index 9bfc16fea3..1abb8da93b 100644 --- a/packages/core-components/src/layout/Page/Page.tsx +++ b/packages/core-components/src/layout/Page/Page.tsx @@ -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 ( - , + , ); } diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index 2fdb2012dc..a315406b58 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -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(); - 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 ? ( {children} diff --git a/packages/core-components/src/layout/Sidebar/Page.tsx b/packages/core-components/src/layout/Sidebar/Page.tsx index 9bf8105f81..17cd5ddee1 100644 --- a/packages/core-components/src/layout/Sidebar/Page.tsx +++ b/packages/core-components/src/layout/Sidebar/Page.tsx @@ -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( - { - isPinned: true, - toggleSidebarPinState: () => {}, - isMobile: false, - }, -); - type PageContextType = { content: { contentRef?: React.MutableRefObject; @@ -137,7 +114,7 @@ export function SidebarPage(props: SidebarPageProps) { const classes = useStyles({ isPinned, sidebarConfig }); return ( -
{props.children}
-
+ ); } diff --git a/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx b/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx index 55b5e770b0..1158cc14df 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx @@ -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 ? ( diff --git a/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx new file mode 100644 index 0000000000..cc83a846e3 --- /dev/null +++ b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.test.tsx @@ -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( + {}, + }} + > + Child + , + ); + 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 }) => ( + { + actualValue = !actualValue; + }, + }} + > + {children} + + ); + const { result } = renderHook(() => useSidebarPinState(), { wrapper }); + + expect(result.current.isPinned).toBe(true); + + act(() => { + result.current.toggleSidebarPinState(); + }); + + waitFor(() => { + expect(result.current.isPinned).toBe(false); + }); + }); + }); +}); diff --git a/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx new file mode 100644 index 0000000000..709973e064 --- /dev/null +++ b/packages/core-components/src/layout/Sidebar/SidebarPinStateContext.tsx @@ -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; +}) => ( + + {children} + +); + +/** + * 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; +}; diff --git a/packages/core-components/src/layout/Sidebar/index.ts b/packages/core-components/src/layout/Sidebar/index.ts index a852b35bf8..e041ecc730 100644 --- a/packages/core-components/src/layout/Sidebar/index.ts +++ b/packages/core-components/src/layout/Sidebar/index.ts @@ -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'; diff --git a/plugins/techdocs/src/reader/transformers/styles/transformer.ts b/plugins/techdocs/src/reader/transformers/styles/transformer.ts index 26ab4ceb26..04a6d476af 100644 --- a/plugins/techdocs/src/reader/transformers/styles/transformer.ts +++ b/plugins/techdocs/src/reader/transformers/styles/transformer.ts @@ -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. diff --git a/plugins/user-settings/src/components/General/UserSettingsAppearanceCard.tsx b/plugins/user-settings/src/components/General/UserSettingsAppearanceCard.tsx index 15ab93c6d0..cd55218db5 100644 --- a/plugins/user-settings/src/components/General/UserSettingsAppearanceCard.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsAppearanceCard.tsx @@ -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 ( diff --git a/plugins/user-settings/src/components/General/UserSettingsPinToggle.test.tsx b/plugins/user-settings/src/components/General/UserSettingsPinToggle.test.tsx index b63ac85b10..a41792768a 100644 --- a/plugins/user-settings/src/components/General/UserSettingsPinToggle.test.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsPinToggle.test.tsx @@ -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('', () => { it('toggles the pin sidebar button', async () => { const mockToggleFn = jest.fn(); const rendered = await renderWithEffects( wrapInTestApp( - ', () => { }} > - , + , ), ); expect(rendered.getByText('Pin Sidebar')).toBeInTheDocument(); diff --git a/plugins/user-settings/src/components/General/UserSettingsPinToggle.tsx b/plugins/user-settings/src/components/General/UserSettingsPinToggle.tsx index 4d71df8113..d218787c05 100644 --- a/plugins/user-settings/src/components/General/UserSettingsPinToggle.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsPinToggle.tsx @@ -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 ( diff --git a/plugins/user-settings/src/components/SettingsPage.tsx b/plugins/user-settings/src/components/SettingsPage.tsx index 8942235fda..dcaf62d435 100644 --- a/plugins/user-settings/src/components/SettingsPage.tsx +++ b/plugins/user-settings/src/components/SettingsPage.tsx @@ -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 =>