Skip to content

Commit

Permalink
[EuiBasicTable] Support indeterminate checkboxes in header row (#7817)
Browse files Browse the repository at this point in the history
  • Loading branch information
cee-chen authored Jun 6, 2024
1 parent 317efdb commit 35caec6
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 10 deletions.
1 change: 1 addition & 0 deletions packages/eui/changelogs/upcoming/7817.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Updated `EuiBasicTable` and `EuiInMemoryTable`s with `selection` - the header row checkbox will now render an indeterminate state if some (but not all) rows are selected
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ exports[`EuiBasicTable renders (kitchen sink) with pagination, selection, sortin
aria-label="Select all rows"
class="euiCheckbox__input"
data-test-subj="checkboxSelectAll"
id="_selection_column-checkbox_generated-id_desktop"
id="_selection_column-checkbox_generated-id"
title="Select all rows"
type="checkbox"
/>
<div
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ exports[`EuiInMemoryTable behavior mobile header 1`] = `
<input
aria-label="Select all rows"
class="euiCheckbox__input"
id="_selection_column-checkbox_generated-id_mobile"
data-test-subj="checkboxSelectAll"
id="_selection_column-checkbox_generated-id"
title="Select all rows"
type="checkbox"
/>
<div
class="euiCheckbox__square"
/>
<label
class="euiCheckbox__label"
for="_selection_column-checkbox_generated-id_mobile"
for="_selection_column-checkbox_generated-id"
>
Select all rows
</label>
Expand Down
67 changes: 67 additions & 0 deletions packages/eui/src/components/basic_table/basic_table.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import React from 'react';
import { fireEvent } from '@testing-library/react';
import { render, screen } from '../../test/rtl';
import { requiredProps } from '../../test';
import { shouldRenderCustomStyles } from '../../test/internal';
Expand Down Expand Up @@ -461,6 +462,72 @@ describe('EuiBasicTable', () => {
expect(onSelectionChange).toHaveBeenCalledWith([]);
expect(container.querySelectorAll('[checked]')).toHaveLength(0);
});

describe('header checkbox', () => {
it('selects all rows', () => {
const props: EuiBasicTableProps<BasicItem> = {
items: basicItems,
columns: basicColumns,
itemId: 'id',
selection: {
onSelectionChange: () => {},
initialSelected: [],
},
};
const { getByTestSubject } = render(<EuiBasicTable {...props} />);
expect(getByTestSubject('checkboxSelectAll')).not.toBeChecked();

fireEvent.click(getByTestSubject('checkboxSelectAll'));

expect(getByTestSubject('checkboxSelectAll')).toBeChecked();
expect(getCheckboxAt(1)).toBeChecked();
expect(getCheckboxAt(2)).toBeChecked();
expect(getCheckboxAt(3)).toBeChecked();
});

it('deselects all rows', () => {
const props: EuiBasicTableProps<BasicItem> = {
items: basicItems,
columns: basicColumns,
itemId: 'id',
selection: {
onSelectionChange: () => {},
initialSelected: basicItems,
},
};
const { getByTestSubject } = render(<EuiBasicTable {...props} />);
expect(getByTestSubject('checkboxSelectAll')).toBeChecked();

fireEvent.click(getByTestSubject('checkboxSelectAll'));

expect(getByTestSubject('checkboxSelectAll')).not.toBeChecked();
expect(getCheckboxAt(1)).not.toBeChecked();
expect(getCheckboxAt(2)).not.toBeChecked();
expect(getCheckboxAt(3)).not.toBeChecked();
});

it('renders an indeterminate header checkbox if some but not all rows are selected', () => {
const props: EuiBasicTableProps<BasicItem> = {
items: basicItems,
columns: basicColumns,
itemId: 'id',
selection: {
onSelectionChange: () => {},
initialSelected: [],
},
};
const { getByTestSubject } = render(<EuiBasicTable {...props} />);
expect(getByTestSubject('checkboxSelectAll')).not.toBeChecked();

fireEvent.click(getCheckboxAt(1));
expect(getCheckboxAt(1)).toBeChecked();
expect(getByTestSubject('checkboxSelectAll')).toBePartiallyChecked();

// Should deselect all rows on indeterminate click
fireEvent.click(getByTestSubject('checkboxSelectAll'));
expect(getCheckboxAt(1)).not.toBeChecked();
});
});
});

test('footers', () => {
Expand Down
24 changes: 17 additions & 7 deletions packages/eui/src/components/basic_table/basic_table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -688,27 +688,37 @@ export class EuiBasicTable<T extends object = any> extends Component<
selectableItems.length > 0 &&
this.state.selection.length === selectableItems.length;

const indeterminate =
!checked &&
this.state.selection &&
selectableItems.length > 0 &&
this.state.selection.length > 0;

const disabled = selectableItems.length === 0;

const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
if (event.target.checked && !indeterminate) {
this.changeSelection(selectableItems);
} else {
this.changeSelection([]);
}
};

return (
<EuiI18n token="euiBasicTable.selectAllRows" default="Select all rows">
{(selectAllRows: string) => (
<EuiI18n
tokens={['euiBasicTable.selectAllRows', 'euiBasicTable.deselectRows']}
defaults={['Select all rows', 'Deselect rows']}
>
{([selectAllRows, deselectRows]: string[]) => (
<EuiCheckbox
id={this.selectAllIdGenerator(isMobile ? 'mobile' : 'desktop')}
id={this.selectAllIdGenerator()}
checked={checked}
indeterminate={indeterminate}
disabled={disabled}
onChange={onChange}
// Only add data-test-subj to one of the checkboxes
data-test-subj={isMobile ? undefined : 'checkboxSelectAll'}
aria-label={selectAllRows}
data-test-subj="checkboxSelectAll"
aria-label={checked || indeterminate ? deselectRows : selectAllRows}
title={checked || indeterminate ? deselectRows : selectAllRows}
label={isMobile ? selectAllRows : null}
/>
)}
Expand Down

0 comments on commit 35caec6

Please sign in to comment.