diff --git a/packages/react/src/components/Popover.tsx b/packages/react/src/components/Popover.tsx index 51e9880d..f7101741 100644 --- a/packages/react/src/components/Popover.tsx +++ b/packages/react/src/components/Popover.tsx @@ -28,6 +28,7 @@ interface PopoverContextType { transitionStatus: TransitionStatus; placement: Placement; sideOffset: number; + popupId: string | undefined; } interface PopoverRootProps { @@ -50,6 +51,7 @@ interface PopoverPositionerProps { interface PopoverPopupProps { id?: string; className?: string; + style?: React.CSSProperties; children: ReactNode; } @@ -72,6 +74,8 @@ function PopoverRoot({ openOnHover = false, delay = 0, closeDelay = 0, children const triggerRef = useRef(null); const hoverTimeoutRef = useRef | null>(null); const pointerMoveHandlerRef = useRef<((event: MouseEvent) => void) | null>(null); + const uniqueId = useId(); + const popupId = uniqueId.replace(/^:([^:]+):$/, '«$1»'); const clearHoverTimeout = useCallback(() => { if (hoverTimeoutRef.current) { @@ -214,11 +218,6 @@ function PopoverRoot({ openOnHover = false, delay = 0, closeDelay = 0, children if (!popupRef.current || !triggerRef.current) return; const popup = popupRef.current; - const popupId = popup.id; - if (popupId) { - popup.style.setProperty('position-anchor', `--${popupId}`); - } - const [side, alignment] = placement.split('-'); popup.style.setProperty('top', `calc(anchor(${side}) - ${sideOffset}px)`); popup.style.setProperty('translate', `0 -100%`); @@ -238,18 +237,27 @@ function PopoverRoot({ openOnHover = false, delay = 0, closeDelay = 0, children transitionStatus, placement, sideOffset, - }), [open, setOpenState, updatePositioning, transitionStatus, placement, sideOffset]); + popupId, + }), [open, setOpenState, updatePositioning, transitionStatus, placement, sideOffset, popupId]); return {children}; } function PopoverTrigger({ children }: PopoverTriggerProps): JSX.Element { - const { triggerRef, open } = usePopoverContext(); + const { triggerRef, open, popupId } = usePopoverContext(); - // eslint-disable-next-line react/no-clone-element, react/no-children-only - return cloneElement(Children.only(children) as JSX.Element, { + const child = Children.only(children) as JSX.Element; + const existingStyle = (child.props as { style?: React.CSSProperties })?.style || {}; + + // eslint-disable-next-line react/no-clone-element + return cloneElement(child, { ref: triggerRef, 'data-popup-open': open ? '' : undefined, + commandfor: popupId ?? undefined, + style: { + ...existingStyle, + ...(popupId ? { anchorName: `--${popupId}` as any } : {}), + }, }); } @@ -263,24 +271,10 @@ function PopoverPositioner({ side = 'top', sideOffset = 5, children }: PopoverPo return <>{children}; } -function PopoverPopup({ id, className, children }: PopoverPopupProps): JSX.Element { - const { popupRef, triggerRef, transitionStatus, placement } = usePopoverContext(); +function PopoverPopup({ className, style, children }: PopoverPopupProps): JSX.Element { + const { popupRef, triggerRef, transitionStatus, placement, popupId } = usePopoverContext(); const triggerElement = triggerRef.current; - const uniqueId = useId(); - const popupId = id ?? uniqueId.replace(/^:([^:]+):$/, '«$1»'); - - useEffect(() => { - if (!popupRef.current || !triggerRef.current) return; - - const popup = popupRef.current; - const trigger = triggerRef.current; - - popup.setAttribute('popover', 'manual'); - trigger.setAttribute('commandfor', popupId); - trigger.style.setProperty('anchor-name', `--${popupId}`); - }, [popupId, popupRef, triggerRef]); - // Copy data attributes from trigger element const dataAttributes = useMemo(() => { if (!triggerElement?.attributes) return {}; @@ -294,8 +288,13 @@ function PopoverPopup({ id, className, children }: PopoverPopupProps): JSX.Eleme return (
} - id={popupId} + id={popupId ?? undefined} className={className} + popover="manual" + style={{ + ...(popupId ? { positionAnchor: `--${popupId}` as any } : {}), + ...style, + }} {...dataAttributes} data-side={placement} data-starting-style={transitionStatus === 'initial' ? '' : undefined} diff --git a/packages/react/src/components/Tooltip.tsx b/packages/react/src/components/Tooltip.tsx index 4cd9181c..2a8758d7 100644 --- a/packages/react/src/components/Tooltip.tsx +++ b/packages/react/src/components/Tooltip.tsx @@ -35,6 +35,7 @@ interface TooltipContextType { placement: Placement; sideOffset: number; collisionPadding: number; + popupId: string | undefined; } interface TooltipRootProps { @@ -58,6 +59,7 @@ interface TooltipPositionerProps { interface TooltipPopupProps { id?: string; className?: string; + style?: React.CSSProperties; children: ReactNode; } @@ -81,6 +83,8 @@ function TooltipRoot({ delay = 0, closeDelay = 0, trackCursorAxis, children }: T const triggerRef = useRef(null); const hoverTimeoutRef = useRef | null>(null); const pointerPositionRef = useRef({ x: 0, y: 0 }); + const uniqueId = useId(); + const popupId = uniqueId.replace(/^:([^:]+):$/, '«$1»'); const updatePositioning = useCallback(({ side, sideOffset, collisionPadding }: UpdatePositioningProps) => { setPlacement(side); @@ -230,11 +234,6 @@ function TooltipRoot({ delay = 0, closeDelay = 0, trackCursorAxis, children }: T if (!popupRef.current || !triggerRef.current) return; const popup = popupRef.current; - const popupId = popup.id; - if (popupId) { - popup.style.setProperty('position-anchor', `--${popupId}`); - } - const [side, alignment] = placement.split('-'); popup.style.setProperty('top', `calc(anchor(${side}) - ${sideOffset}px)`); @@ -272,20 +271,29 @@ function TooltipRoot({ delay = 0, closeDelay = 0, trackCursorAxis, children }: T placement, sideOffset, collisionPadding, + popupId, }), - [open, transitionStatus, updatePositioning, trackCursorAxis, placement, sideOffset, collisionPadding], + [open, transitionStatus, updatePositioning, trackCursorAxis, placement, sideOffset, collisionPadding, popupId], ); return {children}; } function TooltipTrigger({ children }: TooltipTriggerProps): JSX.Element { - const { triggerRef, open } = useTooltipContext(); + const { triggerRef, open, popupId } = useTooltipContext(); - // eslint-disable-next-line react/no-clone-element, react/no-children-only - return cloneElement(Children.only(children) as JSX.Element, { + const child = Children.only(children) as JSX.Element; + const existingStyle = (child.props as { style?: React.CSSProperties })?.style || {}; + + // eslint-disable-next-line react/no-clone-element + return cloneElement(child, { ref: triggerRef, 'data-popup-open': open ? '' : undefined, + commandfor: popupId ?? undefined, + style: { + ...existingStyle, + ...(popupId ? { anchorName: `--${popupId}` as any } : {}), + }, }); } @@ -304,24 +312,10 @@ function TooltipPositioner({ return <>{children}; } -function TooltipPopup({ id, className = '', children }: TooltipPopupProps): JSX.Element | null { - const { popupRef, triggerRef, transitionStatus, placement } = useTooltipContext(); +function TooltipPopup({ className = '', style, children }: TooltipPopupProps): JSX.Element | null { + const { popupRef, triggerRef, transitionStatus, placement, popupId } = useTooltipContext(); const triggerElement = triggerRef.current; - const uniqueId = useId(); - const popupId = id ?? uniqueId.replace(/^:([^:]+):$/, '«$1»'); - - useEffect(() => { - if (!popupRef.current || !triggerRef.current) return; - - const popup = popupRef.current; - const trigger = triggerRef.current; - - popup.setAttribute('popover', 'manual'); - trigger.setAttribute('commandfor', popupId); - trigger.style.setProperty('anchor-name', `--${popupId}`); - }, [popupId, popupRef, triggerRef]); - // Copy data attributes from trigger element const dataAttributes = useMemo(() => { if (!triggerElement?.attributes) return {}; @@ -335,9 +329,14 @@ function TooltipPopup({ id, className = '', children }: TooltipPopupProps): JSX. return (
} - id={popupId} + id={popupId ?? undefined} className={className} role="tooltip" + popover="manual" + style={{ + ...(popupId ? { positionAnchor: `--${popupId}` as any } : {}), + ...style, + }} {...dataAttributes} data-side={placement} data-starting-style={transitionStatus === 'initial' ? '' : undefined} diff --git a/packages/react/src/types.d.ts b/packages/react/src/types.d.ts index 998a371a..4fdac591 100644 --- a/packages/react/src/types.d.ts +++ b/packages/react/src/types.d.ts @@ -4,8 +4,8 @@ declare module '*.module.css' { } declare global { - declare module 'react' { - interface Attributes { + namespace React { + interface HTMLAttributes { popover?: 'auto' | 'manual' | string; commandfor?: string; }