import * as React from 'react'; import type { AriaPopoverProps, FocusScopeProps, Placement } from 'react-aria'; import classnames from 'classnames'; import type { PopoverOverlayProps as CorePopoverOverlayProps, PopoverState } from '@shoptet/ui-core-web'; import { Popover as CorePopover } from '@shoptet/ui-core-web'; import type { Exact, SafeOmit, SafePick } from '@shoptet/utils'; import { tokens } from '../../tokens/tokens'; import type { Spacing } from '../../types'; const DEFAULT_OFFSET: Spacing = 'sm'; const spacingToPx = (spacing: Spacing): number => parseInt(tokens.dimensions.default.offset[spacing === 'xxl' ? '2xl' : spacing], 10); export interface PopoverOverlayInheritedAriaPopoverProps extends SafePick< AriaPopoverProps, 'shouldCloseOnInteractOutside' > {} export interface PopoverOverlayInheritedFocusScopeProps extends SafePick< FocusScopeProps, 'autoFocus' | 'restoreFocus' | 'contain' > {} export interface PopoverOverlayInheritedDivProps extends SafeOmit< React.HTMLAttributes, 'children' | 'className' | 'style' > {} export type PopoverElevation = 'low' | 'medium' | 'high'; export interface PopoverOverlayOwnProps { /** Content of the popover. Can be a render function receiving the overlay trigger state. */ children: React.ReactNode | ((args: { state: PopoverState }) => React.ReactNode); /** * Shadow elevation of the popover. * @default 'default' */ elevation?: PopoverElevation; /** * Gap between the trigger and the popover. * @default 'sm' */ offset?: Spacing | false; /** * Adds internal padding to the popover. * @default false */ padding?: Spacing | false; /** Preferred placement relative to the trigger. */ placement?: Placement | false; } export interface PopoverOverlayProps extends PopoverOverlayOwnProps, PopoverOverlayInheritedAriaPopoverProps, PopoverOverlayInheritedFocusScopeProps, PopoverOverlayInheritedDivProps {} export function PopoverOverlay({ children, elevation = 'medium', offset = DEFAULT_OFFSET, padding = false, placement = 'bottom', shouldCloseOnInteractOutside, autoFocus = true, restoreFocus = true, contain = true, ...rest }: PopoverOverlayProps) { rest satisfies Exact; const className = classnames('popover', { 'popover--padding-xxs': padding === 'xxs', 'popover--padding-xs': padding === 'xs', 'popover--padding-sm': padding === 'sm', 'popover--padding-md': padding === 'md', 'popover--padding-lg': padding === 'lg', 'popover--padding-xl': padding === 'xl', 'popover--padding-xxl': padding === 'xxl', 'popover--padding-3xl': padding === '3xl', 'popover--padding-4xl': padding === '4xl', 'popover--elevation-high': elevation === 'high', 'popover--elevation-medium': elevation === 'medium', 'popover--elevation-low': elevation === 'low', }); const overlayProps: CorePopoverOverlayProps = { ...rest, offset: offset === false ? 0 : spacingToPx(offset), placement, shouldCloseOnInteractOutside, autoFocus, restoreFocus, containFocus: contain, className, }; return ( {typeof children === 'function' ? ({ state: overlayState }) => children({ state: overlayState.state }) : children} ); }