diff --git a/packages/sanity/package.json b/packages/sanity/package.json index e2ceb9a274c..29f3a190f1b 100644 --- a/packages/sanity/package.json +++ b/packages/sanity/package.json @@ -182,7 +182,7 @@ "@sanity/uuid": "^3.0.1", "@sentry/react": "^8.33.0", "@tanstack/react-table": "^8.16.0", - "@tanstack/react-virtual": "3.0.0-beta.54", + "@tanstack/react-virtual": "^3.11.2", "@types/react-is": "^18.3.0", "@types/shallow-equals": "^1.0.0", "@types/speakingurl": "^13.0.3", diff --git a/packages/sanity/src/core/components/commandList/CommandList.tsx b/packages/sanity/src/core/components/commandList/CommandList.tsx index eaf65aa6c05..c4c44978074 100644 --- a/packages/sanity/src/core/components/commandList/CommandList.tsx +++ b/packages/sanity/src/core/components/commandList/CommandList.tsx @@ -114,6 +114,7 @@ const CommandListComponent = forwardRef(fun onlyShowSelectionWhenActive, overscan, renderItem, + testId, wrapAround = true, ...responsivePaddingProps }, @@ -583,6 +584,7 @@ const CommandListComponent = forwardRef(fun ref={setVirtualListElement} sizing="border" tabIndex={rootTabIndex} + data-testid={testId} {...responsivePaddingProps} > {canReceiveFocus && } diff --git a/packages/sanity/src/core/components/commandList/__tests__/CommandList.test.tsx b/packages/sanity/src/core/components/commandList/__tests__/CommandList.test.tsx index a47711048cc..7bc8aa9bf8f 100644 --- a/packages/sanity/src/core/components/commandList/__tests__/CommandList.test.tsx +++ b/packages/sanity/src/core/components/commandList/__tests__/CommandList.test.tsx @@ -1,11 +1,14 @@ +import {afterEach} from 'node:test' + import {studioTheme, ThemeProvider} from '@sanity/ui' -import {render, screen} from '@testing-library/react' +import {render, screen, waitFor} from '@testing-library/react' import userEvent from '@testing-library/user-event' import {useCallback} from 'react' -import {describe, expect, it} from 'vitest' +import {beforeEach, describe, expect, it, vi} from 'vitest' import {CommandList} from '../CommandList' +const COMMAND_LIST_TEST_ID = 'command-list' const CUSTOM_ACTIVE_ATTR = 'my-active-data-attribute' type Item = number @@ -52,6 +55,7 @@ function TestComponent(props: TestComponentProps) { // same as the number of items for the tests to pass overscan={items.length} renderItem={renderItem} + testId={COMMAND_LIST_TEST_ID} /> @@ -59,13 +63,44 @@ function TestComponent(props: TestComponentProps) { } describe('core/components: CommandList', () => { - it('should change active item on pressing arrow keys', () => { + const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect + + const getDOMRect = (width: number, height: number) => ({ + width, + height, + top: 0, + left: 0, + bottom: 0, + right: 0, + x: 0, + y: 0, + toJSON: () => {}, + }) + + beforeEach(() => { + // Virtual list will return an empty list of items unless we have some size, + // so we need to mock getBoundingClientRect to return a size for the list. + // Not pretty, but it's what they recommend for testing outside of browsers: + // https://github.com/TanStack/virtual/issues/641 + Element.prototype.getBoundingClientRect = vi.fn(function (this: Element) { + if (this.getAttribute('data-testid') === COMMAND_LIST_TEST_ID) { + return getDOMRect(350, 800) + } + return getDOMRect(0, 0) + }) + }) + + afterEach(() => { + Element.prototype.getBoundingClientRect = originalGetBoundingClientRect + }) + + it('should change active item on pressing arrow keys', async () => { render() const buttons = screen.getAllByTestId('button') // First button should be active on render - expect(buttons[0]).toHaveAttribute(CUSTOM_ACTIVE_ATTR) + await waitFor(() => expect(buttons[0]).toHaveAttribute(CUSTOM_ACTIVE_ATTR)) // Set second button as active on arrow down userEvent.keyboard('[ArrowDown]') @@ -96,13 +131,13 @@ describe('core/components: CommandList', () => { expect(buttons[3]).not.toHaveAttribute(CUSTOM_ACTIVE_ATTR) }) - it('should set the initial active item based on the initialIndex prop', () => { + it('should set the initial active item based on the initialIndex prop', async () => { render() const buttons = screen.getAllByTestId('button') // Button with index 2 should be active on render - expect(buttons[0]).not.toHaveAttribute(CUSTOM_ACTIVE_ATTR) + await waitFor(() => expect(buttons[0]).not.toHaveAttribute(CUSTOM_ACTIVE_ATTR)) expect(buttons[1]).not.toHaveAttribute(CUSTOM_ACTIVE_ATTR) expect(buttons[2]).toHaveAttribute(CUSTOM_ACTIVE_ATTR) expect(buttons[3]).not.toHaveAttribute(CUSTOM_ACTIVE_ATTR) @@ -123,13 +158,13 @@ describe('core/components: CommandList', () => { expect(buttons[0]).toHaveAttribute(CUSTOM_ACTIVE_ATTR) }) - it('should skip disabled elements', () => { + it('should skip disabled elements', async () => { render() const buttons = screen.getAllByTestId('button') // Second button should be active since the first button is disabled - expect(buttons[1]).toHaveAttribute(CUSTOM_ACTIVE_ATTR) + await waitFor(() => expect(buttons[1]).toHaveAttribute(CUSTOM_ACTIVE_ATTR)) // Fourth button should be active since the third is disabled userEvent.keyboard('[ArrowDown]') diff --git a/packages/sanity/src/core/components/commandList/types.ts b/packages/sanity/src/core/components/commandList/types.ts index 2190bb8e0e7..4435d399bad 100644 --- a/packages/sanity/src/core/components/commandList/types.ts +++ b/packages/sanity/src/core/components/commandList/types.ts @@ -78,6 +78,8 @@ export interface CommandListProps extends ResponsivePaddingProps { overscan?: number /** Rendered component in virtual lists */ renderItem: CommandListRenderItemCallback + /** `data-testid` to apply to outermost container */ + testId?: string /** Allow wraparound keyboard navigation between first and last items */ wrapAround?: boolean } diff --git a/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/List/ListArrayInput.tsx b/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/List/ListArrayInput.tsx index 609e016429c..3125752fce2 100644 --- a/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/List/ListArrayInput.tsx +++ b/packages/sanity/src/core/form/inputs/arrays/ArrayOfObjectsInput/List/ListArrayInput.tsx @@ -33,7 +33,6 @@ export function ListArrayInput(props: ArrayOfObjectsInp elementProps, members, onChange, - onInsert, onItemMove, onUpload, focusPath, @@ -113,7 +112,7 @@ export function ListArrayInput(props: ArrayOfObjectsInp const scroll = instance.scrollElement - const handleScroll = () => { + const handleScroll = (evt?: Event) => { const containerElementTop = containerElement.current?.getBoundingClientRect().top ?? 0 const parentElementTop = parentRef.current?.getBoundingClientRect().top ?? 0 @@ -122,7 +121,7 @@ export function ListArrayInput(props: ArrayOfObjectsInp // We pass a component that we have more control over to avoid issues when wrapped in custom component const itemOffset = Math.floor(parentElementTop - containerElementTop) - callback(scroll.scrollTop - itemOffset) + callback(scroll.scrollTop - itemOffset, Boolean(evt)) } handleScroll() @@ -225,7 +224,7 @@ export function ListArrayInput(props: ArrayOfObjectsInp top: 0, left: 0, width: '100%', - transform: `translateY(${items[0].start}px)`, + transform: items.length > 0 ? `translateY(${items[0].start}px)` : undefined, }} > {items.map((virtualRow) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f443d7ca71..a8b756b99d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1536,8 +1536,8 @@ importers: specifier: ^8.16.0 version: 8.20.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-virtual': - specifier: 3.0.0-beta.54 - version: 3.0.0-beta.54(react@18.3.1) + specifier: ^3.11.2 + version: 3.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/react-is': specifier: ^18.3.0 version: 18.3.1 @@ -5169,17 +5169,18 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-virtual@3.0.0-beta.54': - resolution: {integrity: sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ==} + '@tanstack/react-virtual@3.11.2': + resolution: {integrity: sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 '@tanstack/table-core@8.20.5': resolution: {integrity: sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==} engines: {node: '>=12'} - '@tanstack/virtual-core@3.0.0-beta.54': - resolution: {integrity: sha512-jtkwqdP2rY2iCCDVAFuaNBH3fiEi29aTn2RhtIoky8DTTiCdc48plpHHreLwmv1PICJ4AJUUESaq3xa8fZH8+g==} + '@tanstack/virtual-core@3.11.2': + resolution: {integrity: sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==} '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} @@ -16272,14 +16273,15 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@tanstack/react-virtual@3.0.0-beta.54(react@18.3.1)': + '@tanstack/react-virtual@3.11.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@tanstack/virtual-core': 3.0.0-beta.54 + '@tanstack/virtual-core': 3.11.2 react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) '@tanstack/table-core@8.20.5': {} - '@tanstack/virtual-core@3.0.0-beta.54': {} + '@tanstack/virtual-core@3.11.2': {} '@testing-library/dom@10.4.0': dependencies: