@xcelsior/ui-spreadsheets 1.0.1 → 1.0.3

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @xcelsior/ui-spreadsheets@1.0.1 build /home/circleci/repo/packages/ui/ui-spreadsheets
2
+ > @xcelsior/ui-spreadsheets@1.0.3 build /home/circleci/repo/packages/ui/ui-spreadsheets
3
3
  > tsup && tsc --noEmit
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -10,13 +10,13 @@
10
10
  CLI Cleaning output folder
11
11
  CJS Build start
12
12
  ESM Build start
13
- CJS dist/index.js 146.68 KB
14
- CJS dist/index.js.map 2.62 MB
15
- CJS ⚡️ Build success in 691ms
16
- ESM dist/index.mjs 136.04 KB
17
- ESM dist/index.mjs.map 2.62 MB
18
- ESM ⚡️ Build success in 691ms
13
+ CJS dist/index.js 151.12 KB
14
+ CJS dist/index.js.map 2.63 MB
15
+ CJS ⚡️ Build success in 576ms
16
+ ESM dist/index.mjs 140.30 KB
17
+ ESM dist/index.mjs.map 2.63 MB
18
+ ESM ⚡️ Build success in 577ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 9511ms
20
+ DTS ⚡️ Build success in 8502ms
21
21
  DTS dist/index.d.ts 22.18 KB
22
22
  DTS dist/index.d.mts 22.18 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @xcelsior/ui-spreadsheets
2
2
 
3
+ ## 1.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - e734bf5: improve spreadsheet performance
8
+ - Updated dependencies [e734bf5]
9
+ - @xcelsior/utils@1.0.1
10
+
11
+ ## 1.0.2
12
+
13
+ ### Patch Changes
14
+
15
+ - 05daf41: added keyboard navigation
16
+
3
17
  ## 1.0.1
4
18
 
5
19
  ### Patch Changes
