src/components/Drawer/index.js
import React, { useEffect, useRef, useState, useCallback } from 'react';
import PropTypes from 'prop-types';
import { createPortal } from 'react-dom';
import { ESCAPE_KEY, TAB_KEY } from '../../libs/constants';
import {
disableBodyScroll,
enableBodyScroll,
clearAllBodyScrollLocks,
} from '../../libs/scrollController';
import CounterManager from '../../libs/counterManager';
import manageTab from '../../libs/manageTab';
import RenderIf from '../RenderIf';
import StyledBackDrop from './styled/backDrop';
import StyledContainer from './styled/container';
import StyledContent from './styled/content';
import StyledCloseButton from './styled/closeButton';
import StyledDivider from './styled/divider';
import StyledFooter from './styled/footer';
import Header from './header';
import CloseIcon from './closeIcon';
import { useUniqueIdentifier } from '../../libs/hooks';
import getSlideFrom from './helpers/getSlideFrom';
const DrawerState = {
OPENING: 0,
OPENED: 1,
CLOSING: 2,
CLOSED: 3,
};
/**
* Drawers are surfaces containing supplementary content on your app.
* @category Layout
* */
export default function Drawer(props) {
const {
id,
isOpen,
hideCloseButton,
onRequestClose,
onOpened,
header,
footer,
size,
slideFrom,
children,
className,
style,
} = props;
const headerId = useUniqueIdentifier('drawer-header');
const contentId = useUniqueIdentifier('drawer-content');
const triggerRef = useRef(null);
const drawerRef = useRef(null);
const containerRef = useRef(null);
const contentRef = useRef(null);
const [drawerState, setDrawerState] = useState(
isOpen ? DrawerState.OPENED : DrawerState.CLOSED,
);
useEffect(() => {
const contentElement = contentRef.current;
if (isOpen) {
CounterManager.increment();
disableBodyScroll(contentElement);
triggerRef.current = document.activeElement;
setDrawerState(DrawerState.OPENING);
}
return () => {
if (isOpen) {
CounterManager.decrement();
if (triggerRef.current) triggerRef.current.focus();
if (!CounterManager.hasModalsOpen()) {
enableBodyScroll(contentElement);
}
clearAllBodyScrollLocks();
setDrawerState(DrawerState.CLOSING);
}
};
}, [isOpen]);
useEffect(() => {
if (isOpen && drawerState === DrawerState.OPENED) {
drawerRef.current.focus();
onOpened();
}
}, [drawerState, isOpen, onOpened]);
const onSlideEnd = () => {
if (drawerState === DrawerState.OPENING) {
setDrawerState(DrawerState.OPENED);
} else if (drawerState === DrawerState.CLOSING) {
setDrawerState(DrawerState.CLOSED);
}
};
const closeDrawer = () => onRequestClose();
const handleBackDropClick = useCallback(event => {
if (drawerRef.current.contains(event.target)) {
return null;
}
return closeDrawer();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const node = containerRef.current;
if (drawerState === 1) {
node.addEventListener('click', handleBackDropClick);
}
return () => {
if (node) {
node.removeEventListener('click', handleBackDropClick);
}
};
}, [drawerState, handleBackDropClick]);
const handleKeyPressed = event => {
event.stopPropagation();
if (isOpen && event.keyCode === ESCAPE_KEY && containerRef.current.contains(event.target)) {
closeDrawer();
}
if (event.keyCode === TAB_KEY) {
manageTab(drawerRef.current, event);
}
return null;
};
const drawerIsOpen = [DrawerState.OPENING, DrawerState.OPENED].includes(drawerState);
if (drawerState !== null && drawerState !== DrawerState.CLOSED) {
return createPortal(
<StyledBackDrop
id={id}
role="presentation"
ref={containerRef}
onKeyDown={handleKeyPressed}
>
<StyledContainer
role="dialog"
tabIndex={-1}
aria-labelledby={headerId}
aria-modal
aria-hidden={!drawerIsOpen}
aria-describedby={contentId}
className={className}
isOpen={drawerIsOpen}
style={style}
size={size}
slideFrom={getSlideFrom(slideFrom, 'left')}
ref={drawerRef}
onAnimationEnd={onSlideEnd}
>
<Header id={headerId} content={header} />
<RenderIf isTrue={!hideCloseButton}>
<StyledCloseButton
id="drawer-close-button"
icon={<CloseIcon />}
title="Hide"
onClick={closeDrawer}
/>
</RenderIf>
<StyledContent id={contentId} ref={contentRef}>
{children}
</StyledContent>
<RenderIf isTrue={footer}>
<StyledDivider />
<StyledFooter>{footer}</StyledFooter>
</RenderIf>
</StyledContainer>
</StyledBackDrop>,
document.body,
);
}
return <></>;
}
Drawer.propTypes = {
/** The header can include text or another component,
* and is displayed at the top of the component. */
header: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
/** The size of the drawer. Valid values are small, medium, large and full.
* This value defaults to small. */
size: PropTypes.oneOf(['small', 'medium', 'large', 'full']),
/** The footer can include text or another component
* and is displayed at the bottom of the component. */
footer: PropTypes.node,
/** Controls whether the Drawer is opened or not. If true, the drawer is opened. */
isOpen: PropTypes.bool,
/** A CSS class for the outer element, in addition to the component's base classes. */
className: PropTypes.string,
/** An object with custom style applied to the outer element. */
style: PropTypes.object,
/** The action triggered when the component requested to close
* (e.g click hide button, press esc key or click outside the drawer). */
onRequestClose: PropTypes.func,
/** A callback triggered when the drawer is opened. This is usefull for example to set focus
* to an element inside the drawer's content after it is opened. */
onOpened: PropTypes.func,
/** The id of the outer element. */
id: PropTypes.string,
/**
* This prop that should not be visible in the documentation.
* @ignore
*/
children: PropTypes.node,
/** If true, hide the close button in the drawer. */
hideCloseButton: PropTypes.bool,
/** The position from where the drawer is opened. */
slideFrom: PropTypes.oneOf(['left', 'right']),
};
Drawer.defaultProps = {
isOpen: false,
header: null,
size: 'small',
footer: null,
className: undefined,
style: undefined,
children: null,
id: undefined,
onRequestClose: () => {},
onOpened: () => {},
hideCloseButton: false,
slideFrom: 'left',
};