|
11 | 11 | */
|
12 | 12 |
|
13 | 13 | jest.mock('@react-aria/live-announcer');
|
14 |
| -import {act, fireEvent, render as renderComponent, within} from '@testing-library/react'; |
| 14 | +import {act, fireEvent, render as renderComponent, waitFor, within} from '@testing-library/react'; |
15 | 15 | import {ActionButton} from '@react-spectrum/button';
|
16 | 16 | import {announce} from '@react-aria/live-announcer';
|
17 | 17 | import {CUSTOM_DRAG_TYPE} from '@react-aria/dnd/src/constants';
|
18 | 18 | import {DataTransfer, DataTransferItem, DragEvent} from '@react-aria/dnd/test/mocks';
|
19 |
| -import {DragExample} from '../stories/ListView.stories'; |
| 19 | +import {DragBetweenListsRootOnlyExample, DragExample, DragIntoItemExample, ReorderExample} from '../stories/ListView.stories'; |
20 | 20 | import {Droppable} from '@react-aria/dnd/test/examples';
|
21 | 21 | import {installPointerEvent, triggerPress} from '@react-spectrum/test-utils';
|
22 | 22 | import {Item, ListView} from '../src';
|
@@ -1359,8 +1359,6 @@ describe('ListView', function () {
|
1359 | 1359 | let cellText = getAllByText(cell.textContent);
|
1360 | 1360 | expect(cellText).toHaveLength(1);
|
1361 | 1361 |
|
1362 |
| - // Need raf to be async so the drag preview shows up properly |
1363 |
| - jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => setTimeout(cb, 0)); |
1364 | 1362 | let dataTransfer = new DataTransfer();
|
1365 | 1363 |
|
1366 | 1364 | fireEvent.pointerDown(cell, {pointerType: 'mouse', button: 0, pointerId: 1, clientX: 5, clientY: 5});
|
@@ -1514,7 +1512,7 @@ describe('ListView', function () {
|
1514 | 1512 | act(() => jest.runAllTimers());
|
1515 | 1513 | expect(onDrop).toHaveBeenCalledTimes(1);
|
1516 | 1514 |
|
1517 |
| - expect(await onDrop.mock.calls[0][0].items.length).toBe(4); |
| 1515 | + expect(await onDrop.mock.calls[0][0].items).toHaveLength(4); |
1518 | 1516 | expect(await onDrop.mock.calls[0][0].items[0].getText('text/plain')).toBe('Adobe Photoshop');
|
1519 | 1517 | expect(await onDrop.mock.calls[0][0].items[1].getText('text/plain')).toBe('Adobe XD');
|
1520 | 1518 | expect(await onDrop.mock.calls[0][0].items[2].getText('text/plain')).toBe('Documents');
|
@@ -1681,7 +1679,7 @@ describe('ListView', function () {
|
1681 | 1679 |
|
1682 | 1680 | expect(onDrop).toHaveBeenCalledTimes(1);
|
1683 | 1681 |
|
1684 |
| - expect(await onDrop.mock.calls[0][0].items.length).toBe(4); |
| 1682 | + expect(await onDrop.mock.calls[0][0].items).toHaveLength(4); |
1685 | 1683 | expect(await onDrop.mock.calls[0][0].items[0].getText('text/plain')).toBe('Adobe Photoshop');
|
1686 | 1684 | expect(await onDrop.mock.calls[0][0].items[1].getText('text/plain')).toBe('Adobe XD');
|
1687 | 1685 | expect(await onDrop.mock.calls[0][0].items[2].getText('text/plain')).toBe('Documents');
|
@@ -1849,8 +1847,179 @@ describe('ListView', function () {
|
1849 | 1847 | expect(onSelectionChange).toHaveBeenCalledTimes(0);
|
1850 | 1848 | });
|
1851 | 1849 |
|
| 1850 | + it('should only count the selected keys that exist in the collection when dragging and dropping', async function () { |
| 1851 | + let {getAllByRole} = render( |
| 1852 | + <DragIntoItemExample dragHookOptions={{onDragStart, onDragEnd}} listViewProps={{onSelectionChange, disabledKeys: []}} dropHookOptions={{onDrop}} /> |
| 1853 | + ); |
| 1854 | + |
| 1855 | + userEvent.tab(); |
| 1856 | + let rows = getAllByRole('row'); |
| 1857 | + expect(rows).toHaveLength(7); |
| 1858 | + let droppable = rows[0]; |
| 1859 | + moveFocus('ArrowDown'); |
| 1860 | + fireEvent.keyDown(document.activeElement, {key: 'Enter'}); |
| 1861 | + fireEvent.keyUp(document.activeElement, {key: 'Enter'}); |
| 1862 | + moveFocus('ArrowDown'); |
| 1863 | + fireEvent.keyDown(document.activeElement, {key: 'Enter'}); |
| 1864 | + fireEvent.keyUp(document.activeElement, {key: 'Enter'}); |
| 1865 | + moveFocus('ArrowDown'); |
| 1866 | + fireEvent.keyDown(document.activeElement, {key: 'Enter'}); |
| 1867 | + fireEvent.keyUp(document.activeElement, {key: 'Enter'}); |
| 1868 | + |
| 1869 | + expect(new Set(onSelectionChange.mock.calls[2][0])).toEqual(new Set(['1', '2', '3'])); |
| 1870 | + let draghandle = within(rows[3]).getAllByRole('button')[0]; |
| 1871 | + expect(draghandle).toBeTruthy(); |
| 1872 | + expect(draghandle).toHaveAttribute('draggable', 'true'); |
| 1873 | + |
| 1874 | + moveFocus('ArrowRight'); |
| 1875 | + fireEvent.keyDown(draghandle, {key: 'Enter'}); |
| 1876 | + fireEvent.keyUp(draghandle, {key: 'Enter'}); |
| 1877 | + |
| 1878 | + expect(onDragStart).toHaveBeenCalledTimes(1); |
| 1879 | + expect(onDragStart).toHaveBeenCalledWith({ |
| 1880 | + type: 'dragstart', |
| 1881 | + keys: new Set(['1', '2', '3']), |
| 1882 | + x: 50, |
| 1883 | + y: 25 |
| 1884 | + }); |
| 1885 | + |
| 1886 | + act(() => jest.runAllTimers()); |
| 1887 | + expect(document.activeElement).toBe(droppable); |
| 1888 | + fireEvent.keyDown(droppable, {key: 'Enter'}); |
| 1889 | + fireEvent.keyUp(droppable, {key: 'Enter'}); |
| 1890 | + await act(async () => Promise.resolve()); |
| 1891 | + act(() => jest.runAllTimers()); |
| 1892 | + |
| 1893 | + expect(onDrop).toHaveBeenCalledTimes(1); |
| 1894 | + expect(await onDrop.mock.calls[0][0].items).toHaveLength(3); |
| 1895 | + expect(onDragEnd).toHaveBeenCalledTimes(1); |
| 1896 | + expect(onDragEnd).toHaveBeenCalledWith({ |
| 1897 | + type: 'dragend', |
| 1898 | + keys: new Set(['1', '2', '3']), |
| 1899 | + x: 50, |
| 1900 | + y: 25, |
| 1901 | + dropOperation: 'move' |
| 1902 | + }); |
| 1903 | + onSelectionChange.mockClear(); |
| 1904 | + onDragStart.mockClear(); |
| 1905 | + |
| 1906 | + rows = getAllByRole('row'); |
| 1907 | + expect(rows).toHaveLength(4); |
| 1908 | + |
| 1909 | + // Select the folder and perform a drag. Drag start shouldn't include the previously selected items |
| 1910 | + moveFocus('ArrowDown'); |
| 1911 | + fireEvent.keyDown(droppable, {key: 'Enter'}); |
| 1912 | + fireEvent.keyUp(droppable, {key: 'Enter'}); |
| 1913 | + // Selection change event still has all keys |
| 1914 | + expect(new Set(onSelectionChange.mock.calls[0][0])).toEqual(new Set(['1', '2', '3', '0'])); |
| 1915 | + |
| 1916 | + draghandle = within(rows[0]).getAllByRole('button')[0]; |
| 1917 | + expect(draghandle).toBeTruthy(); |
| 1918 | + expect(draghandle).toHaveAttribute('draggable', 'true'); |
| 1919 | + moveFocus('ArrowRight'); |
| 1920 | + fireEvent.keyDown(draghandle, {key: 'Enter'}); |
| 1921 | + fireEvent.keyUp(draghandle, {key: 'Enter'}); |
| 1922 | + act(() => jest.runAllTimers()); |
| 1923 | + |
| 1924 | + expect(onDragStart).toHaveBeenCalledTimes(1); |
| 1925 | + expect(onDragStart).toHaveBeenCalledWith({ |
| 1926 | + type: 'dragstart', |
| 1927 | + keys: new Set(['0']), |
| 1928 | + x: 50, |
| 1929 | + y: 25 |
| 1930 | + }); |
| 1931 | + |
| 1932 | + fireEvent.keyDown(document.body, {key: 'Escape'}); |
| 1933 | + fireEvent.keyUp(document.body, {key: 'Escape'}); |
| 1934 | + }); |
| 1935 | + |
| 1936 | + it('should automatically focus the newly added dropped item', async function () { |
| 1937 | + let {getAllByRole} = render( |
| 1938 | + <DragBetweenListsRootOnlyExample dragHookOptions={{onDragStart, onDragEnd}} listViewProps={{onSelectionChange, disabledKeys: []}} dropHookOptions={{onDrop}} /> |
| 1939 | + ); |
| 1940 | + |
| 1941 | + let grids = getAllByRole('grid'); |
| 1942 | + expect(grids).toHaveLength(2); |
| 1943 | + let firstListRows = within(grids[0]).getAllByRole('row'); |
| 1944 | + let draggedCell = within(firstListRows[0]).getByRole('gridcell'); |
| 1945 | + let secondListRows = within(grids[1]).getAllByRole('row'); |
| 1946 | + expect(firstListRows).toHaveLength(6); |
| 1947 | + expect(secondListRows).toHaveLength(6); |
| 1948 | + |
| 1949 | + let dataTransfer = new DataTransfer(); |
| 1950 | + fireEvent.pointerDown(draggedCell, {pointerType: 'mouse', button: 0, pointerId: 1, clientX: 0, clientY: 0}); |
| 1951 | + fireEvent(draggedCell, new DragEvent('dragstart', {dataTransfer, clientX: 0, clientY: 0})); |
| 1952 | + |
| 1953 | + act(() => jest.runAllTimers()); |
| 1954 | + expect(onDragStart).toHaveBeenCalledTimes(1); |
| 1955 | + fireEvent.pointerMove(draggedCell, {pointerType: 'mouse', button: 0, pointerId: 1, clientX: 1, clientY: 1}); |
| 1956 | + fireEvent(draggedCell, new DragEvent('drag', {dataTransfer, clientX: 1, clientY: 1})); |
| 1957 | + fireEvent(grids[1], new DragEvent('dragover', {dataTransfer, clientX: 1, clientY: 1})); |
| 1958 | + fireEvent.pointerUp(draggedCell, {pointerType: 'mouse', button: 0, pointerId: 1, clientX: 1, clientY: 1}); |
| 1959 | + fireEvent(draggedCell, new DragEvent('dragend', {dataTransfer, clientX: 1, clientY: 1})); |
| 1960 | + expect(onDragEnd).toHaveBeenCalledTimes(1); |
| 1961 | + fireEvent(grids[1], new DragEvent('drop', {dataTransfer, clientX: 1, clientY: 1})); |
| 1962 | + |
| 1963 | + await waitFor(() => expect(within(grids[1]).getAllByRole('row')).toHaveLength(7), {interval: 10}); |
| 1964 | + expect(onDrop).toHaveBeenCalledTimes(1); |
| 1965 | + |
| 1966 | + grids = getAllByRole('grid'); |
| 1967 | + firstListRows = within(grids[0]).getAllByRole('row'); |
| 1968 | + secondListRows = within(grids[1]).getAllByRole('row'); |
| 1969 | + expect(firstListRows).toHaveLength(5); |
| 1970 | + expect(secondListRows).toHaveLength(7); |
| 1971 | + |
| 1972 | + // The newly added row in the second list should be the active element |
| 1973 | + expect(secondListRows[6]).toBe(document.activeElement); |
| 1974 | + expect(secondListRows[6]).toHaveTextContent('Item One'); |
| 1975 | + |
| 1976 | + for (let [index, row] of secondListRows.entries()) { |
| 1977 | + if (index !== 6) { |
| 1978 | + expect(row).toHaveAttribute('tabIndex', '-1'); |
| 1979 | + } else { |
| 1980 | + expect(row).toHaveAttribute('tabIndex', '0'); |
| 1981 | + } |
| 1982 | + } |
| 1983 | + |
| 1984 | + draggedCell = firstListRows[3]; |
| 1985 | + dataTransfer = new DataTransfer(); |
| 1986 | + fireEvent.pointerDown(draggedCell, {pointerType: 'mouse', button: 0, pointerId: 1, clientX: 0, clientY: 0}); |
| 1987 | + fireEvent(draggedCell, new DragEvent('dragstart', {dataTransfer, clientX: 0, clientY: 0})); |
| 1988 | + |
| 1989 | + act(() => jest.runAllTimers()); |
| 1990 | + expect(onDragStart).toHaveBeenCalledTimes(2); |
| 1991 | + fireEvent.pointerMove(draggedCell, {pointerType: 'mouse', button: 0, pointerId: 1, clientX: 1, clientY: 1}); |
| 1992 | + fireEvent(draggedCell, new DragEvent('drag', {dataTransfer, clientX: 1, clientY: 1})); |
| 1993 | + fireEvent(grids[1], new DragEvent('dragover', {dataTransfer, clientX: 1, clientY: 2})); |
| 1994 | + fireEvent.pointerUp(draggedCell, {pointerType: 'mouse', button: 0, pointerId: 1, clientX: 1, clientY: 1}); |
| 1995 | + fireEvent(draggedCell, new DragEvent('dragend', {dataTransfer, clientX: 1, clientY: 1})); |
| 1996 | + expect(onDragEnd).toHaveBeenCalledTimes(2); |
| 1997 | + fireEvent(grids[1], new DragEvent('drop', {dataTransfer, clientX: 1, clientY: 1})); |
| 1998 | + |
| 1999 | + await waitFor(() => expect(within(grids[1]).getAllByRole('row')).toHaveLength(8), {interval: 10}); |
| 2000 | + expect(onDrop).toHaveBeenCalledTimes(2); |
| 2001 | + |
| 2002 | + grids = getAllByRole('grid'); |
| 2003 | + firstListRows = within(grids[0]).getAllByRole('row'); |
| 2004 | + secondListRows = within(grids[1]).getAllByRole('row'); |
| 2005 | + expect(firstListRows).toHaveLength(4); |
| 2006 | + expect(secondListRows).toHaveLength(8); |
| 2007 | + |
| 2008 | + // The 2nd newly added row in the second list should still be the active element |
| 2009 | + expect(secondListRows[7]).toBe(document.activeElement); |
| 2010 | + expect(secondListRows[7]).toHaveTextContent('Item Five'); |
| 2011 | + |
| 2012 | + for (let [index, row] of secondListRows.entries()) { |
| 2013 | + if (index !== 7) { |
| 2014 | + expect(row).toHaveAttribute('tabIndex', '-1'); |
| 2015 | + } else { |
| 2016 | + expect(row).toHaveAttribute('tabIndex', '0'); |
| 2017 | + } |
| 2018 | + } |
| 2019 | + }); |
| 2020 | + |
1852 | 2021 | describe('accessibility', function () {
|
1853 |
| - it('drag handle should reflect the correct number of draggable rows', async function () { |
| 2022 | + it('drag handle should reflect the correct number of draggable rows', function () { |
1854 | 2023 |
|
1855 | 2024 | let {getAllByRole} = render(
|
1856 | 2025 | <DraggableListView listViewProps={{defaultSelectedKeys: ['a', 'b', 'c']}} />
|
@@ -1885,6 +2054,31 @@ describe('ListView', function () {
|
1885 | 2054 | expect(dragButtonB).toHaveAttribute('aria-label', 'Drag 3 selected items');
|
1886 | 2055 | expect(dragButtonD).toHaveAttribute('aria-label', 'Drag 3 selected items');
|
1887 | 2056 | });
|
| 2057 | + |
| 2058 | + it('disabled rows and invalid drop targets should become aria-hidden when keyboard drag session starts', function () { |
| 2059 | + let {getAllByRole} = render( |
| 2060 | + <ReorderExample listViewProps={{disabledKeys: ['2']}} /> |
| 2061 | + ); |
| 2062 | + |
| 2063 | + let rows = getAllByRole('row'); |
| 2064 | + for (let row of rows) { |
| 2065 | + expect(row).not.toHaveAttribute('aria-hidden'); |
| 2066 | + } |
| 2067 | + |
| 2068 | + let row = rows[0]; |
| 2069 | + let cell = within(row).getByRole('gridcell'); |
| 2070 | + let draghandle = within(cell).getAllByRole('button')[0]; |
| 2071 | + expect(row).toHaveAttribute('draggable', 'true'); |
| 2072 | + fireEvent.keyDown(draghandle, {key: 'Enter'}); |
| 2073 | + fireEvent.keyUp(draghandle, {key: 'Enter'}); |
| 2074 | + |
| 2075 | + for (let row of rows) { |
| 2076 | + expect(row).toHaveAttribute('aria-hidden', 'true'); |
| 2077 | + } |
| 2078 | + |
| 2079 | + fireEvent.keyDown(document.body, {key: 'Escape'}); |
| 2080 | + fireEvent.keyUp(document.body, {key: 'Escape'}); |
| 2081 | + }); |
1888 | 2082 | });
|
1889 | 2083 | });
|
1890 | 2084 | });
|
0 commit comments