Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file.
Empty file.
155 changes: 155 additions & 0 deletions src/components/PageDrawer/PageDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import * as React from 'react';
import {
Drawer,
DrawerActions,
DrawerCloseButton,
DrawerContent,
DrawerContentBody,
DrawerHead,
DrawerPanelContent,
} from '@patternfly/react-core';
import pageStyles from '@patternfly/react-styles/css/components/Page/page';

const usePageDrawerState = () => {
const [isDrawerMounted, setIsDrawerMounted] = React.useState(false);
const [isDrawerExpanded, setIsDrawerExpanded] = React.useState(false);
const [drawerChildren, setDrawerChildren] = React.useState<React.ReactNode>(null);
const drawerFocusRef = React.useRef(document.createElement('span'));
return {
isDrawerMounted,
setIsDrawerMounted,
isDrawerExpanded,
setIsDrawerExpanded,
drawerChildren,
setDrawerChildren,
drawerFocusRef: drawerFocusRef as typeof drawerFocusRef | null,
};
};

type PageDrawerState = ReturnType<typeof usePageDrawerState>;

const PageDrawerContext = React.createContext<PageDrawerState>({
isDrawerMounted: false,
// eslint-disable-next-line @typescript-eslint/no-empty-function
setIsDrawerMounted: () => {},
isDrawerExpanded: false,
// eslint-disable-next-line @typescript-eslint/no-empty-function
setIsDrawerExpanded: () => {},
drawerChildren: null,
// eslint-disable-next-line @typescript-eslint/no-empty-function
setDrawerChildren: () => {},
drawerFocusRef: null,
});

// PageContentWithDrawerProvider should only be rendered as the direct child of a PatternFly Page component.
interface IPageContentWithDrawerProviderProps {
children: React.ReactNode; // The entire content of the page. See usage in client/src/app/layout/DefaultLayout.
}
export const PageContentWithDrawerProvider: React.FC<IPageContentWithDrawerProviderProps> = ({
children,
}) => {
const pageDrawerState = usePageDrawerState();
const { isDrawerMounted, isDrawerExpanded, drawerFocusRef, drawerChildren } = pageDrawerState;
return (
<PageDrawerContext.Provider value={pageDrawerState}>
{isDrawerMounted ? (
<div className={pageStyles.pageDrawer}>
<Drawer
isExpanded={isDrawerExpanded}
onExpand={() => drawerFocusRef?.current?.focus()}
position="right"
>
<DrawerContent
panelContent={
<DrawerPanelContent
isResizable
id="page-drawer-content"
defaultSize="500px"
minSize="150px"
>
{drawerChildren}
</DrawerPanelContent>
}
>
<DrawerContentBody>{children}</DrawerContentBody>
</DrawerContent>
</Drawer>
</div>
) : (
children
)}
</PageDrawerContext.Provider>
);
};

let numPageDrawerContentInstances = 0;

// PageDrawer can be rendered anywhere, but must have only one instance rendered at a time.
export interface IPageDrawerProps {
isExpanded: boolean;
onCloseClick: () => void; // Should be used to update local state such that `isExpanded` becomes false.
children: React.ReactNode; // The content to show in the drawer when `isExpanded` is true.
focusKey?: string | number; // A unique key representing the object being described in the drawer. When this changes, the drawer will regain focus.
}

export const PageDrawer: React.FC<IPageDrawerProps> = ({
isExpanded: localIsExpandedProp,
onCloseClick,
children,
focusKey,
}) => {
const { setIsDrawerMounted, setIsDrawerExpanded, drawerFocusRef, setDrawerChildren } =
React.useContext(PageDrawerContext);

// Prevent Drawer boilerplate from being rendered in PageContentWithDrawerProvider if no PageDrawerContent exists.
// Also, warn if we are trying to render more than one PageDrawerContent (they'll fight over the same state).
React.useEffect(() => {
numPageDrawerContentInstances++;
setIsDrawerMounted(true);
return () => {
numPageDrawerContentInstances--;
setIsDrawerMounted(false);
};
}, [setIsDrawerMounted]);
if (numPageDrawerContentInstances > 1) {
console.warn(
`${numPageDrawerContentInstances} instances of PageDrawerContent are currently rendered! Only one instance of this component should be rendered at a time.`
);
}

// Lift the value of isExpanded out to the context, but derive it from local state such as a selected table row.
// This is the ONLY place where `setIsDrawerExpanded` should be called.
// To expand/collapse the drawer, use the `isExpanded` prop when rendering PageDrawerContent.
React.useEffect(() => {
setIsDrawerExpanded(localIsExpandedProp);
return () => {
setIsDrawerExpanded(false);
};
}, [localIsExpandedProp, setIsDrawerExpanded]);

// If the drawer is already expanded describing app A, then the user clicks app B, we want to send focus back to the drawer.
React.useEffect(() => {
drawerFocusRef?.current?.focus();
}, [drawerFocusRef, focusKey]);

React.useEffect(() => {
console.log(
'DRAWER CHILDREN UPDATING -- do we need to memoize onCloseClick or break useEffect deps array rules?'
);
setDrawerChildren(
<DrawerHead>
<span tabIndex={localIsExpandedProp ? 0 : -1} ref={drawerFocusRef}>
{children}
</span>
<DrawerActions>
<DrawerCloseButton
// We call onCloseClick here instead of setIsDrawerExpanded
// because we want the isExpanded prop of PageDrawerContent to be the source of truth.
onClick={onCloseClick}
/>
</DrawerActions>
</DrawerHead>
);
}, [children, drawerFocusRef, localIsExpandedProp, onCloseClick, setDrawerChildren]);
return null;
};
1 change: 1 addition & 0 deletions src/components/PageDrawer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './PageDrawer';