Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use native copy/paste event handlers #3667

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions src/Cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
`;

Expand All @@ -30,7 +18,6 @@ function Cell<R, SR>(
column,
colSpan,
isCellSelected,
isCopied,
isDraggedOver,
row,
rowIdx,
Expand All @@ -51,7 +38,6 @@ function Cell<R, SR>(
className = getCellClassname(
column,
{
[cellCopiedClassname]: isCopied,
[cellDraggedOverClassname]: isDraggedOver
},
typeof cellClass === 'function' ? cellClass(row) : cellClass,
Expand Down
80 changes: 22 additions & 58 deletions src/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,18 @@ import {
import type {
CalculatedColumn,
CellClickArgs,
CellClipboardEvent,
CellCopyPasteEvent,
CellKeyboardEvent,
CellKeyDownArgs,
CellMouseEvent,
CellNavigationMode,
CellSelectArgs,
Column,
ColumnOrColumnGroup,
CopyEvent,
Direction,
FillEvent,
Maybe,
PasteEvent,
Position,
Renderers,
RowsChangeData,
Expand Down Expand Up @@ -161,8 +161,6 @@ export interface DataGridProps<R, SR = unknown, K extends Key = Key> extends Sha
onSortColumnsChange?: Maybe<(sortColumns: SortColumn[]) => void>;
defaultColumnOptions?: Maybe<DefaultColumnOptions<NoInfer<R>, NoInfer<SR>>>;
onFill?: Maybe<(event: FillEvent<NoInfer<R>>) => NoInfer<R>>;
onCopy?: Maybe<(event: CopyEvent<NoInfer<R>>) => void>;
onPaste?: Maybe<(event: PasteEvent<NoInfer<R>>) => NoInfer<R>>;

/**
* Event props
Expand All @@ -182,6 +180,12 @@ export interface DataGridProps<R, SR = unknown, K extends Key = Key> extends Sha
onCellKeyDown?: Maybe<
(args: CellKeyDownArgs<NoInfer<R>, NoInfer<SR>>, event: CellKeyboardEvent) => void
>;
onCellCopy?: Maybe<
(args: CellCopyPasteEvent<NoInfer<R>, NoInfer<SR>>, event: CellClipboardEvent) => void
>;
onCellPaste?: Maybe<
(args: CellCopyPasteEvent<NoInfer<R>, NoInfer<SR>>, event: CellClipboardEvent) => NoInfer<R>
>;
/** Function called whenever cell selection is changed */
onSelectedCellChange?: Maybe<(args: CellSelectArgs<NoInfer<R>, NoInfer<SR>>) => void>;
/** Called when the grid is scrolled */
Expand Down Expand Up @@ -247,8 +251,8 @@ function DataGrid<R, SR, K extends Key>(
onColumnResize,
onColumnsReorder,
onFill,
onCopy,
onPaste,
onCellCopy,
onCellPaste,
// Toggles and modes
enableVirtualization: rawEnableVirtualization,
// Miscellaneous
Expand Down Expand Up @@ -296,7 +300,6 @@ function DataGrid<R, SR, K extends Key>(
const [measuredColumnWidths, setMeasuredColumnWidths] = useState(
(): ReadonlyMap<string, number> => new Map()
);
const [copiedCell, setCopiedCell] = useState<{ row: R; columnKey: string } | null>(null);
const [isDragging, setDragging] = useState(false);
const [draggedOverRowIdx, setOverRowIdx] = useState<number | undefined>(undefined);
const [scrollToPosition, setScrollToPosition] = useState<PartialPosition | null>(null);
Expand Down Expand Up @@ -594,39 +597,13 @@ function DataGrid<R, SR, K extends Key>(
);
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;

// 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);
return;
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowLeft':
Expand Down Expand Up @@ -670,31 +647,21 @@ function DataGrid<R, SR, K extends Key>(
updateRow(columns[selectedPosition.idx], selectedPosition.rowIdx, selectedPosition.row);
}

function handleCopy() {
function handleCellCopy(event: CellClipboardEvent) {
if (!selectedCellIsWithinViewportBounds) return;
const { idx, rowIdx } = selectedPosition;
const sourceRow = rows[rowIdx];
const sourceColumnKey = columns[idx].key;
setCopiedCell({ row: sourceRow, columnKey: sourceColumnKey });
onCopy?.({ sourceRow, sourceColumnKey });
onCellCopy?.({ row: rows[rowIdx], column: columns[idx] }, event);
}

function handlePaste() {
if (!onPaste || !onRowsChange || copiedCell === null || !isCellEditable(selectedPosition)) {
function handleCellPaste(event: CellClipboardEvent) {
if (!onCellPaste || !onRowsChange || !isCellEditable(selectedPosition)) {
return;
}

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
});

updateRow(targetColumn, rowIdx, updatedTargetRow);
const column = columns[idx];
const updatedRow = onCellPaste({ row: rows[rowIdx], column }, event);
updateRow(column, rowIdx, updatedRow);
}

function handleCellInput(event: KeyboardEvent<HTMLDivElement>) {
Expand All @@ -712,7 +679,7 @@ function DataGrid<R, SR, K extends Key>(
return;
}

if (isCellEditable(selectedPosition) && isDefaultCellInput(event)) {
if (isCellEditable(selectedPosition) && isDefaultCellInput(event, onCellPaste != null)) {
setSelectedPosition(({ idx, rowIdx }) => ({
idx,
rowIdx,
Expand Down Expand Up @@ -1037,11 +1004,6 @@ function DataGrid<R, SR, K extends Key>(
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,
Expand Down Expand Up @@ -1121,6 +1083,8 @@ function DataGrid<R, SR, K extends Key>(
ref={gridRef}
onScroll={handleScroll}
onKeyDown={handleKeyDown}
onCopy={handleCellCopy}
onPaste={handleCellPaste}
data-testid={testId}
>
<DataGridDefaultRenderersProvider value={defaultGridComponents}>
Expand Down
2 changes: 0 additions & 2 deletions src/Row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ function Row<R, SR>(
selectedCellIdx,
isRowSelectionDisabled,
isRowSelected,
copiedCellIdx,
draggedOverCellIdx,
lastFrozenColumnIndex,
row,
Expand Down Expand Up @@ -75,7 +74,6 @@ function Row<R, SR>(
colSpan,
row,
rowIdx,
isCopied: copiedCellIdx === idx,
isDraggedOver: draggedOverCellIdx === idx,
isCellSelected,
onClick: onCellClick,
Expand Down
10 changes: 1 addition & 9 deletions src/TreeDataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 } from './utils';
import type {
CellKeyboardEvent,
CellKeyDownArgs,
Expand Down Expand Up @@ -321,12 +321,6 @@ function TreeDataGrid<R, SR, K extends Key>(
selectCell({ idx, rowIdx: parentRowAndIndex[1] });
}
}

// Prevent copy/paste on group rows
// eslint-disable-next-line @typescript-eslint/no-deprecated
if (isCtrlKeyHeldDown(event) && (event.keyCode === 67 || event.keyCode === 86)) {
event.preventGridDefault();
}
}

function handleRowsChange(updatedRows: R[], { indexes, column }: RowsChangeData<R, SR>) {
Expand Down Expand Up @@ -364,7 +358,6 @@ function TreeDataGrid<R, SR, K extends Key>(
onCellContextMenu,
onRowChange,
lastFrozenColumnIndex,
copiedCellIdx,
draggedOverCellIdx,
setDraggedOverRowIdx,
selectedCellEditor,
Expand Down Expand Up @@ -403,7 +396,6 @@ function TreeDataGrid<R, SR, K extends Key>(
onCellContextMenu,
onRowChange,
lastFrozenColumnIndex,
copiedCellIdx,
draggedOverCellIdx,
setDraggedOverRowIdx,
selectedCellEditor
Expand Down
3 changes: 1 addition & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ export type {
SelectHeaderRowEvent,
SelectRowEvent,
FillEvent,
CopyEvent,
PasteEvent,
CellCopyPasteEvent,
SortDirection,
SortColumn,
ColSpanArgs,
Expand Down
25 changes: 9 additions & 16 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,6 @@ export interface CellRendererProps<TRow, TSummaryRow>
> {
column: CalculatedColumn<TRow, TSummaryRow>;
colSpan: number | undefined;
isCopied: boolean;
isDraggedOver: boolean;
isCellSelected: boolean;
onClick: RenderRowProps<TRow, TSummaryRow>['onCellClick'];
Expand All @@ -167,25 +166,27 @@ export type CellMouseEvent = CellEvent<React.MouseEvent<HTMLDivElement>>;

export type CellKeyboardEvent = CellEvent<React.KeyboardEvent<HTMLDivElement>>;

export type CellClipboardEvent = React.ClipboardEvent<HTMLDivElement>;

export interface CellClickArgs<TRow, TSummaryRow = unknown> {
rowIdx: number;
row: TRow;
column: CalculatedColumn<TRow, TSummaryRow>;
row: TRow;
rowIdx: number;
selectCell: (enableEditor?: boolean) => void;
}

interface SelectCellKeyDownArgs<TRow, TSummaryRow = unknown> {
mode: 'SELECT';
row: TRow;
column: CalculatedColumn<TRow, TSummaryRow>;
row: TRow;
rowIdx: number;
selectCell: (position: Position, enableEditor?: Maybe<boolean>) => void;
}

export interface EditCellKeyDownArgs<TRow, TSummaryRow = unknown> {
mode: 'EDIT';
row: TRow;
column: CalculatedColumn<TRow, TSummaryRow>;
row: TRow;
rowIdx: number;
navigate: () => void;
onClose: (commitChanges?: boolean, shouldFocusCell?: boolean) => void;
Expand Down Expand Up @@ -220,7 +221,6 @@ export interface RenderRowProps<TRow, TSummaryRow = unknown>
extends BaseRenderRowProps<TRow, TSummaryRow> {
row: TRow;
lastFrozenColumnIndex: number;
copiedCellIdx: number | undefined;
draggedOverCellIdx: number | undefined;
selectedCellEditor: ReactElement<RenderEditCellProps<TRow>> | undefined;
onRowChange: (column: CalculatedColumn<TRow, TSummaryRow>, rowIdx: number, newRow: TRow) => void;
Expand Down Expand Up @@ -249,16 +249,9 @@ export interface FillEvent<TRow> {
targetRow: TRow;
}

export interface CopyEvent<TRow> {
sourceColumnKey: string;
sourceRow: TRow;
}

export interface PasteEvent<TRow> {
sourceColumnKey: string;
sourceRow: TRow;
targetColumnKey: string;
targetRow: TRow;
export interface CellCopyPasteEvent<TRow, TSummaryRow = unknown> {
column: CalculatedColumn<TRow, TSummaryRow>;
row: TRow;
}

export interface GroupRow<TRow> {
Expand Down
12 changes: 9 additions & 3 deletions src/utils/keyboardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,16 @@ export function isCtrlKeyHeldDown(e: React.KeyboardEvent): boolean {
return (e.ctrlKey || e.metaKey) && e.key !== 'Control';
}

export function isDefaultCellInput(event: React.KeyboardEvent<HTMLDivElement>): 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
const vKey = 86;

export function isDefaultCellInput(
event: React.KeyboardEvent<HTMLDivElement>,
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);
}

Expand Down
4 changes: 2 additions & 2 deletions test/browser/TreeDataGrid.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { SelectColumn, textEditor, TreeDataGrid } from '../../src';
import { focusSinkClassname } from '../../src/style/core';
import { rowSelected } from '../../src/style/row';
import type { PasteEvent } from '../../src/types';

Check failure on line 9 in test/browser/TreeDataGrid.test.tsx

View workflow job for this annotation

GitHub Actions / test (18)

Module '"../../src/types"' has no exported member 'PasteEvent'.

Check failure on line 9 in test/browser/TreeDataGrid.test.tsx

View workflow job for this annotation

GitHub Actions / test (19)

Module '"../../src/types"' has no exported member 'PasteEvent'.
import {
copySelectedCellOld,
getCellsAtRowIndexOld,
Expand Down Expand Up @@ -99,7 +99,7 @@
(): ReadonlySet<unknown> => new Set<unknown>([])
);

function onPaste(event: PasteEvent<Row>) {
function onCellPaste(event: PasteEvent<Row>) {
return {
...event.targetRow,
[event.targetColumnKey]: event.sourceRow[event.sourceColumnKey as keyof Row]
Expand All @@ -120,7 +120,7 @@
expandedGroupIds={expandedGroupIds}
onExpandedGroupIdsChange={setExpandedGroupIds}
onRowsChange={setRows}
onPaste={onPaste}
onCellPaste={onCellPaste}
/>
);
}
Expand Down
4 changes: 2 additions & 2 deletions test/browser/copyPaste.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { page, userEvent } from '@vitest/browser/context';

import DataGrid from '../../src';
import type { Column, PasteEvent } from '../../src';

Check failure on line 5 in test/browser/copyPaste.test.tsx

View workflow job for this annotation

GitHub Actions / test (18)

Module '"../../src"' has no exported member 'PasteEvent'. Did you mean to use 'import PasteEvent from "../../src"' instead?

Check failure on line 5 in test/browser/copyPaste.test.tsx

View workflow job for this annotation

GitHub Actions / test (19)

Module '"../../src"' has no exported member 'PasteEvent'. Did you mean to use 'import PasteEvent from "../../src"' instead?
import {
copySelectedCell,
getCellsAtRowIndexOld,
Expand Down Expand Up @@ -70,8 +70,8 @@
rows={rows}
bottomSummaryRows={bottomSummaryRows}
onRowsChange={setRows}
onPaste={onPasteCallback ? onPaste : undefined}
onCopy={onCopyCallback ? onCopySpy : undefined}
onCellPaste={onPasteCallback ? onPaste : undefined}
onCellCopy={onCopyCallback ? onCopySpy : undefined}
/>
);
}
Expand Down
Loading
Loading