diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 3ec11f61de..7adcfbd802 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -20,7 +20,6 @@ import { RowSelectionChangeProvider } from './hooks'; import HeaderRow from './HeaderRow'; -import FilterRow from './FilterRow'; import Row from './Row'; import GroupRowRenderer from './GroupRow'; import SummaryRow from './SummaryRow'; @@ -40,7 +39,6 @@ import { import type { CalculatedColumn, Column, - Filters, Position, RowRendererProps, RowsChangeData, @@ -116,8 +114,6 @@ export interface DataGridProps extends Sha rowHeight?: number | ((args: RowHeightArgs) => number) | null; /** The height of the header row in pixels */ headerRowHeight?: number | null; - /** The height of the header filter row in pixels */ - headerFiltersHeight?: number | null; /** The height of each summary row in pixels */ summaryRowHeight?: number | null; @@ -134,8 +130,6 @@ export interface DataGridProps extends Sha sortDirection?: SortDirection | null; /** Function called whenever grid is sorted*/ onSort?: ((columnKey: string, direction: SortDirection) => void) | null; - filters?: Readonly | null; - onFiltersChange?: ((filters: Filters) => void) | null; defaultColumnOptions?: DefaultColumnOptions | null; groupBy?: readonly string[] | null; rowGrouper?: ((rows: readonly R[], columnKey: string) => Record) | null; @@ -165,8 +159,6 @@ export interface DataGridProps extends Sha /** * Toggles and modes */ - /** Toggles whether filters row is displayed or not */ - enableFilterRow?: boolean | null; cellNavigationMode?: CellNavigationMode | null; enableVirtualization?: boolean | null; @@ -195,8 +187,7 @@ function DataGrid( onRowsChange, // Dimensions props rowHeight, - headerRowHeight, - headerFiltersHeight, + headerRowHeight: rawHeaderRowHeight, summaryRowHeight: rawSummaryRowHeight, // Feature props selectedRows, @@ -204,8 +195,6 @@ function DataGrid( sortColumn, sortDirection, onSort, - filters, - onFiltersChange, defaultColumnOptions, groupBy: rawGroupBy, rowGrouper, @@ -222,7 +211,6 @@ function DataGrid( onFill, onPaste, // Toggles and modes - enableFilterRow, cellNavigationMode: rawCellNavigationMode, enableVirtualization, // Miscellaneous @@ -241,11 +229,9 @@ function DataGrid( * defaults */ rowHeight ??= 35; - headerRowHeight ??= typeof rowHeight === 'number' ? rowHeight : 35; - headerFiltersHeight ??= 45; + const headerRowHeight = rawHeaderRowHeight ?? (typeof rowHeight === 'number' ? rowHeight : 35); const summaryRowHeight = rawSummaryRowHeight ?? (typeof rowHeight === 'number' ? rowHeight : 35); const RowRenderer = rowRenderer ?? Row; - enableFilterRow ??= false; const cellNavigationMode = rawCellNavigationMode ?? 'NONE'; enableVirtualization ??= true; const editorPortalTarget = rawEditorPortalTarget ?? body; @@ -283,10 +269,9 @@ function DataGrid( * computed values */ const [gridRef, gridWidth, gridHeight] = useGridDimensions(); - const headerRowsCount = enableFilterRow ? 2 : 1; + const headerRowsCount = 1; const summaryRowsCount = summaryRows?.length ?? 0; - const totalHeaderHeight = headerRowHeight + (enableFilterRow ? headerFiltersHeight : 0); - const clientHeight = gridHeight - totalHeaderHeight - summaryRowsCount * summaryRowHeight; + const clientHeight = gridHeight - headerRowHeight - summaryRowsCount * summaryRowHeight; const isSelectable = selectedRows != null && onSelectedRowsChange != null; const allRowsSelected = useMemo((): boolean => { @@ -353,7 +338,6 @@ function DataGrid( rowOverscanEndIdx, rows, summaryRows, - enableFilterRow, isGroupRow }); @@ -965,7 +949,7 @@ function DataGrid( let startRowIndex = 0; for (let rowIdx = rowOverscanStartIdx; rowIdx <= rowOverscanEndIdx; rowIdx++) { const row = rows[rowIdx]; - const top = getRowTop(rowIdx) + totalHeaderHeight; + const top = getRowTop(rowIdx) + headerRowHeight; if (isGroupRow(row)) { ({ startRowIndex } = row); const isGroupRowSelected = @@ -1067,7 +1051,6 @@ function DataGrid( { ...style, '--header-row-height': `${headerRowHeight}px`, - '--filter-row-height': `${headerFiltersHeight}px`, '--row-width': `${totalColumnWidth}px`, '--summary-row-height': `${summaryRowHeight}px`, ...layoutCssVars @@ -1088,13 +1071,6 @@ function DataGrid( onSort={onSort} lastFrozenColumnIndex={lastFrozenColumnIndex} /> - {enableFilterRow && ( - - columns={viewportColumns} - filters={filters} - onFiltersChange={onFiltersChange} - /> - )} {rows.length === 0 && EmptyRowsRenderer ? ( ) : ( diff --git a/src/FilterRow.tsx b/src/FilterRow.tsx deleted file mode 100644 index 2510cc62eb..0000000000 --- a/src/FilterRow.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { memo } from 'react'; - -import { getCellStyle, getCellClassname } from './utils'; -import type { CalculatedColumn, Filters } from './types'; -import type { DataGridProps } from './DataGrid'; -import { filterRowClassname } from './style'; - -type SharedDataGridProps = Pick, 'filters' | 'onFiltersChange'>; - -interface FilterRowProps extends SharedDataGridProps { - columns: readonly CalculatedColumn[]; -} - -function FilterRow({ columns, filters, onFiltersChange }: FilterRowProps) { - function onChange(key: string, value: unknown) { - const newFilters: Filters = { ...filters }; - newFilters[key] = value; - onFiltersChange?.(newFilters); - } - - return ( -
- {columns.map((column) => { - const { key } = column; - - return ( -
- {column.filterRenderer && ( - onChange(key, value)} - /> - )} -
- ); - })} -
- ); -} - -export default memo(FilterRow) as (props: FilterRowProps) => JSX.Element; diff --git a/src/hooks/useViewportColumns.ts b/src/hooks/useViewportColumns.ts index 51da6316a9..da73ef83e8 100644 --- a/src/hooks/useViewportColumns.ts +++ b/src/hooks/useViewportColumns.ts @@ -13,7 +13,6 @@ interface ViewportColumnsArgs { lastFrozenColumnIndex: number; rowOverscanStartIdx: number; rowOverscanEndIdx: number; - enableFilterRow: boolean; isGroupRow: (row: R | GroupRow) => row is GroupRow; } @@ -27,7 +26,6 @@ export function useViewportColumns({ lastFrozenColumnIndex, rowOverscanStartIdx, rowOverscanEndIdx, - enableFilterRow, isGroupRow }: ViewportColumnsArgs) { // find the column that spans over a column within the visible columns range and adjust colOverscanStartIdx @@ -52,14 +50,6 @@ export function useViewportColumns({ break; } - // check filter row - if ( - enableFilterRow && - updateStartIdx(colIdx, getColSpan(column, lastFrozenColumnIndex, { type: 'FILTER' })) - ) { - break; - } - // check viewport rows for (let rowIdx = rowOverscanStartIdx; rowIdx <= rowOverscanEndIdx; rowIdx++) { const row = rows[rowIdx]; @@ -95,8 +85,7 @@ export function useViewportColumns({ colOverscanStartIdx, lastFrozenColumnIndex, colSpanColumns, - isGroupRow, - enableFilterRow + isGroupRow ]); return useMemo((): readonly CalculatedColumn[] => { diff --git a/src/index.ts b/src/index.ts index 7ccf4c9298..51bed63cc9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,8 +17,6 @@ export type { HeaderRendererProps, CellRendererProps, RowRendererProps, - FilterRendererProps, - Filters, RowsChangeData, SelectRowEvent, FillEvent, diff --git a/src/style/header.ts b/src/style/header.ts index a2d632a19a..37d6298c8f 100644 --- a/src/style/header.ts +++ b/src/style/header.ts @@ -1,32 +1,20 @@ import { css } from '@linaria/core'; -const headerRowAndFilterRow = css` +const headerRow = css` contain: strict; contain: size layout style paint; display: grid; grid-template-columns: var(--template-columns); + grid-template-rows: var(--header-row-height); + height: var(--header-row-height); // needed on Firefox + line-height: var(--header-row-height); width: var(--row-width); position: sticky; + top: 0; background-color: var(--header-background-color); font-weight: bold; z-index: 3; -`; - -const headerRow = css` - grid-template-rows: var(--header-row-height); - height: var(--header-row-height); // needed on Firefox - line-height: var(--header-row-height); - top: 0; touch-action: none; `; -export const headerRowClassname = `rdg-header-row ${headerRowAndFilterRow} ${headerRow}`; - -const filterRow = css` - grid-template-rows: var(--filter-row-height); - height: var(--filter-row-height); // needed on Firefox - line-height: var(--filter-row-height); - top: var(--header-row-height); -`; - -export const filterRowClassname = `rdg-filter-row ${headerRowAndFilterRow} ${filterRow}`; +export const headerRowClassname = `rdg-header-row ${headerRow}`; diff --git a/src/types.ts b/src/types.ts index 0d4646dfd9..fabde7f730 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,8 +52,6 @@ export interface Column { } | null; /** Header renderer for each header cell */ headerRenderer?: React.ComponentType> | null; - /** Component to be used to filter the data of the column */ - filterRenderer?: React.ComponentType> | null; } export interface CalculatedColumn extends Column { @@ -186,14 +184,6 @@ export interface RowRendererProps selectCell: SelectCellFn; } -export interface FilterRendererProps { - column: CalculatedColumn; - value: TFilterValue; - onChange: (value: TFilterValue) => void; -} - -export type Filters = Record; - export interface RowsChangeData { indexes: number[]; column: CalculatedColumn; @@ -243,7 +233,7 @@ export type CellNavigationMode = 'NONE' | 'CHANGE_ROW' | 'LOOP_OVER_ROW'; export type SortDirection = 'ASC' | 'DESC' | 'NONE'; export type ColSpanArgs = - | { type: 'HEADER' | 'FILTER' } + | { type: 'HEADER' } | { type: 'ROW'; row: R } | { type: 'SUMMARY'; row: SR }; diff --git a/stories/demos/HeaderFilters.tsx b/stories/demos/HeaderFilters.tsx index 057db4a3f0..a5f2d2f92b 100644 --- a/stories/demos/HeaderFilters.tsx +++ b/stories/demos/HeaderFilters.tsx @@ -1,11 +1,10 @@ -import { useMemo, useState } from 'react'; -import Select from 'react-select'; +import { createContext, useContext, useMemo, useState } from 'react'; import faker from 'faker'; import { css } from '@linaria/core'; import DataGrid from '../../src'; -import type { Column, Filters } from '../../src'; -import { NumericFilter } from './components/Filters'; +import type { Column } from '../../src'; +import type { HeaderRendererProps, Omit } from '../../src/types'; const rootClassname = css` display: flex; @@ -22,10 +21,21 @@ const toolbarClassname = css` text-align: end; `; +const filterColumnClassName = 'filter-cell'; + const filterContainerClassname = css` - display: flex; - height: inherit; - align-items: center; + .${filterColumnClassName} { + line-height: 35px; + padding: 0; + + > div { + padding: 0 8px; + + &:first-child { + border-bottom: 1px solid var(--border-color); + } + } + } `; const filterClassname = css` @@ -43,38 +53,36 @@ interface Row { complete: number; } -function createRows() { - const rows: Row[] = []; - for (let i = 1; i < 500; i++) { - rows.push({ - id: i, - task: `Task ${i}`, - complete: Math.min(100, Math.round(Math.random() * 110)), - priority: ['Critical', 'High', 'Medium', 'Low'][Math.floor(Math.random() * 4)], - issueType: ['Bug', 'Improvement', 'Epic', 'Story'][Math.floor(Math.random() * 4)], - developer: faker.name.findName() - }); - } - return rows; +interface Filter extends Omit { + complete?: number; + enabled: boolean; } +// Context is needed to read filter values otherwise columns are +// re-created when filters are changed and filter loses focus +const FilterContext = createContext(undefined); + export function HeaderFilters() { const [rows] = useState(createRows); - const [filters, setFilters] = useState({ + const [filters, setFilters] = useState({ task: '', priority: 'Critical', issueType: 'All', developer: '', - complete: '' + complete: undefined, + enabled: true }); - const [enableFilterRow, setEnableFilterRow] = useState(true); - const columns = useMemo((): readonly Column[] => { - const developerOptions = Array.from(new Set(rows.map((r) => r.developer))).map((d) => ({ - label: d, - value: d - })); + const developerOptions = useMemo( + () => + Array.from(new Set(rows.map((r) => r.developer))).map((d) => ({ + label: d, + value: d + })), + [rows] + ); + const columns = useMemo((): readonly Column[] => { return [ { key: 'id', @@ -84,92 +92,128 @@ export function HeaderFilters() { { key: 'task', name: 'Title', - filterRenderer: (p) => ( -
- p.onChange(e.target.value)} - /> -
+ headerCellClass: filterColumnClassName, + headerRenderer: (p) => ( + + {(filters) => ( + + setFilters({ + ...filters, + task: e.target.value + }) + } + /> + )} + ) }, { key: 'priority', name: 'Priority', - filterRenderer: (p) => ( -
- -
+ headerCellClass: filterColumnClassName, + headerRenderer: (p) => ( + + {(filters) => ( + + )} + ) }, { key: 'issueType', name: 'Issue Type', - filterRenderer: (p) => ( -
- -
+ headerCellClass: filterColumnClassName, + headerRenderer: (p) => ( + + {(filters) => ( + + )} + ) }, { key: 'developer', name: 'Developer', - filterRenderer: (p) => ( -
- + setFilters({ + ...filters, + developer: e.target.value + }) + } + list="developers" + /> + + )} + ) }, { key: 'complete', name: '% Complete', - filterRenderer: NumericFilter + headerCellClass: filterColumnClassName, + headerRenderer: (p) => ( + + {(filters) => ( + + setFilters({ + ...filters, + complete: Number.isFinite(e.target.valueAsNumber) + ? e.target.valueAsNumber + : undefined + }) + } + /> + )} + + ) } ]; - }, [rows]); + }, []); const filteredRows = useMemo(() => { return rows.filter((r) => { @@ -177,8 +221,10 @@ export function HeaderFilters() { (filters.task ? r.task.includes(filters.task) : true) && (filters.priority !== 'All' ? r.priority === filters.priority : true) && (filters.issueType !== 'All' ? r.issueType === filters.issueType : true) && - (filters.developer ? r.developer === filters.developer.value : true) && - (filters.complete ? filters.complete.filterValues(r, filters.complete, 'complete') : true) + (filters.developer + ? r.developer.toLowerCase().startsWith(filters.developer.toLowerCase()) + : true) && + (filters.complete !== undefined ? r.complete >= filters.complete : true) ); }); }, [rows, filters]); @@ -189,12 +235,16 @@ export function HeaderFilters() { priority: 'All', issueType: 'All', developer: '', - complete: '' + complete: undefined, + enabled: true }); } function toggleFilters() { - setEnableFilterRow(!enableFilterRow); + setFilters((filters) => ({ + ...filters, + enabled: !filters.enabled + })); } return ( @@ -207,15 +257,51 @@ export function HeaderFilters() { Clear Filters
- + + + + + {developerOptions.map(({ label, value }) => ( + + ))} + ); } -HeaderFilters.storyName = 'Header Filters'; +function FilterRenderer({ + column, + children +}: HeaderRendererProps & { + children: (filters: Filter) => React.ReactElement; +}) { + const filters = useContext(FilterContext)!; + return ( + <> +
{column.name}
+ {filters.enabled &&
{children(filters)}
} + + ); +} + +function createRows() { + const rows: Row[] = []; + for (let i = 1; i < 500; i++) { + rows.push({ + id: i, + task: `Task ${i}`, + complete: Math.min(100, Math.round(Math.random() * 110)), + priority: ['Critical', 'High', 'Medium', 'Low'][Math.floor(Math.random() * 4)], + issueType: ['Bug', 'Improvement', 'Epic', 'Story'][Math.floor(Math.random() * 4)], + developer: faker.name.findName() + }); + } + return rows; +} diff --git a/stories/demos/components/Filters/NumericFilter.tsx b/stories/demos/components/Filters/NumericFilter.tsx deleted file mode 100644 index ffafb4e232..0000000000 --- a/stories/demos/components/Filters/NumericFilter.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import type { Column, FilterRendererProps } from '../../../../src'; - -enum RuleType { - number = 1, - range = 2, - greaterThan = 3, - lessThan = 4 -} - -type Rule = - | { type: RuleType.range; begin: number; end: number } - | { type: RuleType.greaterThan | RuleType.lessThan | RuleType.number; value: number }; - -interface ChangeEvent { - filterTerm: Rule[] | null; - column: Column; - rawValue: string; - filterValues: typeof filterValues; -} - -export function NumericFilter({ - value, - column, - onChange -}: FilterRendererProps, SR>) { - /** Validates the input */ - function handleKeyDown(event: React.KeyboardEvent) { - const result = /[><,0-9-]/.test(event.key); - if (!result) { - event.preventDefault(); - } - } - - function handleChange(event: React.ChangeEvent) { - const { value } = event.target; - const filters = getRules(value); - onChange({ - filterTerm: filters.length > 0 ? filters : null, - column, - rawValue: value, - filterValues - }); - } - - const tooltipText = 'Input Methods: Range (x-y), Greater Than (>x), Less Than ( - - - ? - - - ); -} - -function filterValues( - row: R, - columnFilter: { filterTerm: { [key in string]: Rule } }, - columnKey: keyof R -) { - // implement default filter logic - const value = parseInt(row[columnKey] as unknown as string, 10); - for (const ruleKey in columnFilter.filterTerm) { - const rule = columnFilter.filterTerm[ruleKey]; - - switch (rule.type) { - case RuleType.number: - if (rule.value === value) { - return true; - } - break; - case RuleType.greaterThan: - if (rule.value <= value) { - return true; - } - break; - case RuleType.lessThan: - if (rule.value >= value) { - return true; - } - break; - case RuleType.range: - if (rule.begin <= value && rule.end >= value) { - return true; - } - break; - default: - break; - } - } - - return false; -} - -function getRules(value: string): Rule[] { - if (value === '') { - return []; - } - - // handle each value with comma - return value.split(',').map((str): Rule => { - // handle dash - const dashIdx = str.indexOf('-'); - if (dashIdx > 0) { - const begin = parseInt(str.slice(0, dashIdx), 10); - const end = parseInt(str.slice(dashIdx + 1), 10); - return { type: RuleType.range, begin, end }; - } - - // handle greater then - if (str.includes('>')) { - const begin = parseInt(str.slice(str.indexOf('>') + 1), 10); - return { type: RuleType.greaterThan, value: begin }; - } - - // handle less then - if (str.includes('<')) { - const end = parseInt(str.slice(str.indexOf('<') + 1), 10); - return { type: RuleType.lessThan, value: end }; - } - - // handle normal values - const numericValue = parseInt(str, 10); - return { type: RuleType.number, value: numericValue }; - }); -} diff --git a/stories/demos/components/Filters/index.ts b/stories/demos/components/Filters/index.ts deleted file mode 100644 index 9eaf3e9fcf..0000000000 --- a/stories/demos/components/Filters/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './NumericFilter';