@primer/react
Version:
An implementation of GitHub's Primer Design System using React
166 lines (162 loc) • 6.02 kB
JavaScript
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 };