diff --git a/packages/react/src/popover/root/usePopoverRoot.ts b/packages/react/src/popover/root/usePopoverRoot.ts index 02ecfe22f5..f201743113 100644 --- a/packages/react/src/popover/root/usePopoverRoot.ts +++ b/packages/react/src/popover/root/usePopoverRoot.ts @@ -44,10 +44,11 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo const [triggerElement, setTriggerElement] = React.useState(null); const [positionerElement, setPositionerElement] = React.useState(null); const [openReason, setOpenReason] = React.useState(null); - const [clickEnabled, setClickEnabled] = React.useState(true); + const [hoverEnabled, setHoverEnabled] = React.useState(false); + const [stickIfOpen, setStickIfOpen] = React.useState(true); const popupRef = React.useRef(null); - const clickEnabledTimeoutRef = React.useRef(-1); + const stickIfOpenTimeoutRef = React.useRef(-1); const [open, setOpenUnwrapped] = useControlled({ controlled: externalOpen, @@ -56,8 +57,14 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo state: 'open', }); - if (!open && !clickEnabled) { - setClickEnabled(true); + if (!open) { + if (!stickIfOpen) { + setStickIfOpen(true); + } + + if (!hoverEnabled) { + setHoverEnabled(true); + } } const onOpenChange = useEventCallback(onOpenChangeProp); @@ -86,7 +93,7 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo React.useEffect(() => { return () => { - clearTimeout(clickEnabledTimeoutRef.current); + clearTimeout(stickIfOpenTimeoutRef.current); }; }, []); @@ -103,11 +110,11 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo } if (isHover) { - // Prevent impatient clicks from unexpectedly closing the popover. - setClickEnabled(false); - clearTimeout(clickEnabledTimeoutRef.current); - clickEnabledTimeoutRef.current = window.setTimeout(() => { - setClickEnabled(true); + // Only allow "patient" clicks to close the popover if it's open. + // If they clicked within 500ms of the popover opening, keep it open. + clearTimeout(stickIfOpenTimeoutRef.current); + stickIfOpenTimeoutRef.current = window.setTimeout(() => { + setStickIfOpen(false); }, PATIENT_CLICK_THRESHOLD); ReactDOM.flushSync(changeState); @@ -126,7 +133,7 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo const computedRestMs = delayWithDefault; const hover = useHover(context, { - enabled: openOnHover, + enabled: openOnHover && hoverEnabled, mouseOnly: true, move: false, handleClose: safePolygon(), @@ -137,8 +144,7 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo }); const click = useClick(context, { - enabled: clickEnabled, - stickIfOpen: false, + stickIfOpen, }); const dismiss = useDismiss(context); @@ -149,6 +155,24 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo const { openMethod, triggerProps } = useOpenInteractionType(open); + const getTriggerProps = React.useCallback( + (externalProps?: GenericHTMLProps) => + getReferenceProps(mergeReactProps(externalProps, triggerProps)), + [getReferenceProps, triggerProps], + ); + + const getPopupProps = React.useCallback( + (externalProps?: GenericHTMLProps) => + getFloatingProps( + mergeReactProps(externalProps, { + onPointerDown() { + setHoverEnabled(false); + }, + }), + ), + [getFloatingProps], + ); + return React.useMemo( () => ({ open, @@ -164,29 +188,27 @@ export function usePopoverRoot(params: usePopoverRoot.Parameters): usePopoverRoo setTitleId, descriptionId, setDescriptionId, - getRootTriggerProps: (externalProps?: React.HTMLProps) => - getReferenceProps(mergeReactProps(externalProps, triggerProps)), - getRootPopupProps: getFloatingProps, + getRootTriggerProps: getTriggerProps, + getRootPopupProps: getPopupProps, floatingRootContext: context, instantType, openMethod, openReason, }), [ - mounted, open, - setMounted, setOpen, + mounted, + setMounted, transitionStatus, positionerElement, titleId, descriptionId, - getReferenceProps, - getFloatingProps, + getTriggerProps, + getPopupProps, context, instantType, openMethod, - triggerProps, openReason, ], );