diff --git a/packages/core/API.md b/packages/core/API.md index 00e18d738..b0e174b20 100644 --- a/packages/core/API.md +++ b/packages/core/API.md @@ -624,13 +624,14 @@ export type ProvideEditorCallbackResult = | undefined; export type ProvideEditorCallback = ( - cell: T & { location?: Item } + cell: T & { location?: Item; activation?: CellActivatedEventArgs } ) => ProvideEditorCallbackResult; provideEditor?: ProvideEditorCallback; ``` When provided the `provideEditor` callbacks job is to be a constructor for functional components which have the correct properties to be used by the data grid as an editor. The editor must implement `onChange` and `onFinishedEditing` callbacks as well support the `isHighlighted` flag which tells the editor to begin with any editable text pre-selected so typing will immediately begin to overwrite it. +The `cell` passed to this callback includes a `location` of the activated cell and an `activation` event describing how the editor was opened. --- @@ -1186,10 +1187,18 @@ onCellClicked?: (cell: Item) => void; ## onCellActivated ```ts -onCellActivated?: (cell: Item) => void; +onCellActivated?: ( + cell: Item, + event: CellActivatedEventArgs +) => void; ``` -`onCellActivated` is called whenever the user double clicks, presses Enter or Space, or begins typing with a cell selected. +`onCellActivated` is called whenever the user double clicks, presses Enter or Space, or begins typing with a cell selected. The second argument describes how the activation occurred. + +The `event` parameter is one of: + +- `KeyboardCellActivatedEvent` – contains `inputType: "keyboard"` and a `key` field with the physical key pressed. +- `PointerCellActivatedEvent` – contains `inputType: "pointer"`, a `pointerActivation` reason such as `"double-click"` or `"single-click"`, and an optional `pointerType` (`"mouse"`, `"touch"`, or `"pen"`). --- diff --git a/packages/core/src/data-editor/data-editor.tsx b/packages/core/src/data-editor/data-editor.tsx index de75d8bac..32a42a625 100644 --- a/packages/core/src/data-editor/data-editor.tsx +++ b/packages/core/src/data-editor/data-editor.tsx @@ -31,7 +31,7 @@ import { BooleanIndeterminate, type FillHandleDirection, type EditListItem, - type CellActiviationBehavior, + type CellActivationBehavior, } from "../internal/data-grid/data-grid-types.js"; import DataGridSearch, { type DataGridSearchProps } from "../internal/data-grid-search/data-grid-search.js"; import { browserIsOSX } from "../common/browser-detect.js"; @@ -78,6 +78,7 @@ import { type GridDragEventArgs, mouseEventArgsAreEqual, type GridKeyEventArgs, + type CellActivatedEventArgs, } from "../internal/data-grid/event-args.js"; import { type Keybinds, useKeybindingsWithDefaults } from "./data-editor-keybindings.js"; import type { Highlight } from "../internal/data-grid/render/data-grid-render.cells.js"; @@ -241,7 +242,7 @@ export interface DataEditorProps extends Props, Pick void; + readonly onCellActivated?: (cell: Item, event: CellActivatedEventArgs) => void; /** * Emitted whenever the user initiats a pattern fill using the fill handle. This event provides both @@ -663,7 +664,7 @@ export interface DataEditorProps extends Props, Pick(); const searchInputRef = React.useRef(null); const canvasRef = React.useRef(null); @@ -1431,7 +1433,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction { + (bounds: Rectangle, activation: CellActivatedEventArgs, initialValue?: string) => { if (gridSelection.current === undefined) return; const [col, row] = gridSelection.current.cell; @@ -1466,8 +1468,9 @@ const DataEditorImpl: React.ForwardRefRenderFunction 1 && isHotkey(keys.downFill, event, details)) { fillDown(); @@ -3477,8 +3494,12 @@ const DataEditorImpl: React.ForwardRefRenderFunction DocumentFragment; readonly provideEditor?: ProvideEditorCallback; + readonly activation: CellActivatedEventArgs; readonly validateCell?: ( cell: Item, newValue: EditableGridCell, @@ -69,6 +72,7 @@ const DataGridOverlayEditor: React.FunctionComponent provideEditor, isOutsideClick, customEventTarget, + activation, } = p; const [tempValue, setTempValueRaw] = React.useState(forceEditMode ? content : undefined); @@ -160,13 +164,14 @@ const DataGridOverlayEditor: React.FunctionComponent const [editorProvider, useLabel] = React.useMemo((): [ProvideEditorCallbackResult, boolean] | [] => { if (isInnerOnlyCell(content)) return []; - const cellWithLocation = { ...content, location: cell } as GridCell & { + const cellWithLocation = { ...content, location: cell, activation } as GridCell & { location: Item; + activation: CellActivatedEventArgs; }; const external = provideEditor?.(cellWithLocation); if (external !== undefined) return [external, false]; return [getCellRenderer(content)?.provideEditor?.(cellWithLocation), false]; - }, [cell, content, getCellRenderer, provideEditor]); + }, [cell, content, getCellRenderer, provideEditor, activation]); const { ref, style: stayOnScreenStyle } = useStayOnScreen(); @@ -187,6 +192,7 @@ const DataGridOverlayEditor: React.FunctionComponent = React.FunctionComp readonly isValid?: boolean; readonly theme: Theme; readonly portalElementRef?: React.RefObject; + readonly activation?: CellActivatedEventArgs; }>; type ObjectEditorCallbackResult = { @@ -424,7 +425,7 @@ export function isObjectEditorCallbackResult( /** @category Renderers */ export type ProvideEditorCallback = ( - cell: T & { location?: Item } + cell: T & { location?: Item; activation?: CellActivatedEventArgs } ) => ProvideEditorCallbackResult; /** @category Cells */ @@ -561,7 +562,7 @@ export class CompactSelection { static create = (items: CompactSelectionRanges) => { return new CompactSelection(mergeRanges(items)); - } + }; static empty = (): CompactSelection => { return emptyCompactSelection ?? (emptyCompactSelection = new CompactSelection([])); diff --git a/packages/core/src/internal/data-grid/data-grid.tsx b/packages/core/src/internal/data-grid/data-grid.tsx index 18099b731..cf863c4aa 100644 --- a/packages/core/src/internal/data-grid/data-grid.tsx +++ b/packages/core/src/internal/data-grid/data-grid.tsx @@ -1712,7 +1712,7 @@ const DataGrid: React.ForwardRefRenderFunction = (p, return getMouseArgsForPosition(canvasRef.current, posX, posY, ev); } }), - [canvasRef, damage, getBoundsForItem] + [canvasRef, damage, getBoundsForItem, getMouseArgsForPosition] ); const lastFocusedSubdomNode = React.useRef(); diff --git a/packages/core/src/internal/data-grid/event-args.ts b/packages/core/src/internal/data-grid/event-args.ts index 577b62357..7e678d014 100644 --- a/packages/core/src/internal/data-grid/event-args.ts +++ b/packages/core/src/internal/data-grid/event-args.ts @@ -1,4 +1,4 @@ -import type { Item, Rectangle } from "./data-grid-types.js"; +import type { Item, Rectangle, CellActivationBehavior } from "./data-grid-types.js"; /** @category Types */ export interface BaseGridMouseEventArgs { @@ -101,6 +101,26 @@ export interface HeaderClickedEventArgs extends GridMouseHeaderEventArgs, Preven /** @category Types */ export interface GroupHeaderClickedEventArgs extends GridMouseGroupHeaderEventArgs, PreventableEvent {} +export interface BaseCellActivatedEvent {} + +/** Keyboard-initiated activation */ +export interface KeyboardCellActivatedEvent extends BaseCellActivatedEvent { + readonly inputType: "keyboard"; + readonly key: string; +} + +/** Pointer-initiated activation */ +export interface PointerCellActivatedEvent extends BaseCellActivatedEvent { + readonly inputType: "pointer"; + readonly pointerActivation: CellActivationBehavior; + readonly pointerType?: "mouse" | "touch" | "pen"; +} + +/** The public event type the grid emits */ +export type CellActivatedEventArgs = + | KeyboardCellActivatedEvent + | PointerCellActivatedEvent; + export interface FillPatternEventArgs extends PreventableEvent { patternSource: Rectangle; fillDestination: Rectangle; diff --git a/packages/core/test/data-editor.test.tsx b/packages/core/test/data-editor.test.tsx index a9b0a0507..161e2d764 100644 --- a/packages/core/test/data-editor.test.tsx +++ b/packages/core/test/data-editor.test.tsx @@ -12,6 +12,7 @@ import { type InnerGridCell, type InternalCellRenderer, AllCellRenderers, + type ProvideEditorCallback, } from "../src/index.js"; import type { CustomCell } from "../src/internal/data-grid/data-grid-types.js"; import type { DataEditorRef } from "../src/data-editor/data-editor.js"; @@ -441,7 +442,13 @@ describe("data-editor", () => { }); expect(spy).toHaveBeenCalled(); - expect(spy).toHaveBeenCalledWith([1, 1]); + const event = spy.mock.calls[0][1]; + expect(event).toEqual( + expect.objectContaining({ + inputType: "pointer", + pointerActivation: "double-click", + }) + ); }); describe("cellActivationBehavior", () => { @@ -470,7 +477,7 @@ describe("data-editor", () => { }); expect(spy).toHaveBeenCalled(); - expect(spy).toHaveBeenCalledWith([1, 1]); + expect(spy).toHaveBeenCalledWith([1, 1], expect.anything()); }); test("double-click miss", async () => { @@ -525,7 +532,7 @@ describe("data-editor", () => { }); expect(spy).toHaveBeenCalled(); - expect(spy).toHaveBeenCalledWith([1, 1]); + expect(spy).toHaveBeenCalledWith([1, 1], expect.anything()); }); test("single-click", async () => { @@ -548,7 +555,7 @@ describe("data-editor", () => { ); expect(spy).toHaveBeenCalled(); - expect(spy).toHaveBeenCalledWith([1, 1]); + expect(spy).toHaveBeenCalledWith([1, 1], expect.anything()); }); }); @@ -601,7 +608,13 @@ describe("data-editor", () => { }); expect(spy).toHaveBeenCalled(); - expect(spy).toHaveBeenCalledWith([1, 1]); + const event = spy.mock.calls[0][1]; + expect(event).toEqual( + expect.objectContaining({ + inputType: "keyboard", + key: "Enter", + }) + ); }); test("Toggle boolean with Enter key", async () => { @@ -629,7 +642,7 @@ describe("data-editor", () => { }); expect(spy).toHaveBeenCalled(); - expect(spy).toHaveBeenCalledWith([7, 2]); + expect(spy).toHaveBeenCalledWith([7, 2], expect.anything()); expect(editSpy).toHaveBeenCalledWith([7, 2], { allowOverlay: false, data: true, @@ -662,7 +675,7 @@ describe("data-editor", () => { }); expect(spy).toHaveBeenCalled(); - expect(spy).toHaveBeenCalledWith([1, 1]); + expect(spy).toHaveBeenCalledWith([1, 1], expect.anything()); }); test("Emits activated event when typing", async () => { @@ -689,7 +702,7 @@ describe("data-editor", () => { }); expect(spy).toHaveBeenCalled(); - expect(spy).toHaveBeenCalledWith([1, 1]); + expect(spy).toHaveBeenCalledWith([1, 1], expect.anything()); }); test("keyDown and keyUp events include the cell location", async () => { @@ -1530,6 +1543,39 @@ describe("data-editor", () => { expect(document.body.contains(overlay)).toBe(false); }); + test("Editor provider receives activation info", async () => { + const spy = vi.fn(); + const provider: ProvideEditorCallback = cell => { + spy(cell.activation); + return undefined; + }; + + vi.useFakeTimers(); + render( + , + { wrapper: Context } + ); + prep(); + + const canvas = screen.getByTestId("data-grid-canvas"); + sendClick(canvas, { + clientX: 300, + clientY: 36 + 32 + 16, + }); + + sendClick(canvas, { + clientX: 300, + clientY: 36 + 32 + 16, + }); + + expect(spy).toHaveBeenCalledWith( + expect.objectContaining({ + inputType: "pointer", + pointerActivation: "double-click", + }) + ); + }); + test("Send edit", async () => { const spy = vi.fn(); vi.useFakeTimers();