Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { PreviewCard } from '@base-ui/react/preview-card';
import { screen } from '@mui/internal-test-utils';
import { createRenderer, describeConformance } from '#test-utils';

describe('<Popover.Popup />', () => {
describe('<PreviewCard.Popup />', () => {
const { render } = createRenderer();

describeConformance(<PreviewCard.Popup />, () => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export const PreviewCardPositioner = React.forwardRef(function PreviewCardPositi
});
const updatePosition = positioning.update;

// Re-run positioning once open so the card re-anchors to the latest hovered
// inline line box (its position can change between hovers of the same trigger).
useIsoLayoutEffect(() => {
if (open && mounted) {
updatePosition();
Expand Down
6 changes: 2 additions & 4 deletions packages/react/src/preview-card/root/PreviewCardRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,8 @@ function PreviewCardRootComponent<Payload>(props: PreviewCardRoot.Props<Payload>
});

useIsoLayoutEffect(() => {
if (open) {
if (activeTriggerId == null) {
store.set('payload', undefined);
}
if (open && activeTriggerId == null) {
store.set('payload', undefined);
}
}, [store, activeTriggerId, open]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import * as React from 'react';
import { fastComponentRef } from '@base-ui/utils/fastHooks';
import { useIsoLayoutEffect } from '@base-ui/utils/useIsoLayoutEffect';
import { useRefWithInit } from '@base-ui/utils/useRefWithInit';
import { usePreviewCardRootContext } from '../root/PreviewCardContext';
import type { BaseUIComponentProps } from '../../internals/types';
import { triggerOpenStateMapping } from '../../utils/popupStateMapping';
Expand Down Expand Up @@ -50,6 +51,10 @@ export const PreviewCardTrigger = fastComponentRef(function PreviewCardTrigger(

const triggerElementRef = React.useRef<Element | null>(null);

// `safePolygon()` captures a per-instance timer, so it must not be shared across
// triggers; keep one stable instance per trigger rather than allocating per render.
const handleCloseSafePolygon = useRefWithInit(safePolygon).current;

const delayWithDefault = delay ?? OPEN_DELAY;
const closeDelayWithDefault = closeDelay ?? CLOSE_DELAY;

Expand All @@ -71,7 +76,7 @@ export const PreviewCardTrigger = fastComponentRef(function PreviewCardTrigger(
const hoverProps = useHoverReferenceInteraction(floatingRootContext, {
mouseOnly: true,
move: false,
handleClose: safePolygon(),
handleClose: handleCloseSafePolygon,
delay: () => ({ open: delayWithDefault, close: closeDelayWithDefault }),
triggerElementRef,
isActiveTrigger: isTriggerActive,
Expand Down
8 changes: 3 additions & 5 deletions packages/react/src/tooltip/provider/TooltipProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@ import { TooltipProviderContext } from './TooltipProviderContext';
export const TooltipProvider: React.FC<TooltipProvider.Props> = function TooltipProvider(props) {
const { delay, closeDelay, timeout = 400 } = props;

const contextValue: TooltipProviderContext = React.useMemo(
const { contextValue, delayValue } = React.useMemo(
() => ({
delay,
closeDelay,
contextValue: { delay, closeDelay } satisfies TooltipProviderContext,
delayValue: { open: delay, close: closeDelay },
}),
[delay, closeDelay],
);

const delayValue = React.useMemo(() => ({ open: delay, close: closeDelay }), [delay, closeDelay]);

return (
<TooltipProviderContext.Provider value={contextValue}>
<FloatingDelayGroup delay={delayValue} timeoutMs={timeout}>
Expand Down
20 changes: 10 additions & 10 deletions packages/react/src/tooltip/root/TooltipRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,21 @@ export const TooltipRoot = fastComponent(function TooltipRoot<Payload>(
const instantType = store.useState('instantType');
const lastOpenChangeReason = store.useState('lastOpenChangeReason');

// Animations should be instant in two cases:
// 1) Opening during the provider's instant phase (adjacent tooltip opens instantly)
// 2) Closing because another tooltip opened (reason === 'none')
// Otherwise, allow the animation to play. In particular, do not disable animations
// during the 'ending' phase unless it's due to a sibling opening.
const previousInstantTypeRef = React.useRef<string | undefined | null>(null);
const previousInstantTypeRef = React.useRef<'delay' | 'dismiss' | 'focus' | null | undefined>(
null,
);

useIsoLayoutEffect(() => {
if (openState && disabled) {
store.setOpen(false, createChangeEventDetails(REASONS.disabled));
}
}, [openState, disabled, store]);

// Animations should be instant in two cases:
// 1) Opening during the provider's instant phase (adjacent tooltip opens instantly)
// 2) Closing because another tooltip opened (reason === 'none')
// Otherwise, allow the animation to play. In particular, do not disable animations
// during the 'ending' phase unless it's due to a sibling opening.
useIsoLayoutEffect(() => {
if (
(transitionStatus === 'ending' && lastOpenChangeReason === REASONS.none) ||
Expand All @@ -112,10 +114,8 @@ export const TooltipRoot = fastComponent(function TooltipRoot<Payload>(
}, [transitionStatus, isInstantPhase, lastOpenChangeReason, instantType, store]);

useIsoLayoutEffect(() => {
if (open) {
if (activeTriggerId == null) {
store.set('payload', undefined);
}
if (open && activeTriggerId == null) {
store.set('payload', undefined);
}
}, [store, activeTriggerId, open]);

Expand Down
8 changes: 7 additions & 1 deletion packages/react/src/tooltip/trigger/TooltipTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import * as React from 'react';
import { isElement } from '@floating-ui/utils/dom';
import { fastComponentRef } from '@base-ui/utils/fastHooks';
import { useRefWithInit } from '@base-ui/utils/useRefWithInit';
import { useTimeout } from '@base-ui/utils/useTimeout';
import { useValueAsRef } from '@base-ui/utils/useValueAsRef';
import { useTooltipRootContext } from '../root/TooltipRootContext';
Expand Down Expand Up @@ -135,6 +136,10 @@ export const TooltipTrigger = fastComponentRef(function TooltipTrigger(
const trackCursorAxis = store.useState('trackCursorAxis');
const disableHoverablePopup = store.useState('disableHoverablePopup');

// `safePolygon()` captures a per-instance timer, so it must not be shared across
// triggers; keep one stable instance per trigger rather than allocating per render.
const handleCloseSafePolygon = useRefWithInit(safePolygon).current;

const isNestedTriggerHoveredRef = React.useRef(false);
const nestedTriggerOpenTimeout = useTimeout();
// Local copy so it can be cleared on mouseLeave without resetting the hover hook's own pointerType.
Expand Down Expand Up @@ -185,7 +190,8 @@ export const TooltipTrigger = fastComponentRef(function TooltipTrigger(
enabled: !disabled,
mouseOnly: true,
move: false,
handleClose: !disableHoverablePopup && trackCursorAxis !== 'both' ? safePolygon() : null,
handleClose:
!disableHoverablePopup && trackCursorAxis !== 'both' ? handleCloseSafePolygon : null,
restMs: getOpenDelay,
delay() {
const closeValue = typeof delayRef.current === 'object' ? delayRef.current.close : undefined;
Expand Down
Loading