Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions cypress/e2e/list.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,64 @@ describe('List Page', () => {
.click({ shiftKey: true });
cy.contains('6 items selected');
});

it('should allow to deselect a range with shift key', () => {
cy.contains('1-10 of 13'); // wait for data
cy.get(ListPagePosts.elements.selectItem).eq(0).click();
cy.get(ListPagePosts.elements.selectItem)
.eq(4)
.click({ shiftKey: true });
cy.contains('5 items selected');
cy.get(ListPagePosts.elements.selectedItem).should(els =>
expect(els).to.have.length(5)
);
cy.get(ListPagePosts.elements.selectItem)
.eq(2)
.click({ shiftKey: true });
cy.contains('2 items selected');
cy.get(ListPagePosts.elements.selectedItem).should(els =>
expect(els).to.have.length(2)
);
});

it('should allow alternating shift-select and shift-deselect', () => {
cy.contains('1-10 of 13'); // wait for data
cy.get(ListPagePosts.elements.selectItem).eq(0).click();
cy.get(ListPagePosts.elements.selectItem)
.eq(3)
.click({ shiftKey: true });
cy.contains('4 items selected');
cy.get(ListPagePosts.elements.selectItem)
.eq(4)
.click({ shiftKey: true });
cy.contains('5 items selected');
cy.get(ListPagePosts.elements.selectItem)
.eq(2)
.click({ shiftKey: true });
cy.contains('2 items selected');
cy.get(ListPagePosts.elements.selectItem)
.eq(4)
.click({ shiftKey: true });
cy.contains('5 items selected');
cy.get(ListPagePosts.elements.selectedItem).should(els =>
expect(els).to.have.length(5)
);
});

it('should support shift-deselect after select all then manual deselect', () => {
cy.contains('1-10 of 13'); // wait for data
ListPagePosts.toggleSelectAll();
cy.contains('10 items selected');
cy.get(ListPagePosts.elements.selectItem).eq(1).click();
cy.contains('9 items selected');
cy.get(ListPagePosts.elements.selectItem)
.eq(3)
.click({ shiftKey: true });
cy.contains('7 items selected');
cy.get(ListPagePosts.elements.selectedItem).should(els =>
expect(els).to.have.length(7)
);
});
});

describe('rowClick', () => {
Expand Down
24 changes: 12 additions & 12 deletions packages/ra-core/src/dataTable/DataTableBase.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import difference from 'lodash/difference';
import union from 'lodash/union';
import * as React from 'react';
import { useEffect, useMemo, useRef, type FC, type ReactNode } from 'react';
import union from 'lodash/union';
import difference from 'lodash/difference';

import { OptionalResourceContextProvider, useResourceContext } from '../core';
import { useEvent } from '../util';
import { useListContextWithProps } from '../controller/list/useListContextWithProps';
import { type ListControllerResult } from '../controller/list/useListController';
import { type RowClickFunctionBase } from './types';
import { OptionalResourceContextProvider, useResourceContext } from '../core';
import { type Identifier, type RaRecord, type SortPayload } from '../types';
import { DataTableConfigContext } from './DataTableConfigContext';
import { useEvent } from '../util';
import { DataTableCallbacksContext } from './DataTableCallbacksContext';
import { DataTableConfigContext } from './DataTableConfigContext';
import { DataTableDataContext } from './DataTableDataContext';
import { DataTableSelectedIdsContext } from './DataTableSelectedIdsContext';
import { DataTableSortContext } from './DataTableSortContext';
import { DataTableStoreContext } from './DataTableStoreContext';
import { type RowClickFunctionBase } from './types';

export const DataTableBase = function DataTable<
RecordType extends RaRecord = any,
Expand Down Expand Up @@ -76,8 +76,6 @@ export const DataTableBase = function DataTable<
if (!data) return;
const ids = data.map(record => record.id);
const lastSelectedIndex = ids.indexOf(lastSelected.current);
// @ts-ignore FIXME useEvent prevents using event.currentTarget
lastSelected.current = event.target.checked ? id : null;

if (event.shiftKey && lastSelectedIndex !== -1) {
const index = ids.indexOf(id);
Expand All @@ -86,10 +84,10 @@ export const DataTableBase = function DataTable<
Math.max(lastSelectedIndex, index) + 1
);

// @ts-ignore FIXME useEvent prevents using event.currentTarget
const newSelectedIds = event.target.checked
? union(selectedIds, idsBetweenSelections)
: difference(selectedIds, idsBetweenSelections);
const isClickedItemSelected = selectedIds?.includes(id);
const newSelectedIds = isClickedItemSelected
? difference(selectedIds, idsBetweenSelections)
: union(selectedIds, idsBetweenSelections);

onSelect?.(
isRowSelectable
Expand All @@ -103,6 +101,8 @@ export const DataTableBase = function DataTable<
} else {
onToggleItem?.(id);
}

lastSelected.current = id;
}
);

Expand Down
31 changes: 28 additions & 3 deletions packages/ra-ui-materialui/src/list/datatable/DataTable.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import * as React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import {
Basic,
Columns,
Empty,
StandaloneStatic,
StandaloneDynamic,
Expand,
ExpandSingle,
IsRowExpandable,
IsRowSelectable,
NonPrimitiveData,
StandaloneDynamic,
StandaloneStatic,
} from './DataTable.stories';

describe('DataTable', () => {
Expand Down Expand Up @@ -245,6 +245,31 @@ describe('DataTable', () => {
fireEvent.click(checkboxes[0]);
await screen.findByText('2 items selected');
});
it('should support alternating shift-select and shift-deselect', async () => {
render(<Basic />);
const checkboxes = await screen.findAllByRole('checkbox');
fireEvent.click(checkboxes[1]);
fireEvent.click(checkboxes[4], { shiftKey: true });
await screen.findByText('4 items selected');
fireEvent.click(checkboxes[5], { shiftKey: true });
await screen.findByText('5 items selected');
fireEvent.click(checkboxes[3], { shiftKey: true });
await screen.findByText('2 items selected');
fireEvent.click(checkboxes[5], { shiftKey: true });
await screen.findByText('5 items selected');
});
it('should support shift-deselect after select all then deselect one', async () => {
render(<Basic />);
const checkboxes = await screen.findAllByRole('checkbox');
fireEvent.click(checkboxes[0]);
const selectAllButton = await screen.findByText('Select all');
selectAllButton.click();
await screen.findByText('7 items selected');
fireEvent.click(checkboxes[2]);
await screen.findByText('6 items selected');
fireEvent.click(checkboxes[4], { shiftKey: true });
await screen.findByText('4 items selected');
});
});
describe('isRowSelectable', () => {
it('should allow to disable row selection', async () => {
Expand Down
Loading