import { ReactNode, useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react'; import classnames from 'classnames'; import ConfirmationPopover from './ConfirmationPopover'; import animation from '../animation/animation.css'; import { ANIMATION_STATE, useAnimation } from '../animation/AnimationContext'; import { useRequestAnimationFrame } from '../animation/RequestAnimationFrameContext'; import Backdrop from '../Backdrop'; import StopScrollBehavior from '../behaviors/StopScrollBehavior'; import TrapFocusBehavior from '../behaviors/TrapFocusBehavior'; import Box from '../Box'; import { useDefaultLabelContext } from '../contexts/DefaultLabelProvider'; import { ScrollBoundaryContainerProvider } from '../contexts/ScrollBoundaryContainerProvider'; import Flex from '../Flex'; import focusStyles from '../Focus.css'; import Heading from '../Heading'; import { ESCAPE } from '../keyCodes'; import overlayPanelStyles from '../OverlayPanel.css'; import InternalScrollBoundaryContainer from '../ScrollBoundaryContainer/InternalScrollBoundaryContainerWithForwardRef'; import InternalDismissButton from '../sharedSubcomponents/InternalDismissButton'; import { FixedZIndex } from '../zIndex'; export const PADDING_BOINTS = 6; type NodeOrRenderProp = ReactNode | ((arg1: { onDismissStart: () => void }) => ReactNode); type InternalSheetProps = { accessibilityDismissButtonLabel?: string; accessibilityLabel: string; children: NodeOrRenderProp; closeOnOutsideClick: boolean; footer: NodeOrRenderProp; heading?: string; onAnimationEnd: (arg1: { animationState: 'in' | 'out' }) => void | null | undefined; onDismiss: () => void; dismissConfirmation?: { message?: string; subtext?: string; primaryAction?: { accessibilityLabel?: string; text?: string; onClick?: (arg1: { event: | React.MouseEvent | React.MouseEvent | React.KeyboardEvent | React.KeyboardEvent; }) => void; }; secondaryAction?: { accessibilityLabel?: string; text?: string; onClick?: (arg1: { event: | React.MouseEvent | React.MouseEvent | React.KeyboardEvent | React.KeyboardEvent; }) => void; }; }; size: 'sm' | 'md' | 'lg'; subHeading: NodeOrRenderProp; }; const SIZE_WIDTH_MAP = { sm: 540, md: 720, lg: 900, } as const; export default function InternalOverlayPanel({ accessibilityDismissButtonLabel, accessibilityLabel, children, closeOnOutsideClick, dismissConfirmation, footer, heading, onAnimationEnd, onDismiss, size, subHeading, }: InternalSheetProps) { const [showTopShadow, setShowTopShadow] = useState(false); const [showBottomShadow, setShowBottomShadow] = useState(false); const [showPopover, setShowPopover] = useState(false); const contentRef = useRef(null); const dismissButtonRef = useRef(null); const id = useId(); const { animationState, handleAnimationEnd } = useAnimation(); const { handleRequestAnimationFrame, onExternalDismiss } = useRequestAnimationFrame(); const { accessibilityDismissButtonLabel: accessibilityDismissButtonLabelDefault } = useDefaultLabelContext('OverlayPanel'); const enabledDismiss = typeof dismissConfirmation === 'undefined'; const { message, subtext, primaryAction, secondaryAction } = dismissConfirmation ?? {}; function buildDismissableSubcomponent( component: ReactNode | ((arg1: { onDismissStart: () => void }) => ReactNode), ) { return typeof component === 'function' ? component({ onDismissStart: onExternalDismiss }) : component; } const handleOnAnimationEnd = useCallback(() => { handleAnimationEnd?.(); handleRequestAnimationFrame(); onAnimationEnd?.({ animationState: animationState === ANIMATION_STATE.animatedOpening ? 'in' : 'out', }); }, [animationState, onAnimationEnd, handleAnimationEnd, handleRequestAnimationFrame]); const handleBackdropClick = useCallback(() => { if (closeOnOutsideClick && enabledDismiss) { onExternalDismiss(); } if (closeOnOutsideClick && !enabledDismiss) { setShowPopover(true); } }, [closeOnOutsideClick, onExternalDismiss, enabledDismiss]); // Handle the shadows on top and bottom of the content area when scrolling const updateShadows = useCallback(() => { const target = contentRef.current; if (!target) return; const hasVerticalScrollbar = target.clientHeight < target.scrollHeight; setShowTopShadow(hasVerticalScrollbar && target.scrollTop > 0); setShowBottomShadow( hasVerticalScrollbar && target.offsetHeight + target.scrollTop < target.scrollHeight, ); }, []); useEffect(() => { dismissButtonRef.current?.focus(); }, [dismissButtonRef]); useEffect(() => { function handleKeyDown(event: React.KeyboardEvent) { // Handle onDismiss triggering from ESC keyup event if (event.keyCode === ESCAPE && enabledDismiss) { onExternalDismiss(); } if (event.keyCode === ESCAPE && !enabledDismiss) { setShowPopover((value) => !value); } } // we must use keydown instead of keyup to match Popover events, as Popover gets dismissed in onKeyUp and we need to stopPropagation in the confirmation Popover // @ts-expect-error - TS2769 - No overload matches this call. window.addEventListener('keydown', handleKeyDown); return function cleanup() { // @ts-expect-error - TS2769 - No overload matches this call. window.removeEventListener('keydown', handleKeyDown); }; }, [onExternalDismiss, enabledDismiss]); useEffect(() => { updateShadows(); window.addEventListener('resize', updateShadows); return () => { window.removeEventListener('resize', updateShadows); }; }, [updateShadows]); if (!!subHeading && !heading) { throw new Error( `Gestalt OverlayPanel's \`subHeading\` prop requires to be along with \`heading\` prop. Remove this prop or add \`heading\``, ); } // Use useLayoutEffect instead of useEffect as we need to close the component synchronously after all DOM mutations, useEffect was needed to prevent changing state while still rendering but useEffect will create a ms blink of the full OverlayPanel after closing which gets prevented with useLayoutEffect useLayoutEffect(() => { if (animationState === ANIMATION_STATE.unmount) { onDismiss(); } }, [animationState, onDismiss]); return (
); }