Skip to content

feat: cell activation info #1069

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
15 changes: 12 additions & 3 deletions packages/core/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -624,13 +624,14 @@ export type ProvideEditorCallbackResult<T extends InnerGridCell> =
| undefined;

export type ProvideEditorCallback<T extends InnerGridCell> = (
cell: T & { location?: Item }
cell: T & { location?: Item; activation?: CellActivatedEventArgs }
) => ProvideEditorCallbackResult<T>;

provideEditor?: ProvideEditorCallback<GridCell>;
```

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.

---

Expand Down Expand Up @@ -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"`).

---

Expand Down
45 changes: 33 additions & 12 deletions packages/core/src/data-editor/data-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -241,7 +242,7 @@ export interface DataEditorProps extends Props, Pick<DataGridSearchProps, "image
/** Emitted when a cell is activated, by pressing Enter, Space or double clicking it.
* @group Events
*/
readonly onCellActivated?: (cell: Item) => void;
readonly onCellActivated?: (cell: Item, event: CellActivatedEventArgs) => void;

/**
* Emitted whenever the user initiats a pattern fill using the fill handle. This event provides both
Expand Down Expand Up @@ -663,7 +664,7 @@ export interface DataEditorProps extends Props, Pick<DataGridSearchProps, "image
* Determines when a cell is considered activated and will emit the `onCellActivated` event. Generally an activated
* cell will open to edit mode.
*/
readonly cellActivationBehavior?: CellActiviationBehavior;
readonly cellActivationBehavior?: CellActivationBehavior;

