Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add BulkSelect component #146

Merged
merged 7 commits into from
Jun 11, 2024
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
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`}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might want to provide some sort of MenuToggleCheckboxProps. We can omit the onChange and isChecked props, but the rest should be configurable.

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
Loading