mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
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:
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Vendored
+2
-2
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user