` component, except for `onSelect`, which is managed internally.
+It is also possible to disable row selection using the `isSelectDisabled` function passed to the wrapping data view component through `selection`.
+
### Tree table example
This example shows the tree table variant with expandable rows, custom icons for leaf and parent nodes. Tree table is turned on by passing `isTreeTable` flag to the `DataViewTable` component. You can pass `collapsedIcon`, `expandedIcon` or `leafIcon` to be displayen rows with given status. The tree table rows have to be defined in a format of object with following keys:
- `row` (`DataViewTd[]`) defining the content for each cell in the row.
- `id` (`string`) for the row (used to match items in selection end expand the rows).
- optional `children` (`DataViewTrTree[]`) defining the children rows.
-It is also possible to disable row selection using the `isSelectDisabled` function passed to the wrapping data view component.
+It is also possible to disable row selection using the `isSelectDisabled` function passed to the wrapping data view component through `selection`.
```js file="./DataViewTableTreeExample.tsx"
diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Components/DataViewTableExample.tsx b/packages/module/patternfly-docs/content/extensions/data-view/examples/Components/DataViewTableExample.tsx
index 8805ba9..b9cda39 100644
--- a/packages/module/patternfly-docs/content/extensions/data-view/examples/Components/DataViewTableExample.tsx
+++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Components/DataViewTableExample.tsx
@@ -54,7 +54,7 @@ const rows: DataViewTr[] = repositories.map(({ id, name, branches, prs, workspac
const columns: DataViewTh[] = [
null,
'Repositories',
- { cell: <>Branches> },
+ { cell: <>Branches> },
'Pull requests',
{ cell: 'Workspaces', props: { info: { tooltip: 'More information' } } },
{ cell: 'Last commit', props: { sort: { sortBy: {}, columnIndex: 4 } } },
diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/EventsContext/EventsContext.md b/packages/module/patternfly-docs/content/extensions/data-view/examples/EventsContext/EventsContext.md
index 2c0cfe4..293a1a0 100644
--- a/packages/module/patternfly-docs/content/extensions/data-view/examples/EventsContext/EventsContext.md
+++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/EventsContext/EventsContext.md
@@ -18,6 +18,7 @@ import { Table, Tbody, Th, Thead, Tr, Td } from '@patternfly/react-table';
import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
import { useDataViewEventsContext, DataViewEventsContext, DataViewEventsProvider, EventTypes } from '@patternfly/react-data-view/dist/dynamic/DataViewEventsContext';
+import { useDataViewSelection } from '@patternfly/react-data-view/dist/dynamic/Hooks';
import { Drawer, DrawerContent, DrawerContentBody } from '@patternfly/react-core';
The **data view events context** provides a way of listening to the data view events from the outside of the component.
diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/EventsContext/EventsExample.tsx b/packages/module/patternfly-docs/content/extensions/data-view/examples/EventsContext/EventsExample.tsx
index 19e09be..1447e00 100644
--- a/packages/module/patternfly-docs/content/extensions/data-view/examples/EventsContext/EventsExample.tsx
+++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/EventsContext/EventsExample.tsx
@@ -3,6 +3,8 @@ import { Drawer, DrawerActions, DrawerCloseButton, DrawerContent, DrawerContentB
import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
import { DataViewEventsProvider, EventTypes, useDataViewEventsContext } from '@patternfly/react-data-view/dist/dynamic/DataViewEventsContext';
+import { useDataViewSelection } from '@patternfly/react-data-view/dist/dynamic/Hooks';
+import { ActionsColumn } from '@patternfly/react-table';
interface Repository {
name: string;
@@ -45,7 +47,7 @@ const RepositoryDetail: React.FunctionComponent = ({ sele
return (
-
+
Detail of {selectedRepo?.name}
Branches: {selectedRepo?.branches}
@@ -64,25 +66,45 @@ interface RepositoriesTableProps {
selectedRepo?: Repository;
}
+const rowActions = [
+ {
+ title: 'Some action',
+ onClick: () => console.log('clicked on Some action') // eslint-disable-line no-console
+ },
+ {
+ title:
Another action
,
+ onClick: () => console.log('clicked on Another action') // eslint-disable-line no-console
+ },
+ {
+ isSeparator: true
+ },
+ {
+ title: 'Third action',
+ onClick: () => console.log('clicked on Third action') // eslint-disable-line no-console
+ }
+];
+
const RepositoriesTable: React.FunctionComponent = ({ selectedRepo = undefined }) => {
+ const selection = useDataViewSelection({ matchOption: (a, b) => a.row[0] === b.row[0] });
const { trigger } = useDataViewEventsContext();
const rows = useMemo(() => {
- const handleRowClick = (repo: Repository | undefined) => {
- trigger(EventTypes.rowClick, repo);
+ const handleRowClick = (event, repo: Repository | undefined) => {
+ // prevents drawer toggle on actions or checkbox click
+ (event.target.matches('td') || event.target.matches('tr')) && trigger(EventTypes.rowClick, repo);
};
return repositories.map(repo => ({
- row: Object.values(repo),
+ row: [ ...Object.values(repo), { cell: , props: { isActionCell: true } } ],
props: {
isClickable: true,
- onRowClick: () => handleRowClick(selectedRepo?.name === repo.name ? undefined : repo),
+ onRowClick: (event) => handleRowClick(event, selectedRepo?.name === repo.name ? undefined : repo),
isRowSelected: selectedRepo?.name === repo.name
}
}));
}, [ selectedRepo?.name, trigger ]);
return (
-
+
);
diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/FiltersExample.tsx b/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/FiltersExample.tsx
new file mode 100644
index 0000000..afdbcac
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/FiltersExample.tsx
@@ -0,0 +1,107 @@
+import React, { useMemo } from 'react';
+import { Pagination } from '@patternfly/react-core';
+import { BrowserRouter, useSearchParams } from 'react-router-dom';
+import { useDataViewFilters, useDataViewPagination } from '@patternfly/react-data-view/dist/dynamic/Hooks';
+import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
+import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
+import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar';
+import { DataViewFilterOption, DataViewFilters } from '@patternfly/react-data-view/dist/dynamic/DataViewFilters';
+import { DataViewTextFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTextFilter';
+import { DataViewCheckboxFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewCheckboxFilter';
+
+const perPageOptions = [
+ { title: '5', value: 5 },
+ { title: '10', value: 10 }
+];
+
+interface Repository {
+ name: string;
+ branch: string | null;
+ prs: string | null;
+ workspace: string;
+ lastCommit: string;
+}
+
+interface RepositoryFilters {
+ name: string,
+ branch: string,
+ workspace: string[]
+}
+
+const repositories: Repository[] = [
+ { name: 'Repository one', branch: 'Branch one', prs: 'Pull request one', workspace: 'Workspace one', lastCommit: 'Timestamp one' },
+ { name: 'Repository two', branch: 'Branch two', prs: 'Pull request two', workspace: 'Workspace two', lastCommit: 'Timestamp two' },
+ { name: 'Repository three', branch: 'Branch three', prs: 'Pull request three', workspace: 'Workspace one', lastCommit: 'Timestamp three' },
+ { name: 'Repository four', branch: 'Branch four', prs: 'Pull request four', workspace: 'Workspace one', lastCommit: 'Timestamp four' },
+ { name: 'Repository five', branch: 'Branch five', prs: 'Pull request five', workspace: 'Workspace two', lastCommit: 'Timestamp five' },
+ { name: 'Repository six', branch: 'Branch six', prs: 'Pull request six', workspace: 'Workspace three', lastCommit: 'Timestamp six' }
+];
+
+const filterOptions: DataViewFilterOption[] = [
+ { label: 'Workspace one', value: 'workspace-one' },
+ { label: 'Workspace two', value: 'workspace-two' },
+ { label: 'Workspace three', value: 'workspace-three' }
+];
+
+const columns = [ 'Name', 'Branch', 'Pull requests', 'Workspace', 'Last commit' ];
+
+const ouiaId = 'LayoutExample';
+
+const MyTable: React.FunctionComponent = () => {
+ const [ searchParams, setSearchParams ] = useSearchParams();
+ const { filters, onSetFilters, clearAllFilters } = useDataViewFilters({ initialFilters: { name: '', branch: '', workspace: [] }, searchParams, setSearchParams });
+ const pagination = useDataViewPagination({ perPage: 5 });
+ const { page, perPage } = pagination;
+
+ const filteredData = useMemo(() => repositories.filter(item =>
+ (!filters.name || item.name?.toLocaleLowerCase().includes(filters.name?.toLocaleLowerCase())) &&
+ (!filters.branch || item.branch?.toLocaleLowerCase().includes(filters.branch?.toLocaleLowerCase())) &&
+ (!filters.workspace || filters.workspace.length === 0 || filters.workspace.includes(String(filterOptions.find(option => option.label === item.workspace)?.value)))
+ ), [ filters ]);
+
+ const pageRows = useMemo(() => filteredData
+ .slice((page - 1) * perPage, ((page - 1) * perPage) + perPage)
+ .map(item => Object.values(item)),
+ [ page, perPage, filteredData ]);
+
+ return (
+
+
+ }
+ filters={
+ onSetFilters(values)} values={filters}>
+
+
+
+
+ }
+ />
+
+
+ }
+ />
+
+ );
+}
+
+export const BasicExample: React.FunctionComponent = () => (
+
+
+
+)
diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/Functionality.md b/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/Functionality.md
index c788309..3e263c5 100644
--- a/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/Functionality.md
+++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/Functionality.md
@@ -11,16 +11,19 @@ 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
sortValue: 3
+propComponents: ['DataViewFilters', 'DataViewTextFilter', 'DataViewCheckboxFilter']
sourceLink: https://github.com/patternfly/react-data-view/blob/main/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/Functionality.md
---
import { useMemo } from 'react';
import { BrowserRouter, useSearchParams } from 'react-router-dom';
-import { useDataViewPagination, useDataViewSelection } from '@patternfly/react-data-view/dist/dynamic/Hooks';
+import { useDataViewPagination, useDataViewSelection, useDataViewFilters, useDataViewSort } from '@patternfly/react-data-view/dist/dynamic/Hooks';
import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect';
import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar';
import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
-
+import { DataViewFilters } from '@patternfly/react-data-view/dist/dynamic/DataViewFilters';
+import { DataViewTextFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewTextFilter';
+import { DataViewCheckboxFilter } from '@patternfly/react-data-view/dist/dynamic/DataViewCheckboxFilter';
This is a list of functionality you can use to manage data displayed in the **data view**.
@@ -83,3 +86,65 @@ The `useDataViewSelection` hook manages the selection state of the data view.
```js file="./SelectionExample.tsx"
```
+
+# Filters
+Enables filtering of data records in the data view and displays the applied filter labels.
+
+### Toolbar usage
+The data view toolbar can include a set of filters by passing a React node to the `filters` property. You can use predefined components `DataViewFilters`, `DataViewTextFilter` and `DataViewCheckboxFilter` to customize and handle filtering directly in the toolbar. The `DataViewFilters` is a wrapper allowing conditional filtering using multiple attributes. If you need just a single filter, you can use `DataViewTextFilter`, `DataViewCheckboxFilter` or a different filter component alone. Props of these filter components are listed at the bottom of this page.
+
+You can decide between passing `value` and `onChange` event to every filter separately or pass `values` and `onChange` to the `DataViewFilters` wrapper which make them available to its children. Props directly passed to child filters have a higher priority than the "inherited" ones.
+
+### Filters state
+
+The `useDataViewFilters` hook manages the filter state of the data view. It allows you to define default filter values, synchronize filter state with URL parameters, and handle filter changes efficiently.
+
+**Initial values:**
+- `initialFilters` object with default filter values (if the filter param allows multiple values, pass an array)
+- optional `searchParams` object for managing URL-based filter state
+- optional `setSearchParams` function to update the URL when filters are modified
+
+The `useDataViewFilters` hook works well with the React Router library to support URL-based filtering. Alternatively, you can manage filter state in the URL using `URLSearchParams` and `window.history.pushState` APIs, or other routing libraries. If no URL parameters are provided, the filter state is managed internally.
+
+**Return values:**
+- `filters` object representing the current filter values
+- `onSetFilters` function to update the filter state
+- `clearAllFilters` function to reset all filters to their initial values
+
+### Filtering example
+This example demonstrates the setup and usage of filters within the data view. It includes text filters for different attributes, the ability to clear all filters, and persistence of filter state in the URL.
+
+```js file="./FiltersExample.tsx"
+
+```
+
+### Sort state
+
+The `useDataViewSort` hook manages the sorting state of a data view. It provides an easy way to handle sorting logic, including synchronization with URL parameters and defining default sorting behavior.
+
+**Initial values:**
+- `initialSort` object to set default `sortBy` and `direction` values:
+ - `sortBy`: key of the initial column to sort.
+ - `direction`: default sorting direction (`asc` or `desc`).
+- Optional `searchParams` object to manage URL-based synchronization of sort state.
+- Optional `setSearchParams` function to update the URL parameters when sorting changes.
+- `defaultDirection` to set the default direction when no direction is specified.
+- Customizable parameter names for the URL:
+ - `sortByParam`: name of the URL parameter for the column key.
+ - `directionParam`: name of the URL parameter for the sorting direction.
+
+The `useDataViewSort` hook integrates seamlessly with React Router to manage sort state via URL parameters. Alternatively, you can use `URLSearchParams` and `window.history.pushState` APIs, or other routing libraries. If URL synchronization is not configured, the sort state is managed internally within the component.
+
+**Return values:**
+- `sortBy`: key of the column currently being sorted.
+- `direction`: current sorting direction (`asc` or `desc`).
+- `onSort`: function to handle sorting changes programmatically or via user interaction.
+
+### Sorting example
+
+This example demonstrates how to set up and use sorting functionality within a data view. The implementation includes dynamic sorting by column with persistence of sort state in the URL using React Router.
+
+
+```js file="./SortingExample.tsx"
+
+```
diff --git a/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/SortingExample.tsx b/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/SortingExample.tsx
new file mode 100644
index 0000000..879f1cd
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/data-view/examples/Functionality/SortingExample.tsx
@@ -0,0 +1,87 @@
+/* eslint-disable no-nested-ternary */
+import React, { useMemo } from 'react';
+import { useDataViewSort } from '@patternfly/react-data-view/dist/dynamic/Hooks';
+import { DataViewTable, DataViewTr, DataViewTh } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
+import { ThProps } from '@patternfly/react-table';
+import { BrowserRouter, useSearchParams } from 'react-router-dom';
+
+interface Repository {
+ name: string;
+ branches: string;
+ prs: string;
+ workspaces: string;
+ lastCommit: string;
+};
+
+const COLUMNS = [
+ { label: 'Repository', key: 'name', index: 0 },
+ { label: 'Branch', key: 'branches', index: 1 },
+ { label: 'Pull request', key: 'prs', index: 2 },
+ { label: 'Workspace', key: 'workspaces', index: 3 },
+ { label: 'Last commit', key: 'lastCommit', index: 4 }
+];
+
+const repositories: Repository[] = [
+ { name: 'Repository one', branches: 'Branch one', prs: 'Pull request one', workspaces: 'Workspace one', lastCommit: 'Timestamp one' },
+ { name: 'Repository two', branches: 'Branch two', prs: 'Pull request two', workspaces: 'Workspace two', lastCommit: 'Timestamp two' },
+ { name: 'Repository three', branches: 'Branch three', prs: 'Pull request three', workspaces: 'Workspace three', lastCommit: 'Timestamp three' },
+ { name: 'Repository four', branches: 'Branch four', prs: 'Pull request four', workspaces: 'Workspace four', lastCommit: 'Timestamp four' },
+ { name: 'Repository five', branches: 'Branch five', prs: 'Pull request five', workspaces: 'Workspace five', lastCommit: 'Timestamp five' },
+ { name: 'Repository six', branches: 'Branch six', prs: 'Pull request six', workspaces: 'Workspace six', lastCommit: 'Timestamp six' }
+];
+
+const sortData = (data: Repository[], sortBy: string | undefined, direction: 'asc' | 'desc' | undefined) =>
+ sortBy && direction
+ ? [ ...data ].sort((a, b) =>
+ direction === 'asc'
+ ? a[sortBy] < b[sortBy] ? -1 : a[sortBy] > b[sortBy] ? 1 : 0
+ : a[sortBy] > b[sortBy] ? -1 : a[sortBy] < b[sortBy] ? 1 : 0
+ )
+ : data;
+
+const ouiaId = 'TableExample';
+
+export const MyTable: React.FunctionComponent = () => {
+ const [ searchParams, setSearchParams ] = useSearchParams();
+ const { sortBy, direction, onSort } = useDataViewSort({ searchParams, setSearchParams });
+ const sortByIndex = useMemo(() => COLUMNS.findIndex(item => item.key === sortBy), [ sortBy ]);
+
+ const getSortParams = (columnIndex: number): ThProps['sort'] => ({
+ sortBy: {
+ index: sortByIndex,
+ direction,
+ defaultDirection: 'asc'
+ },
+ onSort: (_event, index, direction) => onSort(_event, COLUMNS[index].key, direction),
+ columnIndex
+ });
+
+ const columns: DataViewTh[] = COLUMNS.map((column, index) => ({
+ cell: column.label,
+ props: { sort: getSortParams(index) }
+ }));
+
+ const rows: DataViewTr[] = useMemo(() => sortData(repositories, sortBy, direction).map(({ name, branches, prs, workspaces, lastCommit }) => [
+ name,
+ branches,
+ prs,
+ workspaces,
+ lastCommit,
+ ]), [ sortBy, direction ]);
+
+ return (
+
+ );
+};
+
+export const BasicExample: React.FunctionComponent = () => (
+
+
+
+)
+
diff --git a/packages/module/patternfly-docs/generated/index.js b/packages/module/patternfly-docs/generated/index.js
index 851e24a..278a0b3 100644
--- a/packages/module/patternfly-docs/generated/index.js
+++ b/packages/module/patternfly-docs/generated/index.js
@@ -49,8 +49,8 @@ module.exports = {
'/extensions/data-view/components/react': {
id: "Components",
title: "Components",
- toc: [{"text":"Data view toolbar"},[{"text":"Basic toolbar example"}],{"text":"Data view table"},[{"text":"Rows and columns customization"},{"text":"Tree table example"},{"text":"Empty state example"}]],
- examples: ["Basic toolbar example","Rows and columns customization","Tree table example","Empty state example"],
+ toc: [{"text":"Data view toolbar"},[{"text":"Basic toolbar example"},{"text":"Actions configuration"},{"text":"Actions example"}],{"text":"Data view table"},[{"text":"Rows and columns customization"},{"text":"Tree table example"},{"text":"Empty state example"},{"text":"Error state example"},{"text":"Loading state example"}]],
+ examples: ["Basic toolbar example","Actions example","Rows and columns customization","Tree table example","Empty state example","Error state example","Loading state example"],
section: "extensions",
subsection: "Data view",
source: "react",
diff --git a/packages/module/src/DataView/DataView.tsx b/packages/module/src/DataView/DataView.tsx
index 094d594..201a586 100644
--- a/packages/module/src/DataView/DataView.tsx
+++ b/packages/module/src/DataView/DataView.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import { Stack, StackItem } from '@patternfly/react-core';
+import { Stack, StackItem, StackProps } from '@patternfly/react-core';
import { DataViewSelection, InternalContextProvider } from '../InternalContext';
export const DataViewState = {
@@ -10,7 +10,8 @@ export const DataViewState = {
export type DataViewState = typeof DataViewState[keyof typeof DataViewState];
-export interface DataViewProps {
+/** extends StackProps */
+export interface DataViewProps extends StackProps {
/** Content rendered inside the data view */
children: React.ReactNode;
/** Custom OUIA ID */
diff --git a/packages/module/src/DataViewCheckboxFilter/DataViewCheckboxFilter.test.tsx b/packages/module/src/DataViewCheckboxFilter/DataViewCheckboxFilter.test.tsx
new file mode 100644
index 0000000..2290651
--- /dev/null
+++ b/packages/module/src/DataViewCheckboxFilter/DataViewCheckboxFilter.test.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import DataViewCheckboxFilter, { DataViewCheckboxFilterProps } from './DataViewCheckboxFilter';
+import DataViewToolbar from '../DataViewToolbar';
+
+describe('DataViewCheckboxFilter component', () => {
+ const defaultProps: DataViewCheckboxFilterProps = {
+ filterId: 'test-checkbox-filter',
+ title: 'Test Checkbox Filter',
+ value: [ 'workspace-one' ],
+ options: [
+ { label: 'Workspace one', value: 'workspace-one' },
+ { label: 'Workspace two', value: 'workspace-two' },
+ { label: 'Workspace three', value: 'workspace-three' },
+ ],
+ };
+
+ it('should render correctly', () => {
+ const { container } = render(
+ } />
+ );
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/packages/module/src/DataViewCheckboxFilter/DataViewCheckboxFilter.tsx b/packages/module/src/DataViewCheckboxFilter/DataViewCheckboxFilter.tsx
new file mode 100644
index 0000000..e52ff7e
--- /dev/null
+++ b/packages/module/src/DataViewCheckboxFilter/DataViewCheckboxFilter.tsx
@@ -0,0 +1,175 @@
+import React from 'react';
+import {
+ Badge,
+ Menu,
+ MenuContent,
+ MenuItem,
+ MenuList,
+ MenuProps,
+ MenuToggle,
+ Popper,
+ ToolbarLabel,
+ ToolbarFilter,
+} from '@patternfly/react-core';
+import { FilterIcon } from '@patternfly/react-icons';
+import { DataViewFilterOption } from '../DataViewFilters';
+
+const isToolbarLabel = (label: string | ToolbarLabel): label is ToolbarLabel =>
+ typeof label === 'object' && 'key' in label;
+
+export const isDataViewFilterOption = (obj: unknown): obj is DataViewFilterOption =>
+ !!obj &&
+ typeof obj === 'object' &&
+ 'label' in obj &&
+ 'value' in obj &&
+ typeof (obj as DataViewFilterOption).value === 'string';
+
+/** extends MenuProps */
+export interface DataViewCheckboxFilterProps extends Omit {
+ /** Unique key for the filter attribute */
+ filterId: string;
+ /** Array of current filter values */
+ value?: string[];
+ /** Filter title displayed in the toolbar */
+ title: string;
+ /** Placeholder text of the menu */
+ placeholder?: string;
+ /** Filter options displayed */
+ options: (DataViewFilterOption | string)[];
+ /** Callback for updating when item selection changes. */
+ onChange?: (event?: React.MouseEvent, values?: string[]) => void;
+ /** Controls visibility of the filter in the toolbar */
+ showToolbarItem?: boolean;
+ /** Controls visibility of the filter icon */
+ showIcon?: boolean;
+ /** Controls visibility of the selected items badge */
+ showBadge?: boolean;
+ /** Custom OUIA ID */
+ ouiaId?: string;
+}
+
+export const DataViewCheckboxFilter: React.FC = ({
+ filterId,
+ title,
+ value = [],
+ onChange,
+ placeholder,
+ options = [],
+ showToolbarItem,
+ showIcon = !placeholder,
+ showBadge = !placeholder,
+ ouiaId = 'DataViewCheckboxFilter',
+ ...props
+}: DataViewCheckboxFilterProps) => {
+ const [ isOpen, setIsOpen ] = React.useState(false);
+ const toggleRef = React.useRef(null);
+ const menuRef = React.useRef(null);
+ const containerRef = React.useRef(null);
+
+ const normalizeOptions = React.useMemo(
+ () =>
+ options.map(option =>
+ typeof option === 'string'
+ ? { label: option, value: option }
+ : option
+ ),
+ [ options ]
+ );
+
+ const handleToggleClick = (event: React.MouseEvent) => {
+ event.stopPropagation();
+ setTimeout(() => {
+ const firstElement = menuRef.current?.querySelector('li > button:not(:disabled)') as HTMLElement;
+ firstElement?.focus();
+ }, 0);
+ setIsOpen(prev => !prev);
+ };
+
+ const handleSelect = (event?: React.MouseEvent, itemId?: string | number) => {
+ const activeItem = String(itemId);
+ const isSelected = value.includes(activeItem);
+
+ onChange?.(
+ event,
+ isSelected ? value.filter(item => item !== activeItem) : [ activeItem, ...value ]
+ );
+ };
+
+ const handleClickOutside = (event: MouseEvent) =>
+ isOpen &&
+ menuRef.current && toggleRef.current &&
+ !menuRef.current.contains(event.target as Node) && !toggleRef.current.contains(event.target as Node)
+ && setIsOpen(false);
+
+
+ React.useEffect(() => {
+ window.addEventListener('click', handleClickOutside);
+ return () => {
+ window.removeEventListener('click', handleClickOutside);
+ };
+ }, [ isOpen ]); // eslint-disable-line react-hooks/exhaustive-deps
+
+ return (
+ {
+ const activeOption = normalizeOptions.find(option => option.value === item);
+ return ({ key: activeOption?.value as string, node: activeOption?.label })
+ })}
+ deleteLabel={(_, label) =>
+ onChange?.(undefined, value.filter(item => item !== (isToolbarLabel(label) ? label.key : label)))
+ }
+ categoryName={title}
+ showToolbarItem={showToolbarItem}
+ >
+ : undefined}
+ badge={value.length > 0 && showBadge ? {value.length} : undefined}
+ style={{ width: '200px' }}
+ >
+ {placeholder ?? title}
+
+ }
+ triggerRef={toggleRef}
+ popper={
+
+ }
+ popperRef={menuRef}
+ appendTo={containerRef.current || undefined}
+ aria-label={`${title ?? filterId} filter`}
+ isVisible={isOpen}
+ />
+
+ );
+};
+
+export default DataViewCheckboxFilter;
diff --git a/packages/module/src/DataViewCheckboxFilter/__snapshots__/DataViewCheckboxFilter.test.tsx.snap b/packages/module/src/DataViewCheckboxFilter/__snapshots__/DataViewCheckboxFilter.test.tsx.snap
new file mode 100644
index 0000000..1087ae1
--- /dev/null
+++ b/packages/module/src/DataViewCheckboxFilter/__snapshots__/DataViewCheckboxFilter.test.tsx.snap
@@ -0,0 +1,197 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DataViewCheckboxFilter component should render correctly 1`] = `
+