Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
22 changes: 20 additions & 2 deletions packages/core/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1019,10 +1019,28 @@ Highlight regions are regions on the grid which get drawn with a background colo
## fillHandle

```ts
fillHandle?: boolean;
fillHandle?: boolean | Partial<FillHandleConfig>;
```

Controls the visibility of the fill handle used for filling cells with the mouse.
Controls the presence of the fill handle used for filling cells with the mouse.

**Configuration for the fill-handle (the small drag handle that appears in the
bottom-right of the current selection).**

```ts
interface FillHandleConfig {
/** Shape of the handle. Defaults to "square". */
shape?: "square" | "circle";
/** Width/height (or diameter for circles) in CSS pixels. Defaults to `6`. */
size?: number;
/** Horizontal offset from the bottom-right corner of the cell (px). */
offsetX?: number;
/** Vertical offset from the bottom-right corner of the cell (px). */
offsetY?: number;
/** Stroke width (px) of the outline that surrounds the handle. Defaults to `1`. */
outline?: number;
}
```

---

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/docs/examples/fill-handle.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export const FillHandle: React.VFC = () => {
columns={cols}
rowMarkers={"both"}
onPaste={true}
fillHandle={true}
fillHandle={{ shape: "circle", size: 6, offsetX: 0, offsetY: 0, outline: 1 }}
keybindings={{ downFill: true, rightFill: true }}
onCellEdited={setCellValue}
trailingRowOptions={{
Expand Down
29 changes: 29 additions & 0 deletions packages/core/src/internal/data-grid/data-grid-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,35 @@ export type CompactSelectionRanges = readonly Slice[];

export type FillHandleDirection = "horizontal" | "vertical" | "orthogonal" | "any";

/**
* Configuration options for the fill-handle (the little drag square in the bottom-right of a selection).
*
* `shape` – Either a square or a circle. Default is `square`.
* `size` – Width/height (or diameter) in CSS pixels. Default is `6`.
* `offsetX` – Horizontal offset from the bottom-right corner of the cell (positive is →). Default is `0`.
* `offsetY` – Vertical offset from the bottom-right corner of the cell (positive is ↓). Default is `0`.
*/
export type FillHandleConfig = {
readonly shape: "square" | "circle";
readonly size: number;
readonly offsetX: number;
readonly offsetY: number;
readonly outline: number;
}

export type FillHandle = boolean | Partial<FillHandleConfig>;

/**
* Default configuration used when `fillHandle` is simply `true`.
*/
export const DEFAULT_FILL_HANDLE: Readonly<FillHandleConfig> = {
shape: "square",
size: 6,
offsetX: 0,
offsetY: 0,
outline: 1,
} as const;

function mergeRanges(input: CompactSelectionRanges) {
if (input.length === 0) {
return [];
Expand Down
64 changes: 41 additions & 23 deletions packages/core/src/internal/data-grid/data-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
booleanCellIsEditable,
type InnerGridColumn,
type DrawCellCallback,
type FillHandle,
DEFAULT_FILL_HANDLE,
} from "./data-grid-types.js";
import { CellSet } from "./cell-set.js";
import { SpriteManager, type SpriteMap } from "./data-grid-sprites.js";
Expand Down Expand Up @@ -145,7 +147,7 @@ export interface DataGridProps {
* @defaultValue false
* @group Editing
*/
readonly fillHandle: boolean | undefined;
readonly fillHandle: FillHandle | undefined;

readonly disabledRows: CompactSelection | undefined;
/**
Expand Down Expand Up @@ -314,13 +316,15 @@ type DamageUpdateList = readonly {
// newValue: GridCell,
}[];

const fillHandleClickSize = 6;

export interface DataGridRef {
focus: () => void;
getBounds: (col?: number, row?: number) => Rectangle | undefined;
damage: (cells: DamageUpdateList) => void;
getMouseArgsForPosition: (posX: number, posY: number, ev?: MouseEvent | TouchEvent) => GridMouseEventArgs | undefined;
getMouseArgsForPosition: (
posX: number,
posY: number,
ev?: MouseEvent | TouchEvent
) => GridMouseEventArgs | undefined;
}

const getRowData = (cell: InnerGridCell, getCellRenderer?: GetCellRendererCallback) => {
Expand Down Expand Up @@ -446,7 +450,10 @@ const DataGrid: React.ForwardRefRenderFunction<DataGridRef, DataGridProps> = (p,
}, [cellYOffset, cellXOffset, translateX, translateY, enableFirefoxRescaling, enableSafariRescaling]);

const mappedColumns = useMappedColumns(columns, freezeColumns);
const stickyX = React.useMemo(() => fixedShadowX ? getStickyWidth(mappedColumns, dragAndDropState) : 0,[mappedColumns, dragAndDropState, fixedShadowX]);
const stickyX = React.useMemo(
() => (fixedShadowX ? getStickyWidth(mappedColumns, dragAndDropState) : 0),
[mappedColumns, dragAndDropState, fixedShadowX]
);

// row: -1 === columnHeader, -2 === groupHeader
const getBoundsForItem = React.useCallback(
Expand Down Expand Up @@ -653,18 +660,29 @@ const DataGrid: React.ForwardRefRenderFunction<DataGridRef, DataGridProps> = (p,
const isEdge = bounds !== undefined && bounds.x + bounds.width - posX < edgeDetectionBuffer;

let isFillHandle = false;
if (fillHandle && selection.current !== undefined) {
const drawFill = fillHandle !== false && fillHandle !== undefined;
if (drawFill && selection.current !== undefined) {
const fill =
typeof fillHandle === "object"
? { ...DEFAULT_FILL_HANDLE, ...fillHandle }
: DEFAULT_FILL_HANDLE;

const fillHandleClickSize = fill.size;
const half = fillHandleClickSize / 2;

const fillHandleLocation = rectBottomRight(selection.current.range);
const fillHandleCellBounds = getBoundsForItem(canvas, fillHandleLocation[0], fillHandleLocation[1]);
const fillBounds = getBoundsForItem(canvas, fillHandleLocation[0], fillHandleLocation[1]);

if (fillHandleCellBounds !== undefined) {
const handleLogicalCenterX = fillHandleCellBounds.x + fillHandleCellBounds.width - 2;
const handleLogicalCenterY = fillHandleCellBounds.y + fillHandleCellBounds.height - 2;
if (fillBounds !== undefined) {
// Handle center sits exactly on the bottom-right corner of the cell.
// Offset by half pixel to align with grid lines.
const centerX = fillBounds.x + fillBounds.width + fill.offsetX - half + 0.5;
const centerY = fillBounds.y + fillBounds.height + fill.offsetY - half + 0.5;

//check if posX and posY are within fillHandleClickSize from handleLogicalCenter
// Check if posX and posY are within fillHandleClickSize from handleLogicalCenter
isFillHandle =
Math.abs(handleLogicalCenterX - posX) < fillHandleClickSize &&
Math.abs(handleLogicalCenterY - posY) < fillHandleClickSize;
Math.abs(centerX - posX) < fillHandleClickSize &&
Math.abs(centerY - posY) < fillHandleClickSize;
}
}

Expand Down Expand Up @@ -951,15 +969,15 @@ const DataGrid: React.ForwardRefRenderFunction<DataGridRef, DataGridProps> = (p,
const cursor = isDragging
? "grabbing"
: canDrag || isResizing
? "col-resize"
: overFill || isFilling
? "crosshair"
: cursorOverride !== undefined
? cursorOverride
: headerHovered || clickableInnerCellHovered || editableBoolHovered || groupHeaderHovered
? "pointer"
: "default";
? "col-resize"
: overFill || isFilling
? "crosshair"
: cursorOverride !== undefined
? cursorOverride
: headerHovered || clickableInnerCellHovered || editableBoolHovered || groupHeaderHovered
? "pointer"
: "default";

const style = React.useMemo(
() => ({
// width,
Expand Down Expand Up @@ -1716,7 +1734,7 @@ const DataGrid: React.ForwardRefRenderFunction<DataGridRef, DataGridProps> = (p,
}

return getMouseArgsForPosition(canvasRef.current, posX, posY, ev);
}
},
}),
[canvasRef, damage, getBoundsForItem]
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,7 @@ export function drawGrid(arg: DrawGridArg, lastArg: DrawGridArg | undefined) {
const selectionCurrent = selection.current;

if (
fillHandle &&
(fillHandle !== false && fillHandle !== undefined) &&
drawFocus &&
selectionCurrent !== undefined &&
damage.has(rectBottomRight(selectionCurrent.range))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable unicorn/no-for-loop */
import { type GridSelection, type InnerGridCell, type Item } from "../data-grid-types.js";
import { type GridSelection, type InnerGridCell, type Item, type FillHandle, DEFAULT_FILL_HANDLE } from "../data-grid-types.js";
import { getStickyWidth, type MappedGridColumn, computeBounds, getFreezeTrailingHeight } from "./data-grid-lib.js";
import { type FullTheme } from "../../../common/styles.js";
import { blend, withAlpha } from "../color-parser.js";
Expand Down Expand Up @@ -188,11 +188,16 @@ export function drawFillHandle(
getCellContent: (cell: Item) => InnerGridCell,
freezeTrailingRows: number,
hasAppendRow: boolean,
fillHandle: boolean,
fillHandle: FillHandle,
rows: number
): (() => void) | undefined {
if (selectedCell.current === undefined) return undefined;

const drawFill = fillHandle !== false && fillHandle !== undefined;
if (!drawFill) return undefined;

const fill = typeof fillHandle === "object" ? { ...DEFAULT_FILL_HANDLE, ...fillHandle } : DEFAULT_FILL_HANDLE;

const range = selectedCell.current.range;
const currentItem = selectedCell.current.cell;
const fillHandleTarget = [range.x + range.width - 1, range.y + range.height - 1];
Expand Down Expand Up @@ -261,7 +266,7 @@ export function drawFillHandle(
}
}

const doHandle = row === fillHandleRow && isFillHandleCol && fillHandle;
const doHandle = row === fillHandleRow && isFillHandleCol && drawFill;

if (doHandle) {
drawHandleCb = () => {
Expand All @@ -270,10 +275,49 @@ export function drawFillHandle(
ctx.rect(clipX, 0, width - clipX, height);
ctx.clip();
}
// Draw a larger, outlined fill-handle similar to Excel / Google Sheets.
const size = fill.size;
const half = size / 2;

// Place the handle so its center sits exactly on the bottom-right corner of the cell.
// Offset by half pixel to align with grid lines.
const hx = cellX + cellWidth + fill.offsetX - half + 0.5;
const hy = drawY + rh + fill.offsetY - half + 0.5;

ctx.beginPath();
ctx.rect(cellX + cellWidth - 4, drawY + rh - 4, 4, 4);
if (fill.shape === "circle") {
ctx.arc(hx + half, hy + half, half, 0, Math.PI * 2);
} else {
ctx.rect(hx, hy, size, size);
}

// Fill
ctx.fillStyle = col.themeOverride?.accentColor ?? theme.accentColor;
ctx.fill();

// Outline (drawn so it doesn't eat into the filled area)
if (fill.outline > 0) {
ctx.lineWidth = fill.outline;
ctx.strokeStyle = theme.bgCell;
if (fill.shape === "circle") {
ctx.beginPath();
ctx.arc(
hx + half,
hy + half,
half + fill.outline / 2,
0,
Math.PI * 2
);
ctx.stroke();
} else {
ctx.strokeRect(
hx - fill.outline / 2,
hy - fill.outline / 2,
size + fill.outline,
size + fill.outline
);
}
}
};
}
return drawHandleCb !== undefined;
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/internal/data-grid/render/draw-grid-arg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
DrawHeaderCallback,
CellList,
DrawCellCallback,
FillHandle,
} from "../data-grid-types.js";
import type { CellSet } from "../cell-set.js";
import type { EnqueueCallback } from "../use-animation-queue.js";
Expand Down Expand Up @@ -52,7 +53,7 @@ export interface DrawGridArg {
readonly isFocused: boolean;
readonly drawFocus: boolean;
readonly selection: GridSelection;
readonly fillHandle: boolean;
readonly fillHandle: FillHandle;
readonly freezeTrailingRows: number;
readonly hasAppendRow: boolean;
readonly hyperWrapping: boolean;
Expand Down