/**
* Controls if focus will trap inside the data grid when doing tab and caret navigation.
Expand Down Expand Up @@ -771,6 +772,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
cell: Item;
highlight: boolean;
forceEditMode: boolean;
activation: CellActivatedEventArgs;
}>();
const searchInputRef = React.useRef<HTMLInputElement | null>(null);
const canvasRef = React.useRef<HTMLCanvasElement | null>(null);
Expand Down Expand Up @@ -1431,7 +1433,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
);

const reselect = React.useCallback(
(bounds: Rectangle, fromKeyboard: boolean, initialValue?: string) => {
(bounds: Rectangle, activation: CellActivatedEventArgs, initialValue?: string) => {
if (gridSelection.current === undefined) return;

const [col, row] = gridSelection.current.cell;
Expand Down Expand Up @@ -1466,8 +1468,9 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
cell: [col, row],
highlight: initialValue === undefined,
forceEditMode: initialValue !== undefined,
activation,
});
} else if (c.kind === GridCellKind.Boolean && fromKeyboard && c.readonly !== true) {
} else if (c.kind === GridCellKind.Boolean && activation.inputType === "keyboard" && c.readonly !== true) {
mangledOnCellsEdited([
{
location: gridSelection.current.cell,
Expand Down Expand Up @@ -1502,6 +1505,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
highlight: true,
cell: [col, row],
forceEditMode: true,
activation: { inputType: "keyboard", key: "Enter" },
});
},
[getMangledCellContent, scrollRef, setOverlaySimple]
Expand Down Expand Up @@ -2058,6 +2062,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
mapper,
lastRowSticky,
setCurrent,
headerRowMarkerDisabled,
setSelectedColumns,
setGridSelection,
onSelectionCleared,
Expand Down Expand Up @@ -2387,8 +2392,16 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
}
}
if (shouldActivate) {
onCellActivated?.([col - rowMarkerOffset, row]);
reselect(a.bounds, false);
const act = a.isDoubleClick === true
? "double-click"
: (c.activationBehaviorOverride ?? cellActivationBehavior);
const activationEvent: CellActivatedEventArgs = {
inputType: "pointer",
pointerActivation: act,
pointerType: a.isTouch ? "touch" : "mouse",
};
onCellActivated?.([col - rowMarkerOffset, row], activationEvent);
reselect(a.bounds, activationEvent);
return true;
}
}
Expand Down Expand Up @@ -3265,8 +3278,12 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
void appendRow(customTargetColumn ?? col);
}, 0);
} else {
onCellActivated?.([col - rowMarkerOffset, row]);
reselect(bounds, true);
const activationEvent: CellActivatedEventArgs = {
inputType: "keyboard",
key: event.key,
};
onCellActivated?.([col - rowMarkerOffset, row], activationEvent);
reselect(bounds, activationEvent);
}
} else if (gridSelection.current.range.height > 1 && isHotkey(keys.downFill, event, details)) {
fillDown();
Expand Down Expand Up @@ -3477,8 +3494,12 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
) {
return;
}
onCellActivated?.([col - rowMarkerOffset, row]);
reselect(event.bounds, true, event.key);
const activationEvent: CellActivatedEventArgs = {
inputType: "keyboard",
key: event.key,
};
onCellActivated?.([col - rowMarkerOffset, row], activationEvent);
reselect(event.bounds, activationEvent, event.key);
event.stopPropagation();
event.preventDefault();
}
Expand Down Expand Up @@ -3543,7 +3564,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
formatted?: string | string[]
): EditListItem | undefined {
const stringifiedRawValue =
typeof rawValue === "object" ? rawValue?.join("\n") ?? "" : rawValue?.toString() ?? "";
typeof rawValue === "object" ? (rawValue?.join("\n") ?? "") : (rawValue?.toString() ?? "");

if (!isInnerOnlyCell(inner) && isReadWriteCell(inner) && inner.readonly !== true) {
const coerced = coercePasteValue?.(stringifiedRawValue, inner);
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ export * from "./internal/data-grid/data-grid-types.js";
export type {
BaseGridMouseEventArgs,
CellClickedEventArgs,
CellActivatedEventArgs,
KeyboardCellActivatedEvent,
PointerCellActivatedEvent,
DragHandler,
FillPatternEventArgs,
GridDragEventArgs,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
type Rectangle,
type ValidatedGridCell,
} from "../data-grid/data-grid-types.js";

import type { CellActivatedEventArgs } from "../data-grid/event-args.js";
import { DataGridOverlayEditorStyle } from "./data-grid-overlay-editor-style.js";
import type { OverlayImageEditorProps } from "./private/image-overlay-editor.js";
import { useStayOnScreen } from "./use-stay-on-screen.js";
Expand All @@ -39,6 +41,7 @@ interface DataGridOverlayEditorProps {
readonly getCellRenderer: GetCellRendererCallback;
readonly markdownDivCreateNode?: (content: string) => DocumentFragment;
readonly provideEditor?: ProvideEditorCallback<GridCell>;
readonly activation: CellActivatedEventArgs;
readonly validateCell?: (
cell: Item,
newValue: EditableGridCell,
Expand Down Expand Up @@ -69,6 +72,7 @@ const DataGridOverlayEditor: React.FunctionComponent<DataGridOverlayEditorProps>
provideEditor,
isOutsideClick,
customEventTarget,
activation,
} = p;

const [tempValue, setTempValueRaw] = React.useState<GridCell | undefined>(forceEditMode ? content : undefined);
Expand Down Expand Up @@ -160,13 +164,14 @@ const DataGridOverlayEditor: React.FunctionComponent<DataGridOverlayEditorProps>

const [editorProvider, useLabel] = React.useMemo((): [ProvideEditorCallbackResult<GridCell>, 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();

Expand All @@ -187,6 +192,7 @@ const DataGridOverlayEditor: React.FunctionComponent<DataGridOverlayEditorProps>
<CustomEditor
portalElementRef={portalElementRef}
isHighlighted={highlight}
activation={activation}
onChange={setTempValue}
value={targetValue}
initialValue={initialValue}
Expand Down
9 changes: 5 additions & 4 deletions packages/core/src/internal/data-grid/data-grid-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { Theme } from "../../common/styles.js";
import { assertNever, proveType } from "../../common/support.js";
import type { OverlayImageEditorProps } from "../data-grid-overlay-editor/private/image-overlay-editor.js";
import type { SpriteManager } from "./data-grid-sprites.js";
import type { BaseGridMouseEventArgs } from "./event-args.js";
import type { BaseGridMouseEventArgs, CellActivatedEventArgs } from "./event-args.js";
import type { ImageWindowLoader } from "./image-window-loader-interface.js";

// Thoughts:
Expand Down Expand Up @@ -305,7 +305,7 @@ export function isRectangleEqual(a: Rectangle | undefined, b: Rectangle | undefi
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
}

export type CellActiviationBehavior = "double-click" | "single-click" | "second-click";
export type CellActivationBehavior = "double-click" | "single-click" | "second-click";

/** @category Cells */
export interface BaseGridCell {
Expand All @@ -317,7 +317,7 @@ export interface BaseGridCell {
readonly contentAlign?: "left" | "right" | "center";
readonly cursor?: CSSProperties["cursor"];
readonly copyData?: string;
readonly activationBehaviorOverride?: CellActiviationBehavior;
readonly activationBehaviorOverride?: CellActivationBehavior;
}

/** @category Cells */
Expand Down Expand Up @@ -396,6 +396,7 @@ export type ProvideEditorComponent<T extends InnerGridCell> = React.FunctionComp
readonly isValid?: boolean;
readonly theme: Theme;
readonly portalElementRef?: React.RefObject<HTMLElement>;
readonly activation: CellActivatedEventArgs;
}>;

type ObjectEditorCallbackResult<T extends InnerGridCell> = {
Expand Down Expand Up @@ -424,7 +425,7 @@ export function isObjectEditorCallbackResult<T extends InnerGridCell>(

/** @category Renderers */
export type ProvideEditorCallback<T extends InnerGridCell> = (
cell: T & { location?: Item }
cell: T & { location?: Item; activation?: CellActivatedEventArgs }
) => ProvideEditorCallbackResult<T>;

/** @category Cells */
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/internal/data-grid/data-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1712,7 +1712,7 @@ const DataGrid: React.ForwardRefRenderFunction<DataGridRef, DataGridProps> = (p,
return getMouseArgsForPosition(canvasRef.current, posX, posY, ev);
}
}),
[canvasRef, damage, getBoundsForItem]
[canvasRef, damage, getBoundsForItem, getMouseArgsForPosition]
);

const lastFocusedSubdomNode = React.useRef<Item>();
Expand Down
22 changes: 21 additions & 1 deletion packages/core/src/internal/data-grid/event-args.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
Loading