import { ComponentProps, ReactNode, useCallback, useEffect, useId, useLayoutEffect } from 'react'; import classnames from 'classnames'; import ContentContainer from './ContentContainer'; import Header from './Header'; 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 Button from '../Button'; import { useDefaultLabelContext } from '../contexts/DefaultLabelProvider'; import { useGlobalEventsHandlerContext } from '../contexts/GlobalEventsHandlerProvider'; import focusStyles from '../Focus.css'; import { ESCAPE } from '../keyCodes'; import Link from '../Link'; import sheetMobileStyles from '../SheetMobile.css'; import { Indexable } from '../zIndex'; type OnClickType = (arg1: { event: | React.MouseEvent | React.KeyboardEvent | React.MouseEvent | React.KeyboardEvent; onDismissStart: () => void; }) => void; type Props = { accessibilityLabel?: string; align: 'start' | 'center'; backIconButton?: { accessibilityLabel: string; onClick: OnClickType; }; children?: ReactNode; closeOnOutsideClick?: boolean; footer?: ReactNode; forwardIconButton?: { accessibilityLabel: string; onClick: OnClickType; }; heading?: ReactNode; onAnimationEnd: (arg1: { animationState: 'in' | 'out' }) => void | null | undefined; onDismiss: () => void; onOutsideClick?: (arg1: { event: React.MouseEvent }) => void; padding?: 'default' | 'none'; primaryAction?: { accessibilityLabel: string; href?: string; label: string; onClick: OnClickType; rel?: ComponentProps['rel']; size?: ComponentProps['size']; target?: ComponentProps['target']; }; role?: 'alertdialog' | 'dialog'; showDismissButton?: boolean; size: 'default' | 'full' | 'auto'; subHeading?: string; zIndex?: Indexable; }; export default function PartialPage({ accessibilityLabel, align, backIconButton, children, closeOnOutsideClick = true, onAnimationEnd, onDismiss, onOutsideClick, footer, forwardIconButton, padding, primaryAction, heading, role, showDismissButton, size, subHeading, zIndex, }: Props) { const id = useId(); // Consumes DefaultLabelProvider const { accessibilityLabel: defaultAccessibilityLabel } = useDefaultLabelContext('SheetMobile'); // Consumes GlobalEventsHandlerProvider const { sheetMobileHandlers } = useGlobalEventsHandlerContext() ?? { sheetMobileHandlers: { onOpen: () => {}, onClose: () => {} }, }; const { onClose, onOpen } = sheetMobileHandlers ?? { onOpen: () => {}, onClose: () => {}, }; useEffect(() => { onOpen?.(); return function cleanup() { onClose?.(); }; }, [onClose, onOpen]); // Consumes AnimationProvider & RequestAnimationFrameProvider const { animationState, handleAnimationEnd } = useAnimation(); const { handleRequestAnimationFrame, onExternalDismiss } = useRequestAnimationFrame(); const handleOnAnimationEnd = useCallback(() => { handleAnimationEnd(); handleRequestAnimationFrame(); onAnimationEnd?.({ animationState: animationState === ANIMATION_STATE.animatedOpening ? 'in' : 'out', }); }, [animationState, onAnimationEnd, handleAnimationEnd, handleRequestAnimationFrame]); // Handle onDismiss triggering from ESC keyup event useEffect(() => { function handleKeyDown(event: React.KeyboardEvent) { if (event.keyCode === ESCAPE) { onExternalDismiss(); } } // @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]); // When SheetMobile is full page displayed in mobile browser, the body scroll is still accessible. Here we disable to just allow the scrolling within Modal useEffect(() => { let prevOverflowStyle = 'auto'; // @ts-expect-error - TS2339 - Property 'body' does not exist on type 'Window & typeof globalThis'. if (window && window.body?.style?.overflow) { // @ts-expect-error - TS2339 - Property 'body' does not exist on type 'Window & typeof globalThis'. prevOverflowStyle = window.body.style.overflow; // @ts-expect-error - TS2339 - Property 'body' does not exist on type 'Window & typeof globalThis'. window.body.style.overflow = 'hidden'; } return () => { // @ts-expect-error - TS2339 - Property 'body' does not exist on type 'Window & typeof globalThis'. if (window && window.body?.style?.overflow) { // @ts-expect-error - TS2339 - Property 'body' does not exist on type 'Window & typeof globalThis'. window.body.style.overflow = prevOverflowStyle; } }; }, []); // 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]); // Handle click outside the bottom sheet const handleBackdropClick: (event: React.MouseEvent) => void = useCallback( (event) => { onOutsideClick?.({ event }); if (closeOnOutsideClick) { onExternalDismiss(); } }, [closeOnOutsideClick, onExternalDismiss, onOutsideClick], ); return (
} padding={padding} > {children}
); }