feat(packages): add flip functionality to popovers/tooltips/menus (#1857)

This commit is contained in:
Sam Potts
2026-07-28 16:30:24 +10:00
committed by GitHub
parent 359e56ca49
commit be89470447
34 changed files with 699 additions and 141 deletions
@@ -0,0 +1,27 @@
import { act, renderHook } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { usePositionedState } from '../use-positioned-state';
describe('usePositionedState', () => {
it('uses the preferred side on the first closed render', () => {
const sides: string[] = [];
const { result, rerender } = renderHook(
({ open }: { open: boolean }) => {
const positioned = usePositionedState<{ open: boolean; side: 'top' | 'bottom' }>({ open, side: 'top' });
sides.push(positioned.state.side);
return positioned;
},
{ initialProps: { open: true } }
);
act(() => result.current.setPositionedSide('bottom'));
expect(result.current.state.side).toBe('bottom');
sides.length = 0;
rerender({ open: false });
expect(sides[0]).toBe('top');
expect(result.current.state.side).toBe('top');
});
});
@@ -0,0 +1,43 @@
'use client';
import { useCallback, useEffect, useMemo, useState } from 'react';
interface PopupState {
open: boolean;
side: string | undefined;
}
interface PositionedState<State extends PopupState> {
state: State;
preferredSide: State['side'];
setPositionedSide: (side: State['side']) => void;
}
export function usePositionedState<State extends PopupState>(preferredState: State): PositionedState<State> {
const preferredSide = preferredState.side;
const [position, setPosition] = useState<{ preferred: State['side']; side: State['side'] }>({
preferred: preferredSide,
side: preferredSide,
});
const side = preferredState.open && position.preferred === preferredSide ? position.side : preferredSide;
const state = useMemo(
() => (side === preferredSide ? preferredState : { ...preferredState, side }),
[side, preferredState, preferredSide]
);
const setPositionedSide = useCallback(
(nextSide: State['side']) =>
setPosition((prev) =>
prev.preferred === preferredSide && prev.side === nextSide ? prev : { preferred: preferredSide, side: nextSide }
),
[preferredSide]
);
useEffect(() => {
if (!preferredState.open) setPositionedSide(preferredSide);
}, [preferredState.open, preferredSide, setPositionedSide]);
return { state, preferredSide, setPositionedSide };
}
+2
View File
@@ -10,6 +10,8 @@ export interface MenuContextValue {
core: MenuCore;
menu: MenuApi;
state: MenuState;
preferredSide: MenuState['side'];
setPositionedSide: (side: MenuState['side']) => void;
stateAttrMap: StateAttrMap<MenuState>;
contentId: string;
anchorName: string;
+32 -13
View File
@@ -8,6 +8,7 @@ import {
getMenuViewportElement,
getMenuViewTransitionAttrs,
getPopupPositionRect,
getPositionedSide,
getPositioningBoundaryRect,
getRootPositionOptions,
isEventWithinElement,
@@ -109,8 +110,19 @@ export const MenuContent = forwardRef<HTMLDivElement, MenuContentProps>(function
{ render, className, style, onKeyDown, onBlur, ...elementProps },
forwardedRef
) {
const { core, menu, state, stateAttrMap, anchorName, contentId, boundary, container, activeSubMenuId } =
useMenuContext();
const {
core,
menu,
state,
preferredSide,
setPositionedSide,
stateAttrMap,
anchorName,
contentId,
boundary,
container,
activeSubMenuId,
} = useMenuContext();
const subMenuCtx = useSubMenuContext();
const isSubmenu = state.isSubmenu;
@@ -229,7 +241,10 @@ export const MenuContent = forwardRef<HTMLDivElement, MenuContentProps>(function
const rootComposedRef = useComposedRefs(forwardedRef, contentRef, internalRef);
const menuViewComposedRef = useComposedRefs(forwardedRef, setMenuViewElement);
const positionOptions = useMemo(() => getRootPositionOptions(state.side, state.align), [state.side, state.align]);
const positionOptions = useMemo(
() => getRootPositionOptions(preferredSide, state.align),
[preferredSide, state.align]
);
const anchorStyle = useMemo(() => {
if (isSubmenu || !positionOptions || !supportsAnchorPositioning()) return null;
@@ -237,7 +252,7 @@ export const MenuContent = forwardRef<HTMLDivElement, MenuContentProps>(function
return rest as CSSProperties;
}, [isSubmenu, anchorName, positionOptions]);
const [manualStyle, setManualStyle] = useState<CSSProperties | null>(null);
const [position, setPosition] = useState<CSSProperties | null>(null);
useLayoutEffect(() => {
if (isSubmenu) return;
@@ -264,7 +279,7 @@ export const MenuContent = forwardRef<HTMLDivElement, MenuContentProps>(function
useLayoutEffect(() => {
if (isSubmenu) return;
if (!state.open) {
setManualStyle(null);
setPosition(null);
return;
}
@@ -284,15 +299,16 @@ export const MenuContent = forwardRef<HTMLDivElement, MenuContentProps>(function
const root = contentElement.getRootNode() as Document | ShadowRoot;
const boundaryElement = resolvePositioningBoundary(boundary, { container, root });
const anchorSupported = supportsAnchorPositioning();
const contentRect = anchorSupported ? undefined : getPopupPositionRect(contentElement);
let contentRect = getPopupPositionRect(contentElement, rootPositionOptions.side);
const boundaryRect = getPositioningBoundaryRect(boundaryElement);
const offsets = resolveOffsets(contentElement);
let side = getPositionedSide(triggerRect, contentRect, boundaryRect, rootPositionOptions, offsets);
let nextStyle = getAnchorPositionStyle(
anchorName,
rootPositionOptions,
{ ...rootPositionOptions, side },
triggerRect,
contentRect,
anchorSupported ? undefined : contentRect,
boundaryRect,
offsets
);
@@ -305,11 +321,13 @@ export const MenuContent = forwardRef<HTMLDivElement, MenuContentProps>(function
);
if (!anchorSupported) {
contentRect = getPopupPositionRect(contentElement, rootPositionOptions.side);
side = getPositionedSide(triggerRect, contentRect, boundaryRect, rootPositionOptions, offsets);
nextStyle = getAnchorPositionStyle(
anchorName,
rootPositionOptions,
{ ...rootPositionOptions, side },
triggerRect,
getPopupPositionRect(contentElement),
contentRect,
boundaryRect,
offsets
);
@@ -317,7 +335,8 @@ export const MenuContent = forwardRef<HTMLDivElement, MenuContentProps>(function
const { positionAnchor: _, ...rootStyle } = nextStyle;
setManualStyle(rootStyle as CSSProperties);
setPosition(rootStyle as CSSProperties);
setPositionedSide(side);
}
measure();
@@ -356,7 +375,7 @@ export const MenuContent = forwardRef<HTMLDivElement, MenuContentProps>(function
window.removeEventListener('scroll', reposition, true);
window.removeEventListener('resize', reposition);
};
}, [isSubmenu, state.open, anchorName, positionOptions, menu, boundary, container]);
}, [isSubmenu, state.open, anchorName, positionOptions, menu, boundary, container, setPositionedSide]);
// ─── Render ───────────────────────────────────────────────────────────────
@@ -393,7 +412,7 @@ export const MenuContent = forwardRef<HTMLDivElement, MenuContentProps>(function
if (!state.open) return null;
const positioningStyle = manualStyle ?? anchorStyle ?? POPOVER_RESET;
const positioningStyle = position ?? anchorStyle ?? POPOVER_RESET;
return renderElement(
'div',
+7 -1
View File
@@ -10,6 +10,7 @@ import { useDestroy } from '../../utils/use-destroy';
import { useLatestRef } from '../../utils/use-latest-ref';
import { useSafeId } from '../../utils/use-safe-id';
import { useOptionalControlsContext } from '../controls/context';
import { usePositionedState } from '../hooks/use-positioned-state';
import { MenuContextProvider, SubMenuContextProvider, useOptionalMenuContext } from './context';
export interface MenuRootProps extends MenuCore.Props {
@@ -98,11 +99,12 @@ export function MenuRoot({
useDestroy(menu);
const input = useSnapshot(menu.input);
const state = useMemo(() => {
const preferredState = useMemo(() => {
core.setProps({ side, align, closeOnEscape, closeOnOutsideClick, isSubmenu });
core.setInput(input);
return core.getState();
}, [core, input, side, align, closeOnEscape, closeOnOutsideClick, isSubmenu]);
const { state, preferredSide, setPositionedSide } = usePositionedState(preferredState);
// Subscribe to navigation state — used by Content/Trigger when this is a root menu.
const navigationInput = useSnapshot(menu.navigationInput);
@@ -116,6 +118,8 @@ export function MenuRoot({
core,
menu,
state,
preferredSide,
setPositionedSide,
stateAttrMap: MenuDataAttrs,
contentId,
anchorName,
@@ -131,6 +135,8 @@ export function MenuRoot({
core,
menu,
state,
preferredSide,
setPositionedSide,
contentId,
anchorName,
boundary,
+42 -1
View File
@@ -19,7 +19,14 @@ import { MenuSeparator } from '../menu-separator';
import { MenuTrigger } from '../menu-trigger';
import { MenuView } from '../menu-view';
afterEach(cleanup);
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
function makeDOMRect(x: number, y: number, width: number, height: number): DOMRect {
return new DOMRect(x, y, width, height);
}
function SubmenuFixture() {
return (
@@ -374,6 +381,40 @@ function DynamicMenuFixture({ showCaptions }: { showCaptions: boolean }) {
}
describe('MenuContent', () => {
it('exposes the positioned side on root content', async () => {
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement) {
if (this.dataset.testid === 'trigger') return makeDOMRect(100, 10, 40, 20);
if (this.dataset.testid === 'content') return makeDOMRect(0, 0, 100, 60);
return makeDOMRect(0, 0, 300, 200);
});
vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockImplementation(function (this: HTMLElement) {
return this.dataset.testid === 'content' ? 100 : 0;
});
vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockImplementation(function (this: HTMLElement) {
return this.dataset.testid === 'content' ? 60 : 0;
});
render(
<MenuRoot defaultOpen side="top" boundary="viewport">
<MenuTrigger
render={(props, state) => <button {...props} data-render-side={state.side} data-testid="trigger" />}
>
Settings
</MenuTrigger>
<MenuContent data-testid="content">
<MenuView>
<MenuItem>Auto</MenuItem>
</MenuView>
</MenuContent>
</MenuRoot>
);
await waitFor(() => {
expect(screen.getByTestId('content').getAttribute('data-side')).toBe('bottom');
expect(screen.getByTestId('trigger').getAttribute('data-render-side')).toBe('bottom');
});
});
it('scopes menu state data attributes to content elements', async () => {
render(
<MenuRoot defaultOpen side="top" align="end">
@@ -8,6 +8,8 @@ export interface PopoverContextValue {
core: PopoverCore;
popover: PopoverApi;
state: PopoverCore.State;
preferredSide: PopoverCore.State['side'];
setPositionedSide: (side: PopoverCore.State['side']) => void;
stateAttrMap: StateAttrMap<PopoverCore.State>;
anchorName: string;
popupId: string;
+26 -13
View File
@@ -4,6 +4,7 @@ import type { PopoverState } from '@videojs/core';
import {
getAnchorPositionStyle,
getPopupPositionRect,
getPositionedSide,
getPositioningBoundaryRect,
isEventWithinElement,
resolveOffsets,
@@ -27,7 +28,18 @@ export const PopoverPopup = forwardRef<HTMLDivElement, PopoverPopupProps>(functi
{ render, className, style, ...elementProps },
forwardedRef
) {
const { core, popover, state, stateAttrMap, anchorName, popupId, boundary, container } = usePopoverContext();
const {
core,
popover,
state,
preferredSide,
setPositionedSide,
stateAttrMap,
anchorName,
popupId,
boundary,
container,
} = usePopoverContext();
const internalRef = useRef<HTMLDivElement>(null);
const popupRef = useCallback(
@@ -44,7 +56,7 @@ export const PopoverPopup = forwardRef<HTMLDivElement, PopoverPopupProps>(functi
// --- Positioning ---
const posOpts = useMemo(() => ({ side: state.side, align: state.align }), [state.side, state.align]);
const posOpts = useMemo(() => ({ side: preferredSide, align: state.align }), [preferredSide, state.align]);
// CSS Anchor Positioning — computed from state, no measurement needed.
// `position-anchor` is set imperatively in the ref callback above
@@ -55,12 +67,12 @@ export const PopoverPopup = forwardRef<HTMLDivElement, PopoverPopupProps>(functi
return rest as CSSProperties;
}, [anchorName, posOpts]);
// Manual fallback — measure rects after layout, before paint.
const [manualStyle, setManualStyle] = useState<CSSProperties | null>(null);
// Measure rects after layout, before paint.
const [position, setPosition] = useState<CSSProperties | null>(null);
useLayoutEffect(() => {
if (!state.open) {
setManualStyle(null);
setPosition(null);
return;
}
@@ -72,20 +84,22 @@ export const PopoverPopup = forwardRef<HTMLDivElement, PopoverPopupProps>(functi
const triggerRect = triggerEl.getBoundingClientRect();
const root = popupEl.getRootNode() as Document | ShadowRoot;
const boundaryElement = resolvePositioningBoundary(boundary, { container, root });
const popupRect = supportsAnchorPositioning() ? undefined : getPopupPositionRect(popupEl);
const popupRect = getPopupPositionRect(popupEl, posOpts.side);
const boundaryRect = getPositioningBoundaryRect(boundaryElement);
const offsets = resolveOffsets(popupEl);
const side = getPositionedSide(triggerRect, popupRect, boundaryRect, posOpts, offsets);
const { positionAnchor: _, ...nextStyle } = getAnchorPositionStyle(
anchorName,
posOpts,
{ ...posOpts, side },
triggerRect,
popupRect,
supportsAnchorPositioning() ? undefined : popupRect,
boundaryRect,
offsets
);
setManualStyle(nextStyle as CSSProperties);
setPosition(nextStyle as CSSProperties);
setPositionedSide(side);
}
measure();
@@ -137,11 +151,10 @@ export const PopoverPopup = forwardRef<HTMLDivElement, PopoverPopupProps>(functi
window.removeEventListener('scroll', reposition, true);
window.removeEventListener('resize', reposition);
};
}, [state.open, anchorName, posOpts, popover, boundary, container]);
}, [state.open, anchorName, posOpts, popover, boundary, container, setPositionedSide]);
// Anchor path uses computed styles; manual path uses measured styles;
// fallback resets UA [popover] defaults until positioning is computed.
const positioningStyle = manualStyle ?? anchorStyle ?? POPOVER_RESET;
// Use measured styles once available; reset UA [popover] defaults until then.
const positioningStyle = position ?? anchorStyle ?? POPOVER_RESET;
// --- Visibility ---
+14 -2
View File
@@ -16,6 +16,7 @@ import { useDestroy } from '../../utils/use-destroy';
import { useLatestRef } from '../../utils/use-latest-ref';
import { useSafeId } from '../../utils/use-safe-id';
import { useOptionalControlsContext } from '../controls/context';
import { usePositionedState } from '../hooks/use-positioned-state';
import { PopoverContextProvider } from './context';
export interface PopoverRootProps extends CorePopoverProps {
@@ -112,11 +113,22 @@ export function PopoverRoot({
const input = useSnapshot(popover.input);
core.setInput(input);
const state = core.getState();
const { state, preferredSide, setPositionedSide } = usePositionedState(core.getState());
return (
<PopoverContextProvider
value={{ core, popover, state, stateAttrMap: PopoverDataAttrs, anchorName, popupId, boundary, container }}
value={{
core,
popover,
state,
preferredSide,
setPositionedSide,
stateAttrMap: PopoverDataAttrs,
anchorName,
popupId,
boundary,
container,
}}
>
{children}
</PopoverContextProvider>
@@ -0,0 +1,45 @@
import { cleanup, render, screen, waitFor } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import * as Popover from '../index.parts';
function makeDOMRect(x: number, y: number, width: number, height: number): DOMRect {
return new DOMRect(x, y, width, height);
}
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
describe('Popover', () => {
it('exposes the positioned side on every part', async () => {
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement) {
if (this.dataset.testid === 'trigger') return makeDOMRect(100, 10, 40, 20);
if (this.dataset.testid === 'popup') return makeDOMRect(0, 0, 100, 60);
return makeDOMRect(0, 0, 300, 200);
});
vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockImplementation(function (this: HTMLElement) {
return this.dataset.testid === 'popup' ? 100 : 0;
});
vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockImplementation(function (this: HTMLElement) {
return this.dataset.testid === 'popup' ? 60 : 0;
});
render(
<Popover.Root defaultOpen side="top" boundary="viewport">
<Popover.Trigger data-testid="trigger">Open</Popover.Trigger>
<Popover.Popup data-testid="popup">
Content
<Popover.Arrow data-testid="arrow" />
</Popover.Popup>
</Popover.Root>
);
await waitFor(() => {
for (const part of ['trigger', 'popup', 'arrow']) {
expect(screen.getByTestId(part).getAttribute('data-side')).toBe('bottom');
}
});
});
});
@@ -13,6 +13,8 @@ export interface TooltipContextValue {
core: TooltipCore;
tooltip: TooltipApi;
state: TooltipCore.State;
preferredSide: TooltipCore.State['side'];
setPositionedSide: (side: TooltipCore.State['side']) => void;
stateAttrMap: StateAttrMap<TooltipCore.State>;
anchorName: string;
popupId: string;
@@ -1,10 +1,18 @@
import { render, waitFor } from '@testing-library/react';
import { popup } from '@videojs/skins/default/tailwind/video.tailwind';
import { useLayoutEffect } from 'react';
import { describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { Tooltip, useOptionalTooltipContext } from '..';
function makeDOMRect(x: number, y: number, width: number, height: number): DOMRect {
return new DOMRect(x, y, width, height);
}
afterEach(() => {
vi.restoreAllMocks();
});
function TooltipContent({ label, shortcut }: { label?: string; shortcut?: string }) {
const tooltip = useOptionalTooltipContext();
const setContent = tooltip?.setContent;
@@ -18,6 +26,37 @@ function TooltipContent({ label, shortcut }: { label?: string; shortcut?: string
}
describe('Tooltip', () => {
it('exposes the positioned side on every part', async () => {
vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function (this: HTMLElement) {
if (this.dataset.testid === 'trigger') return makeDOMRect(100, 10, 40, 20);
if (this.dataset.testid === 'popup') return makeDOMRect(0, 0, 100, 60);
return makeDOMRect(0, 0, 300, 200);
});
vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockImplementation(function (this: HTMLElement) {
return this.dataset.testid === 'popup' ? 100 : 0;
});
vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockImplementation(function (this: HTMLElement) {
return this.dataset.testid === 'popup' ? 60 : 0;
});
const { container } = render(
<Tooltip.Root defaultOpen side="top" boundary="viewport">
<Tooltip.Trigger data-testid="trigger">Play</Tooltip.Trigger>
<Tooltip.Popup data-testid="popup">
<Tooltip.Label data-testid="label">Play</Tooltip.Label>
<Tooltip.Shortcut data-testid="shortcut">K</Tooltip.Shortcut>
<Tooltip.Arrow data-testid="arrow" />
</Tooltip.Popup>
</Tooltip.Root>
);
await waitFor(() => {
for (const part of ['trigger', 'popup', 'label', 'shortcut', 'arrow']) {
expect(container.querySelector(`[data-testid="${part}"]`)?.getAttribute('data-side')).toBe('bottom');
}
});
});
it('renders label and kbd shortcut with skin popup.tooltipShortcut from context', async () => {
const { container } = render(
<Tooltip.Root defaultOpen>
+26 -13
View File
@@ -4,6 +4,7 @@ import { TooltipCSSVars, type TooltipState } from '@videojs/core';
import {
getAnchorPositionStyle,
getPopupPositionRect,
getPositionedSide,
getPositioningBoundaryRect,
isEventWithinElement,
resolveOffsets,
@@ -29,7 +30,18 @@ export const TooltipPopup = forwardRef<HTMLDivElement, TooltipPopupProps>(functi
{ render, className, style, children, ...elementProps },
forwardedRef
) {
const { core, tooltip, state, stateAttrMap, anchorName, popupId, boundary, container } = useTooltipContext();
const {
core,
tooltip,
state,
preferredSide,
setPositionedSide,
stateAttrMap,
anchorName,
popupId,
boundary,
container,
} = useTooltipContext();
const internalRef = useRef<HTMLDivElement>(null);
const popupRef = useCallback(
@@ -46,7 +58,7 @@ export const TooltipPopup = forwardRef<HTMLDivElement, TooltipPopupProps>(functi
// --- Positioning ---
const posOpts = useMemo(() => ({ side: state.side, align: state.align }), [state.side, state.align]);
const posOpts = useMemo(() => ({ side: preferredSide, align: state.align }), [preferredSide, state.align]);
// CSS Anchor Positioning — computed from state, no measurement needed.
// `position-anchor` is set imperatively in the ref callback above
@@ -65,12 +77,12 @@ export const TooltipPopup = forwardRef<HTMLDivElement, TooltipPopupProps>(functi
return rest as CSSProperties;
}, [anchorName, posOpts]);
// Manual fallback — measure rects after layout, before paint.
const [manualStyle, setManualStyle] = useState<CSSProperties | null>(null);
// Measure rects after layout, before paint.
const [position, setPosition] = useState<CSSProperties | null>(null);
useLayoutEffect(() => {
if (!state.open) {
setManualStyle(null);
setPosition(null);
return;
}
@@ -82,21 +94,23 @@ export const TooltipPopup = forwardRef<HTMLDivElement, TooltipPopupProps>(functi
const triggerRect = triggerEl.getBoundingClientRect();
const root = popupEl.getRootNode() as Document | ShadowRoot;
const boundaryElement = resolvePositioningBoundary(boundary, { container, root });
const popupRect = supportsAnchorPositioning() ? undefined : getPopupPositionRect(popupEl);
const popupRect = getPopupPositionRect(popupEl, posOpts.side);
const boundaryRect = getPositioningBoundaryRect(boundaryElement);
const offsets = resolveOffsets(popupEl, TooltipCSSVars);
const side = getPositionedSide(triggerRect, popupRect, boundaryRect, posOpts, offsets);
const { positionAnchor: _, ...nextStyle } = getAnchorPositionStyle(
anchorName,
posOpts,
{ ...posOpts, side },
triggerRect,
popupRect,
supportsAnchorPositioning() ? undefined : popupRect,
boundaryRect,
offsets,
TooltipCSSVars
);
setManualStyle(nextStyle as CSSProperties);
setPosition(nextStyle as CSSProperties);
setPositionedSide(side);
}
measure();
@@ -148,11 +162,10 @@ export const TooltipPopup = forwardRef<HTMLDivElement, TooltipPopupProps>(functi
window.removeEventListener('scroll', reposition, true);
window.removeEventListener('resize', reposition);
};
}, [state.open, anchorName, posOpts, tooltip, boundary, container]);
}, [state.open, anchorName, posOpts, tooltip, boundary, container, setPositionedSide]);
// Anchor path uses computed styles; manual path uses measured styles;
// fallback resets UA [popover] defaults until positioning is computed.
const positioningStyle = manualStyle ?? anchorStyle ?? POPUP_RESET;
// Use measured styles once available; reset UA [popover] defaults until then.
const positioningStyle = position ?? anchorStyle ?? POPUP_RESET;
// --- Visibility ---
@@ -16,6 +16,7 @@ import { useDestroy } from '../../utils/use-destroy';
import { useLatestRef } from '../../utils/use-latest-ref';
import { useSafeId } from '../../utils/use-safe-id';
import { useOptionalControlsContext } from '../controls/context';
import { usePositionedState } from '../hooks/use-positioned-state';
import { type TooltipContent, TooltipContextProvider } from './context';
import { useTooltipGroup } from './group-context';
@@ -115,7 +116,7 @@ export function TooltipRoot({
const input = useSnapshot(tooltip.input);
core.setInput(input);
const state = core.getState();
const { state, preferredSide, setPositionedSide } = usePositionedState(core.getState());
return (
<TooltipContextProvider
@@ -123,6 +124,8 @@ export function TooltipRoot({
core,
tooltip,
state,
preferredSide,
setPositionedSide,
stateAttrMap: TooltipDataAttrs,
anchorName,
popupId,