@xcelsior/ui-spreadsheets 1.3.0 → 1.3.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcelsior/ui-spreadsheets",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -285,7 +285,7 @@ export function Spreadsheet<T extends Record<string, any>>({
285
285
  const [showSettingsModal, setShowSettingsModal] = useState(false);
286
286
 
287
287
  // Filters panel state
288
- const [showFiltersPanel, setShowFiltersPanel] = useState(false);
288
+ const [showFiltersPanel, setShowFiltersPanel] = useState(true);
289
289
 
290
290
  // Undo/Redo hook
291
291
  const {
@@ -465,7 +465,7 @@ export function Spreadsheet<T extends Record<string, any>>({
465
465
  const paginatedData = useMemo(() => {
466
466
  if (serverSide) {
467
467
  // In server-side mode, data is already paginated by the server
468
- return filteredData;
468
+ return filteredData.toArray();
469
469
  }
470
470
  const startIndex = (currentPage - 1) * pageSize;
471
471
  return filteredData.slice(startIndex, startIndex + pageSize);
@@ -718,16 +718,23 @@ export function Spreadsheet<T extends Record<string, any>>({
718
718
  ]
719
719
  );
720
720
 
721
- // Handle cell click - focus only (edit mode requires double-click or F2)
721
+ // Handle cell click - enter edit mode on single click for editable cells
722
+ // Skip edit mode when shift is held (for multi-cell selection)
722
723
  const handleCellClick = useCallback(
723
724
  (rowId: string | number, columnId: string, event: React.MouseEvent) => {
724
725
  event.stopPropagation();
725
726
  handleCellMouseDown(rowId, columnId, event);
727
+ if (!event.shiftKey && !event.metaKey && !event.ctrlKey) {
728
+ const column = columns.find((c) => c.id === columnId);
729
+ if (column?.editable && enableCellEditing) {
730
+ setEditingCell({ rowId, columnId });
731
+ }
732
+ }
726
733
  },
727
- [handleCellMouseDown]
734
+ [handleCellMouseDown, columns, enableCellEditing, setEditingCell]
728
735
  );
729
736
 
730
- // Handle cell double-click - enter edit mode
737
+ // Handle cell double-click - kept for row-level double click handler
731
738
  const handleCellDoubleClick = useCallback(
732
739
  (rowId: string | number, columnId: string) => {
733
740
  const column = columns.find((c) => c.id === columnId);
@@ -1086,17 +1093,62 @@ export function Spreadsheet<T extends Record<string, any>>({
1086
1093
 
1087
1094
  <tbody>
1088
1095
  {isLoading ? (
1089
- <tr>
1090
- <td
1091
- colSpan={columnRenderItems.length + 1}
1092
- className="text-center py-8 text-gray-500"
1093
- >
1094
- <div className="flex items-center justify-center gap-2">
1095
- <div className="w-4 h-4 border-2 border-blue-600 border-t-transparent rounded-full animate-spin" />
1096
- Loading...
1097
- </div>
1098
- </td>
1099
- </tr>
1096
+ Array.from({ length: Math.min(pageSize, 10) }).map((_, rowIdx) => (
1097
+ <tr key={`skeleton-${rowIdx}`}>
1098
+ {/* Row index skeleton */}
1099
+ <td
1100
+ className={cn(
1101
+ 'border border-gray-200 sticky',
1102
+ effectiveCompactMode ? 'px-1.5 py-0.5' : 'px-2.5 py-1.5'
1103
+ )}
1104
+ style={{
1105
+ minWidth: `${ROW_INDEX_COLUMN_WIDTH}px`,
1106
+ width: `${ROW_INDEX_COLUMN_WIDTH}px`,
1107
+ left: 0,
1108
+ zIndex: 40,
1109
+ backgroundColor: rowIdx % 2 !== 0 ? '#f9fafb' : 'white',
1110
+ }}
1111
+ >
1112
+ <div
1113
+ className="h-4 bg-gray-200 rounded animate-pulse"
1114
+ style={{ width: '60%', animationDelay: `${rowIdx * 50}ms` }}
1115
+ />
1116
+ </td>
1117
+ {/* Column skeletons */}
1118
+ {columnRenderItems.map((item, colIdx) => {
1119
+ if (item.type === 'collapsed-placeholder') {
1120
+ return (
1121
+ <td
1122
+ key={`skeleton-${rowIdx}-placeholder-${item.groupId}`}
1123
+ className="border border-gray-200"
1124
+ style={{ backgroundColor: rowIdx % 2 !== 0 ? '#f9fafb' : 'white' }}
1125
+ />
1126
+ );
1127
+ }
1128
+ return (
1129
+ <td
1130
+ key={`skeleton-${rowIdx}-${item.column.id}`}
1131
+ className={cn(
1132
+ 'border border-gray-200',
1133
+ effectiveCompactMode ? 'px-1.5 py-0.5' : 'px-2.5 py-1.5'
1134
+ )}
1135
+ style={{
1136
+ minWidth: item.column.width ?? 120,
1137
+ backgroundColor: rowIdx % 2 !== 0 ? '#f9fafb' : 'white',
1138
+ }}
1139
+ >
1140
+ <div
1141
+ className="h-4 bg-gray-200 rounded animate-pulse"
1142
+ style={{
1143
+ width: `${55 + ((rowIdx * 7 + colIdx * 13) % 35)}%`,
1144
+ animationDelay: `${(rowIdx * columnRenderItems.length + colIdx) * 30}ms`,
1145
+ }}
1146
+ />
1147
+ </td>
1148
+ );
1149
+ })}
1150
+ </tr>
1151
+ ))
1100
1152
  ) : paginatedData.length === 0 ? (
1101
1153
  <tr>
1102
1154
  <td
@@ -365,9 +365,11 @@ const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
365
365
  }
366
366
 
367
367
  if (column.type === 'autocomplete') {
368
- if (column.getOptionLabel) return column.getOptionLabel(value, row);
369
- const match = column.autocompleteOptions?.find((o) => o.value === value);
370
- return match ? match.label : String(value);
368
+ // Use localValue to reflect pending changes before parent re-renders with new data
369
+ const displayVal = localValue ?? value;
370
+ if (column.getOptionLabel) return column.getOptionLabel(displayVal, row);
371
+ const match = column.autocompleteOptions?.find((o) => o.value === displayVal);
372
+ return match ? match.label : (displayVal != null && displayVal !== '' ? String(displayVal) : null);
371
373
  }
372
374
 
373
375
  return String(value);
@@ -386,7 +388,10 @@ const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
386
388
  value={localValue}
387
389
  column={column}
388
390
  compactMode={compactMode}
389
- onConfirm={onConfirm}
391
+ onConfirm={(newVal) => {
392
+ setLocalValue(newVal);
393
+ onConfirm?.(newVal);
394
+ }}
390
395
  onCancel={onCancel}
391
396
  />
392
397
  );
@@ -252,7 +252,12 @@ export function useSpreadsheetFiltering<T extends Record<string, any>>({
252
252
 
253
253
  // Text condition filter (advanced)
254
254
  if (filter.textCondition) {
255
- return applyTextCondition(value, filter.textCondition);
255
+ // For autocomplete columns, filter against the display label instead of the raw value
256
+ const filterableValue =
257
+ column.type === 'autocomplete' && column.getOptionLabel
258
+ ? column.getOptionLabel(value, row)
259
+ : value;
260
+ return applyTextCondition(filterableValue, filter.textCondition);
256
261
  }
257
262
 
258
263
  // Number condition filter (advanced)
@@ -89,6 +89,15 @@ export function useSpreadsheetKeyboardShortcuts({
89
89
  if (!enabled) return;
90
90
 
91
91
  const handleKeyDown = (event: KeyboardEvent) => {
92
+ // Skip keyboard shortcuts when user is typing in a form element (e.g., filter dropdown inputs)
93
+ const activeEl = document.activeElement;
94
+ const isInFormElement = activeEl instanceof HTMLInputElement
95
+ || activeEl instanceof HTMLTextAreaElement
96
+ || activeEl instanceof HTMLSelectElement;
97
+ if (isInFormElement && event.key !== 'Escape') {
98
+ return;
99
+ }
100
+
92
101
  // Escape key
93
102
  if (event.key === 'Escape') {
94
103
  if (showKeyboardShortcuts) {
@@ -39,8 +39,8 @@ export function useSpreadsheetSummary<T>({
39
39
 
40
40
  for (const { position, value } of selectedCellValues) {
41
41
  const column = columns.find((c) => c.id === position.columnId);
42
- // Include values from numeric columns or any parseable number
43
- if (column?.type === 'number' || typeof value === 'number') {
42
+ // Only include values from columns explicitly typed as 'number'
43
+ if (column?.type === 'number') {
44
44
  const num = typeof value === 'number' ? value : parseFloat(value);
45
45
  if (!isNaN(num)) {
46
46
  numericValues.push(num);