// Polyfill for Interest Invoker API (for hover-triggered popovers) import 'invokers'; import React, { useEffect, useLayoutEffect, useMemo, useRef } from 'react'; import { getPlacementStyles, usePopoverPositioning } from './use-popover-positioning'; import type { PopoverAnchorNative, PopoverPlacement, PopoverToggleEvent } from './types'; import styles from './silke-popover-native.scss'; function generateAnchorName(): string { return `--anchor-${Math.random().toString(36).slice(2, 11)}`; } export interface SilkePopoverNativeProps { id?: string; /** Should the popover be hidden */ hide?: boolean; /** Children to add inside the popover */ children?: React.ReactElement | React.ReactElement[]; /** Element to anchor the popover to (ref) */ anchor?: PopoverAnchorNative; /** Where to place the popover relative to the anchor. 'start' aligns the start of the popover with the start of the anchor. 'end' aligns the end of the popover with the end of the anchor. */ placement?: PopoverPlacement; /** Advanced: What slot of the anchor should be used for positioning (overrides placement) */ anchorSlot?: string; /** Advanced: Vertical alignment of the popover (overrides placement) */ align?: string; /** Advanced: Horizontal alignment of the popover (overrides placement) */ justify?: string; /** Offset the popover position horizontally */ offsetX?: number; /** Offset the popover position vertically */ offsetY?: number; /** Should the popover have a backdrop */ hideBackdrop?: boolean; /** Should the popover match the width of the anchor */ matchWidth?: boolean; /** Show popover on hover (hint mode) - automatically shows on anchor hover and stays visible when hovering popover */ hint?: boolean; /** Should the popover be animated */ animate?: boolean; /** Shadow level of the popover */ shadow?: 'level1' | 'level2' | 'level3' | 'level4'; /** Triggered when the popover is dismissed (via light dismiss or escape key) */ onRequestClose?: () => void; } export function SilkePopoverNative({ anchor, children, hide = true, id, placement = 'bottom', anchorSlot, align, justify, offsetX = 0, offsetY = 0, hideBackdrop, matchWidth = false, hint = false, animate = false, shadow, onRequestClose, }: SilkePopoverNativeProps) { const popoverRef = useRef(null); const popoverId = id || `silke-popover-native-${Math.random().toString(36).slice(2, 11)}`; const anchorName = useMemo(() => generateAnchorName(), []); // Convert placement to low-level props, or use provided overrides const placementStyles = useMemo(() => { let styles = getPlacementStyles(placement); if (anchorSlot) { styles = { ...styles, positionArea: anchorSlot }; } if (align) { styles = { ...styles, alignSelf: align }; } if (justify) { styles = { ...styles, justifySelf: justify }; } return styles; }, [placement, anchorSlot, align, justify]); usePopoverPositioning( popoverRef, anchor, anchorName, placementStyles, offsetX, offsetY, matchWidth, ); // Set popover attribute synchronously on mount (before hide/show effect runs) useLayoutEffect(() => { const popover = popoverRef.current; const anchorElement = anchor?.current; if (popover) { const popoverMode = hint ? 'hint' : 'auto'; if (popover.getAttribute('popover') !== popoverMode) { popover.setAttribute('popover', popoverMode); } } // Set interestfor on anchor for hint mode if (hint && anchorElement) { anchorElement.setAttribute('interestfor', popoverId); } else if (anchorElement) { anchorElement.removeAttribute('interestfor'); } }, [hint, anchor, popoverId]); // Handle show/hide state useEffect(() => { if (hint) return; const popover = popoverRef.current; if (!popover || !popover.hasAttribute('popover')) return; const shouldShow = !hide; try { if (shouldShow) { if (!popover.matches(':popover-open')) { popover.showPopover(); } } else { if (popover.matches(':popover-open')) { popover.hidePopover(); } } } catch (e) { console.warn('Popover API error:', e); } }, [hide, hint]); // Handle dismiss events (light dismiss and escape key) useEffect(() => { const popover = popoverRef.current; if (!popover || !onRequestClose) return; const handleToggle = (e: Event) => { const toggleEvent = e as PopoverToggleEvent; if (toggleEvent.newState === 'closed' && onRequestClose) { onRequestClose(); } }; popover.addEventListener('toggle', handleToggle); return () => { popover.removeEventListener('toggle', handleToggle); }; }, [onRequestClose]); // Determine role based on backdrop (modal vs dialog vs region) const role = !hideBackdrop ? 'dialog' : 'region'; const ariaModal = !hideBackdrop ? { 'aria-modal': 'true' as const } : {}; return (
{children}
); }