From 31eeb9c38e2d9597f17d1ea7a5a0ecc98ea2fc81 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Mon, 9 Dec 2024 10:03:30 -0600 Subject: [PATCH 1/5] Use native copy/paste event handlers --- src/DataGrid.tsx | 68 +++++++++++++++++--------------------- src/TreeDataGrid.tsx | 4 +-- src/types.ts | 4 +-- src/utils/keyboardUtils.ts | 13 ++++++-- 4 files changed, 44 insertions(+), 45 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index dc6f6aeb58..33667ffbe7 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -1,4 +1,12 @@ -import { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'; +import { + forwardRef, + useCallback, + useImperativeHandle, + useMemo, + useRef, + useState, + type ClipboardEvent +} from 'react'; import type { Key, KeyboardEvent, RefAttributes } from 'react'; import { flushSync } from 'react-dom'; import clsx from 'clsx'; @@ -161,8 +169,10 @@ export interface DataGridProps extends Sha onSortColumnsChange?: Maybe<(sortColumns: SortColumn[]) => void>; defaultColumnOptions?: Maybe, NoInfer>>; onFill?: Maybe<(event: FillEvent>) => NoInfer>; - onCopy?: Maybe<(event: CopyEvent>) => void>; - onPaste?: Maybe<(event: PasteEvent>) => NoInfer>; + onCopy?: Maybe<(args: CopyEvent>, event: ClipboardEvent) => void>; + onPaste?: Maybe< + (args: PasteEvent>, event: ClipboardEvent) => NoInfer + >; /** * Event props @@ -599,30 +609,6 @@ function DataGrid( const isRowEvent = isTreeGrid && event.target === focusSinkRef.current; if (!isCellEvent && !isRowEvent) return; - // eslint-disable-next-line @typescript-eslint/no-deprecated - const { keyCode } = event; - - if ( - selectedCellIsWithinViewportBounds && - (onPaste != null || onCopy != null) && - isCtrlKeyHeldDown(event) - ) { - // event.key may differ by keyboard input language, so we use event.keyCode instead - // event.nativeEvent.code cannot be used either as it would break copy/paste for the DVORAK layout - const cKey = 67; - const vKey = 86; - if (keyCode === cKey) { - // copy highlighted text only - if (window.getSelection()?.isCollapsed === false) return; - handleCopy(); - return; - } - if (keyCode === vKey) { - handlePaste(); - return; - } - } - switch (event.key) { case 'Escape': setCopiedCell(null); @@ -670,15 +656,18 @@ function DataGrid( updateRow(columns[selectedPosition.idx], selectedPosition.rowIdx, selectedPosition.row); } - function handleCopy() { + function handleCopy(event: React.ClipboardEvent) { + if (!selectedCellIsWithinViewportBounds) return; + // copy highlighted text only + if (window.getSelection()?.isCollapsed === false) return; const { idx, rowIdx } = selectedPosition; const sourceRow = rows[rowIdx]; const sourceColumnKey = columns[idx].key; setCopiedCell({ row: sourceRow, columnKey: sourceColumnKey }); - onCopy?.({ sourceRow, sourceColumnKey }); + onCopy?.({ sourceRow, sourceColumnKey }, event); } - function handlePaste() { + function handlePaste(event: ClipboardEvent) { if (!onPaste || !onRowsChange || copiedCell === null || !isCellEditable(selectedPosition)) { return; } @@ -687,12 +676,15 @@ function DataGrid( const targetColumn = columns[idx]; const targetRow = rows[rowIdx]; - const updatedTargetRow = onPaste({ - sourceRow: copiedCell.row, - sourceColumnKey: copiedCell.columnKey, - targetRow, - targetColumnKey: targetColumn.key - }); + const updatedTargetRow = onPaste( + { + sourceRow: copiedCell.row, + sourceColumnKey: copiedCell.columnKey, + targetRow, + targetColumnKey: targetColumn.key + }, + event + ); updateRow(targetColumn, rowIdx, updatedTargetRow); } @@ -712,7 +704,7 @@ function DataGrid( return; } - if (isCellEditable(selectedPosition) && isDefaultCellInput(event)) { + if (isCellEditable(selectedPosition) && isDefaultCellInput(event, onPaste != null)) { setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, @@ -1121,6 +1113,8 @@ function DataGrid( ref={gridRef} onScroll={handleScroll} onKeyDown={handleKeyDown} + onCopy={handleCopy} + onPaste={handlePaste} data-testid={testId} > diff --git a/src/TreeDataGrid.tsx b/src/TreeDataGrid.tsx index 16e4f87232..fafc9198fb 100644 --- a/src/TreeDataGrid.tsx +++ b/src/TreeDataGrid.tsx @@ -2,7 +2,7 @@ import { forwardRef, useCallback, useMemo } from 'react'; import type { Key, RefAttributes } from 'react'; import { useLatestFunc } from './hooks'; -import { assertIsValidKeyGetter, isCtrlKeyHeldDown } from './utils'; +import { assertIsValidKeyGetter, cKey, isCtrlKeyHeldDown, vKey } from './utils'; import type { CellKeyboardEvent, CellKeyDownArgs, @@ -324,7 +324,7 @@ function TreeDataGrid( // Prevent copy/paste on group rows // eslint-disable-next-line @typescript-eslint/no-deprecated - if (isCtrlKeyHeldDown(event) && (event.keyCode === 67 || event.keyCode === 86)) { + if (isCtrlKeyHeldDown(event) && (event.keyCode === cKey || event.keyCode === vKey)) { event.preventGridDefault(); } } diff --git a/src/types.ts b/src/types.ts index 561bb651be..60dcac07d7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -254,9 +254,7 @@ export interface CopyEvent { sourceRow: TRow; } -export interface PasteEvent { - sourceColumnKey: string; - sourceRow: TRow; +export interface PasteEvent extends CopyEvent { targetColumnKey: string; targetRow: TRow; } diff --git a/src/utils/keyboardUtils.ts b/src/utils/keyboardUtils.ts index e12609eb4f..af1c84a1ed 100644 --- a/src/utils/keyboardUtils.ts +++ b/src/utils/keyboardUtils.ts @@ -52,10 +52,17 @@ export function isCtrlKeyHeldDown(e: React.KeyboardEvent): boolean { return (e.ctrlKey || e.metaKey) && e.key !== 'Control'; } -export function isDefaultCellInput(event: React.KeyboardEvent): boolean { - const vKey = 86; +// event.key may differ by keyboard input language, so we use event.keyCode instead +// event.nativeEvent.code cannot be used either as it would break copy/paste for the DVORAK layout +export const cKey = 67; +export const vKey = 86; + +export function isDefaultCellInput( + event: React.KeyboardEvent, + isUserHandlingPaste: boolean +): boolean { // eslint-disable-next-line @typescript-eslint/no-deprecated - if (isCtrlKeyHeldDown(event) && event.keyCode !== vKey) return false; + if (isCtrlKeyHeldDown(event) && (event.keyCode !== vKey || isUserHandlingPaste)) return false; return !nonInputKeys.has(event.key); } From 403f417f51eb5e4c7b08cc4df62ce1d1bd8068c3 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Wed, 11 Dec 2024 21:40:44 -0600 Subject: [PATCH 2/5] optional types --- src/DataGrid.tsx | 42 ++++++++++++++--------------- src/types.ts | 6 ++++- src/utils/keyboardUtils.ts | 4 +-- test/browser/utils.tsx | 4 ++- website/routes/AllFeatures.lazy.tsx | 2 ++ 5 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 33667ffbe7..49bff4ec72 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -1,12 +1,4 @@ -import { - forwardRef, - useCallback, - useImperativeHandle, - useMemo, - useRef, - useState, - type ClipboardEvent -} from 'react'; +import { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState } from 'react'; import type { Key, KeyboardEvent, RefAttributes } from 'react'; import { flushSync } from 'react-dom'; import clsx from 'clsx'; @@ -41,6 +33,7 @@ import { import type { CalculatedColumn, CellClickArgs, + CellClipboardEvent, CellKeyboardEvent, CellKeyDownArgs, CellMouseEvent, @@ -169,10 +162,8 @@ export interface DataGridProps extends Sha onSortColumnsChange?: Maybe<(sortColumns: SortColumn[]) => void>; defaultColumnOptions?: Maybe, NoInfer>>; onFill?: Maybe<(event: FillEvent>) => NoInfer>; - onCopy?: Maybe<(args: CopyEvent>, event: ClipboardEvent) => void>; - onPaste?: Maybe< - (args: PasteEvent>, event: ClipboardEvent) => NoInfer - >; + onCopy?: Maybe<(args: CopyEvent>, event: CellClipboardEvent) => void>; + onPaste?: Maybe<(args: PasteEvent>, event: CellClipboardEvent) => NoInfer>; /** * Event props @@ -656,19 +647,26 @@ function DataGrid( updateRow(columns[selectedPosition.idx], selectedPosition.rowIdx, selectedPosition.row); } - function handleCopy(event: React.ClipboardEvent) { + function handleCopy(event: CellClipboardEvent) { if (!selectedCellIsWithinViewportBounds) return; - // copy highlighted text only - if (window.getSelection()?.isCollapsed === false) return; const { idx, rowIdx } = selectedPosition; const sourceRow = rows[rowIdx]; const sourceColumnKey = columns[idx].key; - setCopiedCell({ row: sourceRow, columnKey: sourceColumnKey }); onCopy?.({ sourceRow, sourceColumnKey }, event); + + // copy highlighted text only + if (window.getSelection()?.isCollapsed === false) { + setCopiedCell(null); + return; + } + + if (onPaste) { + setCopiedCell({ row: sourceRow, columnKey: sourceColumnKey }); + } } - function handlePaste(event: ClipboardEvent) { - if (!onPaste || !onRowsChange || copiedCell === null || !isCellEditable(selectedPosition)) { + function handlePaste(event: CellClipboardEvent) { + if (!onPaste || !onRowsChange || !isCellEditable(selectedPosition)) { return; } @@ -678,8 +676,8 @@ function DataGrid( const updatedTargetRow = onPaste( { - sourceRow: copiedCell.row, - sourceColumnKey: copiedCell.columnKey, + sourceRow: copiedCell?.row, + sourceColumnKey: copiedCell?.columnKey, targetRow, targetColumnKey: targetColumn.key }, @@ -704,7 +702,7 @@ function DataGrid( return; } - if (isCellEditable(selectedPosition) && isDefaultCellInput(event, onPaste != null)) { + if (isCellEditable(selectedPosition) && isDefaultCellInput(event, copiedCell !== null)) { setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, diff --git a/src/types.ts b/src/types.ts index 60dcac07d7..1ebcb98079 100644 --- a/src/types.ts +++ b/src/types.ts @@ -167,6 +167,8 @@ export type CellMouseEvent = CellEvent>; export type CellKeyboardEvent = CellEvent>; +export type CellClipboardEvent = React.ClipboardEvent; + export interface CellClickArgs { rowIdx: number; row: TRow; @@ -254,7 +256,9 @@ export interface CopyEvent { sourceRow: TRow; } -export interface PasteEvent extends CopyEvent { +export interface PasteEvent { + sourceColumnKey: string | undefined; + sourceRow: TRow | undefined; targetColumnKey: string; targetRow: TRow; } diff --git a/src/utils/keyboardUtils.ts b/src/utils/keyboardUtils.ts index af1c84a1ed..68150ef9bb 100644 --- a/src/utils/keyboardUtils.ts +++ b/src/utils/keyboardUtils.ts @@ -59,10 +59,10 @@ export const vKey = 86; export function isDefaultCellInput( event: React.KeyboardEvent, - isUserHandlingPaste: boolean + isCopyingCell: boolean ): boolean { // eslint-disable-next-line @typescript-eslint/no-deprecated - if (isCtrlKeyHeldDown(event) && (event.keyCode !== vKey || isUserHandlingPaste)) return false; + if (isCtrlKeyHeldDown(event) && (event.keyCode !== vKey || isCopyingCell)) return false; return !nonInputKeys.has(event.key); } diff --git a/test/browser/utils.tsx b/test/browser/utils.tsx index 3b00f7538b..f8b8bb3c7f 100644 --- a/test/browser/utils.tsx +++ b/test/browser/utils.tsx @@ -1,4 +1,4 @@ -import { act, render, screen } from '@testing-library/react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import { page, userEvent } from '@vitest/browser/context'; import { css } from '@linaria/core'; @@ -126,6 +126,7 @@ export async function copySelectedCellOld() { } export function copySelectedCell() { + fireEvent.copy(document.activeElement!); return userEvent.keyboard('{Control>}c{/Control}'); } @@ -137,6 +138,7 @@ export async function pasteSelectedCellOld() { } export function pasteSelectedCell() { + fireEvent.paste(document.activeElement!); return userEvent.keyboard('{Control>}v{/Control}'); } diff --git a/website/routes/AllFeatures.lazy.tsx b/website/routes/AllFeatures.lazy.tsx index 4d412f687b..89c0683eb7 100644 --- a/website/routes/AllFeatures.lazy.tsx +++ b/website/routes/AllFeatures.lazy.tsx @@ -182,6 +182,8 @@ function AllFeatures() { targetColumnKey, targetRow }: PasteEvent): Row { + if (sourceColumnKey === undefined || sourceRow === undefined) return targetRow; + const incompatibleColumns = ['email', 'zipCode', 'date']; if ( sourceColumnKey === 'avatar' || From 3288bf3ac41da32e1fec1c84af6f71c58b3ea28e Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 12 Dec 2024 16:02:46 -0600 Subject: [PATCH 3/5] Move celly copying logic outside the grid --- src/Cell.tsx | 14 ---- src/DataGrid.tsx | 54 ++++--------- src/Row.tsx | 2 - src/TreeDataGrid.tsx | 10 +-- src/index.ts | 3 +- src/types.ts | 15 +--- src/utils/keyboardUtils.ts | 7 +- website/routes/AllFeatures.lazy.tsx | 113 +++++++++++++++++++--------- 8 files changed, 97 insertions(+), 121 deletions(-) diff --git a/src/Cell.tsx b/src/Cell.tsx index 29cd201693..43cd02a188 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -5,21 +5,9 @@ import { useRovingTabIndex } from './hooks'; import { createCellEvent, getCellClassname, getCellStyle, isCellEditableUtil } from './utils'; import type { CellRendererProps } from './types'; -const cellCopied = css` - @layer rdg.Cell { - background-color: #ccccff; - } -`; - -const cellCopiedClassname = `rdg-cell-copied ${cellCopied}`; - const cellDraggedOver = css` @layer rdg.Cell { background-color: #ccccff; - - &.${cellCopied} { - background-color: #9999ff; - } } `; @@ -30,7 +18,6 @@ function Cell( column, colSpan, isCellSelected, - isCopied, isDraggedOver, row, rowIdx, @@ -51,7 +38,6 @@ function Cell( className = getCellClassname( column, { - [cellCopiedClassname]: isCopied, [cellDraggedOverClassname]: isDraggedOver }, typeof cellClass === 'function' ? cellClass(row) : cellClass, diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 49bff4ec72..da14f70786 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -41,11 +41,10 @@ import type { CellSelectArgs, Column, ColumnOrColumnGroup, - CopyEvent, + CopyPasteEvent, Direction, FillEvent, Maybe, - PasteEvent, Position, Renderers, RowsChangeData, @@ -162,8 +161,12 @@ export interface DataGridProps extends Sha onSortColumnsChange?: Maybe<(sortColumns: SortColumn[]) => void>; defaultColumnOptions?: Maybe, NoInfer>>; onFill?: Maybe<(event: FillEvent>) => NoInfer>; - onCopy?: Maybe<(args: CopyEvent>, event: CellClipboardEvent) => void>; - onPaste?: Maybe<(args: PasteEvent>, event: CellClipboardEvent) => NoInfer>; + onCopy?: Maybe< + (args: CopyPasteEvent, NoInfer>, event: CellClipboardEvent) => void + >; + onPaste?: Maybe< + (args: CopyPasteEvent, NoInfer>, event: CellClipboardEvent) => NoInfer + >; /** * Event props @@ -297,7 +300,6 @@ function DataGrid( const [measuredColumnWidths, setMeasuredColumnWidths] = useState( (): ReadonlyMap => new Map() ); - const [copiedCell, setCopiedCell] = useState<{ row: R; columnKey: string } | null>(null); const [isDragging, setDragging] = useState(false); const [draggedOverRowIdx, setOverRowIdx] = useState(undefined); const [scrollToPosition, setScrollToPosition] = useState(null); @@ -595,15 +597,13 @@ function DataGrid( ); if (cellEvent.isGridDefaultPrevented()) return; } + if (!(event.target instanceof Element)) return; const isCellEvent = event.target.closest('.rdg-cell') !== null; const isRowEvent = isTreeGrid && event.target === focusSinkRef.current; if (!isCellEvent && !isRowEvent) return; switch (event.key) { - case 'Escape': - setCopiedCell(null); - return; case 'ArrowUp': case 'ArrowDown': case 'ArrowLeft': @@ -650,19 +650,7 @@ function DataGrid( function handleCopy(event: CellClipboardEvent) { if (!selectedCellIsWithinViewportBounds) return; const { idx, rowIdx } = selectedPosition; - const sourceRow = rows[rowIdx]; - const sourceColumnKey = columns[idx].key; - onCopy?.({ sourceRow, sourceColumnKey }, event); - - // copy highlighted text only - if (window.getSelection()?.isCollapsed === false) { - setCopiedCell(null); - return; - } - - if (onPaste) { - setCopiedCell({ row: sourceRow, columnKey: sourceColumnKey }); - } + onCopy?.({ row: rows[rowIdx], column: columns[idx] }, event); } function handlePaste(event: CellClipboardEvent) { @@ -671,20 +659,9 @@ function DataGrid( } const { idx, rowIdx } = selectedPosition; - const targetColumn = columns[idx]; - const targetRow = rows[rowIdx]; - - const updatedTargetRow = onPaste( - { - sourceRow: copiedCell?.row, - sourceColumnKey: copiedCell?.columnKey, - targetRow, - targetColumnKey: targetColumn.key - }, - event - ); - - updateRow(targetColumn, rowIdx, updatedTargetRow); + const column = columns[idx]; + const updatedRow = onPaste({ row: rows[rowIdx], column }, event); + updateRow(column, rowIdx, updatedRow); } function handleCellInput(event: KeyboardEvent) { @@ -702,7 +679,7 @@ function DataGrid( return; } - if (isCellEditable(selectedPosition) && isDefaultCellInput(event, copiedCell !== null)) { + if (isCellEditable(selectedPosition) && isDefaultCellInput(event, onPaste != null)) { setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, @@ -1027,11 +1004,6 @@ function DataGrid( onCellContextMenu: onCellContextMenuLatest, rowClass, gridRowStart, - copiedCellIdx: - copiedCell !== null && copiedCell.row === row - ? columns.findIndex((c) => c.key === copiedCell.columnKey) - : undefined, - selectedCellIdx: selectedRowIdx === rowIdx ? selectedIdx : undefined, draggedOverCellIdx: getDraggedOverCellIdx(rowIdx), setDraggedOverRowIdx: isDragging ? setDraggedOverRowIdx : undefined, diff --git a/src/Row.tsx b/src/Row.tsx index 9b811a3397..9b9d8e0097 100644 --- a/src/Row.tsx +++ b/src/Row.tsx @@ -15,7 +15,6 @@ function Row( selectedCellIdx, isRowSelectionDisabled, isRowSelected, - copiedCellIdx, draggedOverCellIdx, lastFrozenColumnIndex, row, @@ -75,7 +74,6 @@ function Row( colSpan, row, rowIdx, - isCopied: copiedCellIdx === idx, isDraggedOver: draggedOverCellIdx === idx, isCellSelected, onClick: onCellClick, diff --git a/src/TreeDataGrid.tsx b/src/TreeDataGrid.tsx index fafc9198fb..aa46a01157 100644 --- a/src/TreeDataGrid.tsx +++ b/src/TreeDataGrid.tsx @@ -2,7 +2,7 @@ import { forwardRef, useCallback, useMemo } from 'react'; import type { Key, RefAttributes } from 'react'; import { useLatestFunc } from './hooks'; -import { assertIsValidKeyGetter, cKey, isCtrlKeyHeldDown, vKey } from './utils'; +import { assertIsValidKeyGetter } from './utils'; import type { CellKeyboardEvent, CellKeyDownArgs, @@ -321,12 +321,6 @@ function TreeDataGrid( selectCell({ idx, rowIdx: parentRowAndIndex[1] }); } } - - // Prevent copy/paste on group rows - // eslint-disable-next-line @typescript-eslint/no-deprecated - if (isCtrlKeyHeldDown(event) && (event.keyCode === cKey || event.keyCode === vKey)) { - event.preventGridDefault(); - } } function handleRowsChange(updatedRows: R[], { indexes, column }: RowsChangeData) { @@ -364,7 +358,6 @@ function TreeDataGrid( onCellContextMenu, onRowChange, lastFrozenColumnIndex, - copiedCellIdx, draggedOverCellIdx, setDraggedOverRowIdx, selectedCellEditor, @@ -403,7 +396,6 @@ function TreeDataGrid( onCellContextMenu, onRowChange, lastFrozenColumnIndex, - copiedCellIdx, draggedOverCellIdx, setDraggedOverRowIdx, selectedCellEditor diff --git a/src/index.ts b/src/index.ts index d76283ea28..a3845ef26e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,8 +29,7 @@ export type { SelectHeaderRowEvent, SelectRowEvent, FillEvent, - CopyEvent, - PasteEvent, + CopyPasteEvent, SortDirection, SortColumn, ColSpanArgs, diff --git a/src/types.ts b/src/types.ts index 1ebcb98079..b588593692 100644 --- a/src/types.ts +++ b/src/types.ts @@ -149,7 +149,6 @@ export interface CellRendererProps > { column: CalculatedColumn; colSpan: number | undefined; - isCopied: boolean; isDraggedOver: boolean; isCellSelected: boolean; onClick: RenderRowProps['onCellClick']; @@ -222,7 +221,6 @@ export interface RenderRowProps extends BaseRenderRowProps { row: TRow; lastFrozenColumnIndex: number; - copiedCellIdx: number | undefined; draggedOverCellIdx: number | undefined; selectedCellEditor: ReactElement> | undefined; onRowChange: (column: CalculatedColumn, rowIdx: number, newRow: TRow) => void; @@ -251,16 +249,9 @@ export interface FillEvent { targetRow: TRow; } -export interface CopyEvent { - sourceColumnKey: string; - sourceRow: TRow; -} - -export interface PasteEvent { - sourceColumnKey: string | undefined; - sourceRow: TRow | undefined; - targetColumnKey: string; - targetRow: TRow; +export interface CopyPasteEvent { + row: TRow; + column: CalculatedColumn; } export interface GroupRow { diff --git a/src/utils/keyboardUtils.ts b/src/utils/keyboardUtils.ts index 68150ef9bb..5bb352521e 100644 --- a/src/utils/keyboardUtils.ts +++ b/src/utils/keyboardUtils.ts @@ -54,15 +54,14 @@ export function isCtrlKeyHeldDown(e: React.KeyboardEvent): boolean { // event.key may differ by keyboard input language, so we use event.keyCode instead // event.nativeEvent.code cannot be used either as it would break copy/paste for the DVORAK layout -export const cKey = 67; -export const vKey = 86; +const vKey = 86; export function isDefaultCellInput( event: React.KeyboardEvent, - isCopyingCell: boolean + isUserHandlingPaste: boolean ): boolean { // eslint-disable-next-line @typescript-eslint/no-deprecated - if (isCtrlKeyHeldDown(event) && (event.keyCode !== vKey || isCopyingCell)) return false; + if (isCtrlKeyHeldDown(event) && (event.keyCode !== vKey || isUserHandlingPaste)) return false; return !nonInputKeys.has(event.key); } diff --git a/website/routes/AllFeatures.lazy.tsx b/website/routes/AllFeatures.lazy.tsx index 89c0683eb7..db65f9298f 100644 --- a/website/routes/AllFeatures.lazy.tsx +++ b/website/routes/AllFeatures.lazy.tsx @@ -1,9 +1,10 @@ import { useState } from 'react'; import { createLazyFileRoute } from '@tanstack/react-router'; import { css } from '@linaria/core'; +import clsx from 'clsx'; import DataGrid, { SelectColumn, textEditor } from '../../src'; -import type { Column, CopyEvent, FillEvent, PasteEvent } from '../../src'; +import type { CalculatedColumn, Column, CopyPasteEvent, FillEvent } from '../../src'; import { textEditorClassname } from '../../src/editors/textEditor'; import { useDirection } from '../directionContext'; @@ -22,6 +23,8 @@ const highlightClassname = css` } `; +const copiedRowClassname = css``; + export interface Row { id: string; avatar: string; @@ -171,18 +174,22 @@ function AllFeatures() { const initialRows = Route.useLoaderData(); const [rows, setRows] = useState(initialRows); const [selectedRows, setSelectedRows] = useState((): ReadonlySet => new Set()); + const [copiedCell, setCopiedCell] = useState<{ row: Row; column: CalculatedColumn } | null>( + null + ); function handleFill({ columnKey, sourceRow, targetRow }: FillEvent): Row { return { ...targetRow, [columnKey]: sourceRow[columnKey as keyof Row] }; } - function handlePaste({ - sourceColumnKey, - sourceRow, - targetColumnKey, - targetRow - }: PasteEvent): Row { - if (sourceColumnKey === undefined || sourceRow === undefined) return targetRow; + function handlePaste({ row, column }: CopyPasteEvent): Row { + if (!copiedCell) { + return row; + } + + const sourceColumnKey = copiedCell.column.key; + const sourceRow = copiedCell.row; + const targetColumnKey = column.key; const incompatibleColumns = ['email', 'zipCode', 'date']; if ( @@ -192,42 +199,74 @@ function AllFeatures() { incompatibleColumns.includes(sourceColumnKey)) && sourceColumnKey !== targetColumnKey) ) { - return targetRow; + return row; } - return { ...targetRow, [targetColumnKey]: sourceRow[sourceColumnKey as keyof Row] }; + return { ...row, [targetColumnKey]: sourceRow[sourceColumnKey as keyof Row] }; } - function handleCopy({ sourceRow, sourceColumnKey }: CopyEvent): void { - if (window.isSecureContext) { - navigator.clipboard.writeText(sourceRow[sourceColumnKey as keyof Row]); + function handleCopy( + { row, column }: CopyPasteEvent, + event: React.ClipboardEvent + ): void { + // copy highlighted text only + if (window.getSelection()?.isCollapsed === false) { + setCopiedCell(null); + return; } + + setCopiedCell({ row, column }); + event.clipboardData.setData('text/plain', row[column.key as keyof Row]); + event.preventDefault(); } return ( - row.id === 'id_2'} - onSelectedRowsChange={setSelectedRows} - className="fill-grid" - rowClass={(row, index) => - row.id.includes('7') || index === 0 ? highlightClassname : undefined - } - direction={direction} - onCellClick={(args, event) => { - if (args.column.key === 'title') { - event.preventGridDefault(); - args.selectCell(true); - } - }} - /> + <> + {copiedCell && ( + + )} + row.id === 'id_2'} + onSelectedRowsChange={setSelectedRows} + className="fill-grid" + rowClass={(row, index) => { + return clsx({ + [highlightClassname]: row.id.includes('7') || index === 0, + [copiedRowClassname]: copiedCell?.row === row + }); + }} + direction={direction} + onCellClick={(args, event) => { + if (args.column.key === 'title') { + event.preventGridDefault(); + args.selectCell(true); + } + }} + onCellKeyDown={(_, event) => { + if (event.key === 'Escape') { + setCopiedCell(null); + } + }} + /> + ); } From 2eab97fb15c5fa0952df30790b7b7f9e7bc8f3ce Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 12 Dec 2024 16:34:58 -0600 Subject: [PATCH 4/5] Rename `onCopy/Paste` -> `onCellCopy/Paste` --- src/DataGrid.tsx | 34 ++++++++++++++--------------- src/index.ts | 2 +- src/types.ts | 2 +- test/browser/TreeDataGrid.test.tsx | 4 ++-- test/browser/copyPaste.test.tsx | 4 ++-- website/routes/AllFeatures.lazy.tsx | 12 +++++----- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index da14f70786..25f9f2e0c8 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -34,6 +34,7 @@ import type { CalculatedColumn, CellClickArgs, CellClipboardEvent, + CellCopyPasteEvent, CellKeyboardEvent, CellKeyDownArgs, CellMouseEvent, @@ -41,7 +42,6 @@ import type { CellSelectArgs, Column, ColumnOrColumnGroup, - CopyPasteEvent, Direction, FillEvent, Maybe, @@ -161,12 +161,6 @@ export interface DataGridProps extends Sha onSortColumnsChange?: Maybe<(sortColumns: SortColumn[]) => void>; defaultColumnOptions?: Maybe, NoInfer>>; onFill?: Maybe<(event: FillEvent>) => NoInfer>; - onCopy?: Maybe< - (args: CopyPasteEvent, NoInfer>, event: CellClipboardEvent) => void - >; - onPaste?: Maybe< - (args: CopyPasteEvent, NoInfer>, event: CellClipboardEvent) => NoInfer - >; /** * Event props @@ -186,6 +180,12 @@ export interface DataGridProps extends Sha onCellKeyDown?: Maybe< (args: CellKeyDownArgs, NoInfer>, event: CellKeyboardEvent) => void >; + onCellCopy?: Maybe< + (args: CellCopyPasteEvent, NoInfer>, event: CellClipboardEvent) => void + >; + onCellPaste?: Maybe< + (args: CellCopyPasteEvent, NoInfer>, event: CellClipboardEvent) => NoInfer + >; /** Function called whenever cell selection is changed */ onSelectedCellChange?: Maybe<(args: CellSelectArgs, NoInfer>) => void>; /** Called when the grid is scrolled */ @@ -251,8 +251,8 @@ function DataGrid( onColumnResize, onColumnsReorder, onFill, - onCopy, - onPaste, + onCellCopy, + onCellPaste, // Toggles and modes enableVirtualization: rawEnableVirtualization, // Miscellaneous @@ -647,20 +647,20 @@ function DataGrid( updateRow(columns[selectedPosition.idx], selectedPosition.rowIdx, selectedPosition.row); } - function handleCopy(event: CellClipboardEvent) { + function handleCellCopy(event: CellClipboardEvent) { if (!selectedCellIsWithinViewportBounds) return; const { idx, rowIdx } = selectedPosition; - onCopy?.({ row: rows[rowIdx], column: columns[idx] }, event); + onCellCopy?.({ row: rows[rowIdx], column: columns[idx] }, event); } - function handlePaste(event: CellClipboardEvent) { - if (!onPaste || !onRowsChange || !isCellEditable(selectedPosition)) { + function handleCellPaste(event: CellClipboardEvent) { + if (!onCellPaste || !onRowsChange || !isCellEditable(selectedPosition)) { return; } const { idx, rowIdx } = selectedPosition; const column = columns[idx]; - const updatedRow = onPaste({ row: rows[rowIdx], column }, event); + const updatedRow = onCellPaste({ row: rows[rowIdx], column }, event); updateRow(column, rowIdx, updatedRow); } @@ -679,7 +679,7 @@ function DataGrid( return; } - if (isCellEditable(selectedPosition) && isDefaultCellInput(event, onPaste != null)) { + if (isCellEditable(selectedPosition) && isDefaultCellInput(event, onCellPaste != null)) { setSelectedPosition(({ idx, rowIdx }) => ({ idx, rowIdx, @@ -1083,8 +1083,8 @@ function DataGrid( ref={gridRef} onScroll={handleScroll} onKeyDown={handleKeyDown} - onCopy={handleCopy} - onPaste={handlePaste} + onCopy={handleCellCopy} + onPaste={handleCellPaste} data-testid={testId} > diff --git a/src/index.ts b/src/index.ts index a3845ef26e..be00221294 100644 --- a/src/index.ts +++ b/src/index.ts @@ -29,7 +29,7 @@ export type { SelectHeaderRowEvent, SelectRowEvent, FillEvent, - CopyPasteEvent, + CellCopyPasteEvent, SortDirection, SortColumn, ColSpanArgs, diff --git a/src/types.ts b/src/types.ts index b588593692..b1130e876d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -249,7 +249,7 @@ export interface FillEvent { targetRow: TRow; } -export interface CopyPasteEvent { +export interface CellCopyPasteEvent { row: TRow; column: CalculatedColumn; } diff --git a/test/browser/TreeDataGrid.test.tsx b/test/browser/TreeDataGrid.test.tsx index c5073b4617..8744e564c5 100644 --- a/test/browser/TreeDataGrid.test.tsx +++ b/test/browser/TreeDataGrid.test.tsx @@ -99,7 +99,7 @@ function TestGrid({ groupBy }: { groupBy: string[] }) { (): ReadonlySet => new Set([]) ); - function onPaste(event: PasteEvent) { + function onCellPaste(event: PasteEvent) { return { ...event.targetRow, [event.targetColumnKey]: event.sourceRow[event.sourceColumnKey as keyof Row] @@ -120,7 +120,7 @@ function TestGrid({ groupBy }: { groupBy: string[] }) { expandedGroupIds={expandedGroupIds} onExpandedGroupIdsChange={setExpandedGroupIds} onRowsChange={setRows} - onPaste={onPaste} + onCellPaste={onCellPaste} /> ); } diff --git a/test/browser/copyPaste.test.tsx b/test/browser/copyPaste.test.tsx index f78c389f67..520533acfb 100644 --- a/test/browser/copyPaste.test.tsx +++ b/test/browser/copyPaste.test.tsx @@ -70,8 +70,8 @@ function CopyPasteTest({ rows={rows} bottomSummaryRows={bottomSummaryRows} onRowsChange={setRows} - onPaste={onPasteCallback ? onPaste : undefined} - onCopy={onCopyCallback ? onCopySpy : undefined} + onCellPaste={onPasteCallback ? onPaste : undefined} + onCellCopy={onCopyCallback ? onCopySpy : undefined} /> ); } diff --git a/website/routes/AllFeatures.lazy.tsx b/website/routes/AllFeatures.lazy.tsx index db65f9298f..5903819ef9 100644 --- a/website/routes/AllFeatures.lazy.tsx +++ b/website/routes/AllFeatures.lazy.tsx @@ -4,7 +4,7 @@ import { css } from '@linaria/core'; import clsx from 'clsx'; import DataGrid, { SelectColumn, textEditor } from '../../src'; -import type { CalculatedColumn, Column, CopyPasteEvent, FillEvent } from '../../src'; +import type { CalculatedColumn, CellCopyPasteEvent, Column, FillEvent } from '../../src'; import { textEditorClassname } from '../../src/editors/textEditor'; import { useDirection } from '../directionContext'; @@ -182,7 +182,7 @@ function AllFeatures() { return { ...targetRow, [columnKey]: sourceRow[columnKey as keyof Row] }; } - function handlePaste({ row, column }: CopyPasteEvent): Row { + function handleCellPaste({ row, column }: CellCopyPasteEvent): Row { if (!copiedCell) { return row; } @@ -205,8 +205,8 @@ function AllFeatures() { return { ...row, [targetColumnKey]: sourceRow[sourceColumnKey as keyof Row] }; } - function handleCopy( - { row, column }: CopyPasteEvent, + function handleCellCopy( + { row, column }: CellCopyPasteEvent, event: React.ClipboardEvent ): void { // copy highlighted text only @@ -241,8 +241,8 @@ function AllFeatures() { rowKeyGetter={rowKeyGetter} onRowsChange={setRows} onFill={handleFill} - onCopy={handleCopy} - onPaste={handlePaste} + onCellCopy={handleCellCopy} + onCellPaste={handleCellPaste} rowHeight={30} selectedRows={selectedRows} isRowSelectionDisabled={(row) => row.id === 'id_2'} From 459b184254150a20f165da885195b339d834dad1 Mon Sep 17 00:00:00 2001 From: Aman Mahajan Date: Thu, 12 Dec 2024 16:38:49 -0600 Subject: [PATCH 5/5] Cleanup --- src/types.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/types.ts b/src/types.ts index b1130e876d..a0b13a6f19 100644 --- a/src/types.ts +++ b/src/types.ts @@ -169,24 +169,24 @@ export type CellKeyboardEvent = CellEvent>; export type CellClipboardEvent = React.ClipboardEvent; export interface CellClickArgs { - rowIdx: number; - row: TRow; column: CalculatedColumn; + row: TRow; + rowIdx: number; selectCell: (enableEditor?: boolean) => void; } interface SelectCellKeyDownArgs { mode: 'SELECT'; - row: TRow; column: CalculatedColumn; + row: TRow; rowIdx: number; selectCell: (position: Position, enableEditor?: Maybe) => void; } export interface EditCellKeyDownArgs { mode: 'EDIT'; - row: TRow; column: CalculatedColumn; + row: TRow; rowIdx: number; navigate: () => void; onClose: (commitChanges?: boolean, shouldFocusCell?: boolean) => void; @@ -250,8 +250,8 @@ export interface FillEvent { } export interface CellCopyPasteEvent { - row: TRow; column: CalculatedColumn; + row: TRow; } export interface GroupRow {