186 lines
4.4 KiB
TypeScript
186 lines
4.4 KiB
TypeScript
/**
|
|
* Some code of the Drawer component is based on the Modal component from Gutenberg:
|
|
* https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/modal/index.tsx
|
|
*/
|
|
/**
|
|
* External dependencies
|
|
*/
|
|
import classNames from 'classnames';
|
|
import { useDebounce } from 'use-debounce';
|
|
import type { ForwardedRef, KeyboardEvent, RefObject } from 'react';
|
|
import { __ } from '@wordpress/i18n';
|
|
import {
|
|
createPortal,
|
|
useEffect,
|
|
useRef,
|
|
forwardRef,
|
|
} from '@wordpress/element';
|
|
import { close } from '@wordpress/icons';
|
|
import {
|
|
useFocusReturn,
|
|
useFocusOnMount,
|
|
useConstrainedTabbing,
|
|
useMergeRefs,
|
|
} from '@wordpress/compose';
|
|
|
|
/**
|
|
* Internal dependencies
|
|
*/
|
|
import Button from '../button';
|
|
import * as ariaHelper from './utils/aria-helper';
|
|
import './style.scss';
|
|
|
|
interface DrawerProps {
|
|
children: JSX.Element;
|
|
className?: string;
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
slideIn?: boolean;
|
|
slideOut?: boolean;
|
|
}
|
|
|
|
interface CloseButtonPortalProps {
|
|
onClick: () => void;
|
|
contentRef: RefObject< HTMLDivElement >;
|
|
}
|
|
|
|
const CloseButtonPortal = ( {
|
|
onClick,
|
|
contentRef,
|
|
}: CloseButtonPortalProps ) => {
|
|
const closeButtonWrapper = contentRef?.current?.querySelector(
|
|
'.wc-block-components-drawer__close-wrapper'
|
|
);
|
|
|
|
return closeButtonWrapper
|
|
? createPortal(
|
|
<Button
|
|
className="wc-block-components-drawer__close"
|
|
icon={ close }
|
|
onClick={ onClick }
|
|
label={ __( 'Close', 'woo-gutenberg-products-block' ) }
|
|
showTooltip={ false }
|
|
/>,
|
|
closeButtonWrapper
|
|
)
|
|
: null;
|
|
};
|
|
|
|
const UnforwardedDrawer = (
|
|
{
|
|
children,
|
|
className,
|
|
isOpen,
|
|
onClose,
|
|
slideIn = true,
|
|
slideOut = true,
|
|
}: DrawerProps,
|
|
forwardedRef: ForwardedRef< HTMLDivElement >
|
|
): JSX.Element | null => {
|
|
const [ debouncedIsOpen ] = useDebounce< boolean >( isOpen, 300 );
|
|
const isClosing = ! isOpen && debouncedIsOpen;
|
|
const bodyOpenClassName = 'drawer-open';
|
|
|
|
const onRequestClose = () => {
|
|
document.body.classList.remove( bodyOpenClassName );
|
|
ariaHelper.showApp();
|
|
onClose();
|
|
};
|
|
|
|
const ref = useRef< HTMLDivElement >();
|
|
const focusOnMountRef = useFocusOnMount();
|
|
const constrainedTabbingRef = useConstrainedTabbing();
|
|
const focusReturnRef = useFocusReturn();
|
|
const contentRef = useRef< HTMLDivElement >( null );
|
|
|
|
useEffect( () => {
|
|
if ( isOpen ) {
|
|
ariaHelper.hideApp( ref.current );
|
|
document.body.classList.add( bodyOpenClassName );
|
|
}
|
|
}, [ isOpen, bodyOpenClassName ] );
|
|
|
|
const overlayRef = useMergeRefs( [ ref, forwardedRef ] );
|
|
const drawerRef = useMergeRefs( [
|
|
constrainedTabbingRef,
|
|
focusReturnRef,
|
|
focusOnMountRef,
|
|
] );
|
|
|
|
if ( ! isOpen && ! isClosing ) {
|
|
return null;
|
|
}
|
|
|
|
function handleEscapeKeyDown( event: KeyboardEvent< HTMLDivElement > ) {
|
|
if (
|
|
// Ignore keydowns from IMEs
|
|
event.nativeEvent.isComposing ||
|
|
// Workaround for Mac Safari where the final Enter/Backspace of an IME composition
|
|
// is `isComposing=false`, even though it's technically still part of the composition.
|
|
// These can only be detected by keyCode.
|
|
event.keyCode === 229
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if ( event.code === 'Escape' && ! event.defaultPrevented ) {
|
|
event.preventDefault();
|
|
onRequestClose();
|
|
}
|
|
}
|
|
|
|
return createPortal(
|
|
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
|
|
<div
|
|
ref={ overlayRef }
|
|
className={ classNames(
|
|
'wc-block-components-drawer__screen-overlay',
|
|
{
|
|
'wc-block-components-drawer__screen-overlay--is-hidden':
|
|
! isOpen,
|
|
'wc-block-components-drawer__screen-overlay--with-slide-in':
|
|
slideIn,
|
|
'wc-block-components-drawer__screen-overlay--with-slide-out':
|
|
slideOut,
|
|
}
|
|
) }
|
|
onKeyDown={ handleEscapeKeyDown }
|
|
onClick={ ( e ) => {
|
|
// If click was done directly in the overlay element and not one
|
|
// of its descendants, close the drawer.
|
|
if ( e.target === ref.current ) {
|
|
onRequestClose();
|
|
}
|
|
} }
|
|
>
|
|
<div
|
|
className={ classNames(
|
|
className,
|
|
'wc-block-components-drawer'
|
|
) }
|
|
ref={ drawerRef }
|
|
role="dialog"
|
|
tabIndex={ -1 }
|
|
>
|
|
<div
|
|
className="wc-block-components-drawer__content"
|
|
role="document"
|
|
ref={ contentRef }
|
|
>
|
|
<CloseButtonPortal
|
|
contentRef={ contentRef }
|
|
onClick={ onRequestClose }
|
|
/>
|
|
{ children }
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
document.body
|
|
);
|
|
};
|
|
|
|
const Drawer = forwardRef( UnforwardedDrawer );
|
|
|
|
export default Drawer;
|
|
export { default as DrawerCloseButton } from './close-button';
|