@youp-grid/react 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/YoupGrid.js CHANGED
@@ -18,6 +18,7 @@ export function YoupGrid(props) {
18
18
  const viewportHeight = normalizeHeight(props.height) ?? 420;
19
19
  const loading = props.loading ?? controller.state.remoteRequest?.status === "loading";
20
20
  const infiniteScrollLoading = props.infiniteScrollLoading ?? loading;
21
+ const headerRef = useRef(null);
21
22
  const bodyRef = useRef(null);
22
23
  const lastRowsEndReachedKeyRef = useRef();
23
24
  const skipNextBlurCommitRef = useRef(false);
@@ -31,6 +32,7 @@ export function YoupGrid(props) {
31
32
  const [columnChooserOpen, setColumnChooserOpen] = useState(false);
32
33
  const [columnMenuOpenId, setColumnMenuOpenId] = useState();
33
34
  const showRowSelectionColumn = props.showRowSelectionColumn ?? false;
35
+ const gridEditable = (props.editable ?? true) && !props.readOnly;
34
36
  const displayRows = rowModel.displayRows;
35
37
  const virtualRange = useMemo(() => {
36
38
  return getVirtualRange({
@@ -108,6 +110,44 @@ export function YoupGrid(props) {
108
110
  return row ? { row, displayIndex: item.index } : undefined;
109
111
  })
110
112
  .filter((item) => Boolean(item));
113
+ const getCellEditContext = (row, rowIndex, column) => {
114
+ return {
115
+ row: row.original,
116
+ rowNode: row,
117
+ rowId: row.id,
118
+ rowIndex,
119
+ column,
120
+ columnId: column.id,
121
+ value: column.accessor(row.original),
122
+ };
123
+ };
124
+ const canEditGridCell = (row, rowIndex, column) => {
125
+ if (!gridEditable || column.editable === false) {
126
+ return false;
127
+ }
128
+ return props.canEditCell?.(getCellEditContext(row, rowIndex, column)) ?? true;
129
+ };
130
+ const getGridCellMeta = (row, rowIndex, column) => {
131
+ return (props.getCellMeta?.(getCellEditContext(row, rowIndex, column)) ??
132
+ props.cellMeta?.[`${row.id}:${column.id}`]);
133
+ };
134
+ const createGridCellValueChange = (cell, value) => {
135
+ if (!cell.editable) {
136
+ return undefined;
137
+ }
138
+ if (Object.is(value, cell.value)) {
139
+ return undefined;
140
+ }
141
+ return {
142
+ row: cell.row.original,
143
+ rowId: cell.row.id,
144
+ rowIndex: cell.rowIndex,
145
+ column: cell.column,
146
+ columnId: cell.column.id,
147
+ value,
148
+ previousValue: cell.value,
149
+ };
150
+ };
111
151
  useEffect(() => {
112
152
  if (focusedRowIndex >= rowModel.visibleRows.length) {
113
153
  setFocusedRowIndex(Math.max(0, rowModel.visibleRows.length - 1));
@@ -159,7 +199,7 @@ export function YoupGrid(props) {
159
199
  if (changes.length === 0) {
160
200
  return;
161
201
  }
162
- if (source === "edit" || source === "paste" || source === "fill") {
202
+ if (source === "edit" || source === "paste" || source === "fill" || source === "delete") {
163
203
  valueHistoryRef.current = pushValueHistoryEntry(valueHistoryRef.current, {
164
204
  changes: changes.map((change) => ({
165
205
  rowId: change.rowId,
@@ -170,18 +210,33 @@ export function YoupGrid(props) {
170
210
  })),
171
211
  }, { maxEntries: VALUE_HISTORY_LIMIT });
172
212
  }
173
- for (const change of changes) {
174
- props.onCellValueChange?.({
175
- ...change,
176
- source,
213
+ const emittedChanges = changes.map((change) => ({
214
+ ...change,
215
+ source,
216
+ }));
217
+ for (const change of emittedChanges) {
218
+ props.onCellValueChange?.(change);
219
+ }
220
+ if (source === "paste" || source === "fill") {
221
+ props.onCellsValueChange?.({
222
+ changes: emittedChanges,
223
+ source: source,
177
224
  });
178
225
  }
179
226
  };
227
+ const applyCellValue = (cell, value) => {
228
+ const change = createGridCellValueChange(cell, value);
229
+ if (!change) {
230
+ return;
231
+ }
232
+ applyCellValueChanges([change], "edit");
233
+ };
180
234
  const applyValueHistoryEntry = (entry, source) => {
181
235
  applyCellValueChanges(getHistoryEntryValueChanges({
182
236
  entry,
183
237
  rowModel,
184
238
  source,
239
+ canEditCell: canEditGridCell,
185
240
  }), source);
186
241
  };
187
242
  const undoCellValueChange = () => {
@@ -202,19 +257,28 @@ export function YoupGrid(props) {
202
257
  applyValueHistoryEntry(result.entry, "redo");
203
258
  return true;
204
259
  };
205
- const commitEditingValue = (cell, source = "keyboard") => {
206
- if (source === "blur" && skipNextBlurCommitRef.current) {
260
+ const commitEditingValue = (cell, reason = "enter") => {
261
+ if (reason === "blur" && skipNextBlurCommitRef.current) {
207
262
  skipNextBlurCommitRef.current = false;
208
263
  return;
209
264
  }
210
- if (source === "keyboard") {
265
+ if (reason !== "blur") {
211
266
  skipNextBlurCommitRef.current = true;
212
267
  }
213
- commitEditingCell({
268
+ const change = commitEditingCell({
214
269
  cell,
215
270
  rowModel,
216
- applyCellValueChanges,
271
+ canEditCell: canEditGridCell,
217
272
  });
273
+ if (change) {
274
+ if (!Object.is(change.value, change.previousValue)) {
275
+ applyCellValueChanges([change], "edit");
276
+ }
277
+ props.onCellEditCommit?.({
278
+ ...change,
279
+ reason,
280
+ });
281
+ }
218
282
  setEditingCell(undefined);
219
283
  };
220
284
  const setFocusedCell = (cell, extendSelection = false, selectionAnchor) => {
@@ -245,7 +309,12 @@ export function YoupGrid(props) {
245
309
  controller.setSelectedRows(currentSelectedRowIds.filter((rowId) => !visibleRowIdSet.has(rowId)));
246
310
  };
247
311
  return createElement("div", {
248
- className: ["youp-grid", `youp-grid--density-${density}`, props.className].filter(Boolean).join(" "),
312
+ className: [
313
+ "youp-grid",
314
+ `youp-grid--density-${density}`,
315
+ !gridEditable ? "youp-grid--read-only" : "",
316
+ props.className,
317
+ ].filter(Boolean).join(" "),
249
318
  style: gridStyle,
250
319
  }, renderColumnToolbar({
251
320
  showColumnChooser: props.showColumnChooser ?? true,
@@ -268,12 +337,16 @@ export function YoupGrid(props) {
268
337
  }),
269
338
  });
270
339
  },
340
+ }), renderDisabledReason({
341
+ enabled: !gridEditable,
342
+ reason: props.disabledReason,
271
343
  }), createElement("div", {
272
344
  className: "youp-grid__viewport",
273
345
  role: "grid",
274
346
  "aria-rowcount": displayRows.length,
275
347
  "aria-colcount": rowModel.visibleColumns.length + (showRowSelectionColumn ? 1 : 0),
276
348
  "aria-busy": loading || undefined,
349
+ "aria-readonly": !gridEditable || undefined,
277
350
  onCopy: (event) => {
278
351
  if (editingCell) {
279
352
  return;
@@ -287,7 +360,7 @@ export function YoupGrid(props) {
287
360
  });
288
361
  },
289
362
  onPaste: (event) => {
290
- if (editingCell) {
363
+ if (editingCell || !gridEditable) {
291
364
  return;
292
365
  }
293
366
  handleGridPaste({
@@ -296,10 +369,11 @@ export function YoupGrid(props) {
296
369
  selectionRange,
297
370
  rows: rowModel.visibleRows,
298
371
  columns: visibleColumns,
372
+ canEditCell: canEditGridCell,
299
373
  applyCellValueChanges,
300
374
  });
301
375
  },
302
- }, createElement("div", { className: "youp-grid__header", role: "rowgroup" }, hasHeaderGroups
376
+ }, createElement("div", { className: "youp-grid__header", role: "rowgroup", ref: headerRef }, hasHeaderGroups
303
377
  ? createElement("div", { className: "youp-grid__row youp-grid__row--header-group", role: "row" }, showRowSelectionColumn
304
378
  ? renderSelectionHeaderGroupCell()
305
379
  : undefined, headerGroupLayouts.map((layout) => renderHeaderGroupCell(layout)))
@@ -345,6 +419,9 @@ export function YoupGrid(props) {
345
419
  ref: bodyRef,
346
420
  style: { height: viewportHeight },
347
421
  onScroll: (event) => {
422
+ if (headerRef.current) {
423
+ headerRef.current.style.setProperty("--youp-grid-header-scroll-left", `${event.currentTarget.scrollLeft}px`);
424
+ }
348
425
  setScrollTop(event.currentTarget.scrollTop);
349
426
  },
350
427
  }, rowModel.visibleRows.length === 0 && !props.loading && !props.error
@@ -383,10 +460,16 @@ export function YoupGrid(props) {
383
460
  selectionRange,
384
461
  fillRange,
385
462
  editingCell,
386
- editable: props.editable ?? true,
463
+ editable: gridEditable,
464
+ disabledReason: props.disabledReason,
465
+ canEditCell: canEditGridCell,
466
+ getCellMeta: getGridCellMeta,
387
467
  setRowSelected: (selected) => controller.setRowSelected(row.id, selected),
388
468
  setFocusedCell,
389
469
  startFillHandle: (event) => {
470
+ if (!gridEditable) {
471
+ return;
472
+ }
390
473
  const sourceRange = normalizeCellRange(selectionRange ?? {
391
474
  anchor: focusedCell,
392
475
  focus: focusedCell,
@@ -403,6 +486,7 @@ export function YoupGrid(props) {
403
486
  targetRange,
404
487
  rows: rowModel.visibleRows,
405
488
  columns: visibleColumns,
489
+ canEditCell: canEditGridCell,
406
490
  applyCellValueChanges,
407
491
  });
408
492
  const nextSelectionRange = getFillSelectionRange(sourceRange, targetRange);
@@ -412,6 +496,7 @@ export function YoupGrid(props) {
412
496
  },
413
497
  });
414
498
  },
499
+ applyCellValue,
415
500
  startEditing: startEditingCell,
416
501
  updateEditingDraft: (draftValue) => {
417
502
  setEditingCell((current) => current ? { ...current, draftValue } : current);
@@ -434,6 +519,17 @@ export function YoupGrid(props) {
434
519
  startEditing: startEditingCell,
435
520
  commitEditing: commitEditingValue,
436
521
  cancelEditing: cancelEditingCell,
522
+ applyCellValue,
523
+ deleteCellValues: () => {
524
+ deleteGridCellValues({
525
+ focusedCell,
526
+ selectionRange,
527
+ rows: rowModel.visibleRows,
528
+ columns: visibleColumns,
529
+ canEditCell: canEditGridCell,
530
+ applyCellValueChanges,
531
+ });
532
+ },
437
533
  undoCellValueChange,
438
534
  redoCellValueChange,
439
535
  toggleSelected: () => controller.toggleRowSelected(row.id),
@@ -473,6 +569,12 @@ function renderGridOverlay(context) {
473
569
  }
474
570
  return undefined;
475
571
  }
572
+ function renderDisabledReason(context) {
573
+ if (!context.enabled || context.reason == null) {
574
+ return undefined;
575
+ }
576
+ return createElement("div", { className: "youp-grid__disabled-reason", role: "status" }, context.reason);
577
+ }
476
578
  function renderAggregationFooter(context) {
477
579
  if (!context.enabled) {
478
580
  return undefined;
@@ -761,7 +863,10 @@ function renderRow(context) {
761
863
  const fillTargeted = context.fillRange
762
864
  ? isCellInNormalizedRange(context.rowIndex, columnIndex, context.fillRange)
763
865
  : false;
866
+ const editable = context.canEditCell(context.row, context.rowIndex, layout.column);
867
+ const meta = context.getCellMeta(context.row, context.rowIndex, layout.column);
764
868
  const showFillHandle = !context.editingCell &&
869
+ editable &&
765
870
  context.rowIndex === activeRange.endRowIndex &&
766
871
  columnIndex === activeRange.endColumnIndex;
767
872
  return renderCell({
@@ -776,9 +881,12 @@ function renderRow(context) {
776
881
  showFillHandle,
777
882
  editing,
778
883
  editingCell: context.editingCell,
779
- editable: context.editable,
884
+ editable,
885
+ disabledReason: context.disabledReason,
886
+ meta,
780
887
  setFocusedCell: context.setFocusedCell,
781
888
  startFillHandle: context.startFillHandle,
889
+ applyCellValue: context.applyCellValue,
782
890
  startEditing: context.startEditing,
783
891
  updateEditingDraft: context.updateEditingDraft,
784
892
  cancelEditing: context.cancelEditing,
@@ -826,13 +934,15 @@ function shouldIgnoreRowMouseEvent(event) {
826
934
  function renderCell(context) {
827
935
  const column = context.layout.column;
828
936
  const value = column.accessor(context.row.original);
829
- const editable = context.editable && column.editable !== false;
937
+ const editable = context.editable;
830
938
  const cellContext = {
831
939
  row: context.row,
832
940
  column,
833
941
  value,
834
942
  editing: context.editing,
835
943
  focused: context.focused,
944
+ editable,
945
+ meta: context.meta,
836
946
  };
837
947
  const cellState = {
838
948
  row: context.row,
@@ -841,10 +951,16 @@ function renderCell(context) {
841
951
  columnIndex: context.columnIndex,
842
952
  value,
843
953
  editable,
954
+ meta: context.meta,
844
955
  };
845
956
  const cellContent = context.renderCell
846
957
  ? context.renderCell(cellContext)
847
- : formatCellValue(column, context.row.original, value);
958
+ : renderDefaultCellContent({
959
+ cell: cellState,
960
+ row: context.row.original,
961
+ disabledReason: context.disabledReason,
962
+ applyCellValue: context.applyCellValue,
963
+ });
848
964
  return createElement("div", {
849
965
  key: column.id,
850
966
  className: getCellClassName([
@@ -853,10 +969,14 @@ function renderCell(context) {
853
969
  context.selected ? "youp-grid__cell--range-selected" : "",
854
970
  context.fillTargeted ? "youp-grid__cell--fill-target" : "",
855
971
  context.editing ? "youp-grid__cell--editing" : "",
972
+ !editable ? "youp-grid__cell--disabled" : "",
973
+ context.meta ? `youp-grid__cell--status-${context.meta.status}` : "",
856
974
  ].filter(Boolean).join(" "), context.layout),
857
975
  role: "gridcell",
858
976
  tabIndex: context.focused && !context.editing ? 0 : -1,
977
+ title: getCellTitle(context.meta, context.disabledReason, editable),
859
978
  "aria-colindex": context.columnIndex + context.ariaColumnOffset + 1,
979
+ "aria-readonly": !editable || undefined,
860
980
  "data-youp-row-index": context.rowIndex,
861
981
  "data-youp-column-index": context.columnIndex,
862
982
  "data-youp-column-id": column.id,
@@ -867,34 +987,107 @@ function renderCell(context) {
867
987
  },
868
988
  onDoubleClick: () => {
869
989
  if (editable) {
870
- context.startEditing(createEditingCell(cellState, value));
990
+ if (column.editor === "checkbox") {
991
+ context.applyCellValue(cellState, !Boolean(value));
992
+ }
993
+ else {
994
+ context.startEditing(createEditingCell(cellState, value));
995
+ }
871
996
  }
872
997
  },
873
998
  onKeyDown: (event) => context.onKeyDown(event, cellState),
874
999
  }, context.editing
875
- ? createElement("input", {
876
- className: "youp-grid__cell-editor",
1000
+ ? renderCellEditor({
1001
+ cell: cellState,
1002
+ editingCell: context.editingCell,
1003
+ updateEditingDraft: context.updateEditingDraft,
1004
+ commitEditing: context.commitEditing,
1005
+ onKeyDown: context.onKeyDown,
1006
+ })
1007
+ : cellContent, renderCellStatus(context.meta), !context.editing && context.showFillHandle
1008
+ ? createElement("span", {
1009
+ className: "youp-grid__fill-handle",
1010
+ role: "button",
1011
+ "aria-label": "Fill selection",
1012
+ onMouseDown: context.startFillHandle,
1013
+ })
1014
+ : undefined);
1015
+ }
1016
+ function renderDefaultCellContent(context) {
1017
+ if (context.cell.column.editor === "checkbox") {
1018
+ return createElement("input", {
1019
+ className: "youp-grid__cell-checkbox",
1020
+ type: "checkbox",
1021
+ checked: Boolean(context.cell.value),
1022
+ disabled: !context.cell.editable,
1023
+ title: getCellTitle(context.cell.meta, context.disabledReason, context.cell.editable),
1024
+ "aria-label": context.cell.column.headerName,
1025
+ onChange: (event) => {
1026
+ context.applyCellValue(context.cell, event.currentTarget.checked);
1027
+ },
1028
+ onClick: (event) => {
1029
+ event.stopPropagation();
1030
+ },
1031
+ onKeyDown: (event) => {
1032
+ event.stopPropagation();
1033
+ },
1034
+ });
1035
+ }
1036
+ const text = formatCellValue(context.cell.column, context.row, context.cell.value);
1037
+ const placeholder = context.cell.column.placeholder;
1038
+ const hasPlaceholder = text.length === 0 && Boolean(placeholder);
1039
+ return createElement("span", { className: hasPlaceholder ? "youp-grid__cell-placeholder" : undefined }, hasPlaceholder ? placeholder : text);
1040
+ }
1041
+ function renderCellEditor(context) {
1042
+ if (context.cell.column.editor === "select") {
1043
+ const options = normalizeEditorOptions(context.cell.column.options);
1044
+ return createElement("select", {
1045
+ className: "youp-grid__cell-editor youp-grid__cell-editor--select",
877
1046
  value: context.editingCell?.draftValue ?? "",
878
1047
  autoFocus: true,
1048
+ disabled: !context.cell.editable,
879
1049
  onChange: (event) => {
880
1050
  context.updateEditingDraft(event.currentTarget.value);
881
1051
  },
882
1052
  onBlur: (event) => {
883
- context.commitEditing(createEditingCell(cellState, event.currentTarget.value), "blur");
1053
+ context.commitEditing(createEditingCell(context.cell, event.currentTarget.value), "blur");
884
1054
  },
885
1055
  onKeyDown: (event) => {
886
1056
  event.stopPropagation();
887
- context.onKeyDown(event, cellState);
1057
+ context.onKeyDown(event, context.cell);
888
1058
  },
889
- })
890
- : cellContent, !context.editing && context.showFillHandle
891
- ? createElement("span", {
892
- className: "youp-grid__fill-handle",
893
- role: "button",
894
- "aria-label": "Fill selection",
895
- onMouseDown: context.startFillHandle,
896
- })
897
- : undefined);
1059
+ }, context.cell.column.placeholder
1060
+ ? createElement("option", { key: "__placeholder", value: "", disabled: true }, context.cell.column.placeholder)
1061
+ : undefined, options.map((option) => createElement("option", { key: option.inputValue, value: option.inputValue }, option.label)));
1062
+ }
1063
+ return createElement("input", {
1064
+ className: "youp-grid__cell-editor",
1065
+ type: context.cell.column.editor === "number" ? "number" : "text",
1066
+ value: context.editingCell?.draftValue ?? "",
1067
+ placeholder: context.cell.column.placeholder,
1068
+ autoFocus: true,
1069
+ disabled: !context.cell.editable,
1070
+ onChange: (event) => {
1071
+ context.updateEditingDraft(event.currentTarget.value);
1072
+ },
1073
+ onBlur: (event) => {
1074
+ context.commitEditing(createEditingCell(context.cell, event.currentTarget.value), "blur");
1075
+ },
1076
+ onKeyDown: (event) => {
1077
+ event.stopPropagation();
1078
+ context.onKeyDown(event, context.cell);
1079
+ },
1080
+ });
1081
+ }
1082
+ function renderCellStatus(meta) {
1083
+ if (!meta) {
1084
+ return undefined;
1085
+ }
1086
+ return createElement("span", {
1087
+ className: `youp-grid__cell-status youp-grid__cell-status--${meta.status}`,
1088
+ title: typeof meta.message === "string" ? meta.message : undefined,
1089
+ "aria-hidden": true,
1090
+ });
898
1091
  }
899
1092
  function renderColumnToolbar(context) {
900
1093
  if (!context.showColumnChooser && !context.showCsvExport && !context.showDensityControl) {
@@ -1161,11 +1354,11 @@ function handleCellKeyDown(context) {
1161
1354
  }
1162
1355
  if (context.event.key === "Enter") {
1163
1356
  context.event.preventDefault();
1164
- context.commitEditing(editingCell);
1357
+ context.commitEditing(editingCell, "enter");
1165
1358
  moveFocusedCell({
1166
1359
  ...context,
1167
1360
  nextCell: {
1168
- rowIndex: clamp(context.cell.rowIndex + 1, 0, context.rowCount - 1),
1361
+ rowIndex: clamp(context.cell.rowIndex + (context.event.shiftKey ? -1 : 1), 0, context.rowCount - 1),
1169
1362
  columnIndex: context.cell.columnIndex,
1170
1363
  },
1171
1364
  });
@@ -1173,7 +1366,7 @@ function handleCellKeyDown(context) {
1173
1366
  }
1174
1367
  if (context.event.key === "Tab") {
1175
1368
  context.event.preventDefault();
1176
- context.commitEditing(editingCell);
1369
+ context.commitEditing(editingCell, "tab");
1177
1370
  moveFocusedCell({
1178
1371
  ...context,
1179
1372
  nextCell: getNextTabCell(context.cell, context.rowCount, context.columnCount, context.event.shiftKey),
@@ -1184,23 +1377,41 @@ function handleCellKeyDown(context) {
1184
1377
  }
1185
1378
  if (isUndoShortcut(context.event)) {
1186
1379
  context.event.preventDefault();
1187
- context.undoCellValueChange();
1380
+ if (context.cell.editable) {
1381
+ context.undoCellValueChange();
1382
+ }
1188
1383
  return;
1189
1384
  }
1190
1385
  if (isRedoShortcut(context.event)) {
1191
1386
  context.event.preventDefault();
1192
- context.redoCellValueChange();
1387
+ if (context.cell.editable) {
1388
+ context.redoCellValueChange();
1389
+ }
1390
+ return;
1391
+ }
1392
+ if (context.event.key === "Delete") {
1393
+ context.event.preventDefault();
1394
+ context.deleteCellValues();
1193
1395
  return;
1194
1396
  }
1195
1397
  if (context.event.key === "Enter" || context.event.key === "F2") {
1196
1398
  if (context.cell.editable) {
1197
1399
  context.event.preventDefault();
1198
- context.startEditing(createEditingCell(context.cell, context.cell.value));
1400
+ if (context.cell.column.editor === "checkbox") {
1401
+ context.applyCellValue(context.cell, !Boolean(context.cell.value));
1402
+ }
1403
+ else {
1404
+ context.startEditing(createEditingCell(context.cell, context.cell.value));
1405
+ }
1199
1406
  }
1200
1407
  return;
1201
1408
  }
1202
1409
  if (context.event.key === " ") {
1203
1410
  context.event.preventDefault();
1411
+ if (context.cell.column.editor === "checkbox" && context.cell.editable) {
1412
+ context.applyCellValue(context.cell, !Boolean(context.cell.value));
1413
+ return;
1414
+ }
1204
1415
  if (context.event.shiftKey) {
1205
1416
  context.setFocusedCell(context.cell, true);
1206
1417
  }
@@ -1209,10 +1420,15 @@ function handleCellKeyDown(context) {
1209
1420
  }
1210
1421
  return;
1211
1422
  }
1212
- if (isPrintableKey(context.event)) {
1213
- if (context.cell.editable) {
1214
- context.event.preventDefault();
1215
- context.startEditing(createEditingCell(context.cell, context.event.key));
1423
+ if (isTextEditingKey(context.event)) {
1424
+ if (context.cell.editable && context.cell.column.editor !== "checkbox") {
1425
+ if (canUseKeyAsInitialDraft(context.event)) {
1426
+ context.event.preventDefault();
1427
+ context.startEditing(createEditingCell(context.cell, context.event.key));
1428
+ }
1429
+ else {
1430
+ context.startEditing(createEditingCell(context.cell, ""));
1431
+ }
1216
1432
  }
1217
1433
  return;
1218
1434
  }
@@ -1372,27 +1588,20 @@ function isEditingCell(editingCell, row, column) {
1372
1588
  function commitEditingCell(context) {
1373
1589
  const row = context.rowModel.visibleRows[context.cell.rowIndex];
1374
1590
  const column = context.rowModel.visibleColumns.find((item) => item.id === context.cell.columnId);
1375
- if (!row || !column) {
1376
- return;
1591
+ if (!row || !column || !context.canEditCell(row, context.cell.rowIndex, column)) {
1592
+ return undefined;
1377
1593
  }
1378
1594
  const previousValue = column.accessor(row.original);
1379
- const value = column.valueParser
1380
- ? column.valueParser(context.cell.draftValue, row.original)
1381
- : context.cell.draftValue;
1382
- if (Object.is(value, previousValue)) {
1383
- return;
1384
- }
1385
- context.applyCellValueChanges([
1386
- {
1387
- row: row.original,
1388
- rowId: row.id,
1389
- rowIndex: context.cell.rowIndex,
1390
- column,
1391
- columnId: column.id,
1392
- value,
1393
- previousValue,
1394
- },
1395
- ], "edit");
1595
+ const value = parseDraftValue(column, row.original, context.cell.draftValue);
1596
+ return {
1597
+ row: row.original,
1598
+ rowId: row.id,
1599
+ rowIndex: context.cell.rowIndex,
1600
+ column,
1601
+ columnId: column.id,
1602
+ value,
1603
+ previousValue,
1604
+ };
1396
1605
  }
1397
1606
  function handleGridCopy(context) {
1398
1607
  const range = context.selectionRange ?? {
@@ -1433,11 +1642,11 @@ function handleGridPaste(context) {
1433
1642
  })) {
1434
1643
  const row = context.rows[cell.rowIndex];
1435
1644
  const column = context.columns[cell.columnIndex];
1436
- if (!row || !column || column.editable === false) {
1645
+ if (!row || !column || !context.canEditCell(row, cell.rowIndex, column)) {
1437
1646
  continue;
1438
1647
  }
1439
1648
  const previousValue = column.accessor(row.original);
1440
- const value = column.valueParser ? column.valueParser(cell.value, row.original) : cell.value;
1649
+ const value = parseDraftValue(column, row.original, cell.value);
1441
1650
  if (Object.is(value, previousValue)) {
1442
1651
  continue;
1443
1652
  }
@@ -1453,6 +1662,37 @@ function handleGridPaste(context) {
1453
1662
  }
1454
1663
  context.applyCellValueChanges(changes, "paste");
1455
1664
  }
1665
+ function deleteGridCellValues(context) {
1666
+ const range = normalizeCellRange(context.selectionRange ?? {
1667
+ anchor: context.focusedCell,
1668
+ focus: context.focusedCell,
1669
+ });
1670
+ const changes = [];
1671
+ for (let rowIndex = range.startRowIndex; rowIndex <= range.endRowIndex; rowIndex += 1) {
1672
+ for (let columnIndex = range.startColumnIndex; columnIndex <= range.endColumnIndex; columnIndex += 1) {
1673
+ const row = context.rows[rowIndex];
1674
+ const column = context.columns[columnIndex];
1675
+ if (!row || !column || !context.canEditCell(row, rowIndex, column)) {
1676
+ continue;
1677
+ }
1678
+ const previousValue = column.accessor(row.original);
1679
+ const value = getEmptyCellValue(column, row.original);
1680
+ if (Object.is(value, previousValue)) {
1681
+ continue;
1682
+ }
1683
+ changes.push({
1684
+ row: row.original,
1685
+ rowId: row.id,
1686
+ rowIndex,
1687
+ column,
1688
+ columnId: column.id,
1689
+ value,
1690
+ previousValue,
1691
+ });
1692
+ }
1693
+ }
1694
+ context.applyCellValueChanges(changes, "delete");
1695
+ }
1456
1696
  function startFillHandleDrag(context) {
1457
1697
  context.event.preventDefault();
1458
1698
  context.event.stopPropagation();
@@ -1499,7 +1739,7 @@ function applyFillHandleValues(context) {
1499
1739
  })) {
1500
1740
  const row = context.rows[cell.rowIndex];
1501
1741
  const column = context.columns[cell.columnIndex];
1502
- if (!row || !column || column.editable === false) {
1742
+ if (!row || !column || !context.canEditCell(row, cell.rowIndex, column)) {
1503
1743
  continue;
1504
1744
  }
1505
1745
  const previousValue = column.accessor(row.original);
@@ -1569,16 +1809,20 @@ function getHistoryEntryValueChanges(context) {
1569
1809
  if (!row || !column) {
1570
1810
  continue;
1571
1811
  }
1812
+ const visibleRowIndex = context.rowModel.visibleRows.findIndex((item) => item.id === row.id);
1813
+ const rowIndex = visibleRowIndex >= 0 ? visibleRowIndex : row.index;
1814
+ if (!context.canEditCell(row, rowIndex, column)) {
1815
+ continue;
1816
+ }
1572
1817
  const value = context.source === "undo" ? historyChange.previousValue : historyChange.value;
1573
1818
  const previousValue = column.accessor(row.original);
1574
1819
  if (Object.is(value, previousValue)) {
1575
1820
  continue;
1576
1821
  }
1577
- const visibleRowIndex = context.rowModel.visibleRows.findIndex((item) => item.id === row.id);
1578
1822
  changes.push({
1579
1823
  row: row.original,
1580
1824
  rowId: row.id,
1581
- rowIndex: visibleRowIndex >= 0 ? visibleRowIndex : row.index,
1825
+ rowIndex,
1582
1826
  column,
1583
1827
  columnId: column.id,
1584
1828
  value,
@@ -1588,10 +1832,85 @@ function getHistoryEntryValueChanges(context) {
1588
1832
  return changes;
1589
1833
  }
1590
1834
  function formatCellValue(column, row, value) {
1591
- return column.valueFormatter ? column.valueFormatter(value, row) : String(value ?? "");
1835
+ if (column.valueFormatter) {
1836
+ return column.valueFormatter(value, row);
1837
+ }
1838
+ if (column.editor === "select") {
1839
+ const option = normalizeEditorOptions(column.options).find((item) => Object.is(item.value, value));
1840
+ if (option) {
1841
+ return option.label;
1842
+ }
1843
+ }
1844
+ return String(value ?? "");
1845
+ }
1846
+ function parseDraftValue(column, row, draftValue) {
1847
+ if (column.valueParser) {
1848
+ return column.valueParser(draftValue, row);
1849
+ }
1850
+ if (column.editor === "number") {
1851
+ return draftValue === "" ? undefined : Number(draftValue);
1852
+ }
1853
+ if (column.editor === "checkbox") {
1854
+ return draftValue === "true";
1855
+ }
1856
+ if (column.editor === "select") {
1857
+ const option = normalizeEditorOptions(column.options).find((item) => item.inputValue === draftValue);
1858
+ return option ? option.value : draftValue;
1859
+ }
1860
+ return draftValue;
1861
+ }
1862
+ function getEmptyCellValue(column, row) {
1863
+ if (column.valueParser) {
1864
+ return column.valueParser("", row);
1865
+ }
1866
+ if (column.editor === "checkbox") {
1867
+ return false;
1868
+ }
1869
+ if (column.editor === "number") {
1870
+ return undefined;
1871
+ }
1872
+ return "";
1873
+ }
1874
+ function normalizeEditorOptions(options) {
1875
+ return (options ?? []).map((option) => {
1876
+ const value = getEditorOptionValue(option);
1877
+ return {
1878
+ value,
1879
+ label: typeof option === "object" ? option.label : String(option),
1880
+ inputValue: String(value),
1881
+ };
1882
+ });
1883
+ }
1884
+ function getEditorOptionValue(option) {
1885
+ return typeof option === "object" ? option.value : option;
1886
+ }
1887
+ function getCellTitle(meta, disabledReason, editable) {
1888
+ if (typeof meta?.message === "string") {
1889
+ return meta.message;
1890
+ }
1891
+ if (!editable && typeof disabledReason === "string") {
1892
+ return disabledReason;
1893
+ }
1894
+ return undefined;
1895
+ }
1896
+ function isTextEditingKey(event) {
1897
+ if (event.metaKey || event.ctrlKey || event.altKey) {
1898
+ return false;
1899
+ }
1900
+ return event.key.length === 1 || isCompositionEditingKey(event);
1901
+ }
1902
+ function canUseKeyAsInitialDraft(event) {
1903
+ return event.key.length === 1 && isAsciiPrintableKey(event.key) && !isCompositionEditingKey(event);
1904
+ }
1905
+ function isCompositionEditingKey(event) {
1906
+ return (event.nativeEvent.isComposing ||
1907
+ event.key === "Process" ||
1908
+ event.key === "Dead" ||
1909
+ event.key === "Unidentified" ||
1910
+ event.keyCode === 229);
1592
1911
  }
1593
- function isPrintableKey(event) {
1594
- return event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey;
1912
+ function isAsciiPrintableKey(key) {
1913
+ return key >= " " && key <= "~";
1595
1914
  }
1596
1915
  function isUndoShortcut(event) {
1597
1916
  return (event.metaKey || event.ctrlKey) && !event.altKey && !event.shiftKey && event.key.toLowerCase() === "z";
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { YoupGrid } from "./YoupGrid.ts";
2
2
  export { useYoupGrid } from "./useYoupGrid.ts";
3
- export type { YoupGridCellContext, YoupGridController, YoupGridDensity, YoupGridHeaderContext, YoupGridOptions, YoupGridProps, YoupGridRowEvent, YoupGridRowsEndReachedEvent, YoupGridStateChange, } from "./types.ts";
3
+ export type { YoupGridCanEditCellContext, YoupGridCellEditCommit, YoupGridCellEditCommitReason, YoupGridCellContext, YoupGridCellMeta, YoupGridCellMetaStatus, YoupGridCellsValueChange, YoupGridCellsValueChangeSource, YoupGridController, YoupGridDensity, YoupGridHeaderContext, YoupGridOptions, YoupGridProps, YoupGridRowEvent, YoupGridRowsEndReachedEvent, YoupGridStateChange, } from "./types.ts";
package/dist/styles.css CHANGED
@@ -64,6 +64,15 @@
64
64
  background: #fff;
65
65
  }
66
66
 
67
+ .youp-grid__disabled-reason {
68
+ padding: 8px 12px;
69
+ border-bottom: 1px solid var(--youp-grid-border);
70
+ color: var(--youp-grid-muted);
71
+ background: #f8fafc;
72
+ font-size: 12px;
73
+ font-weight: 700;
74
+ }
75
+
67
76
  .youp-grid__density-control {
68
77
  display: inline-flex;
69
78
  align-items: center;
@@ -151,6 +160,15 @@
151
160
  background: var(--youp-grid-row-hover);
152
161
  }
153
162
 
163
+ .youp-grid__header {
164
+ --youp-grid-header-scroll-left: 0px;
165
+ }
166
+
167
+ .youp-grid__header .youp-grid__cell--header:not(.youp-grid__cell--pinned-left):not(.youp-grid__cell--pinned-right):not(.youp-grid__selection-cell),
168
+ .youp-grid__header .youp-grid__cell--header-group:not(.youp-grid__cell--pinned-left):not(.youp-grid__cell--pinned-right):not(.youp-grid__selection-cell) {
169
+ transform: translateX(calc(-1 * var(--youp-grid-header-scroll-left)));
170
+ }
171
+
154
172
  .youp-grid__row--header {
155
173
  background: var(--youp-grid-header);
156
174
  }
@@ -268,6 +286,10 @@
268
286
  white-space: nowrap;
269
287
  }
270
288
 
289
+ .youp-grid--read-only .youp-grid__cell {
290
+ cursor: default;
291
+ }
292
+
271
293
  .youp-grid__cell:focus {
272
294
  outline: none;
273
295
  }
@@ -289,6 +311,70 @@
289
311
  box-shadow: inset 0 0 0 1px #0ea5e9;
290
312
  }
291
313
 
314
+ .youp-grid__cell--disabled {
315
+ color: var(--youp-grid-muted);
316
+ background: #f8fafc;
317
+ }
318
+
319
+ .youp-grid__cell--status-loading {
320
+ background: #eff6ff;
321
+ }
322
+
323
+ .youp-grid__cell--status-error {
324
+ background: #fef2f2;
325
+ }
326
+
327
+ .youp-grid__cell--status-warning {
328
+ background: #fffbeb;
329
+ }
330
+
331
+ .youp-grid__cell--status-success {
332
+ background: #f0fdf4;
333
+ }
334
+
335
+ .youp-grid__cell-placeholder {
336
+ color: #94a3b8;
337
+ }
338
+
339
+ .youp-grid__cell-checkbox {
340
+ width: 16px;
341
+ height: 16px;
342
+ margin: 0;
343
+ accent-color: #2563eb;
344
+ }
345
+
346
+ .youp-grid__cell-checkbox:disabled {
347
+ cursor: not-allowed;
348
+ }
349
+
350
+ .youp-grid__cell-status {
351
+ position: absolute;
352
+ top: 6px;
353
+ right: 6px;
354
+ z-index: 3;
355
+ width: 7px;
356
+ height: 7px;
357
+ border: 1px solid #fff;
358
+ border-radius: 999px;
359
+ pointer-events: none;
360
+ }
361
+
362
+ .youp-grid__cell-status--loading {
363
+ background: #2563eb;
364
+ }
365
+
366
+ .youp-grid__cell-status--error {
367
+ background: #dc2626;
368
+ }
369
+
370
+ .youp-grid__cell-status--warning {
371
+ background: #d97706;
372
+ }
373
+
374
+ .youp-grid__cell-status--success {
375
+ background: #16a34a;
376
+ }
377
+
292
378
  .youp-grid__cell--focused.youp-grid__cell--fill-target {
293
379
  background: #bae6fd;
294
380
  }
@@ -322,6 +408,16 @@
322
408
  outline: none;
323
409
  }
324
410
 
411
+ .youp-grid__cell-editor--select {
412
+ padding-right: 24px;
413
+ }
414
+
415
+ .youp-grid__cell-editor:disabled {
416
+ color: var(--youp-grid-muted);
417
+ cursor: not-allowed;
418
+ background: #f1f5f9;
419
+ }
420
+
325
421
  .youp-grid__cell--pinned-left,
326
422
  .youp-grid__cell--pinned-right {
327
423
  position: sticky;
@@ -363,6 +459,31 @@
363
459
  background: #e0f2fe;
364
460
  }
365
461
 
462
+ .youp-grid__cell--pinned-left.youp-grid__cell--disabled,
463
+ .youp-grid__cell--pinned-right.youp-grid__cell--disabled {
464
+ background: #f8fafc;
465
+ }
466
+
467
+ .youp-grid__cell--pinned-left.youp-grid__cell--status-loading,
468
+ .youp-grid__cell--pinned-right.youp-grid__cell--status-loading {
469
+ background: #eff6ff;
470
+ }
471
+
472
+ .youp-grid__cell--pinned-left.youp-grid__cell--status-error,
473
+ .youp-grid__cell--pinned-right.youp-grid__cell--status-error {
474
+ background: #fef2f2;
475
+ }
476
+
477
+ .youp-grid__cell--pinned-left.youp-grid__cell--status-warning,
478
+ .youp-grid__cell--pinned-right.youp-grid__cell--status-warning {
479
+ background: #fffbeb;
480
+ }
481
+
482
+ .youp-grid__cell--pinned-left.youp-grid__cell--status-success,
483
+ .youp-grid__cell--pinned-right.youp-grid__cell--status-success {
484
+ background: #f0fdf4;
485
+ }
486
+
366
487
  .youp-grid__cell--focused.youp-grid__cell--left-last {
367
488
  box-shadow:
368
489
  inset 0 0 0 2px #2563eb,
package/dist/types.d.ts CHANGED
@@ -14,7 +14,30 @@ export type YoupGridCellValueChange<TRow> = {
14
14
  previousValue: unknown;
15
15
  source: YoupGridCellValueChangeSource;
16
16
  };
17
- export type YoupGridCellValueChangeSource = "edit" | "paste" | "fill" | "undo" | "redo";
17
+ export type YoupGridCellValueChangeSource = "edit" | "paste" | "fill" | "delete" | "undo" | "redo";
18
+ export type YoupGridCellEditCommitReason = "enter" | "tab" | "blur";
19
+ export type YoupGridCellEditCommit<TRow> = Omit<YoupGridCellValueChange<TRow>, "source"> & {
20
+ reason: YoupGridCellEditCommitReason;
21
+ };
22
+ export type YoupGridCellsValueChangeSource = "paste" | "fill";
23
+ export type YoupGridCellsValueChange<TRow> = {
24
+ changes: YoupGridCellValueChange<TRow>[];
25
+ source: YoupGridCellsValueChangeSource;
26
+ };
27
+ export type YoupGridCellMetaStatus = "loading" | "error" | "warning" | "success";
28
+ export type YoupGridCellMeta = {
29
+ status: YoupGridCellMetaStatus;
30
+ message?: ReactNode;
31
+ };
32
+ export type YoupGridCanEditCellContext<TRow> = {
33
+ row: TRow;
34
+ rowNode: RowNode<TRow>;
35
+ rowId: GridRowId;
36
+ rowIndex: number;
37
+ column: ResolvedColumnDef<TRow>;
38
+ columnId: string;
39
+ value: unknown;
40
+ };
18
41
  export type YoupGridDensity = "compact" | "standard" | "comfortable";
19
42
  export type YoupGridRowEvent<TRow> = {
20
43
  row: TRow;
@@ -84,6 +107,9 @@ export type YoupGridProps<TRow> = YoupGridOptions<TRow> & {
84
107
  style?: CSSProperties;
85
108
  height?: number | string;
86
109
  editable?: boolean;
110
+ readOnly?: boolean;
111
+ canEditCell?: (context: YoupGridCanEditCellContext<TRow>) => boolean;
112
+ disabledReason?: ReactNode;
87
113
  showColumnChooser?: boolean;
88
114
  showColumnMenu?: boolean;
89
115
  showCsvExport?: boolean;
@@ -100,9 +126,13 @@ export type YoupGridProps<TRow> = YoupGridOptions<TRow> & {
100
126
  loadingContent?: ReactNode;
101
127
  error?: boolean;
102
128
  errorContent?: ReactNode;
129
+ cellMeta?: Record<string, YoupGridCellMeta | undefined>;
130
+ getCellMeta?: (context: YoupGridCanEditCellContext<TRow>) => YoupGridCellMeta | undefined;
103
131
  renderCell?: (context: YoupGridCellContext<TRow>) => ReactNode;
104
132
  renderHeader?: (context: YoupGridHeaderContext<TRow>) => ReactNode;
105
133
  onCellValueChange?: (change: YoupGridCellValueChange<TRow>) => void;
134
+ onCellEditCommit?: (commit: YoupGridCellEditCommit<TRow>) => void;
135
+ onCellsValueChange?: (change: YoupGridCellsValueChange<TRow>) => void;
106
136
  onRowClick?: (event: YoupGridRowEvent<TRow>) => void;
107
137
  onRowDoubleClick?: (event: YoupGridRowEvent<TRow>) => void;
108
138
  onRowsEndReached?: (event: YoupGridRowsEndReachedEvent<TRow>) => void;
@@ -114,6 +144,8 @@ export type YoupGridCellContext<TRow> = {
114
144
  value: unknown;
115
145
  editing: boolean;
116
146
  focused: boolean;
147
+ editable: boolean;
148
+ meta?: YoupGridCellMeta;
117
149
  };
118
150
  export type YoupGridHeaderContext<TRow> = {
119
151
  column: ResolvedColumnDef<TRow>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youp-grid/react",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "React adapter for Youp Grid.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -28,7 +28,7 @@
28
28
  "build": "tsc -p tsconfig.build.json && cp src/styles.css dist/styles.css"
29
29
  },
30
30
  "dependencies": {
31
- "@youp-grid/core": "0.1.0"
31
+ "@youp-grid/core": "0.2.1"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "react": ">=18.2.0"