From 965b800c6b889c53824e30a40a77ab7df0a9a24d Mon Sep 17 00:00:00 2001 From: NaokiTateyama Date: Sat, 24 Feb 2024 13:38:55 +0900 Subject: [PATCH] Implementation of Cell Copy/Paste Events Using Clipboard Events --- README.md | 8 ++-- src/DataGrid.tsx | 71 +++++++++++++++++------------------ src/types.ts | 26 +++++++------ test/copyPaste.test.tsx | 4 +- test/utils.tsx | 11 +----- website/demos/AllFeatures.tsx | 11 ++++-- 6 files changed, 63 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index e131eac6cf..21a26107bd 100644 --- a/README.md +++ b/README.md @@ -186,10 +186,6 @@ A number defining the height of summary rows. ###### `onFill?: Maybe<(event: FillEvent) => R>` -###### `onCopy?: Maybe<(event: CopyEvent) => void>` - -###### `onPaste?: Maybe<(event: PasteEvent) => R>` - ###### `onCellClick?: Maybe<(args: CellClickArgs, event: CellMouseEvent) => void>` ###### `onCellDoubleClick?: Maybe<(args: CellClickArgs, event: CellMouseEvent) => void>` @@ -198,6 +194,10 @@ A number defining the height of summary rows. ###### `onCellKeyDown?: Maybe<(args: CellKeyDownArgs, event: CellKeyboardEvent) => void>` +###### `onCopy?: Maybe<(args: CellCopyArgs, event: CellClipboardEvent) => void>` + +###### `onPaste?: Maybe<(args: CellPasteArgs, event: CellClipboardEvent) => R>` + ###### `onSelectedCellChange?: Maybe<(args: CellSelectArgs) => void>;` Triggered when the selected cell is changed. diff --git a/src/DataGrid.tsx b/src/DataGrid.tsx index 59f1fc682f..435378644e 100644 --- a/src/DataGrid.tsx +++ b/src/DataGrid.tsx @@ -31,18 +31,19 @@ import { import type { CalculatedColumn, CellClickArgs, + CellClipboardEvent, + CellCopyArgs, CellKeyboardEvent, CellKeyDownArgs, CellMouseEvent, CellNavigationMode, + CellPasteArgs, CellSelectArgs, Column, ColumnOrColumnGroup, - CopyEvent, Direction, FillEvent, Maybe, - PasteEvent, Position, Renderers, RowsChangeData, @@ -154,8 +155,6 @@ export interface DataGridProps extends Sha onSortColumnsChange?: Maybe<(sortColumns: SortColumn[]) => void>; defaultColumnOptions?: Maybe>; onFill?: Maybe<(event: FillEvent) => R>; - onCopy?: Maybe<(event: CopyEvent) => void>; - onPaste?: Maybe<(event: PasteEvent) => R>; /** * Event props @@ -167,6 +166,10 @@ export interface DataGridProps extends Sha /** Function called whenever a cell is right clicked */ onCellContextMenu?: Maybe<(args: CellClickArgs, event: CellMouseEvent) => void>; onCellKeyDown?: Maybe<(args: CellKeyDownArgs, event: CellKeyboardEvent) => void>; + /** Function called whenever a copy event occurs on a cell */ + onCopy?: Maybe<(args: CellCopyArgs, event: CellClipboardEvent) => void>; + /** Function called whenever a paste event occurs on a cell */ + onPaste?: Maybe<(args: CellPasteArgs, event: CellClipboardEvent) => R>; /** Function called whenever cell selection is changed */ onSelectedCellChange?: Maybe<(args: CellSelectArgs) => void>; /** Called when the grid is scrolled */ @@ -547,29 +550,6 @@ function DataGrid( const isRowEvent = isTreeGrid && event.target === focusSinkRef.current; if (!isCellEvent && !isRowEvent) return; - 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); @@ -617,16 +597,26 @@ function DataGrid( updateRow(columns[selectedPosition.idx], selectedPosition.rowIdx, selectedPosition.row); } - function handleCopy() { + function handleCopy(event: React.ClipboardEvent) { + if ((!onPaste && !onCopy) || !selectedCellIsWithinViewportBounds) return; const { idx, rowIdx } = selectedPosition; const sourceRow = rows[rowIdx]; const sourceColumnKey = columns[idx].key; + const cellEvent = createCellEvent(event); + onCopy?.({ sourceRow, sourceColumnKey }, cellEvent); + if (cellEvent.isGridDefaultPrevented()) return; + setCopiedCell({ row: sourceRow, columnKey: sourceColumnKey }); - onCopy?.({ sourceRow, sourceColumnKey }); } - function handlePaste() { - if (!onPaste || !onRowsChange || copiedCell === null || !isCellEditable(selectedPosition)) { + function handlePaste(event: React.ClipboardEvent) { + if ( + !onPaste || + !onRowsChange || + copiedCell === null || + !isCellEditable(selectedPosition) || + !selectedCellIsWithinViewportBounds + ) { return; } @@ -634,12 +624,17 @@ function DataGrid( const targetColumn = columns[idx]; const targetRow = rows[rowIdx]; - const updatedTargetRow = onPaste({ - sourceRow: copiedCell.row, - sourceColumnKey: copiedCell.columnKey, - targetRow, - targetColumnKey: targetColumn.key - }); + const cellEvent = createCellEvent(event); + const updatedTargetRow = onPaste( + { + sourceRow: copiedCell.row, + sourceColumnKey: copiedCell.columnKey, + targetRow, + targetColumnKey: targetColumn.key + }, + cellEvent + ); + if (cellEvent.isGridDefaultPrevented()) return; updateRow(targetColumn, rowIdx, updatedTargetRow); } @@ -1076,6 +1071,8 @@ function DataGrid( ref={gridRef} onScroll={handleScroll} onKeyDown={handleKeyDown} + onCopy={handleCopy} + onPaste={handlePaste} data-testid={testId} > diff --git a/src/types.ts b/src/types.ts index 703ec14d0d..f7de48b441 100644 --- a/src/types.ts +++ b/src/types.ts @@ -166,6 +166,8 @@ export type CellMouseEvent = CellEvent>; export type CellKeyboardEvent = CellEvent>; +export type CellClipboardEvent = CellEvent>; + export interface CellClickArgs { row: TRow; column: CalculatedColumn; @@ -193,6 +195,18 @@ export type CellKeyDownArgs = | SelectCellKeyDownArgs | EditCellKeyDownArgs; +export interface CellCopyArgs { + sourceColumnKey: string; + sourceRow: TRow; +} + +export interface CellPasteArgs { + sourceColumnKey: string; + sourceRow: TRow; + targetColumnKey: string; + targetRow: TRow; +} + export interface CellSelectArgs { rowIdx: number; row: TRow; @@ -241,18 +255,6 @@ export interface FillEvent { targetRow: TRow; } -export interface CopyEvent { - sourceColumnKey: string; - sourceRow: TRow; -} - -export interface PasteEvent { - sourceColumnKey: string; - sourceRow: TRow; - targetColumnKey: string; - targetRow: TRow; -} - export interface GroupRow { readonly childRows: readonly TRow[]; readonly id: string; diff --git a/test/copyPaste.test.tsx b/test/copyPaste.test.tsx index a68559184c..03e43d2d6e 100644 --- a/test/copyPaste.test.tsx +++ b/test/copyPaste.test.tsx @@ -100,7 +100,7 @@ test('should allow copy if only onCopy is specified', async () => { await userEvent.click(getCellsAtRowIndex(0)[0]); copySelectedCell(); expect(getSelectedCell()).toHaveClass(copyCellClassName); - expect(onCopySpy).toHaveBeenCalledWith({ + expect(onCopySpy.mock.calls[0][0]).toStrictEqual({ sourceRow: initialRows[0], sourceColumnKey: 'col' }); @@ -127,7 +127,7 @@ test('should allow copy/paste if both onPaste & onCopy is specified', async () = await userEvent.click(getCellsAtRowIndex(0)[0]); copySelectedCell(); expect(getSelectedCell()).toHaveClass(copyCellClassName); - expect(onCopySpy).toHaveBeenCalledWith({ + expect(onCopySpy.mock.calls[0][0]).toStrictEqual({ sourceRow: initialRows[0], sourceColumnKey: 'col' }); diff --git a/test/utils.tsx b/test/utils.tsx index 172dff18e2..b7b1aa6a9c 100644 --- a/test/utils.tsx +++ b/test/utils.tsx @@ -68,19 +68,12 @@ export function validateCellPosition(columnIdx: number, rowIdx: number) { } export function copySelectedCell() { - // eslint-disable-next-line testing-library/prefer-user-event - fireEvent.keyDown(document.activeElement!, { - keyCode: '67', - ctrlKey: true - }); + fireEvent.copy(document.activeElement!); } export function pasteSelectedCell() { // eslint-disable-next-line testing-library/prefer-user-event - fireEvent.keyDown(document.activeElement!, { - keyCode: '86', - ctrlKey: true - }); + fireEvent.paste(document.activeElement!); } export async function scrollGrid({ diff --git a/website/demos/AllFeatures.tsx b/website/demos/AllFeatures.tsx index 2cf0df1ff0..149113a9c8 100644 --- a/website/demos/AllFeatures.tsx +++ b/website/demos/AllFeatures.tsx @@ -4,6 +4,7 @@ import { css } from '@linaria/core'; import DataGrid, { SelectColumn, textEditor } from '../../src'; import type { Column, CopyEvent, FillEvent, PasteEvent } from '../../src'; +import { type CellClipboardEvent } from '../../src/types'; import { renderAvatar, renderDropdown } from './renderers'; import type { Props } from './types'; @@ -189,10 +190,12 @@ export default function AllFeatures({ direction }: Props) { return { ...targetRow, [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( + { sourceRow, sourceColumnKey }: CopyEvent, + event: CellClipboardEvent + ): void { + event.preventDefault(); + event.clipboardData.setData('text/plain', sourceRow[sourceColumnKey as keyof Row]); } return (