Skip to content

Commit 95cbc09

Browse files
committed
feat(menu): bump react-virtual
1 parent afddbd9 commit 95cbc09

File tree

4 files changed

+64
-72
lines changed

4 files changed

+64
-72
lines changed

packages/menu/__tests__/Menu.spec.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { MenuProps } from '../src';
22

33
import { Popover } from '@launchpad-ui/popover';
4-
import { describe, expect, it, vi } from 'vitest';
4+
import { beforeEach, describe, expect, it, vi } from 'vitest';
55

66
import { render, screen, userEvent, waitFor } from '../../../test/utils';
77
import { Menu, MenuDivider, MenuItem, MenuSearch } from '../src';
@@ -31,6 +31,16 @@ const createMenu = ({
3131
);
3232

3333
describe('Menu', () => {
34+
// https://github.com/TanStack/virtual/issues/641#issuecomment-2851908893
35+
beforeEach(() => {
36+
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', {
37+
value: 800,
38+
});
39+
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
40+
value: 800,
41+
});
42+
});
43+
3444
it('renders', () => {
3545
render(createMenu({ size: 'sm' }));
3646
expect(screen.getByRole('menu')).toBeInTheDocument();
@@ -39,7 +49,7 @@ describe('Menu', () => {
3949
it('renders with virtualization', () => {
4050
render(createMenu({ enableVirtualization: true }));
4151
const items = screen.getAllByRole('presentation');
42-
expect(items).toHaveLength(5);
52+
expect(items).toHaveLength(7);
4353
});
4454

4555
it('renders the search field', () => {
@@ -155,7 +165,7 @@ describe('Menu', () => {
155165

156166
const user = userEvent.setup();
157167
await user.click(screen.getByText('Target'));
158-
const items = screen.getAllByRole('menuitem');
168+
const items = await screen.findAllByRole('menuitem');
159169

160170
expect(items[0]).toHaveFocus();
161171
await user.keyboard('{arrowdown}');

packages/menu/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"@react-aria/focus": "3.21.0",
4646
"@react-aria/separator": "3.4.11",
4747
"classix": "2.2.0",
48-
"react-virtual": "2.10.4"
48+
"@tanstack/react-virtual": "3.13.12"
4949
},
5050
"peerDependencies": {
5151
"react": "19.1.1",

packages/menu/src/Menu.tsx

Lines changed: 30 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,9 @@ import type { KeyboardEvent, ReactElement, ReactNode } from 'react';
33
import type { MenuItemProps } from './MenuItem';
44

55
import { useFocusManager } from '@react-aria/focus';
6+
import { useVirtualizer } from '@tanstack/react-virtual';
67
import { cx } from 'classix';
7-
import {
8-
Children,
9-
cloneElement,
10-
useCallback,
11-
useEffect,
12-
useId,
13-
useMemo,
14-
useRef,
15-
useState,
16-
} from 'react';
17-
import { useVirtual } from 'react-virtual';
8+
import { Children, cloneElement, useCallback, useEffect, useId, useMemo, useRef } from 'react';
189

1910
import { MenuBase } from './MenuBase';
2011
import { MenuDivider } from './MenuDivider';
@@ -213,15 +204,13 @@ const ItemVirtualizer = <T extends number | string>(props: ItemVirtualizerProps<
213204
const parentRef = useRef<HTMLDivElement | null>(null);
214205
const searchRef = useRef<HTMLInputElement | null>(null);
215206

216-
const [nextFocusValue, setNextFocusValue] = useState<number | null>(null);
217-
218207
const hasSearch = !!searchElement;
219208

220209
const lastVirtualItemIndex = items ? items.length - 1 : 0;
221210

222-
const rowVirtualizer = useVirtual({
223-
size: items !== null ? items.length : 0,
224-
parentRef,
211+
const rowVirtualizer = useVirtualizer({
212+
count: items !== null ? items.length : 0,
213+
getScrollElement: () => parentRef.current,
225214
estimateSize: useCallback(() => itemHeight, [itemHeight]),
226215
overscan,
227216
});
@@ -238,7 +227,8 @@ const ItemVirtualizer = <T extends number | string>(props: ItemVirtualizerProps<
238227
const focusMenuItem = useCallback(
239228
(index: number) => {
240229
rowVirtualizer.scrollToIndex(index);
241-
setNextFocusValue(index);
230+
const element = getNodeForIndex(index, menuId.current);
231+
element?.focus();
242232
},
243233
[rowVirtualizer],
244234
);
@@ -314,16 +304,10 @@ const ItemVirtualizer = <T extends number | string>(props: ItemVirtualizerProps<
314304
},
315305
[handleKeyboardFocusInteraction, menuItemClassName, onSelect],
316306
);
317-
318307
useEffect(() => {
319-
if (nextFocusValue !== null) {
320-
requestAnimationFrame(() => {
321-
const element = getNodeForIndex(nextFocusValue, menuId.current);
322-
element?.focus();
323-
});
324-
setNextFocusValue(null);
325-
}
326-
}, [nextFocusValue]);
308+
const element = getNodeForIndex(0, menuId.current);
309+
element?.focus();
310+
}, []);
327311

328312
/**
329313
* Calls handleFocusForward when the user is attempting to focus forward using
@@ -365,29 +349,25 @@ const ItemVirtualizer = <T extends number | string>(props: ItemVirtualizerProps<
365349
[searchElement, lastVirtualItemIndex, focusMenuItem],
366350
);
367351

368-
const renderItems = useMemo(
369-
() =>
370-
rowVirtualizer.virtualItems.map((virtualRow) => {
371-
if (!items) {
372-
return null;
373-
}
374-
const elem = items[virtualRow.index];
375-
return (
376-
<div
377-
key={virtualRow.index}
378-
ref={virtualRow.measureRef}
379-
role="presentation"
380-
className={styles['VirtualMenu-item']}
381-
style={{
382-
transform: `translateY(${virtualRow.start}px)`,
383-
}}
384-
>
385-
{cloneElement(elem, getItemProps(elem, virtualRow.index))}
386-
</div>
387-
);
388-
}),
389-
[rowVirtualizer.virtualItems, items, getItemProps],
390-
);
352+
const renderItems = rowVirtualizer.getVirtualItems().map((virtualRow) => {
353+
if (!items) {
354+
return null;
355+
}
356+
const elem = items[virtualRow.index];
357+
return (
358+
<div
359+
key={virtualRow.index}
360+
ref={rowVirtualizer.measureElement}
361+
role="presentation"
362+
className={styles['VirtualMenu-item']}
363+
style={{
364+
transform: `translateY(${virtualRow.start}px)`,
365+
}}
366+
>
367+
{cloneElement(elem, getItemProps(elem, virtualRow.index))}
368+
</div>
369+
);
370+
});
391371

392372
return (
393373
<>
@@ -397,7 +377,7 @@ const ItemVirtualizer = <T extends number | string>(props: ItemVirtualizerProps<
397377
role="presentation"
398378
className={styles['VirtualMenu-item-list']}
399379
style={{
400-
height: `${rowVirtualizer.totalSize}px`,
380+
height: `${rowVirtualizer.getTotalSize()}px`,
401381
}}
402382
>
403383
{renderItems}

pnpm-lock.yaml

Lines changed: 20 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)