fix: fix CLS due to popover attribute not SSR (#202)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Wesley Luyten
2025-11-17 19:41:37 -06:00
committed by GitHub
co-authored by Copilot
parent 601a5a3793
commit 09526731f7
3 changed files with 52 additions and 54 deletions
+25 -26
View File
@@ -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<HTMLElement | null>(null);
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | 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 <PopoverContext.Provider value={value}>{children}</PopoverContext.Provider>;
}
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 (
<div
ref={popupRef as React.RefObject<HTMLDivElement>}
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}
+25 -26
View File
@@ -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<HTMLElement | null>(null);
const hoverTimeoutRef = useRef<ReturnType<typeof setTimeout> | 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 <TooltipContext.Provider value={value}>{children}</TooltipContext.Provider>;
}
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 (
<div
ref={popupRef as RefObject<HTMLDivElement>}
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}
+2 -2
View File
@@ -4,8 +4,8 @@ declare module '*.module.css' {
}
declare global {
declare module 'react' {
interface Attributes {
namespace React {
interface HTMLAttributes<T> {
popover?: 'auto' | 'manual' | string;
commandfor?: string;
}