@xcelsior/ui-spreadsheets 1.0.6 → 1.0.8

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.
@@ -28,37 +28,37 @@ const cellPaddingNormal = 'px-2 py-1';
28
28
  * ```
29
29
  */
30
30
  const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
31
- value,
32
- column,
33
- row,
34
- rowIndex,
35
- rowId,
36
- isEditable = false,
37
- isEditing = false,
38
- isFocused = false,
39
- isRowSelected = false,
40
- isRowHovered = false,
41
- highlightColor,
42
- hasComments = false,
43
- unresolvedCommentCount = 0,
44
- isCopied = false,
45
- compactMode = false,
46
- isPinned = false,
47
- pinSide,
48
- leftOffset = 0,
49
- rightOffset = 0,
50
- onClick,
51
- onChange,
52
- onConfirm,
53
- onCancel,
54
- onCopyDown,
55
- onCopyToSelected,
56
- onHighlight,
57
- onAddComment,
58
- onViewComments,
59
- hasSelectedRows = false,
60
- className,
61
- }) => {
31
+ value,
32
+ column,
33
+ row,
34
+ rowIndex,
35
+ rowId,
36
+ isEditable = false,
37
+ isEditing = false,
38
+ isFocused = false,
39
+ isRowSelected = false,
40
+ isRowHovered = false,
41
+ highlightColor,
42
+ hasComments = false,
43
+ unresolvedCommentCount = 0,
44
+ isCopied = false,
45
+ compactMode = false,
46
+ isPinned = false,
47
+ pinSide,
48
+ leftOffset = 0,
49
+ rightOffset = 0,
50
+ onClick,
51
+ onChange,
52
+ onConfirm,
53
+ onCancel,
54
+ onCopyDown,
55
+ onCopyToSelected,
56
+ onHighlight,
57
+ onAddComment,
58
+ onViewComments,
59
+ hasSelectedRows = false,
60
+ className,
61
+ }) => {
62
62
  const [localValue, setLocalValue] = useState(value);
63
63
  const inputRef = useRef<HTMLInputElement>(null);
64
64
  const selectRef = useRef<HTMLSelectElement>(null);
@@ -83,12 +83,11 @@ const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
83
83
  const handleKeyDown = (e: React.KeyboardEvent) => {
84
84
  if (e.key === 'Enter') {
85
85
  e.preventDefault();
86
- onConfirm?.();
86
+ onConfirm?.(localValue);
87
87
  } else if (e.key === 'Escape') {
88
88
  e.preventDefault();
89
89
  e.stopPropagation(); // Prevent global escape handler from clearing focus
90
90
  setLocalValue(value);
91
- onChange?.(value);
92
91
  onCancel?.();
93
92
  }
94
93
  };
@@ -126,60 +125,64 @@ const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
126
125
  const renderEditInput = () => {
127
126
  if (column.type === 'select' && column.options) {
128
127
  return (
129
- <select
130
- ref={selectRef}
128
+ <select
129
+ ref={selectRef}
130
+ value={localValue ?? ''}
131
+ onChange={(e) => {
132
+ const newValue = e.target.value;
133
+ setLocalValue(newValue);
134
+ onConfirm?.(newValue);
135
+ }}
136
+ onKeyDown={handleKeyDown}
137
+ onBlur={() => onConfirm?.(localValue)}
138
+ className={cn(
139
+ 'w-full border border-gray-300 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500',
140
+ compactMode ? 'px-1 py-0.5' : 'px-2 py-1'
141
+ )}
142
+ >
143
+ {column.options.map((option) => (
144
+ <option key={option} value={option}>
145
+ {option}
146
+ </option>
147
+ ))}
148
+ </select>
149
+ );
150
+ }
151
+
152
+ return (
153
+ <input
154
+ ref={inputRef}
155
+ type={column.type === 'number' ? 'number' : 'text'}
156
+ step={column.type === 'number' ? '0.01' : undefined}
131
157
  value={localValue ?? ''}
132
158
  onChange={(e) => {
133
- setLocalValue(e.target.value);
134
- onChange?.(e.target.value);
135
- onConfirm?.();
159
+ const newValue =
160
+ column.type === 'number'
161
+ ? e.target.value === ''
162
+ ? ''
163
+ : parseFloat(e.target.value)
164
+ : e.target.value;
165
+ setLocalValue(newValue);
136
166
  }}
137
167
  onKeyDown={handleKeyDown}
138
- onBlur={() => onConfirm?.()}
168
+ onBlur={() => onConfirm?.(localValue)}
169
+ autoComplete="off"
170
+ autoCorrect="off"
171
+ autoCapitalize="off"
172
+ spellCheck={false}
139
173
  className={cn(
140
- 'w-full border border-gray-300 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500',
141
- compactMode ? 'px-1 py-0.5' : 'px-2 py-1'
174
+ 'w-full border border-gray-300 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500 bg-yellow-50',
175
+ compactMode ? 'px-1 py-0.5' : 'px-2 py-1'
142
176
  )}
143
- >
144
- {column.options.map((option) => (
145
- <option key={option} value={option}>
146
- {option}
147
- </option>
148
- ))}
149
- </select>
150
- );
151
- }
152
-
153
- return (
154
- <input
155
- ref={inputRef}
156
- type={column.type === 'number' ? 'number' : 'text'}
157
- step={column.type === 'number' ? '0.01' : undefined}
158
- value={localValue ?? ''}
159
- onChange={(e) => {
160
- const newValue =
161
- column.type === 'number'
162
- ? e.target.value === ''
163
- ? ''
164
- : parseFloat(e.target.value)
165
- : e.target.value;
166
- setLocalValue(newValue);
167
- onChange?.(newValue);
168
- }}
169
- onKeyDown={handleKeyDown}
170
- onBlur={() => onConfirm?.()}
171
- className={cn(
172
- 'w-full border border-gray-300 rounded text-xs focus:outline-none focus:ring-1 focus:ring-blue-500 bg-yellow-50',
173
- compactMode ? 'px-1 py-0.5' : 'px-2 py-1'
174
- )}
175
- />
177
+ />
176
178
  );
177
179
  };
178
180
 
179
181
  const cellPadding = compactMode ? cellPaddingCompact : cellPaddingNormal;
180
182
 
181
183
  const handleCellKeyDown = (e: React.KeyboardEvent<HTMLTableCellElement>) => {
182
- if (e.key === ' ') {
184
+ // Only handle space key when not editing to allow typing spaces in inputs
185
+ if (e.key === ' ' && !isEditing) {
183
186
  e.preventDefault();
184
187
  onClick?.(e as unknown as React.MouseEvent);
185
188
  }
@@ -198,135 +201,135 @@ const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
198
201
  }
199
202
 
200
203
  return (
201
- <td
202
- onClick={onClick}
203
- onKeyDown={handleCellKeyDown}
204
- data-cell-id={`${rowId}-${column.id}`}
205
- className={cn(
206
- 'border border-gray-200 text-xs group cursor-pointer transition-colors',
207
- cellPadding,
208
- column.align === 'right' && 'text-right',
209
- column.align === 'center' && 'text-center',
210
- isCopied && 'animate-pulse',
211
- isFocused && 'ring-2 ring-blue-500 ring-inset',
212
- isPinned ? 'z-20' : 'z-0',
213
- className
214
- )}
215
- style={{
216
- backgroundColor: getBackgroundColor(),
217
- minWidth: column.minWidth || column.width,
218
- ...positionStyles,
219
- }}
220
- >
221
- {isEditing ? (
222
- renderEditInput()
223
- ) : (
224
- <div className="flex items-center gap-1 relative">
225
- {/* Main content */}
226
- <div
227
- className={cn(
228
- 'flex-1 truncate',
229
- isEditable &&
230
- 'cursor-text hover:bg-gray-50 px-0.5 rounded min-h-[18px] flex items-center bg-yellow-50/50'
231
- )}
232
- title={String(value ?? '')}
233
- >
234
- {renderContent()}
235
- </div>
236
-
237
- {/* Action buttons - show on hover, except comment indicator which is always visible */}
238
- <div className="flex items-center gap-0.5 shrink-0">
239
- {/* Copy down button - hover only */}
240
- {value !== null && value !== undefined && value !== '' && onCopyDown && (
241
- <button
242
- type="button"
243
- onClick={(e) => {
244
- e.stopPropagation();
245
- onCopyDown();
246
- }}
247
- className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-gray-100 hover:bg-gray-200 rounded"
248
- title="Copy value down to rows below"
249
- >
250
- <HiOutlineClipboardCopy className="h-2.5 w-2.5 text-gray-500" />
251
- </button>
252
- )}
204
+ <td
205
+ onClick={onClick}
206
+ onKeyDown={handleCellKeyDown}
207
+ data-cell-id={`${rowId}-${column.id}`}
208
+ className={cn(
209
+ 'border border-gray-200 text-xs group cursor-pointer transition-colors',
210
+ cellPadding,
211
+ column.align === 'right' && 'text-right',
212
+ column.align === 'center' && 'text-center',
213
+ isCopied && 'animate-pulse',
214
+ isFocused && 'ring-2 ring-blue-500 ring-inset',
215
+ isPinned ? 'z-20' : 'z-0',
216
+ className
217
+ )}
218
+ style={{
219
+ backgroundColor: getBackgroundColor(),
220
+ minWidth: column.minWidth || column.width,
221
+ ...positionStyles,
222
+ }}
223
+ >
224
+ {isEditing ? (
225
+ renderEditInput()
226
+ ) : (
227
+ <div className="flex items-center gap-1 relative">
228
+ {/* Main content */}
229
+ <div
230
+ className={cn(
231
+ 'flex-1 truncate',
232
+ isEditable &&
233
+ 'cursor-text hover:bg-gray-50 px-0.5 rounded min-h-[18px] flex items-center bg-yellow-50/50'
234
+ )}
235
+ title={String(value ?? '')}
236
+ >
237
+ {renderContent()}
238
+ </div>
253
239
 
254
- {/* Copy to selected button - hover only */}
255
- {hasSelectedRows &&
256
- value !== null &&
257
- value !== undefined &&
258
- value !== '' &&
259
- onCopyToSelected && (
260
- <button
261
- type="button"
262
- onClick={(e) => {
263
- e.stopPropagation();
264
- onCopyToSelected();
265
- }}
266
- className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-green-100 hover:bg-green-200 rounded"
267
- title="Copy to selected rows"
268
- >
269
- <HiOutlineClipboardCheck className="h-2.5 w-2.5 text-green-600" />
270
- </button>
271
- )}
240
+ {/* Action buttons - show on hover, except comment indicator which is always visible */}
241
+ <div className="flex items-center gap-0.5 shrink-0">
242
+ {/* Copy down button - hover only */}
243
+ {value !== null && value !== undefined && value !== '' && onCopyDown && (
244
+ <button
245
+ type="button"
246
+ onClick={(e) => {
247
+ e.stopPropagation();
248
+ onCopyDown();
249
+ }}
250
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-gray-100 hover:bg-gray-200 rounded"
251
+ title="Copy value down to rows below"
252
+ >
253
+ <HiOutlineClipboardCopy className="h-2.5 w-2.5 text-gray-500" />
254
+ </button>
255
+ )}
272
256
 
273
- {/* Highlight button - hover only */}
274
- {onHighlight && (
275
- <button
276
- type="button"
277
- onClick={(e) => {
278
- e.stopPropagation();
279
- onHighlight();
280
- }}
281
- className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-gray-100 hover:bg-gray-200 rounded"
282
- title="Highlight cell"
283
- >
284
- <AiFillHighlight
285
- className={cn(
286
- 'h-2.5 w-2.5',
287
- highlightColor ? 'text-amber-500' : 'text-gray-500'
257
+ {/* Copy to selected button - hover only */}
258
+ {hasSelectedRows &&
259
+ value !== null &&
260
+ value !== undefined &&
261
+ value !== '' &&
262
+ onCopyToSelected && (
263
+ <button
264
+ type="button"
265
+ onClick={(e) => {
266
+ e.stopPropagation();
267
+ onCopyToSelected();
268
+ }}
269
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-green-100 hover:bg-green-200 rounded"
270
+ title="Copy to selected rows"
271
+ >
272
+ <HiOutlineClipboardCheck className="h-2.5 w-2.5 text-green-600" />
273
+ </button>
288
274
  )}
289
- />
290
- </button>
291
- )}
292
275
 
293
- {/* Comment button - always visible when has comments, hover only when adding */}
294
- {hasComments && onViewComments ? (
295
- <button
296
- type="button"
297
- onClick={(e) => {
298
- e.stopPropagation();
299
- onViewComments();
300
- }}
301
- className="p-0.5 bg-amber-100 hover:bg-amber-200 rounded transition-colors flex items-center gap-0.5"
302
- title={`${unresolvedCommentCount} comment(s) - click to view`}
303
- >
304
- <FaComment className="h-2.5 w-2.5 text-amber-500" />
305
- {unresolvedCommentCount > 0 && (
306
- <span className="text-[9px] font-medium text-amber-600">
276
+ {/* Highlight button - hover only */}
277
+ {onHighlight && (
278
+ <button
279
+ type="button"
280
+ onClick={(e) => {
281
+ e.stopPropagation();
282
+ onHighlight();
283
+ }}
284
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-gray-100 hover:bg-gray-200 rounded"
285
+ title="Highlight cell"
286
+ >
287
+ <AiFillHighlight
288
+ className={cn(
289
+ 'h-2.5 w-2.5',
290
+ highlightColor ? 'text-amber-500' : 'text-gray-500'
291
+ )}
292
+ />
293
+ </button>
294
+ )}
295
+
296
+ {/* Comment button - always visible when has comments, hover only when adding */}
297
+ {hasComments && onViewComments ? (
298
+ <button
299
+ type="button"
300
+ onClick={(e) => {
301
+ e.stopPropagation();
302
+ onViewComments();
303
+ }}
304
+ className="p-0.5 bg-amber-100 hover:bg-amber-200 rounded transition-colors flex items-center gap-0.5"
305
+ title={`${unresolvedCommentCount} comment(s) - click to view`}
306
+ >
307
+ <FaComment className="h-2.5 w-2.5 text-amber-500" />
308
+ {unresolvedCommentCount > 0 && (
309
+ <span className="text-[9px] font-medium text-amber-600">
307
310
  {unresolvedCommentCount > 99
308
- ? '99+'
309
- : unresolvedCommentCount}
311
+ ? '99+'
312
+ : unresolvedCommentCount}
310
313
  </span>
311
- )}
312
- </button>
313
- ) : onAddComment ? (
314
- <button
315
- type="button"
316
- onClick={(e) => {
317
- e.stopPropagation();
318
- onAddComment();
319
- }}
320
- className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-gray-100 hover:bg-gray-200 rounded"
321
- title="Add comment"
322
- >
323
- <FaRegComment className="h-2.5 w-2.5 text-gray-500" />
324
- </button>
325
- ) : null}
314
+ )}
315
+ </button>
316
+ ) : onAddComment ? (
317
+ <button
318
+ type="button"
319
+ onClick={(e) => {
320
+ e.stopPropagation();
321
+ onAddComment();
322
+ }}
323
+ className="opacity-0 group-hover:opacity-100 transition-opacity p-0.5 bg-gray-100 hover:bg-gray-200 rounded"
324
+ title="Add comment"
325
+ >
326
+ <FaRegComment className="h-2.5 w-2.5 text-gray-500" />
327
+ </button>
328
+ ) : null}
329
+ </div>
326
330
  </div>
327
- </div>
328
- )}
329
- </td>
331
+ )}
332
+ </td>
330
333
  );
331
334
  };
332
335
 
package/src/types.ts CHANGED
@@ -312,6 +312,8 @@ export interface SpreadsheetProps<T = any> {
312
312
  onRowDoubleClick?: (row: T, rowIndex: number) => void;
313
313
  /** Callback when a row is cloned/duplicated */
314
314
  onRowClone?: (row: T, rowId: string | number) => void;
315
+ /** Callback when a row is deleted */
316
+ onRowDelete?: (row: T, rowId: string | number) => void;
315
317
  /** Callback when a cell comment is added */
316
318
  onAddCellComment?: (rowId: string | number, columnId: string, comment: string) => void;
317
319
  /** Callback when row highlight is toggled */
@@ -439,10 +441,10 @@ export interface SpreadsheetCellProps {
439
441
  rightOffset?: number;
440
442
  /** Callback when cell is clicked */
441
443
  onClick?: (event: React.MouseEvent) => void;
442
- /** Callback when cell value changes */
444
+ /** Callback when cell value changes (only called on blur/confirm, not during typing) */
443
445
  onChange?: (newValue: any) => void;
444
- /** Callback when editing is confirmed */
445
- onConfirm?: () => void;
446
+ /** Callback when editing is confirmed, receives the final value */
447
+ onConfirm?: (finalValue?: any) => void;
446
448
  /** Callback when editing is cancelled */
447
449
  onCancel?: () => void;
448
450
  /** Callback to copy value down */
@@ -1,61 +0,0 @@
1
-
2
- > @xcelsior/ui-spreadsheets@1.0.3 lint /Users/tuannguyen/Work/xcelsior-packages/packages/ui/ui-spreadsheets
3
- > biome check . && tsc
4
-
5
-  at src/components/Spreadsheet.tsx:779:49 ]8;;https://biomejs.dev/linter/rules/use-key-with-click-events\lint/a11y/useKeyWithClickEvents]8;;\ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
6
-
7
-  ⚠ Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.
8
-  
9
-   777 │                                             {/* Row Index Column */}
10
-   778 │                                             {effectiveShowRowIndex && (
11
-  > 779 │                                                 <td
12
-      │                                                 ^^^
13
-  > 780 │                                                     onClick={(e) => handleRowSelect(rowId, e)}
14
-      ...
15
-  > 806 │                                                     }}
16
-  > 807 │                                                 >
17
-      │                                                 ^
18
-   808 │                                                     <div className={'relative'}>
19
-   809 │                                                         {/* Row number */}
20
-  
21
-  ℹ Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.
22
-  
23
- 
24
-  at src/components/SpreadsheetFilterDropdown.tsx:188:9 ]8;;https://biomejs.dev/linter/rules/no-static-element-interactions\lint/a11y/noStaticElementInteractions]8;;\ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
25
-
26
-  ⚠ Static Elements should not be interactive.
27
-  
28
-   187 │     return (
29
-  > 188 │         <div
30
-      │         ^^^^
31
-  > 189 │             ref={dropdownRef}
32
-      ...
33
-  > 194 │             onClick={(e) => e.stopPropagation()}
34
-  > 195 │         >
35
-      │         ^
36
-   196 │             <div className="px-3 py-2 border-b border-gray-200 bg-gray-50">
37
-   197 │                 <span className="text-xs font-medium text-gray-700">Filter: {column.label}</span>
38
-  
39
-  ℹ To add interactivity such as a mouse or key event listener to a static element, give the element an appropriate role value.
40
-  
41
- 
42
-  at src/components/SpreadsheetFilterDropdown.tsx:188:9 ]8;;https://biomejs.dev/linter/rules/use-key-with-click-events\lint/a11y/useKeyWithClickEvents]8;;\ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
43
-
44
-  ⚠ Enforce to have the onClick mouse event with the onKeyUp, the onKeyDown, or the onKeyPress keyboard event.
45
-  
46
-   187 │     return (
47
-  > 188 │         <div
48
-      │         ^^^^
49
-  > 189 │             ref={dropdownRef}
50
-      ...
51
-  > 194 │             onClick={(e) => e.stopPropagation()}
52
-  > 195 │         >
53
-      │         ^
54
-   196 │             <div className="px-3 py-2 border-b border-gray-200 bg-gray-50">
55
-   197 │                 <span className="text-xs font-medium text-gray-700">Filter: {column.label}</span>
56
-  
57
-  ℹ Actions triggered using mouse events should have corresponding keyboard events to account for keyboard-only navigation.
58
-  
59
- 
60
- Checked 29 files in 11ms. No fixes applied.
61
- Found 3 warnings.