From 8bb0f4bc8a1852cde1c83a167c3a95dc6ae6a083 Mon Sep 17 00:00:00 2001 From: Juan Pablo Garcia Ripa Date: Sun, 19 Sep 2021 00:59:45 +0200 Subject: [PATCH] add LayoutProvider Signed-off-by: Juan Pablo Garcia Ripa --- .changeset/eight-insects-tan.md | 5 ++ packages/core-components/api-report.md | 22 +++++ .../src/layout/Content/Content.tsx | 6 ++ .../layout/LayoutProvider/LayoutProvider.tsx | 58 +++++++++++++ .../src/layout/LayoutProvider/index.ts | 16 ++++ .../src/layout/Sidebar/Bar.tsx | 81 ++++++++++++++----- .../src/layout/Sidebar/Page.tsx | 17 +++- .../layout/Sidebar/SidebarPage.stories.tsx | 72 +++++++++++++++++ .../src/layout/Sidebar/index.ts | 2 + .../SidebarSearch/SidebarSearch.tsx | 7 +- 10 files changed, 263 insertions(+), 23 deletions(-) create mode 100644 .changeset/eight-insects-tan.md create mode 100644 packages/core-components/src/layout/LayoutProvider/LayoutProvider.tsx create mode 100644 packages/core-components/src/layout/LayoutProvider/index.ts create mode 100644 packages/core-components/src/layout/Sidebar/SidebarPage.stories.tsx diff --git a/.changeset/eight-insects-tan.md b/.changeset/eight-insects-tan.md new file mode 100644 index 0000000000..0223b0d275 --- /dev/null +++ b/.changeset/eight-insects-tan.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Add SidebarPageContent component and useContenRef hook to have a reference to content wrapper element diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 8b5d1c61a7..296c1ad011 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -1243,6 +1243,21 @@ export type SidebarPageProps = { }; // @public +// Warning: (ae-missing-release-tag) "SidebarPageContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function SidebarPageContent(props: PropsWithChildren<{}>): JSX.Element; + +// Warning: (ae-missing-release-tag) "SidebarPageContextType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type SidebarPageContextType = { + contentRef?: React_2.MutableRefObject; +}; + +// 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) export const SidebarPinStateContext: React_2.Context; // @public @@ -2404,6 +2419,13 @@ export function TrendLine( }, ): JSX.Element | null; +// Warning: (ae-missing-release-tag) "useContentRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function useContentRef(): + | React_2.MutableRefObject + | undefined; + // Warning: (ae-forgotten-export) The symbol "SetQueryParams" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "useQueryParamState" 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/Content/Content.tsx b/packages/core-components/src/layout/Content/Content.tsx index 1d91556663..8d91d335be 100644 --- a/packages/core-components/src/layout/Content/Content.tsx +++ b/packages/core-components/src/layout/Content/Content.tsx @@ -17,6 +17,7 @@ import { makeStyles, Theme } from '@material-ui/core/styles'; import classNames from 'classnames'; import React, { PropsWithChildren } from 'react'; +import { useLayoutContent } from '../LayoutProvider/LayoutProvider'; /** @public */ export type BackstageContentClassKey = 'root' | 'stretch' | 'noPadding'; @@ -62,9 +63,14 @@ type Props = { export function Content(props: PropsWithChildren) { const { className, stretch, noPadding, children, ...restProps } = props; + + const { contentRef } = useLayoutContent(); + const classes = useStyles(); return (
; + }; +}; + +const LayoutContext = createContext({ + content: { + contentRef: undefined, + }, +}); + +export function LayoutProvider(props: PropsWithChildren<{}>) { + const contentRef = useRef(null); + + const content = { + contentRef, + }; + + return ( + + {props.children} + + ); +} + +export function useLayoutContent() { + const { content } = useContext(LayoutContext); + + const focusContent = () => { + content.contentRef?.current?.focus(); + }; + + return { focusContent, contentRef: content.contentRef }; +} diff --git a/packages/core-components/src/layout/LayoutProvider/index.ts b/packages/core-components/src/layout/LayoutProvider/index.ts new file mode 100644 index 0000000000..4f8b38f7e9 --- /dev/null +++ b/packages/core-components/src/layout/LayoutProvider/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 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. + */ +export { LayoutProvider, useLayoutContent } from './LayoutProvider'; diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index ff907cdda3..e134703f50 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -17,11 +17,17 @@ import { makeStyles } from '@material-ui/core/styles'; import useMediaQuery from '@material-ui/core/useMediaQuery'; import classnames from 'classnames'; -import React, { useState, useContext, useRef } from 'react'; + +import React, { useState, useContext, PropsWithChildren, useRef } from 'react'; +import Button from '@material-ui/core/Button'; + import { sidebarConfig, SidebarContext } from './config'; import { BackstageTheme } from '@backstage/theme'; import { SidebarPinStateContext } from './Page'; import { MobileSidebar } from './MobileSidebar'; +import DoubleArrowRight from './icons/DoubleArrowRight'; +import DoubleArrowLeft from './icons/DoubleArrowLeft'; +import { useLayoutContent } from '../LayoutProvider/LayoutProvider'; /** @public */ export type SidebarClassKey = 'drawer' | 'drawerOpen'; @@ -60,6 +66,15 @@ const useStyles = makeStyles( duration: theme.transitions.duration.shorter, }), }, + visuallyHidden: { + top: 0, + position: 'absolute', + zIndex: 2, + transform: 'translateY(-200%)', + '&:focus': { + transform: 'translateY(5px)', + }, + }, }), { name: 'BackstageSidebar' }, ); @@ -105,6 +120,7 @@ const DesktopSidebar = (props: SidebarProps) => { const { isPinned, toggleSidebarPinState } = useContext( SidebarPinStateContext, ); + const { focusContent } = useLayoutContent(); const handleOpen = () => { if (isPinned || disableExpandOnHover) { @@ -128,6 +144,7 @@ const DesktopSidebar = (props: SidebarProps) => { if (isPinned || disableExpandOnHover) { return; } + focusContent(); if (hoverTimerRef.current) { clearTimeout(hoverTimerRef.current); hoverTimerRef.current = undefined; @@ -158,25 +175,42 @@ const DesktopSidebar = (props: SidebarProps) => { }; return ( - -
+
-
+ Skip to content + + +
{} : handleOpen} + onFocus={ + disableExpandOnHover ? () => {} : ignoreChildEvent(handleOpen) + } + onMouseLeave={disableExpandOnHover ? () => {} : handleClose} + onBlur={ + disableExpandOnHover ? () => {} : ignoreChildEvent(handleClose) + } + > +
+ {children} +
+
+
+ ); }; @@ -201,3 +235,12 @@ export const Sidebar = (props: SidebarProps) => { ); }; +function ignoreChildEvent(handlerFn: (e?: any) => void) { + // TODO type the event + return (event: any) => { + const currentTarget = event?.currentTarget as HTMLElement; + if (!currentTarget?.contains(event.relatedTarget as HTMLElement)) { + handlerFn(event); + } + }; +} diff --git a/packages/core-components/src/layout/Sidebar/Page.tsx b/packages/core-components/src/layout/Sidebar/Page.tsx index f23a6fa3b6..3dc31a5d52 100644 --- a/packages/core-components/src/layout/Sidebar/Page.tsx +++ b/packages/core-components/src/layout/Sidebar/Page.tsx @@ -15,7 +15,14 @@ */ import { makeStyles } from '@material-ui/core/styles'; -import React, { createContext, useEffect, useState } from 'react'; + +import React, { + createContext, + PropsWithChildren, + useEffect, + useState, +} from 'react'; + import { sidebarConfig } from './config'; import { BackstageTheme } from '@backstage/theme'; import { LocalStorage } from './localStorage'; @@ -28,6 +35,7 @@ const useStyles = makeStyles( root: { width: '100%', transition: 'padding-left 0.1s ease-out', + isolation: 'isolate', [theme.breakpoints.up('sm')]: { paddingLeft: ({ isPinned }) => isPinned @@ -38,6 +46,13 @@ const useStyles = makeStyles( paddingBottom: sidebarConfig.mobileSidebarHeight, }, }, + content: { + zIndex: 0, + isolation: 'isolate', + '&:focus': { + outline: 0, + }, + }, }), { name: 'BackstageSidebarPage' }, ); diff --git a/packages/core-components/src/layout/Sidebar/SidebarPage.stories.tsx b/packages/core-components/src/layout/Sidebar/SidebarPage.stories.tsx new file mode 100644 index 0000000000..ca769e63cd --- /dev/null +++ b/packages/core-components/src/layout/Sidebar/SidebarPage.stories.tsx @@ -0,0 +1,72 @@ +/* + * Copyright 2020 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 AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline'; +import HomeOutlinedIcon from '@material-ui/icons/HomeOutlined'; +import React, { useCallback } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { + Sidebar, + SidebarDivider, + SidebarIntro, + SidebarItem, + SidebarSearchField, + SidebarSpace, + SidebarPage, +} from '.'; +import { useLayoutContent } from '../LayoutProvider'; +import { PluginWithTable } from '../Page/Page.stories'; + +export default { + title: 'Layout/SidebarPage', + component: SidebarPage, + decorators: [ + (storyFn: () => JSX.Element) => ( + {storyFn()} + ), + ], +}; + +export const SampleSidebarPage = () => { + return ( + + + + + ); +}; + +function CustomSidebar() { + const { focusContent } = useLayoutContent(); + const handleSearch = (input: string) => { + focusContent(); + // eslint-disable-next-line no-console + console.log(input); + }; + + return ( + + + + + + + + + + + ); +} diff --git a/packages/core-components/src/layout/Sidebar/index.ts b/packages/core-components/src/layout/Sidebar/index.ts index d175b364da..99ce966d0a 100644 --- a/packages/core-components/src/layout/Sidebar/index.ts +++ b/packages/core-components/src/layout/Sidebar/index.ts @@ -26,6 +26,7 @@ export type { SidebarSubmenuItemProps, SidebarSubmenuItemDropdownItem, } from './SidebarSubmenuItem'; + export type { SidebarClassKey, SidebarProps } from './Bar'; export { SidebarPage, @@ -36,6 +37,7 @@ export type { SidebarPageClassKey, SidebarPageProps, } from './Page'; + export { SidebarDivider, SidebarItem, diff --git a/plugins/search/src/components/SidebarSearch/SidebarSearch.tsx b/plugins/search/src/components/SidebarSearch/SidebarSearch.tsx index 9f6be2333f..26b9583660 100644 --- a/plugins/search/src/components/SidebarSearch/SidebarSearch.tsx +++ b/plugins/search/src/components/SidebarSearch/SidebarSearch.tsx @@ -18,8 +18,8 @@ import React, { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { rootRouteRef } from '../../plugin'; -import { SidebarSearchField } from '@backstage/core-components'; import { useRouteRef, IconComponent } from '@backstage/core-plugin-api'; +import { SidebarSearchField, useContentRef } from '@backstage/core-components'; export type SidebarSearchProps = { icon?: IconComponent; @@ -27,14 +27,15 @@ export type SidebarSearchProps = { export const SidebarSearch = (props: SidebarSearchProps) => { const searchRoute = useRouteRef(rootRouteRef); + const contentRef = useContentRef(); const navigate = useNavigate(); const handleSearch = useCallback( (query: string): void => { const queryString = qs.stringify({ query }, { addQueryPrefix: true }); - + contentRef?.current?.focus(); navigate(`${searchRoute()}${queryString}`); }, - [navigate, searchRoute], + [navigate, searchRoute, contentRef], ); return (