-
Notifications
You must be signed in to change notification settings - Fork 24
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
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
4e688a3
feat(BulkSelect): Add BulkSelect
fhlavac 1917848
Add BulkSelect docs
fhlavac 63169f4
Test BulkSelect
fhlavac 1adc87b
Update the cypress tests
fhlavac 68a6579
Add menuToggleCheckboxProps and configurable ID
fhlavac 1b37107
Fix typo
fhlavac d01d4ba
Add a shouldFocusToggleOnSelect flag
fhlavac File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); |
37 changes: 37 additions & 0 deletions
37
...nfly-docs/content/extensions/component-groups/examples/BulkSelect/BulkSelect.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
|
||
``` |
28 changes: 28 additions & 0 deletions
28
...fly-docs/content/extensions/component-groups/examples/BulkSelect/BulkSelectAllExample.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))} | ||
/> | ||
); | ||
} |
26 changes: 26 additions & 0 deletions
26
...ernfly-docs/content/extensions/component-groups/examples/BulkSelect/BulkSelectExample.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))} | ||
/> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 theonChange
andisChecked
props, but the rest should be configurable.