diff --git a/react/filterPanel/src/Example/Example.tsx b/react/filterPanel/src/Example/Example.tsx index 3ca4a32..a24dc57 100644 --- a/react/filterPanel/src/Example/Example.tsx +++ b/react/filterPanel/src/Example/Example.tsx @@ -1,5 +1,6 @@ import { Grid, Column } from '@carbon/react'; import { FilterPanel } from './FilterPanel'; +import { WithFilterableMultiSelect } from './WithFilterableMultiSelect'; import './example.scss'; export const Example = () => ( @@ -7,5 +8,9 @@ export const Example = () => ( + + + ); + diff --git a/react/filterPanel/src/Example/FilterPanel.tsx b/react/filterPanel/src/Example/FilterPanel.tsx index ef57210..8c4b4ba 100644 --- a/react/filterPanel/src/Example/FilterPanel.tsx +++ b/react/filterPanel/src/Example/FilterPanel.tsx @@ -75,6 +75,9 @@ const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { return itemRank.passed; }; +// a unique table id +const tableId = 'table-' + Math.random().toString(36).substring(2, 15); + const columnHelper = createColumnHelper(); const columns = [ @@ -245,7 +248,7 @@ export const FilterPanel = () => { const animatePanel = () => { if (popoverOpen) { animate( - '.panel--container', + `#${tableId} .panel--container`, { opacity: [1, 0], transform: [`translateX(0px)`, `translateX(-320px)`], @@ -256,7 +259,7 @@ export const FilterPanel = () => { ); // .cds--data-table-content animate( - '.cds--data-table-content', + `#${tableId} .cds--data-table-content`, { width: '100%', transform: 'translateX(0px)', @@ -267,7 +270,7 @@ export const FilterPanel = () => { ); } else { animate( - '.panel--container', + `#${tableId} .panel--container`, { opacity: [0, 1], transform: [`translateX(-320px)`, `translateX(0px)`], @@ -277,7 +280,7 @@ export const FilterPanel = () => { } ); animate( - '.filter-panel-example .cds--data-table-content', + `#${tableId} .cds--data-table-content`, { width: 'calc(100% - 336px)', transform: 'translateX(336px)', @@ -292,6 +295,7 @@ export const FilterPanel = () => { return ( ); }; + diff --git a/react/filterPanel/src/Example/WithFilterableMultiSelect.tsx b/react/filterPanel/src/Example/WithFilterableMultiSelect.tsx new file mode 100644 index 0000000..06799bd --- /dev/null +++ b/react/filterPanel/src/Example/WithFilterableMultiSelect.tsx @@ -0,0 +1,609 @@ +import React, { + Dispatch, + SetStateAction, + useState, + useRef, + useEffect, +} from 'react'; +import { animate } from 'motion'; +import cx from 'classnames'; +import { + DataTable, + IconButton, + Layer, + TextInput, + Dropdown, + ButtonSet, + Button, + NumberInput, + FilterableMultiSelect, +} from '@carbon/react'; +import { Close, Filter } from '@carbon/react/icons'; +const { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableHeader, + TableRow, + TableToolbar, + TableToolbarContent, + TableToolbarSearch, +} = DataTable; + +import { + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, + FilterFn, + getFilteredRowModel, + ColumnFiltersState, + Column, + Header, + getFacetedUniqueValues, + ColumnFilter, +} from '@tanstack/react-table'; +import { rankItem } from '@tanstack/match-sorter-utils'; + +import { makeData } from './makeData'; +import { TagOverflow, pkg } from '@carbon/ibm-products'; + +pkg.component.TagOverflow = true; + +type Resource = { + id: string; + name: string; + rule: string; + status: string; + other: string; + example: string; +}; + +// Define a custom fuzzy filter function that will apply ranking info to rows (using match-sorter utils) +const fuzzyFilter: FilterFn = (row, columnId, value, addMeta) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value); + + // Store the itemRank info + addMeta({ + itemRank, + }); + + // Return if the item should be filtered in/out + return itemRank.passed; +}; + +// a unique table id +const tableId = 'table-' + Math.random().toString(36).substring(2, 15); + +const columnHelper = createColumnHelper(); + +const columns = [ + columnHelper.accessor((row) => row.name, { + id: 'name', + cell: (info) => {info.getValue()}, + header: () => Name, + }), + columnHelper.accessor('rule', { + header: () => 'Rule', + cell: (info) => info.renderValue(), + filterFn: 'arrIncludesSome', + meta: { + filterVariant: 'checkbox', + }, + }), + columnHelper.accessor('status', { + header: () => Status, + meta: { + filterVariant: 'select', + }, + }), + columnHelper.accessor('other', { + header: 'Other', + }), + columnHelper.accessor('example', { + header: 'Example', + filterFn: 'weakEquals', + meta: { + filterVariant: 'number', + }, + }), +]; + +export const WithFilterableMultiSelect = () => { + const [data] = useState(makeData(7)); + const [globalFilter, setGlobalFilter] = React.useState(''); + const [popoverOpen, setPopoverOpen] = useState(false); + const [columnFilters, setColumnFilters] = useState([]); + const [localFilters, setLocalFilters] = useState([]); + + const table = useReactTable({ + data, + columns, + filterFns: { + fuzzy: fuzzyFilter, //define as a filter function that can be used in column definitions + }, + state: { + globalFilter, + columnFilters, + }, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), //client side filtering + onGlobalFilterChange: setGlobalFilter, + globalFilterFn: 'fuzzy', //apply fuzzy filter to the global filter (most common use case for fuzzy filter) + getFacetedUniqueValues: getFacetedUniqueValues(), + }); + + interface ExtendedColFilter extends ColumnFilter { + label: string; + onClose: () => void; + filter: boolean; + } + + const buildTagFilters = () => { + const tagFilters = columnFilters.map((c: ExtendedColFilter) => { + const buildTag = (col, checkboxParentColumnData?: ColumnFilter) => { + const tagData = {} as ExtendedColFilter; + tagData.label = + typeof col === 'string' + ? `${checkboxParentColumnData.id}: ${col}` + : `${col.id}: ${col.value}`; + tagData.onClose = () => { + if (typeof col === 'string') { + const groupValues = checkboxParentColumnData.value as string[]; + const newGroupValues = groupValues.filter((val) => val !== col); + const foundLocalIndex = localFilters.findIndex( + (f) => f.id === checkboxParentColumnData.id + ); + const foundColumnIndex = columnFilters.findIndex( + (f) => f.id === checkboxParentColumnData.id + ); + const tempLocal = [...localFilters]; + const tempColumnFilters = [...columnFilters]; + if (foundLocalIndex > -1) { + tempLocal.splice(foundLocalIndex, 1); + if (!newGroupValues.length) { + setLocalFilters(tempLocal); + } else { + setLocalFilters([ + ...tempLocal, + { id: checkboxParentColumnData.id, value: newGroupValues }, + ]); + } + } + if (foundColumnIndex > -1) { + tempColumnFilters.splice(foundColumnIndex, 1); + if (!newGroupValues.length) { + setColumnFilters(tempColumnFilters); + } else { + setColumnFilters([ + ...tempColumnFilters, + { id: checkboxParentColumnData.id, value: newGroupValues }, + ]); + } + } + return; + } + const parentData = + typeof col === 'string' ? checkboxParentColumnData : col; + const foundLocalIndex = localFilters.findIndex( + (f) => f.id === parentData.id && f.value === parentData.value + ); + const foundColumnIndex = columnFilters.findIndex( + (f) => f.id === parentData.id && f.value === parentData.value + ); + const tempFilters = [...localFilters]; + const tempColumnFilters = [...columnFilters]; + if (foundColumnIndex > -1) { + tempColumnFilters.splice(foundColumnIndex, 1); + setColumnFilters(tempColumnFilters); + } + if (foundLocalIndex > -1) { + tempFilters.splice(foundLocalIndex, 1); + setLocalFilters(tempFilters); + } + const tableFullColumn = table.getColumn(parentData.id); + tableFullColumn.setFilterValue(undefined); + }; + tagData.filter = true; + tagData.id = typeof col === 'string' ? col : col.id; + return tagData; + }; + if (Array.isArray(c.value)) { + return c.value.map((val) => buildTag(val, c)); + } + return buildTag(c); + }); + return tagFilters.flat(); + }; + + const returnFocusToFlyoutTrigger = () => { + if (popoverRef?.current) { + const triggerButton = popoverRef?.current.querySelector('button'); + triggerButton?.focus(); + } + }; + + const containerRef = useRef(); + const popoverRef = useRef(); + + useEffect(() => { + const tableContainer = containerRef?.current; + if (tableContainer as HTMLDivElement) { + const tableContent = (tableContainer as HTMLDivElement)?.querySelector( + '.cds--data-table-content' + ); + if (tableContent) { + (tableContainer as HTMLDivElement)?.style.setProperty( + '--table-height', + `${(tableContent as HTMLDivElement)?.clientHeight}px` + ); + } + } + }, []); + + const animatePanel = () => { + if (popoverOpen) { + animate( + `#${tableId} .panel--container`, + { + opacity: [1, 0], + transform: [`translateX(0px)`, `translateX(-320px)`], + }, + { + duration: 0.25, + } + ); + // .cds--data-table-content + animate( + `#${tableId} .cds--data-table-content`, + { + width: '100%', + transform: 'translateX(0px)', + }, + { + duration: 0.25, + } + ); + } else { + animate( + `#${tableId} .panel--container`, + { + opacity: [0, 1], + transform: [`translateX(-320px)`, `translateX(0px)`], + }, + { + duration: 0.25, + } + ); + animate( + `#${tableId} .cds--data-table-content`, + { + width: 'calc(100% - 336px)', + transform: 'translateX(336px)', + }, + { + duration: 0.25, + } + ); + } + }; + + return ( + + + + + { + setPopoverOpen((prev) => !prev); + animatePanel(); + }} + label="Filter" + kind="ghost" + className={cx({ + [`filter--panel__triggering-icon-open`]: popoverOpen, + })}> + + + ) => + setGlobalFilter(event.target.value) + } + placeholder="Search all columns..." + persistent + /> + + + {buildTagFilters().length ? ( + + + { + setLocalFilters([]); + table.resetColumnFilters(); + }}> + Clear filters + + + ) : null} + + {popoverOpen && ( + <> + + + { + setPopoverOpen(false); + animatePanel(); + }}> + + + Filter + + + + {table.getHeaderGroups().map((headerGroup, index) => ( + + {headerGroup.headers.map((header, index) => { + if (header.column.getCanFilter()) { + return ( + + {popoverOpen && ( + + )} + + ); + } + })} + + ))} + + + + { + table.resetColumnFilters(); + setPopoverOpen(false); + returnFocusToFlyoutTrigger(); + setLocalFilters([]); + animatePanel(); + }}> + Clear + + { + setColumnFilters(localFilters); + setPopoverOpen(false); + returnFocusToFlyoutTrigger(); + animatePanel(); + }}> + Filter + + + + > + )} + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + + + + + ); +}; + +const FilterColumn = ({ + column, + header, + setLocalFilters, + localFilters, +}: { + column: Column; + header: Header; + setLocalFilters: Dispatch>; + localFilters: ColumnFiltersState; +}) => { + const { filterVariant } = column.columnDef.meta ?? {}; + + const sortedUniqueValues = React.useMemo( + () => + Array.from(column.getFacetedUniqueValues().keys()).sort().slice(0, 5000), + [column] + ); + + return filterVariant === 'select' ? ( + + c.id === column.id)?.value as string + } + renderSelectedItem={() => ( + + {localFilters.find((c) => c.id === column.id)?.value as string} + + )} + itemToElement={(item: { value: any }) => ( + {item.value} + )} + onChange={({ selectedItem }) => { + const temp = [...localFilters]; + const foundIndex = temp.findIndex((c) => c.id === column.id); + if (foundIndex > -1) { + temp.splice(foundIndex, 1); + temp.push({ id: column.id, value: selectedItem }); + setLocalFilters(temp); + return; + } + setLocalFilters([...temp, { id: column.id, value: selectedItem }]); + }} + /> + + ) : filterVariant === 'checkbox' ? ( + + (item ? item : '')} + items={sortedUniqueValues} + initialSelectedItems={ + localFilters.find((c) => c.id === column.id)?.value as string[] + } + onChange={(selected) => { + const selectedItem = selected.selectedItems; + const temp = [...localFilters]; + const foundIndex = temp.findIndex((c) => c.id === column.id); + if (foundIndex > -1) { + if (selectedItem.length === 0) { + temp.splice(foundIndex, 1); + } else { + temp[foundIndex] = { id: column.id, value: selectedItem }; + } + } else { + temp.push({ id: column.id, value: selectedItem }); + } + setLocalFilters(temp); + }} + selectionFeedback="top-after-reopen" + titleText={`Filter ${column.id}`} + placeholder="Choose a rule" + filterItems={(items, { inputValue }) => + items.filter((item) => + item.toLowerCase().includes(inputValue.toLowerCase()) + ) + } + /> + + ) : filterVariant === 'number' ? ( + + c.id === column.id)?.value as number} + hideSteppers + label={column.id} + onChange={(_, { value }) => { + const temp = [...localFilters]; + const foundLocalFilter = temp.filter((f) => f.id === column.id); + const foundFilterIndex = foundLocalFilter.length + ? temp.findIndex((f) => f.id === foundLocalFilter[0].id) + : -1; + if (foundFilterIndex > -1) { + temp.splice(foundFilterIndex, 1); + temp.push({ id: column.id, value }); + setLocalFilters(temp); + return; + } else { + setLocalFilters([...temp, { id: column.id, value }]); + return; + } + }} + /> + + ) : ( + + { + // column.setFilterValue(event.target.value) // instant filter option + const temp = [...localFilters]; + const foundLocalFilter = temp.filter((f) => f.id === column.id); + const foundFilterIndex = foundLocalFilter.length + ? temp.findIndex((f) => f.id === foundLocalFilter[0].id) + : -1; + if (foundFilterIndex > -1) { + temp.splice(foundFilterIndex, 1); + temp.push({ id: column.id, value: event.target.value }); + setLocalFilters(temp); + return; + } else { + setLocalFilters([ + ...temp, + { id: column.id, value: event.target.value }, + ]); + return; + } + }} + placeholder={`Filter ${column.id}`} + type="text" + value={ + (localFilters.find((c) => c.id === column.id)?.value as string) ?? '' + } + labelText={flexRender( + header.column.columnDef.header, + header.getContext() + )} + id={column.id} + /> + + ); +}; +
Filter