From cd928bc973ec55576e64af0dae589ddce9ba1857 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Thu, 30 Jul 2020 13:00:44 -0500 Subject: [PATCH 01/79] Initial commit --- src/DataGrid.tsx | 122 ++++++++++++++++++++----- src/GroupedRow.tsx | 69 ++++++++++++++ src/formatters/SelectCellFormatter.tsx | 3 +- src/hooks/useViewportColumns.ts | 17 +++- stories/demos/CommonFeatures.tsx | 1 + 5 files changed, 188 insertions(+), 24 deletions(-) create mode 100644 src/GroupedRow.tsx diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 68c05b21ea..6d33e5692e 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -6,15 +6,18 @@ import React, { useEffect, useImperativeHandle, useCallback, - createElement + createElement, + useMemo } from 'react'; import clsx from 'clsx'; +import { groupBy as lodashGroupBy } from 'lodash'; import { useGridWidth, useViewportColumns } from './hooks'; import EventBus from './EventBus'; import HeaderRow from './HeaderRow'; import FilterRow from './FilterRow'; import Row from './Row'; +import GroupedRow from './GroupedRow'; import SummaryRow from './SummaryRow'; import { ValueFormatter } from './formatters'; import { legacyCellInput } from './editors'; @@ -53,6 +56,11 @@ interface EditCellState extends Position { key: string | null; } +interface GroupedRow { + __isGroup: boolean; + key: string; +} + export interface DataGridHandle { scrollToColumn: (colIdx: number) => void; scrollToRow: (rowIdx: number) => void; @@ -121,6 +129,7 @@ export interface DataGridProps extends Share onSort?: (columnKey: string, direction: SortDirection) => void; filters?: Filters; onFiltersChange?: (filters: Filters) => void; + groupBy?: string; // TODO: support multiple columns and custom grouping logic /** * Custom renderers @@ -189,6 +198,7 @@ function DataGrid({ onSort, filters, onFiltersChange, + groupBy, // Custom renderers defaultFormatter = ValueFormatter, rowRenderer: RowRenderer = Row, @@ -223,6 +233,7 @@ function DataGrid({ const [copiedPosition, setCopiedPosition] = useState(null); const [isDragging, setDragging] = useState(false); const [draggedOverRowIdx, setOverRowIdx] = useState(undefined); + const [expandedGroupIds, setExpandedGroupIds] = useState>(new Set()); const setDraggedOverRowIdx = useCallback((rowIdx?: number) => { setOverRowIdx(rowIdx); @@ -252,7 +263,8 @@ function DataGrid({ columnWidths, defaultFormatter, scrollLeft, - viewportWidth + viewportWidth, + groupBy }); const totalHeaderHeight = headerRowHeight + (enableFilters ? headerFiltersHeight : 0); @@ -262,11 +274,31 @@ function DataGrid({ - summaryRowsCount * rowHeight - (totalColumnWidth > viewportWidth ? getScrollbarSize() : 0); + const groupedRows = useMemo(() => { + if (!groupBy) return; + return lodashGroupBy(rows, groupBy); + }, [groupBy, rows]); + + const [calculatedRows, totalRowCount] = useMemo(() => { + if (!groupedRows) return [rows, rows.length]; + + const flattenedRows = []; + let rowCount = 0; + for (const key in groupedRows) { + flattenedRows.push({ key, __isGroup: true }); + if (expandedGroupIds.has(key)) { + flattenedRows.push(...groupedRows[key]); + } + rowCount = rowCount + groupedRows[key].length + 1; + } + return [flattenedRows, rowCount]; + }, [expandedGroupIds, groupedRows, rows]); + const [rowOverscanStartIdx, rowOverscanEndIdx] = getVerticalRangeToRender( clientHeight, rowHeight, scrollTop, - rows.length + calculatedRows.length ); /** @@ -276,8 +308,14 @@ function DataGrid({ if (selectedPosition === prevSelectedPosition.current || selectedPosition.mode === 'EDIT' || !isCellWithinBounds(selectedPosition)) return; prevSelectedPosition.current = selectedPosition; scrollToCell(selectedPosition); + + const row = calculatedRows[selectedPosition.rowIdx]; + if (isGroupedRow(row)) return; + + // Let the formatter handle focus const column = columns[selectedPosition.idx]; - if (column.formatterOptions?.focusable) return; // Let the formatter handle focus + if (column.formatterOptions?.focusable) return; + focusSinkRef.current!.focus(); }); @@ -287,7 +325,9 @@ function DataGrid({ const handleRowSelectionChange = ({ rowIdx, checked, isShiftClick }: SelectRowEvent) => { assertIsValidKey(rowKey); const newSelectedRows = new Set(selectedRows); - const rowId = rows[rowIdx][rowKey]; + const row = calculatedRows[rowIdx]; + if (isGroupedRow(row)) return; // TODO: add a checkbox to select the group + const rowId = row[rowKey]; if (checked) { newSelectedRows.add(rowId); @@ -296,7 +336,10 @@ function DataGrid({ if (isShiftClick && previousRowIdx !== -1 && previousRowIdx !== rowIdx) { const step = Math.sign(rowIdx - previousRowIdx); for (let i = previousRowIdx + step; i !== rowIdx; i += step) { - newSelectedRows.add(rows[i][rowKey]); + const row = calculatedRows[i]; + if (!isGroupedRow(row)) { + newSelectedRows.add(row[rowKey]); + } } } } else { @@ -308,7 +351,7 @@ function DataGrid({ }; return eventBus.subscribe('SELECT_ROW', handleRowSelectionChange); - }, [eventBus, onSelectedRowsChange, rows, rowKey, selectedRows]); + }, [calculatedRows, eventBus, onSelectedRowsChange, rowKey, selectedRows]); useEffect(() => { return eventBus.subscribe('SELECT_CELL', selectCell); @@ -382,6 +425,9 @@ function DataGrid({ }, [columnWidths, onColumnResize]); function handleCommit({ cellKey, rowIdx, updated }: CommitEvent) { + if (groupBy) { + rowIdx = rows.indexOf(calculatedRows[rowIdx] as R); + } onRowsUpdate?.({ cellKey, fromRow: rowIdx, @@ -426,8 +472,9 @@ function DataGrid({ function handleCellInput(event: React.KeyboardEvent) { const { key } = event; + const row = calculatedRows[selectedPosition.rowIdx]; + if (isGroupedRow(row)) return; const column = columns[selectedPosition.idx]; - const row = rows[selectedPosition.rowIdx]; const canOpenEditor = selectedPosition.mode === 'SELECT' && isCellEditable(selectedPosition); const isActivatedByUser = (column.unsafe_onCellInput ?? legacyCellInput)(event, row) === true; @@ -495,8 +542,12 @@ function DataGrid({ /** * utils */ + function isGroupedRow(row: R | GroupedRow): row is GroupedRow { + return (row as GroupedRow).__isGroup !== undefined; + } + function isCellWithinBounds({ idx, rowIdx }: Position): boolean { - return rowIdx >= 0 && rowIdx < rows.length && idx >= 0 && idx < columns.length; + return rowIdx >= 0 && rowIdx < calculatedRows.length && idx >= 0 && idx < columns.length; } function isCellEditable(position: Position): boolean { @@ -564,13 +615,13 @@ function DataGrid({ return { idx: idx + 1, rowIdx }; case 'Tab': if (selectedPosition.idx === -1 && selectedPosition.rowIdx === -1) { - return shiftKey ? { idx: columns.length - 1, rowIdx: rows.length - 1 } : { idx: 0, rowIdx: 0 }; + return shiftKey ? { idx: columns.length - 1, rowIdx: calculatedRows.length - 1 } : { idx: 0, rowIdx: 0 }; } return { idx: idx + (shiftKey ? -1 : 1), rowIdx }; case 'Home': return ctrlKey ? { idx: 0, rowIdx: 0 } : { idx: 0, rowIdx }; case 'End': - return ctrlKey ? { idx: columns.length - 1, rowIdx: rows.length - 1 } : { idx: columns.length - 1, rowIdx }; + return ctrlKey ? { idx: columns.length - 1, rowIdx: calculatedRows.length - 1 } : { idx: columns.length - 1, rowIdx }; case 'PageUp': return { idx, rowIdx: rowIdx - Math.floor(clientHeight / rowHeight) }; case 'PageDown': @@ -587,7 +638,7 @@ function DataGrid({ let mode = cellNavigationMode; if (key === 'Tab') { // If we are in a position to leave the grid, stop editing but stay in that cell - if (canExitGrid({ shiftKey, cellNavigationMode, columns, rowsCount: rows.length, selectedPosition })) { + if (canExitGrid({ shiftKey, cellNavigationMode, columns, rowsCount: calculatedRows.length, selectedPosition })) { // Allow focus to leave the grid so the next control in the tab order can be focused return; } @@ -602,7 +653,7 @@ function DataGrid({ nextPosition = getNextSelectedCellPosition({ columns, - rowsCount: rows.length, + rowsCount: calculatedRows.length, cellNavigationMode: mode, nextPosition }); @@ -653,9 +704,38 @@ function DataGrid({ function getViewportRows() { const rowElements = []; - for (let rowIdx = rowOverscanStartIdx; rowIdx <= rowOverscanEndIdx; rowIdx++) { - const row = rows[rowIdx]; + const top = rowIdx * rowHeight + totalHeaderHeight; + const row = calculatedRows[rowIdx]; + if (isGroupedRow(row)) { + const { key } = row; + rowElements.push( + { + setSelectedPosition(({ idx }) => ({ idx: idx === -1 ? 0 : idx, rowIdx, mode: 'SELECT' })); + }} + onKeyDown={handleKeyDown} + toggleGroup={() => { + const newExpandedGroupIds = new Set(expandedGroupIds); + if (expandedGroupIds.has(key)) { + newExpandedGroupIds.delete(key); + } else { + newExpandedGroupIds.add(key); + } + setExpandedGroupIds(newExpandedGroupIds); + }} + /> + ); + continue; + } + let key: string | number = rowIdx; let isRowSelected = false; if (rowKey !== undefined) { @@ -679,7 +759,7 @@ function DataGrid({ isRowSelected={isRowSelected} onRowClick={onRowClick} rowClass={rowClass} - top={rowIdx * rowHeight + totalHeaderHeight} + top={top} copiedCellIdx={copiedPosition?.rowIdx === rowIdx ? copiedPosition.idx : undefined} draggedOverCellIdx={getDraggedOverCellIdx(rowIdx)} setDraggedOverRowIdx={isDragging ? setDraggedOverRowIdx : undefined} @@ -692,7 +772,7 @@ function DataGrid({ } // Reset the positions if the current values are no longer valid. This can happen if a column or row is removed - if (selectedPosition.idx >= columns.length || selectedPosition.rowIdx >= rows.length) { + if (selectedPosition.idx >= columns.length || selectedPosition.rowIdx >= calculatedRows.length) { setSelectedPosition({ idx: -1, rowIdx: -1, mode: 'SELECT' }); setCopiedPosition(null); setDraggedOverRowIdx(undefined); @@ -706,7 +786,7 @@ function DataGrid({ aria-describedby={ariaDescribedBy} aria-multiselectable={isSelectable ? true : undefined} aria-colcount={columns.length} - aria-rowcount={headerRowsCount + rows.length + summaryRowsCount} + aria-rowcount={headerRowsCount + calculatedRows.length + summaryRowsCount} className={clsx('rdg', { 'rdg-viewport-dragging': isDragging })} style={{ width, @@ -739,7 +819,7 @@ function DataGrid({ onFiltersChange={onFiltersChange} /> )} - {rows.length === 0 && emptyRowsRenderer ? createElement(emptyRowsRenderer) : ( + {calculatedRows.length === 0 && emptyRowsRenderer ? createElement(emptyRowsRenderer) : ( <>
({ className="rdg-focus-sink" onKeyDown={handleKeyDown} /> -
+
{getViewportRows()} {summaryRows?.map((row, rowIdx) => ( - aria-rowindex={headerRowsCount + rows.length + rowIdx + 1} + aria-rowindex={headerRowsCount + totalRowCount + rowIdx + 1} key={rowIdx} rowIdx={rowIdx} row={row} diff --git a/src/GroupedRow.tsx b/src/GroupedRow.tsx new file mode 100644 index 0000000000..bc3ae9d6b1 --- /dev/null +++ b/src/GroupedRow.tsx @@ -0,0 +1,69 @@ +import React, { useRef, useLayoutEffect } from 'react'; +import clsx from 'clsx'; + +type SharedDivProps = Pick, + | 'onClick' + | 'onKeyDown' + | 'aria-rowindex' +>; + +interface GroupedRowProps extends SharedDivProps { + groupKey: string; + top: number; + width: number; + isExpanded: boolean; + isSelected: boolean; + toggleGroup: () => void; +} + +export default function GroupedRow({ + 'aria-rowindex': ariaRowIndex, + groupKey, + top, + width, + isExpanded, + isSelected, + onClick, + onKeyDown, + toggleGroup +}: GroupedRowProps) { + const cellRef = useRef(null); + useLayoutEffect(() => { + if (!isSelected) return; + cellRef.current?.focus(); + }, [isSelected]); + + function handleKeyDown(e: React.KeyboardEvent) { + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault(); + toggleGroup(); + } + } + + return ( +
+
+ + {isExpanded ? '\u25BC' : '\u25B6'} {groupKey} + +
+
+ ); +} diff --git a/src/formatters/SelectCellFormatter.tsx b/src/formatters/SelectCellFormatter.tsx index bff462c360..5d89748811 100644 --- a/src/formatters/SelectCellFormatter.tsx +++ b/src/formatters/SelectCellFormatter.tsx @@ -1,4 +1,4 @@ -import React, { useLayoutEffect, useRef } from 'react'; +import React, { useRef, useLayoutEffect } from 'react'; import clsx from 'clsx'; type SharedInputProps = Pick, @@ -24,7 +24,6 @@ export function SelectCellFormatter({ 'aria-labelledby': ariaLabelledBy }: SelectCellFormatterProps) { const inputRef = useRef(null); - useLayoutEffect(() => { if (!isCellSelected) return; inputRef.current?.focus(); diff --git a/src/hooks/useViewportColumns.ts b/src/hooks/useViewportColumns.ts index cb37c4488c..e96dbeae82 100644 --- a/src/hooks/useViewportColumns.ts +++ b/src/hooks/useViewportColumns.ts @@ -12,6 +12,7 @@ interface ViewportColumnsArgs extends SharedDataGridPr viewportWidth: number; scrollLeft: number; columnWidths: ReadonlyMap; + groupBy?: string; } export function useViewportColumns({ @@ -20,8 +21,22 @@ export function useViewportColumns({ columnWidths, viewportWidth, defaultFormatter, - scrollLeft + scrollLeft, + groupBy }: ViewportColumnsArgs) { + rawColumns = useMemo(() => { + if (!groupBy) return rawColumns; + // TODO: make it generic + const selectColumn = rawColumns.find(c => c.key === 'select-row'); + const groupByColumn = rawColumns.find(c => c.key === groupBy); + const remaningColumns = rawColumns.filter(c => c.key !== groupBy && c.key !== 'select-row'); + return [ + selectColumn!, + { ...groupByColumn!, frozen: true }, + ...remaningColumns + ]; + }, [groupBy, rawColumns]); + const { columns, lastFrozenColumnIndex, totalColumnWidth } = useMemo(() => { return getColumnMetrics({ columns: rawColumns, diff --git a/stories/demos/CommonFeatures.tsx b/stories/demos/CommonFeatures.tsx index 22da2f096b..c1f118d75f 100644 --- a/stories/demos/CommonFeatures.tsx +++ b/stories/demos/CommonFeatures.tsx @@ -287,6 +287,7 @@ export default function CommonFeatures() { sortDirection={sortDirection} onSort={handleSort} summaryRows={summaryRows} + groupBy="country" /> )} From 3d6dd338097a654fe21af72cb4e938b11cba1d10 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Thu, 6 Aug 2020 15:56:10 -0500 Subject: [PATCH 02/79] Group by multiple columns --- src/DataGrid.tsx | 75 +++++-------------- src/GroupedRow.tsx | 6 +- src/hooks/index.ts | 1 + ...wportColumns.ts => useViewportColumns.tsx} | 37 ++++++--- src/hooks/useViewportRows.ts | 65 ++++++++++++++++ src/types.ts | 10 +++ src/utils/columnUtils.ts | 2 +- src/utils/index.ts | 6 ++ stories/demos/CommonFeatures.tsx | 4 +- 9 files changed, 138 insertions(+), 68 deletions(-) rename src/hooks/{useViewportColumns.ts => useViewportColumns.tsx} (62%) create mode 100644 src/hooks/useViewportRows.ts diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 6d33e5692e..6d8940f132 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -6,18 +6,16 @@ import React, { useEffect, useImperativeHandle, useCallback, - createElement, - useMemo + createElement } from 'react'; import clsx from 'clsx'; -import { groupBy as lodashGroupBy } from 'lodash'; -import { useGridWidth, useViewportColumns } from './hooks'; +import { useGridWidth, useViewportColumns, useViewportRows } from './hooks'; import EventBus from './EventBus'; import HeaderRow from './HeaderRow'; import FilterRow from './FilterRow'; import Row from './Row'; -import GroupedRow from './GroupedRow'; +import GroupedRowRenderer from './GroupedRow'; import SummaryRow from './SummaryRow'; import { ValueFormatter } from './formatters'; import { legacyCellInput } from './editors'; @@ -29,7 +27,8 @@ import { getNextSelectedCellPosition, isSelectedCellEditable, canExitGrid, - isCtrlKeyHeldDown + isCtrlKeyHeldDown, + isGroupedRow } from './utils'; import { @@ -56,11 +55,6 @@ interface EditCellState extends Position { key: string | null; } -interface GroupedRow { - __isGroup: boolean; - key: string; -} - export interface DataGridHandle { scrollToColumn: (colIdx: number) => void; scrollToRow: (rowIdx: number) => void; @@ -129,7 +123,7 @@ export interface DataGridProps extends Share onSort?: (columnKey: string, direction: SortDirection) => void; filters?: Filters; onFiltersChange?: (filters: Filters) => void; - groupBy?: string; // TODO: support multiple columns and custom grouping logic + groupBy?: readonly string[]; // TODO: support custom grouping logic /** * Custom renderers @@ -179,7 +173,7 @@ export interface DataGridProps extends Share function DataGrid({ // Grid and data Props columns: rawColumns, - rows, + rows: rawRows, summaryRows, rowKey, onRowsUpdate, @@ -233,7 +227,6 @@ function DataGrid({ const [copiedPosition, setCopiedPosition] = useState(null); const [isDragging, setDragging] = useState(false); const [draggedOverRowIdx, setOverRowIdx] = useState(undefined); - const [expandedGroupIds, setExpandedGroupIds] = useState>(new Set()); const setDraggedOverRowIdx = useCallback((rowIdx?: number) => { setOverRowIdx(rowIdx); @@ -274,25 +267,7 @@ function DataGrid({ - summaryRowsCount * rowHeight - (totalColumnWidth > viewportWidth ? getScrollbarSize() : 0); - const groupedRows = useMemo(() => { - if (!groupBy) return; - return lodashGroupBy(rows, groupBy); - }, [groupBy, rows]); - - const [calculatedRows, totalRowCount] = useMemo(() => { - if (!groupedRows) return [rows, rows.length]; - - const flattenedRows = []; - let rowCount = 0; - for (const key in groupedRows) { - flattenedRows.push({ key, __isGroup: true }); - if (expandedGroupIds.has(key)) { - flattenedRows.push(...groupedRows[key]); - } - rowCount = rowCount + groupedRows[key].length + 1; - } - return [flattenedRows, rowCount]; - }, [expandedGroupIds, groupedRows, rows]); + const { calculatedRows, totalRowCount, toggleGroup } = useViewportRows({ rawRows, groupBy }); const [rowOverscanStartIdx, rowOverscanEndIdx] = getVerticalRangeToRender( clientHeight, @@ -426,7 +401,7 @@ function DataGrid({ function handleCommit({ cellKey, rowIdx, updated }: CommitEvent) { if (groupBy) { - rowIdx = rows.indexOf(calculatedRows[rowIdx] as R); + rowIdx = rawRows.indexOf(calculatedRows[rowIdx] as R); } onRowsUpdate?.({ cellKey, @@ -441,7 +416,7 @@ function DataGrid({ function handleCopy() { const { idx, rowIdx } = selectedPosition; - const value = rows[rowIdx][columns[idx].key as keyof R]; + const value = rawRows[rowIdx][columns[idx].key as keyof R]; setCopiedPosition({ idx, rowIdx, value }); } @@ -489,7 +464,7 @@ function DataGrid({ const { idx, rowIdx } = selectedPosition; const column = columns[idx]; const cellKey = column.key; - const value = rows[rowIdx][cellKey as keyof R]; + const value = rawRows[rowIdx][cellKey as keyof R]; onRowsUpdate?.({ cellKey, @@ -528,12 +503,12 @@ function DataGrid({ const column = columns[selectedPosition.idx]; const cellKey = column.key; - const value = rows[selectedPosition.rowIdx][cellKey as keyof R]; + const value = rawRows[selectedPosition.rowIdx][cellKey as keyof R]; onRowsUpdate?.({ cellKey, fromRow: selectedPosition.rowIdx, - toRow: rows.length - 1, + toRow: rawRows.length - 1, updated: { [cellKey]: value } as unknown as never, action: UpdateActions.COLUMN_FILL }); @@ -542,9 +517,6 @@ function DataGrid({ /** * utils */ - function isGroupedRow(row: R | GroupedRow): row is GroupedRow { - return (row as GroupedRow).__isGroup !== undefined; - } function isCellWithinBounds({ idx, rowIdx }: Position): boolean { return rowIdx >= 0 && rowIdx < calculatedRows.length && idx >= 0 && idx < columns.length; @@ -552,7 +524,7 @@ function DataGrid({ function isCellEditable(position: Position): boolean { return isCellWithinBounds(position) - && isSelectedCellEditable({ columns, rows, selectedPosition: position, onCheckCellIsEditable }); + && isSelectedCellEditable({ columns, rows: rawRows, selectedPosition: position, onCheckCellIsEditable }); } function selectCell(position: Position, enableEditor = false): void { @@ -710,27 +682,20 @@ function DataGrid({ if (isGroupedRow(row)) { const { key } = row; rowElements.push( - { setSelectedPosition(({ idx }) => ({ idx: idx === -1 ? 0 : idx, rowIdx, mode: 'SELECT' })); }} onKeyDown={handleKeyDown} - toggleGroup={() => { - const newExpandedGroupIds = new Set(expandedGroupIds); - if (expandedGroupIds.has(key)) { - newExpandedGroupIds.delete(key); - } else { - newExpandedGroupIds.add(key); - } - setExpandedGroupIds(newExpandedGroupIds); - }} + toggleGroup={() => toggleGroup(key)} /> ); continue; @@ -801,11 +766,11 @@ function DataGrid({ > rowKey={rowKey} - rows={rows} + rows={rawRows} columns={viewportColumns} onColumnResize={handleColumnResize} lastFrozenColumnIndex={lastFrozenColumnIndex} - allRowsSelected={selectedRows?.size === rows.length} + allRowsSelected={selectedRows?.size === rawRows.length} onSelectedRowsChange={onSelectedRowsChange} sortColumn={sortColumn} sortDirection={sortDirection} diff --git a/src/GroupedRow.tsx b/src/GroupedRow.tsx index bc3ae9d6b1..88279b00ce 100644 --- a/src/GroupedRow.tsx +++ b/src/GroupedRow.tsx @@ -11,6 +11,7 @@ interface GroupedRowProps extends SharedDivProps { groupKey: string; top: number; width: number; + columnWidth: number; isExpanded: boolean; isSelected: boolean; toggleGroup: () => void; @@ -21,6 +22,7 @@ export default function GroupedRow({ groupKey, top, width, + columnWidth, isExpanded, isSelected, onClick, @@ -50,7 +52,7 @@ export default function GroupedRow({ >
@@ -61,7 +63,7 @@ export default function GroupedRow({ onClick={toggleGroup} onKeyDown={handleKeyDown} > - {isExpanded ? '\u25BC' : '\u25B6'} {groupKey} + {groupKey}{' '}{isExpanded ? '\u25BC' : '\u25B6'}
diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 89f6b0bfc3..b48e8697e0 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -2,3 +2,4 @@ export * from './useCombinedRefs'; export * from './useClickOutside'; export * from './useGridWidth'; export * from './useViewportColumns'; +export * from './useViewportRows'; diff --git a/src/hooks/useViewportColumns.ts b/src/hooks/useViewportColumns.tsx similarity index 62% rename from src/hooks/useViewportColumns.ts rename to src/hooks/useViewportColumns.tsx index e96dbeae82..e7774d33a2 100644 --- a/src/hooks/useViewportColumns.ts +++ b/src/hooks/useViewportColumns.tsx @@ -1,7 +1,7 @@ -import { useMemo } from 'react'; +import React, { useMemo } from 'react'; -import { CalculatedColumn } from '../types'; -import { getColumnMetrics, getHorizontalRangeToRender, getViewportColumns } from '../utils'; +import { CalculatedColumn, Column } from '../types'; +import { getColumnMetrics, getHorizontalRangeToRender, getViewportColumns, canEdit, isGroupedRow } from '../utils'; import { DataGridProps } from '../DataGrid'; type SharedDataGridProps = @@ -12,7 +12,7 @@ interface ViewportColumnsArgs extends SharedDataGridPr viewportWidth: number; scrollLeft: number; columnWidths: ReadonlyMap; - groupBy?: string; + groupBy?: readonly string[]; } export function useViewportColumns({ @@ -25,17 +25,36 @@ export function useViewportColumns({ groupBy }: ViewportColumnsArgs) { rawColumns = useMemo(() => { - if (!groupBy) return rawColumns; + if (!groupBy || groupBy.length === 0) return rawColumns; + // TODO: make it generic const selectColumn = rawColumns.find(c => c.key === 'select-row'); - const groupByColumn = rawColumns.find(c => c.key === groupBy); - const remaningColumns = rawColumns.filter(c => c.key !== groupBy && c.key !== 'select-row'); + const groupByColumns: Column[] = rawColumns + .filter(c => groupBy.includes(c.key)) + .map(f => { + const updatedColumn: Column = { ...f }; + updatedColumn.frozen = true; + updatedColumn.formatter = (p) => { + if (isGroupedRow(p.row)) { + const F = f.formatter || defaultFormatter; + return ; + } + return null; + }; + updatedColumn.editable = row => { + return isGroupedRow(row) ? false : canEdit(updatedColumn, row); + }; + return updatedColumn; + }) + .sort((c1, c2) => groupBy.findIndex(k => k === c1.key) - groupBy.findIndex(k => k === c2.key)); + + const remaningColumns = rawColumns.filter(c => !groupBy.includes(c.key) && c.key !== 'select-row'); return [ selectColumn!, - { ...groupByColumn!, frozen: true }, + ...groupByColumns, ...remaningColumns ]; - }, [groupBy, rawColumns]); + }, [defaultFormatter, groupBy, rawColumns]); const { columns, lastFrozenColumnIndex, totalColumnWidth } = useMemo(() => { return getColumnMetrics({ diff --git a/src/hooks/useViewportRows.ts b/src/hooks/useViewportRows.ts new file mode 100644 index 0000000000..729dbe2ad0 --- /dev/null +++ b/src/hooks/useViewportRows.ts @@ -0,0 +1,65 @@ +import { useMemo, useState } from 'react'; +import { groupBy as lodashGroupBy, Dictionary } from 'lodash'; + +import { GroupedRow, GroupByDictionary } from '../types'; + +interface CalculatedRowsArgs { + rawRows: readonly R[]; + groupBy?: readonly string[]; +} + +export function useViewportRows({ rawRows, groupBy }: CalculatedRowsArgs) { + const [expandedGroupIds, setExpandedGroupIds] = useState>(new Set()); + + const groupedRows = useMemo(() => { + if (!groupBy || groupBy.length === 0) return; + + function groupParent(rows: readonly R[], [groupByKey, ...remainingGroupByKeys]: readonly string[]) { + const parentGroup = lodashGroupBy(rows, groupByKey); + if (remainingGroupByKeys.length === 0) return parentGroup; + const childGroups: Dictionary> = {}; + for (const key in parentGroup) { + childGroups[key] = groupParent(parentGroup[key], remainingGroupByKeys); + } + + return childGroups; + } + + return groupParent(rawRows, groupBy); + }, [groupBy, rawRows]); + + const [calculatedRows, totalRowCount] = useMemo(() => { + if (!groupedRows) return [rawRows, rawRows.length]; + + function expandGroup(groupedRows: GroupByDictionary, level: number): Array { + const flattenedRows: Array = []; + for (const key in groupedRows) { + const isExpanded = expandedGroupIds.has(key); + flattenedRows.push({ key, __isGroup: true, level, isExpanded }); + if (isExpanded) { + const groupedRow = groupedRows[key]; + if (Array.isArray(groupedRow)) { + flattenedRows.push(...groupedRow); + } else { + flattenedRows.push(...expandGroup(groupedRow, level + 1)); + } + } + } + + return flattenedRows; + } + return [expandGroup(groupedRows, 0), 0]; + }, [expandedGroupIds, groupedRows, rawRows]); + + function toggleGroup(key: string) { + const newExpandedGroupIds = new Set(expandedGroupIds); + if (expandedGroupIds.has(key)) { + newExpandedGroupIds.delete(key); + } else { + newExpandedGroupIds.add(key); + } + setExpandedGroupIds(newExpandedGroupIds); + } + + return { calculatedRows, totalRowCount, toggleGroup }; +} diff --git a/src/types.ts b/src/types.ts index 47d839d596..0b551481a0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { KeyboardEvent } from 'react'; +import { Dictionary } from 'lodash'; import { UpdateActions } from './enums'; import EventBus from './EventBus'; @@ -189,3 +190,12 @@ export interface SelectRowEvent { checked: boolean; isShiftClick: boolean; } + +export type GroupByDictionary = Dictionary | Dictionary>; + +export interface GroupedRow { + __isGroup: boolean; + key: string; + level: number; + isExpanded: boolean; +} diff --git a/src/utils/columnUtils.ts b/src/utils/columnUtils.ts index a41ac4a987..8475bbf3d3 100644 --- a/src/utils/columnUtils.ts +++ b/src/utils/columnUtils.ts @@ -105,7 +105,7 @@ function clampColumnWidth( // Logic extented to allow for functions to be passed down in column.editable // this allows us to decide whether we can be editing from a cell level -export function canEdit(column: CalculatedColumn, row: R): boolean { +export function canEdit(column: Column, row: R): boolean { if (typeof column.editable === 'function') { return column.editable(row); } diff --git a/src/utils/index.ts b/src/utils/index.ts index 07f4f17083..dc07e6ae34 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,9 +1,15 @@ +import { GroupedRow } from '../types'; + export * from './domUtils'; export * from './columnUtils'; export * from './viewportUtils'; export * from './keyboardUtils'; export * from './selectedCellUtils'; +export function isGroupedRow(row: R | GroupedRow): row is GroupedRow { + return (row as GroupedRow).__isGroup !== undefined; +} + export function assertIsValidKey(key: unknown): asserts key is keyof R { if (key === undefined) { throw new Error('Please specify the rowKey prop to use selection'); diff --git a/stories/demos/CommonFeatures.tsx b/stories/demos/CommonFeatures.tsx index c1f118d75f..c49537e400 100644 --- a/stories/demos/CommonFeatures.tsx +++ b/stories/demos/CommonFeatures.tsx @@ -214,6 +214,8 @@ function createRows(): readonly Row[] { return rows; } +const groupBy = ['country', 'area']; + export default function CommonFeatures() { const [rows, setRows] = useState(createRows); const [[sortColumn, sortDirection], setSort] = useState<[string, SortDirection]>(['id', 'NONE']); @@ -287,7 +289,7 @@ export default function CommonFeatures() { sortDirection={sortDirection} onSort={handleSort} summaryRows={summaryRows} - groupBy="country" + groupBy={groupBy} /> )} From bc00cbc9cd5deaf1f5071e293305136c7c8ad518 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Thu, 6 Aug 2020 16:18:43 -0500 Subject: [PATCH 03/79] Cleanup viewport rows --- src/DataGrid.tsx | 74 ++++++++++++++++++-------------- src/hooks/useViewportColumns.tsx | 1 + src/hooks/useViewportRows.ts | 48 +++++++++++++-------- 3 files changed, 73 insertions(+), 50 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 6d8940f132..39d8e978ab 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -15,7 +15,7 @@ import EventBus from './EventBus'; import HeaderRow from './HeaderRow'; import FilterRow from './FilterRow'; import Row from './Row'; -import GroupedRowRenderer from './GroupedRow'; +import GroupedRow from './GroupedRow'; import SummaryRow from './SummaryRow'; import { ValueFormatter } from './formatters'; import { legacyCellInput } from './editors'; @@ -23,7 +23,6 @@ import { assertIsValidKey, getColumnScrollPosition, getScrollbarSize, - getVerticalRangeToRender, getNextSelectedCellPosition, isSelectedCellEditable, canExitGrid, @@ -228,6 +227,9 @@ function DataGrid({ const [isDragging, setDragging] = useState(false); const [draggedOverRowIdx, setOverRowIdx] = useState(undefined); + // TODO: change it to props + const [expandedGroupIds, setExpandedGroupIds] = useState>(new Set()); + const setDraggedOverRowIdx = useCallback((rowIdx?: number) => { setOverRowIdx(rowIdx); latestDraggedOverRowIdx.current = rowIdx; @@ -267,14 +269,14 @@ function DataGrid({ - summaryRowsCount * rowHeight - (totalColumnWidth > viewportWidth ? getScrollbarSize() : 0); - const { calculatedRows, totalRowCount, toggleGroup } = useViewportRows({ rawRows, groupBy }); - - const [rowOverscanStartIdx, rowOverscanEndIdx] = getVerticalRangeToRender( - clientHeight, + const { viewportRows, rows, startRowIdx, totalRowCount } = useViewportRows({ + rawRows, + groupBy, rowHeight, + clientHeight, scrollTop, - calculatedRows.length - ); + expandedGroupIds + }); /** * effects @@ -284,7 +286,7 @@ function DataGrid({ prevSelectedPosition.current = selectedPosition; scrollToCell(selectedPosition); - const row = calculatedRows[selectedPosition.rowIdx]; + const row = rows[selectedPosition.rowIdx]; if (isGroupedRow(row)) return; // Let the formatter handle focus @@ -300,7 +302,7 @@ function DataGrid({ const handleRowSelectionChange = ({ rowIdx, checked, isShiftClick }: SelectRowEvent) => { assertIsValidKey(rowKey); const newSelectedRows = new Set(selectedRows); - const row = calculatedRows[rowIdx]; + const row = rows[rowIdx]; if (isGroupedRow(row)) return; // TODO: add a checkbox to select the group const rowId = row[rowKey]; @@ -311,7 +313,7 @@ function DataGrid({ if (isShiftClick && previousRowIdx !== -1 && previousRowIdx !== rowIdx) { const step = Math.sign(rowIdx - previousRowIdx); for (let i = previousRowIdx + step; i !== rowIdx; i += step) { - const row = calculatedRows[i]; + const row = rows[i]; if (!isGroupedRow(row)) { newSelectedRows.add(row[rowKey]); } @@ -326,7 +328,7 @@ function DataGrid({ }; return eventBus.subscribe('SELECT_ROW', handleRowSelectionChange); - }, [calculatedRows, eventBus, onSelectedRowsChange, rowKey, selectedRows]); + }, [rows, eventBus, onSelectedRowsChange, rowKey, selectedRows]); useEffect(() => { return eventBus.subscribe('SELECT_CELL', selectCell); @@ -401,7 +403,7 @@ function DataGrid({ function handleCommit({ cellKey, rowIdx, updated }: CommitEvent) { if (groupBy) { - rowIdx = rawRows.indexOf(calculatedRows[rowIdx] as R); + rowIdx = rawRows.indexOf(rows[rowIdx] as R); } onRowsUpdate?.({ cellKey, @@ -447,7 +449,7 @@ function DataGrid({ function handleCellInput(event: React.KeyboardEvent) { const { key } = event; - const row = calculatedRows[selectedPosition.rowIdx]; + const row = rows[selectedPosition.rowIdx]; if (isGroupedRow(row)) return; const column = columns[selectedPosition.idx]; const canOpenEditor = selectedPosition.mode === 'SELECT' && isCellEditable(selectedPosition); @@ -519,7 +521,7 @@ function DataGrid({ */ function isCellWithinBounds({ idx, rowIdx }: Position): boolean { - return rowIdx >= 0 && rowIdx < calculatedRows.length && idx >= 0 && idx < columns.length; + return rowIdx >= 0 && rowIdx < rows.length && idx >= 0 && idx < columns.length; } function isCellEditable(position: Position): boolean { @@ -587,13 +589,13 @@ function DataGrid({ return { idx: idx + 1, rowIdx }; case 'Tab': if (selectedPosition.idx === -1 && selectedPosition.rowIdx === -1) { - return shiftKey ? { idx: columns.length - 1, rowIdx: calculatedRows.length - 1 } : { idx: 0, rowIdx: 0 }; + return shiftKey ? { idx: columns.length - 1, rowIdx: rows.length - 1 } : { idx: 0, rowIdx: 0 }; } return { idx: idx + (shiftKey ? -1 : 1), rowIdx }; case 'Home': return ctrlKey ? { idx: 0, rowIdx: 0 } : { idx: 0, rowIdx }; case 'End': - return ctrlKey ? { idx: columns.length - 1, rowIdx: calculatedRows.length - 1 } : { idx: columns.length - 1, rowIdx }; + return ctrlKey ? { idx: columns.length - 1, rowIdx: rows.length - 1 } : { idx: columns.length - 1, rowIdx }; case 'PageUp': return { idx, rowIdx: rowIdx - Math.floor(clientHeight / rowHeight) }; case 'PageDown': @@ -610,7 +612,7 @@ function DataGrid({ let mode = cellNavigationMode; if (key === 'Tab') { // If we are in a position to leave the grid, stop editing but stay in that cell - if (canExitGrid({ shiftKey, cellNavigationMode, columns, rowsCount: calculatedRows.length, selectedPosition })) { + if (canExitGrid({ shiftKey, cellNavigationMode, columns, rowsCount: rows.length, selectedPosition })) { // Allow focus to leave the grid so the next control in the tab order can be focused return; } @@ -625,7 +627,7 @@ function DataGrid({ nextPosition = getNextSelectedCellPosition({ columns, - rowsCount: calculatedRows.length, + rowsCount: rows.length, cellNavigationMode: mode, nextPosition }); @@ -633,6 +635,16 @@ function DataGrid({ selectCell(nextPosition); } + function toggleGroup(key: string) { + const newExpandedGroupIds = new Set(expandedGroupIds); + if (expandedGroupIds.has(key)) { + newExpandedGroupIds.delete(key); + } else { + newExpandedGroupIds.add(key); + } + setExpandedGroupIds(newExpandedGroupIds); + } + function getDraggedOverCellIdx(currentRowIdx: number): number | undefined { if (draggedOverRowIdx === undefined) return; const { rowIdx } = selectedPosition; @@ -675,14 +687,13 @@ function DataGrid({ } function getViewportRows() { - const rowElements = []; - for (let rowIdx = rowOverscanStartIdx; rowIdx <= rowOverscanEndIdx; rowIdx++) { + return viewportRows.map((row, index) => { + const rowIdx = startRowIdx + index; const top = rowIdx * rowHeight + totalHeaderHeight; - const row = calculatedRows[rowIdx]; if (isGroupedRow(row)) { const { key } = row; - rowElements.push( - ({ toggleGroup={() => toggleGroup(key)} /> ); - continue; } let key: string | number = rowIdx; @@ -711,7 +721,7 @@ function DataGrid({ } } - rowElements.push( + return ( ({ selectedCellProps={getSelectedCellProps(rowIdx)} /> ); - } - - return rowElements; + }); } // Reset the positions if the current values are no longer valid. This can happen if a column or row is removed - if (selectedPosition.idx >= columns.length || selectedPosition.rowIdx >= calculatedRows.length) { + if (selectedPosition.idx >= columns.length || selectedPosition.rowIdx >= rows.length) { setSelectedPosition({ idx: -1, rowIdx: -1, mode: 'SELECT' }); setCopiedPosition(null); setDraggedOverRowIdx(undefined); @@ -751,7 +759,7 @@ function DataGrid({ aria-describedby={ariaDescribedBy} aria-multiselectable={isSelectable ? true : undefined} aria-colcount={columns.length} - aria-rowcount={headerRowsCount + calculatedRows.length + summaryRowsCount} + aria-rowcount={headerRowsCount + rows.length + summaryRowsCount} className={clsx('rdg', { 'rdg-viewport-dragging': isDragging })} style={{ width, @@ -784,7 +792,7 @@ function DataGrid({ onFiltersChange={onFiltersChange} /> )} - {calculatedRows.length === 0 && emptyRowsRenderer ? createElement(emptyRowsRenderer) : ( + {rows.length === 0 && emptyRowsRenderer ? createElement(emptyRowsRenderer) : ( <>
({ className="rdg-focus-sink" onKeyDown={handleKeyDown} /> -
+
{getViewportRows()} {summaryRows?.map((row, rowIdx) => ( diff --git a/src/hooks/useViewportColumns.tsx b/src/hooks/useViewportColumns.tsx index e7774d33a2..51b409a407 100644 --- a/src/hooks/useViewportColumns.tsx +++ b/src/hooks/useViewportColumns.tsx @@ -32,6 +32,7 @@ export function useViewportColumns({ const groupByColumns: Column[] = rawColumns .filter(c => groupBy.includes(c.key)) .map(f => { + // TODO: move the logic to GroupedRow const updatedColumn: Column = { ...f }; updatedColumn.frozen = true; updatedColumn.formatter = (p) => { diff --git a/src/hooks/useViewportRows.ts b/src/hooks/useViewportRows.ts index 729dbe2ad0..33b44f849f 100644 --- a/src/hooks/useViewportRows.ts +++ b/src/hooks/useViewportRows.ts @@ -1,16 +1,26 @@ -import { useMemo, useState } from 'react'; +import { useMemo } from 'react'; import { groupBy as lodashGroupBy, Dictionary } from 'lodash'; import { GroupedRow, GroupByDictionary } from '../types'; +import { getVerticalRangeToRender } from '../utils'; -interface CalculatedRowsArgs { +interface ViewportRowsArgs { rawRows: readonly R[]; + rowHeight: number; + clientHeight: number; + scrollTop: number; groupBy?: readonly string[]; + expandedGroupIds?: Set; } -export function useViewportRows({ rawRows, groupBy }: CalculatedRowsArgs) { - const [expandedGroupIds, setExpandedGroupIds] = useState>(new Set()); - +export function useViewportRows({ + rawRows, + rowHeight, + clientHeight, + scrollTop, + groupBy, + expandedGroupIds +}: ViewportRowsArgs) { const groupedRows = useMemo(() => { if (!groupBy || groupBy.length === 0) return; @@ -28,13 +38,13 @@ export function useViewportRows({ rawRows, groupBy }: CalculatedRowsArgs< return groupParent(rawRows, groupBy); }, [groupBy, rawRows]); - const [calculatedRows, totalRowCount] = useMemo(() => { + const [rows, totalRowCount] = useMemo(() => { if (!groupedRows) return [rawRows, rawRows.length]; function expandGroup(groupedRows: GroupByDictionary, level: number): Array { const flattenedRows: Array = []; for (const key in groupedRows) { - const isExpanded = expandedGroupIds.has(key); + const isExpanded = expandedGroupIds?.has(key) ?? false; flattenedRows.push({ key, __isGroup: true, level, isExpanded }); if (isExpanded) { const groupedRow = groupedRows[key]; @@ -51,15 +61,19 @@ export function useViewportRows({ rawRows, groupBy }: CalculatedRowsArgs< return [expandGroup(groupedRows, 0), 0]; }, [expandedGroupIds, groupedRows, rawRows]); - function toggleGroup(key: string) { - const newExpandedGroupIds = new Set(expandedGroupIds); - if (expandedGroupIds.has(key)) { - newExpandedGroupIds.delete(key); - } else { - newExpandedGroupIds.add(key); - } - setExpandedGroupIds(newExpandedGroupIds); - } + const [rowOverscanStartIdx, rowOverscanEndIdx] = getVerticalRangeToRender( + clientHeight, + rowHeight, + scrollTop, + rows.length + ); + + const viewportRows = rows.slice(rowOverscanStartIdx, rowOverscanEndIdx + 1); - return { calculatedRows, totalRowCount, toggleGroup }; + return { + viewportRows, + rows, + startRowIdx: rowOverscanStartIdx, + totalRowCount + }; } From 654ce7ff8542483acc61c757bf928774b6b95164 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Fri, 7 Aug 2020 13:30:10 -0500 Subject: [PATCH 04/79] memoize group row, add groupRowRenderer prop --- src/DataGrid.tsx | 59 ++++++++++++++++++------------ src/EventBus.ts | 2 + src/GroupRow.tsx | 70 +++++++++++++++++++++++++++++++++++ src/GroupedRow.tsx | 71 ------------------------------------ src/hooks/useViewportRows.ts | 10 ++--- src/types.ts | 14 ++++++- src/utils/index.ts | 6 +-- 7 files changed, 128 insertions(+), 104 deletions(-) create mode 100644 src/GroupRow.tsx delete mode 100644 src/GroupedRow.tsx diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 39d8e978ab..970049a570 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -15,7 +15,7 @@ import EventBus from './EventBus'; import HeaderRow from './HeaderRow'; import FilterRow from './FilterRow'; import Row from './Row'; -import GroupedRow from './GroupedRow'; +import GroupRow from './GroupRow'; import SummaryRow from './SummaryRow'; import { ValueFormatter } from './formatters'; import { legacyCellInput } from './editors'; @@ -38,6 +38,7 @@ import { FormatterProps, Position, RowRendererProps, + GroupRowRendererProps, RowsUpdateEvent, SelectRowEvent, CommitEvent, @@ -129,6 +130,7 @@ export interface DataGridProps extends Share */ defaultFormatter?: React.ComponentType>; rowRenderer?: React.ComponentType>; + groupRowRenderer?: React.ComponentType>; emptyRowsRenderer?: React.ComponentType; /** @@ -195,6 +197,7 @@ function DataGrid({ // Custom renderers defaultFormatter = ValueFormatter, rowRenderer: RowRenderer = Row, + groupRowRenderer: GroupRowRenderer = GroupRow, emptyRowsRenderer, // Event props onRowClick, @@ -334,6 +337,28 @@ function DataGrid({ return eventBus.subscribe('SELECT_CELL', selectCell); }); + useEffect(() => { + function toggleGroup(expandedGroupId: unknown) { + const newExpandedGroupIds = new Set(expandedGroupIds); + if (expandedGroupIds.has(expandedGroupId)) { + newExpandedGroupIds.delete(expandedGroupId); + } else { + newExpandedGroupIds.add(expandedGroupId); + } + setExpandedGroupIds(newExpandedGroupIds); + } + + return eventBus.subscribe('TOGGLE_GROUP', toggleGroup); + }, [eventBus, expandedGroupIds]); + + useEffect(() => { + function selectGroupRow(rowIdx: number) { + const idx = selectedPosition.idx === -1 ? 0 : selectedPosition.idx; + selectCell({ idx, rowIdx }); + } + return eventBus.subscribe('SELECT_GROUP_ROW', selectGroupRow); + }); + useImperativeHandle(ref, () => ({ scrollToColumn(idx: number) { scrollToCell({ idx }); @@ -635,16 +660,6 @@ function DataGrid({ selectCell(nextPosition); } - function toggleGroup(key: string) { - const newExpandedGroupIds = new Set(expandedGroupIds); - if (expandedGroupIds.has(key)) { - newExpandedGroupIds.delete(key); - } else { - newExpandedGroupIds.add(key); - } - setExpandedGroupIds(newExpandedGroupIds); - } - function getDraggedOverCellIdx(currentRowIdx: number): number | undefined { if (draggedOverRowIdx === undefined) return; const { rowIdx } = selectedPosition; @@ -692,21 +707,19 @@ function DataGrid({ const top = rowIdx * rowHeight + totalHeaderHeight; if (isGroupedRow(row)) { const { key } = row; + const isSelected = selectedPosition.rowIdx === rowIdx; return ( - { - setSelectedPosition(({ idx }) => ({ idx: idx === -1 ? 0 : idx, rowIdx, mode: 'SELECT' })); - }} - onKeyDown={handleKeyDown} - toggleGroup={() => toggleGroup(key)} + isSelected={isSelected} + eventBus={eventBus} + onKeyDown={isSelected ? handleKeyDown : undefined} /> ); } diff --git a/src/EventBus.ts b/src/EventBus.ts index 7acbda57e5..5f5478d320 100644 --- a/src/EventBus.ts +++ b/src/EventBus.ts @@ -3,6 +3,8 @@ import { Position, SelectRowEvent } from './types'; interface EventMap { SELECT_CELL: (position: Position, openEditor?: boolean) => void; SELECT_ROW: (event: SelectRowEvent) => void; + SELECT_GROUP_ROW: (rowIdx: number) => void; + TOGGLE_GROUP: (expandedGroupId: unknown) => void; } type EventName = keyof EventMap; diff --git a/src/GroupRow.tsx b/src/GroupRow.tsx new file mode 100644 index 0000000000..835c2643b5 --- /dev/null +++ b/src/GroupRow.tsx @@ -0,0 +1,70 @@ +import React, { useRef, useLayoutEffect, memo } from 'react'; +import clsx from 'clsx'; +import { GroupRowRendererProps } from './types'; + +function GroupedRow({ + 'aria-rowindex': ariaRowIndex, + viewportColumns, + row, + rowIdx, + top, + width, + isSelected, + eventBus, + onKeyDown, + ...props +}: GroupRowRendererProps) { + const cellRef = useRef(null); + useLayoutEffect(() => { + if (!isSelected) return; + cellRef.current?.focus(); + }, [isSelected]); + + function selectGroup() { + eventBus.dispatch('SELECT_GROUP_ROW', rowIdx); + } + + function toggleGroup() { + eventBus.dispatch('TOGGLE_GROUP', row.key); + } + + function handleKeyDown(event: React.KeyboardEvent) { + const { key } = event; + if (['ArrowLeft', 'ArrowRight', 'Enter', ' '].includes(key)) { + event.preventDefault(); + event.stopPropagation(); + if (key === ' ' || key === 'Enter') { + toggleGroup(); + } + } + } + + return ( +
+
+ + {row.key}{' '}{row.isExpanded ? '\u25BC' : '\u25B6'} + +
+
+ ); +} + +export default memo(GroupedRow) as (props: GroupRowRendererProps) => JSX.Element; diff --git a/src/GroupedRow.tsx b/src/GroupedRow.tsx deleted file mode 100644 index 88279b00ce..0000000000 --- a/src/GroupedRow.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React, { useRef, useLayoutEffect } from 'react'; -import clsx from 'clsx'; - -type SharedDivProps = Pick, - | 'onClick' - | 'onKeyDown' - | 'aria-rowindex' ->; - -interface GroupedRowProps extends SharedDivProps { - groupKey: string; - top: number; - width: number; - columnWidth: number; - isExpanded: boolean; - isSelected: boolean; - toggleGroup: () => void; -} - -export default function GroupedRow({ - 'aria-rowindex': ariaRowIndex, - groupKey, - top, - width, - columnWidth, - isExpanded, - isSelected, - onClick, - onKeyDown, - toggleGroup -}: GroupedRowProps) { - const cellRef = useRef(null); - useLayoutEffect(() => { - if (!isSelected) return; - cellRef.current?.focus(); - }, [isSelected]); - - function handleKeyDown(e: React.KeyboardEvent) { - if (e.key === ' ' || e.key === 'Enter') { - e.preventDefault(); - toggleGroup(); - } - } - - return ( -
-
- - {groupKey}{' '}{isExpanded ? '\u25BC' : '\u25B6'} - -
-
- ); -} diff --git a/src/hooks/useViewportRows.ts b/src/hooks/useViewportRows.ts index 33b44f849f..3e5eb7ccea 100644 --- a/src/hooks/useViewportRows.ts +++ b/src/hooks/useViewportRows.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { groupBy as lodashGroupBy, Dictionary } from 'lodash'; -import { GroupedRow, GroupByDictionary } from '../types'; +import { GroupRow, GroupByDictionary } from '../types'; import { getVerticalRangeToRender } from '../utils'; interface ViewportRowsArgs { @@ -41,13 +41,13 @@ export function useViewportRows({ const [rows, totalRowCount] = useMemo(() => { if (!groupedRows) return [rawRows, rawRows.length]; - function expandGroup(groupedRows: GroupByDictionary, level: number): Array { - const flattenedRows: Array = []; - for (const key in groupedRows) { + function expandGroup(rows: GroupByDictionary, level: number): Array { + const flattenedRows: Array = []; + for (const key in rows) { const isExpanded = expandedGroupIds?.has(key) ?? false; flattenedRows.push({ key, __isGroup: true, level, isExpanded }); if (isExpanded) { - const groupedRow = groupedRows[key]; + const groupedRow = rows[key]; if (Array.isArray(groupedRow)) { flattenedRows.push(...groupedRow); } else { diff --git a/src/types.ts b/src/types.ts index 0b551481a0..e746b85bd1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -157,6 +157,16 @@ export interface RowRendererProps extends Omit void; } +export interface GroupRowRendererProps extends Omit, 'style' | 'children'> { + viewportColumns: readonly CalculatedColumn[]; + row: Readonly; + rowIdx: number; + top: number; + width: number; + isSelected: boolean; + eventBus: EventBus; +} + export interface FilterRendererProps { column: CalculatedColumn; value: TFilterValue; @@ -193,9 +203,9 @@ export interface SelectRowEvent { export type GroupByDictionary = Dictionary | Dictionary>; -export interface GroupedRow { +export interface GroupRow { __isGroup: boolean; - key: string; + key: unknown; level: number; isExpanded: boolean; } diff --git a/src/utils/index.ts b/src/utils/index.ts index dc07e6ae34..2174540bf8 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,4 @@ -import { GroupedRow } from '../types'; +import { GroupRow } from '../types'; export * from './domUtils'; export * from './columnUtils'; @@ -6,8 +6,8 @@ export * from './viewportUtils'; export * from './keyboardUtils'; export * from './selectedCellUtils'; -export function isGroupedRow(row: R | GroupedRow): row is GroupedRow { - return (row as GroupedRow).__isGroup !== undefined; +export function isGroupedRow(row: R | GroupRow): row is GroupRow { + return (row as GroupRow).__isGroup !== undefined; } export function assertIsValidKey(key: unknown): asserts key is keyof R { From 067f4e60784c76d2d43bae6590c474fa34081e26 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Fri, 7 Aug 2020 14:51:22 -0500 Subject: [PATCH 05/79] Cleanup useViewportColumns --- src/Cell.tsx | 2 + src/Columns.tsx | 4 +- src/DataGrid.tsx | 2 +- src/GroupRow.tsx | 5 ++- ...wportColumns.tsx => useViewportColumns.ts} | 43 +++++++------------ src/types.ts | 3 +- src/utils/selectedCellUtils.ts | 1 + stories/demos/CommonFeatures.tsx | 2 +- 8 files changed, 30 insertions(+), 32 deletions(-) rename src/hooks/{useViewportColumns.tsx => useViewportColumns.ts} (67%) diff --git a/src/Cell.tsx b/src/Cell.tsx index bb082ab643..1d9c3bc53f 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -85,6 +85,8 @@ function Cell({ ); } + if (column.rowGroup) return; + return ( <> = { - key: 'select-row', + key: SELECT_COLUMN_KEY, name: '', width: 35, maxWidth: 35, diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 970049a570..2d48bc0588 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -123,7 +123,7 @@ export interface DataGridProps extends Share onSort?: (columnKey: string, direction: SortDirection) => void; filters?: Filters; onFiltersChange?: (filters: Filters) => void; - groupBy?: readonly string[]; // TODO: support custom grouping logic + groupBy?: readonly string[]; // TODO: support custom grouping logic amd totals /** * Custom renderers diff --git a/src/GroupRow.tsx b/src/GroupRow.tsx index 835c2643b5..db13d50ccf 100644 --- a/src/GroupRow.tsx +++ b/src/GroupRow.tsx @@ -1,6 +1,7 @@ import React, { useRef, useLayoutEffect, memo } from 'react'; import clsx from 'clsx'; import { GroupRowRendererProps } from './types'; +import { SELECT_COLUMN_KEY } from './Columns'; function GroupedRow({ 'aria-rowindex': ariaRowIndex, @@ -39,6 +40,8 @@ function GroupedRow({ } } + const columnIndex = viewportColumns[0].key === SELECT_COLUMN_KEY ? row.level + 1 : row.level; + return (
({ >
= Pick, 'columns'> & @@ -27,35 +28,23 @@ export function useViewportColumns({ rawColumns = useMemo(() => { if (!groupBy || groupBy.length === 0) return rawColumns; - // TODO: make it generic - const selectColumn = rawColumns.find(c => c.key === 'select-row'); - const groupByColumns: Column[] = rawColumns + const selectColumn = rawColumns.find(c => c.key === SELECT_COLUMN_KEY); + const groupByColumns = rawColumns .filter(c => groupBy.includes(c.key)) - .map(f => { - // TODO: move the logic to GroupedRow - const updatedColumn: Column = { ...f }; - updatedColumn.frozen = true; - updatedColumn.formatter = (p) => { - if (isGroupedRow(p.row)) { - const F = f.formatter || defaultFormatter; - return ; - } - return null; - }; - updatedColumn.editable = row => { - return isGroupedRow(row) ? false : canEdit(updatedColumn, row); - }; - return updatedColumn; - }) + .map(c => ({ ...c, frozen: true, rowGroup: true })) .sort((c1, c2) => groupBy.findIndex(k => k === c1.key) - groupBy.findIndex(k => k === c2.key)); - - const remaningColumns = rawColumns.filter(c => !groupBy.includes(c.key) && c.key !== 'select-row'); - return [ - selectColumn!, + const remaningColumns = rawColumns.filter(c => !groupBy.includes(c.key) && c.key !== SELECT_COLUMN_KEY); + const sortedColumns = [ ...groupByColumns, ...remaningColumns ]; - }, [defaultFormatter, groupBy, rawColumns]); + + if (selectColumn) { + sortedColumns.unshift(selectColumn); + } + + return sortedColumns; + }, [groupBy, rawColumns]); const { columns, lastFrozenColumnIndex, totalColumnWidth } = useMemo(() => { return getColumnMetrics({ diff --git a/src/types.ts b/src/types.ts index e746b85bd1..83247421f9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,6 +52,7 @@ export interface CalculatedColumn extends Column>; } @@ -159,7 +160,7 @@ export interface RowRendererProps extends Omit extends Omit, 'style' | 'children'> { viewportColumns: readonly CalculatedColumn[]; - row: Readonly; + row: GroupRow; rowIdx: number; top: number; width: number; diff --git a/src/utils/selectedCellUtils.ts b/src/utils/selectedCellUtils.ts index c16833ca64..669218d95b 100644 --- a/src/utils/selectedCellUtils.ts +++ b/src/utils/selectedCellUtils.ts @@ -12,6 +12,7 @@ interface IsSelectedCellEditableOpts { export function isSelectedCellEditable({ selectedPosition, columns, rows, onCheckCellIsEditable }: IsSelectedCellEditableOpts): boolean { const column = columns[selectedPosition.idx]; const row = rows[selectedPosition.rowIdx]; + if (column.rowGroup) return false; const isCellEditable = onCheckCellIsEditable ? onCheckCellIsEditable({ row, column, ...selectedPosition }) : true; return isCellEditable && canEdit(column, row); } diff --git a/stories/demos/CommonFeatures.tsx b/stories/demos/CommonFeatures.tsx index c49537e400..f4456b02b1 100644 --- a/stories/demos/CommonFeatures.tsx +++ b/stories/demos/CommonFeatures.tsx @@ -214,7 +214,7 @@ function createRows(): readonly Row[] { return rows; } -const groupBy = ['country', 'area']; +const groupBy = ['country', 'transaction']; export default function CommonFeatures() { const [rows, setRows] = useState(createRows); From c5de19e97eb2a332ee327202cd3a740d7f5a6f58 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Fri, 14 Aug 2020 11:55:43 -0500 Subject: [PATCH 06/79] Fix keys --- src/EventBus.ts | 2 +- src/GroupRow.tsx | 2 +- src/hooks/useViewportRows.ts | 17 ++++++++++++----- src/types.ts | 8 ++++++-- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/EventBus.ts b/src/EventBus.ts index 5f5478d320..62a44c2fd1 100644 --- a/src/EventBus.ts +++ b/src/EventBus.ts @@ -4,7 +4,7 @@ interface EventMap { SELECT_CELL: (position: Position, openEditor?: boolean) => void; SELECT_ROW: (event: SelectRowEvent) => void; SELECT_GROUP_ROW: (rowIdx: number) => void; - TOGGLE_GROUP: (expandedGroupId: unknown) => void; + TOGGLE_GROUP: (id: unknown) => void; } type EventName = keyof EventMap; diff --git a/src/GroupRow.tsx b/src/GroupRow.tsx index db13d50ccf..088fff81b7 100644 --- a/src/GroupRow.tsx +++ b/src/GroupRow.tsx @@ -26,7 +26,7 @@ function GroupedRow({ } function toggleGroup() { - eventBus.dispatch('TOGGLE_GROUP', row.key); + eventBus.dispatch('TOGGLE_GROUP', row.id); } function handleKeyDown(event: React.KeyboardEvent) { diff --git a/src/hooks/useViewportRows.ts b/src/hooks/useViewportRows.ts index 3e5eb7ccea..3dd6f0fdd3 100644 --- a/src/hooks/useViewportRows.ts +++ b/src/hooks/useViewportRows.ts @@ -41,24 +41,31 @@ export function useViewportRows({ const [rows, totalRowCount] = useMemo(() => { if (!groupedRows) return [rawRows, rawRows.length]; - function expandGroup(rows: GroupByDictionary, level: number): Array { + function expandGroup(rows: GroupByDictionary, parentKey: string, level: number): Array { const flattenedRows: Array = []; for (const key in rows) { - const isExpanded = expandedGroupIds?.has(key) ?? false; - flattenedRows.push({ key, __isGroup: true, level, isExpanded }); + const id = `${parentKey}__${key}`; + const isExpanded = expandedGroupIds?.has(id) ?? false; + flattenedRows.push({ + id, + key, + level, + isExpanded, + __isGroup: true + }); if (isExpanded) { const groupedRow = rows[key]; if (Array.isArray(groupedRow)) { flattenedRows.push(...groupedRow); } else { - flattenedRows.push(...expandGroup(groupedRow, level + 1)); + flattenedRows.push(...expandGroup(groupedRow, key, level + 1)); } } } return flattenedRows; } - return [expandGroup(groupedRows, 0), 0]; + return [expandGroup(groupedRows, '', 0), 0]; }, [expandedGroupIds, groupedRows, rawRows]); const [rowOverscanStartIdx, rowOverscanEndIdx] = getVerticalRangeToRender( diff --git a/src/types.ts b/src/types.ts index 8fd63e7a19..ec33dedcb5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { KeyboardEvent } from 'react'; -import { Dictionary } from 'lodash'; import { UpdateActions } from './enums'; import EventBus from './EventBus'; @@ -204,11 +203,16 @@ export interface SelectRowEvent { isShiftClick: boolean; } +export interface Dictionary { + [index: string]: T; +} + export type GroupByDictionary = Dictionary | Dictionary>; export interface GroupRow { - __isGroup: boolean; + id: string; key: unknown; level: number; isExpanded: boolean; + __isGroup: boolean; } From 4f8e333e67434a6b7f5b658b441f3f300c27ab6f Mon Sep 17 00:00:00 2001 From: Mahajan Date: Mon, 17 Aug 2020 07:48:41 -0500 Subject: [PATCH 07/79] Add column.groupFormatter prop and checkbox column --- src/Columns.tsx | 11 ++++ src/DataGrid.tsx | 33 +++++------ src/EventBus.ts | 1 - src/GroupRow.tsx | 75 +++++++++++++------------ src/formatters/ToggleGroupFormatter.tsx | 37 ++++++++++++ src/formatters/index.ts | 1 + src/hooks/useViewportColumns.ts | 24 +++++--- src/hooks/useViewportRows.ts | 40 +++++++------ src/types.ts | 28 +++++++-- src/utils/index.ts | 4 +- stories/demos/CommonFeatures.tsx | 9 ++- style/row.less | 34 +++++++++++ style/variables.less | 1 + 13 files changed, 207 insertions(+), 91 deletions(-) create mode 100644 src/formatters/ToggleGroupFormatter.tsx diff --git a/src/Columns.tsx b/src/Columns.tsx index 70a4cc4e4b..1178aa15df 100644 --- a/src/Columns.tsx +++ b/src/Columns.tsx @@ -33,6 +33,17 @@ export const SelectColumn: Column = { /> ); }, + groupFormatter(props) { + return ( + {}} + /> + ); + }, formatterOptions: { focusable: true } diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 26f334cd00..8f199e1214 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -15,7 +15,7 @@ import EventBus from './EventBus'; import HeaderRow from './HeaderRow'; import FilterRow from './FilterRow'; import Row from './Row'; -import GroupRow from './GroupRow'; +import GroupRowRenderer from './GroupRow'; import SummaryRow from './SummaryRow'; import { legacyCellInput } from './editors'; import { @@ -36,11 +36,11 @@ import { Filters, Position, RowRendererProps, - GroupRowRendererProps, RowsUpdateEvent, SelectRowEvent, CommitEvent, - SelectedCellProps + SelectedCellProps, + Dictionary } from './types'; import { CellNavigationMode, SortDirection, UpdateActions } from './enums'; @@ -127,13 +127,13 @@ export interface DataGridProps extends Share filters?: Filters; onFiltersChange?: (filters: Filters) => void; defaultColumnOptions?: DefaultColumnOptions; - groupBy?: readonly string[]; // TODO: support custom grouping logic amd totals + groupBy?: readonly string[]; + rowGrouper?: (rows: readonly R[], columnKey: string) => Dictionary; /** * Custom renderers */ rowRenderer?: React.ComponentType>; - groupRowRenderer?: React.ComponentType>; emptyRowsRenderer?: React.ComponentType; /** @@ -197,9 +197,9 @@ function DataGrid({ onFiltersChange, defaultColumnOptions, groupBy, + rowGrouper, // Custom renderers rowRenderer: RowRenderer = Row, - groupRowRenderer: GroupRowRenderer = GroupRow, emptyRowsRenderer, // Event props onRowClick, @@ -262,8 +262,9 @@ function DataGrid({ columnWidths, scrollLeft, viewportWidth, + defaultColumnOptions, groupBy, - defaultColumnOptions + rowGrouper }); const totalHeaderHeight = headerRowHeight + (enableFilters ? headerFiltersHeight : 0); @@ -276,6 +277,7 @@ function DataGrid({ const { viewportRows, rows, startRowIdx, totalRowCount } = useViewportRows({ rawRows, groupBy, + rowGrouper, rowHeight, clientHeight, scrollTop, @@ -352,14 +354,6 @@ function DataGrid({ return eventBus.subscribe('TOGGLE_GROUP', toggleGroup); }, [eventBus, expandedGroupIds]); - useEffect(() => { - function selectGroupRow(rowIdx: number) { - const idx = selectedPosition.idx === -1 ? 0 : selectedPosition.idx; - selectCell({ idx, rowIdx }); - } - return eventBus.subscribe('SELECT_GROUP_ROW', selectGroupRow); - }); - useImperativeHandle(ref, () => ({ scrollToColumn(idx: number) { scrollToCell({ idx }); @@ -707,18 +701,19 @@ function DataGrid({ const rowIdx = startRowIdx + index; const top = rowIdx * rowHeight + totalHeaderHeight; if (isGroupedRow(row)) { - const { key } = row; const isSelected = selectedPosition.rowIdx === rowIdx; return ( selectedRows?.has(cr[rowKey!]))} eventBus={eventBus} onKeyDown={isSelected ? handleKeyDown : undefined} /> diff --git a/src/EventBus.ts b/src/EventBus.ts index 62a44c2fd1..e8fe095733 100644 --- a/src/EventBus.ts +++ b/src/EventBus.ts @@ -3,7 +3,6 @@ import { Position, SelectRowEvent } from './types'; interface EventMap { SELECT_CELL: (position: Position, openEditor?: boolean) => void; SELECT_ROW: (event: SelectRowEvent) => void; - SELECT_GROUP_ROW: (rowIdx: number) => void; TOGGLE_GROUP: (id: unknown) => void; } diff --git a/src/GroupRow.tsx b/src/GroupRow.tsx index 088fff81b7..7ac2ba5ea6 100644 --- a/src/GroupRow.tsx +++ b/src/GroupRow.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useLayoutEffect, memo } from 'react'; +import React, { memo } from 'react'; import clsx from 'clsx'; import { GroupRowRendererProps } from './types'; import { SELECT_COLUMN_KEY } from './Columns'; @@ -8,64 +8,65 @@ function GroupedRow({ viewportColumns, row, rowIdx, + lastFrozenColumnIndex, top, - width, - isSelected, + isCellSelected, + isRowSelected, + groupBy, eventBus, onKeyDown, ...props }: GroupRowRendererProps) { - const cellRef = useRef(null); - useLayoutEffect(() => { - if (!isSelected) return; - cellRef.current?.focus(); - }, [isSelected]); + const level = viewportColumns[0].key === SELECT_COLUMN_KEY ? row.level + 1 : row.level; function selectGroup() { - eventBus.dispatch('SELECT_GROUP_ROW', rowIdx); + eventBus.dispatch('SELECT_CELL', { rowIdx, idx: level }); } function toggleGroup() { eventBus.dispatch('TOGGLE_GROUP', row.id); } - function handleKeyDown(event: React.KeyboardEvent) { - const { key } = event; - if (['ArrowLeft', 'ArrowRight', 'Enter', ' '].includes(key)) { - event.preventDefault(); - event.stopPropagation(); - if (key === ' ' || key === 'Enter') { - toggleGroup(); - } - } - } - - const columnIndex = viewportColumns[0].key === SELECT_COLUMN_KEY ? row.level + 1 : row.level; return (
-
- ( +
- {row.key}{' '}{row.isExpanded ? '\u25BC' : '\u25B6'} - -
+ {column.groupFormatter && (groupBy.includes(column.key) ? level === column.idx : true) && ( + + )} +
+ ))}
); } diff --git a/src/formatters/ToggleGroupFormatter.tsx b/src/formatters/ToggleGroupFormatter.tsx new file mode 100644 index 0000000000..0d15437984 --- /dev/null +++ b/src/formatters/ToggleGroupFormatter.tsx @@ -0,0 +1,37 @@ +import React, { useRef, useLayoutEffect } from 'react'; +import { GroupFormatterProps } from '../types'; + +export function ToggleGroupedFormatter({ + row, + isCellSelected, + toggleGroup +}: GroupFormatterProps) { + const cellRef = useRef(null); + useLayoutEffect(() => { + if (!isCellSelected) return; + cellRef.current?.focus(); + }, [isCellSelected]); + + function handleKeyDown(event: React.KeyboardEvent) { + const { key } = event; + if (['ArrowLeft', 'ArrowRight', 'Enter', ' '].includes(key)) { + event.preventDefault(); + event.stopPropagation(); + if (key === ' ' || key === 'Enter') { + toggleGroup(); + } + } + } + + return ( + + {row.key}{' '}{row.isExpanded ? '\u25BC' : '\u25B6'} + + ); +} diff --git a/src/formatters/index.ts b/src/formatters/index.ts index 998b261e06..e4af276e17 100644 --- a/src/formatters/index.ts +++ b/src/formatters/index.ts @@ -1,3 +1,4 @@ export * from './SelectCellFormatter'; export * from './SimpleCellFormatter'; export * from './ValueFormatter'; +export * from './ToggleGroupFormatter'; diff --git a/src/hooks/useViewportColumns.ts b/src/hooks/useViewportColumns.ts index 6f09829a25..76cb1615fd 100644 --- a/src/hooks/useViewportColumns.ts +++ b/src/hooks/useViewportColumns.ts @@ -4,15 +4,19 @@ import { CalculatedColumn } from '../types'; import { getColumnMetrics, getHorizontalRangeToRender, getViewportColumns } from '../utils'; import { DataGridProps } from '../DataGrid'; import { SELECT_COLUMN_KEY } from '../Columns'; -import { ValueFormatter } from '../formatters'; +import { ValueFormatter, ToggleGroupedFormatter } from '../formatters'; -type SharedDataGridProps = Pick, 'columns' | 'defaultColumnOptions'>; +type SharedDataGridProps = Pick, + | 'columns' + | 'defaultColumnOptions' + | 'groupBy' + | 'rowGrouper' +>; interface ViewportColumnsArgs extends SharedDataGridProps { viewportWidth: number; scrollLeft: number; columnWidths: ReadonlyMap; - groupBy?: readonly string[]; } export function useViewportColumns({ @@ -21,15 +25,21 @@ export function useViewportColumns({ viewportWidth, scrollLeft, defaultColumnOptions, - groupBy + groupBy, + rowGrouper }: ViewportColumnsArgs) { rawColumns = useMemo(() => { - if (!groupBy || groupBy.length === 0) return rawColumns; + if (!groupBy || groupBy.length === 0 || !rowGrouper) return rawColumns; const selectColumn = rawColumns.find(c => c.key === SELECT_COLUMN_KEY); const groupByColumns = rawColumns .filter(c => groupBy.includes(c.key)) - .map(c => ({ ...c, frozen: true, rowGroup: true })) + .map(c => ({ + ...c, + frozen: true, + rowGroup: true, + groupFormatter: c.groupFormatter ?? ToggleGroupedFormatter + })) .sort((c1, c2) => groupBy.findIndex(k => k === c1.key) - groupBy.findIndex(k => k === c2.key)); const remaningColumns = rawColumns.filter(c => !groupBy.includes(c.key) && c.key !== SELECT_COLUMN_KEY); const sortedColumns = [ @@ -42,7 +52,7 @@ export function useViewportColumns({ } return sortedColumns; - }, [groupBy, rawColumns]); + }, [groupBy, rowGrouper, rawColumns]); const minColumnWidth = defaultColumnOptions?.minWidth ?? 80; const defaultFormatter = defaultColumnOptions?.formatter ?? ValueFormatter; diff --git a/src/hooks/useViewportRows.ts b/src/hooks/useViewportRows.ts index 3dd6f0fdd3..e4f4d27fbc 100644 --- a/src/hooks/useViewportRows.ts +++ b/src/hooks/useViewportRows.ts @@ -1,7 +1,6 @@ import { useMemo } from 'react'; -import { groupBy as lodashGroupBy, Dictionary } from 'lodash'; -import { GroupRow, GroupByDictionary } from '../types'; +import { GroupRow, GroupByDictionary, Dictionary } from '../types'; import { getVerticalRangeToRender } from '../utils'; interface ViewportRowsArgs { @@ -10,6 +9,7 @@ interface ViewportRowsArgs { clientHeight: number; scrollTop: number; groupBy?: readonly string[]; + rowGrouper?: (rows: readonly R[], columnKey: string) => Dictionary; expandedGroupIds?: Set; } @@ -19,46 +19,52 @@ export function useViewportRows({ clientHeight, scrollTop, groupBy, + rowGrouper, expandedGroupIds }: ViewportRowsArgs) { const groupedRows = useMemo(() => { - if (!groupBy || groupBy.length === 0) return; + if (!groupBy || groupBy.length === 0 || !rowGrouper) return; - function groupParent(rows: readonly R[], [groupByKey, ...remainingGroupByKeys]: readonly string[]) { - const parentGroup = lodashGroupBy(rows, groupByKey); + const groupRows = (rows: readonly R[], [groupByKey, ...remainingGroupByKeys]: readonly string[]) => { + const parentGroup = rowGrouper(rows, groupByKey); if (remainingGroupByKeys.length === 0) return parentGroup; - const childGroups: Dictionary> = {}; + const childGroups: GroupByDictionary = {}; for (const key in parentGroup) { - childGroups[key] = groupParent(parentGroup[key], remainingGroupByKeys); + const childRows = parentGroup[key]; + childGroups[key] = { + rows: childRows, + groups: groupRows(childRows, remainingGroupByKeys) + }; } return childGroups; - } + }; - return groupParent(rawRows, groupBy); - }, [groupBy, rawRows]); + return groupRows(rawRows, groupBy); + }, [groupBy, rowGrouper, rawRows]); - const [rows, totalRowCount] = useMemo(() => { + const [rows, totalRowCount] = useMemo(() => { // TODO: fix totalRowCount if (!groupedRows) return [rawRows, rawRows.length]; - function expandGroup(rows: GroupByDictionary, parentKey: string, level: number): Array { - const flattenedRows: Array = []; + function expandGroup(rows: GroupByDictionary, parentKey: string, level: number): Array | R> { + const flattenedRows: Array> = []; for (const key in rows) { const id = `${parentKey}__${key}`; const isExpanded = expandedGroupIds?.has(id) ?? false; + const group = rows[key]; flattenedRows.push({ id, key, level, isExpanded, + childRows: Array.isArray(group) ? group : group.rows, __isGroup: true }); if (isExpanded) { - const groupedRow = rows[key]; - if (Array.isArray(groupedRow)) { - flattenedRows.push(...groupedRow); + if (Array.isArray(group)) { + flattenedRows.push(...group); } else { - flattenedRows.push(...expandGroup(groupedRow, key, level + 1)); + flattenedRows.push(...expandGroup(group.groups, key, level + 1)); } } } diff --git a/src/types.ts b/src/types.ts index ec33dedcb5..b84efd6d33 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,6 +26,8 @@ export interface Column { }; /** Formatter to be used to render the summary cell content */ summaryFormatter?: React.ComponentType>; + /** Formatter to be used to render the group cell content */ + groupFormatter?: React.ComponentType>; /** Enables cell editing. If set and no editor property specified, then a textinput will be used as the cell editor */ editable?: boolean | ((row: TRow) => boolean); /** Determines whether column is frozen or not */ @@ -85,6 +87,14 @@ export interface SummaryFormatterProps { row: TSummaryRow; } +export interface GroupFormatterProps { + column: CalculatedColumn; + row: GroupRow; + isCellSelected: boolean; + isRowSelected: boolean; + toggleGroup: () => void; +} + export interface EditorProps { ref: React.Ref>; column: CalculatedColumn; @@ -161,11 +171,13 @@ export interface RowRendererProps extends Omit extends Omit, 'style' | 'children'> { viewportColumns: readonly CalculatedColumn[]; - row: GroupRow; + row: GroupRow; rowIdx: number; + lastFrozenColumnIndex: number; + groupBy: readonly string[]; top: number; - width: number; - isSelected: boolean; + isCellSelected: boolean; + isRowSelected: boolean; eventBus: EventBus; } @@ -207,12 +219,16 @@ export interface Dictionary { [index: string]: T; } -export type GroupByDictionary = Dictionary | Dictionary>; +export type GroupByDictionary = Dictionary | Dictionary<{ + rows: TRow[]; + groups: GroupByDictionary; +}>; -export interface GroupRow { +export interface GroupRow { + __isGroup: true; + childRows: TRow[]; id: string; key: unknown; level: number; isExpanded: boolean; - __isGroup: boolean; } diff --git a/src/utils/index.ts b/src/utils/index.ts index 2174540bf8..77631834c3 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -6,8 +6,8 @@ export * from './viewportUtils'; export * from './keyboardUtils'; export * from './selectedCellUtils'; -export function isGroupedRow(row: R | GroupRow): row is GroupRow { - return (row as GroupRow).__isGroup !== undefined; +export function isGroupedRow(row: R | GroupRow): row is GroupRow { + return (row as GroupRow).__isGroup !== undefined; } export function assertIsValidKey(key: unknown): asserts key is keyof R { diff --git a/stories/demos/CommonFeatures.tsx b/stories/demos/CommonFeatures.tsx index 7d274a0bb7..fe5e7f82e8 100644 --- a/stories/demos/CommonFeatures.tsx +++ b/stories/demos/CommonFeatures.tsx @@ -1,6 +1,7 @@ import React, { useState, useCallback, useMemo } from 'react'; import faker from 'faker'; import { AutoSizer } from 'react-virtualized'; +import { groupBy as rowGrouper } from 'lodash'; import DataGrid, { SelectColumn, Column, RowsUpdateEvent, SortDirection } from '../../src'; const dateFormatter = new Intl.DateTimeFormat(navigator.language); @@ -128,6 +129,10 @@ const columns: readonly Column[] = [ width: 100, formatter(props) { return ; + }, + groupFormatter({ row }) { + const totals = row.childRows.reduce((prev, { budget }) => prev + budget, 0); + return ; } }, { @@ -186,12 +191,11 @@ function createRows(): readonly Row[] { return rows; } -const groupBy = ['country', 'transaction']; - export default function CommonFeatures() { const [rows, setRows] = useState(createRows); const [[sortColumn, sortDirection], setSort] = useState<[string, SortDirection]>(['id', 'NONE']); const [selectedRows, setSelectedRows] = useState(() => new Set()); + const [groupBy] = useState(['country', 'transaction']); const summaryRows = useMemo(() => { const summaryRow: SummaryRow = { id: 'total_0', totalCount: rows.length, yesCount: rows.filter(r => r.available).length }; @@ -266,6 +270,7 @@ export default function CommonFeatures() { onSort={handleSort} summaryRows={summaryRows} groupBy={groupBy} + rowGrouper={rowGrouper} /> )} diff --git a/style/row.less b/style/row.less index 578ba1762e..65c0f15fd7 100644 --- a/style/row.less +++ b/style/row.less @@ -32,3 +32,37 @@ border-top: 2px solid darken(@borderColor, 20%); } } + +.rdg-group-row { + > .rdg-cell { + border-right-width: 0; + } + + > .rdg-cell:last-child, + > .rdg-cell-frozen-last { + border-right-width: 1px; + } +} + +.rdg-group-row-selected { + .rdg-cell { + box-shadow: + inset 0 2px 0 0 @selectedBorderColor, + inset 0 -2px 0 0 @selectedBorderColor; + } + + .rdg-cell:first-child { + box-shadow: + inset 0 2px 0 0 @selectedBorderColor, + inset 0 -2px 0 0 @selectedBorderColor, + inset 2px 0 0 0 @selectedBorderColor; + } + + .rdg-cell:last-child { + box-shadow: + inset 0 2px 0 0 @selectedBorderColor, + inset 0 -2px 0 0 @selectedBorderColor, + inset -2px 0 0 0 @selectedBorderColor; + } +} + diff --git a/style/variables.less b/style/variables.less index 782b5711c6..2290b003f6 100644 --- a/style/variables.less +++ b/style/variables.less @@ -1,4 +1,5 @@ @borderColor: #ddd; +@selectedBorderColor: #66afe9; .user-select(@value: none) { -webkit-user-select: @value; From 93f3a970f16970143e9bc141fe7015e502409e82 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Mon, 17 Aug 2020 13:10:07 -0500 Subject: [PATCH 08/79] Checkbox selection for group row --- src/Columns.tsx | 2 +- src/DataGrid.tsx | 37 +++++++++++++++++++------------- src/GroupRow.tsx | 4 ++++ src/types.ts | 1 + stories/demos/CommonFeatures.tsx | 2 +- 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/Columns.tsx b/src/Columns.tsx index 1178aa15df..9b974de505 100644 --- a/src/Columns.tsx +++ b/src/Columns.tsx @@ -40,7 +40,7 @@ export const SelectColumn: Column = { tabIndex={-1} isCellSelected={props.isCellSelected} value={props.isRowSelected} - onChange={() => {}} + onChange={props.onRowSelectionChange} /> ); }, diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 9c3ec4b0b6..e8d1bc112f 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -315,25 +315,32 @@ function DataGrid({ assertIsValidKey(rowKey); const newSelectedRows = new Set(selectedRows); const row = rows[rowIdx]; - if (isGroupedRow(row)) return; // TODO: add a checkbox to select the group - const rowId = row[rowKey]; - - if (checked) { - newSelectedRows.add(rowId); - const previousRowIdx = lastSelectedRowIdx.current; - lastSelectedRowIdx.current = rowIdx; - if (isShiftClick && previousRowIdx !== -1 && previousRowIdx !== rowIdx) { - const step = Math.sign(rowIdx - previousRowIdx); - for (let i = previousRowIdx + step; i !== rowIdx; i += step) { - const row = rows[i]; - if (!isGroupedRow(row)) { + if (isGroupedRow(row)) { + for (const childRow of row.childRows) { + if (checked) { + newSelectedRows.add(childRow[rowKey]); + } else { + newSelectedRows.delete(childRow[rowKey]); + } + } + } else { + const rowId = row[rowKey]; + if (checked) { + newSelectedRows.add(rowId); + const previousRowIdx = lastSelectedRowIdx.current; + lastSelectedRowIdx.current = rowIdx; + if (isShiftClick && previousRowIdx !== -1 && previousRowIdx !== rowIdx) { + const step = Math.sign(rowIdx - previousRowIdx); + for (let i = previousRowIdx + step; i !== rowIdx; i += step) { + const row = rows[i]; + if (isGroupedRow(row)) continue; newSelectedRows.add(row[rowKey]); } } + } else { + newSelectedRows.delete(rowId); + lastSelectedRowIdx.current = -1; } - } else { - newSelectedRows.delete(rowId); - lastSelectedRowIdx.current = -1; } onSelectedRowsChange(newSelectedRows); diff --git a/src/GroupRow.tsx b/src/GroupRow.tsx index 7ac2ba5ea6..5a7e9dac53 100644 --- a/src/GroupRow.tsx +++ b/src/GroupRow.tsx @@ -27,6 +27,9 @@ function GroupedRow({ eventBus.dispatch('TOGGLE_GROUP', row.id); } + function onRowSelectionChange(checked: boolean) { + eventBus.dispatch('SELECT_ROW', { rowIdx, checked, isShiftClick: false }); + } return (
({ column={column} isCellSelected={isCellSelected} isRowSelected={isRowSelected} + onRowSelectionChange={onRowSelectionChange} toggleGroup={toggleGroup} /> )} diff --git a/src/types.ts b/src/types.ts index 4bfda8b023..8cfedb9b70 100644 --- a/src/types.ts +++ b/src/types.ts @@ -102,6 +102,7 @@ export interface GroupFormatterProps { row: GroupRow; isCellSelected: boolean; isRowSelected: boolean; + onRowSelectionChange: (checked: boolean) => void; toggleGroup: () => void; } diff --git a/stories/demos/CommonFeatures.tsx b/stories/demos/CommonFeatures.tsx index afe9261aec..a0f118bb8d 100644 --- a/stories/demos/CommonFeatures.tsx +++ b/stories/demos/CommonFeatures.tsx @@ -213,7 +213,7 @@ export default function CommonFeatures() { const [rows, setRows] = useState(createRows); const [[sortColumn, sortDirection], setSort] = useState<[string, SortDirection]>(['id', 'NONE']); const [selectedRows, setSelectedRows] = useState(() => new Set()); - const [groupBy] = useState(['transaction']); + const [groupBy] = useState(['country', 'transaction']); const countries = useMemo(() => { return [...new Set(rows.map(r => r.country))].sort(); From 1d609252b5c63f47f89460809af2156e13bbb70c Mon Sep 17 00:00:00 2001 From: Mahajan Date: Mon, 17 Aug 2020 14:40:37 -0500 Subject: [PATCH 09/79] Validate groupBy columns --- src/DataGrid.tsx | 10 +++++----- src/hooks/useViewportColumns.ts | 22 ++++++++++++++-------- src/hooks/useViewportRows.ts | 15 +++++++++------ 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index e8d1bc112f..d4012664e8 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -202,7 +202,7 @@ function DataGrid({ filters, onFiltersChange, defaultColumnOptions, - groupBy, + groupBy: rawGroupBy, rowGrouper, // Custom renderers rowRenderer: RowRenderer = Row, @@ -263,13 +263,13 @@ function DataGrid({ const summaryRowsCount = summaryRows?.length ?? 0; const isSelectable = selectedRows !== undefined && onSelectedRowsChange !== undefined; - const { columns, viewportColumns, totalColumnWidth, lastFrozenColumnIndex } = useViewportColumns({ + const { columns, viewportColumns, totalColumnWidth, lastFrozenColumnIndex, groupBy } = useViewportColumns({ columns: rawColumns, columnWidths, scrollLeft, viewportWidth, defaultColumnOptions, - groupBy, + groupBy: rawGroupBy, rowGrouper }); @@ -435,7 +435,7 @@ function DataGrid({ }, [columnWidths, onColumnResize]); function getRawRowIdx(rowIdx: number) { - return groupBy ? rawRows.indexOf(rows[rowIdx] as R) : rowIdx; + return groupBy.length > 0 && rowGrouper ? rawRows.indexOf(rows[rowIdx] as R) : rowIdx; } function handleCommit({ cellKey, rowIdx, updated }: CommitEvent) { @@ -790,7 +790,7 @@ function DataGrid({ row={row} rowIdx={rowIdx} lastFrozenColumnIndex={lastFrozenColumnIndex} - groupBy={groupBy!} + groupBy={groupBy} top={top} isCellSelected={isSelected} isRowSelected={row.childRows.every(cr => selectedRows?.has(cr[rowKey!]))} diff --git a/src/hooks/useViewportColumns.ts b/src/hooks/useViewportColumns.ts index 76cb1615fd..a16464c36f 100644 --- a/src/hooks/useViewportColumns.ts +++ b/src/hooks/useViewportColumns.ts @@ -25,13 +25,19 @@ export function useViewportColumns({ viewportWidth, scrollLeft, defaultColumnOptions, - groupBy, + groupBy: rawGroupBy, rowGrouper }: ViewportColumnsArgs) { - rawColumns = useMemo(() => { - if (!groupBy || groupBy.length === 0 || !rowGrouper) return rawColumns; + const [sortedColumns, groupBy] = useMemo(() => { + if (!rawGroupBy || !rowGrouper) return [rawColumns, []]; + + // Find valid groupBy columns + const groupBy: readonly string[] = rawGroupBy.filter(g => rawColumns.find(c => c.key === g) !== undefined); + if (groupBy.length === 0) return [rawColumns, groupBy]; const selectColumn = rawColumns.find(c => c.key === SELECT_COLUMN_KEY); + + // Move group columns after the select column const groupByColumns = rawColumns .filter(c => groupBy.includes(c.key)) .map(c => ({ @@ -51,8 +57,8 @@ export function useViewportColumns({ sortedColumns.unshift(selectColumn); } - return sortedColumns; - }, [groupBy, rowGrouper, rawColumns]); + return [sortedColumns, groupBy]; + }, [rawColumns, rawGroupBy, rowGrouper]); const minColumnWidth = defaultColumnOptions?.minWidth ?? 80; const defaultFormatter = defaultColumnOptions?.formatter ?? ValueFormatter; @@ -61,7 +67,7 @@ export function useViewportColumns({ const { columns, lastFrozenColumnIndex, totalColumnWidth } = useMemo(() => { return getColumnMetrics({ - columns: rawColumns, + columns: sortedColumns, minColumnWidth, viewportWidth, columnWidths, @@ -69,7 +75,7 @@ export function useViewportColumns({ defaultResizable, defaultFormatter }); - }, [columnWidths, defaultFormatter, defaultResizable, defaultSortable, minColumnWidth, rawColumns, viewportWidth]); + }, [columnWidths, defaultFormatter, defaultResizable, defaultSortable, minColumnWidth, sortedColumns, viewportWidth]); const [colOverscanStartIdx, colOverscanEndIdx] = useMemo((): [number, number] => { return getHorizontalRangeToRender( @@ -88,5 +94,5 @@ export function useViewportColumns({ ); }, [colOverscanEndIdx, colOverscanStartIdx, columns]); - return { columns, viewportColumns, totalColumnWidth, lastFrozenColumnIndex }; + return { columns, viewportColumns, totalColumnWidth, lastFrozenColumnIndex, groupBy }; } diff --git a/src/hooks/useViewportRows.ts b/src/hooks/useViewportRows.ts index e4f4d27fbc..d98f904230 100644 --- a/src/hooks/useViewportRows.ts +++ b/src/hooks/useViewportRows.ts @@ -8,7 +8,7 @@ interface ViewportRowsArgs { rowHeight: number; clientHeight: number; scrollTop: number; - groupBy?: readonly string[]; + groupBy: readonly string[]; rowGrouper?: (rows: readonly R[], columnKey: string) => Dictionary; expandedGroupIds?: Set; } @@ -23,11 +23,13 @@ export function useViewportRows({ expandedGroupIds }: ViewportRowsArgs) { const groupedRows = useMemo(() => { - if (!groupBy || groupBy.length === 0 || !rowGrouper) return; + if (groupBy.length === 0 || !rowGrouper) return; const groupRows = (rows: readonly R[], [groupByKey, ...remainingGroupByKeys]: readonly string[]) => { const parentGroup = rowGrouper(rows, groupByKey); if (remainingGroupByKeys.length === 0) return parentGroup; + + // Recursively group each parent group const childGroups: GroupByDictionary = {}; for (const key in parentGroup) { const childRows = parentGroup[key]; @@ -46,10 +48,10 @@ export function useViewportRows({ const [rows, totalRowCount] = useMemo(() => { // TODO: fix totalRowCount if (!groupedRows) return [rawRows, rawRows.length]; - function expandGroup(rows: GroupByDictionary, parentKey: string, level: number): Array | R> { + const expandGroup = (rows: GroupByDictionary, parentKey: string | undefined, level: number): Array | R> => { const flattenedRows: Array> = []; for (const key in rows) { - const id = `${parentKey}__${key}`; + const id = parentKey !== undefined ? `${parentKey}__${key}` : key; const isExpanded = expandedGroupIds?.has(id) ?? false; const group = rows[key]; flattenedRows.push({ @@ -70,8 +72,9 @@ export function useViewportRows({ } return flattenedRows; - } - return [expandGroup(groupedRows, '', 0), 0]; + }; + + return [expandGroup(groupedRows, undefined, 0), 0]; }, [expandedGroupIds, groupedRows, rawRows]); const [rowOverscanStartIdx, rowOverscanEndIdx] = getVerticalRangeToRender( From cd46e7421f778974bb247cdad50067f85d6aa263 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Mon, 17 Aug 2020 15:39:04 -0500 Subject: [PATCH 10/79] Ckeck isSelectable --- src/DataGrid.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index d4012664e8..9ca4f883f7 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -793,7 +793,7 @@ function DataGrid({ groupBy={groupBy} top={top} isCellSelected={isSelected} - isRowSelected={row.childRows.every(cr => selectedRows?.has(cr[rowKey!]))} + isRowSelected={isSelectable && row.childRows.every(cr => selectedRows?.has(cr[rowKey!]))} eventBus={eventBus} onKeyDown={isSelected ? handleKeyDown : undefined} /> From f82ec68c2c46ee701be87b0fcd64aa7518b46c22 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Mon, 17 Aug 2020 16:25:36 -0500 Subject: [PATCH 11/79] ColSpan --- src/GroupRow.tsx | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/GroupRow.tsx b/src/GroupRow.tsx index 5a7e9dac53..eb56b62d34 100644 --- a/src/GroupRow.tsx +++ b/src/GroupRow.tsx @@ -1,6 +1,6 @@ import React, { memo } from 'react'; import clsx from 'clsx'; -import { GroupRowRendererProps } from './types'; +import { GroupRowRendererProps, CalculatedColumn } from './types'; import { SELECT_COLUMN_KEY } from './Columns'; function GroupedRow({ @@ -17,10 +17,12 @@ function GroupedRow({ onKeyDown, ...props }: GroupRowRendererProps) { - const level = viewportColumns[0].key === SELECT_COLUMN_KEY ? row.level + 1 : row.level; + const { level } = row; + // Select is always the first column + const idx = viewportColumns[0].key === SELECT_COLUMN_KEY ? level + 1 : level; function selectGroup() { - eventBus.dispatch('SELECT_CELL', { rowIdx, idx: level }); + eventBus.dispatch('SELECT_CELL', { rowIdx, idx }); } function toggleGroup() { @@ -31,6 +33,18 @@ function GroupedRow({ eventBus.dispatch('SELECT_ROW', { rowIdx, checked, isShiftClick: false }); } + // Expand groupBy column widths + const visibleColumns: CalculatedColumn[] = [...viewportColumns]; + visibleColumns[idx] = { ...visibleColumns[idx] }; + let colSpan = 0; + for (let i = idx + 1; i < visibleColumns.length; i++) { + const nextColumn = visibleColumns[i]; + if (!nextColumn.frozen || (nextColumn.groupFormatter && !groupBy.includes(nextColumn.key))) break; + visibleColumns[idx].width += nextColumn.width; + colSpan++; + } + visibleColumns.splice(idx + 1, colSpan); + return (
({ style={{ top }} {...props} > - {viewportColumns.map(column => ( + {visibleColumns.map(column => (
({ left: column.left }} > - {column.groupFormatter && (groupBy.includes(column.key) ? level === column.idx : true) && ( + {column.groupFormatter && (groupBy.includes(column.key) ? idx === column.idx : true) && ( Date: Tue, 18 Aug 2020 08:12:02 -0500 Subject: [PATCH 12/79] Disable copy/paste and cell drag down on treegrid --- src/DataGrid.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 9ca4f883f7..48fae6c16a 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -290,6 +290,14 @@ function DataGrid({ expandedGroupIds }); + const hasGroups = groupBy.length > 0 && rowGrouper; + + if (hasGroups) { + // TODO: finalize if these flags need to be supported on treegrid + enableCellDragAndDrop = false; + enableCellDragAndDrop = false; + } + /** * effects */ @@ -435,7 +443,7 @@ function DataGrid({ }, [columnWidths, onColumnResize]); function getRawRowIdx(rowIdx: number) { - return groupBy.length > 0 && rowGrouper ? rawRows.indexOf(rows[rowIdx] as R) : rowIdx; + return hasGroups ? rawRows.indexOf(rows[rowIdx] as R) : rowIdx; } function handleCommit({ cellKey, rowIdx, updated }: CommitEvent) { @@ -579,7 +587,7 @@ function DataGrid({ const column = columns[selectedPosition.idx]; const cellKey = column.key; - const value = rawRows[selectedPosition.rowIdx][cellKey as keyof R]; // TODO: handle grouping + const value = rawRows[selectedPosition.rowIdx][cellKey as keyof R]; onRowsUpdate?.({ cellKey, From 8f06cb8b13b04c99ad1565a61e7752420bd75303 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Tue, 18 Aug 2020 10:09:07 -0500 Subject: [PATCH 13/79] Add treegrid aria attributes --- src/DataGrid.tsx | 4 ++-- src/GroupRow.tsx | 6 +++++- src/hooks/useViewportRows.ts | 10 ++++++---- src/types.ts | 4 +++- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 48fae6c16a..3d4ae5ecd9 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -855,13 +855,13 @@ function DataGrid({ return (
({ }: GroupRowRendererProps) { const { level } = row; // Select is always the first column - const idx = viewportColumns[0].key === SELECT_COLUMN_KEY ? level + 1 : level; + const idx = viewportColumns[0].key === SELECT_COLUMN_KEY ? level : level - 1; // aria-level is 1-based function selectGroup() { eventBus.dispatch('SELECT_CELL', { rowIdx, idx }); @@ -48,6 +48,10 @@ function GroupedRow({ return (
({ const expandGroup = (rows: GroupByDictionary, parentKey: string | undefined, level: number): Array | R> => { const flattenedRows: Array> = []; - for (const key in rows) { + Object.keys(rows).forEach((key, index, keys) => { const id = parentKey !== undefined ? `${parentKey}__${key}` : key; const isExpanded = expandedGroupIds?.has(id) ?? false; const group = rows[key]; flattenedRows.push({ id, key, - level, isExpanded, childRows: Array.isArray(group) ? group : group.rows, + level, + setSize: keys.length, + posInSet: index + 1, // aria-posinset is 1-based __isGroup: true }); if (isExpanded) { @@ -69,12 +71,12 @@ export function useViewportRows({ flattenedRows.push(...expandGroup(group.groups, key, level + 1)); } } - } + }); return flattenedRows; }; - return [expandGroup(groupedRows, undefined, 0), 0]; + return [expandGroup(groupedRows, undefined, 1), 0]; // aria-level is 1-based }, [expandedGroupIds, groupedRows, rawRows]); const [rowOverscanStartIdx, rowOverscanEndIdx] = getVerticalRangeToRender( diff --git a/src/types.ts b/src/types.ts index 8cfedb9b70..5e8e9ef4fa 100644 --- a/src/types.ts +++ b/src/types.ts @@ -255,6 +255,8 @@ export interface GroupRow { childRows: TRow[]; id: string; key: unknown; - level: number; isExpanded: boolean; + level: number; + setSize: number; + posInSet: number; } From 25e2269f9d88dd3d73119c3471e10685bfafc913 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Tue, 18 Aug 2020 10:41:03 -0500 Subject: [PATCH 14/79] Fix rowsCount --- src/DataGrid.tsx | 6 +++--- src/GroupRow.tsx | 2 +- src/hooks/useViewportRows.ts | 17 ++++++++++------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 3d4ae5ecd9..46b976bc93 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -280,7 +280,7 @@ function DataGrid({ - summaryRowsCount * rowHeight - (totalColumnWidth > viewportWidth ? getScrollbarSize() : 0); - const { viewportRows, rows, startRowIdx, totalRowCount } = useViewportRows({ + const { viewportRows, rows, startRowIdx, rowsCount } = useViewportRows({ rawRows, groupBy, rowGrouper, @@ -861,7 +861,7 @@ function DataGrid({ aria-describedby={ariaDescribedBy} aria-multiselectable={isSelectable ? true : undefined} aria-colcount={columns.length} - aria-rowcount={headerRowsCount + rows.length + summaryRowsCount} // TODO: fix rowcount + aria-rowcount={headerRowsCount + rowsCount + summaryRowsCount} className={clsx('rdg', { 'rdg-viewport-dragging': isDragging })} style={{ width, @@ -906,7 +906,7 @@ function DataGrid({ {getViewportRows()} {summaryRows?.map((row, rowIdx) => ( - aria-rowindex={headerRowsCount + totalRowCount + rowIdx + 1} + aria-rowindex={headerRowsCount + rowsCount + rowIdx + 1} key={rowIdx} rowIdx={rowIdx} row={row} diff --git a/src/GroupRow.tsx b/src/GroupRow.tsx index d08483d560..e495244c76 100644 --- a/src/GroupRow.tsx +++ b/src/GroupRow.tsx @@ -49,7 +49,7 @@ function GroupedRow({
({ rowGrouper, expandedGroupIds }: ViewportRowsArgs) { - const groupedRows = useMemo(() => { - if (groupBy.length === 0 || !rowGrouper) return; + const [groupedRows, rowsCount] = useMemo(() => { + if (groupBy.length === 0 || !rowGrouper) return [undefined, rawRows.length]; + let rowsCount = 0; const groupRows = (rows: readonly R[], [groupByKey, ...remainingGroupByKeys]: readonly string[]) => { const parentGroup = rowGrouper(rows, groupByKey); + rowsCount += Object.keys(parentGroup).length; if (remainingGroupByKeys.length === 0) return parentGroup; // Recursively group each parent group const childGroups: GroupByDictionary = {}; for (const key in parentGroup) { const childRows = parentGroup[key]; + rowsCount += childRows.length; childGroups[key] = { rows: childRows, groups: groupRows(childRows, remainingGroupByKeys) @@ -42,11 +45,11 @@ export function useViewportRows({ return childGroups; }; - return groupRows(rawRows, groupBy); + return [groupRows(rawRows, groupBy), rowsCount]; }, [groupBy, rowGrouper, rawRows]); - const [rows, totalRowCount] = useMemo(() => { // TODO: fix totalRowCount - if (!groupedRows) return [rawRows, rawRows.length]; + const rows = useMemo(() => { + if (!groupedRows) return rawRows; const expandGroup = (rows: GroupByDictionary, parentKey: string | undefined, level: number): Array | R> => { const flattenedRows: Array> = []; @@ -76,7 +79,7 @@ export function useViewportRows({ return flattenedRows; }; - return [expandGroup(groupedRows, undefined, 1), 0]; // aria-level is 1-based + return expandGroup(groupedRows, undefined, 1); // aria-level is 1-based }, [expandedGroupIds, groupedRows, rawRows]); const [rowOverscanStartIdx, rowOverscanEndIdx] = getVerticalRangeToRender( @@ -92,6 +95,6 @@ export function useViewportRows({ viewportRows, rows, startRowIdx: rowOverscanStartIdx, - totalRowCount + rowsCount }; } From 2d856dc460240e4d996e530c654c488042a56d1a Mon Sep 17 00:00:00 2001 From: Mahajan Date: Tue, 18 Aug 2020 16:07:19 -0500 Subject: [PATCH 15/79] Fix rowsCount --- src/DataGrid.tsx | 2 +- src/formatters/ToggleGroupFormatter.tsx | 4 +-- src/hooks/useViewportRows.ts | 40 ++++++++++--------------- src/types.ts | 6 ++-- 4 files changed, 21 insertions(+), 31 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 46b976bc93..6d2b259333 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -820,7 +820,7 @@ function DataGrid({ return ( ({ function handleKeyDown(event: React.KeyboardEvent) { const { key } = event; - if (['ArrowLeft', 'ArrowRight', 'Enter', ' '].includes(key)) { + if (['ArrowLeft', 'ArrowRight', 'Enter'].includes(key)) { event.preventDefault(); event.stopPropagation(); - if (key === ' ' || key === 'Enter') { + if (key === 'Enter') { toggleGroup(); } } diff --git a/src/hooks/useViewportRows.ts b/src/hooks/useViewportRows.ts index 8798ad95d8..42774b1996 100644 --- a/src/hooks/useViewportRows.ts +++ b/src/hooks/useViewportRows.ts @@ -25,54 +25,44 @@ export function useViewportRows({ const [groupedRows, rowsCount] = useMemo(() => { if (groupBy.length === 0 || !rowGrouper) return [undefined, rawRows.length]; - let rowsCount = 0; - const groupRows = (rows: readonly R[], [groupByKey, ...remainingGroupByKeys]: readonly string[]) => { - const parentGroup = rowGrouper(rows, groupByKey); - rowsCount += Object.keys(parentGroup).length; - if (remainingGroupByKeys.length === 0) return parentGroup; - - // Recursively group each parent group - const childGroups: GroupByDictionary = {}; - for (const key in parentGroup) { - const childRows = parentGroup[key]; - rowsCount += childRows.length; - childGroups[key] = { - rows: childRows, - groups: groupRows(childRows, remainingGroupByKeys) - }; + const groupRows = (rows: readonly R[], [groupByKey, ...remainingGroupByKeys]: readonly string[]): [GroupByDictionary, number] => { + let rowsCount = 0; + const groups: GroupByDictionary = {}; + for (const [key, childRows] of Object.entries(rowGrouper(rows, groupByKey))) { + // Recursively group each parent group + const [childGroups, childRowsCount] = remainingGroupByKeys.length === 0 ? [childRows, childRows.length] : groupRows(childRows, remainingGroupByKeys); + rowsCount += childRowsCount + 1; // 1 for parent row + groups[key] = { childRows, childGroups }; } - return childGroups; + return [groups, rowsCount]; }; - return [groupRows(rawRows, groupBy), rowsCount]; + return groupRows(rawRows, groupBy); }, [groupBy, rowGrouper, rawRows]); const rows = useMemo(() => { if (!groupedRows) return rawRows; - const expandGroup = (rows: GroupByDictionary, parentKey: string | undefined, level: number): Array | R> => { + const expandGroup = (rows: GroupByDictionary | R[], parentKey: string | undefined, level: number): Array | R> => { + if (Array.isArray(rows)) return rows; const flattenedRows: Array> = []; Object.keys(rows).forEach((key, index, keys) => { const id = parentKey !== undefined ? `${parentKey}__${key}` : key; const isExpanded = expandedGroupIds?.has(id) ?? false; - const group = rows[key]; + const { childRows, childGroups } = rows[key]; flattenedRows.push({ id, key, isExpanded, - childRows: Array.isArray(group) ? group : group.rows, + childRows, level, setSize: keys.length, posInSet: index + 1, // aria-posinset is 1-based __isGroup: true }); if (isExpanded) { - if (Array.isArray(group)) { - flattenedRows.push(...group); - } else { - flattenedRows.push(...expandGroup(group.groups, key, level + 1)); - } + flattenedRows.push(...expandGroup(childGroups, key, level + 1)); } }); diff --git a/src/types.ts b/src/types.ts index 5e8e9ef4fa..0063fc2fb0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -245,9 +245,9 @@ export interface Dictionary { [index: string]: T; } -export type GroupByDictionary = Dictionary | Dictionary<{ - rows: TRow[]; - groups: GroupByDictionary; +export type GroupByDictionary = Dictionary<{ + childRows: TRow[]; + childGroups: TRow[] | GroupByDictionary; }>; export interface GroupRow { From 4815742d56e8f88ee09ba70d9279af0fe63dd98e Mon Sep 17 00:00:00 2001 From: Mahajan Date: Wed, 19 Aug 2020 07:35:29 -0500 Subject: [PATCH 16/79] Fix aria-rowindex and aria-rowcount and update grouprow props --- src/DataGrid.tsx | 25 ++++++++++++++++++------- src/GroupRow.tsx | 25 ++++++++++++++++--------- src/formatters/ToggleGroupFormatter.tsx | 5 +++-- src/hooks/useViewportRows.ts | 25 ++++++++++++++----------- src/types.ts | 14 +++++++++++--- stories/demos/CommonFeatures.tsx | 4 ++-- 6 files changed, 64 insertions(+), 34 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 6d2b259333..b7ee19d59e 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -483,7 +483,7 @@ function DataGrid({ function handleCopy() { const { idx, rowIdx } = selectedPosition; - const value = rawRows[rowIdx][columns[idx].key as keyof R]; // TODO: handle grouping + const value = rawRows[rowIdx][columns[idx].key as keyof R]; setCopiedPosition({ idx, rowIdx, value }); } @@ -785,21 +785,31 @@ function DataGrid({ } function getViewportRows() { + // TODO: cleanup rowIndex/rowIdx logic + let startRowIndex = 0; return viewportRows.map((row, index) => { const rowIdx = startRowIdx + index; const top = rowIdx * rowHeight + totalHeaderHeight; if (isGroupedRow(row)) { const isSelected = selectedPosition.rowIdx === rowIdx; + ({ startRowIndex } = row); return ( - + aria-level={row.level + 1} // aria-level is 1-based + aria-setsize={row.setSize} + aria-posinset={row.posInSet + 1} // aria-posinset is 1-based + aria-rowindex={headerRowsCount + startRowIndex + 1} // aria-rowindex is 1 based + key={row.id} // TODO: id or index? + id={row.id} + groupKey={row.key} viewportColumns={viewportColumns} - row={row} + childRows={row.childRows} rowIdx={rowIdx} lastFrozenColumnIndex={lastFrozenColumnIndex} groupBy={groupBy} top={top} + level={row.level} + isExpanded={row.isExpanded} isCellSelected={isSelected} isRowSelected={isSelectable && row.childRows.every(cr => selectedRows?.has(cr[rowKey!]))} eventBus={eventBus} @@ -807,8 +817,9 @@ function DataGrid({ /> ); } + startRowIndex++; - let key: string | number = rowIdx; + let key: string | number = hasGroups ? startRowIndex : rowIdx; let isRowSelected = false; if (rowKey !== undefined) { const rowId = row[rowKey]; @@ -820,7 +831,7 @@ function DataGrid({ return ( ({ - 'aria-rowindex': ariaRowIndex, + id, + groupKey, viewportColumns, - row, + childRows, rowIdx, lastFrozenColumnIndex, top, + level, + isExpanded, isCellSelected, isRowSelected, groupBy, eventBus, onKeyDown, + 'aria-setsize': ariaSetSize, + 'aria-posinset': ariaPosInSet, + 'aria-rowindex': ariaRowIndex, ...props }: GroupRowRendererProps) { - const { level } = row; // Select is always the first column - const idx = viewportColumns[0].key === SELECT_COLUMN_KEY ? level : level - 1; // aria-level is 1-based + const idx = viewportColumns[0].key === SELECT_COLUMN_KEY ? level + 1 : level; function selectGroup() { eventBus.dispatch('SELECT_CELL', { rowIdx, idx }); } function toggleGroup() { - eventBus.dispatch('TOGGLE_GROUP', row.id); + eventBus.dispatch('TOGGLE_GROUP', id); } function onRowSelectionChange(checked: boolean) { @@ -49,10 +54,10 @@ function GroupedRow({
({ > {column.groupFormatter && (groupBy.includes(column.key) ? idx === column.idx : true) && ( ({ - row, + groupKey, + isExpanded, isCellSelected, toggleGroup }: GroupFormatterProps) { @@ -31,7 +32,7 @@ export function ToggleGroupedFormatter({ onClick={toggleGroup} onKeyDown={handleKeyDown} > - {row.key}{' '}{row.isExpanded ? '\u25BC' : '\u25B6'} + {groupKey}{' '}{isExpanded ? '\u25BC' : '\u25B6'} ); } diff --git a/src/hooks/useViewportRows.ts b/src/hooks/useViewportRows.ts index 42774b1996..2c2c97f041 100644 --- a/src/hooks/useViewportRows.ts +++ b/src/hooks/useViewportRows.ts @@ -25,20 +25,22 @@ export function useViewportRows({ const [groupedRows, rowsCount] = useMemo(() => { if (groupBy.length === 0 || !rowGrouper) return [undefined, rawRows.length]; - const groupRows = (rows: readonly R[], [groupByKey, ...remainingGroupByKeys]: readonly string[]): [GroupByDictionary, number] => { - let rowsCount = 0; + const groupRows = (rows: readonly R[], [groupByKey, ...remainingGroupByKeys]: readonly string[], startIndex: number): [GroupByDictionary, number] => { + let groupRowsCount = 0; const groups: GroupByDictionary = {}; for (const [key, childRows] of Object.entries(rowGrouper(rows, groupByKey))) { // Recursively group each parent group - const [childGroups, childRowsCount] = remainingGroupByKeys.length === 0 ? [childRows, childRows.length] : groupRows(childRows, remainingGroupByKeys); - rowsCount += childRowsCount + 1; // 1 for parent row - groups[key] = { childRows, childGroups }; + const [childGroups, childRowsCount] = remainingGroupByKeys.length === 0 + ? [childRows, childRows.length] + : groupRows(childRows, remainingGroupByKeys, startIndex + groupRowsCount + 1); // 1 for parent row + groups[key] = { childRows, childGroups, startRowIndex: startIndex + groupRowsCount }; + groupRowsCount += childRowsCount + 1; // 1 for parent row } - return [groups, rowsCount]; + return [groups, groupRowsCount]; }; - return groupRows(rawRows, groupBy); + return groupRows(rawRows, groupBy, 0); }, [groupBy, rowGrouper, rawRows]); const rows = useMemo(() => { @@ -47,18 +49,19 @@ export function useViewportRows({ const expandGroup = (rows: GroupByDictionary | R[], parentKey: string | undefined, level: number): Array | R> => { if (Array.isArray(rows)) return rows; const flattenedRows: Array> = []; - Object.keys(rows).forEach((key, index, keys) => { + Object.keys(rows).forEach((key, posInSet, keys) => { const id = parentKey !== undefined ? `${parentKey}__${key}` : key; const isExpanded = expandedGroupIds?.has(id) ?? false; - const { childRows, childGroups } = rows[key]; + const { childRows, childGroups, startRowIndex } = rows[key]; flattenedRows.push({ id, key, isExpanded, childRows, level, + posInSet, + startRowIndex, setSize: keys.length, - posInSet: index + 1, // aria-posinset is 1-based __isGroup: true }); if (isExpanded) { @@ -69,7 +72,7 @@ export function useViewportRows({ return flattenedRows; }; - return expandGroup(groupedRows, undefined, 1); // aria-level is 1-based + return expandGroup(groupedRows, undefined, 0); }, [expandedGroupIds, groupedRows, rawRows]); const [rowOverscanStartIdx, rowOverscanEndIdx] = getVerticalRangeToRender( diff --git a/src/types.ts b/src/types.ts index 0063fc2fb0..311b500671 100644 --- a/src/types.ts +++ b/src/types.ts @@ -98,8 +98,10 @@ export interface SummaryFormatterProps { } export interface GroupFormatterProps { + groupKey: unknown; column: CalculatedColumn; - row: GroupRow; + childRows: TRow[]; + isExpanded: boolean; isCellSelected: boolean; isRowSelected: boolean; onRowSelectionChange: (checked: boolean) => void; @@ -196,12 +198,16 @@ export interface RowRendererProps extends Omit extends Omit, 'style' | 'children'> { + id: string; + groupKey: unknown; viewportColumns: readonly CalculatedColumn[]; - row: GroupRow; + childRows: TRow[]; rowIdx: number; lastFrozenColumnIndex: number; groupBy: readonly string[]; top: number; + level: number; + isExpanded: boolean; isCellSelected: boolean; isRowSelected: boolean; eventBus: EventBus; @@ -248,6 +254,7 @@ export interface Dictionary { export type GroupByDictionary = Dictionary<{ childRows: TRow[]; childGroups: TRow[] | GroupByDictionary; + startRowIndex: number; }>; export interface GroupRow { @@ -257,6 +264,7 @@ export interface GroupRow { key: unknown; isExpanded: boolean; level: number; - setSize: number; posInSet: number; + setSize: number; + startRowIndex: number; } diff --git a/stories/demos/CommonFeatures.tsx b/stories/demos/CommonFeatures.tsx index a0f118bb8d..7988d3cff8 100644 --- a/stories/demos/CommonFeatures.tsx +++ b/stories/demos/CommonFeatures.tsx @@ -147,8 +147,8 @@ function getColumns(countries: string[]): readonly Column[] { formatter(props) { return ; }, - groupFormatter({ row }) { - const totals = row.childRows.reduce((prev, { budget }) => prev + budget, 0); + groupFormatter({ childRows }) { + const totals = childRows.reduce((prev, { budget }) => prev + budget, 0); return ; } }, From 3efd6c830e9231c22f029ae5bc19d1cdeadf09c6 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Wed, 19 Aug 2020 11:01:50 -0500 Subject: [PATCH 17/79] Change expandedGroupIds/onExpandedGroupIdsChange to props --- src/DataGrid.tsx | 17 +++++++++-------- src/hooks/useViewportRows.ts | 6 +++--- stories/demos/CommonFeatures.tsx | 3 +++ 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index b7ee19d59e..1ca6e1da00 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -134,6 +134,8 @@ export interface DataGridProps extends Share defaultColumnOptions?: DefaultColumnOptions; groupBy?: readonly string[]; rowGrouper?: (rows: readonly R[], columnKey: string) => Dictionary; + expandedGroupIds?: Set; + onExpandedGroupIdsChange?: (expandedGroupIds: Set) => void; /** * Custom renderers @@ -204,6 +206,8 @@ function DataGrid({ defaultColumnOptions, groupBy: rawGroupBy, rowGrouper, + expandedGroupIds, + onExpandedGroupIdsChange, // Custom renderers rowRenderer: RowRenderer = Row, emptyRowsRenderer, @@ -238,9 +242,6 @@ function DataGrid({ const [isDragging, setDragging] = useState(false); const [draggedOverRowIdx, setOverRowIdx] = useState(undefined); - // TODO: change it to props - const [expandedGroupIds, setExpandedGroupIds] = useState>(new Set()); - const setDraggedOverRowIdx = useCallback((rowIdx?: number) => { setOverRowIdx(rowIdx); latestDraggedOverRowIdx.current = rowIdx; @@ -363,17 +364,18 @@ function DataGrid({ useEffect(() => { function toggleGroup(expandedGroupId: unknown) { + if (!onExpandedGroupIdsChange) return; const newExpandedGroupIds = new Set(expandedGroupIds); - if (expandedGroupIds.has(expandedGroupId)) { + if (newExpandedGroupIds.has(expandedGroupId)) { newExpandedGroupIds.delete(expandedGroupId); } else { newExpandedGroupIds.add(expandedGroupId); } - setExpandedGroupIds(newExpandedGroupIds); + onExpandedGroupIdsChange(newExpandedGroupIds); } return eventBus.subscribe('TOGGLE_GROUP', toggleGroup); - }, [eventBus, expandedGroupIds]); + }, [eventBus, expandedGroupIds, onExpandedGroupIdsChange]); useImperativeHandle(ref, () => ({ scrollToColumn(idx: number) { @@ -785,7 +787,6 @@ function DataGrid({ } function getViewportRows() { - // TODO: cleanup rowIndex/rowIdx logic let startRowIndex = 0; return viewportRows.map((row, index) => { const rowIdx = startRowIdx + index; @@ -831,7 +832,7 @@ function DataGrid({ return ( ({ const [groupedRows, rowsCount] = useMemo(() => { if (groupBy.length === 0 || !rowGrouper) return [undefined, rawRows.length]; - const groupRows = (rows: readonly R[], [groupByKey, ...remainingGroupByKeys]: readonly string[], startIndex: number): [GroupByDictionary, number] => { + const groupRows = (rows: readonly R[], [groupByKey, ...remainingGroupByKeys]: readonly string[], startRowIndex: number): [GroupByDictionary, number] => { let groupRowsCount = 0; const groups: GroupByDictionary = {}; for (const [key, childRows] of Object.entries(rowGrouper(rows, groupByKey))) { // Recursively group each parent group const [childGroups, childRowsCount] = remainingGroupByKeys.length === 0 ? [childRows, childRows.length] - : groupRows(childRows, remainingGroupByKeys, startIndex + groupRowsCount + 1); // 1 for parent row - groups[key] = { childRows, childGroups, startRowIndex: startIndex + groupRowsCount }; + : groupRows(childRows, remainingGroupByKeys, startRowIndex + groupRowsCount + 1); // 1 for parent row + groups[key] = { childRows, childGroups, startRowIndex: startRowIndex + groupRowsCount }; groupRowsCount += childRowsCount + 1; // 1 for parent row } diff --git a/stories/demos/CommonFeatures.tsx b/stories/demos/CommonFeatures.tsx index 7988d3cff8..d55be0ecb7 100644 --- a/stories/demos/CommonFeatures.tsx +++ b/stories/demos/CommonFeatures.tsx @@ -213,6 +213,7 @@ export default function CommonFeatures() { const [rows, setRows] = useState(createRows); const [[sortColumn, sortDirection], setSort] = useState<[string, SortDirection]>(['id', 'NONE']); const [selectedRows, setSelectedRows] = useState(() => new Set()); + const [expandedGroupIds, setExpandedGroupIds] = useState>(new Set()); const [groupBy] = useState(['country', 'transaction']); const countries = useMemo(() => { @@ -296,6 +297,8 @@ export default function CommonFeatures() { summaryRows={summaryRows} groupBy={groupBy} rowGrouper={rowGrouper} + expandedGroupIds={expandedGroupIds} + onExpandedGroupIdsChange={setExpandedGroupIds} /> )} From 33c23ff04823737826794008dd97a7ff54d9a42c Mon Sep 17 00:00:00 2001 From: Mahajan Date: Wed, 19 Aug 2020 11:14:09 -0500 Subject: [PATCH 18/79] Use 1 loop --- src/DataGrid.tsx | 19 +++++++++++-------- src/hooks/useViewportRows.ts | 6 ++---- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 1ca6e1da00..1c7758192f 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -281,7 +281,7 @@ function DataGrid({ - summaryRowsCount * rowHeight - (totalColumnWidth > viewportWidth ? getScrollbarSize() : 0); - const { viewportRows, rows, startRowIdx, rowsCount } = useViewportRows({ + const { rowOverscanStartIdx, rowOverscanEndIdx, rows, rowsCount } = useViewportRows({ rawRows, groupBy, rowGrouper, @@ -620,7 +620,6 @@ function DataGrid({ /** * utils */ - function isCellWithinBounds({ idx, rowIdx }: Position): boolean { return rowIdx >= 0 && rowIdx < rows.length && idx >= 0 && idx < columns.length; } @@ -787,14 +786,15 @@ function DataGrid({ } function getViewportRows() { + const rowElements = []; let startRowIndex = 0; - return viewportRows.map((row, index) => { - const rowIdx = startRowIdx + index; + for (let rowIdx = rowOverscanStartIdx; rowIdx <= rowOverscanEndIdx; rowIdx++) { + const row = rows[rowIdx]; const top = rowIdx * rowHeight + totalHeaderHeight; if (isGroupedRow(row)) { const isSelected = selectedPosition.rowIdx === rowIdx; ({ startRowIndex } = row); - return ( + rowElements.push( aria-level={row.level + 1} // aria-level is 1-based aria-setsize={row.setSize} @@ -817,9 +817,10 @@ function DataGrid({ onKeyDown={isSelected ? handleKeyDown : undefined} /> ); + continue; } - startRowIndex++; + startRowIndex++; let key: string | number = hasGroups ? startRowIndex : rowIdx; let isRowSelected = false; if (rowKey !== undefined) { @@ -830,7 +831,7 @@ function DataGrid({ } } - return ( + rowElements.push( ({ selectedCellProps={getSelectedCellProps(rowIdx)} /> ); - }); + } + + return rowElements; } // Reset the positions if the current values are no longer valid. This can happen if a column or row is removed diff --git a/src/hooks/useViewportRows.ts b/src/hooks/useViewportRows.ts index 78d7ea579a..2a695ac404 100644 --- a/src/hooks/useViewportRows.ts +++ b/src/hooks/useViewportRows.ts @@ -82,12 +82,10 @@ export function useViewportRows({ rows.length ); - const viewportRows = rows.slice(rowOverscanStartIdx, rowOverscanEndIdx + 1); - return { - viewportRows, + rowOverscanStartIdx, + rowOverscanEndIdx, rows, - startRowIdx: rowOverscanStartIdx, rowsCount }; } From bd4c1210b2a103c934eced792a6f3e6eb4c2d312 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Wed, 19 Aug 2020 13:09:35 -0500 Subject: [PATCH 19/79] Cleanup --- src/DataGrid.tsx | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 1c7758192f..2536954fb4 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -332,24 +332,26 @@ function DataGrid({ newSelectedRows.delete(childRow[rowKey]); } } - } else { - const rowId = row[rowKey]; - if (checked) { - newSelectedRows.add(rowId); - const previousRowIdx = lastSelectedRowIdx.current; - lastSelectedRowIdx.current = rowIdx; - if (isShiftClick && previousRowIdx !== -1 && previousRowIdx !== rowIdx) { - const step = Math.sign(rowIdx - previousRowIdx); - for (let i = previousRowIdx + step; i !== rowIdx; i += step) { - const row = rows[i]; - if (isGroupedRow(row)) continue; - newSelectedRows.add(row[rowKey]); - } + onSelectedRowsChange(newSelectedRows); + return; + } + + const rowId = row[rowKey]; + if (checked) { + newSelectedRows.add(rowId); + const previousRowIdx = lastSelectedRowIdx.current; + lastSelectedRowIdx.current = rowIdx; + if (isShiftClick && previousRowIdx !== -1 && previousRowIdx !== rowIdx) { + const step = Math.sign(rowIdx - previousRowIdx); + for (let i = previousRowIdx + step; i !== rowIdx; i += step) { + const row = rows[i]; + if (isGroupedRow(row)) continue; + newSelectedRows.add(row[rowKey]); } - } else { - newSelectedRows.delete(rowId); - lastSelectedRowIdx.current = -1; } + } else { + newSelectedRows.delete(rowId); + lastSelectedRowIdx.current = -1; } onSelectedRowsChange(newSelectedRows); @@ -800,7 +802,7 @@ function DataGrid({ aria-setsize={row.setSize} aria-posinset={row.posInSet + 1} // aria-posinset is 1-based aria-rowindex={headerRowsCount + startRowIndex + 1} // aria-rowindex is 1 based - key={row.id} // TODO: id or index? + key={row.id} id={row.id} groupKey={row.key} viewportColumns={viewportColumns} From cbfd03fcce3af866ecec041da9b6bb8fa63b61d4 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Wed, 19 Aug 2020 14:41:30 -0500 Subject: [PATCH 20/79] Add GroupCell component --- src/DataGrid.tsx | 6 ++-- src/GroupCell.tsx | 73 +++++++++++++++++++++++++++++++++++++++++++++++ src/GroupRow.tsx | 54 +++++++++++------------------------ src/types.ts | 3 +- 4 files changed, 93 insertions(+), 43 deletions(-) create mode 100644 src/GroupCell.tsx diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 2536954fb4..cbd888ed61 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -794,7 +794,6 @@ function DataGrid({ const row = rows[rowIdx]; const top = rowIdx * rowHeight + totalHeaderHeight; if (isGroupedRow(row)) { - const isSelected = selectedPosition.rowIdx === rowIdx; ({ startRowIndex } = row); rowElements.push( @@ -809,14 +808,13 @@ function DataGrid({ childRows={row.childRows} rowIdx={rowIdx} lastFrozenColumnIndex={lastFrozenColumnIndex} - groupBy={groupBy} top={top} level={row.level} isExpanded={row.isExpanded} - isCellSelected={isSelected} + selectedCellIdx={selectedPosition.rowIdx === rowIdx ? selectedPosition.idx : undefined} isRowSelected={isSelectable && row.childRows.every(cr => selectedRows?.has(cr[rowKey!]))} eventBus={eventBus} - onKeyDown={isSelected ? handleKeyDown : undefined} + onKeyDown={selectedPosition.rowIdx === rowIdx ? handleKeyDown : undefined} /> ); continue; diff --git a/src/GroupCell.tsx b/src/GroupCell.tsx new file mode 100644 index 0000000000..29d90ba585 --- /dev/null +++ b/src/GroupCell.tsx @@ -0,0 +1,73 @@ +import React, { memo } from 'react'; +import clsx from 'clsx'; +import { GroupRowRendererProps, CalculatedColumn } from './types'; + +type SharedGroupRowRendererProps = Pick, + | 'id' + | 'rowIdx' + | 'groupKey' + | 'childRows' + | 'isExpanded' + | 'isRowSelected' + | 'lastFrozenColumnIndex' + | 'eventBus' +>; + +interface GroupCellProps extends SharedGroupRowRendererProps { + column: CalculatedColumn; + isCellSelected: boolean; + groupColumnIndex: number; +} + +function GroupCell({ + id, + rowIdx, + groupKey, + childRows, + lastFrozenColumnIndex, + isExpanded, + isCellSelected, + isRowSelected, + eventBus, + column, + groupColumnIndex +}: GroupCellProps) { + function toggleGroup() { + eventBus.dispatch('TOGGLE_GROUP', id); + } + + function onRowSelectionChange(checked: boolean) { + eventBus.dispatch('SELECT_ROW', { rowIdx, checked, isShiftClick: false }); + } + + return ( +
+ {column.groupFormatter && (column.rowGroup ? groupColumnIndex === column.idx : true) && ( + + )} +
+ ); +} + +export default memo(GroupCell) as (props: GroupCellProps) => JSX.Element; diff --git a/src/GroupRow.tsx b/src/GroupRow.tsx index 656f9472dc..614f43fe3b 100644 --- a/src/GroupRow.tsx +++ b/src/GroupRow.tsx @@ -2,6 +2,7 @@ import React, { memo } from 'react'; import clsx from 'clsx'; import { GroupRowRendererProps, CalculatedColumn } from './types'; import { SELECT_COLUMN_KEY } from './Columns'; +import GroupCell from './GroupCell'; function GroupedRow({ id, @@ -13,9 +14,8 @@ function GroupedRow({ top, level, isExpanded, - isCellSelected, + selectedCellIdx, isRowSelected, - groupBy, eventBus, onKeyDown, 'aria-setsize': ariaSetSize, @@ -30,21 +30,13 @@ function GroupedRow({ eventBus.dispatch('SELECT_CELL', { rowIdx, idx }); } - function toggleGroup() { - eventBus.dispatch('TOGGLE_GROUP', id); - } - - function onRowSelectionChange(checked: boolean) { - eventBus.dispatch('SELECT_ROW', { rowIdx, checked, isShiftClick: false }); - } - // Expand groupBy column widths const visibleColumns: CalculatedColumn[] = [...viewportColumns]; visibleColumns[idx] = { ...visibleColumns[idx] }; let colSpan = 0; for (let i = idx + 1; i < visibleColumns.length; i++) { const nextColumn = visibleColumns[i]; - if (!nextColumn.frozen || (nextColumn.groupFormatter && !groupBy.includes(nextColumn.key))) break; + if (!nextColumn.frozen || (nextColumn.groupFormatter && !nextColumn.rowGroup)) break; visibleColumns[idx].width += nextColumn.width; colSpan++; } @@ -61,7 +53,7 @@ function GroupedRow({ className={clsx('rdg-row rdg-group-row', { 'rdg-row-even': rowIdx % 2 === 0, 'rdg-row-odd': rowIdx % 2 !== 0, - 'rdg-group-row-selected': isCellSelected + 'rdg-group-row-selected': selectedCellIdx !== undefined })} onClick={selectGroup} onKeyDown={onKeyDown} @@ -69,32 +61,20 @@ function GroupedRow({ {...props} > {visibleColumns.map(column => ( -
key={column.key} - className={clsx('rdg-cell', { - 'rdg-cell-frozen': column.frozen, - 'rdg-cell-frozen-last': column.idx === lastFrozenColumnIndex - })} - style={{ - width: column.width, - left: column.left - }} - > - {column.groupFormatter && (groupBy.includes(column.key) ? idx === column.idx : true) && ( - - )} -
+ id={id} + rowIdx={rowIdx} + groupKey={groupKey} + childRows={childRows} + isExpanded={isExpanded} + isRowSelected={isRowSelected} + isCellSelected={selectedCellIdx !== undefined && idx === column.idx} // TODO: fir selectedCell logic + eventBus={eventBus} + column={column} + lastFrozenColumnIndex={lastFrozenColumnIndex} + groupColumnIndex={idx} + /> ))}
); diff --git a/src/types.ts b/src/types.ts index 311b500671..50a8d24f04 100644 --- a/src/types.ts +++ b/src/types.ts @@ -204,11 +204,10 @@ export interface GroupRowRendererProps extends Omit childRows: TRow[]; rowIdx: number; lastFrozenColumnIndex: number; - groupBy: readonly string[]; top: number; level: number; + selectedCellIdx?: number; isExpanded: boolean; - isCellSelected: boolean; isRowSelected: boolean; eventBus: EventBus; } From b0df2383e3fd50fdfc3394e8918838bb8f6ccaca Mon Sep 17 00:00:00 2001 From: Mahajan Date: Thu, 20 Aug 2020 20:59:05 -0500 Subject: [PATCH 21/79] Add row selection for group rows --- src/Columns.tsx | 2 + src/DataGrid.tsx | 61 ++++++++++++++++++++++--- src/GroupCell.tsx | 3 +- src/GroupRow.tsx | 31 +++++++------ src/formatters/SelectCellFormatter.tsx | 5 +- src/formatters/ToggleGroupFormatter.tsx | 23 +++++----- src/hooks/useViewportColumns.ts | 16 +++++-- src/hooks/useViewportRows.ts | 2 + src/types.ts | 3 +- src/utils/index.ts | 2 +- stories/demos/CommonFeatures.tsx | 3 +- style/cell.less | 2 +- style/row.less | 5 +- 13 files changed, 112 insertions(+), 46 deletions(-) diff --git a/src/Columns.tsx b/src/Columns.tsx index 9b974de505..009f441aa0 100644 --- a/src/Columns.tsx +++ b/src/Columns.tsx @@ -41,6 +41,8 @@ export const SelectColumn: Column = { isCellSelected={props.isCellSelected} value={props.isRowSelected} onChange={props.onRowSelectionChange} + // Stop propagation to prevent row selection + onClick={event => event.stopPropagation()} /> ); }, diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index cbd888ed61..465de4635b 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -307,13 +307,12 @@ function DataGrid({ prevSelectedPosition.current = selectedPosition; scrollToCell(selectedPosition); - const row = rows[selectedPosition.rowIdx]; - if (isGroupedRow(row)) return; - // Let the formatter handle focus - const column = columns[selectedPosition.idx]; - if (column.formatterOptions?.focusable) return; - + const formatterOptions = columns[selectedPosition.idx]?.formatterOptions; + const row = rows[selectedPosition.rowIdx]; + if ((typeof formatterOptions?.focusable === 'function' && formatterOptions?.focusable(row)) || formatterOptions?.focusable === true) { + return; + } focusSinkRef.current!.focus(); }); @@ -622,8 +621,13 @@ function DataGrid({ /** * utils */ + function isRowWithinBounds(rowIdx: number) { + return rowIdx >= 0 && rowIdx < rows.length; + } + function isCellWithinBounds({ idx, rowIdx }: Position): boolean { - return rowIdx >= 0 && rowIdx < rows.length && idx >= 0 && idx < columns.length; + const minIdx = isRowWithinBounds(rowIdx) && isGroupedRow(rows[rowIdx]) ? -1 : 0; + return isRowWithinBounds(rowIdx) && idx >= minIdx && idx < columns.length; } function isCellEditable(position: Position): boolean { @@ -644,6 +648,39 @@ function DataGrid({ onSelectedCellChange?.({ ...position }); } + function selectRow(key: string) { + const row = rows[selectedPosition.rowIdx]; + if (isGroupedRow(row) && selectedPosition.idx === -1) { + const isRowExpanded = expandedGroupIds?.has(row.id); + if ( + // If a row is focused, and it is expanded, collaps the current row. + (key === 'ArrowLeft' && isRowExpanded) + // If a row is focused, and it is collapsed, expand the current row. + || (key === 'ArrowRight' && !isRowExpanded) + ) { + eventBus.dispatch('TOGGLE_GROUP', row.id); + return true; + } + + // If a row is focused, and it is collapsed, move to the parent row (if there is one). + if (key === 'ArrowLeft' && !isRowExpanded && row.level !== 0) { + let parentRowIdx = -1; + for (let i = selectedPosition.rowIdx - 1; i >= 0; i--) { + const parentRow = rows[i]; + if (isGroupedRow(parentRow) && parentRow.key === row.parentKey) { + parentRowIdx = i; + break; + } + } + if (parentRowIdx !== -1) { + setSelectedPosition(position => ({ ...position, rowIdx: parentRowIdx })); + return true; + } + } + } + return false; + } + function closeEditor() { if (selectedPosition.mode === 'SELECT') return; setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, mode: 'SELECT' })); @@ -714,6 +751,13 @@ function DataGrid({ const { key, shiftKey } = event; const ctrlKey = isCtrlKeyHeldDown(event); let nextPosition = getNextPosition(key, ctrlKey, shiftKey); + if (isRowWithinBounds(nextPosition.rowIdx)) { + const row = rows[nextPosition.rowIdx]; + // Select the first cell when the selected position changes from a group row to regular row + if (!isGroupedRow(row) && nextPosition.idx === -1) { + nextPosition.idx = 0; + } + } let mode = cellNavigationMode; if (key === 'Tab') { // If we are in a position to leave the grid, stop editing but stay in that cell @@ -730,6 +774,9 @@ function DataGrid({ // Do not allow focus to leave event.preventDefault(); + const isRowSelected = selectRow(key); + if (isRowSelected) return; + nextPosition = getNextSelectedCellPosition({ columns, rowsCount: rows.length, diff --git a/src/GroupCell.tsx b/src/GroupCell.tsx index 29d90ba585..bd973debc0 100644 --- a/src/GroupCell.tsx +++ b/src/GroupCell.tsx @@ -47,7 +47,8 @@ function GroupCell({ key={column.key} className={clsx('rdg-cell', { 'rdg-cell-frozen': column.frozen, - 'rdg-cell-frozen-last': column.idx === lastFrozenColumnIndex + 'rdg-cell-frozen-last': column.idx === lastFrozenColumnIndex, + 'rdg-cell-selected': isCellSelected })} style={{ width: column.width, diff --git a/src/GroupRow.tsx b/src/GroupRow.tsx index 614f43fe3b..9ec15d9ef7 100644 --- a/src/GroupRow.tsx +++ b/src/GroupRow.tsx @@ -1,6 +1,6 @@ import React, { memo } from 'react'; import clsx from 'clsx'; -import { GroupRowRendererProps, CalculatedColumn } from './types'; +import { GroupRowRendererProps } from './types'; import { SELECT_COLUMN_KEY } from './Columns'; import GroupCell from './GroupCell'; @@ -27,20 +27,21 @@ function GroupedRow({ const idx = viewportColumns[0].key === SELECT_COLUMN_KEY ? level + 1 : level; function selectGroup() { - eventBus.dispatch('SELECT_CELL', { rowIdx, idx }); + eventBus.dispatch('SELECT_CELL', { rowIdx, idx: -1 }); } + // TODO: how to handle this with cell selection? // Expand groupBy column widths - const visibleColumns: CalculatedColumn[] = [...viewportColumns]; - visibleColumns[idx] = { ...visibleColumns[idx] }; - let colSpan = 0; - for (let i = idx + 1; i < visibleColumns.length; i++) { - const nextColumn = visibleColumns[i]; - if (!nextColumn.frozen || (nextColumn.groupFormatter && !nextColumn.rowGroup)) break; - visibleColumns[idx].width += nextColumn.width; - colSpan++; - } - visibleColumns.splice(idx + 1, colSpan); + // const visibleColumns: CalculatedColumn[] = [...viewportColumns]; + // visibleColumns[idx] = { ...visibleColumns[idx] }; + // let colSpan = 0; + // for (let i = idx + 1; i < visibleColumns.length; i++) { + // const nextColumn = visibleColumns[i]; + // if (!nextColumn.frozen || (nextColumn.groupFormatter && !nextColumn.rowGroup)) break; + // visibleColumns[idx].width += nextColumn.width; + // colSpan++; + // } + // visibleColumns.splice(idx + 1, colSpan); return (
({ className={clsx('rdg-row rdg-group-row', { 'rdg-row-even': rowIdx % 2 === 0, 'rdg-row-odd': rowIdx % 2 !== 0, - 'rdg-group-row-selected': selectedCellIdx !== undefined + 'rdg-group-row-selected': selectedCellIdx === -1 // Select row if there is no selected cell })} onClick={selectGroup} onKeyDown={onKeyDown} style={{ top }} {...props} > - {visibleColumns.map(column => ( + {viewportColumns.map(column => ( key={column.key} id={id} @@ -69,7 +70,7 @@ function GroupedRow({ childRows={childRows} isExpanded={isExpanded} isRowSelected={isRowSelected} - isCellSelected={selectedCellIdx !== undefined && idx === column.idx} // TODO: fir selectedCell logic + isCellSelected={selectedCellIdx === column.idx} eventBus={eventBus} column={column} lastFrozenColumnIndex={lastFrozenColumnIndex} diff --git a/src/formatters/SelectCellFormatter.tsx b/src/formatters/SelectCellFormatter.tsx index 5d89748811..08dd65eaf5 100644 --- a/src/formatters/SelectCellFormatter.tsx +++ b/src/formatters/SelectCellFormatter.tsx @@ -4,6 +4,7 @@ import clsx from 'clsx'; type SharedInputProps = Pick, | 'disabled' | 'tabIndex' + | 'onClick' | 'aria-label' | 'aria-labelledby' >; @@ -19,6 +20,7 @@ export function SelectCellFormatter({ tabIndex, isCellSelected, disabled, + onClick, onChange, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy @@ -43,8 +45,9 @@ export function SelectCellFormatter({ type="checkbox" className="rdg-checkbox-input" disabled={disabled} - onChange={handleChange} checked={value} + onChange={handleChange} + onClick={onClick} />
diff --git a/src/formatters/ToggleGroupFormatter.tsx b/src/formatters/ToggleGroupFormatter.tsx index 30c263a0c5..a2e25b4580 100644 --- a/src/formatters/ToggleGroupFormatter.tsx +++ b/src/formatters/ToggleGroupFormatter.tsx @@ -13,26 +13,25 @@ export function ToggleGroupedFormatter({ cellRef.current?.focus(); }, [isCellSelected]); - function handleKeyDown(event: React.KeyboardEvent) { - const { key } = event; - if (['ArrowLeft', 'ArrowRight', 'Enter'].includes(key)) { - event.preventDefault(); - event.stopPropagation(); - if (key === 'Enter') { - toggleGroup(); - } + function handleKeyDown({ key }: React.KeyboardEvent) { + if (key === 'Enter') { + toggleGroup(); } } return ( - {groupKey}{' '}{isExpanded ? '\u25BC' : '\u25B6'} + {groupKey}{' '} + + {isExpanded ? '\u25BC' : '\u25B6'} + ); } diff --git a/src/hooks/useViewportColumns.ts b/src/hooks/useViewportColumns.ts index a16464c36f..1cb21d7c1e 100644 --- a/src/hooks/useViewportColumns.ts +++ b/src/hooks/useViewportColumns.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react'; -import { CalculatedColumn } from '../types'; -import { getColumnMetrics, getHorizontalRangeToRender, getViewportColumns } from '../utils'; +import { CalculatedColumn, GroupRow } from '../types'; +import { getColumnMetrics, getHorizontalRangeToRender, getViewportColumns, isGroupedRow } from '../utils'; import { DataGridProps } from '../DataGrid'; import { SELECT_COLUMN_KEY } from '../Columns'; import { ValueFormatter, ToggleGroupedFormatter } from '../formatters'; @@ -40,11 +40,19 @@ export function useViewportColumns({ // Move group columns after the select column const groupByColumns = rawColumns .filter(c => groupBy.includes(c.key)) - .map(c => ({ + .map((c, index) => ({ ...c, frozen: true, rowGroup: true, - groupFormatter: c.groupFormatter ?? ToggleGroupedFormatter + groupFormatter: c.groupFormatter ?? ToggleGroupedFormatter, + formatterOptions: { + focusable(row: R | GroupRow) { + if (isGroupedRow(row)) { + return row.level === index; + } + return false; + } + } })) .sort((c1, c2) => groupBy.findIndex(k => k === c1.key) - groupBy.findIndex(k => k === c2.key)); const remaningColumns = rawColumns.filter(c => !groupBy.includes(c.key) && c.key !== SELECT_COLUMN_KEY); diff --git a/src/hooks/useViewportRows.ts b/src/hooks/useViewportRows.ts index 2a695ac404..09510e3812 100644 --- a/src/hooks/useViewportRows.ts +++ b/src/hooks/useViewportRows.ts @@ -50,12 +50,14 @@ export function useViewportRows({ if (Array.isArray(rows)) return rows; const flattenedRows: Array> = []; Object.keys(rows).forEach((key, posInSet, keys) => { + // TODO: should users have control over the gerenated key? const id = parentKey !== undefined ? `${parentKey}__${key}` : key; const isExpanded = expandedGroupIds?.has(id) ?? false; const { childRows, childGroups, startRowIndex } = rows[key]; flattenedRows.push({ id, key, + parentKey, isExpanded, childRows, level, diff --git a/src/types.ts b/src/types.ts index 50a8d24f04..814af22a42 100644 --- a/src/types.ts +++ b/src/types.ts @@ -22,7 +22,7 @@ export interface Column { /** Formatter to be used to render the cell content */ formatter?: React.ComponentType>; formatterOptions?: { - focusable?: boolean; + focusable?: boolean | ((row: TRow | GroupRow) => boolean); }; /** Formatter to be used to render the summary cell content */ summaryFormatter?: React.ComponentType>; @@ -261,6 +261,7 @@ export interface GroupRow { childRows: TRow[]; id: string; key: unknown; + parentKey: unknown; isExpanded: boolean; level: number; posInSet: number; diff --git a/src/utils/index.ts b/src/utils/index.ts index 77631834c3..de1c0e384f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -7,7 +7,7 @@ export * from './keyboardUtils'; export * from './selectedCellUtils'; export function isGroupedRow(row: R | GroupRow): row is GroupRow { - return (row as GroupRow).__isGroup !== undefined; + return (row as GroupRow).__isGroup === true; } export function assertIsValidKey(key: unknown): asserts key is keyof R { diff --git a/stories/demos/CommonFeatures.tsx b/stories/demos/CommonFeatures.tsx index d55be0ecb7..08dda59616 100644 --- a/stories/demos/CommonFeatures.tsx +++ b/stories/demos/CommonFeatures.tsx @@ -154,7 +154,8 @@ function getColumns(countries: string[]): readonly Column[] { }, { key: 'transaction', - name: 'Transaction type' + name: 'Transaction type', + width: 200 }, { key: 'account', diff --git a/style/cell.less b/style/cell.less index 4ee53d6e0f..ca14ec72e4 100644 --- a/style/cell.less +++ b/style/cell.less @@ -24,7 +24,7 @@ } .rdg-cell-selected { - box-shadow: inset 0 0 0 2px #66afe9; + box-shadow: inset 0 0 0 2px @selectedBorderColor; } .rdg-cell-copied { diff --git a/style/row.less b/style/row.less index 65c0f15fd7..06c3e9b502 100644 --- a/style/row.less +++ b/style/row.less @@ -35,12 +35,13 @@ .rdg-group-row { > .rdg-cell { - border-right-width: 0; + // TODO: fix broken border + border-right-color: transparent; } > .rdg-cell:last-child, > .rdg-cell-frozen-last { - border-right-width: 1px; + border-right-color: @borderColor; } } From c7b7156e9483993d2879453c65088228b181d366 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Thu, 20 Aug 2020 22:08:56 -0500 Subject: [PATCH 22/79] Cleanup --- src/DataGrid.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 465de4635b..6865556a19 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -307,10 +307,10 @@ function DataGrid({ prevSelectedPosition.current = selectedPosition; scrollToCell(selectedPosition); - // Let the formatter handle focus - const formatterOptions = columns[selectedPosition.idx]?.formatterOptions; + const focusable = columns[selectedPosition.idx]?.formatterOptions?.focusable; const row = rows[selectedPosition.rowIdx]; - if ((typeof formatterOptions?.focusable === 'function' && formatterOptions?.focusable(row)) || formatterOptions?.focusable === true) { + if ((typeof focusable === 'function' && focusable(row)) || focusable === true) { + // Let the formatter handle focus return; } focusSinkRef.current!.focus(); @@ -734,6 +734,7 @@ function DataGrid({ return shiftKey ? { idx: columns.length - 1, rowIdx: rows.length - 1 } : { idx: 0, rowIdx: 0 }; } return { idx: idx + (shiftKey ? -1 : 1), rowIdx }; + // TODO: fix keyboard support for group row case 'Home': return ctrlKey ? { idx: 0, rowIdx: 0 } : { idx: 0, rowIdx }; case 'End': From 6d5ab53bac81295ff266ebaccf2fd8b97fe1b67f Mon Sep 17 00:00:00 2001 From: Mahajan Date: Fri, 21 Aug 2020 15:43:25 -0500 Subject: [PATCH 23/79] Handle Home and End keys for GroupRow --- src/DataGrid.tsx | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 677581a4e3..6a1c245f3b 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -654,19 +654,18 @@ function DataGrid({ function selectRow(key: string) { const row = rows[selectedPosition.rowIdx]; if (isGroupedRow(row) && selectedPosition.idx === -1) { - const isRowExpanded = expandedGroupIds?.has(row.id); if ( // If a row is focused, and it is expanded, collaps the current row. - (key === 'ArrowLeft' && isRowExpanded) + (key === 'ArrowLeft' && row.isExpanded) // If a row is focused, and it is collapsed, expand the current row. - || (key === 'ArrowRight' && !isRowExpanded) + || (key === 'ArrowRight' && !row.isExpanded) ) { eventBus.dispatch('TOGGLE_GROUP', row.id); return true; } // If a row is focused, and it is collapsed, move to the parent row (if there is one). - if (key === 'ArrowLeft' && !isRowExpanded && row.level !== 0) { + if (key === 'ArrowLeft' && !row.isExpanded && row.level !== 0) { let parentRowIdx = -1; for (let i = selectedPosition.rowIdx - 1; i >= 0; i--) { const parentRow = rows[i]; @@ -723,6 +722,7 @@ function DataGrid({ function getNextPosition(key: string, ctrlKey: boolean, shiftKey: boolean): Position { const { idx, rowIdx } = selectedPosition; + const isRowSelected = isRowWithinBounds(rowIdx) && idx === -1; switch (key) { case 'ArrowUp': return { idx, rowIdx: rowIdx - 1 }; @@ -737,11 +737,20 @@ function DataGrid({ return shiftKey ? { idx: columns.length - 1, rowIdx: rows.length - 1 } : { idx: 0, rowIdx: 0 }; } return { idx: idx + (shiftKey ? -1 : 1), rowIdx }; - // TODO: fix keyboard support for group row case 'Home': - return ctrlKey ? { idx: 0, rowIdx: 0 } : { idx: 0, rowIdx }; + // Move focus to the first row + if (isRowSelected) return { idx, rowIdx: 0 }; + // Move focus to the first cell in the first row + if (ctrlKey) return { idx: 0, rowIdx: 0 }; + // Move focus to the first cell in the row containing focus + return { idx: 0, rowIdx }; case 'End': - return ctrlKey ? { idx: columns.length - 1, rowIdx: rows.length - 1 } : { idx: columns.length - 1, rowIdx }; + // Move focus to the last row. + if (isRowSelected) return { idx, rowIdx: rows.length - 1 }; + // Move focus to the last cell in the last row + if (ctrlKey) return { idx: columns.length - 1, rowIdx: rows.length - 1 }; + // Moves focus to the last cell in the row that contains focus. + return { idx: columns.length - 1, rowIdx }; case 'PageUp': return { idx, rowIdx: rowIdx - Math.floor(clientHeight / rowHeight) }; case 'PageDown': From f0c894d1f60ce1202cf715b0a604f9ad976186ec Mon Sep 17 00:00:00 2001 From: Mahajan Date: Fri, 21 Aug 2020 16:15:41 -0500 Subject: [PATCH 24/79] Clanup navigation logic --- src/DataGrid.tsx | 81 ++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 6a1c245f3b..547e0ae28f 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -41,7 +41,8 @@ import { CommitEvent, SelectedCellProps, EditCellProps, - Dictionary + Dictionary, + GroupRow } from './types'; import { CellNavigationMode, SortDirection, UpdateActions } from './enums'; @@ -395,9 +396,10 @@ function DataGrid({ * event handlers */ function handleKeyDown(event: React.KeyboardEvent) { + const { key } = event; if (enableCellCopyPaste && isCtrlKeyHeldDown(event) && isCellWithinBounds(selectedPosition)) { - // event.key may be uppercase `C` or `V` - const lowerCaseKey = event.key.toLowerCase(); + // key may be uppercase `C` or `V` + const lowerCaseKey = key.toLowerCase(); if (lowerCaseKey === 'c') { handleCopy(); return; @@ -408,6 +410,21 @@ function DataGrid({ } } + const row = rows[selectedPosition.rowIdx]; + if ( + isGroupedRow(row) + && selectedPosition.idx === -1 + && ( + // Collapse the current row if it is focused and is in expanded state + (key === 'ArrowLeft' && row.isExpanded) + // Expand the current row if it is focused and is in collapsed state + || (key === 'ArrowRight' && !row.isExpanded) + )) { + event.preventDefault(); // Prevents scrolling + eventBus.dispatch('TOGGLE_GROUP', row.id); + return; + } + switch (event.key) { case 'Escape': setCopiedPosition(null); @@ -651,38 +668,6 @@ function DataGrid({ onSelectedCellChange?.({ ...position }); } - function selectRow(key: string) { - const row = rows[selectedPosition.rowIdx]; - if (isGroupedRow(row) && selectedPosition.idx === -1) { - if ( - // If a row is focused, and it is expanded, collaps the current row. - (key === 'ArrowLeft' && row.isExpanded) - // If a row is focused, and it is collapsed, expand the current row. - || (key === 'ArrowRight' && !row.isExpanded) - ) { - eventBus.dispatch('TOGGLE_GROUP', row.id); - return true; - } - - // If a row is focused, and it is collapsed, move to the parent row (if there is one). - if (key === 'ArrowLeft' && !row.isExpanded && row.level !== 0) { - let parentRowIdx = -1; - for (let i = selectedPosition.rowIdx - 1; i >= 0; i--) { - const parentRow = rows[i]; - if (isGroupedRow(parentRow) && parentRow.key === row.parentKey) { - parentRowIdx = i; - break; - } - } - if (parentRowIdx !== -1) { - setSelectedPosition(position => ({ ...position, rowIdx: parentRowIdx })); - return true; - } - } - } - return false; - } - function closeEditor() { if (selectedPosition.mode === 'SELECT') return; setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, mode: 'SELECT' })); @@ -722,7 +707,24 @@ function DataGrid({ function getNextPosition(key: string, ctrlKey: boolean, shiftKey: boolean): Position { const { idx, rowIdx } = selectedPosition; - const isRowSelected = isRowWithinBounds(rowIdx) && idx === -1; + const isGroupRowSelected = isRowWithinBounds(rowIdx) && idx === -1; + + // If a row is focused, and it is collapsed, move to the parent row (if there is one). + if (isGroupRowSelected && key === 'ArrowLeft') { + const row = rows[rowIdx] as GroupRow; + let parentRowIdx = -1; + for (let i = selectedPosition.rowIdx - 1; i >= 0; i--) { + const parentRow = rows[i]; + if (isGroupedRow(parentRow) && parentRow.key === row.parentKey) { + parentRowIdx = i; + break; + } + } + if (parentRowIdx !== -1) { + return { idx, rowIdx: parentRowIdx }; + } + } + switch (key) { case 'ArrowUp': return { idx, rowIdx: rowIdx - 1 }; @@ -739,14 +741,14 @@ function DataGrid({ return { idx: idx + (shiftKey ? -1 : 1), rowIdx }; case 'Home': // Move focus to the first row - if (isRowSelected) return { idx, rowIdx: 0 }; + if (isGroupRowSelected) return { idx, rowIdx: 0 }; // Move focus to the first cell in the first row if (ctrlKey) return { idx: 0, rowIdx: 0 }; // Move focus to the first cell in the row containing focus return { idx: 0, rowIdx }; case 'End': // Move focus to the last row. - if (isRowSelected) return { idx, rowIdx: rows.length - 1 }; + if (isGroupRowSelected) return { idx, rowIdx: rows.length - 1 }; // Move focus to the last cell in the last row if (ctrlKey) return { idx: columns.length - 1, rowIdx: rows.length - 1 }; // Moves focus to the last cell in the row that contains focus. @@ -787,9 +789,6 @@ function DataGrid({ // Do not allow focus to leave event.preventDefault(); - const isRowSelected = selectRow(key); - if (isRowSelected) return; - nextPosition = getNextSelectedCellPosition({ columns, rowsCount: rows.length, From e2f91167c30bc51a145301b39ff258618a1cf10a Mon Sep 17 00:00:00 2001 From: Mahajan Date: Fri, 21 Aug 2020 16:24:23 -0500 Subject: [PATCH 25/79] Cleanup --- src/DataGrid.tsx | 3 +-- src/utils/selectedCellUtils.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 547e0ae28f..8e66de36e4 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -646,8 +646,7 @@ function DataGrid({ } function isCellWithinBounds({ idx, rowIdx }: Position): boolean { - const minIdx = isRowWithinBounds(rowIdx) && isGroupedRow(rows[rowIdx]) ? -1 : 0; - return isRowWithinBounds(rowIdx) && idx >= minIdx && idx < columns.length; + return isRowWithinBounds(rowIdx) && idx >= (isGroupedRow(rows[rowIdx]) ? -1 : 0) && idx < columns.length; } function isCellEditable(position: Position): boolean { diff --git a/src/utils/selectedCellUtils.ts b/src/utils/selectedCellUtils.ts index f305a69e66..958439958e 100644 --- a/src/utils/selectedCellUtils.ts +++ b/src/utils/selectedCellUtils.ts @@ -13,8 +13,7 @@ interface IsSelectedCellEditableOpts { export function isSelectedCellEditable({ selectedPosition, columns, rows, onCheckCellIsEditable }: IsSelectedCellEditableOpts): boolean { const column = columns[selectedPosition.idx]; const row = rows[selectedPosition.rowIdx]; - if (column.rowGroup) return false; - if (isGroupedRow(row)) return false; + if (column.rowGroup || isGroupedRow(row)) return false; const isCellEditable = onCheckCellIsEditable ? onCheckCellIsEditable({ row, column, ...selectedPosition }) : true; return isCellEditable && canEdit(column, row); } From 2deba451b2872eedc653dc5fc2b46179d64eb3cf Mon Sep 17 00:00:00 2001 From: Mahajan Date: Sat, 22 Aug 2020 13:35:43 -0500 Subject: [PATCH 26/79] Improve navigation logic --- src/DataGrid.tsx | 37 +++++++++++++++++-------------------- src/GroupRow.tsx | 24 ++++++------------------ src/Row.tsx | 6 ++++-- 3 files changed, 27 insertions(+), 40 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 8e66de36e4..7aab9c1db0 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -293,6 +293,7 @@ function DataGrid({ }); const hasGroups = groupBy.length > 0 && rowGrouper; + const minColIdx = hasGroups ? -1 : 0; if (hasGroups) { // TODO: finalize if these flags need to be supported on treegrid @@ -412,8 +413,7 @@ function DataGrid({ const row = rows[selectedPosition.rowIdx]; if ( - isGroupedRow(row) - && selectedPosition.idx === -1 + isGroupRowSelected(row) && ( // Collapse the current row if it is focused and is in expanded state (key === 'ArrowLeft' && row.isExpanded) @@ -641,12 +641,8 @@ function DataGrid({ /** * utils */ - function isRowWithinBounds(rowIdx: number) { - return rowIdx >= 0 && rowIdx < rows.length; - } - function isCellWithinBounds({ idx, rowIdx }: Position): boolean { - return isRowWithinBounds(rowIdx) && idx >= (isGroupedRow(rows[rowIdx]) ? -1 : 0) && idx < columns.length; + return rowIdx >= 0 && rowIdx < rows.length && idx >= minColIdx && idx < columns.length; } function isCellEditable(position: Position): boolean { @@ -704,13 +700,21 @@ function DataGrid({ } } + function isGroupRowSelected(row?: R | GroupRow): row is GroupRow { + return row !== undefined && isGroupedRow(row) && selectedPosition.idx === -1; + } + function getNextPosition(key: string, ctrlKey: boolean, shiftKey: boolean): Position { const { idx, rowIdx } = selectedPosition; - const isGroupRowSelected = isRowWithinBounds(rowIdx) && idx === -1; + const row = rows[rowIdx]; - // If a row is focused, and it is collapsed, move to the parent row (if there is one). - if (isGroupRowSelected && key === 'ArrowLeft') { - const row = rows[rowIdx] as GroupRow; + // If a group row is focused, and it is collapsed, move to the parent group row (if there is one). + if ( + key === 'ArrowLeft' + && isGroupRowSelected(row) + && !row.isExpanded + && row.level !== 0 + ) { let parentRowIdx = -1; for (let i = selectedPosition.rowIdx - 1; i >= 0; i--) { const parentRow = rows[i]; @@ -740,14 +744,14 @@ function DataGrid({ return { idx: idx + (shiftKey ? -1 : 1), rowIdx }; case 'Home': // Move focus to the first row - if (isGroupRowSelected) return { idx, rowIdx: 0 }; + if (isGroupRowSelected(row)) return { idx, rowIdx: 0 }; // Move focus to the first cell in the first row if (ctrlKey) return { idx: 0, rowIdx: 0 }; // Move focus to the first cell in the row containing focus return { idx: 0, rowIdx }; case 'End': // Move focus to the last row. - if (isGroupRowSelected) return { idx, rowIdx: rows.length - 1 }; + if (isGroupRowSelected(row)) return { idx, rowIdx: rows.length - 1 }; // Move focus to the last cell in the last row if (ctrlKey) return { idx: columns.length - 1, rowIdx: rows.length - 1 }; // Moves focus to the last cell in the row that contains focus. @@ -765,13 +769,6 @@ function DataGrid({ const { key, shiftKey } = event; const ctrlKey = isCtrlKeyHeldDown(event); let nextPosition = getNextPosition(key, ctrlKey, shiftKey); - if (isRowWithinBounds(nextPosition.rowIdx)) { - const row = rows[nextPosition.rowIdx]; - // Select the first cell when the selected position changes from a group row to regular row - if (!isGroupedRow(row) && nextPosition.idx === -1) { - nextPosition.idx = 0; - } - } let mode = cellNavigationMode; if (key === 'Tab') { // If we are in a position to leave the grid, stop editing but stay in that cell diff --git a/src/GroupRow.tsx b/src/GroupRow.tsx index 9ec15d9ef7..cd71a5a9bd 100644 --- a/src/GroupRow.tsx +++ b/src/GroupRow.tsx @@ -30,19 +30,6 @@ function GroupedRow({ eventBus.dispatch('SELECT_CELL', { rowIdx, idx: -1 }); } - // TODO: how to handle this with cell selection? - // Expand groupBy column widths - // const visibleColumns: CalculatedColumn[] = [...viewportColumns]; - // visibleColumns[idx] = { ...visibleColumns[idx] }; - // let colSpan = 0; - // for (let i = idx + 1; i < visibleColumns.length; i++) { - // const nextColumn = visibleColumns[i]; - // if (!nextColumn.frozen || (nextColumn.groupFormatter && !nextColumn.rowGroup)) break; - // visibleColumns[idx].width += nextColumn.width; - // colSpan++; - // } - // visibleColumns.splice(idx + 1, colSpan); - return (
({ aria-posinset={ariaPosInSet} aria-rowindex={ariaRowIndex} aria-expanded={isExpanded} - className={clsx('rdg-row rdg-group-row', { - 'rdg-row-even': rowIdx % 2 === 0, - 'rdg-row-odd': rowIdx % 2 !== 0, - 'rdg-group-row-selected': selectedCellIdx === -1 // Select row if there is no selected cell - })} + className={clsx( + 'rdg-row', + 'rdg-group-row', + `rdg-row-${rowIdx % 2 === 0 ? 'even' : 'odd'}`, { + 'rdg-group-row-selected': selectedCellIdx === -1 // Select row if there is no selected cell + })} onClick={selectGroup} onKeyDown={onKeyDown} style={{ top }} diff --git a/src/Row.tsx b/src/Row.tsx index d83b0e3d61..dc509ca878 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -33,8 +33,10 @@ function Row({ className = clsx( 'rdg-row', - `rdg-row-${rowIdx % 2 === 0 ? 'even' : 'odd'}`, - { 'rdg-row-selected': isRowSelected }, + `rdg-row-${rowIdx % 2 === 0 ? 'even' : 'odd'}`, { + 'rdg-row-selected': isRowSelected, + 'rdg-group-row-selected': selectedCellProps?.idx === -1 + }, rowClass?.(row), className ); From a63d05122556a157277d0a8eb82cf2935f18cd39 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Sat, 22 Aug 2020 19:59:52 -0500 Subject: [PATCH 27/79] Cleanup --- src/DataGrid.tsx | 61 ++++++++++++++++----------------- src/hooks/useViewportColumns.ts | 4 +-- src/utils/index.ts | 2 +- src/utils/selectedCellUtils.ts | 4 +-- 4 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 7aab9c1db0..324f7b5b45 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -26,7 +26,7 @@ import { canExitGrid, isCtrlKeyHeldDown, isDefaultCellInput, - isGroupedRow + isGroupRow } from './utils'; import { @@ -41,8 +41,7 @@ import { CommitEvent, SelectedCellProps, EditCellProps, - Dictionary, - GroupRow + Dictionary } from './types'; import { CellNavigationMode, SortDirection, UpdateActions } from './enums'; @@ -325,7 +324,7 @@ function DataGrid({ assertIsValidKey(rowKey); const newSelectedRows = new Set(selectedRows); const row = rows[rowIdx]; - if (isGroupedRow(row)) { + if (isGroupRow(row)) { for (const childRow of row.childRows) { if (checked) { newSelectedRows.add(childRow[rowKey]); @@ -346,7 +345,7 @@ function DataGrid({ const step = Math.sign(rowIdx - previousRowIdx); for (let i = previousRowIdx + step; i !== rowIdx; i += step) { const row = rows[i]; - if (isGroupedRow(row)) continue; + if (isGroupRow(row)) continue; newSelectedRows.add(row[rowKey]); } } @@ -398,7 +397,14 @@ function DataGrid({ */ function handleKeyDown(event: React.KeyboardEvent) { const { key } = event; - if (enableCellCopyPaste && isCtrlKeyHeldDown(event) && isCellWithinBounds(selectedPosition)) { + const row = rows[selectedPosition.rowIdx]; + + if ( + enableCellCopyPaste + && isCtrlKeyHeldDown(event) + && isCellWithinBounds(selectedPosition) + && !isGroupRow(row) + ) { // key may be uppercase `C` or `V` const lowerCaseKey = key.toLowerCase(); if (lowerCaseKey === 'c') { @@ -411,13 +417,14 @@ function DataGrid({ } } - const row = rows[selectedPosition.rowIdx]; if ( - isGroupRowSelected(row) + isCellWithinBounds(selectedPosition) + && isGroupRow(row) + && selectedPosition.idx === -1 && ( - // Collapse the current row if it is focused and is in expanded state + // Collapse the current group row if it is focused and is in expanded state (key === 'ArrowLeft' && row.isExpanded) - // Expand the current row if it is focused and is in collapsed state + // Expand the current group row if it is focused and is in collapsed state || (key === 'ArrowRight' && !row.isExpanded) )) { event.preventDefault(); // Prevents scrolling @@ -537,9 +544,9 @@ function DataGrid({ function handleCellInput(event: React.KeyboardEvent) { if (!isCellWithinBounds(selectedPosition)) return; - const { key } = event; const row = rows[selectedPosition.rowIdx]; - if (isGroupedRow(row)) return; + if (isGroupRow(row)) return; + const { key } = event; const column = columns[selectedPosition.idx]; if (selectedPosition.mode === 'EDIT') { @@ -700,25 +707,23 @@ function DataGrid({ } } - function isGroupRowSelected(row?: R | GroupRow): row is GroupRow { - return row !== undefined && isGroupedRow(row) && selectedPosition.idx === -1; - } - function getNextPosition(key: string, ctrlKey: boolean, shiftKey: boolean): Position { const { idx, rowIdx } = selectedPosition; const row = rows[rowIdx]; + const isRowSelected = isCellWithinBounds(selectedPosition) && idx === -1; // If a group row is focused, and it is collapsed, move to the parent group row (if there is one). if ( key === 'ArrowLeft' - && isGroupRowSelected(row) + && isRowSelected + && isGroupRow(row) && !row.isExpanded && row.level !== 0 ) { let parentRowIdx = -1; for (let i = selectedPosition.rowIdx - 1; i >= 0; i--) { const parentRow = rows[i]; - if (isGroupedRow(parentRow) && parentRow.key === row.parentKey) { + if (isGroupRow(parentRow) && parentRow.key === row.parentKey) { parentRowIdx = i; break; } @@ -743,19 +748,13 @@ function DataGrid({ } return { idx: idx + (shiftKey ? -1 : 1), rowIdx }; case 'Home': - // Move focus to the first row - if (isGroupRowSelected(row)) return { idx, rowIdx: 0 }; - // Move focus to the first cell in the first row - if (ctrlKey) return { idx: 0, rowIdx: 0 }; - // Move focus to the first cell in the row containing focus - return { idx: 0, rowIdx }; + // If row is selected then move focus to the first row + if (isRowSelected) return { idx, rowIdx: 0 }; + return ctrlKey ? { idx: 0, rowIdx: 0 } : { idx: 0, rowIdx }; case 'End': - // Move focus to the last row. - if (isGroupRowSelected(row)) return { idx, rowIdx: rows.length - 1 }; - // Move focus to the last cell in the last row - if (ctrlKey) return { idx: columns.length - 1, rowIdx: rows.length - 1 }; - // Moves focus to the last cell in the row that contains focus. - return { idx: columns.length - 1, rowIdx }; + // If row is selected then move focus to the last row. + if (isRowSelected) return { idx, rowIdx: rows.length - 1 }; + return ctrlKey ? { idx: columns.length - 1, rowIdx: rows.length - 1 } : { idx: columns.length - 1, rowIdx }; case 'PageUp': return { idx, rowIdx: rowIdx - Math.floor(clientHeight / rowHeight) }; case 'PageDown': @@ -848,7 +847,7 @@ function DataGrid({ for (let rowIdx = rowOverscanStartIdx; rowIdx <= rowOverscanEndIdx; rowIdx++) { const row = rows[rowIdx]; const top = rowIdx * rowHeight + totalHeaderHeight; - if (isGroupedRow(row)) { + if (isGroupRow(row)) { ({ startRowIndex } = row); rowElements.push( diff --git a/src/hooks/useViewportColumns.ts b/src/hooks/useViewportColumns.ts index 1cb21d7c1e..4105355ed7 100644 --- a/src/hooks/useViewportColumns.ts +++ b/src/hooks/useViewportColumns.ts @@ -1,7 +1,7 @@ import { useMemo } from 'react'; import { CalculatedColumn, GroupRow } from '../types'; -import { getColumnMetrics, getHorizontalRangeToRender, getViewportColumns, isGroupedRow } from '../utils'; +import { getColumnMetrics, getHorizontalRangeToRender, getViewportColumns, isGroupRow } from '../utils'; import { DataGridProps } from '../DataGrid'; import { SELECT_COLUMN_KEY } from '../Columns'; import { ValueFormatter, ToggleGroupedFormatter } from '../formatters'; @@ -47,7 +47,7 @@ export function useViewportColumns({ groupFormatter: c.groupFormatter ?? ToggleGroupedFormatter, formatterOptions: { focusable(row: R | GroupRow) { - if (isGroupedRow(row)) { + if (isGroupRow(row)) { return row.level === index; } return false; diff --git a/src/utils/index.ts b/src/utils/index.ts index de1c0e384f..3bbbbf6651 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -6,7 +6,7 @@ export * from './viewportUtils'; export * from './keyboardUtils'; export * from './selectedCellUtils'; -export function isGroupedRow(row: R | GroupRow): row is GroupRow { +export function isGroupRow(row: R | GroupRow): row is GroupRow { return (row as GroupRow).__isGroup === true; } diff --git a/src/utils/selectedCellUtils.ts b/src/utils/selectedCellUtils.ts index 958439958e..4cd496c7ee 100644 --- a/src/utils/selectedCellUtils.ts +++ b/src/utils/selectedCellUtils.ts @@ -1,7 +1,7 @@ import { CellNavigationMode } from '../enums'; import { canEdit } from './columnUtils'; import { CalculatedColumn, Position, GroupRow } from '../types'; -import { isGroupedRow } from '.'; +import { isGroupRow } from '.'; interface IsSelectedCellEditableOpts { selectedPosition: Position; @@ -13,7 +13,7 @@ interface IsSelectedCellEditableOpts { export function isSelectedCellEditable({ selectedPosition, columns, rows, onCheckCellIsEditable }: IsSelectedCellEditableOpts): boolean { const column = columns[selectedPosition.idx]; const row = rows[selectedPosition.rowIdx]; - if (column.rowGroup || isGroupedRow(row)) return false; + if (column.rowGroup || isGroupRow(row)) return false; const isCellEditable = onCheckCellIsEditable ? onCheckCellIsEditable({ row, column, ...selectedPosition }) : true; return isCellEditable && canEdit(column, row); } From 410e248fe891f5e9c54f4e83acce2d171358df3f Mon Sep 17 00:00:00 2001 From: Mahajan Date: Mon, 24 Aug 2020 12:45:02 -0500 Subject: [PATCH 28/79] Handle copy/paste --- src/DataGrid.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index c34f1aba87..ef1009a35e 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -295,8 +295,7 @@ function DataGrid({ const minColIdx = hasGroups ? -1 : 0; if (hasGroups) { - // TODO: finalize if these flags need to be supported on treegrid - enableCellDragAndDrop = false; + // TODO: finalize if cell drag needs to be supported for treegrid enableCellDragAndDrop = false; } @@ -409,6 +408,7 @@ function DataGrid({ && isCtrlKeyHeldDown(event) && isCellWithinBounds(selectedPosition) && !isGroupRow(row) + && selectedPosition.idx !== -1 ) { // key may be uppercase `C` or `V` const lowerCaseKey = key.toLowerCase(); @@ -506,7 +506,8 @@ function DataGrid({ function handleCopy() { const { idx, rowIdx } = selectedPosition; - const value = rawRows[rowIdx][columns[idx].key as keyof R]; + const rawRowIdx = getRawRowIdx(rowIdx); + const value = rawRows[rawRowIdx][columns[idx].key as keyof R]; setCopiedPosition({ idx, rowIdx, value }); } @@ -519,17 +520,16 @@ function DataGrid({ return; } - const { rowIdx: toRow } = selectedPosition; - + const fromRow = getRawRowIdx(copiedPosition.rowIdx); + const fromCellKey = columns[copiedPosition.idx].key; + const toRow = getRawRowIdx(selectedPosition.rowIdx); const cellKey = columns[selectedPosition.idx].key; - const { rowIdx: fromRow, idx, value } = copiedPosition; - const fromCellKey = columns[idx].key; onRowsUpdate?.({ cellKey, fromRow, toRow, - updated: { [cellKey]: value } as unknown as never, + updated: { [cellKey]: copiedPosition.value } as unknown as never, action: UpdateActions.COPY_PASTE, fromCellKey }); From 58beda751aaa1a86ce8048aec7b6af06de360cad Mon Sep 17 00:00:00 2001 From: Mahajan Date: Mon, 24 Aug 2020 14:35:08 -0500 Subject: [PATCH 29/79] Grouping story --- stories/demos/CommonFeatures.tsx | 14 +-- stories/demos/Grouping.tsx | 108 +++++++++++++++++++++ stories/demos/LegacyGrouping.tsx | 156 ------------------------------- stories/index.tsx | 4 +- 4 files changed, 111 insertions(+), 171 deletions(-) create mode 100644 stories/demos/Grouping.tsx delete mode 100644 stories/demos/LegacyGrouping.tsx diff --git a/stories/demos/CommonFeatures.tsx b/stories/demos/CommonFeatures.tsx index 08dda59616..309cf0d311 100644 --- a/stories/demos/CommonFeatures.tsx +++ b/stories/demos/CommonFeatures.tsx @@ -1,7 +1,6 @@ import React, { useState, useCallback, useMemo } from 'react'; import faker from 'faker'; import { AutoSizer } from 'react-virtualized'; -import { groupBy as rowGrouper } from 'lodash'; import DataGrid, { SelectColumn, Column, RowsUpdateEvent, SortDirection } from '../../src'; import { TextEditor } from './components/Editors/TextEditor'; import { SelectEditor } from './components/Editors/SelectEditor'; @@ -146,16 +145,11 @@ function getColumns(countries: string[]): readonly Column[] { width: 100, formatter(props) { return ; - }, - groupFormatter({ childRows }) { - const totals = childRows.reduce((prev, { budget }) => prev + budget, 0); - return ; } }, { key: 'transaction', - name: 'Transaction type', - width: 200 + name: 'Transaction type' }, { key: 'account', @@ -214,8 +208,6 @@ export default function CommonFeatures() { const [rows, setRows] = useState(createRows); const [[sortColumn, sortDirection], setSort] = useState<[string, SortDirection]>(['id', 'NONE']); const [selectedRows, setSelectedRows] = useState(() => new Set()); - const [expandedGroupIds, setExpandedGroupIds] = useState>(new Set()); - const [groupBy] = useState(['country', 'transaction']); const countries = useMemo(() => { return [...new Set(rows.map(r => r.country))].sort(); @@ -296,10 +288,6 @@ export default function CommonFeatures() { sortDirection={sortDirection} onSort={handleSort} summaryRows={summaryRows} - groupBy={groupBy} - rowGrouper={rowGrouper} - expandedGroupIds={expandedGroupIds} - onExpandedGroupIdsChange={setExpandedGroupIds} /> )} diff --git a/stories/demos/Grouping.tsx b/stories/demos/Grouping.tsx new file mode 100644 index 0000000000..d756d2e289 --- /dev/null +++ b/stories/demos/Grouping.tsx @@ -0,0 +1,108 @@ +import React, { useState } from 'react'; +import { AutoSizer } from 'react-virtualized'; +import { groupBy as rowGrouper } from 'lodash'; +import faker from 'faker'; + +import DataGrid, { Column, Row, SelectColumn } from '../../src'; + +interface Row { + id: number; + country: string; + year: number; + athlete: string; + gold: number; + silver: number; + bronze: number; +} + +const columns: Column[] = [ + SelectColumn, + { + key: 'country', + name: 'Country' + }, + { + key: 'year', + name: 'Year' + }, + { + key: 'athlete', + name: 'Athlete' + }, + { + key: 'gold', + name: 'Gold', + groupFormatter({ childRows }) { + return <>{childRows.reduce((prev, { gold }) => prev + gold, 0)}; + } + }, + { + key: 'silver', + name: 'Silver', + groupFormatter({ childRows }) { + return <>{childRows.reduce((prev, { silver }) => prev + silver, 0)}; + } + }, + { + key: 'bronze', + name: 'Bronze', + groupFormatter({ childRows }) { + return <>{childRows.reduce((prev, { silver }) => prev + silver, 0)}; + } + }, + { + key: 'total', + name: 'Total', + formatter({ row }) { + return <>{row.gold + row.silver + row.bronze}; + }, + groupFormatter({ childRows }) { + return <>{childRows.reduce((prev, row) => prev + row.gold + row.silver + row.bronze, 0)}; + } + } +]; + +function createRows(): Row[] { + const rows: Row[] = []; + for (let i = 1; i < 10000; i++) { + rows.push({ + id: i, + year: 2015 + faker.random.number(3), + country: faker.address.country(), + athlete: faker.name.findName(), + gold: faker.random.number(5), + silver: faker.random.number(5), + bronze: faker.random.number(5) + }); + } + + return rows.sort((r1, r2) => r1.country.localeCompare(r2.country)); +} + +export default function Grouping() { + const [rows] = useState(createRows); + const [selectedRows, setSelectedRows] = useState(() => new Set()); + const [groupBy] = useState(['country', 'year']); + const [expandedGroupIds, setExpandedGroupIds] = useState>(new Set()); + + return ( + + {({ height, width }) => ( + + )} + + ); +} diff --git a/stories/demos/LegacyGrouping.tsx b/stories/demos/LegacyGrouping.tsx deleted file mode 100644 index d69eab1060..0000000000 --- a/stories/demos/LegacyGrouping.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { groupBy } from 'lodash'; - -import DataGrid, { Column, Row, RowRendererProps } from '../../src'; -import './LegacyGrouping.less'; - -interface RowGroupMetaData { - groupId: string; - groupKey: string; - treeDepth: number; - isExpanded: boolean; - columnGroupName: string; -} - -interface RowGroup { - __metaData: RowGroupMetaData; -} - -interface RowData { - id: number; - task: string; - complete: number; - priority: string; - issueType: string; -} - -type GridRow = RowGroup | RowData; - -interface GroupRowRendererProps extends RowRendererProps { - onRowExpandToggle: (groupId: string) => void; -} - -const columns: Column[] = [ - { - key: 'id', - name: 'ID', - width: 80 - }, - { - key: 'task', - name: 'Title' - }, - { - key: 'priority', - name: 'Priority' - }, - { - key: 'issueType', - name: 'Issue Type' - }, - { - key: 'complete', - name: '% Complete' - } -]; - -const groups = ['priority', 'issueType']; - -function createRows(): GridRow[] { - const rows = []; - 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() * 3) + 1)], - issueType: ['Bug', 'Improvement', 'Epic', 'Story'][Math.floor((Math.random() * 3) + 1)] - }); - } - - return rows; -} - -function groupByColumn(rows: GridRow[], columnKeys: string[], expandedGroups: Set, treeDepth = 0, parentId = '') { - if (columnKeys.length === 0) return rows; - const gridRows: GridRow[] = []; - const [columnKey, ...remainingColumnKeys] = columnKeys; - const groupedRows = groupBy(rows, columnKey); - const groupedKeys = Object.keys(groupedRows); - - for (const groupKey of groupedKeys) { - const groupId = parentId ? `${parentId}_${groupKey}` : groupKey; - const isExpanded = expandedGroups.has(groupId); - const rowGroupHeader: GridRow = { - __metaData: { - groupId, - groupKey, - treeDepth, - isExpanded, - columnGroupName: columns.find(c => c.key === columnKey)!.name - } - }; - gridRows.push(rowGroupHeader); - if (isExpanded) { - gridRows.push(...groupByColumn(groupedRows[groupKey], remainingColumnKeys, expandedGroups, treeDepth + 1, groupId)); - } - } - - return gridRows; -} - -function isRowGroup(row: GridRow): row is RowGroup { - return typeof (row as RowGroup).__metaData !== 'undefined'; -} - -function GroupRowRenderer({ onRowExpandToggle, ...props }: GroupRowRendererProps) { - if (!isRowGroup(props.row)) { - return ; - } - - const { groupKey, isExpanded, treeDepth, columnGroupName, groupId } = props.row.__metaData; - return ( -
- onRowExpandToggle(groupId)} - > - {isExpanded ? String.fromCharCode(9660) : String.fromCharCode(9658)} - - {' '}{columnGroupName}: {groupKey} -
- ); -} - -export default function LegacyGrouping() { - const [rows] = useState(createRows); - const [expandedGroups, setExpandedGroups] = useState(() => new Set(['Low', 'Low_Epic'])); - - const gridRows = useMemo(() => { - return groupByColumn(rows, groups, expandedGroups); - }, [rows, expandedGroups]); - - function onRowExpandToggle(groupId: string) { - const newExpandedGroups = new Set(expandedGroups); - if (newExpandedGroups.has(groupId)) { - newExpandedGroups.delete(groupId); - } else { - newExpandedGroups.add(groupId); - } - setExpandedGroups(newExpandedGroups); - } - - return ( - } - /> - ); -} diff --git a/stories/index.tsx b/stories/index.tsx index 363d8f51c4..5c9addfdef 100644 --- a/stories/index.tsx +++ b/stories/index.tsx @@ -16,7 +16,7 @@ import CellNavigation from './demos/CellNavigation'; import HeaderFilters from './demos/HeaderFilters'; import ColumnsReordering from './demos/ColumnsReordering'; import RowsReordering from './demos/RowsReordering'; -import LegacyGrouping from './demos/LegacyGrouping'; +import Grouping from './demos/Grouping'; storiesOf('Demos', module) .add('Common Features', () => ) @@ -31,4 +31,4 @@ storiesOf('Demos', module) .add('Header Filters', () => ) .add('Columns Reordering', () => ) .add('Rows Reordering', () => ) - .add('Legacy Grouping', () => ); + .add('Grouping', () => ); From 0142a4dab65860153693395f65eacab0295f3a4e Mon Sep 17 00:00:00 2001 From: Mahajan Date: Mon, 24 Aug 2020 16:54:01 -0500 Subject: [PATCH 30/79] Add select to choose group by field --- stories/demos/Grouping.tsx | 74 +++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 21 deletions(-) diff --git a/stories/demos/Grouping.tsx b/stories/demos/Grouping.tsx index d756d2e289..63ad9dfc1b 100644 --- a/stories/demos/Grouping.tsx +++ b/stories/demos/Grouping.tsx @@ -1,6 +1,7 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { AutoSizer } from 'react-virtualized'; import { groupBy as rowGrouper } from 'lodash'; +import Select from 'react-select'; import faker from 'faker'; import DataGrid, { Column, Row, SelectColumn } from '../../src'; @@ -9,12 +10,15 @@ interface Row { id: number; country: string; year: number; + sport: string; athlete: string; gold: number; silver: number; bronze: number; } +const sports = ['Swimming', 'Gymnastics', 'Speed Skating', 'Cross Country Skiing', 'Short-Track Speed Skating', 'Diving', 'Cycling', 'Biathlon', 'Alpine Skiing', 'Ski Jumping', 'Nordic Combined', 'Athletics', 'Table Tennis', 'Tennis', 'Synchronized Swimming', 'Shooting', 'Rowing', 'Fencing', 'Equestrian', 'Canoeing', 'Bobsleigh', 'Badminton', 'Archery', 'Wrestling', 'Weightlifting', 'Waterpolo', 'Wrestling', 'Weightlifting']; + const columns: Column[] = [ SelectColumn, { @@ -25,6 +29,10 @@ const columns: Column[] = [ key: 'year', name: 'Year' }, + { + key: 'sport', + name: 'Sport' + }, { key: 'athlete', name: 'Athlete' @@ -69,6 +77,7 @@ function createRows(): Row[] { id: i, year: 2015 + faker.random.number(3), country: faker.address.country(), + sport: sports[faker.random.number(sports.length)], athlete: faker.name.findName(), gold: faker.random.number(5), silver: faker.random.number(5), @@ -76,33 +85,56 @@ function createRows(): Row[] { }); } - return rows.sort((r1, r2) => r1.country.localeCompare(r2.country)); + return rows.sort((r1, r2) => r2.country.localeCompare(r1.country)); } +const options = [ + { value: 'country', label: 'Country' }, + { value: 'year', label: 'Year' }, + { value: 'sport', label: 'Sport' } +]; + export default function Grouping() { const [rows] = useState(createRows); const [selectedRows, setSelectedRows] = useState(() => new Set()); - const [groupBy] = useState(['country', 'year']); - const [expandedGroupIds, setExpandedGroupIds] = useState>(new Set()); + const [selectedOptions, setSelectedOptions] = useState([options[0], options[1]]); + const [expandedGroupIds, setExpandedGroupIds] = useState>(new Set(['United States of America', 'United States of America__2015'])); + + const groupBy = useMemo(() => selectedOptions === null ? undefined : selectedOptions.map(o => o.value), [selectedOptions]); return ( - - {({ height, width }) => ( - + From 0b1c8d238535b66c8a0a40df7029c7d1e7f7338b Mon Sep 17 00:00:00 2001 From: Mahajan Date: Tue, 25 Aug 2020 06:05:02 -0500 Subject: [PATCH 32/79] Cleanup --- stories/demos/Grouping.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/stories/demos/Grouping.tsx b/stories/demos/Grouping.tsx index d91a4504c4..835771c75b 100644 --- a/stories/demos/Grouping.tsx +++ b/stories/demos/Grouping.tsx @@ -18,6 +18,11 @@ interface Row { bronze: number; } +interface Option { + value: string; + label: string; +} + const sports = ['Swimming', 'Gymnastics', 'Speed Skating', 'Cross Country Skiing', 'Short-Track Speed Skating', 'Diving', 'Cycling', 'Biathlon', 'Alpine Skiing', 'Ski Jumping', 'Nordic Combined', 'Athletics', 'Table Tennis', 'Tennis', 'Synchronized Swimming', 'Shooting', 'Rowing', 'Fencing', 'Equestrian', 'Canoeing', 'Bobsleigh', 'Badminton', 'Archery', 'Wrestling', 'Weightlifting', 'Waterpolo', 'Wrestling', 'Weightlifting']; const columns: Column[] = [ @@ -111,15 +116,17 @@ const options = [ export default function Grouping() { const [rows] = useState(createRows); const [selectedRows, setSelectedRows] = useState(() => new Set()); - const [selectedOptions, setSelectedOptions] = useState([options[0], options[1]]); + const [selectedOptions, setSelectedOptions] = useState([options[0], options[1]]); const [expandedGroupIds, setExpandedGroupIds] = useState>(new Set(['United States of America', 'United States of America__2015'])); - const groupBy = useMemo(() => selectedOptions === null ? undefined : selectedOptions.map(o => o.value), [selectedOptions]); + const groupBy = useMemo(() => selectedOptions?.map(o => o.value), [selectedOptions]); function onSortEnd({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) { + if (!selectedOptions) return; const newOptions = [...selectedOptions]; newOptions.splice(newIndex < 0 ? newOptions.length + newIndex : newIndex, 0, newOptions.splice(oldIndex, 1)[0]); setSelectedOptions(newOptions); + setExpandedGroupIds(new Set()); } return ( @@ -132,7 +139,7 @@ export default function Grouping() { onSortEnd={onSortEnd} distance={4} getHelperDimensions={({ node }) => node.getBoundingClientRect()} - // react-select props: + // react-select props isMulti value={selectedOptions} onChange={options => { From 11c10d0fab1ce7e8c075d3741605679372b05d86 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Tue, 25 Aug 2020 07:23:54 -0500 Subject: [PATCH 33/79] Improve types --- stories/demos/Grouping.tsx | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/stories/demos/Grouping.tsx b/stories/demos/Grouping.tsx index 835771c75b..5aec112265 100644 --- a/stories/demos/Grouping.tsx +++ b/stories/demos/Grouping.tsx @@ -1,11 +1,12 @@ import React, { useState, useMemo } from 'react'; import { AutoSizer } from 'react-virtualized'; import { groupBy as rowGrouper } from 'lodash'; -import Select, { components } from 'react-select'; +import Select, { components, ValueType, OptionsType, Props as SelectProps } from 'react-select'; import { SortableContainer, SortableElement } from 'react-sortable-hoc'; import faker from 'faker'; import DataGrid, { Column, Row, SelectColumn } from '../../src'; +import Option from 'react-select/src/components/Option'; interface Row { id: number; @@ -104,9 +105,9 @@ const SortableMultiValue = SortableElement((props: any) => { return ; }); -const SortableSelect = SortableContainer(Select); +const SortableSelect = SortableContainer>(Select); -const options = [ +const options: OptionsType