From 1b998a3fe6db0a12ca14bb9ce01e7d629a0253b4 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Tue, 23 Jun 2020 17:14:40 -0500 Subject: [PATCH 01/42] Initial commit --- src/Cell.tsx | 25 ++++- src/DataGrid.tsx | 185 ++++++++++++++++++++++++++++++----- src/EventBus.ts | 1 + src/Row.tsx | 4 + src/common/types.ts | 4 + src/hooks/index.ts | 1 + src/hooks/useCombinedRefs.ts | 10 ++ src/utils/index.ts | 14 +++ style/cell.less | 9 ++ 9 files changed, 223 insertions(+), 30 deletions(-) create mode 100644 src/hooks/index.ts create mode 100644 src/hooks/useCombinedRefs.ts diff --git a/src/Cell.tsx b/src/Cell.tsx index f9a22b250b..39fb718d26 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -1,12 +1,15 @@ -import React, { forwardRef, memo } from 'react'; +import React, { forwardRef, memo, useEffect, useRef } from 'react'; import clsx from 'clsx'; import { CellRendererProps } from './common/types'; import { preventDefault, wrapEvent } from './utils'; +import { useCombinedRefs } from './hooks'; function Cell({ className, column, + isSelected, + isCopied, isRowSelected, lastFrozenColumnIndex, row, @@ -15,10 +18,12 @@ function Cell({ onRowClick, onClick, onDoubleClick, + onKeyDown, onContextMenu, onDragOver, ...props }: CellRendererProps, ref: React.Ref) { + const cellRef = useRef(null); function selectCell(openEditor?: boolean) { eventBus.dispatch('SELECT_CELL', { idx: column.idx, rowIdx }, openEditor); } @@ -28,6 +33,10 @@ function Cell({ onRowClick?.(rowIdx, row, column); } + function handleKeyDown(event: React.KeyboardEvent) { + eventBus.dispatch('CELL_KEYDOWN', event); + } + function handleCellContextMenu() { selectCell(); } @@ -40,12 +49,20 @@ function Cell({ eventBus.dispatch('SELECT_ROW', { rowIdx, checked, isShiftClick }); } + useEffect(() => { + if (isSelected) { + cellRef.current?.focus(); + } + }, [isSelected]); + const { cellClass } = column; 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': isSelected, + 'rdg-cell-copied': isCopied }, typeof cellClass === 'function' ? cellClass(row) : cellClass, className @@ -53,8 +70,9 @@ function Cell({ return (
({ onDoubleClick={wrapEvent(handleCellDoubleClick, onDoubleClick)} onContextMenu={wrapEvent(handleCellContextMenu, onContextMenu)} onDragOver={wrapEvent(preventDefault, onDragOver)} + onKeyDown={wrapEvent(handleKeyDown, onKeyDown)} {...props} > void; @@ -222,6 +235,14 @@ function DataGrid({ }); }, [columnWidths, rawColumns, defaultFormatter, minColumnWidth, viewportWidth]); + const [selectedPosition, setSelectedPosition] = useState(() => { + if (enableCellAutoFocus && document.activeElement === document.body && columns.length > 0 && rows.length > 0) { + return { idx: 0, rowIdx: 0, status: 'SELECT' }; + } + return { idx: -1, rowIdx: -1, status: 'SELECT' }; + }); + const [copiedPosition, setCopiedPosition] = useState(null); + const [colOverscanStartIdx, colOverscanEndIdx] = useMemo((): [number, number] => { return getHorizontalRangeToRender( columns, @@ -300,6 +321,14 @@ function DataGrid({ return eventBus.subscribe('SELECT_ROW', handleRowSelectionChange); }, [eventBus, onSelectedRowsChange, rows, rowKey, selectedRows]); + useEffect(() => { + return eventBus.subscribe('SELECT_CELL', selectCell); + }); + + useEffect(() => { + return eventBus.subscribe('CELL_KEYDOWN', onKeyDown); + }); + useImperativeHandle(ref, () => ({ scrollToColumn(idx: number) { scrollToCell({ idx }); @@ -332,13 +361,134 @@ function DataGrid({ onColumnResize?.(column.idx, width); }, [columnWidths, onColumnResize]); - function handleRowsUpdate(event: RowsUpdateEvent) { - onRowsUpdate?.(event); + function onKeyDown(event: React.KeyboardEvent): void { + const column = columns[selectedPosition.idx]; + const row = rows[selectedPosition.rowIdx]; + const isActivatedByUser = (column.unsafe_onCellInput ?? legacyCellInput)(event, row) === true; + + const { key } = event; + if (enableCellCopyPaste && isCtrlKeyHeldDown(event)) { + // event.key may be uppercase `C` or `V` + const lowerCaseKey = event.key.toLowerCase(); + if (lowerCaseKey === 'c') return handleCopy(); + if (lowerCaseKey === 'v') return handlePaste(); + } + + const canOpenEditor = selectedPosition.status === 'SELECT' && isCellEditable(selectedPosition); + + switch (key) { + case 'Enter': + if (canOpenEditor) { + setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, status: 'EDIT', key: 'Enter' })); + } else if (selectedPosition.status === 'EDIT') { + setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, status: 'SELECT' })); + } + break; + case 'Escape': + // closeEditor(); + setCopiedPosition(null); + break; + // case 'Tab': + // onPressTab(event); + // break; + case 'ArrowUp': + case 'ArrowDown': + case 'ArrowLeft': + case 'ArrowRight': + event.preventDefault(); + selectCell(getNextPosition(key)); + break; + default: + if (canOpenEditor && isActivatedByUser) { + setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, status: 'EDIT', key })); + } + break; + } } /** * utils */ + function isCellWithinBounds({ idx, rowIdx }: Position): boolean { + return rowIdx >= 0 && rowIdx < rows.length && idx >= 0 && idx < columns.length; + } + + function isCellEditable(position: Position) { + return isCellWithinBounds(position) + && isSelectedCellEditable({ columns, rows, selectedPosition: position, onCheckCellIsEditable }); + } + + function selectCell(position: Position, enableEditor = false): void { + if (!isCellWithinBounds(position)) return; + + if (enableEditor && isCellEditable(position)) { + setSelectedPosition({ ...position, status: 'EDIT', key: null }); + } else { + setSelectedPosition({ ...position, status: 'SELECT' }); + } + scrollToCell(position); + onSelectedCellChange?.({ ...position }); + } + + function getNextPosition(key: string, mode = cellNavigationMode, shiftKey = false) { + const { idx, rowIdx } = selectedPosition; + let nextPosition: Position; + switch (key) { + case 'ArrowUp': + nextPosition = { idx, rowIdx: rowIdx - 1 }; + break; + case 'ArrowDown': + nextPosition = { idx, rowIdx: rowIdx + 1 }; + break; + case 'ArrowLeft': + nextPosition = { idx: idx - 1, rowIdx }; + break; + case 'ArrowRight': + nextPosition = { idx: idx + 1, rowIdx }; + break; + case 'Tab': + nextPosition = { idx: idx + (shiftKey ? -1 : 1), rowIdx }; + break; + default: + nextPosition = { idx, rowIdx }; + break; + } + + return getNextSelectedCellPosition({ + columns, + rowsCount: rows.length, + cellNavigationMode: mode, + nextPosition + }); + } + + function handleCopy(): void { + const { idx, rowIdx } = selectedPosition; + const value = rows[rowIdx][columns[idx].key as keyof R]; + setCopiedPosition({ idx, rowIdx, value }); + } + + function handlePaste(): void { + if (copiedPosition === null || !isCellEditable(selectedPosition)) { + return; + } + + const { rowIdx: toRow } = selectedPosition; + + 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, + action: UpdateActions.COPY_PASTE, + fromCellKey + }); + } + function getFrozenColumnsWidth() { if (lastFrozenColumnIndex === -1) return 0; const lastFrozenCol = columns[lastFrozenColumnIndex]; @@ -393,6 +543,8 @@ function DataGrid({ row={row} viewportColumns={viewportColumns} lastFrozenColumnIndex={lastFrozenColumnIndex} + selectedCellIdx={selectedPosition.rowIdx === rowIdx ? selectedPosition.idx : undefined} + copiedCellIdx={copiedPosition?.rowIdx === rowIdx ? copiedPosition.idx : undefined} eventBus={eventBus} isRowSelected={isRowSelected} onRowClick={onRowClick} @@ -441,29 +593,8 @@ function DataGrid({ )} {rows.length === 0 && emptyRowsRenderer ? createElement(emptyRowsRenderer) : ( <> - {viewportWidth > 0 && ( - - rows={rows} - rowHeight={rowHeight} - columns={columns} - enableCellAutoFocus={enableCellAutoFocus} - enableCellCopyPaste={enableCellCopyPaste} - enableCellDragAndDrop={enableCellDragAndDrop} - cellNavigationMode={cellNavigationMode} - eventBus={eventBus} - gridRef={gridRef} - totalHeaderHeight={totalHeaderHeight} - scrollLeft={scrollLeft} - scrollTop={scrollTop} - scrollToCell={scrollToCell} - editorPortalTarget={editorPortalTarget} - onCheckCellIsEditable={onCheckCellIsEditable} - onRowsUpdate={handleRowsUpdate} - onSelectedCellChange={onSelectedCellChange} - /> - )}
- {getViewportRows()} + {viewportWidth > 0 && getViewportRows()} {summaryRows?.map((row, rowIdx) => ( key={rowIdx} diff --git a/src/EventBus.ts b/src/EventBus.ts index 0ca931a62a..fd691e7b7c 100644 --- a/src/EventBus.ts +++ b/src/EventBus.ts @@ -2,6 +2,7 @@ import { Position, SelectRowEvent } from './common/types'; interface EventMap { SELECT_CELL: (position: Position, enableEditor?: boolean) => void; + CELL_KEYDOWN: (event: React.KeyboardEvent) => void; SELECT_ROW: (event: SelectRowEvent) => void; DRAG_ENTER: (overRowIdx: number) => void; } diff --git a/src/Row.tsx b/src/Row.tsx index 08782154ac..a0eaa56053 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -12,6 +12,8 @@ function Row({ rowIdx, isRowSelected, lastFrozenColumnIndex, + selectedCellIdx, + copiedCellIdx, onRowClick, row, viewportColumns, @@ -60,6 +62,8 @@ function Row({ column={column} lastFrozenColumnIndex={lastFrozenColumnIndex} row={row} + isSelected={selectedCellIdx === column.idx} + isCopied={copiedCellIdx === column.idx} isRowSelected={isRowSelected} eventBus={eventBus} onRowClick={onRowClick} diff --git a/src/common/types.ts b/src/common/types.ts index f6381880e4..8119ebd695 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -109,6 +109,8 @@ export interface CellRendererProps extends Omit) => void; } @@ -119,6 +121,8 @@ export interface RowRendererProps extends Omit>; rowIdx: number; lastFrozenColumnIndex: number; + selectedCellIdx?: number; + copiedCellIdx?: number; isRowSelected: boolean; eventBus: EventBus; onRowClick?: (rowIdx: number, row: TRow, column: CalculatedColumn) => void; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000000..0a613bf24b --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1 @@ +export * from './useCombinedRefs'; diff --git a/src/hooks/useCombinedRefs.ts b/src/hooks/useCombinedRefs.ts new file mode 100644 index 0000000000..d9b985cfcf --- /dev/null +++ b/src/hooks/useCombinedRefs.ts @@ -0,0 +1,10 @@ +import { useMemo } from 'react'; +import { wrapRefs } from '../utils'; + +export function useCombinedRefs(...refs: readonly React.Ref[]) { + return useMemo( + () => wrapRefs(...refs), + // eslint-disable-next-line react-hooks/exhaustive-deps + refs + ); +} diff --git a/src/utils/index.ts b/src/utils/index.ts index f07e14fdf2..07f4f17083 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -9,3 +9,17 @@ export function assertIsValidKey(key: unknown): asserts key is keyof R { throw new Error('Please specify the rowKey prop to use selection'); } } + +export function wrapRefs(...refs: readonly React.Ref[]) { + return (handle: T | null) => { + for (const ref of refs) { + if (typeof ref === 'function') { + ref(handle); + } else if (ref !== null) { + // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31065 + // @ts-expect-error + ref.current = handle; + } + } + }; +} diff --git a/style/cell.less b/style/cell.less index f08dbe58fe..9b85f1027b 100644 --- a/style/cell.less +++ b/style/cell.less @@ -28,3 +28,12 @@ pointer-events: none; outline: none; } + +.rdg-cell-selected { + box-shadow: inset 0 0 0 2px #66afe9; + outline: 0; +} + +.rdg-cell-copied { + background: rgba(0, 0, 255, 0.2) !important; +} From 2eaa5a070eb95b62e13998e891d6cf5db95404ef Mon Sep 17 00:00:00 2001 From: Mahajan Date: Tue, 23 Jun 2020 17:19:17 -0500 Subject: [PATCH 02/42] Copy Tab logic --- src/DataGrid.tsx | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 8227926959..5c50b84c1f 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -27,7 +27,8 @@ import { getViewportColumns, getNextSelectedCellPosition, isSelectedCellEditable, - isCtrlKeyHeldDown + isCtrlKeyHeldDown, + canExitGrid } from './utils'; import { @@ -388,9 +389,9 @@ function DataGrid({ // closeEditor(); setCopiedPosition(null); break; - // case 'Tab': - // onPressTab(event); - // break; + case 'Tab': + onPressTab(event); + break; case 'ArrowUp': case 'ArrowDown': case 'ArrowLeft': @@ -489,6 +490,27 @@ function DataGrid({ }); } + function onPressTab(e: React.KeyboardEvent): void { + // If we are in a position to leave the grid, stop editing but stay in that cell + if (canExitGrid(e, { cellNavigationMode, columns, rowsCount: rows.length, selectedPosition })) { + if (selectedPosition.status === 'EDIT') { + // closeEditor(); + return; + } + + // Reset the selected position before exiting + setSelectedPosition({ idx: -1, rowIdx: -1, status: 'SELECT' }); + return; + } + + e.preventDefault(); + const tabCellNavigationMode = cellNavigationMode === CellNavigationMode.NONE + ? CellNavigationMode.CHANGE_ROW + : cellNavigationMode; + const nextPosition = getNextPosition('Tab', tabCellNavigationMode, e.shiftKey); + selectCell(nextPosition); + } + function getFrozenColumnsWidth() { if (lastFrozenColumnIndex === -1) return 0; const lastFrozenCol = columns[lastFrozenColumnIndex]; From b085271f6ba4686b5cd07bb2c829078b962f9421 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Wed, 24 Jun 2020 16:57:01 -0500 Subject: [PATCH 03/42] Initial cell drag implementation --- src/Cell.tsx | 53 ++++++++++++++---- src/DataGrid.tsx | 86 +++++++++++++++++++++++++++-- src/EventBus.test.ts | 14 ++--- src/EventBus.ts | 9 ++- src/Row.tsx | 6 +- src/common/types.ts | 4 ++ src/masks/InteractionMasks.test.tsx | 6 +- src/masks/InteractionMasks.tsx | 4 +- style/cell.less | 31 +++++++++++ 9 files changed, 181 insertions(+), 32 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index 39fb718d26..5c329e1907 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -2,7 +2,7 @@ import React, { forwardRef, memo, useEffect, useRef } from 'react'; import clsx from 'clsx'; import { CellRendererProps } from './common/types'; -import { preventDefault, wrapEvent } from './utils'; +import { preventDefault, wrapEvent, canEdit } from './utils'; import { useCombinedRefs } from './hooks'; function Cell({ @@ -10,11 +10,13 @@ function Cell({ column, isSelected, isCopied, + isDraggedOver, isRowSelected, lastFrozenColumnIndex, row, rowIdx, eventBus, + enableCellDragAndDrop, onRowClick, onClick, onDoubleClick, @@ -25,10 +27,10 @@ function Cell({ }: CellRendererProps, ref: React.Ref) { const cellRef = useRef(null); function selectCell(openEditor?: boolean) { - eventBus.dispatch('SELECT_CELL', { idx: column.idx, rowIdx }, openEditor); + eventBus.dispatch('CELL_SELECT', { idx: column.idx, rowIdx }, openEditor); } - function handleCellClick() { + function handleClick() { selectCell(); onRowClick?.(rowIdx, row, column); } @@ -37,16 +39,37 @@ function Cell({ eventBus.dispatch('CELL_KEYDOWN', event); } - function handleCellContextMenu() { + function handleContextMenu() { selectCell(); } - function handleCellDoubleClick() { + function handleDoubleClick() { selectCell(true); } + function handleDragStart(event: React.DragEvent) { + event.dataTransfer.effectAllowed = 'copy'; + // Setting data is required to make an element draggable in FF + const transferData = JSON.stringify({}); + try { + event.dataTransfer.setData('text/plain', transferData); + } catch (ex) { + // IE only supports 'text' and 'URL' for the 'type' argument + event.dataTransfer.setData('text', transferData); + } + eventBus.dispatch('CELL_DRAG_START'); + } + + function handleDragEnd() { + eventBus.dispatch('CELL_DRAG_END'); + } + + function handleDragHandleDoubleClick() { + eventBus.dispatch('CELL_DRAG_HANDLE_DOUBLE_CLICK'); + } + function onRowSelectionChange(checked: boolean, isShiftClick: boolean) { - eventBus.dispatch('SELECT_ROW', { rowIdx, checked, isShiftClick }); + eventBus.dispatch('ROW_SELECT', { rowIdx, checked, isShiftClick }); } useEffect(() => { @@ -62,7 +85,8 @@ function Cell({ 'rdg-cell-frozen': column.frozen, 'rdg-cell-frozen-last': column.idx === lastFrozenColumnIndex, 'rdg-cell-selected': isSelected, - 'rdg-cell-copied': isCopied + 'rdg-cell-copied': isCopied, + 'rdg-cell-dragged-over': isDraggedOver }, typeof cellClass === 'function' ? cellClass(row) : cellClass, className @@ -77,9 +101,9 @@ function Cell({ width: column.width, left: column.left }} - onClick={wrapEvent(handleCellClick, onClick)} - onDoubleClick={wrapEvent(handleCellDoubleClick, onDoubleClick)} - onContextMenu={wrapEvent(handleCellContextMenu, onContextMenu)} + onClick={wrapEvent(handleClick, onClick)} + onDoubleClick={wrapEvent(handleDoubleClick, onDoubleClick)} + onContextMenu={wrapEvent(handleContextMenu, onContextMenu)} onDragOver={wrapEvent(preventDefault, onDragOver)} onKeyDown={wrapEvent(handleKeyDown, onKeyDown)} {...props} @@ -91,6 +115,15 @@ function Cell({ isRowSelected={isRowSelected} onRowSelectionChange={onRowSelectionChange} /> + {enableCellDragAndDrop && isSelected && canEdit(column, row) && ( +
+ )}
); } diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 5c50b84c1f..1d25b2ba12 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -53,7 +53,6 @@ interface EditCellState extends Position { key: string | null; } - export interface DataGridHandle { scrollToColumn: (colIdx: number) => void; scrollToRow: (rowIdx: number) => void; @@ -243,6 +242,7 @@ function DataGrid({ return { idx: -1, rowIdx: -1, status: 'SELECT' }; }); const [copiedPosition, setCopiedPosition] = useState(null); + const [draggedOverRowIdx, setDraggedOverRowIdx] = useState(undefined); const [colOverscanStartIdx, colOverscanEndIdx] = useMemo((): [number, number] => { return getHorizontalRangeToRender( @@ -319,15 +319,38 @@ function DataGrid({ onSelectedRowsChange(newSelectedRows); }; - return eventBus.subscribe('SELECT_ROW', handleRowSelectionChange); + return eventBus.subscribe('ROW_SELECT', handleRowSelectionChange); }, [eventBus, onSelectedRowsChange, rows, rowKey, selectedRows]); useEffect(() => { - return eventBus.subscribe('SELECT_CELL', selectCell); + return eventBus.subscribe('CELL_SELECT', selectCell); }); useEffect(() => { - return eventBus.subscribe('CELL_KEYDOWN', onKeyDown); + return eventBus.subscribe('CELL_KEYDOWN', handleKeyDown); + }); + + useEffect(() => { + const handleDragEnter = (overRowIdx: number) => { + setDraggedOverRowIdx(overRowIdx); + }; + return eventBus.subscribe('ROW_DRAG_ENTER', handleDragEnter); + }, [eventBus]); + + useEffect(() => { + function handleDragStart() { + setDraggedOverRowIdx(selectedPosition.rowIdx); + } + + return eventBus.subscribe('CELL_DRAG_START', handleDragStart); + }, [eventBus, selectedPosition]); + + useEffect(() => { + return eventBus.subscribe('CELL_DRAG_END', handleDragEnd); + }); + + useEffect(() => { + return eventBus.subscribe('CELL_DRAG_HANDLE_DOUBLE_CLICK', handleDragHandleDoubleClick); }); useImperativeHandle(ref, () => ({ @@ -340,7 +363,7 @@ function DataGrid({ current.scrollTop = rowIdx * rowHeight; }, selectCell(position: Position, openEditor?: boolean) { - eventBus.dispatch('SELECT_CELL', position, openEditor); + eventBus.dispatch('CELL_SELECT', position, openEditor); } })); @@ -362,7 +385,7 @@ function DataGrid({ onColumnResize?.(column.idx, width); }, [columnWidths, onColumnResize]); - function onKeyDown(event: React.KeyboardEvent): void { + function handleKeyDown(event: React.KeyboardEvent): void { const column = columns[selectedPosition.idx]; const row = rows[selectedPosition.rowIdx]; const isActivatedByUser = (column.unsafe_onCellInput ?? legacyCellInput)(event, row) === true; @@ -407,6 +430,39 @@ function DataGrid({ } } + function handleDragEnd() { + if (typeof draggedOverRowIdx === 'undefined') return; + + const { idx, rowIdx } = selectedPosition; + const column = columns[idx]; + const cellKey = column.key; + const value = rows[rowIdx][cellKey as keyof R]; + + onRowsUpdate?.({ + cellKey, + fromRow: rowIdx, + toRow: draggedOverRowIdx, + updated: { [cellKey]: value } as unknown as never, + action: UpdateActions.CELL_DRAG + }); + + setDraggedOverRowIdx(undefined); + } + + function handleDragHandleDoubleClick(): void { + const column = columns[selectedPosition.idx]; + const cellKey = column.key; + const value = rows[selectedPosition.rowIdx][cellKey as keyof R]; + + onRowsUpdate?.({ + cellKey, + fromRow: selectedPosition.rowIdx, + toRow: rows.length - 1, + updated: { [cellKey]: value } as unknown as never, + action: UpdateActions.COLUMN_FILL + }); + } + /** * utils */ @@ -543,6 +599,15 @@ function DataGrid({ } } + function isDraggedOver(currentRowIdx: number) { + if (typeof draggedOverRowIdx === 'undefined') return; + const { rowIdx } = selectedPosition; + + return rowIdx < draggedOverRowIdx + ? rowIdx < currentRowIdx && currentRowIdx <= draggedOverRowIdx + : rowIdx > currentRowIdx && currentRowIdx >= draggedOverRowIdx; + } + function getViewportRows() { const rowElements = []; @@ -567,11 +632,13 @@ function DataGrid({ lastFrozenColumnIndex={lastFrozenColumnIndex} selectedCellIdx={selectedPosition.rowIdx === rowIdx ? selectedPosition.idx : undefined} copiedCellIdx={copiedPosition?.rowIdx === rowIdx ? copiedPosition.idx : undefined} + draggedOverCellIdx={isDraggedOver(rowIdx) ? selectedPosition.idx : undefined} eventBus={eventBus} isRowSelected={isRowSelected} onRowClick={onRowClick} rowClass={rowClass} top={rowIdx * rowHeight + totalHeaderHeight} + enableCellDragAndDrop={enableCellDragAndDrop} /> ); } @@ -579,6 +646,13 @@ function DataGrid({ 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 > rows.length) { + setSelectedPosition({ idx: -1, rowIdx: -1, status: 'SELECT' }); + setCopiedPosition(null); + setDraggedOverRowIdx(undefined); + } + return (
{ const eventAHandler2 = jest.fn(); const eventBHandler = jest.fn(); - eventBus.subscribe('SELECT_CELL', eventAHandler1); - eventBus.subscribe('SELECT_CELL', eventAHandler2); - eventBus.subscribe('SELECT_ROW', eventBHandler); + eventBus.subscribe('CELL_SELECT', eventAHandler1); + eventBus.subscribe('CELL_SELECT', eventAHandler2); + eventBus.subscribe('ROW_SELECT', eventBHandler); - eventBus.dispatch('SELECT_CELL', { idx: 1, rowIdx: 2 }, true); + eventBus.dispatch('CELL_SELECT', { idx: 1, rowIdx: 2 }, true); expect(eventAHandler1).toHaveBeenCalledWith({ idx: 1, rowIdx: 2 }, true); expect(eventAHandler2).toHaveBeenCalledWith({ idx: 1, rowIdx: 2 }, true); @@ -23,11 +23,11 @@ describe('EventBus', () => { const eventAHandler1 = jest.fn(); const eventAHandler2 = jest.fn(); - eventBus.subscribe('SELECT_CELL', eventAHandler1); - const unsubscribeEventAHandler2 = eventBus.subscribe('SELECT_CELL', eventAHandler2); + eventBus.subscribe('CELL_SELECT', eventAHandler1); + const unsubscribeEventAHandler2 = eventBus.subscribe('CELL_SELECT', eventAHandler2); unsubscribeEventAHandler2(); - eventBus.dispatch('SELECT_CELL', { idx: 1, rowIdx: 2 }, true); + eventBus.dispatch('CELL_SELECT', { idx: 1, rowIdx: 2 }, true); expect(eventAHandler1).toHaveBeenCalledWith({ idx: 1, rowIdx: 2 }, true); expect(eventAHandler2).not.toHaveBeenCalled(); diff --git a/src/EventBus.ts b/src/EventBus.ts index fd691e7b7c..687560a6ce 100644 --- a/src/EventBus.ts +++ b/src/EventBus.ts @@ -1,10 +1,13 @@ import { Position, SelectRowEvent } from './common/types'; interface EventMap { - SELECT_CELL: (position: Position, enableEditor?: boolean) => void; + CELL_SELECT: (position: Position, enableEditor?: boolean) => void; CELL_KEYDOWN: (event: React.KeyboardEvent) => void; - SELECT_ROW: (event: SelectRowEvent) => void; - DRAG_ENTER: (overRowIdx: number) => void; + CELL_DRAG_START: () => void; + CELL_DRAG_END: () => void; + CELL_DRAG_HANDLE_DOUBLE_CLICK: () => void; + ROW_SELECT: (event: SelectRowEvent) => void; + ROW_DRAG_ENTER: (overRowIdx: number) => void; } type EventName = keyof EventMap; diff --git a/src/Row.tsx b/src/Row.tsx index a0eaa56053..51ffc5add8 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -14,6 +14,7 @@ function Row({ lastFrozenColumnIndex, selectedCellIdx, copiedCellIdx, + draggedOverCellIdx, onRowClick, row, viewportColumns, @@ -22,12 +23,13 @@ function Row({ onDrop, rowClass, top, + enableCellDragAndDrop, ...props }: RowRendererProps) { function handleDragEnter(event: React.DragEvent) { // Prevent default to allow drop event.preventDefault(); - eventBus.dispatch('DRAG_ENTER', rowIdx); + eventBus.dispatch('ROW_DRAG_ENTER', rowIdx); } function handleDragOver(event: React.DragEvent) { @@ -64,8 +66,10 @@ function Row({ row={row} isSelected={selectedCellIdx === column.idx} isCopied={copiedCellIdx === column.idx} + isDraggedOver={draggedOverCellIdx === column.idx} isRowSelected={isRowSelected} eventBus={eventBus} + enableCellDragAndDrop={enableCellDragAndDrop} onRowClick={onRowClick} /> ))} diff --git a/src/common/types.ts b/src/common/types.ts index 8119ebd695..b4220d4e63 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -111,7 +111,9 @@ export interface CellRendererProps extends Omit) => void; } @@ -123,11 +125,13 @@ export interface RowRendererProps extends Omit) => void; rowClass?: (row: TRow) => string | undefined; top: number; + enableCellDragAndDrop: boolean; } export interface FilterRendererProps { diff --git a/src/masks/InteractionMasks.test.tsx b/src/masks/InteractionMasks.test.tsx index 09f7bf81fd..5c28e2615e 100644 --- a/src/masks/InteractionMasks.test.tsx +++ b/src/masks/InteractionMasks.test.tsx @@ -51,7 +51,7 @@ describe('InteractionMasks', () => { const wrapper = mount(); if (initialPosition) { act(() => { - props.eventBus.dispatch('SELECT_CELL', initialPosition); + props.eventBus.dispatch('CELL_SELECT', initialPosition); }); wrapper.update(); onSelectedCellChange.mockReset(); @@ -636,7 +636,7 @@ describe('InteractionMasks', () => { ]; const { wrapper, props } = setup({ rows }); act(() => { - props.eventBus.dispatch('SELECT_CELL', { idx: 1, rowIdx: 2 }); + props.eventBus.dispatch('CELL_SELECT', { idx: 1, rowIdx: 2 }); }); return { wrapper, props }; }; @@ -670,7 +670,7 @@ describe('InteractionMasks', () => { it('should update the selected cell with the copied value on paste', () => { const { wrapper, props } = setupCopy(); act(() => { - props.eventBus.dispatch('SELECT_CELL', { idx: 1, rowIdx: 2 }); + props.eventBus.dispatch('CELL_SELECT', { idx: 1, rowIdx: 2 }); }); // Copy selected cell pressKey(wrapper, 'c', { ctrlKey: true }); diff --git a/src/masks/InteractionMasks.tsx b/src/masks/InteractionMasks.tsx index 24d85cc3f1..abb54d963a 100644 --- a/src/masks/InteractionMasks.tsx +++ b/src/masks/InteractionMasks.tsx @@ -91,7 +91,7 @@ export default function InteractionMasks({ }, [selectedPosition]); useEffect(() => { - return eventBus.subscribe('SELECT_CELL', selectCell); + return eventBus.subscribe('CELL_SELECT', selectCell); }); useEffect(() => { @@ -99,7 +99,7 @@ export default function InteractionMasks({ const handleDragEnter = (overRowIdx: number) => { setDraggedPosition({ ...draggedPosition, overRowIdx }); }; - return eventBus.subscribe('DRAG_ENTER', handleDragEnter); + return eventBus.subscribe('ROW_DRAG_ENTER', handleDragEnter); }, [draggedPosition, eventBus]); const closeEditor = useCallback(() => { diff --git a/style/cell.less b/style/cell.less index 9b85f1027b..de97c7148c 100644 --- a/style/cell.less +++ b/style/cell.less @@ -37,3 +37,34 @@ .rdg-cell-copied { background: rgba(0, 0, 255, 0.2) !important; } + +.rdg-cell-drag-handle { + pointer-events: auto; + position: absolute; + bottom: -5px; + right: -4px; + background: #66afe9; + width: 8px; + height: 8px; + border: 1px solid #fff; + border-right: 0px; + border-bottom: 0px; + cursor: crosshair; + cursor: -moz-grab; + cursor: -webkit-grab; + cursor: grab; +} + +.rdg-cell-selected:hover .rdg-cell-drag-handle { + bottom: -8px; + right: -7px; + background: white; + width: 16px; + height: 16px; + border: 1px solid #66afe9; +} + +.rdg-cell-dragged-over { + background: rgba(0, 0, 255, 0.2) !important; + box-shadow: inset 1px 0 black, inset -1px 0 black; +} From 1bc4ecaa5d86d4de449f1002d49164c211f84bb1 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Fri, 26 Jun 2020 07:12:37 -0500 Subject: [PATCH 04/42] Initial cell editing implementation --- src/Cell.tsx | 156 ++++++++++++++++++---- src/DataGrid.tsx | 235 ++++++++++++--------------------- src/EventBus.ts | 10 +- src/Row.tsx | 2 + src/common/types.ts | 2 + src/masks/InteractionMasks.tsx | 11 +- src/utils/selectedCellUtils.ts | 5 +- 7 files changed, 234 insertions(+), 187 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index 5c329e1907..e0e3e5d1cc 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -1,10 +1,31 @@ -import React, { forwardRef, memo, useEffect, useRef } from 'react'; +import React, { forwardRef, memo, useEffect, useRef, useState } from 'react'; import clsx from 'clsx'; -import { CellRendererProps } from './common/types'; -import { preventDefault, wrapEvent, canEdit } from './utils'; +import { legacyCellInput } from './editors/CellInputHandlers'; +import EditorContainer from './editors/EditorContainer'; +import EditorPortal from './editors/EditorPortal'; +import { CellRendererProps, CommitEvent, Position } from './common/types'; +import { preventDefault, wrapEvent, canEdit, isCtrlKeyHeldDown } from './utils'; import { useCombinedRefs } from './hooks'; +function getNextPosition(key: string, shiftKey: boolean, currentPosition: Position) { + const { idx, rowIdx } = currentPosition; + switch (key) { + case 'ArrowUp': + return { idx, rowIdx: rowIdx - 1 }; + case 'ArrowDown': + return { idx, rowIdx: rowIdx + 1 }; + case 'ArrowLeft': + return { idx: idx - 1, rowIdx }; + case 'ArrowRight': + return { idx: idx + 1, rowIdx }; + case 'Tab': + return { idx: idx + (shiftKey ? -1 : 1), rowIdx }; + default: + return currentPosition; + } +} + function Cell({ className, column, @@ -15,6 +36,7 @@ function Cell({ lastFrozenColumnIndex, row, rowIdx, + rowHeight, eventBus, enableCellDragAndDrop, onRowClick, @@ -26,8 +48,20 @@ function Cell({ ...props }: CellRendererProps, ref: React.Ref) { const cellRef = useRef(null); + const [isEditing, setIsEditing] = useState(false); + const [inputKey, setInputKey] = useState(null); + + useEffect(() => { + if (!isEditing && isSelected) { + cellRef.current?.focus(); + } + }, [isEditing, isSelected]); + function selectCell(openEditor?: boolean) { - eventBus.dispatch('CELL_SELECT', { idx: column.idx, rowIdx }, openEditor); + if (openEditor && canEdit(column, row)) { + setIsEditing(true); + } + eventBus.dispatch('CELL_SELECT', { idx: column.idx, rowIdx }); } function handleClick() { @@ -36,7 +70,56 @@ function Cell({ } function handleKeyDown(event: React.KeyboardEvent) { - eventBus.dispatch('CELL_KEYDOWN', event); + const isActivatedByUser = (column.unsafe_onCellInput ?? legacyCellInput)(event, row) === true; + const canOpenEditor = !isEditing && canEdit(column, row); + + const { key, shiftKey } = event; + if (isCtrlKeyHeldDown(event)) { + // event.key may be uppercase `C` or `V` + const lowerCaseKey = key.toLowerCase(); + if (lowerCaseKey === 'c') { + eventBus.dispatch('CELL_COPY', row[column.key as keyof R]); + return; + } + + if (lowerCaseKey === 'v') { + eventBus.dispatch('CELL_PASTE', { idx: column.idx, rowIdx }); + return; + } + } + + if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(key)) { + event.preventDefault(); + eventBus.dispatch('CELL_NAVIGATE', + key, + shiftKey, + getNextPosition(key, shiftKey, { idx: column.idx, rowIdx }) + ); + return; + } + + // Use CELL_EDIT action instead? + switch (key) { + case 'Enter': + if (canOpenEditor) { + setIsEditing(true); + setInputKey(key); + } else if (isEditing) { + setIsEditing(false); + setInputKey(null); + } + break; + case 'Escape': + setIsEditing(false); + setInputKey(null); + break; + default: + if (canOpenEditor && isActivatedByUser) { + setIsEditing(true); + setInputKey(key); + } + break; + } } function handleContextMenu() { @@ -64,7 +147,8 @@ function Cell({ eventBus.dispatch('CELL_DRAG_END'); } - function handleDragHandleDoubleClick() { + function handleDragHandleDoubleClick(event: React.MouseEvent) { + event.stopPropagation(); eventBus.dispatch('CELL_DRAG_HANDLE_DOUBLE_CLICK'); } @@ -72,11 +156,10 @@ function Cell({ eventBus.dispatch('ROW_SELECT', { rowIdx, checked, isShiftClick }); } - useEffect(() => { - if (isSelected) { - cellRef.current?.focus(); - } - }, [isSelected]); + function onCommit(event: CommitEvent): void { + eventBus.dispatch('CELL_COMMIT', event); + setIsEditing(false); + } const { cellClass } = column; className = clsx( @@ -108,21 +191,42 @@ function Cell({ onKeyDown={wrapEvent(handleKeyDown, onKeyDown)} {...props} > - - {enableCellDragAndDrop && isSelected && canEdit(column, row) && ( -
+ {!isEditing && ( + <> + + {enableCellDragAndDrop && isSelected && canEdit(column, row) && ( +
+ )} + + )} + {isEditing && ( + + + firstEditorKeyPress={inputKey} + onCommit={onCommit} + onCommitCancel={() => setIsEditing(false)} + rowIdx={rowIdx} + row={row} + rowHeight={rowHeight} + column={column} + scrollLeft={0} + scrollTop={0} + left={cellRef.current?.getBoundingClientRect().left ?? 0} + top={cellRef.current?.getBoundingClientRect().top ?? 0} + /> + )}
); diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 1d25b2ba12..f89273ea0d 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -16,7 +16,6 @@ import FilterRow from './FilterRow'; import Row from './Row'; import SummaryRow from './SummaryRow'; import { ValueFormatter } from './formatters'; -import { legacyCellInput } from './editors/CellInputHandlers'; import { assertIsValidKey, getColumnMetrics, @@ -27,7 +26,6 @@ import { getViewportColumns, getNextSelectedCellPosition, isSelectedCellEditable, - isCtrlKeyHeldDown, canExitGrid } from './utils'; @@ -40,16 +38,17 @@ import { Position, RowRendererProps, RowsUpdateEvent, - SelectRowEvent + SelectRowEvent, + CommitEvent } from './common/types'; import { CellNavigationMode, SortDirection, UpdateActions } from './common/enums'; interface SelectCellState extends Position { - status: 'SELECT'; + mode: 'SELECT'; } interface EditCellState extends Position { - status: 'EDIT'; + mode: 'EDIT'; key: string | null; } @@ -142,8 +141,6 @@ export interface DataGridProps { */ /** Toggles whether filters row is displayed or not */ enableFilters?: boolean; - /** Toggles whether cells should be autofocused */ - enableCellAutoFocus?: boolean; enableCellCopyPaste?: boolean; enableCellDragAndDrop?: boolean; cellNavigationMode?: CellNavigationMode; @@ -197,7 +194,6 @@ function DataGrid({ onCheckCellIsEditable, // Toggles and modes enableFilters = false, - enableCellAutoFocus = true, enableCellCopyPaste = false, enableCellDragAndDrop = false, cellNavigationMode = CellNavigationMode.NONE, @@ -219,6 +215,9 @@ function DataGrid({ const [scrollTop, setScrollTop] = useState(0); const [scrollLeft, setScrollLeft] = useState(0); const [columnWidths, setColumnWidths] = useState>(() => new Map()); + const [selectedPosition, setSelectedPosition] = useState({ idx: -1, rowIdx: -1, mode: 'SELECT' }); + const [copiedPosition, setCopiedPosition] = useState(null); + const [draggedOverRowIdx, setDraggedOverRowIdx] = useState(undefined); /** * computed values @@ -235,15 +234,6 @@ function DataGrid({ }); }, [columnWidths, rawColumns, defaultFormatter, minColumnWidth, viewportWidth]); - const [selectedPosition, setSelectedPosition] = useState(() => { - if (enableCellAutoFocus && document.activeElement === document.body && columns.length > 0 && rows.length > 0) { - return { idx: 0, rowIdx: 0, status: 'SELECT' }; - } - return { idx: -1, rowIdx: -1, status: 'SELECT' }; - }); - const [copiedPosition, setCopiedPosition] = useState(null); - const [draggedOverRowIdx, setDraggedOverRowIdx] = useState(undefined); - const [colOverscanStartIdx, colOverscanEndIdx] = useMemo((): [number, number] => { return getHorizontalRangeToRender( columns, @@ -327,7 +317,74 @@ function DataGrid({ }); useEffect(() => { - return eventBus.subscribe('CELL_KEYDOWN', handleKeyDown); + function navigate(key: string, shiftKey: boolean, nextPosition: Position) { + 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 })) { + // Reset the selected position before exiting + setSelectedPosition({ idx: -1, rowIdx: -1, mode: 'SELECT' }); + return; + } + + mode = cellNavigationMode === CellNavigationMode.NONE + ? CellNavigationMode.CHANGE_ROW + : cellNavigationMode; + } + + nextPosition = getNextSelectedCellPosition({ + columns, + rowsCount: rows.length, + cellNavigationMode: mode, + nextPosition + }); + + selectCell(nextPosition); + } + + return eventBus.subscribe('CELL_NAVIGATE', navigate); + }); + + useEffect(() => { + if (!enableCellCopyPaste) return; + + function handleCopy(value: unknown) { + const { idx, rowIdx } = selectedPosition; + setCopiedPosition({ idx, rowIdx, value }); + } + + return eventBus.subscribe('CELL_COPY', handleCopy); + }); + + useEffect(() => { + if (!enableCellCopyPaste) return; + + function handlePaste(position: Position) { + if (copiedPosition === null || !isCellEditable(position)) { + return; + } + + const { rowIdx: toRow } = position; + + const cellKey = columns[position.idx].key; + const { rowIdx: fromRow, idx, value } = copiedPosition; + const fromCellKey = columns[idx].key; + + onRowsUpdate?.({ + cellKey, + fromRow, + toRow, + updated: { [cellKey]: value } as unknown as never, + action: UpdateActions.COPY_PASTE, + fromCellKey + }); + } + + return eventBus.subscribe('CELL_PASTE', handlePaste); + }); + + useEffect(() => { + return eventBus.subscribe('CELL_COMMIT', handleCommit); }); useEffect(() => { @@ -362,9 +419,7 @@ function DataGrid({ if (!current) return; current.scrollTop = rowIdx * rowHeight; }, - selectCell(position: Position, openEditor?: boolean) { - eventBus.dispatch('CELL_SELECT', position, openEditor); - } + selectCell })); /** @@ -385,49 +440,14 @@ function DataGrid({ onColumnResize?.(column.idx, width); }, [columnWidths, onColumnResize]); - function handleKeyDown(event: React.KeyboardEvent): void { - const column = columns[selectedPosition.idx]; - const row = rows[selectedPosition.rowIdx]; - const isActivatedByUser = (column.unsafe_onCellInput ?? legacyCellInput)(event, row) === true; - - const { key } = event; - if (enableCellCopyPaste && isCtrlKeyHeldDown(event)) { - // event.key may be uppercase `C` or `V` - const lowerCaseKey = event.key.toLowerCase(); - if (lowerCaseKey === 'c') return handleCopy(); - if (lowerCaseKey === 'v') return handlePaste(); - } - - const canOpenEditor = selectedPosition.status === 'SELECT' && isCellEditable(selectedPosition); - - switch (key) { - case 'Enter': - if (canOpenEditor) { - setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, status: 'EDIT', key: 'Enter' })); - } else if (selectedPosition.status === 'EDIT') { - setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, status: 'SELECT' })); - } - break; - case 'Escape': - // closeEditor(); - setCopiedPosition(null); - break; - case 'Tab': - onPressTab(event); - break; - case 'ArrowUp': - case 'ArrowDown': - case 'ArrowLeft': - case 'ArrowRight': - event.preventDefault(); - selectCell(getNextPosition(key)); - break; - default: - if (canOpenEditor && isActivatedByUser) { - setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, status: 'EDIT', key })); - } - break; - } + function handleCommit({ cellKey, rowIdx, updated }: CommitEvent) { + onRowsUpdate?.({ + cellKey, + fromRow: rowIdx, + toRow: rowIdx, + updated, + action: UpdateActions.CELL_UPDATE + }); } function handleDragEnd() { @@ -479,94 +499,14 @@ function DataGrid({ if (!isCellWithinBounds(position)) return; if (enableEditor && isCellEditable(position)) { - setSelectedPosition({ ...position, status: 'EDIT', key: null }); + setSelectedPosition({ ...position, mode: 'EDIT', key: null }); } else { - setSelectedPosition({ ...position, status: 'SELECT' }); + setSelectedPosition({ ...position, mode: 'SELECT' }); } scrollToCell(position); onSelectedCellChange?.({ ...position }); } - function getNextPosition(key: string, mode = cellNavigationMode, shiftKey = false) { - const { idx, rowIdx } = selectedPosition; - let nextPosition: Position; - switch (key) { - case 'ArrowUp': - nextPosition = { idx, rowIdx: rowIdx - 1 }; - break; - case 'ArrowDown': - nextPosition = { idx, rowIdx: rowIdx + 1 }; - break; - case 'ArrowLeft': - nextPosition = { idx: idx - 1, rowIdx }; - break; - case 'ArrowRight': - nextPosition = { idx: idx + 1, rowIdx }; - break; - case 'Tab': - nextPosition = { idx: idx + (shiftKey ? -1 : 1), rowIdx }; - break; - default: - nextPosition = { idx, rowIdx }; - break; - } - - return getNextSelectedCellPosition({ - columns, - rowsCount: rows.length, - cellNavigationMode: mode, - nextPosition - }); - } - - function handleCopy(): void { - const { idx, rowIdx } = selectedPosition; - const value = rows[rowIdx][columns[idx].key as keyof R]; - setCopiedPosition({ idx, rowIdx, value }); - } - - function handlePaste(): void { - if (copiedPosition === null || !isCellEditable(selectedPosition)) { - return; - } - - const { rowIdx: toRow } = selectedPosition; - - 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, - action: UpdateActions.COPY_PASTE, - fromCellKey - }); - } - - function onPressTab(e: React.KeyboardEvent): void { - // If we are in a position to leave the grid, stop editing but stay in that cell - if (canExitGrid(e, { cellNavigationMode, columns, rowsCount: rows.length, selectedPosition })) { - if (selectedPosition.status === 'EDIT') { - // closeEditor(); - return; - } - - // Reset the selected position before exiting - setSelectedPosition({ idx: -1, rowIdx: -1, status: 'SELECT' }); - return; - } - - e.preventDefault(); - const tabCellNavigationMode = cellNavigationMode === CellNavigationMode.NONE - ? CellNavigationMode.CHANGE_ROW - : cellNavigationMode; - const nextPosition = getNextPosition('Tab', tabCellNavigationMode, e.shiftKey); - selectCell(nextPosition); - } - function getFrozenColumnsWidth() { if (lastFrozenColumnIndex === -1) return 0; const lastFrozenCol = columns[lastFrozenColumnIndex]; @@ -628,6 +568,7 @@ function DataGrid({ key={key} rowIdx={rowIdx} row={row} + rowHeight={rowHeight} viewportColumns={viewportColumns} lastFrozenColumnIndex={lastFrozenColumnIndex} selectedCellIdx={selectedPosition.rowIdx === rowIdx ? selectedPosition.idx : undefined} @@ -648,7 +589,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) { - setSelectedPosition({ idx: -1, rowIdx: -1, status: 'SELECT' }); + setSelectedPosition({ idx: -1, rowIdx: -1, mode: 'SELECT' }); setCopiedPosition(null); setDraggedOverRowIdx(undefined); } diff --git a/src/EventBus.ts b/src/EventBus.ts index 687560a6ce..0fb7679930 100644 --- a/src/EventBus.ts +++ b/src/EventBus.ts @@ -1,8 +1,12 @@ -import { Position, SelectRowEvent } from './common/types'; +import { Position, SelectRowEvent, CommitEvent } from './common/types'; interface EventMap { - CELL_SELECT: (position: Position, enableEditor?: boolean) => void; - CELL_KEYDOWN: (event: React.KeyboardEvent) => void; + CELL_SELECT: (position: Position) => void; + CELL_EDIT: (position: Position) => void; + CELL_NAVIGATE: (key: string, shiftKey: boolean, nextPosition: Position) => void; + CELL_COPY: (value: unknown) => void; + CELL_PASTE: (position: Position) => void; + CELL_COMMIT: (event: CommitEvent) => void; CELL_DRAG_START: () => void; CELL_DRAG_END: () => void; CELL_DRAG_HANDLE_DOUBLE_CLICK: () => void; diff --git a/src/Row.tsx b/src/Row.tsx index 51ffc5add8..11addf41fe 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -23,6 +23,7 @@ function Row({ onDrop, rowClass, top, + rowHeight, enableCellDragAndDrop, ...props }: RowRendererProps) { @@ -64,6 +65,7 @@ function Row({ column={column} lastFrozenColumnIndex={lastFrozenColumnIndex} row={row} + rowHeight={rowHeight} isSelected={selectedCellIdx === column.idx} isCopied={copiedCellIdx === column.idx} isDraggedOver={draggedOverCellIdx === column.idx} diff --git a/src/common/types.ts b/src/common/types.ts index b4220d4e63..08c78a4304 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -108,6 +108,7 @@ export interface CellRendererProps extends Omit; lastFrozenColumnIndex: number; row: TRow; + rowHeight: number; isRowSelected: boolean; isSelected: boolean; isCopied: boolean; @@ -122,6 +123,7 @@ export interface RowRendererProps extends Omit>; rowIdx: number; + rowHeight: number; lastFrozenColumnIndex: number; selectedCellIdx?: number; copiedCellIdx?: number; diff --git a/src/masks/InteractionMasks.tsx b/src/masks/InteractionMasks.tsx index abb54d963a..7888a8d359 100644 --- a/src/masks/InteractionMasks.tsx +++ b/src/masks/InteractionMasks.tsx @@ -28,7 +28,6 @@ type SharedCanvasProps = Pick, | 'onSelectedCellChange' > & Pick>, | 'rowHeight' - | 'enableCellAutoFocus' | 'enableCellCopyPaste' | 'enableCellDragAndDrop' | 'cellNavigationMode' @@ -60,7 +59,6 @@ export default function InteractionMasks({ rows, rowHeight, eventBus, - enableCellAutoFocus, enableCellCopyPaste, enableCellDragAndDrop, editorPortalTarget, @@ -74,12 +72,7 @@ export default function InteractionMasks({ onRowsUpdate, scrollToCell }: InteractionMasksProps) { - const [selectedPosition, setSelectedPosition] = useState(() => { - if (enableCellAutoFocus && document.activeElement === document.body && columns.length > 0 && rows.length > 0) { - return { idx: 0, rowIdx: 0, status: 'SELECT' }; - } - return { idx: -1, rowIdx: -1, status: 'SELECT' }; - }); + const [selectedPosition, setSelectedPosition] = useState({ idx: -1, rowIdx: -1, status: 'SELECT' }); const [copiedPosition, setCopiedPosition] = useState(null); const [draggedPosition, setDraggedPosition] = useState(null); const selectionMaskRef = useRef(null); @@ -205,7 +198,7 @@ export default function InteractionMasks({ function onPressTab(e: React.KeyboardEvent): void { // If we are in a position to leave the grid, stop editing but stay in that cell - if (canExitGrid(e, { cellNavigationMode, columns, rowsCount: rows.length, selectedPosition })) { + if (canExitGrid({ cellNavigationMode, columns, rowsCount: rows.length, selectedPosition, shiftKey: e.shiftKey })) { if (selectedPosition.status === 'EDIT') { closeEditor(); return; diff --git a/src/utils/selectedCellUtils.ts b/src/utils/selectedCellUtils.ts index eb7a7c12fd..0ccc1e3d80 100644 --- a/src/utils/selectedCellUtils.ts +++ b/src/utils/selectedCellUtils.ts @@ -95,9 +95,10 @@ interface CanExitGridOpts { columns: readonly CalculatedColumn[]; rowsCount: number; selectedPosition: Position; + shiftKey: boolean; } -export function canExitGrid(event: React.KeyboardEvent, { cellNavigationMode, columns, rowsCount, selectedPosition: { rowIdx, idx } }: CanExitGridOpts): boolean { +export function canExitGrid({ cellNavigationMode, columns, rowsCount, selectedPosition: { rowIdx, idx }, shiftKey }: CanExitGridOpts): boolean { // When the cellNavigationMode is 'none' or 'changeRow', you can exit the grid if you're at the first or last cell of the grid // When the cellNavigationMode is 'loopOverRow', there is no logical exit point so you can't exit the grid if (cellNavigationMode === CellNavigationMode.NONE || cellNavigationMode === CellNavigationMode.CHANGE_ROW) { @@ -105,7 +106,7 @@ export function canExitGrid(event: React.KeyboardEvent, { cellNavigationM const atFirstCellInRow = idx === 0; const atLastRow = rowIdx === rowsCount - 1; const atFirstRow = rowIdx === 0; - const shift = event.shiftKey === true; + const shift = shiftKey; return shift ? atFirstCellInRow && atFirstRow : atLastCellInRow && atLastRow; } From 54de1df1c5bcf7eeab0eac2c3de2cb001611ff3a Mon Sep 17 00:00:00 2001 From: Mahajan Date: Fri, 26 Jun 2020 12:58:59 -0500 Subject: [PATCH 05/42] Move editorContainer to the DataGrid component --- src/Cell.tsx | 150 ++++------------------------ src/DataGrid.tsx | 234 +++++++++++++++++++++++++++++--------------- src/EventBus.ts | 9 +- src/Row.tsx | 2 - src/common/types.ts | 2 - 5 files changed, 180 insertions(+), 217 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index e0e3e5d1cc..a6fdc20378 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -1,31 +1,10 @@ -import React, { forwardRef, memo, useEffect, useRef, useState } from 'react'; +import React, { forwardRef, memo, useEffect, useRef } from 'react'; import clsx from 'clsx'; -import { legacyCellInput } from './editors/CellInputHandlers'; -import EditorContainer from './editors/EditorContainer'; -import EditorPortal from './editors/EditorPortal'; -import { CellRendererProps, CommitEvent, Position } from './common/types'; -import { preventDefault, wrapEvent, canEdit, isCtrlKeyHeldDown } from './utils'; +import { CellRendererProps } from './common/types'; +import { preventDefault, wrapEvent, canEdit } from './utils'; import { useCombinedRefs } from './hooks'; -function getNextPosition(key: string, shiftKey: boolean, currentPosition: Position) { - const { idx, rowIdx } = currentPosition; - switch (key) { - case 'ArrowUp': - return { idx, rowIdx: rowIdx - 1 }; - case 'ArrowDown': - return { idx, rowIdx: rowIdx + 1 }; - case 'ArrowLeft': - return { idx: idx - 1, rowIdx }; - case 'ArrowRight': - return { idx: idx + 1, rowIdx }; - case 'Tab': - return { idx: idx + (shiftKey ? -1 : 1), rowIdx }; - default: - return currentPosition; - } -} - function Cell({ className, column, @@ -36,32 +15,25 @@ function Cell({ lastFrozenColumnIndex, row, rowIdx, - rowHeight, eventBus, enableCellDragAndDrop, onRowClick, onClick, onDoubleClick, - onKeyDown, onContextMenu, onDragOver, ...props }: CellRendererProps, ref: React.Ref) { const cellRef = useRef(null); - const [isEditing, setIsEditing] = useState(false); - const [inputKey, setInputKey] = useState(null); useEffect(() => { - if (!isEditing && isSelected) { + if (isSelected) { cellRef.current?.focus(); } - }, [isEditing, isSelected]); + }, [isSelected]); function selectCell(openEditor?: boolean) { - if (openEditor && canEdit(column, row)) { - setIsEditing(true); - } - eventBus.dispatch('CELL_SELECT', { idx: column.idx, rowIdx }); + eventBus.dispatch('CELL_SELECT', { idx: column.idx, rowIdx }, openEditor); } function handleClick() { @@ -69,59 +41,6 @@ function Cell({ onRowClick?.(rowIdx, row, column); } - function handleKeyDown(event: React.KeyboardEvent) { - const isActivatedByUser = (column.unsafe_onCellInput ?? legacyCellInput)(event, row) === true; - const canOpenEditor = !isEditing && canEdit(column, row); - - const { key, shiftKey } = event; - if (isCtrlKeyHeldDown(event)) { - // event.key may be uppercase `C` or `V` - const lowerCaseKey = key.toLowerCase(); - if (lowerCaseKey === 'c') { - eventBus.dispatch('CELL_COPY', row[column.key as keyof R]); - return; - } - - if (lowerCaseKey === 'v') { - eventBus.dispatch('CELL_PASTE', { idx: column.idx, rowIdx }); - return; - } - } - - if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(key)) { - event.preventDefault(); - eventBus.dispatch('CELL_NAVIGATE', - key, - shiftKey, - getNextPosition(key, shiftKey, { idx: column.idx, rowIdx }) - ); - return; - } - - // Use CELL_EDIT action instead? - switch (key) { - case 'Enter': - if (canOpenEditor) { - setIsEditing(true); - setInputKey(key); - } else if (isEditing) { - setIsEditing(false); - setInputKey(null); - } - break; - case 'Escape': - setIsEditing(false); - setInputKey(null); - break; - default: - if (canOpenEditor && isActivatedByUser) { - setIsEditing(true); - setInputKey(key); - } - break; - } - } - function handleContextMenu() { selectCell(); } @@ -156,11 +75,6 @@ function Cell({ eventBus.dispatch('ROW_SELECT', { rowIdx, checked, isShiftClick }); } - function onCommit(event: CommitEvent): void { - eventBus.dispatch('CELL_COMMIT', event); - setIsEditing(false); - } - const { cellClass } = column; className = clsx( 'rdg-cell', @@ -188,45 +102,23 @@ function Cell({ onDoubleClick={wrapEvent(handleDoubleClick, onDoubleClick)} onContextMenu={wrapEvent(handleContextMenu, onContextMenu)} onDragOver={wrapEvent(preventDefault, onDragOver)} - onKeyDown={wrapEvent(handleKeyDown, onKeyDown)} {...props} > - {!isEditing && ( - <> - - {enableCellDragAndDrop && isSelected && canEdit(column, row) && ( -
- )} - - )} - {isEditing && ( - - - firstEditorKeyPress={inputKey} - onCommit={onCommit} - onCommitCancel={() => setIsEditing(false)} - rowIdx={rowIdx} - row={row} - rowHeight={rowHeight} - column={column} - scrollLeft={0} - scrollTop={0} - left={cellRef.current?.getBoundingClientRect().left ?? 0} - top={cellRef.current?.getBoundingClientRect().top ?? 0} - /> - + + {enableCellDragAndDrop && isSelected && canEdit(column, row) && ( +
)}
); diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index f89273ea0d..ba2784778c 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -16,6 +16,9 @@ import FilterRow from './FilterRow'; import Row from './Row'; import SummaryRow from './SummaryRow'; import { ValueFormatter } from './formatters'; +import { legacyCellInput } from './editors/CellInputHandlers'; +import EditorContainer from './editors/EditorContainer'; +import EditorPortal from './editors/EditorPortal'; import { assertIsValidKey, getColumnMetrics, @@ -26,7 +29,8 @@ import { getViewportColumns, getNextSelectedCellPosition, isSelectedCellEditable, - canExitGrid + canExitGrid, + isCtrlKeyHeldDown } from './utils'; import { @@ -153,6 +157,25 @@ export interface DataGridProps { rowClass?: (row: R) => string | undefined; } + +function getNextPosition(key: string, shiftKey: boolean, currentPosition: Position) { + const { idx, rowIdx } = currentPosition; + switch (key) { + case 'ArrowUp': + return { idx, rowIdx: rowIdx - 1 }; + case 'ArrowDown': + return { idx, rowIdx: rowIdx + 1 }; + case 'ArrowLeft': + return { idx: idx - 1, rowIdx }; + case 'ArrowRight': + return { idx: idx + 1, rowIdx }; + case 'Tab': + return { idx: idx + (shiftKey ? -1 : 1), rowIdx }; + default: + return currentPosition; + } +} + /** * Main API Component to render a data grid of rows and columns * @@ -316,77 +339,6 @@ function DataGrid({ return eventBus.subscribe('CELL_SELECT', selectCell); }); - useEffect(() => { - function navigate(key: string, shiftKey: boolean, nextPosition: Position) { - 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 })) { - // Reset the selected position before exiting - setSelectedPosition({ idx: -1, rowIdx: -1, mode: 'SELECT' }); - return; - } - - mode = cellNavigationMode === CellNavigationMode.NONE - ? CellNavigationMode.CHANGE_ROW - : cellNavigationMode; - } - - nextPosition = getNextSelectedCellPosition({ - columns, - rowsCount: rows.length, - cellNavigationMode: mode, - nextPosition - }); - - selectCell(nextPosition); - } - - return eventBus.subscribe('CELL_NAVIGATE', navigate); - }); - - useEffect(() => { - if (!enableCellCopyPaste) return; - - function handleCopy(value: unknown) { - const { idx, rowIdx } = selectedPosition; - setCopiedPosition({ idx, rowIdx, value }); - } - - return eventBus.subscribe('CELL_COPY', handleCopy); - }); - - useEffect(() => { - if (!enableCellCopyPaste) return; - - function handlePaste(position: Position) { - if (copiedPosition === null || !isCellEditable(position)) { - return; - } - - const { rowIdx: toRow } = position; - - const cellKey = columns[position.idx].key; - const { rowIdx: fromRow, idx, value } = copiedPosition; - const fromCellKey = columns[idx].key; - - onRowsUpdate?.({ - cellKey, - fromRow, - toRow, - updated: { [cellKey]: value } as unknown as never, - action: UpdateActions.COPY_PASTE, - fromCellKey - }); - } - - return eventBus.subscribe('CELL_PASTE', handlePaste); - }); - - useEffect(() => { - return eventBus.subscribe('CELL_COMMIT', handleCommit); - }); - useEffect(() => { const handleDragEnter = (overRowIdx: number) => { setDraggedOverRowIdx(overRowIdx); @@ -425,7 +377,27 @@ function DataGrid({ /** * event handlers */ - function onGridScroll(event: React.UIEvent) { + function handleKeyDown(event: React.KeyboardEvent) { + const { key, shiftKey } = event; + const { idx, rowIdx } = selectedPosition; + const column = columns[idx]; + if (enableCellCopyPaste && isCtrlKeyHeldDown(event)) { + // event.key may be uppercase `C` or `V` + const lowerCaseKey = event.key.toLowerCase(); + if (lowerCaseKey === 'c') return handleCopy(); + if (lowerCaseKey === 'v') return handlePaste(); + } + + if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(key)) { + event.preventDefault(); + navigate(key, event.shiftKey, getNextPosition(key, shiftKey, { idx: column.idx, rowIdx })); + return; + } + + handleCellInput(event); + } + + function handleScroll(event: React.UIEvent) { const { scrollTop, scrollLeft } = event.currentTarget; setScrollTop(scrollTop); setScrollLeft(scrollLeft); @@ -448,6 +420,8 @@ function DataGrid({ updated, action: UpdateActions.CELL_UPDATE }); + + setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, mode: 'SELECT' })); } function handleDragEnd() { @@ -483,6 +457,45 @@ function DataGrid({ }); } + function handleCopy() { + const { idx, rowIdx } = selectedPosition; + const value = rows[rowIdx][columns[idx].key as keyof R]; + setCopiedPosition({ idx, rowIdx, value }); + } + + function handlePaste() { + if (copiedPosition === null || !isCellEditable(selectedPosition)) { + return; + } + + const { rowIdx: toRow } = selectedPosition; + + 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, + action: UpdateActions.COPY_PASTE, + fromCellKey + }); + } + + function handleCellInput(event: React.KeyboardEvent) { + const { key } = event; + 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; + + if (canOpenEditor && (key === 'Enter' || isActivatedByUser)) { + setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, key, mode: 'EDIT' })); + } + } + /** * utils */ @@ -548,6 +561,67 @@ function DataGrid({ : rowIdx > currentRowIdx && currentRowIdx >= draggedOverRowIdx; } + function getEditorContainer() { + if (selectedPosition.mode === 'SELECT') return null; + + const column = columns[selectedPosition.idx]; + const row = rows[selectedPosition.rowIdx]; + let editorLeft = 0; + let editorTop = 0; + + if (gridRef.current !== null) { + const { left, top } = gridRef.current.getBoundingClientRect(); + const { scrollTop: docTop, scrollLeft: docLeft } = document.scrollingElement || document.documentElement; + const gridLeft = left + docLeft; + const gridTop = top + docTop; + editorLeft = gridLeft + column.left - (column.frozen ? 0 : scrollLeft); + editorTop = gridTop + totalHeaderHeight + selectedPosition.rowIdx * rowHeight - scrollTop; + } + + return ( + + + firstEditorKeyPress={selectedPosition.key} + onCommit={handleCommit} + onCommitCancel={() => setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, mode: 'SELECT' }))} + rowIdx={selectedPosition.rowIdx} + row={row} + rowHeight={rowHeight} + column={column} + scrollLeft={scrollLeft} + scrollTop={scrollTop} + left={editorLeft} + top={editorTop} + /> + + ); + } + + function navigate(key: string, shiftKey: boolean, nextPosition: Position) { + 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 })) { + // Reset the selected position before exiting + setSelectedPosition({ idx: -1, rowIdx: -1, mode: 'SELECT' }); + return; + } + + mode = cellNavigationMode === CellNavigationMode.NONE + ? CellNavigationMode.CHANGE_ROW + : cellNavigationMode; + } + + nextPosition = getNextSelectedCellPosition({ + columns, + rowsCount: rows.length, + cellNavigationMode: mode, + nextPosition + }); + + selectCell(nextPosition); + } + function getViewportRows() { const rowElements = []; @@ -568,10 +642,9 @@ function DataGrid({ key={key} rowIdx={rowIdx} row={row} - rowHeight={rowHeight} viewportColumns={viewportColumns} lastFrozenColumnIndex={lastFrozenColumnIndex} - selectedCellIdx={selectedPosition.rowIdx === rowIdx ? selectedPosition.idx : undefined} + selectedCellIdx={selectedPosition.rowIdx === rowIdx && selectedPosition.mode === 'SELECT' ? selectedPosition.idx : undefined} copiedCellIdx={copiedPosition?.rowIdx === rowIdx ? copiedPosition.idx : undefined} draggedOverCellIdx={isDraggedOver(rowIdx) ? selectedPosition.idx : undefined} eventBus={eventBus} @@ -584,7 +657,14 @@ function DataGrid({ ); } - return rowElements; + return ( +
+ {rowElements} + {getEditorContainer()} +
+ ); } // Reset the positions if the current values are no longer valid. This can happen if a column or row is removed @@ -606,7 +686,7 @@ function DataGrid({ '--row-height': `${rowHeight}px` } as unknown as React.CSSProperties} ref={gridRef} - onScroll={onGridScroll} + onScroll={handleScroll} > rowKey={rowKey} diff --git a/src/EventBus.ts b/src/EventBus.ts index 0fb7679930..02a1283ede 100644 --- a/src/EventBus.ts +++ b/src/EventBus.ts @@ -1,12 +1,7 @@ -import { Position, SelectRowEvent, CommitEvent } from './common/types'; +import { Position, SelectRowEvent } from './common/types'; interface EventMap { - CELL_SELECT: (position: Position) => void; - CELL_EDIT: (position: Position) => void; - CELL_NAVIGATE: (key: string, shiftKey: boolean, nextPosition: Position) => void; - CELL_COPY: (value: unknown) => void; - CELL_PASTE: (position: Position) => void; - CELL_COMMIT: (event: CommitEvent) => void; + CELL_SELECT: (position: Position, openEditor?: boolean) => void; CELL_DRAG_START: () => void; CELL_DRAG_END: () => void; CELL_DRAG_HANDLE_DOUBLE_CLICK: () => void; diff --git a/src/Row.tsx b/src/Row.tsx index 11addf41fe..51ffc5add8 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -23,7 +23,6 @@ function Row({ onDrop, rowClass, top, - rowHeight, enableCellDragAndDrop, ...props }: RowRendererProps) { @@ -65,7 +64,6 @@ function Row({ column={column} lastFrozenColumnIndex={lastFrozenColumnIndex} row={row} - rowHeight={rowHeight} isSelected={selectedCellIdx === column.idx} isCopied={copiedCellIdx === column.idx} isDraggedOver={draggedOverCellIdx === column.idx} diff --git a/src/common/types.ts b/src/common/types.ts index 08c78a4304..b4220d4e63 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -108,7 +108,6 @@ export interface CellRendererProps extends Omit; lastFrozenColumnIndex: number; row: TRow; - rowHeight: number; isRowSelected: boolean; isSelected: boolean; isCopied: boolean; @@ -123,7 +122,6 @@ export interface RowRendererProps extends Omit>; rowIdx: number; - rowHeight: number; lastFrozenColumnIndex: number; selectedCellIdx?: number; copiedCellIdx?: number; From 85d95a219b9a47fcf3eb5e0856083883aec0b22a Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Fri, 26 Jun 2020 13:10:04 -0500 Subject: [PATCH 06/42] Update src/utils/selectedCellUtils.ts Co-authored-by: Nicolas Stepien <567105+nstepien@users.noreply.github.com> --- src/utils/selectedCellUtils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/utils/selectedCellUtils.ts b/src/utils/selectedCellUtils.ts index 0ccc1e3d80..d063629e92 100644 --- a/src/utils/selectedCellUtils.ts +++ b/src/utils/selectedCellUtils.ts @@ -106,9 +106,8 @@ export function canExitGrid({ cellNavigationMode, columns, rowsCount, sel const atFirstCellInRow = idx === 0; const atLastRow = rowIdx === rowsCount - 1; const atFirstRow = rowIdx === 0; - const shift = shiftKey; - return shift ? atFirstCellInRow && atFirstRow : atLastCellInRow && atLastRow; + return shiftKey ? atFirstCellInRow && atFirstRow : atLastCellInRow && atLastRow; } return false; From 66b0dfc5d6e7180ec1d93089034ffba861985137 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Fri, 26 Jun 2020 13:07:40 -0500 Subject: [PATCH 07/42] Cleanup --- src/DataGrid.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index ba2784778c..1242cc2819 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -378,9 +378,6 @@ function DataGrid({ * event handlers */ function handleKeyDown(event: React.KeyboardEvent) { - const { key, shiftKey } = event; - const { idx, rowIdx } = selectedPosition; - const column = columns[idx]; if (enableCellCopyPaste && isCtrlKeyHeldDown(event)) { // event.key may be uppercase `C` or `V` const lowerCaseKey = event.key.toLowerCase(); @@ -388,9 +385,9 @@ function DataGrid({ if (lowerCaseKey === 'v') return handlePaste(); } - if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(key)) { + if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(event.key)) { event.preventDefault(); - navigate(key, event.shiftKey, getNextPosition(key, shiftKey, { idx: column.idx, rowIdx })); + navigate(event.key, event.shiftKey); return; } @@ -597,7 +594,8 @@ function DataGrid({ ); } - function navigate(key: string, shiftKey: boolean, nextPosition: Position) { + function navigate(key: string, shiftKey: boolean) { + let nextPosition = getNextPosition(key, shiftKey, selectedPosition); let mode = cellNavigationMode; if (key === 'Tab') { // If we are in a position to leave the grid, stop editing but stay in that cell From 526fa66560cd6330eb26f047feea8e3373740d07 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Fri, 26 Jun 2020 14:14:26 -0500 Subject: [PATCH 08/42] Remove masks components --- src/editors/EditorContainer.tsx | 12 +- src/masks/CellMask.test.tsx | 38 -- src/masks/CellMask.tsx | 22 - src/masks/DragMask.test.tsx | 63 --- src/masks/DragMask.tsx | 33 -- src/masks/InteractionMasks.test.tsx | 766 ---------------------------- src/masks/InteractionMasks.tsx | 383 -------------- style/index.less | 1 - style/interaction-masks.less | 46 -- 9 files changed, 4 insertions(+), 1360 deletions(-) delete mode 100644 src/masks/CellMask.test.tsx delete mode 100644 src/masks/CellMask.tsx delete mode 100644 src/masks/DragMask.test.tsx delete mode 100644 src/masks/DragMask.tsx delete mode 100644 src/masks/InteractionMasks.test.tsx delete mode 100644 src/masks/InteractionMasks.tsx delete mode 100644 style/interaction-masks.less diff --git a/src/editors/EditorContainer.tsx b/src/editors/EditorContainer.tsx index 49a7e4f9da..36b1ffbc08 100644 --- a/src/editors/EditorContainer.tsx +++ b/src/editors/EditorContainer.tsx @@ -4,16 +4,9 @@ import clsx from 'clsx'; import { CalculatedColumn, Editor, CommitEvent } from '../common/types'; import SimpleTextEditor from './SimpleTextEditor'; import ClickOutside from './ClickOutside'; -import { InteractionMasksProps } from '../masks/InteractionMasks'; import { preventDefault } from '../utils'; -type SharedInteractionMasksProps = Pick, - | 'scrollLeft' - | 'scrollTop' - | 'rowHeight' ->; - -export interface EditorContainerProps extends SharedInteractionMasksProps { +export interface EditorContainerProps { rowIdx: number; row: R; column: CalculatedColumn; @@ -22,6 +15,9 @@ export interface EditorContainerProps extends SharedInteractionMasksProps firstEditorKeyPress: string | null; top: number; left: number; + scrollLeft: number; + scrollTop: number; + rowHeight: number; } export default function EditorContainer({ diff --git a/src/masks/CellMask.test.tsx b/src/masks/CellMask.test.tsx deleted file mode 100644 index 932eb3c41b..0000000000 --- a/src/masks/CellMask.test.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import { sel } from '../test/utils'; -import CellMask from './CellMask'; - -describe('CellMask', () => { - const setup = (children?: JSX.Element) => { - const props = { - height: 30, - width: 50, - left: 5, - top: 10, - zIndex: 1 - }; - - const wrapper = shallow({children}); - return wrapper.find(sel('cell-mask')); - }; - - it('should render the mask with correct style', () => { - const mask = setup(); - - expect(mask.prop('style')).toMatchObject({ - height: 30, - width: 50, - zIndex: 1, - transform: 'translate(5px, 10px)' - }); - }); - - it('should render any children', () => { - const FakeChild =
test
; - const mask = setup(FakeChild); - - expect(mask.contains(FakeChild)).toBe(true); - }); -}); diff --git a/src/masks/CellMask.tsx b/src/masks/CellMask.tsx deleted file mode 100644 index 0441d77c41..0000000000 --- a/src/masks/CellMask.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React, { forwardRef } from 'react'; -import clsx from 'clsx'; -import { Dimension } from '../common/types'; - -export type CellMaskProps = React.HTMLAttributes & Dimension; - -export default forwardRef(function CellMask({ width, height, top, left, zIndex, className, ...props }, ref) { - return ( -
- ); -}); diff --git a/src/masks/DragMask.test.tsx b/src/masks/DragMask.test.tsx deleted file mode 100644 index b4ab6a63fc..0000000000 --- a/src/masks/DragMask.test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; - -import { Position } from '../common/types'; -import CellMask from './CellMask'; -import DragMask, { DraggedPosition } from './DragMask'; - -describe('DragMask', () => { - const setup = (draggedPosition: DraggedPosition) => { - const props = { - getSelectedDimensions({ rowIdx }: Position) { - const heights: { [key: number]: number } = { - 2: 20, - 3: 30, - 4: 40, - 5: 50, - 6: 60 - }; - const height = heights[rowIdx]; - - return { - height, - width: 50, - left: 5, - top: 90, - zIndex: 1 - }; - }, - draggedPosition - }; - - const wrapper = shallow(); - return wrapper.find(CellMask); - }; - - it('should not render the CellMask component if the drag handle is on the same row as the dragged cell', () => { - const mask = setup({ idx: 0, rowIdx: 2, overRowIdx: 2 }); - - expect(mask).toHaveLength(0); - }); - - it('should render the CellMask component with correct position for the dragged down cell', () => { - const mask = setup({ idx: 0, rowIdx: 2, overRowIdx: 4 }); - - expect(mask.props()).toMatchObject({ - height: 70, - width: 50, - left: 5, - top: 90 - }); - }); - - it('should render the CellMask component with correct position for the dragged up cell', () => { - const mask = setup({ idx: 0, rowIdx: 6, overRowIdx: 4 }); - - expect(mask.props()).toMatchObject({ - height: 90, - width: 50, - left: 5, - top: 90 - }); - }); -}); diff --git a/src/masks/DragMask.tsx b/src/masks/DragMask.tsx deleted file mode 100644 index 3cf1b31b32..0000000000 --- a/src/masks/DragMask.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import { Position, Dimension } from '../common/types'; -import CellMask from './CellMask'; - -export interface DraggedPosition extends Position { - overRowIdx: number; -} - -interface Props { - draggedPosition: DraggedPosition; - getSelectedDimensions: (position: Position) => Dimension; -} - -export default function DragMask({ draggedPosition, getSelectedDimensions }: Props) { - const { overRowIdx, idx, rowIdx } = draggedPosition; - if (rowIdx === overRowIdx) return null; - - const isDraggedOverDown = rowIdx < overRowIdx; - const startRowIdx = isDraggedOverDown ? rowIdx + 1 : overRowIdx; - const endRowIdx = isDraggedOverDown ? overRowIdx : rowIdx - 1; - const className = isDraggedOverDown ? 'react-grid-cell-dragged-over-down' : 'react-grid-cell-dragged-over-up'; - - const dimensions = getSelectedDimensions({ idx, rowIdx: startRowIdx }); - for (let currentRowIdx = startRowIdx + 1; currentRowIdx <= endRowIdx; currentRowIdx++) { - const { height } = getSelectedDimensions({ idx, rowIdx: currentRowIdx }); - dimensions.height += height; - } - - return ( - - ); -} diff --git a/src/masks/InteractionMasks.test.tsx b/src/masks/InteractionMasks.test.tsx deleted file mode 100644 index 5c28e2615e..0000000000 --- a/src/masks/InteractionMasks.test.tsx +++ /dev/null @@ -1,766 +0,0 @@ -/* eslint-disable jest/no-commented-out-tests */ -/* eslint-disable sonarjs/no-identical-functions */ -import React from 'react'; -import { mount } from 'enzyme'; -import { act } from 'react-dom/test-utils'; - -import InteractionMasks, { InteractionMasksProps } from './InteractionMasks'; -// import SelectionRangeMask from '../SelectionRangeMask'; -import DragMask from './DragMask'; -import EventBus from '../EventBus'; -import EditorContainer from '../editors/EditorContainer'; -import { createColumns } from '../test/utils'; -import { CellNavigationMode, UpdateActions } from '../common/enums'; -import { Position } from '../common/types'; - -const NUMBER_OF_COLUMNS = 10; -const ROWS_COUNT = 5; -const columns = createColumns(NUMBER_OF_COLUMNS); - -// Enzyme sets the className on the ForwardRef component also so we need to filter div -const selectionMaskSelector = 'div.rdg-selected'; -const copyMaskSelector = 'div.rdg-cell-copied'; - -interface Row { - [key: string]: React.ReactNode; -} - -describe('InteractionMasks', () => { - function setup(overrideProps?: Partial>, initialPosition?: Position) { - const onSelectedCellChange = jest.fn(); - const props: InteractionMasksProps = { - columns, - rows: Array(ROWS_COUNT).fill({ col1: 1 }), - rowHeight: 30, - totalHeaderHeight: 30, - scrollToCell: jest.fn(), - onSelectedCellChange, - onRowsUpdate: jest.fn(), - enableCellAutoFocus: false, - enableCellCopyPaste: true, - enableCellDragAndDrop: true, - cellNavigationMode: CellNavigationMode.NONE, - eventBus: new EventBus(), - editorPortalTarget: document.body, - gridRef: React.createRef(), - scrollLeft: 0, - scrollTop: 0, - ...overrideProps - }; - - const wrapper = mount(); - if (initialPosition) { - act(() => { - props.eventBus.dispatch('CELL_SELECT', initialPosition); - }); - wrapper.update(); - onSelectedCellChange.mockReset(); - } - return { wrapper, props }; - } - - const pressKey = (wrapper: ReturnType['wrapper'], key: string, eventData?: Partial) => { - act(() => { - wrapper.simulate('keydown', { key, preventDefault: () => null, ...eventData }); - }); - }; - - const simulateTab = (wrapper: ReturnType['wrapper'], shiftKey = false, preventDefault = () => { }) => { - act(() => { - pressKey(wrapper, 'Tab', { shiftKey, preventDefault }); - }); - }; - - describe('Rendered masks', () => { - describe('When a single cell is selected', () => { - describe('within grid bounds', () => { - it('should render a SelectionMask component', () => { - const { wrapper } = setup({}, { idx: 0, rowIdx: 0 }); - expect(wrapper.find(selectionMaskSelector)).toHaveLength(1); - }); - }); - - describe('outside grid bounds', () => { - it('should not render a SelectionMask component', () => { - const { wrapper } = setup(); - expect(wrapper.find(selectionMaskSelector)).toHaveLength(0); - }); - }); - }); - - // describe('When a cell range is selected', () => { - // it('should render a SelectionRangeMask component', () => { - // const { wrapper } = setup({}, { - // selectedPosition: { idx: 0, rowIdx: 0 }, - // selectedRange: { - // topLeft: { idx: 0, rowIdx: 0 }, - // bottomRight: { idx: 1, rowIdx: 1 }, - // startCell: { idx: 0, rowIdx: 0 }, - // cursorCell: null, - // isDragging: false - // } - // }); - // expect(wrapper.find(SelectionRangeMask).length).toBe(1); - // }); - - // it('should render a SelectionMask component on the range\'s start cell', () => { - // const { wrapper } = setup({}, { - // selectedPosition: { idx: 0, rowIdx: 0 }, - // selectedRange: { - // topLeft: { idx: 0, rowIdx: 0 }, - // bottomRight: { idx: 1, rowIdx: 1 }, - // startCell: { idx: 0, rowIdx: 0 }, - // cursorCell: null, - // isDragging: false - // } - // }); - // expect(wrapper.find(SelectionMask).length).toBe(1); - // expect(wrapper.find(SelectionMask).props()).toEqual({ height: 30, left: 0, top: 0, width: 100, zIndex: 1 }); - // }); - // }); - }); - - // describe('Range selection functionality', () => { - // describe('with the cursor', () => { - // it('should update the single-cell selectedPosition on starting a selection', () => { - // const { props, wrapper } = setup(); - // props.eventBus.dispatch(EventTypes.SELECT_START, { idx: 2, rowIdx: 2 }); - // expect(wrapper.state('selectedPosition')).toEqual({ idx: 2, rowIdx: 2 }); - // }); - - // it('should update the multi-cell selectedRange on starting a selection', () => { - // const { props, wrapper } = setup(); - // props.eventBus.dispatch(EventTypes.SELECT_START, { idx: 2, rowIdx: 2 }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.topLeft).toEqual({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.bottomRight).toEqual({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.startCell).toEqual({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.cursorCell).toEqual({ idx: 2, rowIdx: 2 }); - // }); - - // describe('moving the cursor to a new cell, mid-select', () => { - // function innerSetup() { - // const { props, wrapper } = setup(); - // props.eventBus.dispatch(EventTypes.SELECT_START, { idx: 2, rowIdx: 2 }); - // return { props, wrapper }; - // } - - // it('should update topLeft (and cursor) when moving left', () => { - // const { props, wrapper } = innerSetup(); - // props.eventBus.dispatch(EventTypes.SELECT_UPDATE, { idx: 1, rowIdx: 2 }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.topLeft).toEqual({ idx: 1, rowIdx: 2 }); - // expect(selectedRange.bottomRight).toEqual({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.startCell).toEqual({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.cursorCell).toEqual({ idx: 1, rowIdx: 2 }); - // }); - - // it('should update topLeft (and cursor) when moving up', () => { - // const { props, wrapper } = innerSetup(); - // props.eventBus.dispatch(EventTypes.SELECT_UPDATE, { idx: 2, rowIdx: 1 }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.topLeft).toEqual({ idx: 2, rowIdx: 1 }); - // expect(selectedRange.bottomRight).toEqual({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.startCell).toEqual({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.cursorCell).toEqual({ idx: 2, rowIdx: 1 }); - // }); - - // it('should update bottomRight (and cursor) when moving right', () => { - // const { props, wrapper } = innerSetup(); - // props.eventBus.dispatch(EventTypes.SELECT_UPDATE, { idx: 3, rowIdx: 2 }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.topLeft).toEqual({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.bottomRight).toEqual({ idx: 3, rowIdx: 2 }); - // expect(selectedRange.startCell).toEqual({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.cursorCell).toEqual({ idx: 3, rowIdx: 2 }); - // }); - - // it('should update bottomRight (and cursor) when moving down', () => { - // const { props, wrapper } = innerSetup(); - // props.eventBus.dispatch(EventTypes.SELECT_UPDATE, { idx: 2, rowIdx: 3 }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.topLeft).toEqual({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.bottomRight).toEqual({ idx: 2, rowIdx: 3 }); - // expect(selectedRange.startCell).toEqual({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.cursorCell).toEqual({ idx: 2, rowIdx: 3 }); - // }); - // }); - - // it('should not update state when moving the cursor but not mid-select', () => { - // const { props, wrapper } = setup(); - // props.eventBus.dispatch(EventTypes.SELECT_UPDATE, { idx: 2, rowIdx: 2 }); - // expect(wrapper.state('selectedRange').startCell).toBeNull(); - // }); - - // it('should not update state when moving the cursor after a selection has ended', () => { - // const { props, wrapper } = setup(); - // props.eventBus.dispatch(EventTypes.SELECT_START, { idx: 2, rowIdx: 2 }); - // props.eventBus.dispatch(EventTypes.SELECT_END); - // props.eventBus.dispatch(EventTypes.SELECT_UPDATE, { idx: 3, rowIdx: 3 }); - // expect(wrapper.state('selectedRange').cursorCell).toEqual({ idx: 2, rowIdx: 2 }); - // }); - - // it('should give focus to InteractionMasks once a selection has ended', () => { - // // We have to use mount, rather than shallow, so that InteractionMasks has a ref to it's node, used for focusing - // const { wrapper, props } = setup(undefined, undefined, true); - // props.eventBus.dispatch(EventTypes.SELECT_START, { idx: 2, rowIdx: 2 }); - // jest.spyOn(wrapper.instance(), 'focus').mockImplementation(() => { }); - // props.eventBus.dispatch(EventTypes.SELECT_END); - // expect(wrapper.instance().focus).toHaveBeenCalled(); - // }); - // }); - // }); - - // describe('Keyboard range selection functionality', () => { - // const selectRange = (eventBus: EventBus, from: Position, to: Position) => { - // eventBus.dispatch(EventTypes.SELECT_START, from); - // eventBus.dispatch(EventTypes.SELECT_UPDATE, to); - // eventBus.dispatch(EventTypes.SELECT_END); - // }; - - // describe('when a range is already selected', () => { - // describe('when the cursor cell is not in outer bounds', () => { - // function innerSetup() { - // const setupResult = setup(); - // selectRange(setupResult.props.eventBus, { idx: 2, rowIdx: 2 }, { idx: 3, rowIdx: 3 }); - // return setupResult; - // } - - // it('should shrink the selection upwards on Shift+Up', () => { - // const { wrapper } = innerSetup(); - // pressKey(wrapper, 'ArrowUp', { shiftKey: true }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.topLeft).toMatchObject({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.bottomRight).toMatchObject({ idx: 3, rowIdx: 2 }); - // expect(selectedRange.cursorCell).toMatchObject({ idx: 3, rowIdx: 2 }); - // expect(selectedRange.startCell).toMatchObject({ idx: 2, rowIdx: 2 }); - // }); - - // it('should shrink the selection leftwards on Shift+Left', () => { - // const { wrapper } = innerSetup(); - // pressKey(wrapper, 'ArrowLeft', { shiftKey: true }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.topLeft).toMatchObject({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.bottomRight).toMatchObject({ idx: 2, rowIdx: 3 }); - // expect(selectedRange.cursorCell).toMatchObject({ idx: 2, rowIdx: 3 }); - // expect(selectedRange.startCell).toMatchObject({ idx: 2, rowIdx: 2 }); - // }); - - // it('should grow the selection downwards on Shift+Down', () => { - // const { wrapper } = innerSetup(); - // pressKey(wrapper, 'ArrowDown', { shiftKey: true }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.topLeft).toMatchObject({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.bottomRight).toMatchObject({ idx: 3, rowIdx: 4 }); - // expect(selectedRange.cursorCell).toMatchObject({ idx: 3, rowIdx: 4 }); - // expect(selectedRange.startCell).toMatchObject({ idx: 2, rowIdx: 2 }); - // }); - - // it('should grow the selection rightwards on Shift+Right', () => { - // const { wrapper } = innerSetup(); - // pressKey(wrapper, 'ArrowRight', { shiftKey: true }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.topLeft).toMatchObject({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.bottomRight).toMatchObject({ idx: 4, rowIdx: 3 }); - // expect(selectedRange.cursorCell).toMatchObject({ idx: 4, rowIdx: 3 }); - // expect(selectedRange.startCell).toMatchObject({ idx: 2, rowIdx: 2 }); - // }); - // }); - - // describe('when the next cell is out of bounds', () => { - // it('should not grow the selection on Shift+Up', () => { - // const { props, wrapper } = setup(); - // selectRange(props.eventBus, { idx: 1, rowIdx: 1 }, { idx: 0, rowIdx: 0 }); - // pressKey(wrapper, 'ArrowUp', { shiftKey: true }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.cursorCell).toMatchObject({ idx: 0, rowIdx: 0 }); - // }); - - // it('should not grow the selection on Shift+Left', () => { - // const { props, wrapper } = setup(); - // selectRange(props.eventBus, { idx: 1, rowIdx: 1 }, { idx: 0, rowIdx: 0 }); - // pressKey(wrapper, 'ArrowLeft', { shiftKey: true }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.cursorCell).toEqual({ idx: 0, rowIdx: 0 }); - // }); - - // it('should not grow the selection on Shift+Right', () => { - // const { props, wrapper } = setup(); - // selectRange(props.eventBus, { idx: 2, rowIdx: 2 }, { idx: NUMBER_OF_COLUMNS - 1, rowIdx: 3 }); - // pressKey(wrapper, 'ArrowRight', { shiftKey: true }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.cursorCell).toEqual({ idx: NUMBER_OF_COLUMNS - 1, rowIdx: 3 }); - // }); - - // it('should not grow the selection on Shift+Down', () => { - // const { props, wrapper } = setup(); - // selectRange(props.eventBus, { idx: 2, rowIdx: 2 }, { idx: 2, rowIdx: ROWS_COUNT - 1 }); - // pressKey(wrapper, 'ArrowDown', { shiftKey: true }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.cursorCell).toEqual({ idx: 2, rowIdx: ROWS_COUNT - 1 }); - // }); - // }); - // }); - - // describe('when only a single cell is selected', () => { - // function innerSetup() { - // const currentCell = { idx: 2, rowIdx: 2 }; - // const setupResult = setup({}, { selectedPosition: currentCell }); - // setupResult.props.eventBus.dispatch(EventTypes.SELECT_START, currentCell); - // setupResult.props.eventBus.dispatch(EventTypes.SELECT_END); - // return setupResult; - // } - - // it('should grow the selection range left on Shift+Left', () => { - // const { wrapper } = innerSetup(); - // pressKey(wrapper, 'ArrowLeft', { shiftKey: true }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.topLeft).toMatchObject({ idx: 1, rowIdx: 2 }); - // expect(selectedRange.bottomRight).toMatchObject({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.cursorCell).toMatchObject({ idx: 1, rowIdx: 2 }); - // expect(selectedRange.startCell).toMatchObject({ idx: 2, rowIdx: 2 }); - // }); - - // it('should grow the selection range right on Shift+Right', () => { - // const { wrapper } = innerSetup(); - // pressKey(wrapper, 'ArrowRight', { shiftKey: true }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.topLeft).toMatchObject({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.bottomRight).toMatchObject({ idx: 3, rowIdx: 2 }); - // expect(selectedRange.cursorCell).toMatchObject({ idx: 3, rowIdx: 2 }); - // expect(selectedRange.startCell).toMatchObject({ idx: 2, rowIdx: 2 }); - // }); - - // it('should grow the selection range up on Shift+Up', () => { - // const { wrapper } = innerSetup(); - // pressKey(wrapper, 'ArrowUp', { shiftKey: true }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.topLeft).toMatchObject({ idx: 2, rowIdx: 1 }); - // expect(selectedRange.bottomRight).toMatchObject({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.cursorCell).toMatchObject({ idx: 2, rowIdx: 1 }); - // expect(selectedRange.startCell).toMatchObject({ idx: 2, rowIdx: 2 }); - // }); - - // it('should grow the selection range down on Shift+Down', () => { - // const { wrapper } = innerSetup(); - // pressKey(wrapper, 'ArrowDown', { shiftKey: true }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.topLeft).toMatchObject({ idx: 2, rowIdx: 2 }); - // expect(selectedRange.bottomRight).toMatchObject({ idx: 2, rowIdx: 3 }); - // expect(selectedRange.cursorCell).toMatchObject({ idx: 2, rowIdx: 3 }); - // expect(selectedRange.startCell).toMatchObject({ idx: 2, rowIdx: 2 }); - // }); - // }); - - // describe('when no range has ever been selected', () => { - // function innerSetup() { - // const currentCell = { idx: 0, rowIdx: 0 }; - // return setup({}, { selectedPosition: currentCell }); - // } - - // it('should grow the selection range right on Shift+Right', () => { - // const { wrapper } = innerSetup(); - // pressKey(wrapper, 'ArrowRight', { shiftKey: true }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.topLeft).toMatchObject({ idx: 0, rowIdx: 0 }); - // expect(selectedRange.bottomRight).toMatchObject({ idx: 1, rowIdx: 0 }); - // expect(selectedRange.cursorCell).toMatchObject({ idx: 1, rowIdx: 0 }); - // expect(selectedRange.startCell).toMatchObject({ idx: 0, rowIdx: 0 }); - // }); - - // it('should grow the selection range down on Shift+Down', () => { - // const { wrapper } = innerSetup(); - // pressKey(wrapper, 'ArrowDown', { shiftKey: true }); - // const selectedRange = wrapper.state('selectedRange'); - // expect(selectedRange.topLeft).toMatchObject({ idx: 0, rowIdx: 0 }); - // expect(selectedRange.bottomRight).toMatchObject({ idx: 0, rowIdx: 1 }); - // expect(selectedRange.cursorCell).toMatchObject({ idx: 0, rowIdx: 1 }); - // expect(selectedRange.startCell).toMatchObject({ idx: 0, rowIdx: 0 }); - // }); - // }); - // }); - - // describe('Range selection events', () => { - // it('should fire onSelectedCellRangeChange on starting a selection', () => { - // const { props } = setup(); - // props.eventBus.dispatch(EventTypes.SELECT_START, { idx: 2, rowIdx: 2 }); - // expect(props.onSelectedCellRangeChange).toHaveBeenCalledWith(expect.objectContaining({ - // topLeft: { idx: 2, rowIdx: 2 }, - // bottomRight: { idx: 2, rowIdx: 2 }, - // isDragging: true - // })); - // }); - - // it('should fire onSelectedCellRangeChange on updating a selection', () => { - // const { props } = setup(); - // props.eventBus.dispatch(EventTypes.SELECT_START, { idx: 2, rowIdx: 2 }); - // props.eventBus.dispatch(EventTypes.SELECT_UPDATE, { idx: 3, rowIdx: 3 }); - // expect(props.onSelectedCellRangeChange).toHaveBeenCalledWith(expect.objectContaining({ - // topLeft: { idx: 2, rowIdx: 2 }, - // bottomRight: { idx: 3, rowIdx: 3 }, - // isDragging: true - // })); - // }); - - // it('should fire onSelectedCellRangeChange on completing a selection', () => { - // const { props } = setup(); - // props.eventBus.dispatch(EventTypes.SELECT_START, { idx: 2, rowIdx: 2 }); - // props.eventBus.dispatch(EventTypes.SELECT_UPDATE, { idx: 3, rowIdx: 3 }); - // props.eventBus.dispatch(EventTypes.SELECT_END); - // expect(props.onSelectedCellRangeChange).toHaveBeenCalledWith(expect.objectContaining({ - // topLeft: { idx: 2, rowIdx: 2 }, - // bottomRight: { idx: 3, rowIdx: 3 }, - // isDragging: false - // })); - // }); - - // it('should fire onSelectedCellRangeChange and onCRSCompleted on modifying a selection via they keyboard', () => { - // const currentCell = { idx: 0, rowIdx: 0 }; - // const { wrapper, props } = setup({}, { selectedPosition: currentCell }); - // pressKey(wrapper, 'ArrowRight', { shiftKey: true }); - // expect(props.onSelectedCellRangeChange).toHaveBeenCalledWith(expect.objectContaining({ - // topLeft: { idx: 0, rowIdx: 0 }, - // bottomRight: { idx: 1, rowIdx: 0 } - // })); - // }); - // }); - - describe('Keyboard navigation functionality', () => { - it('Press enter should enable editing', () => { - const { wrapper } = setup({}, { idx: 0, rowIdx: 0 }); - pressKey(wrapper, 'Enter'); - wrapper.update(); - expect(wrapper.find(EditorContainer)).toHaveLength(1); - }); - - describe('When current selected cell is not in outer bounds', () => { - it('Press arrow up should move up', () => { - const { wrapper, props } = setup({}, { idx: 0, rowIdx: 1 }); - pressKey(wrapper, 'ArrowUp'); - expect(props.onSelectedCellChange).toHaveBeenCalledWith({ idx: 0, rowIdx: 0 }); - }); - - it('Press arrow right should move right', () => { - const { wrapper, props } = setup({}, { idx: 0, rowIdx: 0 }); - pressKey(wrapper, 'ArrowRight'); - expect(props.onSelectedCellChange).toHaveBeenCalledWith({ idx: 1, rowIdx: 0 }); - }); - - it('Press arrow down should move down', () => { - const { wrapper, props } = setup({}, { idx: 0, rowIdx: 0 }); - pressKey(wrapper, 'ArrowDown'); - expect(props.onSelectedCellChange).toHaveBeenCalledWith({ idx: 0, rowIdx: 1 }); - }); - - it('Press arrow left should move left', () => { - const { wrapper, props } = setup({}, { idx: 1, rowIdx: 0 }); - pressKey(wrapper, 'ArrowLeft'); - expect(props.onSelectedCellChange).toHaveBeenCalledWith({ idx: 0, rowIdx: 0 }); - }); - - it('Press tab should move right', () => { - const { wrapper, props } = setup({}, { idx: 0, rowIdx: 0 }); - pressKey(wrapper, 'Tab'); - expect(props.onSelectedCellChange).toHaveBeenCalledWith({ idx: 1, rowIdx: 0 }); - }); - - it('Press shiftKey + tab should move left', () => { - const { wrapper, props } = setup({}, { idx: 1, rowIdx: 0 }); - pressKey(wrapper, 'Tab', { shiftKey: true }); - expect(props.onSelectedCellChange).toHaveBeenCalledWith({ idx: 0, rowIdx: 0 }); - }); - }); - - describe('When next cell is out of bounds', () => { - it('Press arrow left should not move left', () => { - const { wrapper, props } = setup({}, { idx: 0, rowIdx: 0 }); - pressKey(wrapper, 'ArrowLeft'); - expect(props.onSelectedCellChange).toHaveBeenCalledTimes(0); - }); - - it('Press arrow right should not move right', () => { - const { wrapper, props } = setup({}, { idx: NUMBER_OF_COLUMNS - 1, rowIdx: 0 }); - pressKey(wrapper, 'ArrowRight'); - expect(props.onSelectedCellChange).toHaveBeenCalledTimes(0); - }); - - it('Press arrow up should not move up', () => { - const { wrapper, props } = setup({}, { idx: 0, rowIdx: 0 }); - pressKey(wrapper, 'ArrowUp'); - expect(props.onSelectedCellChange).toHaveBeenCalledTimes(0); - }); - }); - - describe('using keyboard to navigate through the grid by pressing Tab or Shift+Tab', () => { - // enzyme doesn't allow dom keyboard navigation, but we can assume that if - // prevent default isn't called, it lets the dom do normal navigation - - const assertGridWasExited = (wrapper: ReturnType['wrapper']) => { - expect(wrapper.find(selectionMaskSelector)).toHaveLength(0); - }; - - const tabCell = (props: Partial>, shiftKey?: boolean, state?: { selectedPosition: Position }) => { - const { wrapper, props: { onSelectedCellChange } } = setup(props, state?.selectedPosition); - const preventDefaultSpy = jest.fn(); - simulateTab(wrapper, shiftKey, preventDefaultSpy); - wrapper.update(); - return { wrapper, preventDefaultSpy, onSelectedCellChange }; - }; - - const assertExitGridOnTab = (props: Partial>, shiftKey?: boolean, state?: { selectedPosition: Position }) => { - const { wrapper, preventDefaultSpy } = tabCell(props, shiftKey, state); - expect(preventDefaultSpy).not.toHaveBeenCalled(); - assertGridWasExited(wrapper); - }; - - const assertSelectedCellOnTab = (props: Partial>, shiftKey?: boolean, state?: { selectedPosition: Position }) => { - const { preventDefaultSpy, onSelectedCellChange } = tabCell(props, shiftKey, state); - expect(preventDefaultSpy).toHaveBeenCalled(); - return expect(onSelectedCellChange); - }; - - describe('when cellNavigationMode is changeRow', () => { - const cellNavigationMode = CellNavigationMode.CHANGE_ROW; - // it('allows the user to exit the grid with Tab if there are no rows', () => { - // assertExitGridOnTab({ cellNavigationMode, rows: [] }); - // }); - // it('allows the user to exit the grid with Shift+Tab if there are no rows', () => { - // assertExitGridOnTab({ cellNavigationMode, rows: [] }, true); - // }); - it('allows the user to exit to the grid with Shift+Tab at the first cell of the grid', () => { - const selectedPosition = { rowIdx: 0, idx: 0 }; - assertExitGridOnTab({ cellNavigationMode }, true, { selectedPosition }); - }); - it('allows the user to exit the grid when they press Tab at the last cell in the grid', () => { - const selectedPosition = { rowIdx: ROWS_COUNT - 1, idx: NUMBER_OF_COLUMNS - 1 }; - assertExitGridOnTab({ cellNavigationMode }, false, { selectedPosition }); - }); - it('goes to the next cell when the user presses Tab and they are not at the end of a row', () => { - const selectedPosition = { rowIdx: 3, idx: 3 }; - assertSelectedCellOnTab({ cellNavigationMode }, false, { selectedPosition }) - .toHaveBeenCalledWith({ rowIdx: 3, idx: 4 }); - }); - it('goes to the beginning of the next row when the user presses Tab and they are at the end of a row', () => { - const selectedPosition = { rowIdx: 2, idx: NUMBER_OF_COLUMNS - 1 }; - assertSelectedCellOnTab({ cellNavigationMode }, false, { selectedPosition }) - .toHaveBeenCalledWith({ rowIdx: 3, idx: 0 }); - }); - it('goes to the previous cell when the user presses Shift+Tab and they are not at the beginning of a row', () => { - const selectedPosition = { rowIdx: 2, idx: 2 }; - assertSelectedCellOnTab({ cellNavigationMode }, true, { selectedPosition }) - .toHaveBeenCalledWith({ rowIdx: 2, idx: 1 }); - }); - it('goes to the end of the previous row when the user presses Shift+Tab and they are at the beginning of a row', () => { - const selectedPosition = { rowIdx: 2, idx: 0 }; - assertSelectedCellOnTab({ cellNavigationMode }, true, { selectedPosition }) - .toHaveBeenCalledWith({ rowIdx: 1, idx: NUMBER_OF_COLUMNS - 1 }); - }); - }); - - describe('when cellNavigationMode is none', () => { - const cellNavigationMode = CellNavigationMode.NONE; - // it('allows the user to exit the grid with Tab if there are no rows', () => { - // assertExitGridOnTab({ cellNavigationMode, rows: [] }); - // }); - // it('allows the user to exit the grid with Shift+Tab if there are no rows', () => { - // assertExitGridOnTab({ cellNavigationMode, rows: [] }, true); - // }); - it('allows the user to exit the grid when they press Shift+Tab at the first cell of the grid', () => { - const selectedPosition = { rowIdx: 0, idx: 0 }; - assertExitGridOnTab({ cellNavigationMode }, true, { selectedPosition }); - }); - it('allows the user to exit the grid when they press Tab at the last cell in the grid', () => { - const selectedPosition = { rowIdx: ROWS_COUNT - 1, idx: NUMBER_OF_COLUMNS - 1 }; - assertExitGridOnTab({ cellNavigationMode }, false, { selectedPosition }); - }); - it('goes to the next cell when the user presses Tab and they are not at the end of a row', () => { - const selectedPosition = { rowIdx: 3, idx: 3 }; - assertSelectedCellOnTab({ cellNavigationMode }, false, { selectedPosition }) - .toHaveBeenCalledWith({ rowIdx: 3, idx: 4 }); - }); - it('goes to the first cell of the next row when they press Tab and they are at the end of a row', () => { - const selectedPosition = { rowIdx: 3, idx: NUMBER_OF_COLUMNS - 1 }; - assertSelectedCellOnTab({ cellNavigationMode }, false, { selectedPosition }) - .toHaveBeenCalledWith({ rowIdx: 4, idx: 0 }); - }); - it('goes to the previous cell when the user presses Shift+Tab and they are not at the beginning of a row', () => { - const selectedPosition = { rowIdx: 2, idx: 2 }; - assertSelectedCellOnTab({ cellNavigationMode }, true, { selectedPosition }) - .toHaveBeenCalledWith({ rowIdx: 2, idx: 1 }); - }); - it('goes to the last cell of the previous row when they press Shift+Tab and they are at the beginning of a row', () => { - const selectedPosition = { rowIdx: 3, idx: 0 }; - assertSelectedCellOnTab({ cellNavigationMode }, true, { selectedPosition }) - .toHaveBeenCalledWith({ rowIdx: 2, idx: NUMBER_OF_COLUMNS - 1 }); - }); - }); - - describe('when cellNavigationMode is loopOverRow', () => { - const cellNavigationMode = CellNavigationMode.LOOP_OVER_ROW; - // it('allows the user to exit the grid with Tab if there are no rows', () => { - // assertExitGridOnTab({ cellNavigationMode, rows: [] }); - // }); - // it('allows the user to exit the grid with Shift+Tab if there are no rows', () => { - // assertExitGridOnTab({ cellNavigationMode, rows: [] }, true); - // }); - it('goes to the first cell in the row when the user presses Tab and they are at the end of a row', () => { - const selectedPosition = { rowIdx: ROWS_COUNT - 1, idx: NUMBER_OF_COLUMNS - 1 }; - assertSelectedCellOnTab({ cellNavigationMode }, false, { selectedPosition }) - .toHaveBeenCalledWith({ rowIdx: ROWS_COUNT - 1, idx: 0 }); - }); - it('goes to the last cell in the row when the user presses Shift+Tab and they are at the beginning of a row', () => { - const selectedPosition = { rowIdx: 0, idx: 0 }; - assertSelectedCellOnTab({ cellNavigationMode }, true, { selectedPosition }) - .toHaveBeenCalledWith({ rowIdx: 0, idx: NUMBER_OF_COLUMNS - 1 }); - }); - it('goes to the next cell when the user presses Tab and they are not at the end of a row', () => { - const selectedPosition = { rowIdx: 3, idx: 3 }; - assertSelectedCellOnTab({ cellNavigationMode }, false, { selectedPosition }) - .toHaveBeenCalledWith({ rowIdx: 3, idx: 4 }); - }); - it('goes to the previous cell when the user presses Shift+Tab and they are not at the beginning of a row', () => { - const selectedPosition = { rowIdx: 2, idx: 2 }; - assertSelectedCellOnTab({ cellNavigationMode }, true, { selectedPosition }) - .toHaveBeenCalledWith({ rowIdx: 2, idx: 1 }); - }); - }); - }); - }); - - describe('Copy functionality', () => { - const setupCopy = () => { - const rows = [ - { Column1: '1' }, - { Column1: '2' }, - { Column1: '3' } - ]; - const { wrapper, props } = setup({ rows }); - act(() => { - props.eventBus.dispatch('CELL_SELECT', { idx: 1, rowIdx: 2 }); - }); - return { wrapper, props }; - }; - - it('should not render a CopyMask component if there is no copied cell', () => { - const { wrapper } = setupCopy(); - expect(wrapper.find(copyMaskSelector)).toHaveLength(0); - }); - - it('should render a CopyMask component when a cell is copied', () => { - const { wrapper } = setupCopy(); - pressKey(wrapper, 'c', { ctrlKey: true }); - wrapper.update(); - expect(wrapper.find(copyMaskSelector)).toHaveLength(1); - expect(wrapper.find(copyMaskSelector).props().style).toStrictEqual({ - height: 30, - width: 100, - transform: 'translate(100px, 60px)', - zIndex: 1 - }); - }); - - it('should remove the CopyMask component on escape', () => { - const { wrapper } = setupCopy(); - pressKey(wrapper, 'c', { ctrlKey: true }); - pressKey(wrapper, 'Escape'); - wrapper.update(); - expect(wrapper.find(copyMaskSelector)).toHaveLength(0); - }); - - it('should update the selected cell with the copied value on paste', () => { - const { wrapper, props } = setupCopy(); - act(() => { - props.eventBus.dispatch('CELL_SELECT', { idx: 1, rowIdx: 2 }); - }); - // Copy selected cell - pressKey(wrapper, 'c', { ctrlKey: true }); - // Move up - pressKey(wrapper, 'ArrowUp'); - // Paste copied cell - pressKey(wrapper, 'v', { ctrlKey: true }); - - expect(props.onRowsUpdate).toHaveBeenCalledWith({ - cellKey: 'Column1', - fromRow: 2, - toRow: 1, - updated: { Column1: '3' }, - action: UpdateActions.COPY_PASTE, - fromCellKey: 'Column1' - }); - }); - }); - - describe('Drag functionality', () => { - const setupDrag = (rowIdx = 2) => { - const rows = [ - { Column1: '1' }, - { Column1: '2' }, - { Column1: '3' }, - { Column1: '4' }, - { Column1: '5' } - ]; - const { wrapper, props } = setup({ rows }, { idx: 1, rowIdx }); - return { wrapper, props }; - }; - - it('should not render the DragMask component if drag has not started', () => { - const { wrapper } = setupDrag(); - - expect(wrapper.find(DragMask)).toHaveLength(0); - }); - - it('should render the DragMask component on cell drag', () => { - const { wrapper } = setupDrag(); - const setData = jest.fn(); - wrapper.find('.drag-handle').simulate('dragstart', { - target: { className: 'test' }, - dataTransfer: { setData } - }); - - expect(wrapper.find(DragMask)).toHaveLength(1); - expect(setData).toHaveBeenCalled(); - }); - - it('should update the dragged over cells on downwards drag end', () => { - const { wrapper, props } = setupDrag(); - const setData = jest.fn(); - wrapper.find('.drag-handle').simulate('dragstart', { - target: { className: 'test' }, - dataTransfer: { setData } - }); - act(() => { - props.eventBus.dispatch('DRAG_ENTER', 6); - }); - wrapper.find('.drag-handle').simulate('dragEnd'); - - expect(props.onRowsUpdate).toHaveBeenCalledWith({ - cellKey: 'Column1', - fromRow: 2, - toRow: 6, - updated: { Column1: '3' }, - action: UpdateActions.CELL_DRAG - }); - }); - - it('should update the dragged over cells on upwards drag end', () => { - const { wrapper, props } = setupDrag(4); - const setData = jest.fn(); - wrapper.find('.drag-handle').simulate('dragstart', { - target: { className: 'test' }, - dataTransfer: { setData } - }); - act(() => { - props.eventBus.dispatch('DRAG_ENTER', 0); - }); - wrapper.find('.drag-handle').simulate('dragEnd'); - - expect(props.onRowsUpdate).toHaveBeenCalledWith({ - cellKey: 'Column1', - fromRow: 4, - toRow: 0, - updated: { Column1: '5' }, - action: UpdateActions.CELL_DRAG - }); - }); - }); -}); diff --git a/src/masks/InteractionMasks.tsx b/src/masks/InteractionMasks.tsx deleted file mode 100644 index 7888a8d359..0000000000 --- a/src/masks/InteractionMasks.tsx +++ /dev/null @@ -1,383 +0,0 @@ -import React, { useState, useRef, useEffect, useCallback } from 'react'; - -// Components -import CellMask from './CellMask'; -import DragMask, { DraggedPosition } from './DragMask'; -import EditorContainer from '../editors/EditorContainer'; -import EditorPortal from '../editors/EditorPortal'; -import { legacyCellInput } from '../editors/CellInputHandlers'; - -// Utils -import { - isCtrlKeyHeldDown, - getSelectedDimensions as getDimensions, - getNextSelectedCellPosition, - canExitGrid, - isSelectedCellEditable -} from '../utils'; - -// Types -import EventBus from '../EventBus'; -import { UpdateActions, CellNavigationMode } from '../common/enums'; -import { CalculatedColumn, Position, Dimension, CommitEvent } from '../common/types'; -import { DataGridProps } from '../DataGrid'; - -type SharedCanvasProps = Pick, - | 'rows' - | 'onCheckCellIsEditable' - | 'onSelectedCellChange' -> & Pick>, - | 'rowHeight' - | 'enableCellCopyPaste' - | 'enableCellDragAndDrop' - | 'cellNavigationMode' - | 'editorPortalTarget' - | 'onRowsUpdate' ->; - -interface SelectCellState extends Position { - status: 'SELECT'; -} - -interface EditCellState extends Position { - status: 'EDIT'; - key: string | null; -} - -export interface InteractionMasksProps extends SharedCanvasProps { - columns: readonly CalculatedColumn[]; - gridRef: React.RefObject; - totalHeaderHeight: number; - scrollLeft: number; - scrollTop: number; - eventBus: EventBus; - scrollToCell: (cell: Position) => void; -} - -export default function InteractionMasks({ - columns, - rows, - rowHeight, - eventBus, - enableCellCopyPaste, - enableCellDragAndDrop, - editorPortalTarget, - cellNavigationMode, - gridRef, - totalHeaderHeight, - scrollLeft, - scrollTop, - onSelectedCellChange, - onCheckCellIsEditable, - onRowsUpdate, - scrollToCell -}: InteractionMasksProps) { - const [selectedPosition, setSelectedPosition] = useState({ idx: -1, rowIdx: -1, status: 'SELECT' }); - const [copiedPosition, setCopiedPosition] = useState(null); - const [draggedPosition, setDraggedPosition] = useState(null); - const selectionMaskRef = useRef(null); - - // Focus on the selection mask when the selected position is changed or the editor is closed - useEffect(() => { - if (selectedPosition.rowIdx === -1 || selectedPosition.idx === -1 || selectedPosition.status === 'EDIT') return; - selectionMaskRef.current?.focus(); - }, [selectedPosition]); - - useEffect(() => { - return eventBus.subscribe('CELL_SELECT', selectCell); - }); - - useEffect(() => { - if (draggedPosition === null) return; - const handleDragEnter = (overRowIdx: number) => { - setDraggedPosition({ ...draggedPosition, overRowIdx }); - }; - return eventBus.subscribe('ROW_DRAG_ENTER', handleDragEnter); - }, [draggedPosition, eventBus]); - - const closeEditor = useCallback(() => { - setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, status: 'SELECT' })); - }, []); - - // 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) { - setSelectedPosition({ idx: -1, rowIdx: -1, status: 'SELECT' }); - setCopiedPosition(null); - setDraggedPosition(null); - } - - function getEditorPosition() { - if (gridRef.current === null) return { left: 0, top: 0 }; - const { left, top } = gridRef.current.getBoundingClientRect(); - const { scrollTop: docTop, scrollLeft: docLeft } = document.scrollingElement || document.documentElement; - const gridLeft = left + docLeft; - const gridTop = top + docTop; - const column = columns[selectedPosition.idx]; - return { - left: gridLeft + column.left - (column.frozen ? 0 : scrollLeft), - top: gridTop + totalHeaderHeight + selectedPosition.rowIdx * rowHeight - scrollTop - }; - } - - function getNextPosition(key: string, mode = cellNavigationMode, shiftKey = false) { - const { idx, rowIdx } = selectedPosition; - let nextPosition: Position; - switch (key) { - case 'ArrowUp': - nextPosition = { idx, rowIdx: rowIdx - 1 }; - break; - case 'ArrowDown': - nextPosition = { idx, rowIdx: rowIdx + 1 }; - break; - case 'ArrowLeft': - nextPosition = { idx: idx - 1, rowIdx }; - break; - case 'ArrowRight': - nextPosition = { idx: idx + 1, rowIdx }; - break; - case 'Tab': - nextPosition = { idx: idx + (shiftKey ? -1 : 1), rowIdx }; - break; - default: - nextPosition = { idx, rowIdx }; - break; - } - - return getNextSelectedCellPosition({ - columns, - rowsCount: rows.length, - cellNavigationMode: mode, - nextPosition - }); - } - - function onKeyDown(event: React.KeyboardEvent): void { - const column = columns[selectedPosition.idx]; - const row = rows[selectedPosition.rowIdx]; - const isActivatedByUser = (column.unsafe_onCellInput ?? legacyCellInput)(event, row) === true; - - const { key } = event; - if (enableCellCopyPaste && isCtrlKeyHeldDown(event)) { - // event.key may be uppercase `C` or `V` - const lowerCaseKey = event.key.toLowerCase(); - if (lowerCaseKey === 'c') return handleCopy(); - if (lowerCaseKey === 'v') return handlePaste(); - } - - const canOpenEditor = selectedPosition.status === 'SELECT' && isCellEditable(selectedPosition); - - switch (key) { - case 'Enter': - if (canOpenEditor) { - setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, status: 'EDIT', key: 'Enter' })); - } else if (selectedPosition.status === 'EDIT') { - setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, status: 'SELECT' })); - } - break; - case 'Escape': - closeEditor(); - setCopiedPosition(null); - break; - case 'Tab': - onPressTab(event); - break; - case 'ArrowUp': - case 'ArrowDown': - case 'ArrowLeft': - case 'ArrowRight': - event.preventDefault(); - selectCell(getNextPosition(key)); - break; - default: - if (canOpenEditor && isActivatedByUser) { - setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, status: 'EDIT', key })); - } - break; - } - } - - function onPressTab(e: React.KeyboardEvent): void { - // If we are in a position to leave the grid, stop editing but stay in that cell - if (canExitGrid({ cellNavigationMode, columns, rowsCount: rows.length, selectedPosition, shiftKey: e.shiftKey })) { - if (selectedPosition.status === 'EDIT') { - closeEditor(); - return; - } - - // Reset the selected position before exiting - setSelectedPosition({ idx: -1, rowIdx: -1, status: 'SELECT' }); - return; - } - - e.preventDefault(); - const tabCellNavigationMode = cellNavigationMode === CellNavigationMode.NONE - ? CellNavigationMode.CHANGE_ROW - : cellNavigationMode; - const nextPosition = getNextPosition('Tab', tabCellNavigationMode, e.shiftKey); - selectCell(nextPosition); - } - - function handleCopy(): void { - const { idx, rowIdx } = selectedPosition; - const value = rows[rowIdx][columns[idx].key as keyof R]; - setCopiedPosition({ idx, rowIdx, value }); - } - - function handlePaste(): void { - if (copiedPosition === null || !isCellEditable(selectedPosition)) { - return; - } - - const { rowIdx: toRow } = selectedPosition; - - 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, - action: UpdateActions.COPY_PASTE, - fromCellKey - }); - } - - function isCellWithinBounds({ idx, rowIdx }: Position): boolean { - return rowIdx >= 0 && rowIdx < rows.length && idx >= 0 && idx < columns.length; - } - - function isCellEditable(position: Position) { - return isCellWithinBounds(position) - && isSelectedCellEditable({ columns, rows, selectedPosition: position, onCheckCellIsEditable }); - } - - function selectCell(position: Position, enableEditor = false): void { - if (!isCellWithinBounds(position)) return; - - if (enableEditor && isCellEditable(position)) { - setSelectedPosition({ ...position, status: 'EDIT', key: null }); - } else { - setSelectedPosition({ ...position, status: 'SELECT' }); - } - scrollToCell(position); - onSelectedCellChange?.({ ...position }); - } - - function isDragEnabled(): boolean { - return enableCellDragAndDrop && isCellEditable(selectedPosition); - } - - function handleDragStart(e: React.DragEvent): void { - e.dataTransfer.effectAllowed = 'copy'; - // Setting data is required to make an element draggable in FF - const transferData = JSON.stringify(selectedPosition); - try { - e.dataTransfer.setData('text/plain', transferData); - } catch (ex) { - // IE only supports 'text' and 'URL' for the 'type' argument - e.dataTransfer.setData('text', transferData); - } - setDraggedPosition({ ...selectedPosition, overRowIdx: selectedPosition.rowIdx }); - } - - function handleDragEnd() { - if (draggedPosition === null) return; - - const { rowIdx, overRowIdx } = draggedPosition; - const column = columns[draggedPosition.idx]; - const cellKey = column.key; - const value = rows[rowIdx][cellKey as keyof R]; - - onRowsUpdate({ - cellKey, - fromRow: rowIdx, - toRow: overRowIdx, - updated: { [cellKey]: value } as unknown as never, - action: UpdateActions.CELL_DRAG - }); - - setDraggedPosition(null); - } - - function onDragHandleDoubleClick(): void { - const column = columns[selectedPosition.idx]; - const cellKey = column.key; - const value = rows[selectedPosition.rowIdx][cellKey as keyof R]; - - onRowsUpdate({ - cellKey, - fromRow: selectedPosition.rowIdx, - toRow: rows.length - 1, - updated: { [cellKey]: value } as unknown as never, - action: UpdateActions.COLUMN_FILL - }); - } - - function onCommit({ cellKey, rowIdx, updated }: CommitEvent): void { - onRowsUpdate({ - cellKey, - fromRow: rowIdx, - toRow: rowIdx, - updated, - action: UpdateActions.CELL_UPDATE - }); - closeEditor(); - } - - function getSelectedDimensions(selectedPosition: Position): Dimension { - return getDimensions({ selectedPosition, columns, scrollLeft, rowHeight }); - } - - return ( -
- {copiedPosition && isCellWithinBounds(copiedPosition) && ( - - )} - {draggedPosition && isCellWithinBounds(draggedPosition) && ( - - )} - {selectedPosition.status === 'SELECT' && isCellWithinBounds(selectedPosition) && ( - - {isDragEnabled() && ( -
- )} - - )} - {selectedPosition.status === 'EDIT' && isCellWithinBounds(selectedPosition) && ( - - - firstEditorKeyPress={selectedPosition.key} - onCommit={onCommit} - onCommitCancel={closeEditor} - rowIdx={selectedPosition.rowIdx} - row={rows[selectedPosition.rowIdx]} - rowHeight={rowHeight} - column={columns[selectedPosition.idx]} - scrollLeft={scrollLeft} - scrollTop={scrollTop} - {...getEditorPosition()} - /> - - )} -
- ); -} diff --git a/style/index.less b/style/index.less index f542087f21..0a1d19fdd1 100644 --- a/style/index.less +++ b/style/index.less @@ -3,5 +3,4 @@ @import 'core.less'; @import 'editor.less'; @import 'header.less'; -@import 'interaction-masks.less'; @import 'row.less'; diff --git a/style/interaction-masks.less b/style/interaction-masks.less deleted file mode 100644 index 2efe01992c..0000000000 --- a/style/interaction-masks.less +++ /dev/null @@ -1,46 +0,0 @@ -.rdg-selected { - border: 2px solid #66afe9; -} - -.rdg-selected .drag-handle { - pointer-events: auto; - position: absolute; - bottom: -5px; - right: -4px; - background: #66afe9; - width: 8px; - height: 8px; - border: 1px solid #fff; - border-right: 0px; - border-bottom: 0px; - cursor: crosshair; - cursor: -moz-grab; - cursor: -webkit-grab; - cursor: grab; -} - -.rdg-selected:hover .drag-handle { - bottom: -8px; - right: -7px; - background: white; - width: 16px; - height: 16px; - border: 1px solid #66afe9; -} - -.react-grid-cell-dragged-over-up, .react-grid-cell-dragged-over-down { - border: 1px dashed black; - background: rgba(0, 0, 255, 0.2) !important; -} - -.react-grid-cell-dragged-over-up { - border-bottom-width: 0; -} - -.react-grid-cell-dragged-over-down { - border-top-width: 0; -} - -.rdg-cell-copied { - background: rgba(0, 0, 255, 0.2) !important; -} From 0600a9f9f8b9dee0b64f088b81f91a539021f39b Mon Sep 17 00:00:00 2001 From: Mahajan Date: Fri, 26 Jun 2020 15:00:03 -0500 Subject: [PATCH 09/42] Cancel copying --- src/DataGrid.tsx | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 1242cc2819..09e6b9fd06 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -385,13 +385,22 @@ function DataGrid({ if (lowerCaseKey === 'v') return handlePaste(); } - if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(event.key)) { - event.preventDefault(); - navigate(event.key, event.shiftKey); - return; + switch (event.key) { + case 'Escape': + setCopiedPosition(null); + return; + case 'ArrowUp': + case 'ArrowDown': + case 'ArrowLeft': + case 'ArrowRight': + case 'Tab': + event.preventDefault(); + navigate(event.key, event.shiftKey); + break; + default: + handleCellInput(event); + break; } - - handleCellInput(event); } function handleScroll(event: React.UIEvent) { From a59665345e26a0680ccfdb8f25b05123d5ee6a77 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Fri, 26 Jun 2020 15:23:11 -0500 Subject: [PATCH 10/42] Remove edit check --- src/Cell.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index a6fdc20378..2f357e408d 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -2,7 +2,7 @@ import React, { forwardRef, memo, useEffect, useRef } from 'react'; import clsx from 'clsx'; import { CellRendererProps } from './common/types'; -import { preventDefault, wrapEvent, canEdit } from './utils'; +import { preventDefault, wrapEvent } from './utils'; import { useCombinedRefs } from './hooks'; function Cell({ @@ -111,7 +111,7 @@ function Cell({ isRowSelected={isRowSelected} onRowSelectionChange={onRowSelectionChange} /> - {enableCellDragAndDrop && isSelected && canEdit(column, row) && ( + {enableCellDragAndDrop && isSelected && (
Date: Sun, 28 Jun 2020 20:55:39 -0500 Subject: [PATCH 11/42] Cleanup --- src/DataGrid.tsx | 25 ++++++++----------------- src/editors/index.ts | 3 +++ src/index.ts | 2 +- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 09e6b9fd06..ced85c5cff 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -16,9 +16,7 @@ import FilterRow from './FilterRow'; import Row from './Row'; import SummaryRow from './SummaryRow'; import { ValueFormatter } from './formatters'; -import { legacyCellInput } from './editors/CellInputHandlers'; -import EditorContainer from './editors/EditorContainer'; -import EditorPortal from './editors/EditorPortal'; +import { legacyCellInput, EditorContainer, EditorPortal } from './editors'; import { assertIsValidKey, getColumnMetrics, @@ -340,19 +338,14 @@ function DataGrid({ }); useEffect(() => { - const handleDragEnter = (overRowIdx: number) => { - setDraggedOverRowIdx(overRowIdx); - }; - return eventBus.subscribe('ROW_DRAG_ENTER', handleDragEnter); - }, [eventBus]); + if (!enableCellDragAndDrop) return; + return eventBus.subscribe('ROW_DRAG_ENTER', setDraggedOverRowIdx); + }, [enableCellDragAndDrop, eventBus]); useEffect(() => { - function handleDragStart() { - setDraggedOverRowIdx(selectedPosition.rowIdx); - } - - return eventBus.subscribe('CELL_DRAG_START', handleDragStart); - }, [eventBus, selectedPosition]); + if (!enableCellDragAndDrop) return; + return eventBus.subscribe('CELL_DRAG_START', () => setDraggedOverRowIdx(selectedPosition.rowIdx)); + }, [enableCellDragAndDrop, eventBus, selectedPosition]); useEffect(() => { return eventBus.subscribe('CELL_DRAG_END', handleDragEnd); @@ -665,9 +658,7 @@ function DataGrid({ } return ( -
+
{rowElements} {getEditorContainer()}
diff --git a/src/editors/index.ts b/src/editors/index.ts index d58276ced9..f0538e005f 100644 --- a/src/editors/index.ts +++ b/src/editors/index.ts @@ -1 +1,4 @@ export { default as SimpleTextEditor } from './SimpleTextEditor'; +export { default as EditorPortal } from './EditorPortal'; +export { default as EditorContainer } from './EditorContainer'; +export * from './CellInputHandlers'; diff --git a/src/index.ts b/src/index.ts index 4d0736eaa4..0ab9bef505 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,6 @@ export { default as Cell } from './Cell'; export { default as Row } from './Row'; export * from './Columns'; export * from './formatters'; -export * from './editors'; +export { SimpleTextEditor } from './editors'; export * from './common/enums'; export * from './common/types'; From 362414ebc2bd4366f0a3216ed7b352cbe7a9767f Mon Sep 17 00:00:00 2001 From: Mahajan Date: Mon, 29 Jun 2020 09:22:24 -0500 Subject: [PATCH 12/42] Address comments --- src/DataGrid.tsx | 54 ++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index ced85c5cff..cdaf0d1b77 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -560,6 +560,32 @@ function DataGrid({ : rowIdx > currentRowIdx && currentRowIdx >= draggedOverRowIdx; } + function navigate(key: string, shiftKey: boolean) { + let nextPosition = getNextPosition(key, shiftKey, selectedPosition); + 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 })) { + // Reset the selected position before exiting + setSelectedPosition({ idx: -1, rowIdx: -1, mode: 'SELECT' }); + return; + } + + mode = cellNavigationMode === CellNavigationMode.NONE + ? CellNavigationMode.CHANGE_ROW + : cellNavigationMode; + } + + nextPosition = getNextSelectedCellPosition({ + columns, + rowsCount: rows.length, + cellNavigationMode: mode, + nextPosition + }); + + selectCell(nextPosition); + } + function getEditorContainer() { if (selectedPosition.mode === 'SELECT') return null; @@ -596,32 +622,6 @@ function DataGrid({ ); } - function navigate(key: string, shiftKey: boolean) { - let nextPosition = getNextPosition(key, shiftKey, selectedPosition); - 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 })) { - // Reset the selected position before exiting - setSelectedPosition({ idx: -1, rowIdx: -1, mode: 'SELECT' }); - return; - } - - mode = cellNavigationMode === CellNavigationMode.NONE - ? CellNavigationMode.CHANGE_ROW - : cellNavigationMode; - } - - nextPosition = getNextSelectedCellPosition({ - columns, - rowsCount: rows.length, - cellNavigationMode: mode, - nextPosition - }); - - selectCell(nextPosition); - } - function getViewportRows() { const rowElements = []; @@ -666,7 +666,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 >= rows.length) { setSelectedPosition({ idx: -1, rowIdx: -1, mode: 'SELECT' }); setCopiedPosition(null); setDraggedOverRowIdx(undefined); From 36d0ce960819314af820a6caaac21aeb9cf0402f Mon Sep 17 00:00:00 2001 From: Mahajan Date: Mon, 29 Jun 2020 16:13:47 -0500 Subject: [PATCH 13/42] Move DragHandle to the parent DataGrid component --- src/Cell.tsx | 32 ----------- src/DataGrid.tsx | 135 +++++++++++++++++++++++++------------------- src/EventBus.ts | 4 -- src/Row.tsx | 9 ++- src/common/types.ts | 5 +- style/cell.less | 24 ++++---- 6 files changed, 93 insertions(+), 116 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index 2f357e408d..96bb0f5222 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -16,7 +16,6 @@ function Cell({ row, rowIdx, eventBus, - enableCellDragAndDrop, onRowClick, onClick, onDoubleClick, @@ -49,28 +48,6 @@ function Cell({ selectCell(true); } - function handleDragStart(event: React.DragEvent) { - event.dataTransfer.effectAllowed = 'copy'; - // Setting data is required to make an element draggable in FF - const transferData = JSON.stringify({}); - try { - event.dataTransfer.setData('text/plain', transferData); - } catch (ex) { - // IE only supports 'text' and 'URL' for the 'type' argument - event.dataTransfer.setData('text', transferData); - } - eventBus.dispatch('CELL_DRAG_START'); - } - - function handleDragEnd() { - eventBus.dispatch('CELL_DRAG_END'); - } - - function handleDragHandleDoubleClick(event: React.MouseEvent) { - event.stopPropagation(); - eventBus.dispatch('CELL_DRAG_HANDLE_DOUBLE_CLICK'); - } - function onRowSelectionChange(checked: boolean, isShiftClick: boolean) { eventBus.dispatch('ROW_SELECT', { rowIdx, checked, isShiftClick }); } @@ -111,15 +88,6 @@ function Cell({ isRowSelected={isRowSelected} onRowSelectionChange={onRowSelectionChange} /> - {enableCellDragAndDrop && isSelected && ( -
- )}
); } diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index cdaf0d1b77..cf9d8ba8a6 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -9,6 +9,7 @@ import React, { useCallback, createElement } from 'react'; +import clsx from 'clsx'; import EventBus from './EventBus'; import HeaderRow from './HeaderRow'; @@ -337,24 +338,6 @@ function DataGrid({ return eventBus.subscribe('CELL_SELECT', selectCell); }); - useEffect(() => { - if (!enableCellDragAndDrop) return; - return eventBus.subscribe('ROW_DRAG_ENTER', setDraggedOverRowIdx); - }, [enableCellDragAndDrop, eventBus]); - - useEffect(() => { - if (!enableCellDragAndDrop) return; - return eventBus.subscribe('CELL_DRAG_START', () => setDraggedOverRowIdx(selectedPosition.rowIdx)); - }, [enableCellDragAndDrop, eventBus, selectedPosition]); - - useEffect(() => { - return eventBus.subscribe('CELL_DRAG_END', handleDragEnd); - }); - - useEffect(() => { - return eventBus.subscribe('CELL_DRAG_HANDLE_DOUBLE_CLICK', handleDragHandleDoubleClick); - }); - useImperativeHandle(ref, () => ({ scrollToColumn(idx: number) { scrollToCell({ idx }); @@ -423,39 +406,6 @@ function DataGrid({ setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, mode: 'SELECT' })); } - function handleDragEnd() { - if (typeof draggedOverRowIdx === 'undefined') return; - - const { idx, rowIdx } = selectedPosition; - const column = columns[idx]; - const cellKey = column.key; - const value = rows[rowIdx][cellKey as keyof R]; - - onRowsUpdate?.({ - cellKey, - fromRow: rowIdx, - toRow: draggedOverRowIdx, - updated: { [cellKey]: value } as unknown as never, - action: UpdateActions.CELL_DRAG - }); - - setDraggedOverRowIdx(undefined); - } - - function handleDragHandleDoubleClick(): void { - const column = columns[selectedPosition.idx]; - const cellKey = column.key; - const value = rows[selectedPosition.rowIdx][cellKey as keyof R]; - - onRowsUpdate?.({ - cellKey, - fromRow: selectedPosition.rowIdx, - toRow: rows.length - 1, - updated: { [cellKey]: value } as unknown as never, - action: UpdateActions.COLUMN_FILL - }); - } - function handleCopy() { const { idx, rowIdx } = selectedPosition; const value = rows[rowIdx][columns[idx].key as keyof R]; @@ -495,6 +445,55 @@ function DataGrid({ } } + function handleDragStart(event: React.DragEvent) { + event.dataTransfer.effectAllowed = 'copy'; + // Setting data is required to make an element draggable in FF + const transferData = JSON.stringify({}); + try { + event.dataTransfer.setData('text/plain', transferData); + } catch (ex) { + // IE only supports 'text' and 'URL' for the 'type' argument + event.dataTransfer.setData('text', transferData); + } + setDraggedOverRowIdx(selectedPosition.rowIdx); + } + + function handleDragEnd() { + if (typeof draggedOverRowIdx === 'undefined') return; + + const { idx, rowIdx } = selectedPosition; + const column = columns[idx]; + const cellKey = column.key; + const value = rows[rowIdx][cellKey as keyof R]; + + onRowsUpdate?.({ + cellKey, + fromRow: rowIdx, + toRow: draggedOverRowIdx, + updated: { [cellKey]: value } as unknown as never, + action: UpdateActions.CELL_DRAG + }); + + setDraggedOverRowIdx(undefined); + } + + function handleDragHandleDoubleClick(event: React.MouseEvent) { + event.stopPropagation(); + + const column = columns[selectedPosition.idx]; + const cellKey = column.key; + const value = rows[selectedPosition.rowIdx][cellKey as keyof R]; + + onRowsUpdate?.({ + cellKey, + fromRow: selectedPosition.rowIdx, + toRow: rows.length - 1, + updated: { [cellKey]: value } as unknown as never, + action: UpdateActions.COLUMN_FILL + }); + } + + /** * utils */ @@ -586,6 +585,25 @@ function DataGrid({ selectCell(nextPosition); } + function getDragHandle() { + if (!enableCellDragAndDrop || !isCellEditable(selectedPosition) || selectedPosition.mode === 'EDIT') return null; + const { idx, rowIdx } = selectedPosition; + const top = (rowIdx * rowHeight) + totalHeaderHeight + rowHeight; + const column = columns[idx]; + const left = column.left + column.width + (column.frozen ? scrollLeft : 0); // TODO: use position: sticky + + return ( +
+ ); + } + function getEditorContainer() { if (selectedPosition.mode === 'SELECT') return null; @@ -652,17 +670,12 @@ function DataGrid({ onRowClick={onRowClick} rowClass={rowClass} top={rowIdx * rowHeight + totalHeaderHeight} - enableCellDragAndDrop={enableCellDragAndDrop} + setDraggedOverRowIdx={setDraggedOverRowIdx} /> ); } - return ( -
- {rowElements} - {getEditorContainer()} -
- ); + return rowElements; } // Reset the positions if the current values are no longer valid. This can happen if a column or row is removed @@ -709,7 +722,11 @@ function DataGrid({ {rows.length === 0 && emptyRowsRenderer ? createElement(emptyRowsRenderer) : ( <>
- {viewportWidth > 0 && getViewportRows()} +
+ {viewportWidth > 0 && getViewportRows()} + {getEditorContainer()} +
+ {getDragHandle()} {summaryRows?.map((row, rowIdx) => ( key={rowIdx} diff --git a/src/EventBus.ts b/src/EventBus.ts index 02a1283ede..51d46a94ff 100644 --- a/src/EventBus.ts +++ b/src/EventBus.ts @@ -2,11 +2,7 @@ import { Position, SelectRowEvent } from './common/types'; interface EventMap { CELL_SELECT: (position: Position, openEditor?: boolean) => void; - CELL_DRAG_START: () => void; - CELL_DRAG_END: () => void; - CELL_DRAG_HANDLE_DOUBLE_CLICK: () => void; ROW_SELECT: (event: SelectRowEvent) => void; - ROW_DRAG_ENTER: (overRowIdx: number) => void; } type EventName = keyof EventMap; diff --git a/src/Row.tsx b/src/Row.tsx index 51ffc5add8..0b671c4dcc 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -15,21 +15,21 @@ function Row({ selectedCellIdx, copiedCellIdx, draggedOverCellIdx, - onRowClick, row, viewportColumns, + onRowClick, + rowClass, + setDraggedOverRowIdx, onDragEnter, onDragOver, onDrop, - rowClass, top, - enableCellDragAndDrop, ...props }: RowRendererProps) { function handleDragEnter(event: React.DragEvent) { // Prevent default to allow drop event.preventDefault(); - eventBus.dispatch('ROW_DRAG_ENTER', rowIdx); + setDraggedOverRowIdx(rowIdx); } function handleDragOver(event: React.DragEvent) { @@ -69,7 +69,6 @@ function Row({ isDraggedOver={draggedOverCellIdx === column.idx} isRowSelected={isRowSelected} eventBus={eventBus} - enableCellDragAndDrop={enableCellDragAndDrop} onRowClick={onRowClick} /> ))} diff --git a/src/common/types.ts b/src/common/types.ts index b4220d4e63..4d99956bb7 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -113,7 +113,6 @@ export interface CellRendererProps extends Omit) => void; } @@ -128,10 +127,10 @@ export interface RowRendererProps extends Omit) => void; rowClass?: (row: TRow) => string | undefined; - top: number; - enableCellDragAndDrop: boolean; + setDraggedOverRowIdx: (overRowIdx: number) => void; } export interface FilterRendererProps { diff --git a/style/cell.less b/style/cell.less index de97c7148c..1a75668c11 100644 --- a/style/cell.less +++ b/style/cell.less @@ -23,12 +23,6 @@ box-shadow: 2px 0 5px -2px rgba(136, 136, 136, .3); } -.rdg-cell-mask { - position: absolute; - pointer-events: none; - outline: none; -} - .rdg-cell-selected { box-shadow: inset 0 0 0 2px #66afe9; outline: 0; @@ -41,27 +35,31 @@ .rdg-cell-drag-handle { pointer-events: auto; position: absolute; - bottom: -5px; - right: -4px; background: #66afe9; width: 8px; height: 8px; border: 1px solid #fff; - border-right: 0px; - border-bottom: 0px; + border-right: 0; + border-bottom: 0; + margin-left: -6px; + margin-top: -6px; cursor: crosshair; cursor: -moz-grab; cursor: -webkit-grab; cursor: grab; } -.rdg-cell-selected:hover .rdg-cell-drag-handle { - bottom: -8px; - right: -7px; +.rdg-cell-drag-handle-frozen { + z-index: 1; +} + +.rdg-cell-drag-handle:hover { background: white; width: 16px; height: 16px; border: 1px solid #66afe9; + margin-left: -10px; + margin-top: -10px; } .rdg-cell-dragged-over { From ff7d61ed5b4effaad696f6d829f1164e092821e3 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Mon, 29 Jun 2020 16:33:47 -0500 Subject: [PATCH 14/42] Do not paste on the copied cell --- src/DataGrid.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index cf9d8ba8a6..d6d862addd 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -413,7 +413,11 @@ function DataGrid({ } function handlePaste() { - if (copiedPosition === null || !isCellEditable(selectedPosition)) { + if ( + copiedPosition === null + || !isCellEditable(selectedPosition) + || (copiedPosition.idx === selectedPosition.idx && copiedPosition.rowIdx === selectedPosition.rowIdx) + ) { return; } From 34f11c1489891e9a68d5742fec65e2951019ecd2 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Mon, 29 Jun 2020 16:55:54 -0500 Subject: [PATCH 15/42] Remove unnecessary class --- src/DataGrid.tsx | 6 +++--- style/cell.less | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index d6d862addd..ccabf9ade6 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -9,7 +9,6 @@ import React, { useCallback, createElement } from 'react'; -import clsx from 'clsx'; import EventBus from './EventBus'; import HeaderRow from './HeaderRow'; @@ -589,16 +588,17 @@ function DataGrid({ selectCell(nextPosition); } + // TODO: Use "position: sticky" and fix drag handle showing on top of frozen columns function getDragHandle() { if (!enableCellDragAndDrop || !isCellEditable(selectedPosition) || selectedPosition.mode === 'EDIT') return null; const { idx, rowIdx } = selectedPosition; const top = (rowIdx * rowHeight) + totalHeaderHeight + rowHeight; const column = columns[idx]; - const left = column.left + column.width + (column.frozen ? scrollLeft : 0); // TODO: use position: sticky + const left = column.left + column.width + (column.frozen ? scrollLeft : 0); return (
Date: Mon, 29 Jun 2020 21:46:54 -0500 Subject: [PATCH 16/42] Fix copy/dragged cell styles --- style/cell.less | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/style/cell.less b/style/cell.less index 5ccb7ba543..715f18281e 100644 --- a/style/cell.less +++ b/style/cell.less @@ -29,7 +29,7 @@ } .rdg-cell-copied { - background: rgba(0, 0, 255, 0.2) !important; + background-color:#ccccff; } .rdg-cell-drag-handle { @@ -59,6 +59,12 @@ } .rdg-cell-dragged-over { - background: rgba(0, 0, 255, 0.2) !important; - box-shadow: inset 1px 0 black, inset -1px 0 black; + background-color:#ccccff; + border-left: 1px dashed black; + border-right: 1px dashed black; + padding-left: 7px; +} + +.rdg-cell-copied.rdg-cell-dragged-over { + background-color:#9999ff; } From f9b91519a49a8bb562312eff11c8e5b0f60e8479 Mon Sep 17 00:00:00 2001 From: Nicolas Stepien Date: Tue, 30 Jun 2020 18:05:02 +0100 Subject: [PATCH 17/42] Address dragging issues --- src/Cell.tsx | 8 ++-- src/DataGrid.tsx | 105 ++++++++++++++++++++++---------------------- src/Row.tsx | 24 +++------- src/common/types.ts | 2 +- style/cell.less | 29 +++++------- style/core.less | 4 ++ 6 files changed, 77 insertions(+), 95 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index 96bb0f5222..7b970f6e85 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -2,7 +2,7 @@ import React, { forwardRef, memo, useEffect, useRef } from 'react'; import clsx from 'clsx'; import { CellRendererProps } from './common/types'; -import { preventDefault, wrapEvent } from './utils'; +import { wrapEvent } from './utils'; import { useCombinedRefs } from './hooks'; function Cell({ @@ -20,7 +20,6 @@ function Cell({ onClick, onDoubleClick, onContextMenu, - onDragOver, ...props }: CellRendererProps, ref: React.Ref) { const cellRef = useRef(null); @@ -78,7 +77,6 @@ function Cell({ onClick={wrapEvent(handleClick, onClick)} onDoubleClick={wrapEvent(handleDoubleClick, onDoubleClick)} onContextMenu={wrapEvent(handleContextMenu, onContextMenu)} - onDragOver={wrapEvent(preventDefault, onDragOver)} {...props} > ({ isRowSelected={isRowSelected} onRowSelectionChange={onRowSelectionChange} /> + {isSelected && ( + // if (!enableCellDragAndDrop || !isCellEditable(selectedPosition) || selectedPosition.mode === 'EDIT') return null; +
+ )}
); } diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index ccabf9ade6..3ed16857e7 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -9,6 +9,7 @@ import React, { useCallback, createElement } from 'react'; +import clsx from 'clsx'; import EventBus from './EventBus'; import HeaderRow from './HeaderRow'; @@ -238,6 +239,7 @@ function DataGrid({ const [columnWidths, setColumnWidths] = useState>(() => new Map()); const [selectedPosition, setSelectedPosition] = useState({ idx: -1, rowIdx: -1, mode: 'SELECT' }); const [copiedPosition, setCopiedPosition] = useState(null); + const [isDragging, setDragging] = useState(false); const [draggedOverRowIdx, setDraggedOverRowIdx] = useState(undefined); /** @@ -337,6 +339,25 @@ function DataGrid({ return eventBus.subscribe('CELL_SELECT', selectCell); }); + useEffect(() => { + if (!enableCellDragAndDrop || isDragging || draggedOverRowIdx === undefined) return; + + const { idx, rowIdx } = selectedPosition; + const column = columns[idx]; + const cellKey = column.key; + const value = rows[rowIdx][cellKey as keyof R]; + + onRowsUpdate?.({ + cellKey, + fromRow: rowIdx, + toRow: draggedOverRowIdx, + updated: { [cellKey]: value } as unknown as never, + action: UpdateActions.CELL_DRAG + }); + + setDraggedOverRowIdx(undefined); + }); + useImperativeHandle(ref, () => ({ scrollToColumn(idx: number) { scrollToCell({ idx }); @@ -448,39 +469,34 @@ function DataGrid({ } } - function handleDragStart(event: React.DragEvent) { - event.dataTransfer.effectAllowed = 'copy'; - // Setting data is required to make an element draggable in FF - const transferData = JSON.stringify({}); - try { - event.dataTransfer.setData('text/plain', transferData); - } catch (ex) { - // IE only supports 'text' and 'URL' for the 'type' argument - event.dataTransfer.setData('text', transferData); - } - setDraggedOverRowIdx(selectedPosition.rowIdx); - } + function handleMouseDown(event: React.MouseEvent) { + if (!enableCellDragAndDrop) return; + const { target } = event; + if (!(target instanceof HTMLDivElement && target.className === 'rdg-cell-drag-handle')) return; - function handleDragEnd() { - if (typeof draggedOverRowIdx === 'undefined') return; + setDragging(true); + window.addEventListener('mouseover', onMouseover); + window.addEventListener('mouseup', onMouseup); - const { idx, rowIdx } = selectedPosition; - const column = columns[idx]; - const cellKey = column.key; - const value = rows[rowIdx][cellKey as keyof R]; - - onRowsUpdate?.({ - cellKey, - fromRow: rowIdx, - toRow: draggedOverRowIdx, - updated: { [cellKey]: value } as unknown as never, - action: UpdateActions.CELL_DRAG - }); + function onMouseover(event: MouseEvent) { + // Trigger onMouseup in edge cases where we release the mouse button but `mouseup` isn't triggered, + // for example when releasing the mouse button outside the iframe the grid is rendered in. + // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons + if (event.buttons !== 1) onMouseup(); + } - setDraggedOverRowIdx(undefined); + function onMouseup() { + window.removeEventListener('mouseover', onMouseover); + window.removeEventListener('mouseup', onMouseup); + setDragging(false); + } } - function handleDragHandleDoubleClick(event: React.MouseEvent) { + function handleDoubleClick(event: React.MouseEvent) { + if (!enableCellDragAndDrop) return; + const { target } = event; + if (!(target instanceof HTMLDivElement && target.className === 'rdg-cell-drag-handle')) return; + event.stopPropagation(); const column = columns[selectedPosition.idx]; @@ -496,7 +512,6 @@ function DataGrid({ }); } - /** * utils */ @@ -554,7 +569,7 @@ function DataGrid({ } function isDraggedOver(currentRowIdx: number) { - if (typeof draggedOverRowIdx === 'undefined') return; + if (draggedOverRowIdx === undefined) return; const { rowIdx } = selectedPosition; return rowIdx < draggedOverRowIdx @@ -588,26 +603,6 @@ function DataGrid({ selectCell(nextPosition); } - // TODO: Use "position: sticky" and fix drag handle showing on top of frozen columns - function getDragHandle() { - if (!enableCellDragAndDrop || !isCellEditable(selectedPosition) || selectedPosition.mode === 'EDIT') return null; - const { idx, rowIdx } = selectedPosition; - const top = (rowIdx * rowHeight) + totalHeaderHeight + rowHeight; - const column = columns[idx]; - const left = column.left + column.width + (column.frozen ? scrollLeft : 0); - - return ( -
- ); - } - function getEditorContainer() { if (selectedPosition.mode === 'SELECT') return null; @@ -674,7 +669,7 @@ function DataGrid({ onRowClick={onRowClick} rowClass={rowClass} top={rowIdx * rowHeight + totalHeaderHeight} - setDraggedOverRowIdx={setDraggedOverRowIdx} + setDraggedOverRowIdx={isDragging ? setDraggedOverRowIdx : undefined} /> ); } @@ -726,11 +721,15 @@ function DataGrid({ {rows.length === 0 && emptyRowsRenderer ? createElement(emptyRowsRenderer) : ( <>
-
+
{viewportWidth > 0 && getViewportRows()} {getEditorContainer()}
- {getDragHandle()} {summaryRows?.map((row, rowIdx) => ( key={rowIdx} diff --git a/src/Row.tsx b/src/Row.tsx index 0b671c4dcc..01d36bebba 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -3,7 +3,7 @@ import clsx from 'clsx'; import Cell from './Cell'; import { RowRendererProps } from './common/types'; -import { preventDefault, wrapEvent } from './utils'; +import { wrapEvent } from './utils'; function Row({ cellRenderer: CellRenderer = Cell, @@ -20,21 +20,12 @@ function Row({ onRowClick, rowClass, setDraggedOverRowIdx, - onDragEnter, - onDragOver, - onDrop, + onMouseEnter, top, ...props }: RowRendererProps) { - function handleDragEnter(event: React.DragEvent) { - // Prevent default to allow drop - event.preventDefault(); - setDraggedOverRowIdx(rowIdx); - } - - function handleDragOver(event: React.DragEvent) { - event.preventDefault(); - event.dataTransfer.dropEffect = 'copy'; + function handleDragEnter() { + setDraggedOverRowIdx?.(rowIdx); } className = clsx( @@ -45,15 +36,10 @@ function Row({ className ); - // Regarding onDrop: the default in Firefox is to treat data in dataTransfer as a URL, - // and perform navigation on it, even if the data type used is 'text'. - // To bypass this, we need to capture and prevent the drop event. return (
diff --git a/src/common/types.ts b/src/common/types.ts index 4d99956bb7..c4693a9ed2 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -130,7 +130,7 @@ export interface RowRendererProps extends Omit) => void; rowClass?: (row: TRow) => string | undefined; - setDraggedOverRowIdx: (overRowIdx: number) => void; + setDraggedOverRowIdx?: (overRowIdx: number) => void; } export interface FilterRendererProps { diff --git a/style/cell.less b/style/cell.less index 715f18281e..60238c919c 100644 --- a/style/cell.less +++ b/style/cell.less @@ -33,29 +33,20 @@ } .rdg-cell-drag-handle { - pointer-events: auto; + cursor: move; position: absolute; - background: #66afe9; + right: 0; + bottom: 0; width: 8px; height: 8px; - border: 1px solid #fff; - border-right: 0; - border-bottom: 0; - margin-left: -6px; - margin-top: -6px; - cursor: crosshair; - cursor: -moz-grab; - cursor: -webkit-grab; - cursor: grab; -} + background: #66afe9; -.rdg-cell-drag-handle:hover { - background: white; - width: 16px; - height: 16px; - border: 1px solid #66afe9; - margin-left: -10px; - margin-top: -10px; + &:hover { + width: 16px; + height: 16px; + border: 2px solid #66afe9; + background: #fff; + } } .rdg-cell-dragged-over { diff --git a/style/core.less b/style/core.less index 091321b1d9..b89b782378 100644 --- a/style/core.less +++ b/style/core.less @@ -26,3 +26,7 @@ z-index: 0; } } + +.rdg-viewport-dragging { + cursor: move; +} From 5e355ed10137acea6090efb9aa4af668c8fad3c0 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Tue, 30 Jun 2020 14:14:54 -0500 Subject: [PATCH 18/42] Pass down dragHandle component --- src/Cell.tsx | 6 ++---- src/DataGrid.tsx | 24 +++++++++++++----------- src/Row.tsx | 2 ++ src/common/types.ts | 2 ++ 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index 7b970f6e85..156e8d007f 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -16,6 +16,7 @@ function Cell({ row, rowIdx, eventBus, + dragHandle, onRowClick, onClick, onDoubleClick, @@ -86,10 +87,7 @@ function Cell({ isRowSelected={isRowSelected} onRowSelectionChange={onRowSelectionChange} /> - {isSelected && ( - // if (!enableCellDragAndDrop || !isCellEditable(selectedPosition) || selectedPosition.mode === 'EDIT') return null; -
- )} + {dragHandle}
); } diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 3ed16857e7..a2e19fda75 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -469,11 +469,7 @@ function DataGrid({ } } - function handleMouseDown(event: React.MouseEvent) { - if (!enableCellDragAndDrop) return; - const { target } = event; - if (!(target instanceof HTMLDivElement && target.className === 'rdg-cell-drag-handle')) return; - + function handleMouseDown() { setDragging(true); window.addEventListener('mouseover', onMouseover); window.addEventListener('mouseup', onMouseup); @@ -493,10 +489,6 @@ function DataGrid({ } function handleDoubleClick(event: React.MouseEvent) { - if (!enableCellDragAndDrop) return; - const { target } = event; - if (!(target instanceof HTMLDivElement && target.className === 'rdg-cell-drag-handle')) return; - event.stopPropagation(); const column = columns[selectedPosition.idx]; @@ -639,6 +631,17 @@ function DataGrid({ ); } + function getDragHandle() { + if (!enableCellDragAndDrop || !isCellEditable(selectedPosition) || selectedPosition.mode === 'EDIT') return null; + return ( +
+ ); + } + function getViewportRows() { const rowElements = []; @@ -670,6 +673,7 @@ function DataGrid({ rowClass={rowClass} top={rowIdx * rowHeight + totalHeaderHeight} setDraggedOverRowIdx={isDragging ? setDraggedOverRowIdx : undefined} + dragHandle={selectedPosition.rowIdx === rowIdx ? getDragHandle() : undefined} /> ); } @@ -724,8 +728,6 @@ function DataGrid({
{viewportWidth > 0 && getViewportRows()} {getEditorContainer()} diff --git a/src/Row.tsx b/src/Row.tsx index 01d36bebba..8f3bbec8ca 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -17,6 +17,7 @@ function Row({ draggedOverCellIdx, row, viewportColumns, + dragHandle, onRowClick, rowClass, setDraggedOverRowIdx, @@ -55,6 +56,7 @@ function Row({ isDraggedOver={draggedOverCellIdx === column.idx} isRowSelected={isRowSelected} eventBus={eventBus} + dragHandle={selectedCellIdx === column.idx ? dragHandle : undefined} onRowClick={onRowClick} /> ))} diff --git a/src/common/types.ts b/src/common/types.ts index c4693a9ed2..30f40eb608 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -113,6 +113,7 @@ export interface CellRendererProps extends Omit) => void; } @@ -128,6 +129,7 @@ export interface RowRendererProps extends Omit) => void; rowClass?: (row: TRow) => string | undefined; setDraggedOverRowIdx?: (overRowIdx: number) => void; From c4b6ad766872899a51092382d12fbaefcc169a2b Mon Sep 17 00:00:00 2001 From: Mahajan Date: Tue, 30 Jun 2020 14:15:26 -0500 Subject: [PATCH 19/42] Fix styles --- stories/demos/AllFeatures.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stories/demos/AllFeatures.less b/stories/demos/AllFeatures.less index 672d908a65..b5c899e28e 100644 --- a/stories/demos/AllFeatures.less +++ b/stories/demos/AllFeatures.less @@ -1,8 +1,8 @@ -.highlight { +.highlight .rdg-cell { background-color: #9370DB; color: white; } -.rdg-row.highlight:hover { +.highlight:hover .rdg-cell { background-color: #800080; } From 787ab44a7086ac91b56ee99dea60397b65f4d2e9 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Tue, 30 Jun 2020 16:01:07 -0500 Subject: [PATCH 20/42] Remove unused function --- src/common/types.ts | 8 -------- src/utils/selectedCellUtils.ts | 26 +------------------------- 2 files changed, 1 insertion(+), 33 deletions(-) diff --git a/src/common/types.ts b/src/common/types.ts index 30f40eb608..b042a72552 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -56,14 +56,6 @@ export interface Position { rowIdx: number; } -export interface Dimension { - width: number; - height: number; - top: number; - left: number; - zIndex: number; -} - export interface Editor { getInputNode: () => Element | Text | undefined | null; getValue: () => TValue; diff --git a/src/utils/selectedCellUtils.ts b/src/utils/selectedCellUtils.ts index d063629e92..b9aa18046f 100644 --- a/src/utils/selectedCellUtils.ts +++ b/src/utils/selectedCellUtils.ts @@ -1,30 +1,6 @@ import { CellNavigationMode } from '../common/enums'; import { canEdit } from './columnUtils'; -import { CalculatedColumn, Position, Dimension } from '../common/types'; - -// above unfrozen cells, below frozen cells -const zCellMask = 1; -// above frozen cells, below header/filter/summary rows -const zFrozenCellMask = 2; - -interface GetSelectedDimensionsOpts { - selectedPosition: Position; - columns: readonly CalculatedColumn[]; - rowHeight: number; - scrollLeft: number; -} - -export function getSelectedDimensions({ selectedPosition: { idx, rowIdx }, columns, rowHeight, scrollLeft }: GetSelectedDimensionsOpts): Dimension { - if (idx < 0) { - return { width: 0, left: 0, top: 0, height: rowHeight, zIndex: 1 }; - } - const column = columns[idx]; - const { width } = column; - const left = column.frozen ? column.left + scrollLeft : column.left; - const top = rowIdx * rowHeight; - const zIndex = column.frozen ? zFrozenCellMask : zCellMask; - return { width, left, top, height: rowHeight, zIndex }; -} +import { CalculatedColumn, Position } from '../common/types'; interface IsSelectedCellEditableOpts { selectedPosition: Position; From 9404987b828fb7c4048df77d5eb5d5c168cca1b1 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Tue, 30 Jun 2020 21:00:44 -0500 Subject: [PATCH 21/42] Move getNextPosition to selectedCellUtils --- src/DataGrid.tsx | 20 +------------------- src/utils/selectedCellUtils.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index a2e19fda75..61a2f40aa9 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -26,6 +26,7 @@ import { getScrollbarSize, getVerticalRangeToRender, getViewportColumns, + getNextPosition, getNextSelectedCellPosition, isSelectedCellEditable, canExitGrid, @@ -156,25 +157,6 @@ export interface DataGridProps { rowClass?: (row: R) => string | undefined; } - -function getNextPosition(key: string, shiftKey: boolean, currentPosition: Position) { - const { idx, rowIdx } = currentPosition; - switch (key) { - case 'ArrowUp': - return { idx, rowIdx: rowIdx - 1 }; - case 'ArrowDown': - return { idx, rowIdx: rowIdx + 1 }; - case 'ArrowLeft': - return { idx: idx - 1, rowIdx }; - case 'ArrowRight': - return { idx: idx + 1, rowIdx }; - case 'Tab': - return { idx: idx + (shiftKey ? -1 : 1), rowIdx }; - default: - return currentPosition; - } -} - /** * Main API Component to render a data grid of rows and columns * diff --git a/src/utils/selectedCellUtils.ts b/src/utils/selectedCellUtils.ts index b9aa18046f..8b3cd46dad 100644 --- a/src/utils/selectedCellUtils.ts +++ b/src/utils/selectedCellUtils.ts @@ -23,6 +23,24 @@ interface GetNextSelectedCellPositionOpts { nextPosition: Position; } +export function getNextPosition(key: string, shiftKey: boolean, currentPosition: Position) { + const { idx, rowIdx } = currentPosition; + switch (key) { + case 'ArrowUp': + return { idx, rowIdx: rowIdx - 1 }; + case 'ArrowDown': + return { idx, rowIdx: rowIdx + 1 }; + case 'ArrowLeft': + return { idx: idx - 1, rowIdx }; + case 'ArrowRight': + return { idx: idx + 1, rowIdx }; + case 'Tab': + return { idx: idx + (shiftKey ? -1 : 1), rowIdx }; + default: + return currentPosition; + } +} + export function getNextSelectedCellPosition({ cellNavigationMode, columns, rowsCount, nextPosition }: GetNextSelectedCellPositionOpts): Position { if (cellNavigationMode !== CellNavigationMode.NONE) { const { idx, rowIdx } = nextPosition; From 1b8c3a570a4bb9a8685ad851f3f946ba8d198533 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Tue, 30 Jun 2020 21:24:12 -0500 Subject: [PATCH 22/42] Use ref to get the latest draggedOverRowIdx --- src/DataGrid.tsx | 64 ++++++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 61a2f40aa9..b7a3439ee4 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -205,12 +205,6 @@ function DataGrid({ editorPortalTarget = document.body, rowClass }: DataGridProps, ref: React.Ref) { - /** - * refs - * */ - const gridRef = useRef(null); - const lastSelectedRowIdx = useRef(-1); - /** * states */ @@ -224,6 +218,13 @@ function DataGrid({ const [isDragging, setDragging] = useState(false); const [draggedOverRowIdx, setDraggedOverRowIdx] = useState(undefined); + /** + * refs + * */ + const gridRef = useRef(null); + const latestDraggedOverRowIdx = useRef(draggedOverRowIdx); + const lastSelectedRowIdx = useRef(-1); + /** * computed values */ @@ -322,22 +323,7 @@ function DataGrid({ }); useEffect(() => { - if (!enableCellDragAndDrop || isDragging || draggedOverRowIdx === undefined) return; - - const { idx, rowIdx } = selectedPosition; - const column = columns[idx]; - const cellKey = column.key; - const value = rows[rowIdx][cellKey as keyof R]; - - onRowsUpdate?.({ - cellKey, - fromRow: rowIdx, - toRow: draggedOverRowIdx, - updated: { [cellKey]: value } as unknown as never, - action: UpdateActions.CELL_DRAG - }); - - setDraggedOverRowIdx(undefined); + latestDraggedOverRowIdx.current = draggedOverRowIdx; }); useImperativeHandle(ref, () => ({ @@ -451,22 +437,42 @@ function DataGrid({ } } + function handleDragEnd() { + if (latestDraggedOverRowIdx.current === undefined) return; + + const { idx, rowIdx } = selectedPosition; + const column = columns[idx]; + const cellKey = column.key; + const value = rows[rowIdx][cellKey as keyof R]; + + onRowsUpdate?.({ + cellKey, + fromRow: rowIdx, + toRow: latestDraggedOverRowIdx.current, + updated: { [cellKey]: value } as unknown as never, + action: UpdateActions.CELL_DRAG + }); + + setDraggedOverRowIdx(undefined); + } + function handleMouseDown() { setDragging(true); - window.addEventListener('mouseover', onMouseover); - window.addEventListener('mouseup', onMouseup); + window.addEventListener('mouseover', onMouseOver); + window.addEventListener('mouseup', onMouseUp); - function onMouseover(event: MouseEvent) { + function onMouseOver(event: MouseEvent) { // Trigger onMouseup in edge cases where we release the mouse button but `mouseup` isn't triggered, // for example when releasing the mouse button outside the iframe the grid is rendered in. // https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons - if (event.buttons !== 1) onMouseup(); + if (event.buttons !== 1) onMouseUp(); } - function onMouseup() { - window.removeEventListener('mouseover', onMouseover); - window.removeEventListener('mouseup', onMouseup); + function onMouseUp() { + window.removeEventListener('mouseover', onMouseOver); + window.removeEventListener('mouseup', onMouseUp); setDragging(false); + handleDragEnd(); } } From a9a26135115df820b43b7df74f0ef8119ccfaa81 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Tue, 30 Jun 2020 21:28:17 -0500 Subject: [PATCH 23/42] Revert EventBus changes --- src/Cell.tsx | 4 ++-- src/DataGrid.tsx | 4 ++-- src/EventBus.test.ts | 14 +++++++------- src/EventBus.ts | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index 156e8d007f..2d928c8116 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -32,7 +32,7 @@ function Cell({ }, [isSelected]); function selectCell(openEditor?: boolean) { - eventBus.dispatch('CELL_SELECT', { idx: column.idx, rowIdx }, openEditor); + eventBus.dispatch('SELECT_CELL', { idx: column.idx, rowIdx }, openEditor); } function handleClick() { @@ -49,7 +49,7 @@ function Cell({ } function onRowSelectionChange(checked: boolean, isShiftClick: boolean) { - eventBus.dispatch('ROW_SELECT', { rowIdx, checked, isShiftClick }); + eventBus.dispatch('SELECT_ROW', { rowIdx, checked, isShiftClick }); } const { cellClass } = column; diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index b7a3439ee4..fc6a1bea9a 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -315,11 +315,11 @@ function DataGrid({ onSelectedRowsChange(newSelectedRows); }; - return eventBus.subscribe('ROW_SELECT', handleRowSelectionChange); + return eventBus.subscribe('SELECT_ROW', handleRowSelectionChange); }, [eventBus, onSelectedRowsChange, rows, rowKey, selectedRows]); useEffect(() => { - return eventBus.subscribe('CELL_SELECT', selectCell); + return eventBus.subscribe('SELECT_CELL', selectCell); }); useEffect(() => { diff --git a/src/EventBus.test.ts b/src/EventBus.test.ts index 5f3b64c4a1..e1c38a874b 100644 --- a/src/EventBus.test.ts +++ b/src/EventBus.test.ts @@ -7,11 +7,11 @@ describe('EventBus', () => { const eventAHandler2 = jest.fn(); const eventBHandler = jest.fn(); - eventBus.subscribe('CELL_SELECT', eventAHandler1); - eventBus.subscribe('CELL_SELECT', eventAHandler2); - eventBus.subscribe('ROW_SELECT', eventBHandler); + eventBus.subscribe('SELECT_CELL', eventAHandler1); + eventBus.subscribe('SELECT_CELL', eventAHandler2); + eventBus.subscribe('SELECT_ROW', eventBHandler); - eventBus.dispatch('CELL_SELECT', { idx: 1, rowIdx: 2 }, true); + eventBus.dispatch('SELECT_CELL', { idx: 1, rowIdx: 2 }, true); expect(eventAHandler1).toHaveBeenCalledWith({ idx: 1, rowIdx: 2 }, true); expect(eventAHandler2).toHaveBeenCalledWith({ idx: 1, rowIdx: 2 }, true); @@ -23,11 +23,11 @@ describe('EventBus', () => { const eventAHandler1 = jest.fn(); const eventAHandler2 = jest.fn(); - eventBus.subscribe('CELL_SELECT', eventAHandler1); - const unsubscribeEventAHandler2 = eventBus.subscribe('CELL_SELECT', eventAHandler2); + eventBus.subscribe('SELECT_CELL', eventAHandler1); + const unsubscribeEventAHandler2 = eventBus.subscribe('SELECT_CELL', eventAHandler2); unsubscribeEventAHandler2(); - eventBus.dispatch('CELL_SELECT', { idx: 1, rowIdx: 2 }, true); + eventBus.dispatch('SELECT_CELL', { idx: 1, rowIdx: 2 }, true); expect(eventAHandler1).toHaveBeenCalledWith({ idx: 1, rowIdx: 2 }, true); expect(eventAHandler2).not.toHaveBeenCalled(); diff --git a/src/EventBus.ts b/src/EventBus.ts index 51d46a94ff..7e484edc93 100644 --- a/src/EventBus.ts +++ b/src/EventBus.ts @@ -1,8 +1,8 @@ import { Position, SelectRowEvent } from './common/types'; interface EventMap { - CELL_SELECT: (position: Position, openEditor?: boolean) => void; - ROW_SELECT: (event: SelectRowEvent) => void; + SELECT_CELL: (position: Position, openEditor?: boolean) => void; + SELECT_ROW: (event: SelectRowEvent) => void; } type EventName = keyof EventMap; From 336c793ff27878cdede949fc7b2e54564079dec2 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Tue, 30 Jun 2020 21:39:01 -0500 Subject: [PATCH 24/42] Fix type errors --- src/Cell.test.tsx | 12 ++++++++++-- src/Row.test.tsx | 3 ++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Cell.test.tsx b/src/Cell.test.tsx index 3ebf587eba..bc8caf4081 100644 --- a/src/Cell.test.tsx +++ b/src/Cell.test.tsx @@ -22,7 +22,11 @@ const testProps: CellRendererProps = { lastFrozenColumnIndex: -1, row: { id: 1, description: 'Wicklow' }, isRowSelected: false, - eventBus: new EventBus() + eventBus: new EventBus(), + isSelected: false, + isCopied: false, + isDraggedOver: false, + dragHandle: null }; const renderComponent = (extraProps?: PropsWithChildren>>) => { @@ -60,7 +64,11 @@ describe('Cell', () => { lastFrozenColumnIndex: -1, row: helpers.rows[11], isRowSelected: false, - eventBus: new EventBus() + eventBus: new EventBus(), + isSelected: false, + isCopied: false, + isDraggedOver: false, + dragHandle: null }; it('passes classname property', () => { diff --git a/src/Row.test.tsx b/src/Row.test.tsx index 896699e587..6facfd63f9 100644 --- a/src/Row.test.tsx +++ b/src/Row.test.tsx @@ -25,7 +25,8 @@ describe('Row', () => { lastFrozenColumnIndex: -1, isRowSelected: false, eventBus: new EventBus(), - top: 0 + top: 0, + dragHandle: null }; it('passes classname property', () => { From ac2c6305008509fd1bd04905d3bf3420c301e20f Mon Sep 17 00:00:00 2001 From: Mahajan Date: Wed, 1 Jul 2020 22:05:46 -0500 Subject: [PATCH 25/42] Specify return type --- src/DataGrid.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index fc6a1bea9a..ece7e5e240 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -499,7 +499,7 @@ function DataGrid({ return rowIdx >= 0 && rowIdx < rows.length && idx >= 0 && idx < columns.length; } - function isCellEditable(position: Position) { + function isCellEditable(position: Position): boolean { return isCellWithinBounds(position) && isSelectedCellEditable({ columns, rows, selectedPosition: position, onCheckCellIsEditable }); } @@ -516,13 +516,13 @@ function DataGrid({ onSelectedCellChange?.({ ...position }); } - function getFrozenColumnsWidth() { + function getFrozenColumnsWidth(): number { if (lastFrozenColumnIndex === -1) return 0; const lastFrozenCol = columns[lastFrozenColumnIndex]; return lastFrozenCol.left + lastFrozenCol.width; } - function scrollToCell({ idx, rowIdx }: Partial) { + function scrollToCell({ idx, rowIdx }: Partial): void { const { current } = gridRef; if (!current) return; @@ -548,8 +548,8 @@ function DataGrid({ } } - function isDraggedOver(currentRowIdx: number) { - if (draggedOverRowIdx === undefined) return; + function isDraggedOver(currentRowIdx: number): boolean { + if (draggedOverRowIdx === undefined) return false; const { rowIdx } = selectedPosition; return rowIdx < draggedOverRowIdx From 57ec91df66a562a648ab703765552e10ef3f334d Mon Sep 17 00:00:00 2001 From: Mahajan Date: Thu, 2 Jul 2020 11:44:12 -0500 Subject: [PATCH 26/42] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b43b476d8..679cc4ce9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - Check the [Context Menu]() example - ⚠️ `enableCellSelect` + - ⚠️ `enableCellAutoFocus` - ⚠️ `getValidFilterValues` - ⚠️ `onCellCopyPaste` - ⚠️ `onSelectedCellRangeChange` From 0504d294fe0875fd02297a5ac5e506859228ef37 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Mon, 6 Jul 2020 10:57:00 -0500 Subject: [PATCH 27/42] Add selectedCellProps props Select cell only on update --- src/Cell.test.tsx | 8 +-- src/Cell.tsx | 92 ++++++++++++++++++--------- src/DataGrid.tsx | 107 +++++++++++++------------------- src/Row.test.tsx | 3 +- src/Row.tsx | 6 +- src/common/types.ts | 28 +++++++-- src/editors/EditorContainer.tsx | 10 +-- style/core.less | 2 +- 8 files changed, 139 insertions(+), 117 deletions(-) diff --git a/src/Cell.test.tsx b/src/Cell.test.tsx index bc8caf4081..8bd8abc895 100644 --- a/src/Cell.test.tsx +++ b/src/Cell.test.tsx @@ -23,10 +23,8 @@ const testProps: CellRendererProps = { row: { id: 1, description: 'Wicklow' }, isRowSelected: false, eventBus: new EventBus(), - isSelected: false, isCopied: false, - isDraggedOver: false, - dragHandle: null + isDraggedOver: false }; const renderComponent = (extraProps?: PropsWithChildren>>) => { @@ -65,10 +63,8 @@ describe('Cell', () => { row: helpers.rows[11], isRowSelected: false, eventBus: new EventBus(), - isSelected: false, isCopied: false, - isDraggedOver: false, - dragHandle: null + isDraggedOver: false }; it('passes classname property', () => { diff --git a/src/Cell.tsx b/src/Cell.tsx index 2d928c8116..f33b3103dd 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -1,6 +1,7 @@ import React, { forwardRef, memo, useEffect, useRef } from 'react'; import clsx from 'clsx'; +import { EditorContainer, EditorPortal } from './editors'; import { CellRendererProps } from './common/types'; import { wrapEvent } from './utils'; import { useCombinedRefs } from './hooks'; @@ -8,7 +9,6 @@ import { useCombinedRefs } from './hooks'; function Cell({ className, column, - isSelected, isCopied, isDraggedOver, isRowSelected, @@ -16,20 +16,41 @@ function Cell({ row, rowIdx, eventBus, - dragHandle, + selectedCellProps, onRowClick, + onKeyDown, onClick, onDoubleClick, onContextMenu, ...props }: CellRendererProps, ref: React.Ref) { const cellRef = useRef(null); + const isMounted = useRef(false); + const isSelected = selectedCellProps !== undefined; + const isEditing = selectedCellProps?.mode === 'EDIT'; + const setFocus = isSelected && !isEditing; + + const { cellClass } = column; + className = clsx( + 'rdg-cell', + { + 'rdg-cell-frozen': column.frozen, + 'rdg-cell-frozen-last': column.idx === lastFrozenColumnIndex, + 'rdg-cell-selected': isSelected, + 'rdg-cell-copied': isCopied, + 'rdg-cell-dragged-over': isDraggedOver + }, + typeof cellClass === 'function' ? cellClass(row) : cellClass, + className + ); useEffect(() => { - if (isSelected) { + if (!isMounted.current) { + isMounted.current = true; + } else if (setFocus) { cellRef.current?.focus(); } - }, [isSelected]); + }, [setFocus]); function selectCell(openEditor?: boolean) { eventBus.dispatch('SELECT_CELL', { idx: column.idx, rowIdx }, openEditor); @@ -52,19 +73,40 @@ function Cell({ eventBus.dispatch('SELECT_ROW', { rowIdx, checked, isShiftClick }); } - const { cellClass } = column; - className = clsx( - 'rdg-cell', - { - 'rdg-cell-frozen': column.frozen, - 'rdg-cell-frozen-last': column.idx === lastFrozenColumnIndex, - 'rdg-cell-selected': isSelected, - 'rdg-cell-copied': isCopied, - 'rdg-cell-dragged-over': isDraggedOver - }, - typeof cellClass === 'function' ? cellClass(row) : cellClass, - className - ); + function getCellContent() { + if (selectedCellProps && selectedCellProps.mode === 'EDIT') { + const { editorPortalTarget, ...editorProps } = selectedCellProps.editorContainerProps; + const { left, top } = cellRef.current!.getBoundingClientRect(); + + return ( + + + {...editorProps} + rowIdx={rowIdx} + row={row} + column={column} + left={left} + top={top} + /> + + ); + } + + return ( + <> + + {selectedCellProps?.dragHandleProps && ( +
+ )} + + ); + } return (
({ width: column.width, left: column.left }} - onClick={wrapEvent(handleClick, onClick)} - onDoubleClick={wrapEvent(handleDoubleClick, onDoubleClick)} - onContextMenu={wrapEvent(handleContextMenu, onContextMenu)} + onKeyDown={selectedCellProps?.onKeyDown ? wrapEvent(selectedCellProps.onKeyDown, onKeyDown) : onKeyDown} + onClick={isEditing ? onClick : wrapEvent(handleClick, onClick)} + onDoubleClick={isEditing ? onDoubleClick : wrapEvent(handleDoubleClick, onDoubleClick)} + onContextMenu={isEditing ? onContextMenu : wrapEvent(handleContextMenu, onContextMenu)} {...props} > - - {dragHandle} + {getCellContent()}
); } diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index ece7e5e240..24f9831a97 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -17,7 +17,7 @@ import FilterRow from './FilterRow'; import Row from './Row'; import SummaryRow from './SummaryRow'; import { ValueFormatter } from './formatters'; -import { legacyCellInput, EditorContainer, EditorPortal } from './editors'; +import { legacyCellInput } from './editors'; import { assertIsValidKey, getColumnMetrics, @@ -43,7 +43,8 @@ import { RowRendererProps, RowsUpdateEvent, SelectRowEvent, - CommitEvent + CommitEvent, + SelectedCellProps } from './common/types'; import { CellNavigationMode, SortDirection, UpdateActions } from './common/enums'; @@ -548,15 +549,6 @@ function DataGrid({ } } - function isDraggedOver(currentRowIdx: number): boolean { - if (draggedOverRowIdx === undefined) return false; - const { rowIdx } = selectedPosition; - - return rowIdx < draggedOverRowIdx - ? rowIdx < currentRowIdx && currentRowIdx <= draggedOverRowIdx - : rowIdx > currentRowIdx && currentRowIdx >= draggedOverRowIdx; - } - function navigate(key: string, shiftKey: boolean) { let nextPosition = getNextPosition(key, shiftKey, selectedPosition); let mode = cellNavigationMode; @@ -583,51 +575,45 @@ function DataGrid({ selectCell(nextPosition); } - function getEditorContainer() { - if (selectedPosition.mode === 'SELECT') return null; + function getDraggedOverCellIdx(currentRowIdx: number): number | undefined { + if (draggedOverRowIdx === undefined) return; + const { rowIdx } = selectedPosition; - const column = columns[selectedPosition.idx]; - const row = rows[selectedPosition.rowIdx]; - let editorLeft = 0; - let editorTop = 0; - - if (gridRef.current !== null) { - const { left, top } = gridRef.current.getBoundingClientRect(); - const { scrollTop: docTop, scrollLeft: docLeft } = document.scrollingElement || document.documentElement; - const gridLeft = left + docLeft; - const gridTop = top + docTop; - editorLeft = gridLeft + column.left - (column.frozen ? 0 : scrollLeft); - editorTop = gridTop + totalHeaderHeight + selectedPosition.rowIdx * rowHeight - scrollTop; - } + const isDraggedOver = rowIdx < draggedOverRowIdx + ? rowIdx < currentRowIdx && currentRowIdx <= draggedOverRowIdx + : rowIdx > currentRowIdx && currentRowIdx >= draggedOverRowIdx; - return ( - - - firstEditorKeyPress={selectedPosition.key} - onCommit={handleCommit} - onCommitCancel={() => setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, mode: 'SELECT' }))} - rowIdx={selectedPosition.rowIdx} - row={row} - rowHeight={rowHeight} - column={column} - scrollLeft={scrollLeft} - scrollTop={scrollTop} - left={editorLeft} - top={editorTop} - /> - - ); + return isDraggedOver ? selectedPosition.idx : undefined; } - function getDragHandle() { - if (!enableCellDragAndDrop || !isCellEditable(selectedPosition) || selectedPosition.mode === 'EDIT') return null; - return ( -
- ); + function getSelectedCellProps(rowIdx: number): SelectedCellProps | undefined { + if (selectedPosition.rowIdx !== rowIdx) return; + + if (selectedPosition.mode === 'EDIT') { + return { + mode: 'EDIT', + idx: selectedPosition.idx, + onKeyDown: handleKeyDown, + editorContainerProps: { + editorPortalTarget, + rowHeight, + scrollLeft, + scrollTop, + firstEditorKeyPress: selectedPosition.key, + onCommit: handleCommit, + onCommitCancel: () => setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, mode: 'SELECT' })) + } + }; + } + + return { + mode: 'SELECT', + idx: selectedPosition.idx, + onKeyDown: handleKeyDown, + dragHandleProps: enableCellDragAndDrop && isCellEditable(selectedPosition) + ? { onMouseDown: handleMouseDown, onDoubleClick: handleDoubleClick } + : undefined + }; } function getViewportRows() { @@ -652,16 +638,15 @@ function DataGrid({ row={row} viewportColumns={viewportColumns} lastFrozenColumnIndex={lastFrozenColumnIndex} - selectedCellIdx={selectedPosition.rowIdx === rowIdx && selectedPosition.mode === 'SELECT' ? selectedPosition.idx : undefined} - copiedCellIdx={copiedPosition?.rowIdx === rowIdx ? copiedPosition.idx : undefined} - draggedOverCellIdx={isDraggedOver(rowIdx) ? selectedPosition.idx : undefined} eventBus={eventBus} isRowSelected={isRowSelected} onRowClick={onRowClick} rowClass={rowClass} top={rowIdx * rowHeight + totalHeaderHeight} + copiedCellIdx={copiedPosition?.rowIdx === rowIdx ? copiedPosition.idx : undefined} + draggedOverCellIdx={getDraggedOverCellIdx(rowIdx)} setDraggedOverRowIdx={isDragging ? setDraggedOverRowIdx : undefined} - dragHandle={selectedPosition.rowIdx === rowIdx ? getDragHandle() : undefined} + selectedCellProps={getSelectedCellProps(rowIdx)} /> ); } @@ -678,7 +663,7 @@ function DataGrid({ return (
({ {rows.length === 0 && emptyRowsRenderer ? createElement(emptyRowsRenderer) : ( <>
-
- {viewportWidth > 0 && getViewportRows()} - {getEditorContainer()} -
+ {viewportWidth > 0 && getViewportRows()} {summaryRows?.map((row, rowIdx) => ( key={rowIdx} diff --git a/src/Row.test.tsx b/src/Row.test.tsx index 6facfd63f9..896699e587 100644 --- a/src/Row.test.tsx +++ b/src/Row.test.tsx @@ -25,8 +25,7 @@ describe('Row', () => { lastFrozenColumnIndex: -1, isRowSelected: false, eventBus: new EventBus(), - top: 0, - dragHandle: null + top: 0 }; it('passes classname property', () => { diff --git a/src/Row.tsx b/src/Row.tsx index 8f3bbec8ca..2ff87be0ba 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -12,12 +12,11 @@ function Row({ rowIdx, isRowSelected, lastFrozenColumnIndex, - selectedCellIdx, copiedCellIdx, draggedOverCellIdx, row, viewportColumns, - dragHandle, + selectedCellProps, onRowClick, rowClass, setDraggedOverRowIdx, @@ -51,12 +50,11 @@ function Row({ column={column} lastFrozenColumnIndex={lastFrozenColumnIndex} row={row} - isSelected={selectedCellIdx === column.idx} isCopied={copiedCellIdx === column.idx} isDraggedOver={draggedOverCellIdx === column.idx} isRowSelected={isRowSelected} eventBus={eventBus} - dragHandle={selectedCellIdx === column.idx ? dragHandle : undefined} + selectedCellProps={selectedCellProps?.idx === column.idx ? selectedCellProps : undefined} onRowClick={onRowClick} /> ))} diff --git a/src/common/types.ts b/src/common/types.ts index b042a72552..c2fd87fa1f 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -95,17 +95,38 @@ export interface HeaderRendererProps { onAllRowsSelectionChange: (checked: boolean) => void; } +export interface SharedEditorContainerProps { + editorPortalTarget: Element; + firstEditorKeyPress: string | null; + scrollLeft: number; + scrollTop: number; + rowHeight: number; + onCommit: (e: CommitEvent) => void; + onCommitCancel: () => void; +} + +export type SelectedCellProps = { + mode: 'EDIT'; + idx: number; + onKeyDown: (event: React.KeyboardEvent) => void; + editorContainerProps: SharedEditorContainerProps; +} | { + mode: 'SELECT'; + idx: number; + onKeyDown: (event: React.KeyboardEvent) => void; + dragHandleProps?: Pick, 'onMouseDown' | 'onDoubleClick'>; +}; + export interface CellRendererProps extends Omit, 'style' | 'children'> { rowIdx: number; column: CalculatedColumn; lastFrozenColumnIndex: number; row: TRow; isRowSelected: boolean; - isSelected: boolean; isCopied: boolean; isDraggedOver: boolean; eventBus: EventBus; - dragHandle: React.ReactNode; + selectedCellProps?: SelectedCellProps; onRowClick?: (rowIdx: number, row: TRow, column: CalculatedColumn) => void; } @@ -115,13 +136,12 @@ export interface RowRendererProps extends Omit>; rowIdx: number; lastFrozenColumnIndex: number; - selectedCellIdx?: number; copiedCellIdx?: number; draggedOverCellIdx?: number; isRowSelected: boolean; eventBus: EventBus; top: number; - dragHandle: React.ReactNode; + selectedCellProps?: SelectedCellProps; onRowClick?: (rowIdx: number, row: TRow, column: CalculatedColumn) => void; rowClass?: (row: TRow) => string | undefined; setDraggedOverRowIdx?: (overRowIdx: number) => void; diff --git a/src/editors/EditorContainer.tsx b/src/editors/EditorContainer.tsx index 36b1ffbc08..5bae587fa3 100644 --- a/src/editors/EditorContainer.tsx +++ b/src/editors/EditorContainer.tsx @@ -1,23 +1,17 @@ import React, { KeyboardEvent, useRef, useState, useLayoutEffect, useCallback, useEffect } from 'react'; import clsx from 'clsx'; -import { CalculatedColumn, Editor, CommitEvent } from '../common/types'; +import { CalculatedColumn, Editor, Omit, SharedEditorContainerProps } from '../common/types'; import SimpleTextEditor from './SimpleTextEditor'; import ClickOutside from './ClickOutside'; import { preventDefault } from '../utils'; -export interface EditorContainerProps { +export interface EditorContainerProps extends Omit { rowIdx: number; row: R; column: CalculatedColumn; - onCommit: (e: CommitEvent) => void; - onCommitCancel: () => void; - firstEditorKeyPress: string | null; top: number; left: number; - scrollLeft: number; - scrollTop: number; - rowHeight: number; } export default function EditorContainer({ diff --git a/style/core.less b/style/core.less index b89b782378..4cf631d332 100644 --- a/style/core.less +++ b/style/core.less @@ -27,6 +27,6 @@ } } -.rdg-viewport-dragging { +.rdg-viewport-dragging .rdg-row { cursor: move; } From 3cc29bd99afbf74652eac89772ac0d865723e862 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Mon, 6 Jul 2020 11:03:43 -0500 Subject: [PATCH 28/42] Remove isMouted check --- src/Cell.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index f33b3103dd..3255dff372 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -25,7 +25,6 @@ function Cell({ ...props }: CellRendererProps, ref: React.Ref) { const cellRef = useRef(null); - const isMounted = useRef(false); const isSelected = selectedCellProps !== undefined; const isEditing = selectedCellProps?.mode === 'EDIT'; const setFocus = isSelected && !isEditing; @@ -45,9 +44,7 @@ function Cell({ ); useEffect(() => { - if (!isMounted.current) { - isMounted.current = true; - } else if (setFocus) { + if (setFocus) { cellRef.current?.focus(); } }, [setFocus]); From de4248a1a242dda936ef3a0edb021119c3c87207 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Mon, 6 Jul 2020 11:28:24 -0500 Subject: [PATCH 29/42] Add the row containing the selected cell if not included in the vertical range --- src/DataGrid.tsx | 67 +++++++++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 24f9831a97..1354e6dab9 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -616,39 +616,48 @@ function DataGrid({ }; } + function getRow(rowIdx: number) { + const row = rows[rowIdx]; + let key: string | number = rowIdx; + let isRowSelected = false; + if (rowKey !== undefined) { + const rowId = row[rowKey]; + isRowSelected = selectedRows?.has(rowId) ?? false; + if (typeof rowId === 'string' || typeof rowId === 'number') { + key = rowId; + } + } + + return ( + + ); + } + function getViewportRows() { const rowElements = []; - for (let rowIdx = rowOverscanStartIdx; rowIdx <= rowOverscanEndIdx; rowIdx++) { - const row = rows[rowIdx]; - let key: string | number = rowIdx; - let isRowSelected = false; - if (rowKey !== undefined) { - const rowId = row[rowKey]; - isRowSelected = selectedRows?.has(rowId) ?? false; - if (typeof rowId === 'string' || typeof rowId === 'number') { - key = rowId; - } - } + // Add the row containing the selected cell if not included in the vertical range + if (isCellWithinBounds(selectedPosition) && (selectedPosition.rowIdx < rowOverscanStartIdx || selectedPosition.rowIdx > rowOverscanEndIdx)) { + rowElements.push(getRow(selectedPosition.rowIdx)); + } - rowElements.push( - - ); + for (let rowIdx = rowOverscanStartIdx; rowIdx <= rowOverscanEndIdx; rowIdx++) { + rowElements.push(getRow(rowIdx)); } return rowElements; From 3acd68134bd1767088568abaab04efe3ad927c6c Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Mon, 6 Jul 2020 12:45:55 -0500 Subject: [PATCH 30/42] Update src/DataGrid.tsx Co-authored-by: Nicolas Stepien <567105+nstepien@users.noreply.github.com> --- src/DataGrid.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 1354e6dab9..8733ee83d0 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -221,7 +221,7 @@ function DataGrid({ /** * refs - * */ + */ const gridRef = useRef(null); const latestDraggedOverRowIdx = useRef(draggedOverRowIdx); const lastSelectedRowIdx = useRef(-1); From 689c727cdec7f63fc5089f0cf90f92d3db031f07 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Tue, 7 Jul 2020 09:58:48 -0500 Subject: [PATCH 31/42] Address comments --- src/DataGrid.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 8733ee83d0..b7e052391e 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -217,7 +217,12 @@ function DataGrid({ const [selectedPosition, setSelectedPosition] = useState({ idx: -1, rowIdx: -1, mode: 'SELECT' }); const [copiedPosition, setCopiedPosition] = useState(null); const [isDragging, setDragging] = useState(false); - const [draggedOverRowIdx, setDraggedOverRowIdx] = useState(undefined); + const [draggedOverRowIdx, setOverRowIdx] = useState(undefined); + + const setDraggedOverRowIdx = useCallback((rowIdx?: number) => { + setOverRowIdx(rowIdx); + latestDraggedOverRowIdx.current = rowIdx; + }, []); /** * refs @@ -323,10 +328,6 @@ function DataGrid({ return eventBus.subscribe('SELECT_CELL', selectCell); }); - useEffect(() => { - latestDraggedOverRowIdx.current = draggedOverRowIdx; - }); - useImperativeHandle(ref, () => ({ scrollToColumn(idx: number) { scrollToCell({ idx }); From a4beb50fa3e093651eeda8ce451842d8e4e8cc8b Mon Sep 17 00:00:00 2001 From: Mahajan Date: Wed, 8 Jul 2020 10:34:55 -0500 Subject: [PATCH 32/42] Set focus in useLayoutEffect and set tabIndex to -1 --- src/Cell.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index 3255dff372..baaf8c5a5d 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef, memo, useEffect, useRef } from 'react'; +import React, { forwardRef, memo, useLayoutEffect, useRef } from 'react'; import clsx from 'clsx'; import { EditorContainer, EditorPortal } from './editors'; @@ -43,7 +43,7 @@ function Cell({ className ); - useEffect(() => { + useLayoutEffect(() => { if (setFocus) { cellRef.current?.focus(); } @@ -109,7 +109,7 @@ function Cell({
Date: Wed, 8 Jul 2020 10:38:54 -0500 Subject: [PATCH 33/42] setFocus -> shouldFocus --- src/Cell.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index baaf8c5a5d..f2245e6995 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -27,7 +27,7 @@ function Cell({ const cellRef = useRef(null); const isSelected = selectedCellProps !== undefined; const isEditing = selectedCellProps?.mode === 'EDIT'; - const setFocus = isSelected && !isEditing; + const shouldFocus = isSelected && !isEditing; const { cellClass } = column; className = clsx( @@ -44,10 +44,10 @@ function Cell({ ); useLayoutEffect(() => { - if (setFocus) { + if (shouldFocus) { cellRef.current?.focus(); } - }, [setFocus]); + }, [shouldFocus]); function selectCell(openEditor?: boolean) { eventBus.dispatch('SELECT_CELL', { idx: column.idx, rowIdx }, openEditor); From ba7d7e368d9e873a2f3f1e878bda4d3575ef988f Mon Sep 17 00:00:00 2001 From: Mahajan Date: Wed, 8 Jul 2020 13:10:01 -0500 Subject: [PATCH 34/42] Address comments --- src/DataGrid.tsx | 3 ++- src/common/types.ts | 17 +++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index b7e052391e..384c4045cb 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -458,7 +458,8 @@ function DataGrid({ setDraggedOverRowIdx(undefined); } - function handleMouseDown() { + function handleMouseDown(event: React.MouseEvent) { + if (event.button !== 0) return; setDragging(true); window.addEventListener('mouseover', onMouseOver); window.addEventListener('mouseup', onMouseUp); diff --git a/src/common/types.ts b/src/common/types.ts index c2fd87fa1f..44be661330 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -105,17 +105,22 @@ export interface SharedEditorContainerProps { onCommitCancel: () => void; } -export type SelectedCellProps = { - mode: 'EDIT'; +interface SelectedCellPropsBase { idx: number; onKeyDown: (event: React.KeyboardEvent) => void; +} + +interface SelectedCellPropsEdit extends SelectedCellPropsBase { + mode: 'EDIT'; editorContainerProps: SharedEditorContainerProps; -} | { +} + +interface SelectedCellPropsSelect extends SelectedCellPropsBase { mode: 'SELECT'; - idx: number; - onKeyDown: (event: React.KeyboardEvent) => void; dragHandleProps?: Pick, 'onMouseDown' | 'onDoubleClick'>; -}; +} + +export type SelectedCellProps = SelectedCellPropsEdit | SelectedCellPropsSelect; export interface CellRendererProps extends Omit, 'style' | 'children'> { rowIdx: number; From 5a5b9fe7721768aa6b8ca1839c8b5addf2511dfc Mon Sep 17 00:00:00 2001 From: Mahajan Date: Wed, 8 Jul 2020 13:13:48 -0500 Subject: [PATCH 35/42] Cleanup --- src/DataGrid.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 384c4045cb..4a99db2169 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -347,8 +347,14 @@ function DataGrid({ if (enableCellCopyPaste && isCtrlKeyHeldDown(event)) { // event.key may be uppercase `C` or `V` const lowerCaseKey = event.key.toLowerCase(); - if (lowerCaseKey === 'c') return handleCopy(); - if (lowerCaseKey === 'v') return handlePaste(); + if (lowerCaseKey === 'c') { + handleCopy(); + return; + } + if (lowerCaseKey === 'v') { + handlePaste(); + return; + } } switch (event.key) { From a4ecddfb861ddb91d06dcb5fc504d1dd669d2d52 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Wed, 8 Jul 2020 13:16:49 -0500 Subject: [PATCH 36/42] use event.buttons --- src/DataGrid.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 4a99db2169..a6908ed2f7 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -465,7 +465,7 @@ function DataGrid({ } function handleMouseDown(event: React.MouseEvent) { - if (event.button !== 0) return; + if (event.buttons !== 1) return; setDragging(true); window.addEventListener('mouseover', onMouseOver); window.addEventListener('mouseup', onMouseUp); From eda56d046a9e09a4ea436a5dd98a9de9696c111e Mon Sep 17 00:00:00 2001 From: Mahajan Date: Mon, 13 Jul 2020 12:48:30 -0500 Subject: [PATCH 37/42] Better focus handling --- src/Cell.tsx | 10 +-- src/DataGrid.tsx | 156 ++++++++++++++++++++++----------- src/common/types.ts | 2 +- src/utils/selectedCellUtils.ts | 18 ---- style/cell.less | 1 - 5 files changed, 106 insertions(+), 81 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index f2245e6995..4d8d52fe5a 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef, memo, useLayoutEffect, useRef } from 'react'; +import React, { forwardRef, memo, useRef } from 'react'; import clsx from 'clsx'; import { EditorContainer, EditorPortal } from './editors'; @@ -27,7 +27,6 @@ function Cell({ const cellRef = useRef(null); const isSelected = selectedCellProps !== undefined; const isEditing = selectedCellProps?.mode === 'EDIT'; - const shouldFocus = isSelected && !isEditing; const { cellClass } = column; className = clsx( @@ -43,12 +42,6 @@ function Cell({ className ); - useLayoutEffect(() => { - if (shouldFocus) { - cellRef.current?.focus(); - } - }, [shouldFocus]); - function selectCell(openEditor?: boolean) { eventBus.dispatch('SELECT_CELL', { idx: column.idx, rowIdx }, openEditor); } @@ -109,7 +102,6 @@ function Cell({
({ * refs */ const gridRef = useRef(null); + const rowsContainerRef = useRef(null); + const prevSelectedPosition = useRef(selectedPosition); const latestDraggedOverRowIdx = useRef(draggedOverRowIdx); const lastSelectedRowIdx = useRef(-1); @@ -295,6 +296,15 @@ function DataGrid({ }; }, [width]); + useLayoutEffect(() => { + if (selectedPosition === prevSelectedPosition.current || selectedPosition.mode === 'EDIT' || !isCellWithinBounds(selectedPosition)) return; + prevSelectedPosition.current = selectedPosition; + scrollToCell(selectedPosition); + if (document.activeElement !== rowsContainerRef.current) { + rowsContainerRef.current!.focus({ preventScroll: true }); + } + }); + useEffect(() => { if (!onSelectedRowsChange) return; @@ -344,7 +354,10 @@ function DataGrid({ * event handlers */ function handleKeyDown(event: React.KeyboardEvent) { - if (enableCellCopyPaste && isCtrlKeyHeldDown(event)) { + // if (event.currentTarget !== event.target && selectedPosition.mode === 'SELECT') return; + // if (!isCellWithinBounds(selectedPosition)) return; + + if (enableCellCopyPaste && isCtrlKeyHeldDown(event) && isCellWithinBounds(selectedPosition)) { // event.key may be uppercase `C` or `V` const lowerCaseKey = event.key.toLowerCase(); if (lowerCaseKey === 'c') { @@ -366,8 +379,11 @@ function DataGrid({ case 'ArrowLeft': case 'ArrowRight': case 'Tab': - event.preventDefault(); - navigate(event.key, event.shiftKey); + case 'Home': + case 'End': + case 'PageUp': + case 'PageDown': + navigate(event); break; default: handleCellInput(event); @@ -399,7 +415,7 @@ function DataGrid({ action: UpdateActions.CELL_UPDATE }); - setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, mode: 'SELECT' })); + closeEditor(); } function handleCopy() { @@ -521,10 +537,13 @@ function DataGrid({ } else { setSelectedPosition({ ...position, mode: 'SELECT' }); } - scrollToCell(position); onSelectedCellChange?.({ ...position }); } + function closeEditor() { + setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, mode: 'SELECT' })); + } + function getFrozenColumnsWidth(): number { if (lastFrozenColumnIndex === -1) return 0; const lastFrozenCol = columns[lastFrozenColumnIndex]; @@ -557,14 +576,44 @@ function DataGrid({ } } - function navigate(key: string, shiftKey: boolean) { - let nextPosition = getNextPosition(key, shiftKey, selectedPosition); + function getNextPosition(key: string, ctrlKey: boolean, shiftKey: boolean): Position { + const { idx, rowIdx } = selectedPosition; + switch (key) { + case 'ArrowUp': + return { idx, rowIdx: rowIdx - 1 }; + case 'ArrowDown': + return { idx, rowIdx: rowIdx + 1 }; + case 'ArrowLeft': + return { idx: idx - 1, rowIdx }; + case 'ArrowRight': + 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 { 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 }; + case 'PageUp': + return { idx, rowIdx: rowIdx - Math.floor(clientHeight / rowHeight) }; + case 'PageDown': + return { idx, rowIdx: rowIdx + Math.floor(clientHeight / rowHeight) }; + default: + return selectedPosition; + } + } + + function navigate(event: React.KeyboardEvent) { + const { key, shiftKey } = event; + const ctrlKey = isCtrlKeyHeldDown(event); + let nextPosition = getNextPosition(key, ctrlKey, shiftKey); 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 })) { - // Reset the selected position before exiting - setSelectedPosition({ idx: -1, rowIdx: -1, mode: 'SELECT' }); + // Allow focus to leave the grid so the next control in the tab order can be focused return; } @@ -573,6 +622,9 @@ function DataGrid({ : cellNavigationMode; } + // Do not allow focus to leave + event.preventDefault(); + nextPosition = getNextSelectedCellPosition({ columns, rowsCount: rows.length, @@ -609,7 +661,7 @@ function DataGrid({ scrollTop, firstEditorKeyPress: selectedPosition.key, onCommit: handleCommit, - onCommitCancel: () => setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, mode: 'SELECT' })) + onCommitCancel: closeEditor } }; } @@ -617,55 +669,45 @@ function DataGrid({ return { mode: 'SELECT', idx: selectedPosition.idx, - onKeyDown: handleKeyDown, dragHandleProps: enableCellDragAndDrop && isCellEditable(selectedPosition) ? { onMouseDown: handleMouseDown, onDoubleClick: handleDoubleClick } : undefined }; } - function getRow(rowIdx: number) { - const row = rows[rowIdx]; - let key: string | number = rowIdx; - let isRowSelected = false; - if (rowKey !== undefined) { - const rowId = row[rowKey]; - isRowSelected = selectedRows?.has(rowId) ?? false; - if (typeof rowId === 'string' || typeof rowId === 'number') { - key = rowId; - } - } - - return ( - - ); - } - function getViewportRows() { const rowElements = []; - // Add the row containing the selected cell if not included in the vertical range - if (isCellWithinBounds(selectedPosition) && (selectedPosition.rowIdx < rowOverscanStartIdx || selectedPosition.rowIdx > rowOverscanEndIdx)) { - rowElements.push(getRow(selectedPosition.rowIdx)); - } - for (let rowIdx = rowOverscanStartIdx; rowIdx <= rowOverscanEndIdx; rowIdx++) { - rowElements.push(getRow(rowIdx)); + const row = rows[rowIdx]; + let key: string | number = rowIdx; + let isRowSelected = false; + if (rowKey !== undefined) { + const rowId = row[rowKey]; + isRowSelected = selectedRows?.has(rowId) ?? false; + if (typeof rowId === 'string' || typeof rowId === 'number') { + key = rowId; + } + } + + rowElements.push( + + ); } return rowElements; @@ -714,8 +756,18 @@ function DataGrid({ )} {rows.length === 0 && emptyRowsRenderer ? createElement(emptyRowsRenderer) : ( <> -
- {viewportWidth > 0 && getViewportRows()} +
+ {getViewportRows()} {summaryRows?.map((row, rowIdx) => ( key={rowIdx} diff --git a/src/common/types.ts b/src/common/types.ts index 44be661330..e8c1dbf23c 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -107,7 +107,7 @@ export interface SharedEditorContainerProps { interface SelectedCellPropsBase { idx: number; - onKeyDown: (event: React.KeyboardEvent) => void; + onKeyDown?: (event: React.KeyboardEvent) => void; } interface SelectedCellPropsEdit extends SelectedCellPropsBase { diff --git a/src/utils/selectedCellUtils.ts b/src/utils/selectedCellUtils.ts index 8b3cd46dad..b9aa18046f 100644 --- a/src/utils/selectedCellUtils.ts +++ b/src/utils/selectedCellUtils.ts @@ -23,24 +23,6 @@ interface GetNextSelectedCellPositionOpts { nextPosition: Position; } -export function getNextPosition(key: string, shiftKey: boolean, currentPosition: Position) { - const { idx, rowIdx } = currentPosition; - switch (key) { - case 'ArrowUp': - return { idx, rowIdx: rowIdx - 1 }; - case 'ArrowDown': - return { idx, rowIdx: rowIdx + 1 }; - case 'ArrowLeft': - return { idx: idx - 1, rowIdx }; - case 'ArrowRight': - return { idx: idx + 1, rowIdx }; - case 'Tab': - return { idx: idx + (shiftKey ? -1 : 1), rowIdx }; - default: - return currentPosition; - } -} - export function getNextSelectedCellPosition({ cellNavigationMode, columns, rowsCount, nextPosition }: GetNextSelectedCellPositionOpts): Position { if (cellNavigationMode !== CellNavigationMode.NONE) { const { idx, rowIdx } = nextPosition; diff --git a/style/cell.less b/style/cell.less index 60238c919c..f37325c3fb 100644 --- a/style/cell.less +++ b/style/cell.less @@ -25,7 +25,6 @@ .rdg-cell-selected { box-shadow: inset 0 0 0 2px #66afe9; - outline: 0; } .rdg-cell-copied { From 90116862a901ff0f61152f55127920c4b2d218f7 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Mon, 13 Jul 2020 13:10:38 -0500 Subject: [PATCH 38/42] Remove comments --- src/DataGrid.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 2aed1ade43..9e3d2e56e9 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -354,9 +354,6 @@ function DataGrid({ * event handlers */ function handleKeyDown(event: React.KeyboardEvent) { - // if (event.currentTarget !== event.target && selectedPosition.mode === 'SELECT') return; - // if (!isCellWithinBounds(selectedPosition)) return; - if (enableCellCopyPaste && isCtrlKeyHeldDown(event) && isCellWithinBounds(selectedPosition)) { // event.key may be uppercase `C` or `V` const lowerCaseKey = event.key.toLowerCase(); From 421d26552e3cb125cd1de2d9a10be7c44c70eab2 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Mon, 13 Jul 2020 20:57:18 -0500 Subject: [PATCH 39/42] Check valid selection --- src/DataGrid.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 9e3d2e56e9..cfa7676b49 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -383,7 +383,9 @@ function DataGrid({ navigate(event); break; default: - handleCellInput(event); + if (isCellWithinBounds(selectedPosition)) { + handleCellInput(event); + } break; } } From 6ffb06a3a59bb16f3e7201b3e8abf21afb5307be Mon Sep 17 00:00:00 2001 From: Mahajan Date: Tue, 14 Jul 2020 18:30:50 -0500 Subject: [PATCH 40/42] Even better focus implementation --- src/Cell.tsx | 2 -- src/DataGrid.tsx | 23 ++++++++++------------- src/common/types.ts | 1 - style/core.less | 11 ++++++++++- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index 4d8d52fe5a..6b199cab2b 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -18,7 +18,6 @@ function Cell({ eventBus, selectedCellProps, onRowClick, - onKeyDown, onClick, onDoubleClick, onContextMenu, @@ -106,7 +105,6 @@ function Cell({ width: column.width, left: column.left }} - onKeyDown={selectedCellProps?.onKeyDown ? wrapEvent(selectedCellProps.onKeyDown, onKeyDown) : onKeyDown} onClick={isEditing ? onClick : wrapEvent(handleClick, onClick)} onDoubleClick={isEditing ? onDoubleClick : wrapEvent(handleDoubleClick, onDoubleClick)} onContextMenu={isEditing ? onContextMenu : wrapEvent(handleContextMenu, onContextMenu)} diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index cfa7676b49..5cc5cd2efd 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -227,7 +227,7 @@ function DataGrid({ * refs */ const gridRef = useRef(null); - const rowsContainerRef = useRef(null); + const focusSinkRef = useRef(null); const prevSelectedPosition = useRef(selectedPosition); const latestDraggedOverRowIdx = useRef(draggedOverRowIdx); const lastSelectedRowIdx = useRef(-1); @@ -300,9 +300,7 @@ function DataGrid({ if (selectedPosition === prevSelectedPosition.current || selectedPosition.mode === 'EDIT' || !isCellWithinBounds(selectedPosition)) return; prevSelectedPosition.current = selectedPosition; scrollToCell(selectedPosition); - if (document.activeElement !== rowsContainerRef.current) { - rowsContainerRef.current!.focus({ preventScroll: true }); - } + focusSinkRef.current!.focus(); }); useEffect(() => { @@ -652,7 +650,6 @@ function DataGrid({ return { mode: 'EDIT', idx: selectedPosition.idx, - onKeyDown: handleKeyDown, editorContainerProps: { editorPortalTarget, rowHeight, @@ -756,17 +753,17 @@ function DataGrid({ {rows.length === 0 && emptyRowsRenderer ? createElement(emptyRowsRenderer) : ( <>
- {getViewportRows()} +
+ {getViewportRows()} +
{summaryRows?.map((row, rowIdx) => ( key={rowIdx} diff --git a/src/common/types.ts b/src/common/types.ts index e8c1dbf23c..338d3c97df 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -107,7 +107,6 @@ export interface SharedEditorContainerProps { interface SelectedCellPropsBase { idx: number; - onKeyDown?: (event: React.KeyboardEvent) => void; } interface SelectedCellPropsEdit extends SelectedCellPropsBase { diff --git a/style/core.less b/style/core.less index 4cf631d332..aefbea42bb 100644 --- a/style/core.less +++ b/style/core.less @@ -1,4 +1,4 @@ -@import './variables.less'; +@import "./variables.less"; .rdg { // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context @@ -27,6 +27,15 @@ } } +.rdg-focus-sink { + position: sticky; + top: 0; + left: 0; + height: 0; + width: 0; + outline: 0; +} + .rdg-viewport-dragging .rdg-row { cursor: move; } From 4eb110042edc3b84c97eac6cc33342bf6b1308be Mon Sep 17 00:00:00 2001 From: Mahajan Date: Tue, 14 Jul 2020 20:30:04 -0500 Subject: [PATCH 41/42] Cleanup handleKeyDown usage --- src/Cell.tsx | 2 ++ src/DataGrid.tsx | 10 ++++------ src/common/types.ts | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index 6b199cab2b..51e435677b 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -18,6 +18,7 @@ function Cell({ eventBus, selectedCellProps, onRowClick, + onKeyDown, onClick, onDoubleClick, onContextMenu, @@ -105,6 +106,7 @@ function Cell({ width: column.width, left: column.left }} + onKeyDown={selectedCellProps ? wrapEvent(selectedCellProps.onKeyDown, onKeyDown) : onKeyDown} onClick={isEditing ? onClick : wrapEvent(handleClick, onClick)} onDoubleClick={isEditing ? onDoubleClick : wrapEvent(handleDoubleClick, onDoubleClick)} onContextMenu={isEditing ? onContextMenu : wrapEvent(handleContextMenu, onContextMenu)} diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 5cc5cd2efd..4f8dba8d28 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -650,6 +650,7 @@ function DataGrid({ return { mode: 'EDIT', idx: selectedPosition.idx, + onKeyDown: handleKeyDown, editorContainerProps: { editorPortalTarget, rowHeight, @@ -665,6 +666,7 @@ function DataGrid({ return { mode: 'SELECT', idx: selectedPosition.idx, + onKeyDown: handleKeyDown, dragHandleProps: enableCellDragAndDrop && isCellEditable(selectedPosition) ? { onMouseDown: handleMouseDown, onDoubleClick: handleDoubleClick } : undefined @@ -758,12 +760,8 @@ function DataGrid({ className="rdg-focus-sink" onKeyDown={handleKeyDown} /> -
- {getViewportRows()} -
+
+ {getViewportRows()} {summaryRows?.map((row, rowIdx) => ( key={rowIdx} diff --git a/src/common/types.ts b/src/common/types.ts index 338d3c97df..44be661330 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -107,6 +107,7 @@ export interface SharedEditorContainerProps { interface SelectedCellPropsBase { idx: number; + onKeyDown: (event: React.KeyboardEvent) => void; } interface SelectedCellPropsEdit extends SelectedCellPropsBase { From c8cb1946fceb12e5978d03b216f1811e14cc81e5 Mon Sep 17 00:00:00 2001 From: Mahajan Date: Wed, 15 Jul 2020 11:15:20 -0500 Subject: [PATCH 42/42] Remove drag cell borders --- style/cell.less | 3 --- 1 file changed, 3 deletions(-) diff --git a/style/cell.less b/style/cell.less index f37325c3fb..9111a9da99 100644 --- a/style/cell.less +++ b/style/cell.less @@ -50,9 +50,6 @@ .rdg-cell-dragged-over { background-color:#ccccff; - border-left: 1px dashed black; - border-right: 1px dashed black; - padding-left: 7px; } .rdg-cell-copied.rdg-cell-dragged-over {