Skip to content

Commit

Permalink
Add BulkSelect component (patternfly#146)
Browse files Browse the repository at this point in the history
  • Loading branch information
fhlavac authored Jun 11, 2024
1 parent db776ce commit 0e40ad7
Show file tree
Hide file tree
Showing 9 changed files with 525 additions and 0 deletions.
97 changes: 97 additions & 0 deletions cypress/component/BulkSelect.cy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, { useState } from 'react';
import BulkSelect, { BulkSelectProps, BulkSelectValue } from '../../packages/module/dist/dynamic/BulkSelect';

interface DataItem {
name: string
};

const BulkSelectTestComponent = ({ canSelectAll, isDataPaginated }: Omit<BulkSelectProps, 'onSelect' | 'selectedCount' >) => {
const [ selected, setSelected ] = useState<DataItem[]>([]);

const allData = [ { name: '1' }, { name: '2' }, { name: '3' }, { name: '4' }, { name: '5' }, { name: '6' } ];
const pageData = [ { name: '1' }, { name: '2' }, { name: '3' }, { name: '4' }, { name: '5' } ];
const pageDataNames = pageData.map((item) => item.name);
const pageSelected = pageDataNames.every(item => selected.find(selectedItem => selectedItem.name === item));

const handleBulkSelect = (value: BulkSelectValue) => {
value === BulkSelectValue.none && setSelected([]);
value === BulkSelectValue.page && setSelected(pageData);
value === BulkSelectValue.all && setSelected(allData);
value === BulkSelectValue.nonePage && setSelected(selected.filter(item => !pageDataNames.includes(item.name)))};

return (
<BulkSelect
isDataPaginated={isDataPaginated}
canSelectAll={canSelectAll}
pageCount={pageData.length}
totalCount={allData.length}
selectedCount={selected.length}
pageSelected={pageSelected}
pagePartiallySelected={pageDataNames.some(item => selected.find(selectedItem => selectedItem.name === item)) && !pageSelected}
onSelect={handleBulkSelect}
/>
);
};

describe('BulkSelect', () => {
it('renders the bulk select without all', () => {
cy.mount(
<BulkSelectTestComponent />
);
cy.get('[data-ouia-component-id="BulkSelect-checkbox"]').should('exist');
cy.get('[data-ouia-component-id="BulkSelect-toggle"]').click();
cy.get('[data-ouia-component-id="BulkSelect-select-all"]').should('not.exist');
cy.get('[data-ouia-component-id="BulkSelect-select-page"]').should('exist');
cy.get('[data-ouia-component-id="BulkSelect-select-none"]').should('exist');

cy.contains('0 selected').should('not.exist');
});

it('renders the bulk select with all and without page', () => {
cy.mount(
<BulkSelectTestComponent canSelectAll isDataPaginated={false} />
);
cy.get('[data-ouia-component-id="BulkSelect-checkbox"]').should('exist');
cy.get('[data-ouia-component-id="BulkSelect-toggle"]').click();
cy.get('[data-ouia-component-id="BulkSelect-select-all"]').should('exist');
cy.get('[data-ouia-component-id="BulkSelect-select-page"]').should('not.exist');
cy.get('[data-ouia-component-id="BulkSelect-select-none"]').should('exist');

cy.contains('0 selected').should('not.exist');
});

it('renders the bulk select with data', () => {
cy.mount(
<BulkSelectTestComponent canSelectAll />
);

// Initial state
cy.get('input[type="checkbox"]').each(($checkbox) => {
cy.wrap($checkbox).should('not.be.checked');
});

// Checkbox select
cy.get('[data-ouia-component-id="BulkSelect-checkbox"]').first().click();
cy.get('input[type="checkbox"]').should('be.checked');
cy.contains('5 selected').should('exist');

// Select none
cy.get('[data-ouia-component-id="BulkSelect-toggle"]').first().click({ force: true });
cy.get('[data-ouia-component-id="BulkSelect-select-none"]').first().click();
cy.get('input[type="checkbox"]').should('not.be.checked');

// Select all
cy.get('[data-ouia-component-id="BulkSelect-toggle"]').first().click({ force: true });
cy.get('[data-ouia-component-id="BulkSelect-select-all"]').first().click();
cy.contains('6 selected').should('exist');

// Checkbox deselect
cy.get('[data-ouia-component-id="BulkSelect-checkbox"]').first().click({ force: true });
cy.contains('1 selected').should('exist');

// Select page
cy.get('[data-ouia-component-id="BulkSelect-toggle"]').first().click({ force: true });
cy.get('[data-ouia-component-id="BulkSelect-select-page"]').first().click();
cy.contains('5 selected').should('exist');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
# Sidenav top-level section
# should be the same for all markdown files
section: extensions
subsection: Component groups
# Sidenav secondary level section
# should be the same for all markdown files
id: Bulk select
# Tab (react | react-demos | html | html-demos | design-guidelines | accessibility)
source: react
# If you use typescript, the name of the interface to display props for
# These are found through the sourceProps function provided in patternfly-docs.source.js
propComponents: ['BulkSelect']
sourceLink: https://github.com/patternfly/react-component-groups/blob/main/packages/module/patternfly-docs/content/extensions/component-groups/examples/BulkSelect/BulkSelect.md
---
import { useState } from 'react';
import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect';

The **bulk select** provides a way of selecting data records in batches. You can select all data at once, all data on current page or deselect all.

## Examples

### Basic paginated bulk select

To display a default bulk select, you need to pass number of selected items using `selectedCount`, the `onSelect` callback accepting bulk select option values and selecting data accordingly, `pageCount` defining number of items on the current page, `pageSelected` and `pagePartiallySelected` boolean flags to define the state os the bulk select checkbox..

```js file="./BulkSelectExample.tsx"

```

### Bulk select with all option

To display an option for selecting all data at once, pass `canSelectAll` flag together with `totalCount` of data entries. You can also remove the page select option by setting `isDataPaginated` to `false`,

```js file="./BulkSelectAllExample.tsx"

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { useState } from 'react';
import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect';

const allData = [ "Item 1", "Item 2" , "Item 3", "Item4", "Item 5" ];
const pageData = [ "Item 1", "Item 2" ];

export const BasicExample: React.FunctionComponent = () => {
const [ selected, setSelected ] = useState<string[]>(pageData);

const handleBulkSelect = (value: BulkSelectValue) => {
value === BulkSelectValue.none && setSelected([]);
value === BulkSelectValue.all && setSelected(allData);
value === BulkSelectValue.nonePage && setSelected(selected.filter(item => !pageData.includes(item)));
value === BulkSelectValue.page && setSelected(pageData);
};

return (
<BulkSelect
canSelectAll
selectedCount={selected.length}
pageCount={pageData.length}
totalCount={allData.length}
onSelect={handleBulkSelect}
pageSelected={pageData.every(item => selected.includes(item))}
pagePartiallySelected={pageData.some(item => selected.includes(item)) && !pageData.every(item => selected.includes(item))}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { useState } from 'react';
import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect';

const allData = [ "Item 1", "Item 2" , "Item 3", "Item4", "Item 5" ];
const pageData = [ "Item 1", "Item 2" ];

export const BasicExample: React.FunctionComponent = () => {
const [ selected, setSelected ] = useState<string[]>([]);

const handleBulkSelect = (value: BulkSelectValue) => {
value === BulkSelectValue.none && setSelected([]);
value === BulkSelectValue.all && setSelected(allData);
value === BulkSelectValue.nonePage && setSelected(selected.filter(item => !pageData.includes(item)));
value === BulkSelectValue.page && setSelected(pageData);
};

return (
<BulkSelect
selectedCount={selected.length}
pageCount={pageData.length}
onSelect={handleBulkSelect}
pageSelected={pageData.every(item => selected.includes(item))}
pagePartiallySelected={pageData.some(item => selected.includes(item)) && !pageData.every(item => selected.includes(item))}
/>
);
}
18 changes: 18 additions & 0 deletions packages/module/src/BulkSelect/BulkSelect.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';
import { render } from '@testing-library/react';
import BulkSelect from './BulkSelect';

describe('BulkSelect component', () => {
test('should render', () => {
expect(render(
<BulkSelect
canSelectAll
pageCount={5}
totalCount={10}
selectedCount={2}
pageSelected={false}
pagePartiallySelected={true}
onSelect={() => null}
/>)).toMatchSnapshot();
});
});
136 changes: 136 additions & 0 deletions packages/module/src/BulkSelect/BulkSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import React, { useMemo, useState } from 'react';
import {
Dropdown,
DropdownItem,
DropdownList,
DropdownProps,
MenuToggle,
MenuToggleCheckbox,
MenuToggleCheckboxProps,
MenuToggleElement,
Text
} from '@patternfly/react-core';

export const BulkSelectValue = {
all: 'all',
none: 'none',
page: 'page',
nonePage: 'nonePage'
} as const;

export type BulkSelectValue = typeof BulkSelectValue[keyof typeof BulkSelectValue];

export interface BulkSelectProps extends Omit<DropdownProps, 'toggle' | 'onSelect'> {
/** BulkSelect className */
className?: string;
/** Indicates whether selectable items are paginated */
isDataPaginated?: boolean;
/** Indicates whether "Select all" option should be available */
canSelectAll?: boolean;
/** Number of entries present in current page */
pageCount?: number;
/** Number of selected entries */
selectedCount: number;
/** Number of all entries */
totalCount?: number;
/** Indicates if ALL current page items are selected */
pageSelected?: boolean;
/** Indicates if ONLY some current page items are selected */
pagePartiallySelected?: boolean;
/** Callback called on item select */
onSelect: (value: BulkSelectValue) => void;
/** Custom OUIA ID */
ouiaId?: string;
/** Additional props for MenuToggleCheckbox */
menuToggleCheckboxProps?: Omit<MenuToggleCheckboxProps, 'onChange' | 'isChecked' | 'instance' | 'ref'>;
}

export const BulkSelect: React.FC<BulkSelectProps> = ({
isDataPaginated = true,
canSelectAll,
pageSelected,
pagePartiallySelected,
pageCount,
selectedCount = 0,
totalCount,
ouiaId = 'BulkSelect',
onSelect,
menuToggleCheckboxProps,
...props
}: BulkSelectProps) => {
const [ isOpen, setOpen ] = useState(false);

const splitButtonDropdownItems = useMemo(
() => (
<>
<DropdownItem ouiaId={`${ouiaId}-select-none`} value={BulkSelectValue.none} key={BulkSelectValue.none}>
Select none (0)
</DropdownItem>
{isDataPaginated && (
<DropdownItem ouiaId={`${ouiaId}-select-page`} value={BulkSelectValue.page} key={BulkSelectValue.page}>
{`Select page${pageCount ? ` (${pageCount})` : ''}`}
</DropdownItem>
)}
{canSelectAll && (
<DropdownItem ouiaId={`${ouiaId}-select-all`} value={BulkSelectValue.all} key={BulkSelectValue.all}>
{`Select all${totalCount ? ` (${totalCount})` : ''}`}
</DropdownItem>
)}
</>
),
[ isDataPaginated, canSelectAll, ouiaId, pageCount, totalCount ]
);

const allOption = isDataPaginated ? BulkSelectValue.page : BulkSelectValue.all;
const noneOption = isDataPaginated ? BulkSelectValue.nonePage : BulkSelectValue.none;

return (
<Dropdown
shouldFocusToggleOnSelect
ouiaId={`${ouiaId}-dropdown`}
onSelect={(_e, value) => {
setOpen(!isOpen);
onSelect?.(value as BulkSelectValue);
}}
isOpen={isOpen}
onOpenChange={(isOpen: boolean) => setOpen(isOpen)}
toggle={(toggleRef: React.Ref<MenuToggleElement>) => (
<MenuToggle
ref={toggleRef}
isExpanded={isOpen}
onClick={() => setOpen(!isOpen)}
aria-label="Bulk select toggle"
data-ouia-component-id={`${ouiaId}-toggle`}
splitButtonOptions={{
items: [
<MenuToggleCheckbox
ouiaId={`${ouiaId}-checkbox`}
id={`${ouiaId}-checkbox`}
key="bulk-select-checkbox"
aria-label={`Select ${allOption}`}
isChecked={
(isDataPaginated && pagePartiallySelected) ||
(!isDataPaginated && selectedCount > 0)
? null
: pageSelected || selectedCount === totalCount
}
onChange={(checked) => onSelect?.(!checked || checked === null ? noneOption : allOption)}
{...menuToggleCheckboxProps}
/>,
selectedCount > 0 ? (
<Text ouiaId={`${ouiaId}-text`} key="bulk-select-text">
{`${selectedCount} selected`}
</Text>
) : null
]
}}
/>
)}
{...props}
>
<DropdownList>{splitButtonDropdownItems}</DropdownList>
</Dropdown>
);
};

export default BulkSelect;
Loading

0 comments on commit 0e40ad7

Please sign in to comment.