Skip to content

Commit 92bba1d

Browse files
authored
Add optional hover effectto text cell and allow overriding the acitivation behavior at a per cell level (#890)
* Add optional hover effectto text cell and allow overriding the acitivation behavior at a per cell level * Fix build
1 parent 0673b91 commit 92bba1d

File tree

10 files changed

+85
-14
lines changed

10 files changed

+85
-14
lines changed

packages/core/src/cells/cell-types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ interface BaseCellRenderer<T extends InnerGridCell> {
6060
readonly kind: T["kind"];
6161
readonly draw: DrawCallback<T>;
6262
readonly drawPrep?: PrepCallback;
63-
readonly needsHover?: boolean;
63+
readonly needsHover?: boolean | ((cell: T) => boolean);
6464
readonly needsHoverPosition?: boolean;
6565
readonly measure?: (ctx: CanvasRenderingContext2D, cell: T, theme: FullTheme) => number;
6666

packages/core/src/cells/text-cell.tsx

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,55 @@
11
/* eslint-disable react/display-name */
22
import * as React from "react";
33
import { GrowingEntry } from "../internal/growing-entry/growing-entry.js";
4-
import { drawTextCell, prepTextCell } from "../internal/data-grid/render/data-grid-lib.js";
4+
import {
5+
drawTextCell,
6+
measureTextCached,
7+
prepTextCell,
8+
roundedRect,
9+
} from "../internal/data-grid/render/data-grid-lib.js";
510
import { GridCellKind, type TextCell } from "../internal/data-grid/data-grid-types.js";
611
import type { InternalCellRenderer } from "./cell-types.js";
12+
import { withAlpha } from "../internal/data-grid/color-parser.js";
713

814
export const textCellRenderer: InternalCellRenderer<TextCell> = {
915
getAccessibilityString: c => c.data?.toString() ?? "",
1016
kind: GridCellKind.Text,
11-
needsHover: false,
17+
needsHover: textCell => textCell.hoverEffect === true,
1218
needsHoverPosition: false,
1319
drawPrep: prepTextCell,
1420
useLabel: true,
15-
draw: a => (drawTextCell(a, a.cell.displayData, a.cell.contentAlign, a.cell.allowWrapping, a.hyperWrapping), true),
21+
draw: a => {
22+
const { cell, hoverAmount, hyperWrapping, ctx, rect, theme, overrideCursor } = a;
23+
const { displayData, contentAlign, hoverEffect, allowWrapping } = cell;
24+
if (hoverEffect === true && hoverAmount > 0) {
25+
ctx.textBaseline = "alphabetic";
26+
const padX = theme.cellHorizontalPadding;
27+
const padY = theme.cellVerticalPadding;
28+
const m = measureTextCached(displayData, ctx, theme.baseFontFull, "alphabetic");
29+
const maxH = rect.height - padY;
30+
const h = Math.min(maxH, m.actualBoundingBoxAscent * 2.5);
31+
ctx.beginPath();
32+
roundedRect(
33+
ctx,
34+
rect.x + padX / 2,
35+
rect.y + (rect.height - h) / 2 + 1,
36+
m.width + padX * 3,
37+
h - 1,
38+
theme.roundingRadius ?? 4
39+
);
40+
ctx.globalAlpha = hoverAmount;
41+
ctx.fillStyle = withAlpha(theme.textDark, 0.1);
42+
ctx.fill();
43+
44+
// restore
45+
ctx.globalAlpha = 1;
46+
ctx.fillStyle = theme.textDark;
47+
ctx.textBaseline = "middle";
48+
49+
overrideCursor?.("text");
50+
}
51+
drawTextCell(a, displayData, contentAlign, allowWrapping, hyperWrapping);
52+
},
1653
measure: (ctx, cell, t) => {
1754
const lines = cell.displayData.split("\n", cell.allowWrapping === true ? undefined : 1);
1855
let maxLineWidth = 0;

packages/core/src/cells/uri-cell.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function getTextRect(
4141
export const uriCellRenderer: InternalCellRenderer<UriCell> = {
4242
getAccessibilityString: c => c.data?.toString() ?? "",
4343
kind: GridCellKind.Uri,
44-
needsHover: true,
44+
needsHover: uriCell => uriCell.hoverEffect === true,
4545
needsHoverPosition: true,
4646
useLabel: true,
4747
drawPrep: prepTextCell,

packages/core/src/data-editor/data-editor.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
BooleanIndeterminate,
3232
type FillHandleDirection,
3333
type EditListItem,
34+
type CellActiviationBehavior,
3435
} from "../internal/data-grid/data-grid-types.js";
3536
import DataGridSearch, { type DataGridSearchProps } from "../internal/data-grid-search/data-grid-search.js";
3637
import { browserIsOSX } from "../common/browser-detect.js";
@@ -622,7 +623,7 @@ export interface DataEditorProps extends Props, Pick<DataGridSearchProps, "image
622623
* Determines when a cell is considered activated and will emit the `onCellActivated` event. Generally an activated
623624
* cell will open to edit mode.
624625
*/
625-
readonly cellActivationBehavior?: "double-click" | "single-click" | "second-click";
626+
readonly cellActivationBehavior?: CellActiviationBehavior;
626627

627628
/**
628629
* Controls if focus will trap inside the data grid when doing tab and caret navigation.
@@ -2223,7 +2224,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
22232224
if (isPrevented.current || gridSelection.current === undefined) return false;
22242225

22252226
let shouldActivate = false;
2226-
switch (cellActivationBehavior) {
2227+
switch (c.activationBehaviorOverride ?? cellActivationBehavior) {
22272228
case "double-click":
22282229
case "second-click": {
22292230
if (mouse?.previousSelection?.current?.cell === undefined) break;

packages/core/src/docs/examples/cell-activated-event.stories.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
defaultProps,
1010
useAllMockedKinds,
1111
} from "../../data-editor/stories/utils.js";
12-
import type { Item } from "../../internal/data-grid/data-grid-types.js";
12+
import type { GridCell, Item } from "../../internal/data-grid/data-grid-types.js";
1313
import { SimpleThemeWrapper } from "../../stories/story-utils.js";
1414
import type { DataEditorCoreProps } from "../../index.js";
1515

@@ -28,6 +28,21 @@ export default {
2828
export const CellActivatedEvent: React.VFC<Pick<DataEditorCoreProps, "cellActivationBehavior">> = p => {
2929
const { cols, getCellContent, onColumnResize, setCellValue } = useAllMockedKinds();
3030

31+
const getCellContentMangled = React.useCallback(
32+
(item: Item): GridCell => {
33+
const result = getCellContent(item);
34+
if (item[0] === 3) {
35+
return {
36+
...result,
37+
activationBehaviorOverride: "single-click",
38+
hoverEffect: true,
39+
} as any;
40+
}
41+
return result;
42+
},
43+
[getCellContent]
44+
);
45+
3146
const [lastActivated, setLastActivated] = React.useState<Item | undefined>(undefined);
3247

3348
const onCellActivated = React.useCallback((cell: Item) => {
@@ -51,8 +66,9 @@ export const CellActivatedEvent: React.VFC<Pick<DataEditorCoreProps, "cellActiva
5166
}>
5267
<DataEditor
5368
{...defaultProps}
69+
// editorBloom={[-1, -4]}
5470
cellActivationBehavior={p.cellActivationBehavior}
55-
getCellContent={getCellContent}
71+
getCellContent={getCellContentMangled}
5672
//initialSize={[849, 967]}
5773
//scrollOffsetY={10_000}
5874
getCellsForSelection={true}

packages/core/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ export { markerCellRenderer } from "./cells/marker-cell.js";
6161
export { bubbleCellRenderer } from "./cells/bubble-cell.js";
6262
export { protectedCellRenderer } from "./cells/protected-cell.js";
6363
export { rowIDCellRenderer } from "./cells/row-id-cell.js";
64+
export { AllCellRenderers } from "./cells/index.js";
65+
export { sprites } from "./internal/data-grid/sprites.js";
66+
export { default as ImageWindowLoaderImpl } from "./common/image-window-loader.js";
6467
export * from "./data-editor/copy-paste.js";
6568

6669
/**

packages/core/src/internal/data-grid/data-grid-types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,8 @@ export function isRectangleEqual(a: Rectangle | undefined, b: Rectangle | undefi
299299
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
300300
}
301301

302+
export type CellActiviationBehavior = "double-click" | "single-click" | "second-click";
303+
302304
/** @category Cells */
303305
export interface BaseGridCell {
304306
readonly allowOverlay: boolean;
@@ -309,6 +311,7 @@ export interface BaseGridCell {
309311
readonly contentAlign?: "left" | "right" | "center";
310312
readonly cursor?: CSSProperties["cursor"];
311313
readonly copyData?: string;
314+
readonly activationBehaviorOverride?: CellActiviationBehavior;
312315
}
313316

314317
/** @category Cells */
@@ -331,6 +334,7 @@ export interface TextCell extends BaseGridCell {
331334
readonly data: string;
332335
readonly readonly?: boolean;
333336
readonly allowWrapping?: boolean;
337+
readonly hoverEffect?: boolean;
334338
}
335339

336340
/** @category Cells */

packages/core/src/internal/data-grid/data-grid.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,9 +1198,10 @@ const DataGrid: React.ForwardRefRenderFunction<DataGridRef, DataGridProps> = (p,
11981198
}
11991199
const cell = getCellContent(hoveredItem as [number, number], true);
12001200
const r = getCellRenderer(cell);
1201-
am.setHovered(
1202-
(r === undefined && cell.kind === GridCellKind.Custom) || r?.needsHover === true ? hoveredItem : undefined
1203-
);
1201+
const cellNeedsHover =
1202+
(r === undefined && cell.kind === GridCellKind.Custom) ||
1203+
(r?.needsHover !== undefined && (typeof r.needsHover === "boolean" ? r.needsHover : r.needsHover(cell)));
1204+
am.setHovered(cellNeedsHover ? hoveredItem : undefined);
12041205
}, [getCellContent, getCellRenderer, hoveredItem]);
12051206

12061207
const hoveredRef = React.useRef<GridMouseEventArgs>();
@@ -1236,6 +1237,7 @@ const DataGrid: React.ForwardRefRenderFunction<DataGridRef, DataGridProps> = (p,
12361237
};
12371238

12381239
if (!mouseEventArgsAreEqual(args, hoveredRef.current)) {
1240+
setDrawCursorOverride(undefined);
12391241
onItemHovered?.(args);
12401242
maybeSetHoveredInfo(
12411243
args.kind === outOfBoundsKind ? undefined : [args.location, [args.localEventX, args.localEventY]],

packages/core/src/internal/data-grid/render/data-grid-lib.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,13 @@ function makeCacheKey(
325325
}
326326

327327
/** @category Drawing */
328-
export function measureTextCached(s: string, ctx: CanvasRenderingContext2D, font?: string): TextMetrics {
329-
const key = makeCacheKey(s, ctx, "middle", font);
328+
export function measureTextCached(
329+
s: string,
330+
ctx: CanvasRenderingContext2D,
331+
font?: string,
332+
baseline: "middle" | "alphabetic" = "middle"
333+
): TextMetrics {
334+
const key = makeCacheKey(s, ctx, baseline, font);
330335
let metrics = metricsCache[key];
331336
if (metrics === undefined) {
332337
metrics = ctx.measureText(s);

packages/core/test/data-grid-lib.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ function makeCol(title: string, sourceIndex: number, sticky: boolean, width: num
2727
style: undefined,
2828
themeOverride: undefined,
2929
trailingRowOptions: undefined,
30+
growOffset: undefined,
31+
rowMarker: undefined,
32+
rowMarkerChecked: undefined,
3033
};
3134
}
3235

0 commit comments

Comments
 (0)