diff --git a/packages/react/src/select/root/SelectRoot.test.tsx b/packages/react/src/select/root/SelectRoot.test.tsx index 0e276997e75..fe419c16684 100644 --- a/packages/react/src/select/root/SelectRoot.test.tsx +++ b/packages/react/src/select/root/SelectRoot.test.tsx @@ -963,6 +963,148 @@ describe('', () => { expect(screen.getByRole('option', { name: 'b' })).toHaveAttribute('data-highlighted'); }); }); + + it('recomputes positioning before the popup becomes visible again after touch dismiss', async ({ + onTestFinished, + }) => { + globalThis.BASE_UI_ANIMATIONS_DISABLED = false; + onTestFinished(() => { + globalThis.BASE_UI_ANIMATIONS_DISABLED = true; + }); + + const onOpenChangeComplete = vi.fn(); + const items = Array.from({ length: 80 }, (_, index) => `Item ${index + 1}`); + const style = ` + @keyframes select-reopen-test { + to { + opacity: 0; + transform: scale(0.9); + } + } + + .reopen-test-popup { + width: 120px; + transition: + transform 150ms, + opacity 150ms; + } + + .reopen-test-popup[data-starting-style], + .reopen-test-popup[data-ending-style] { + animation: select-reopen-test 20ms linear; + } + + .reopen-test-list { + max-height: var(--available-height); + overflow-y: auto; + } + `; + + function Test() { + const [open, setOpen] = React.useState(false); + const [paddingTop, setPaddingTop] = React.useState(0); + const triggerRef = React.useRef(null); + + React.useLayoutEffect(() => { + const trigger = triggerRef.current; + if (!trigger) { + return; + } + + const gap = + document.documentElement.clientHeight - trigger.getBoundingClientRect().bottom; + if (Math.abs(gap - 100) <= 1) { + return; + } + + setPaddingTop((prev) => prev + gap - 100); + }, [paddingTop]); + + return ( + + {/* eslint-disable-next-line react/no-danger */} + + Outside + + Open + + + + + + + + Start + + {items.map((item) => ( + + {item} + + ))} + + End + + + + + + + + + ); + } + + const { user } = await render(); + + const trigger = screen.getByRole('combobox'); + const outside = screen.getByTestId('outside'); + + await waitFor(() => { + const gap = document.documentElement.clientHeight - trigger.getBoundingClientRect().bottom; + expect(Math.abs(gap - 100)).toBeLessThanOrEqual(1); + }); + + function fireTouchPress() { + fireEvent.pointerDown(trigger, { pointerType: 'touch' }); + fireEvent.mouseDown(trigger); + } + + fireTouchPress(); + + await waitFor(() => { + expect(screen.queryByRole('listbox')).not.toBe(null); + }); + + const initialPositioner = screen.getByTestId('positioner'); + + expect(initialPositioner).toHaveAttribute('data-side', 'top'); + + fireEvent.pointerDown(outside, { pointerType: 'touch' }); + fireEvent.mouseDown(outside); + + await waitFor(() => { + expect(trigger).toHaveAttribute('aria-expanded', 'false'); + expect(onOpenChangeComplete.mock.calls.some(([value]) => value === false)).toBe(true); + expect(screen.getByTestId('positioner').style.opacity).toBe('0'); + }); + + fireTouchPress(); + + await waitFor(() => { + expect(screen.getByTestId('positioner').style.opacity).not.toBe('0'); + }); + + const reopenedPositioner = screen.getByTestId('positioner'); + const reopenedList = screen.getByRole('listbox'); + expect(reopenedPositioner).toHaveAttribute('data-side', 'top'); + expect(reopenedList.getBoundingClientRect().height).toBeGreaterThan(200); + + await user.click(outside); + }); }); describe('prop: actionsRef', () => { diff --git a/packages/react/src/utils/useAnchorPositioning.ts b/packages/react/src/utils/useAnchorPositioning.ts index a214cad7c41..0c8a287e05a 100644 --- a/packages/react/src/utils/useAnchorPositioning.ts +++ b/packages/react/src/utils/useAnchorPositioning.ts @@ -160,6 +160,7 @@ export function useAnchorPositioning( const anchorFnCallback = useStableCallback(anchorFn); const anchorDep = anchorFn ? anchorFnCallback : anchor; const anchorValueRef = useValueAsRef(anchor); + const mountedRef = useValueAsRef(mounted); const direction = useDirection(); const isRtl = direction === 'rtl'; @@ -324,6 +325,9 @@ export function useAnchorPositioning( size({ ...commonCollisionProps, apply({ elements: { floating }, availableWidth, availableHeight, rects }) { + if (!mountedRef.current) { + return; + } const floatingStyle = floating.style; floatingStyle.setProperty('--available-width', `${availableWidth}px`); floatingStyle.setProperty('--available-height', `${availableHeight}px`); @@ -425,6 +429,7 @@ export function useAnchorPositioning( floatingStyles: originalFloatingStyles, } = useFloating({ rootContext: floatingRootContext, + open: keepMounted ? mounted : undefined, placement, middleware, strategy: positionMethod,