diff --git a/e2e/components/DataTable/DataTable-test.avt.e2e.js b/e2e/components/DataTable/DataTable-test.avt.e2e.js index 5aee29d2dab7..9e87f0a9f0b9 100644 --- a/e2e/components/DataTable/DataTable-test.avt.e2e.js +++ b/e2e/components/DataTable/DataTable-test.avt.e2e.js @@ -215,6 +215,52 @@ test.describe('@avt DataTable', () => { 'components-datatable-filtering--default' ); }); + + test('@avt-keyboard-nav', async ({ page }) => { + await visitStory(page, { + component: 'DataTable', + id: 'components-datatable-filtering--default', + globals: { + theme: 'white', + }, + }); + + // Start off by manually focusing the filtering input + await page.getByLabel('Filtering').focus(); + await expect(page.getByLabel('Filtering')).toBeFocused(); + + await page.keyboard.press('Enter'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Enter'); + // Selecting the first checkbox + await page.keyboard.press('Space'); + + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + // Presisng the primary button Apply Filter + await page.keyboard.press('Enter'); + + // + await expect(page.getByText('443')).not.toBeVisible(); + + // Coming back to the filtering button and pressing Enter to open + await page.keyboard.press('Shift+Tab'); + await page.keyboard.press('Enter'); + + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + await page.keyboard.press('Tab'); + // Presisng the secondary button Reset Filter + await page.keyboard.press('Enter'); + + // All elements should be visible now + await expect(page.getByText('443').first()).toBeVisible(); + }); }); test.describe('@avt selection', () => { diff --git a/packages/react/src/components/DataTable/DataTable.mdx b/packages/react/src/components/DataTable/DataTable.mdx index a4df823d261a..91fdd53049d2 100644 --- a/packages/react/src/components/DataTable/DataTable.mdx +++ b/packages/react/src/components/DataTable/DataTable.mdx @@ -278,6 +278,72 @@ the `onInputChange` function provided to you from `DataTable`'s render prop and pass it to the `onChange` prop of `TableToolbarSearch` in your `TableToolbar` component. +### Multiple filters with batch updates + +The filtering story shows an example of how to implement the +[batch filtering](https://carbondesignsystem.com/patterns/filtering/#multiple-filters-with-batch-updates) +pattern. + +To implement this in your own project you can copy the +[`TableToolbarFilter`](https://github.com/carbon-design-system/carbon/blob/15339-datatable/packages/react/src/components/DataTable/stories/examples/TableToolbarFilter.tsx) +example component into your code. You can then import and use it inside +`TableToolbarContent`: + +```jsx + +``` + +You can write the code to handle the filters in the same file where you have +your DataTable with your data. Use the code below as a template and then +customize it as needed. Make sure to replace "YOUR_DATA" with the initial data +you want to pass into the DataTable. + +```jsx +const [renderedRows, setRenderedRows] = useState(YOUR_DATA); + +const handleTableFilter = (selectedCheckboxes) => { + setRenderedRows([]); + + for (let i = 0; i < selectedCheckboxes.length; i++) { + // Filter the items inside the rows list + const filteredRows = YOUR_DATA.filter((row) => { + return Object.values(row).some((value) => + String(value) + .toLowerCase() + .includes(selectedCheckboxes[i].toLowerCase()) + ); + }); + + setRenderedRows((prevData) => { + // Filter out duplicate rows + const uniqueRows = filteredRows.filter((row) => { + return !prevData.some((prevRow) => { + return Object.keys(row).every((key) => { + return row[key] === prevRow[key]; + }); + }); + }); + return [...prevData, ...uniqueRows]; + }); + } +}; + +const handleOnResetFilter = () => { + setRenderedRows(rows); +}; +``` + +Finally, pass the array of rows from the `useState` into the DataTable. + +```jsx + + ... + +``` + ## Batch actions You can combine batch actions with the `DataTable` component to allow the user diff --git a/packages/react/src/components/DataTable/stories/DataTable-filtering.stories.js b/packages/react/src/components/DataTable/stories/DataTable-filtering.stories.js index 21ff1dc7e6c5..3fad8117161f 100644 --- a/packages/react/src/components/DataTable/stories/DataTable-filtering.stories.js +++ b/packages/react/src/components/DataTable/stories/DataTable-filtering.stories.js @@ -6,7 +6,7 @@ */ import { action } from '@storybook/addon-actions'; -import React from 'react'; +import React, { useState } from 'react'; import Button from '../../Button'; import DataTable, { Table, @@ -24,6 +24,10 @@ import DataTable, { } from '..'; import { rows, headers } from './shared'; import mdx from '../DataTable.mdx'; +import TableToolbarFilter from './examples/TableToolbarFilter'; +import Checkbox from '../../Checkbox'; +import { usePrefix } from '../../../internal/usePrefix'; +import './datatable-story.scss'; export default { title: 'Components/DataTable/Filtering', @@ -62,118 +66,196 @@ export default { }, }; -export const Default = () => ( - - {({ - rows, - headers, - getHeaderProps, - getRowProps, - getTableProps, - onInputChange, - }) => ( - - - - {/* pass in `onInputChange` change here to make filtering work */} - - - - Action 1 - - - Action 2 - - - Action 3 - - - - - - - - - {headers.map((header) => ( - - {header.header} - - ))} - - - - {rows.map((row) => ( - - {row.cells.map((cell) => ( - {cell.value} +export const Default = () => { + const [renderedRows, setRenderedRows] = useState(rows); + + const handleTableFilter = (selectedCheckboxes) => { + setRenderedRows([]); + + for (let i = 0; i < selectedCheckboxes.length; i++) { + // Filter the items inside the rows list + const filteredRows = rows.filter((row) => { + return Object.values(row).some((value) => + String(value) + .toLowerCase() + .includes(selectedCheckboxes[i].toLowerCase()) + ); + }); + + setRenderedRows((prevData) => { + // Filter out duplicate rows + const uniqueRows = filteredRows.filter((row) => { + return !prevData.some((prevRow) => { + return Object.keys(row).every((key) => { + return row[key] === prevRow[key]; + }); + }); + }); + return [...prevData, ...uniqueRows]; + }); + } + }; + + const handleOnResetFilter = () => { + setRenderedRows(rows); + }; + + return ( + + {({ + rows, + headers, + getHeaderProps, + getRowProps, + getTableProps, + onInputChange, + }) => ( + + + + {/* pass in `onInputChange` change here to make filtering work */} + + + + + Action 1 + + + Action 2 + + + Action 3 + + + + + +
+ + + {headers.map((header) => ( + + {header.header} + ))} - ))} - -
-
- )} -
-); - -export const Playground = (args) => ( - - {({ - rows, - headers, - getHeaderProps, - getRowProps, - getTableProps, - onInputChange, - }) => ( - - - - {/* pass in `onInputChange` change here to make filtering work */} - { - action('TableToolbarSearch - onChange')(evt); - onInputChange(evt); - }} - /> - - - Action 1 - - - Action 2 - - - Action 3 - - - - - - - - - {headers.map((header) => ( - - {header.header} - + + + {rows.map((row) => ( + + {row.cells.map((cell) => ( + {cell.value} + ))} + ))} - - - - {rows.map((row) => ( - - {row.cells.map((cell) => ( - {cell.value} + +
+
+ )} +
+ ); +}; + +export const Playground = (args) => { + const [renderedRows, setRenderedRows] = useState(rows); + + const handleTableFilter = (selectedCheckboxes) => { + setRenderedRows([]); + + for (let i = 0; i < selectedCheckboxes.length; i++) { + // Filter the items inside the rows list + const filteredRows = rows.filter((row) => { + return Object.values(row).some((value) => + String(value) + .toLowerCase() + .includes(selectedCheckboxes[i].toLowerCase()) + ); + }); + + setRenderedRows((prevData) => { + // Filter out duplicate rows + const uniqueRows = filteredRows.filter((row) => { + return !prevData.some((prevRow) => { + return Object.keys(row).every((key) => { + return row[key] === prevRow[key]; + }); + }); + }); + return [...prevData, ...uniqueRows]; + }); + } + }; + + const handleOnResetFilter = () => { + setRenderedRows(rows); + }; + + return ( + + {({ + rows, + headers, + getHeaderProps, + getRowProps, + getTableProps, + onInputChange, + }) => ( + + + + {/* pass in `onInputChange` change here to make filtering work */} + { + action('TableToolbarSearch - onChange')(evt); + onInputChange(evt); + }} + /> + + + + Action 1 + + + Action 2 + + + Action 3 + + + + + + + + + {headers.map((header) => ( + + {header.header} + ))} - ))} - -
-
- )} -
-); + + + {rows.map((row) => ( + + {row.cells.map((cell) => ( + {cell.value} + ))} + + ))} + + + + )} + + ); +}; Playground.argTypes = { filterRows: { diff --git a/packages/react/src/components/DataTable/stories/datatable-story.scss b/packages/react/src/components/DataTable/stories/datatable-story.scss index 77923b6b0edd..f2e0e8cd93b6 100644 --- a/packages/react/src/components/DataTable/stories/datatable-story.scss +++ b/packages/react/src/components/DataTable/stories/datatable-story.scss @@ -18,3 +18,7 @@ #storybook-root .slug-column-table .cds--data-table-content { overflow: initial; } + +.cds--container-checkbox { + padding: 1rem; +} diff --git a/packages/react/src/components/DataTable/stories/examples/TableToolbarFilter.tsx b/packages/react/src/components/DataTable/stories/examples/TableToolbarFilter.tsx new file mode 100644 index 000000000000..4d9344922ad2 --- /dev/null +++ b/packages/react/src/components/DataTable/stories/examples/TableToolbarFilter.tsx @@ -0,0 +1,180 @@ +import cx from 'classnames'; +import React, { ChangeEvent, useState } from 'react'; +import PropTypes from 'prop-types'; + +import { Filter } from '@carbon/icons-react'; + +import { usePrefix } from '../../../../internal/usePrefix'; +import { Popover, PopoverContent } from '../../../Popover'; +import Button from '../../../Button'; +import Checkbox from '../../../Checkbox'; +import { Layer } from '../../../Layer'; + +export type PopoverAlignment = + | 'top' + | 'bottom' + | 'left' + | 'right' + | 'top-start' + | 'top-end' + | 'bottom-start' + | 'bottom-end' + | 'left-end' + | 'left-start' + | 'right-end' + | 'right-start'; + +interface TableToolbarFilterProps { + /** + * Specify how the popover should align with the trigger element + */ + align?: PopoverAlignment; + + /** + * Provide an optional class name for the toolbar filter + */ + className?: string; + + /** + * Provide an optional hook that is called each time the input is updated + */ + onChange?: ( + event: '' | ChangeEvent, + value?: string + ) => void; + + /** + * Provide an function that is called when the apply button is clicked + */ + onApplyFilter?: (selectedCheckboxes: Array) => void; + + /** + * Provide an function that is called when the reset button is clicked + */ + onResetFilter?: () => void; +} + +const TableToolbarFilter = ({ + align = 'bottom-end', + onApplyFilter, + onResetFilter, + className, + ...rest +}: TableToolbarFilterProps) => { + const [isOpen, setIsOpen] = useState(false); + const [selectedCheckboxes, setSelectedCheckboxes] = useState([]); + + + const prefix = usePrefix(); + + const toolbarActionClasses = cx( + className, + `${prefix}--toolbar-action ${prefix}--overflow-menu` + ); + + const handleApplyFilter = () => { + setIsOpen(false); + if (onApplyFilter) { + onApplyFilter(selectedCheckboxes); + } + }; + + const handleResetFilter = () => { + setIsOpen(false); + setSelectedCheckboxes([]) + if (onResetFilter) { + onResetFilter(); + } + }; + + const handleCheckboxChange = (e: ChangeEvent) => { + const checkboxId = e.target.id; + const isChecked = e.target.checked; + + const checkboxValue: HTMLSpanElement | null = document.querySelector( + `label[for="${checkboxId}"]` + ); + + if (isChecked && checkboxValue) { + setSelectedCheckboxes([...selectedCheckboxes, checkboxValue.innerText]); + } else { + setSelectedCheckboxes(selectedCheckboxes.filter(item => item !== checkboxValue?.innerText)); + } + } + + return ( + + + open={isOpen} + isTabTip + onRequestClose={() => setIsOpen(false)} + align={align} + {...rest}> + + +
+
+ + Filter options + + + + + +
+
+ + +
+ +
+ ); +}; + +TableToolbarFilter.propTypes = { + /** + * Specify how the popover should align with the trigger element + */ + align: PropTypes.string, + + /** + * Provide an optional class name for the search container + */ + className: PropTypes.string, + + /** + * Provide an function that is called when the apply button is clicked + */ + onApplyFilter: PropTypes.func, + + /** + * Provide an optional hook that is called each time the input is updated + */ + onChange: PropTypes.func, + + /** + * Provide an function that is called when the reset button is clicked + */ + onResetFilter: PropTypes.func, +}; + +export default TableToolbarFilter;