Skip to content

Commit 382a20e

Browse files
authored
feat: onRowAppended and onColumnAppended (#1062)
* feat: onRowAppended and onColumnAppended * fix onRowAppended spelling * fix: fallback to updateSelectedCell if onRowAppended or onColumnAppended not defined * fix spelling in changelog * docs: appendColumn and onColumnAppended
1 parent 6c463f7 commit 382a20e

File tree

7 files changed

+260
-63
lines changed

7 files changed

+260
-63
lines changed

packages/core/API.md

Lines changed: 85 additions & 46 deletions
Large diffs are not rendered by default.

packages/core/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ Want to add more vertical space to the vertical scrolling area? Simply set the d
467467

468468
### **Arbitrary insertion into Grid**
469469

470-
The `onRowAppeneded` callback has been augmented to allow returning the index of the blank row that has been inserted into the data model. This allows for more varied insertion stories. Shout out to @pzcfg!
470+
The `onRowAppended` callback has been augmented to allow returning the index of the blank row that has been inserted into the data model. This allows for more varied insertion stories. Shout out to @pzcfg!
471471

472472
### **ScrollTo now supported**
473473

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

Lines changed: 111 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ export interface DataEditorProps extends Props, Pick<DataGridSearchProps, "image
218218
* @group Editing
219219
*/
220220
readonly onRowAppended?: () => Promise<"top" | "bottom" | number | undefined> | void;
221+
/** Emitted whenever a column append operation is requested. Append location can be set in callback.
222+
* @group Editing
223+
*/
224+
readonly onColumnAppended?: () => Promise<"left" | "right" | number | undefined> | void;
221225
/** Emitted when a column header should show a context menu. Usually right click.
222226
* @group Events
223227
*/
@@ -705,6 +709,12 @@ export interface DataEditorRef {
705709
* @returns A promise which waits for the append to complete.
706710
*/
707711
appendRow: (col: number, openOverlay?: boolean, behavior?: ScrollBehavior) => Promise<void>;
712+
/**
713+
* Programatically appends a column.
714+
* @param row The row index to focus in the new column.
715+
* @returns A promise which waits for the append to complete.
716+
*/
717+
appendColumn: (row: number, openOverlay?: boolean) => Promise<void>;
708718
/**
709719
* Triggers cells to redraw.
710720
*/
@@ -744,7 +754,7 @@ const loadingCell: GridCell = {
744754
allowOverlay: false,
745755
};
746756

747-
const emptyGridSelection: GridSelection = {
757+
export const emptyGridSelection: GridSelection = {
748758
columns: CompactSelection.empty(),
749759
rows: CompactSelection.empty(),
750760
current: undefined,
@@ -808,6 +818,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
808818
keybindings: keybindingsIn,
809819
editOnType = true,
810820
onRowAppended,
821+
onColumnAppended,
811822
onColumnMoved,
812823
validateCell: validateCellIn,
813824
highlightRegions: highlightRegionsIn,
@@ -929,7 +940,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
929940
const rowMarkerWidth = rowMarkerWidthRaw ?? (rowsIn > 10_000 ? 48 : rowsIn > 1000 ? 44 : rowsIn > 100 ? 36 : 32);
930941
const hasRowMarkers = rowMarkers !== "none";
931942
const rowMarkerOffset = hasRowMarkers ? 1 : 0;
932-
const showTrailingBlankRow = onRowAppended !== undefined;
943+
const showTrailingBlankRow = trailingRowOptions !== undefined;
933944
const lastRowSticky = trailingRowOptions?.sticky === true;
934945

935946
const [showSearchInner, setShowSearchInner] = React.useState(false);
@@ -1649,10 +1660,16 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
16491660

16501661
const focusCallback = React.useRef(focusOnRowFromTrailingBlankRow);
16511662
const getCellContentRef = React.useRef(getCellContent);
1652-
const rowsRef = React.useRef(rows);
1663+
16531664
focusCallback.current = focusOnRowFromTrailingBlankRow;
16541665
getCellContentRef.current = getCellContent;
1666+
1667+
const rowsRef = React.useRef(rows);
16551668
rowsRef.current = rows;
1669+
1670+
const colsRef = React.useRef(mangledCols.length);
1671+
colsRef.current = mangledCols.length;
1672+
16561673
const appendRow = React.useCallback(
16571674
async (col: number, openOverlay: boolean = true, behavior?: ScrollBehavior): Promise<void> => {
16581675
const c = mangledCols[col];
@@ -1710,6 +1727,57 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
17101727
[mangledCols, onRowAppended, rowMarkerOffset, rows, setCurrent]
17111728
);
17121729

1730+
const appendColumn = React.useCallback(
1731+
async (row: number, openOverlay: boolean = true): Promise<void> => {
1732+
const appendResult = onColumnAppended?.();
1733+
1734+
let r: "left" | "right" | number | undefined = undefined;
1735+
let right = true;
1736+
if (appendResult !== undefined) {
1737+
r = await appendResult;
1738+
if (r === "left") right = false;
1739+
if (typeof r === "number") right = false;
1740+
}
1741+
1742+
let backoff = 0;
1743+
const doFocus = () => {
1744+
if (colsRef.current <= mangledCols.length) {
1745+
if (backoff < 500) {
1746+
window.setTimeout(doFocus, backoff);
1747+
}
1748+
backoff = 50 + backoff * 2;
1749+
return;
1750+
}
1751+
1752+
const col = typeof r === "number" ? r : right ? mangledCols.length : 0;
1753+
scrollTo(col - rowMarkerOffset, row);
1754+
setCurrent(
1755+
{
1756+
cell: [col, row],
1757+
range: {
1758+
x: col,
1759+
y: row,
1760+
width: 1,
1761+
height: 1,
1762+
},
1763+
},
1764+
false,
1765+
false,
1766+
"edit"
1767+
);
1768+
1769+
const cell = getCellContentRef.current([col - rowMarkerOffset, row]);
1770+
if (cell.allowOverlay && isReadWriteCell(cell) && cell.readonly !== true && openOverlay) {
1771+
window.setTimeout(() => {
1772+
focusCallback.current(col, row);
1773+
}, 0);
1774+
}
1775+
};
1776+
doFocus();
1777+
},
1778+
[mangledCols, onColumnAppended, rowMarkerOffset, scrollTo, setCurrent]
1779+
);
1780+
17131781
const getCustomNewRowTargetColumn = React.useCallback(
17141782
(col: number): number | undefined => {
17151783
const customTargetColumn =
@@ -2991,14 +3059,29 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
29913059

29923060
const [movX, movY] = movement;
29933061
if (gridSelection.current !== undefined && (movX !== 0 || movY !== 0)) {
2994-
const isEditingTrailingRow =
2995-
gridSelection.current.cell[1] === mangledRows - 1 && newValue !== undefined;
2996-
updateSelectedCell(
2997-
clamp(gridSelection.current.cell[0] + movX, 0, mangledCols.length - 1),
2998-
clamp(gridSelection.current.cell[1] + movY, 0, mangledRows - 1),
2999-
isEditingTrailingRow,
3000-
false
3001-
);
3062+
const isEditingLastRow = gridSelection.current.cell[1] === mangledRows - 1 && newValue !== undefined;
3063+
const isEditingLastCol =
3064+
gridSelection.current.cell[0] === mangledCols.length - 1 && newValue !== undefined;
3065+
let updateSelected = true;
3066+
if (isEditingLastRow && movY === 1 && onRowAppended !== undefined) {
3067+
updateSelected = false;
3068+
const col = gridSelection.current.cell[0] + movX;
3069+
const customTargetColumn = getCustomNewRowTargetColumn(col);
3070+
void appendRow(customTargetColumn ?? col, false);
3071+
}
3072+
if (isEditingLastCol && movX === 1 && onColumnAppended !== undefined) {
3073+
updateSelected = false;
3074+
const row = gridSelection.current.cell[1] + movY;
3075+
void appendColumn(row, false);
3076+
}
3077+
if (updateSelected) {
3078+
updateSelectedCell(
3079+
clamp(gridSelection.current.cell[0] + movX, 0, mangledCols.length - 1),
3080+
clamp(gridSelection.current.cell[1] + movY, 0, mangledRows - 1),
3081+
isEditingLastRow,
3082+
false
3083+
);
3084+
}
30023085
}
30033086
onFinishedEditing?.(newValue, movement);
30043087
},
@@ -3011,6 +3094,11 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
30113094
mangledRows,
30123095
updateSelectedCell,
30133096
mangledCols.length,
3097+
appendRow,
3098+
appendColumn,
3099+
onRowAppended,
3100+
onColumnAppended,
3101+
getCustomNewRowTargetColumn,
30143102
]
30153103
);
30163104

@@ -3862,6 +3950,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
38623950
forwardedRef,
38633951
() => ({
38643952
appendRow: (col: number, openOverlay?: boolean) => appendRow(col + rowMarkerOffset, openOverlay),
3953+
appendColumn: (row: number, openOverlay?: boolean) => appendColumn(row, openOverlay),
38653954
updateCells: damageList => {
38663955
if (rowMarkerOffset !== 0) {
38673956
damageList = damageList.map(x => ({ cell: [x.cell[0] + rowMarkerOffset, x.cell[1]] }));
@@ -3971,7 +4060,17 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
39714060
};
39724061
},
39734062
}),
3974-
[appendRow, normalSizeColumn, scrollRef, onCopy, onKeyDown, onPasteInternal, rowMarkerOffset, scrollTo]
4063+
[
4064+
appendRow,
4065+
appendColumn,
4066+
normalSizeColumn,
4067+
scrollRef,
4068+
onCopy,
4069+
onKeyDown,
4070+
onPasteInternal,
4071+
rowMarkerOffset,
4072+
scrollTo,
4073+
]
39754074
);
39764075

39774076
const [selCol, selRow] = currentCell ?? [];

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export { useColumnSizer } from "./data-editor/use-column-sizer.js";
4545
export type { DataEditorRef } from "./data-editor/data-editor.js";
4646
export { DataEditorAll as DataEditor } from "./data-editor-all.js";
4747
export type { DataEditorAllProps as DataEditorProps } from "./data-editor-all.js";
48+
export { emptyGridSelection } from "./data-editor/data-editor.js";
4849

4950
export { DataEditor as DataEditorCore } from "./data-editor/data-editor.js";
5051
export type { DataEditorProps as DataEditorCoreProps } from "./data-editor/data-editor.js";

packages/core/test/data-editor-input.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ const EventedDataEditor = React.forwardRef<DataEditorRef, DataEditorProps>((p, r
199199
[p]
200200
);
201201

202-
const onRowAppened = React.useCallback(() => {
202+
const onRowAppended = React.useCallback(() => {
203203
setExtraRows(cv => cv + 1);
204204
void p.onRowAppended?.();
205205
}, [p]);
@@ -211,7 +211,7 @@ const EventedDataEditor = React.forwardRef<DataEditorRef, DataEditorProps>((p, r
211211
gridSelection={sel}
212212
onGridSelectionChange={onGridSelectionChange}
213213
rows={p.rows + extraRows}
214-
onRowAppended={p.onRowAppended === undefined ? undefined : onRowAppened}
214+
onRowAppended={p.onRowAppended === undefined ? undefined : onRowAppended}
215215
/>
216216
);
217217
});

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2962,6 +2962,42 @@ describe("data-editor", () => {
29622962
expect(Element.prototype.scrollTo).toHaveBeenCalled();
29632963
});
29642964

2965+
test("appendRow ref without trailing row", async () => {
2966+
const spy = vi.fn();
2967+
const ref = React.createRef<DataEditorRef>();
2968+
vi.useFakeTimers();
2969+
render(
2970+
<EventedDataEditor {...basicProps} onRowAppended={spy} ref={ref} trailingRowOptions={undefined} />,
2971+
{
2972+
wrapper: Context,
2973+
}
2974+
);
2975+
prep();
2976+
2977+
await act(async () => {
2978+
await ref.current?.appendRow(1, false);
2979+
});
2980+
2981+
expect(spy).toHaveBeenCalled();
2982+
});
2983+
2984+
test("appendColumn ref", async () => {
2985+
const spy = vi.fn();
2986+
const ref = React.createRef<DataEditorRef>();
2987+
vi.useFakeTimers();
2988+
render(
2989+
<EventedDataEditor {...basicProps} onColumnAppended={spy} ref={ref} trailingRowOptions={undefined} />,
2990+
{ wrapper: Context }
2991+
);
2992+
prep();
2993+
2994+
await act(async () => {
2995+
await ref.current?.appendColumn(0, false);
2996+
});
2997+
2998+
expect(spy).toHaveBeenCalled();
2999+
});
3000+
29653001
test("Click row marker", async () => {
29663002
const spy = vi.fn();
29673003
vi.useFakeTimers();

packages/core/test/test-utils.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ export const Context: React.FC = p => {
251251
export const EventedDataEditor = React.forwardRef<DataEditorRef, DataEditorProps>((p, ref) => {
252252
const [sel, setSel] = React.useState<GridSelection | undefined>(p.gridSelection);
253253
const [extraRows, setExtraRows] = React.useState(0);
254+
const [extraCols, setExtraCols] = React.useState(0);
254255

255256
const onGridSelectionChange = React.useCallback(
256257
(s: GridSelection) => {
@@ -260,19 +261,40 @@ export const EventedDataEditor = React.forwardRef<DataEditorRef, DataEditorProps
260261
[p]
261262
);
262263

263-
const onRowAppened = React.useCallback(() => {
264+
const onRowAppended = React.useCallback(() => {
264265
setExtraRows(cv => cv + 1);
265266
void p.onRowAppended?.();
266267
}, [p]);
267268

269+
const onColumnAppended = React.useCallback(() => {
270+
setExtraCols(cv => cv + 1);
271+
void p.onColumnAppended?.();
272+
}, [p]);
273+
274+
const columns = React.useMemo(() => p.columns.concat(Array.from({ length: extraCols }, (_, i) => ({ title: `Z${i}`, width: 50 }))), [p.columns, extraCols]);
275+
276+
const getCellContent = React.useCallback(
277+
(cell: Item): GridCell => {
278+
const [c] = cell;
279+
if (c >= p.columns.length) {
280+
return { kind: GridCellKind.Text, allowOverlay: true, data: "", displayData: "" };
281+
}
282+
return p.getCellContent(cell);
283+
},
284+
[p, p.getCellContent]
285+
);
286+
268287
return (
269288
<DataEditor
270289
{...p}
290+
columns={columns}
291+
getCellContent={getCellContent}
271292
ref={ref}
272293
gridSelection={sel}
273294
onGridSelectionChange={onGridSelectionChange}
274295
rows={p.rows + extraRows}
275-
onRowAppended={p.onRowAppended === undefined ? undefined : onRowAppened}
296+
onRowAppended={p.onRowAppended === undefined ? undefined : onRowAppended}
297+
onColumnAppended={p.onColumnAppended === undefined ? undefined : onColumnAppended}
276298
/>
277299
);
278300
});

0 commit comments

Comments
 (0)