UNPKG

@primer/react

Version:

An implementation of GitHub's Primer Design System using React

166 lines (162 loc) 6.02 kB
import { useCallback, useEffect } from 'react'; import { useFocusTrap } from '../hooks/useFocusTrap.js'; import { useFocusZone } from '../hooks/useFocusZone.js'; import { useId } from '../hooks/useId.js'; import { useResponsiveValue } from '../hooks/useResponsiveValue.js'; import { IconButton } from '../Button/IconButton.js'; import { XIcon } from '@primer/octicons-react'; import classes from './AnchoredOverlay.module.css.js'; import { clsx } from 'clsx'; import { jsxs, Fragment, jsx } from 'react/jsx-runtime'; import { useProvidedRefOrCreate } from '../hooks/useProvidedRefOrCreate.js'; import { useRenderForcingRef } from '../hooks/useRenderForcingRef.js'; import { useAnchoredPosition } from '../hooks/useAnchoredPosition.js'; import Overlay from '../Overlay/Overlay.js'; const defaultVariant = { regular: 'anchored', narrow: 'anchored' }; const defaultCloseButtonProps = {}; /** * An `AnchoredOverlay` provides an anchor that will open a floating overlay positioned relative to the anchor. * The overlay can be opened and navigated using keyboard or mouse. */ const AnchoredOverlay = ({ renderAnchor, anchorRef: externalAnchorRef, anchorId: externalAnchorId, children, open, onOpen, onClose, height, width, overlayProps, focusTrapSettings, focusZoneSettings, side = (overlayProps === null || overlayProps === void 0 ? void 0 : overlayProps['anchorSide']) || 'outside-bottom', align = 'start', alignmentOffset, anchorOffset, className, pinPosition, variant = defaultVariant, preventOverflow = true, onPositionChange, displayCloseButton = true, closeButtonProps = defaultCloseButtonProps }) => { const anchorRef = useProvidedRefOrCreate(externalAnchorRef); const [overlayRef, updateOverlayRef] = useRenderForcingRef(); const anchorId = useId(externalAnchorId); const onClickOutside = useCallback(() => onClose === null || onClose === void 0 ? void 0 : onClose('click-outside'), [onClose]); const onEscape = useCallback(() => onClose === null || onClose === void 0 ? void 0 : onClose('escape'), [onClose]); const onAnchorKeyDown = useCallback(event => { if (!event.defaultPrevented) { if (!open && ['ArrowDown', 'ArrowUp', ' ', 'Enter'].includes(event.key)) { onOpen === null || onOpen === void 0 ? void 0 : onOpen('anchor-key-press', event); event.preventDefault(); } } }, [open, onOpen]); const onAnchorClick = useCallback(event_0 => { if (event_0.defaultPrevented || event_0.button !== 0) { return; } if (!open) { onOpen === null || onOpen === void 0 ? void 0 : onOpen('anchor-click'); } else { onClose === null || onClose === void 0 ? void 0 : onClose('anchor-click'); } }, [open, onOpen, onClose]); const positionChange = position => { if (onPositionChange && position) { onPositionChange({ position }); } }; const { position: position_0 } = useAnchoredPosition({ anchorElementRef: anchorRef, floatingElementRef: overlayRef, pinPosition, side, align, alignmentOffset, anchorOffset, onPositionChange: positionChange }, [overlayRef.current]); useEffect(() => { // ensure overlay ref gets cleared when closed, so position can reset between closing/re-opening if (!open && overlayRef.current) { updateOverlayRef(null); } }, [open, overlayRef, updateOverlayRef]); useFocusZone({ containerRef: overlayRef, disabled: !open || !position_0, ...focusZoneSettings }); useFocusTrap({ containerRef: overlayRef, disabled: !open || !position_0, ...focusTrapSettings }); const currentResponsiveVariant = useResponsiveValue(variant, 'anchored'); const showXIcon = onClose && variant.narrow === 'fullscreen' && displayCloseButton; const XButtonAriaLabelledBy = closeButtonProps['aria-labelledby']; const XButtonAriaLabel = closeButtonProps['aria-label']; return /*#__PURE__*/jsxs(Fragment, { children: [renderAnchor && renderAnchor({ ref: anchorRef, id: anchorId, 'aria-haspopup': 'true', 'aria-expanded': open, tabIndex: 0, onClick: onAnchorClick, onKeyDown: onAnchorKeyDown }), open ? /*#__PURE__*/jsxs(Overlay, { returnFocusRef: anchorRef, onClickOutside: onClickOutside, ignoreClickRefs: [anchorRef], onEscape: onEscape, ref: updateOverlayRef, role: "none", visibility: position_0 ? 'visible' : 'hidden', height: height, width: width, top: currentResponsiveVariant === 'anchored' ? (position_0 === null || position_0 === void 0 ? void 0 : position_0.top) || 0 : undefined, left: currentResponsiveVariant === 'anchored' ? (position_0 === null || position_0 === void 0 ? void 0 : position_0.left) || 0 : undefined, responsiveVariant: variant.narrow === 'fullscreen' ? 'fullscreen' : undefined, "data-variant": currentResponsiveVariant, anchorSide: position_0 === null || position_0 === void 0 ? void 0 : position_0.anchorSide, className: className, preventOverflow: preventOverflow, ...overlayProps, children: [showXIcon ? /*#__PURE__*/jsx("div", { className: classes.ResponsiveCloseButtonContainer, children: /*#__PURE__*/jsx(IconButton, { ...closeButtonProps, type: "button", variant: "invisible", icon: XIcon, ...(XButtonAriaLabelledBy ? { 'aria-labelledby': XButtonAriaLabelledBy, 'aria-label': undefined } : { 'aria-label': XButtonAriaLabel !== null && XButtonAriaLabel !== void 0 ? XButtonAriaLabel : 'Close', 'aria-labelledby': undefined }), className: clsx(classes.ResponsiveCloseButton, closeButtonProps.className), onClick: () => { onClose('close'); } }) }) : null, children] }) : null] }); }; AnchoredOverlay.displayName = 'AnchoredOverlay'; export { AnchoredOverlay };