UNPKG

@utahdts/utah-design-system

Version:
120 lines (111 loc) 4.33 kB
import React, { useCallback, useRef } from 'react'; import { useFloating, autoUpdate, offset as floatingOffset, shift, flip, arrow } from '@floating-ui/react-dom'; import { ICON_BUTTON_APPEARANCE } from '../../enums/buttonEnums'; import { popupPlacement } from '../../enums/popupPlacement'; import { useClickOutside } from '../../hooks/useClickOutside'; import { useGlobalKeyEvent } from '../../hooks/useGlobalKeyEvent'; import { joinClassNames } from '../../util/joinClassNames'; import { IconButton } from '../buttons/IconButton'; /** @typedef {import('@utahdts/utah-design-system-header').PopupPlacement} PopupPlacement */ /** * @param {object} props * @param {string} props.ariaLabelledBy usually the id of the button that controls the popup * @param {import('react').ReactNode} props.children The content of the popup * @param {string} [props.className] CSS class to apply to the popup * @param {boolean} [props.hasCloseButton] the top right `X` close button * @param {string} props.id used for hooking up to the button that controls the popup by aria-control * @param {import('react').RefObject<HTMLDivElement | null>} [props.innerRef] ref to the popup wrapper * @param {boolean} props.isVisible Control the visibility of the popup * @param {number | {mainAxis: number, crossAxis: number, alignmentAxis?: number}} [props.offset] offset of popped content from * @param {(e: React.UIEvent, isVisible: boolean) => void} props.onVisibleChange popup closed; (e, newVisibility) => { ... do something ... } * @param {PopupPlacement} [props.placement] The Popup Placement * @param {import('react').RefObject<HTMLElement | null>} props.referenceElement the anchor element around which the popup content will pop * @param {'dialog' | 'grid' | 'listbox' | 'menu' | 'tree'} props.role popup must tell its role for accessibility * @returns {import('react').JSX.Element} */ export function Popup({ ariaLabelledBy, children, className, hasCloseButton, id, innerRef: draftInnerRef, isVisible, offset = {mainAxis: 10, crossAxis: 0}, onVisibleChange, placement = popupPlacement.BOTTOM, referenceElement, role, ...rest }) { const popupRef = useRef(/** @type {HTMLDivElement | null} */(null)); const arrowRef = useRef(/** @type {HTMLDivElement | null} */(null)); if (draftInnerRef) { // eslint-disable-next-line no-param-reassign draftInnerRef.current = popupRef.current; } const { floatingStyles, middlewareData } = useFloating({ elements: { reference: referenceElement.current, floating: popupRef.current, }, middleware: [ floatingOffset(offset), flip(), shift(), arrow({ element: arrowRef.current, }), ], open: isVisible, placement, whileElementsMounted: autoUpdate, }); useGlobalKeyEvent({ whichKeyCode: 'Escape', onKeyUp: (e) => onVisibleChange(e, false), }); const onVisibleChangeCallback = useCallback( /** @param {import('react').KeyboardEvent} e */ (e) => { onVisibleChange(e, false); }, [onVisibleChange] ); useClickOutside([popupRef, referenceElement], onVisibleChangeCallback, !isVisible); return ( <div aria-labelledby={ariaLabelledBy} id={id} ref={popupRef} style={floatingStyles} className={joinClassNames( 'popup__wrapper', className, hasCloseButton ? 'popup__wrapper--close-button' : null, isVisible ? 'popup__wrapper--visible' : 'popup__wrapper--hidden' )} role={role} data-popup-placement={middlewareData?.offset?.placement || placement} inert={!isVisible} {...rest} > <div className="popup__content"> { hasCloseButton ? ( <IconButton appearance={ICON_BUTTON_APPEARANCE.BORDERLESS} className="popup__close-button" icon={<span className="utds-icon-before-x-icon" aria-hidden="true" />} onClick={(e) => onVisibleChange(e, false)} title="Close popup" size="small" /> ) : undefined } {children} <div ref={arrowRef} style={{left: middlewareData.arrow?.x, top: middlewareData.arrow?.y}} className="popup__arrow" /> </div> </div> ); }