package/dist/index.js CHANGED
@@ -212,7 +212,7 @@ var SpreadsheetCell = ({
212
212
  column,
213
213
  row,
214
214
  rowIndex,
215
- rowId: _rowId,
215
+ rowId,
216
216
  isEditable = false,
217
217
  isEditing = false,
218
218
  isFocused = false,
@@ -261,6 +261,7 @@ var SpreadsheetCell = ({
261
261
  onConfirm?.();
262
262
  } else if (e.key === "Escape") {
263
263
  e.preventDefault();
264
+ e.stopPropagation();
264
265
  setLocalValue(value);
265
266
  onChange?.(value);
266
267
  onCancel?.();
@@ -332,7 +333,7 @@ var SpreadsheetCell = ({
332
333
  };
333
334
  const cellPadding = compactMode ? cellPaddingCompact : cellPaddingNormal;
334
335
  const handleCellKeyDown = (e) => {
335
- if (e.key === "Enter" || e.key === " ") {
336
+ if (e.key === " ") {
336
337
  e.preventDefault();
337
338
  onClick?.(e);
338
339
  }
@@ -352,6 +353,7 @@ var SpreadsheetCell = ({
352
353
  {
353
354
  onClick,
354
355
  onKeyDown: handleCellKeyDown,
356
+ "data-cell-id": `${rowId}-${column.id}`,
355
357
  className: cn(
356
358
  "border border-gray-200 text-xs group cursor-pointer transition-colors",
357
359
  cellPadding,
@@ -2147,6 +2149,7 @@ function KeyboardShortcutsModal({
2147
2149
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ShortcutSection, { title: "General", children: shortcuts.general.map((shortcut, index) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ShortcutRow, { label: shortcut.label, keys: shortcut.keys }, index)) }),
2148
2150
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ShortcutSection, { title: "Row Selection", children: shortcuts.rowSelection.map((shortcut, index) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ShortcutRow, { label: shortcut.label, keys: shortcut.keys }, index)) }),
2149
2151
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ShortcutSection, { title: "Editing", children: shortcuts.editing.map((shortcut, index) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ShortcutRow, { label: shortcut.label, keys: shortcut.keys }, index)) }),
2152
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ShortcutSection, { title: "Cell Navigation", children: shortcuts.navigation.map((shortcut, index) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ShortcutRow, { label: shortcut.label, keys: shortcut.keys }, index)) }),
2150
2153
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ShortcutSection, { title: "Row Actions (hover over row #)", children: shortcuts.rowActions.map((action, index) => /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)("div", { className: "flex items-center justify-between", children: [
2151
2154
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "text-gray-600 text-sm", children: action.label }),
2152
2155
  /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "text-gray-500 text-xs", children: action.description })
@@ -2176,6 +2179,7 @@ var import_design_system = require("@xcelsior/design-system");
2176
2179
 
2177
2180
  // src/hooks/useSpreadsheetFiltering.ts
2178
2181
  var import_react10 = require("react");
2182
+ var import_utils9 = require("@xcelsior/utils");
2179
2183
  function useSpreadsheetFiltering({
2180
2184
  data,
2181
2185
  columns,
@@ -2322,19 +2326,10 @@ function useSpreadsheetFiltering({
2322
2326
  },
2323
2327
  []
2324
2328
  );
2325
- const filteredData = (0, import_react10.useMemo)(() => {
2326
- if (!data || !Array.isArray(data)) return [];
2327
- if (serverSide) {
2328
- return data;
2329
- }
2330
- if (!columns || !Array.isArray(columns)) return data;
2331
- let result = [...data];
2332
- for (const [columnId, filter] of Object.entries(filters)) {
2333
- if (!filter) continue;
2334
- const column = columns.find((c) => c.id === columnId);
2335
- if (!column) continue;
2336
- result = result.filter((row) => {
2337
- const value = column.getValue ? column.getValue(row) : row[columnId];
2329
+ const buildFilterPredicate = (0, import_react10.useCallback)(
2330
+ (column, filter) => {
2331
+ return (row) => {
2332
+ const value = column.getValue ? column.getValue(row) : row[column.id];
2338
2333
  if (filter.includeBlanks && isBlankValue(value)) {
2339
2334
  return true;
2340
2335
  }
@@ -2361,32 +2356,48 @@ function useSpreadsheetFiltering({
2361
2356
  if (filter.max !== void 0 && numValue > Number(filter.max)) return false;
2362
2357
  }
2363
2358
  return true;
2364
- });
2365
- }
2366
- if (sortConfig) {
2367
- const column = columns.find((c) => c.id === sortConfig.columnId);
2368
- result.sort((a, b) => {
2369
- const aValue = column?.getValue ? column.getValue(a) : a[sortConfig.columnId];
2370
- const bValue = column?.getValue ? column.getValue(b) : b[sortConfig.columnId];
2359
+ };
2360
+ },
2361
+ [applyTextCondition, applyNumberCondition, applyDateCondition]
2362
+ );
2363
+ const buildSortComparator = (0, import_react10.useCallback)(
2364
+ (column, direction) => {
2365
+ return (a, b) => {
2366
+ const aValue = column?.getValue ? column.getValue(a) : a[sortConfig?.columnId];
2367
+ const bValue = column?.getValue ? column.getValue(b) : b[sortConfig?.columnId];
2371
2368
  if (aValue === null || aValue === void 0) return 1;
2372
2369
  if (bValue === null || bValue === void 0) return -1;
2373
2370
  if (typeof aValue === "string" && typeof bValue === "string") {
2374
- return sortConfig.direction === "asc" ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue);
2371
+ return direction === "asc" ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue);
2375
2372
  }
2376
- return sortConfig.direction === "asc" ? aValue < bValue ? -1 : aValue > bValue ? 1 : 0 : aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
2377
- });
2373
+ return direction === "asc" ? aValue < bValue ? -1 : aValue > bValue ? 1 : 0 : aValue > bValue ? -1 : aValue < bValue ? 1 : 0;
2374
+ };
2375
+ },
2376
+ [sortConfig?.columnId]
2377
+ );
2378
+ const filteredData = (0, import_react10.useMemo)(() => {
2379
+ if (!data || !Array.isArray(data)) return import_utils9.LazyArray.empty();
2380
+ if (serverSide) {
2381
+ return import_utils9.LazyArray.from(data);
2378
2382
  }
2379
- return result;
2380
- }, [
2381
- data,
2382
- filters,
2383
- sortConfig,
2384
- columns,
2385
- serverSide,
2386
- applyDateCondition,
2387
- applyNumberCondition,
2388
- applyTextCondition
2389
- ]);
2383
+ if (!columns || !Array.isArray(columns)) return import_utils9.LazyArray.from(data);
2384
+ let lazyResult = import_utils9.LazyArray.from(data);
2385
+ const filterChain = new import_utils9.FilterChain();
2386
+ for (const [columnId, filter] of Object.entries(filters)) {
2387
+ if (!filter) continue;
2388
+ const column = columns.find((c) => c.id === columnId);
2389
+ if (!column) continue;
2390
+ filterChain.add(buildFilterPredicate(column, filter));
2391
+ }
2392
+ if (!filterChain.isEmpty) {
2393
+ lazyResult = filterChain.applyTo(lazyResult);
2394
+ }
2395
+ if (sortConfig) {
2396
+ const column = columns.find((c) => c.id === sortConfig.columnId);
2397
+ lazyResult = lazyResult.sort(buildSortComparator(column, sortConfig.direction));
2398
+ }
2399
+ return lazyResult;
2400
+ }, [data, filters, sortConfig, columns, serverSide, buildFilterPredicate, buildSortComparator]);
2390
2401
  const handleFilterChange = (0, import_react10.useCallback)(
2391
2402
  (columnId, filter) => {
2392
2403
  const newFilters = { ...filters };
@@ -2614,6 +2625,10 @@ function useSpreadsheetKeyboardShortcuts({
2614
2625
  onUndo,
2615
2626
  onRedo,
2616
2627
  onEscape,
2628
+ onNavigate,
2629
+ onEnterEditMode,
2630
+ hasFocusedCell = false,
2631
+ isEditing = false,
2617
2632
  customShortcuts = [],
2618
2633
  enabled = true
2619
2634
  } = {}) {
@@ -2626,7 +2641,7 @@ function useSpreadsheetKeyboardShortcuts({
2626
2641
  if (event.key === "Escape") {
2627
2642
  if (showKeyboardShortcuts) {
2628
2643
  setShowKeyboardShortcuts(false);
2629
- } else {
2644
+ } else if (!isEditing) {
2630
2645
  onEscape?.();
2631
2646
  }
2632
2647
  return;
@@ -2646,6 +2661,33 @@ function useSpreadsheetKeyboardShortcuts({
2646
2661
  onRedo?.();
2647
2662
  return;
2648
2663
  }
2664
+ if (hasFocusedCell && !isEditing && !event.metaKey && !event.ctrlKey) {
2665
+ if (event.key === "ArrowUp") {
2666
+ event.preventDefault();
2667
+ onNavigate?.("up");
2668
+ return;
2669
+ }
2670
+ if (event.key === "ArrowDown") {
2671
+ event.preventDefault();
2672
+ onNavigate?.("down");
2673
+ return;
2674
+ }
2675
+ if (event.key === "ArrowLeft") {
2676
+ event.preventDefault();
2677
+ onNavigate?.("left");
2678
+ return;
2679
+ }
2680
+ if (event.key === "ArrowRight") {
2681
+ event.preventDefault();
2682
+ onNavigate?.("right");
2683
+ return;
2684
+ }
2685
+ if (event.key === "F2") {
2686
+ event.preventDefault();
2687
+ onEnterEditMode?.();
2688
+ return;
2689
+ }
2690
+ }
2649
2691
  for (const shortcut of customShortcuts) {
2650
2692
  const modifiersMatch = (!shortcut.modifiers?.meta || event.metaKey) && (!shortcut.modifiers?.ctrl || event.ctrlKey) && (!shortcut.modifiers?.shift || event.shiftKey) && (!shortcut.modifiers?.alt || event.altKey);
2651
2693
  if (event.key === shortcut.key && modifiersMatch) {
@@ -2657,7 +2699,18 @@ function useSpreadsheetKeyboardShortcuts({
2657
2699
  };
2658
2700
  document.addEventListener("keydown", handleKeyDown);
2659
2701
  return () => document.removeEventListener("keydown", handleKeyDown);
2660
- }, [enabled, showKeyboardShortcuts, onUndo, onRedo, onEscape, customShortcuts]);
2702
+ }, [
2703
+ enabled,
2704
+ showKeyboardShortcuts,
2705
+ onUndo,
2706
+ onRedo,
2707
+ onEscape,
2708
+ onNavigate,
2709
+ onEnterEditMode,
2710
+ hasFocusedCell,
2711
+ isEditing,
2712
+ customShortcuts
2713
+ ]);
2661
2714
  const shortcuts = {
2662
2715
  general: [
2663
2716
  { label: "Show keyboard shortcuts", keys: [modifierKey, "/"] },
@@ -2674,6 +2727,13 @@ function useSpreadsheetKeyboardShortcuts({
2674
2727
  { label: "Confirm cell edit", keys: ["Enter"] },
2675
2728
  { label: "Cancel cell edit", keys: ["Escape"] }
2676
2729
  ],
2730
+ navigation: [
2731
+ { label: "Navigate up", keys: ["\u2191"] },
2732
+ { label: "Navigate down", keys: ["\u2193"] },
2733
+ { label: "Navigate left", keys: ["\u2190"] },
2734
+ { label: "Navigate right", keys: ["\u2192"] },
2735
+ { label: "Edit cell", keys: ["F2"] }
2736
+ ],
2677
2737
  rowActions: [
2678
2738
  { label: "Duplicate row", description: "Click duplicate icon" },
2679
2739
  { label: "Highlight row", description: "Click highlight icon" },
@@ -2900,23 +2960,82 @@ function Spreadsheet({
2900
2960
  markAsChanged();
2901
2961
  }
2902
2962
  }, [popRedoEntry, onCellEdit, markAsChanged]);
2963
+ const paginatedData = (0, import_react14.useMemo)(() => {
2964
+ if (serverSide) {
2965
+ return filteredData;
2966
+ }
2967
+ const startIndex = (currentPage - 1) * pageSize;
2968
+ return filteredData.slice(startIndex, startIndex + pageSize);
2969
+ }, [filteredData, currentPage, pageSize, serverSide]);
2970
+ const handleNavigate = (0, import_react14.useCallback)(
2971
+ (direction) => {
2972
+ if (!focusedCell) return;
2973
+ const currentRowIndex = paginatedData.findIndex(
2974
+ (r) => getRowId(r) === focusedCell.rowId
2975
+ );
2976
+ const currentColIndex = visibleColumns.findIndex((c) => c.id === focusedCell.columnId);
2977
+ if (currentRowIndex === -1 || currentColIndex === -1) return;
2978
+ let newRowIndex = currentRowIndex;
2979
+ let newColIndex = currentColIndex;
2980
+ switch (direction) {
2981
+ case "up":
2982
+ newRowIndex = Math.max(0, currentRowIndex - 1);
2983
+ break;
2984
+ case "down":
2985
+ newRowIndex = Math.min(paginatedData.length - 1, currentRowIndex + 1);
2986
+ break;
2987
+ case "left":
2988
+ newColIndex = Math.max(0, currentColIndex - 1);
2989
+ break;
2990
+ case "right":
2991
+ newColIndex = Math.min(visibleColumns.length - 1, currentColIndex + 1);
2992
+ break;
2993
+ }
2994
+ const newRowId = getRowId(paginatedData[newRowIndex]);
2995
+ const newColumnId = visibleColumns[newColIndex].id;
2996
+ setFocusedCell({ rowId: newRowId, columnId: newColumnId });
2997
+ setEditingCell(null);
2998
+ requestAnimationFrame(() => {
2999
+ const cellElement = tableRef.current?.querySelector(
3000
+ `[data-cell-id="${newRowId}-${newColumnId}"]`
3001
+ );
3002
+ if (cellElement) {
3003
+ cellElement.scrollIntoView({
3004
+ behavior: "smooth",
3005
+ block: "nearest",
3006
+ inline: "nearest"
3007
+ });
3008
+ }
3009
+ });
3010
+ },
3011
+ [focusedCell, paginatedData, visibleColumns, getRowId]
3012
+ );
3013
+ const handleEnterEditMode = (0, import_react14.useCallback)(() => {
3014
+ if (!focusedCell) return;
3015
+ const column = (columns || []).find((c) => c.id === focusedCell.columnId);
3016
+ if (column?.editable && enableCellEditing) {
3017
+ const row = (data || []).find((r) => getRowId(r) === focusedCell.rowId);
3018
+ if (row) {
3019
+ const value = column.getValue ? column.getValue(row) : row[focusedCell.columnId];
3020
+ setEditingCell({ rowId: focusedCell.rowId, columnId: focusedCell.columnId });
3021
+ setEditValue(value);
3022
+ }
3023
+ }
3024
+ }, [focusedCell, columns, data, getRowId, enableCellEditing]);
2903
3025
  const { showKeyboardShortcuts, setShowKeyboardShortcuts, shortcuts } = useSpreadsheetKeyboardShortcuts({
2904
3026
  onUndo: applyUndo,
2905
3027
  onRedo: applyRedo,
2906
3028
  onEscape: handleEscapeCallback,
3029
+ onNavigate: handleNavigate,
3030
+ onEnterEditMode: handleEnterEditMode,
3031
+ hasFocusedCell: focusedCell !== null,
3032
+ isEditing: editingCell !== null,
2907
3033
  enabled: true
2908
3034
  });
2909
3035
  const effectiveShowRowIndex = spreadsheetSettings.showRowIndex !== false;
2910
3036
  const rowIndexHighlightColor = getColumnHighlight(ROW_INDEX_COLUMN_ID);
2911
3037
  const tableRef = (0, import_react14.useRef)(null);
2912
3038
  const effectiveTotalItems = serverSide ? totalItems ?? data.length : filteredData.length;
2913
- const paginatedData = (0, import_react14.useMemo)(() => {
2914
- if (serverSide) {
2915
- return filteredData;
2916
- }
2917
- const startIndex = (currentPage - 1) * pageSize;
2918
- return filteredData.slice(startIndex, startIndex + pageSize);
2919
- }, [filteredData, currentPage, pageSize, serverSide]);
2920
3039
  const totalPages = Math.max(1, Math.ceil(effectiveTotalItems / pageSize));
2921
3040
  (0, import_react14.useEffect)(() => {
2922
3041
  if (!serverSide && currentPage > totalPages) {