@xcelsior/ui-spreadsheets 1.1.1 → 1.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xcelsior/ui-spreadsheets",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -1,4 +1,3 @@
1
- import type React from 'react';
2
1
  import { HiFilter } from 'react-icons/hi';
3
2
  import { AiFillHighlight } from 'react-icons/ai';
4
3
  import { MdOutlinePushPin, MdPushPin } from 'react-icons/md';
@@ -55,21 +54,8 @@ export function ColumnHeaderActions({
55
54
  unpinnedTitle = 'Pin column',
56
55
  className,
57
56
  }: ColumnHeaderActionsProps) {
58
- const handleClick = (e: React.MouseEvent) => {
59
- e.stopPropagation();
60
- };
61
-
62
- const handleKeyDown = (e: React.KeyboardEvent) => {
63
- e.stopPropagation();
64
- };
65
-
66
57
  return (
67
- <button
68
- type="button"
69
- className={cn('flex items-center gap-0.5', className)}
70
- onClick={handleClick}
71
- onKeyDown={handleKeyDown}
72
- >
58
+ <div className={cn('flex items-center gap-0.5', className)}>
73
59
  {/* Filter button */}
74
60
  {enableFiltering && onFilterClick && (
75
61
  <button
@@ -133,7 +119,7 @@ export function ColumnHeaderActions({
133
119
  )}
134
120
  </button>
135
121
  )}
136
- </button>
122
+ </div>
137
123
  );
138
124
  }
139
125
 
@@ -215,6 +215,7 @@ A feature-rich spreadsheet component with Excel-like functionality.
215
215
  getRowId: { control: false },
216
216
  onCellsEdit: { action: 'onCellsEdit' },
217
217
  onSelectionChange: { action: 'onSelectionChange' },
218
+ onSettingsChange: { action: 'onSettingsChange' },
218
219
  onSortChange: { action: 'onSortChange' },
219
220
  onFilterChange: { action: 'onFilterChange' },
220
221
  onRowClick: { action: 'onRowClick' },
@@ -146,7 +146,8 @@ export function Spreadsheet<T extends Record<string, any>>({
146
146
  onSortChange,
147
147
  serverSide,
148
148
  controlledFilters,
149
- controlledSortConfig: controlledSortConfig ?? spreadsheetSettings?.defaultSort,
149
+ controlledSortConfig,
150
+ defaultSortConfig: spreadsheetSettings.defaultSort,
150
151
  });
151
152
 
152
153
  // Highlighting hook
@@ -183,6 +184,7 @@ export function Spreadsheet<T extends Record<string, any>>({
183
184
  } = useSpreadsheetPinning({
184
185
  columns,
185
186
  columnGroups,
187
+ defaultPinnedColumns: initialSettings?.defaultPinnedColumns,
186
188
  });
187
189
 
188
190
  // Comments hook
@@ -271,6 +273,35 @@ export function Spreadsheet<T extends Record<string, any>>({
271
273
  }));
272
274
  }, [sortConfig]);
273
275
 
276
+ // Sync pinned columns to spreadsheetSettings when pinning changes
277
+ useEffect(() => {
278
+ const pinnedColumnIds = Array.from(pinnedColumns.keys());
279
+ setSpreadsheetSettings((prev) => {
280
+ // Only update if the arrays are different to avoid unnecessary re-renders
281
+ const prevIds = prev.defaultPinnedColumns;
282
+ if (
283
+ prevIds.length === pinnedColumnIds.length &&
284
+ prevIds.every((id, idx) => id === pinnedColumnIds[idx])
285
+ ) {
286
+ return prev;
287
+ }
288
+ return {
289
+ ...prev,
290
+ defaultPinnedColumns: pinnedColumnIds,
291
+ };
292
+ });
293
+ }, [pinnedColumns]);
294
+
295
+ // Notify parent when settings change (skip initial render)
296
+ const isInitialMount = useRef(true);
297
+ useEffect(() => {
298
+ if (isInitialMount.current) {
299
+ isInitialMount.current = false;
300
+ return;
301
+ }
302
+ onSettingsChange?.(spreadsheetSettings);
303
+ }, [spreadsheetSettings, onSettingsChange]);
304
+
274
305
  const applyUndo = useCallback(() => {
275
306
  const entry = popUndoEntry();
276
307
  if (!entry || !onCellsEdit) return;
@@ -231,7 +231,7 @@ const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
231
231
  onKeyDown={handleCellKeyDown}
232
232
  data-cell-id={`${rowId}-${column.id}`}
233
233
  className={cn(
234
- 'border border-gray-200 group cursor-pointer select-none',
234
+ 'border border-gray-200 group cursor-pointer transition-colors select-none',
235
235
  compactMode ? 'text-[10px]' : 'text-xs',
236
236
  cellPadding,
237
237
  column.align === 'right' && 'text-right',
@@ -6,6 +6,7 @@ import { isBlankValue } from '../utils';
6
6
  export interface UseSpreadsheetFilteringOptions<T> {
7
7
  data: T[];
8
8
  columns: SpreadsheetColumn<T>[];
9
+ defaultSortConfig?: SpreadsheetSortConfig | null;
9
10
  onFilterChange?: (filters: Record<string, SpreadsheetColumnFilter>) => void;
10
11
  onSortChange?: (sortConfig: SpreadsheetSortConfig | null) => void;
11
12
  /**
@@ -52,13 +53,14 @@ export function useSpreadsheetFiltering<T extends Record<string, any>>({
52
53
  serverSide = false,
53
54
  controlledFilters,
54
55
  controlledSortConfig,
56
+ defaultSortConfig,
55
57
  }: UseSpreadsheetFilteringOptions<T>): UseSpreadsheetFilteringReturn<T> {
56
58
  // Internal state for uncontrolled mode
57
59
  const [internalFilters, setInternalFilters] = useState<Record<string, SpreadsheetColumnFilter>>(
58
60
  {}
59
61
  );
60
62
  const [internalSortConfig, setInternalSortConfig] = useState<SpreadsheetSortConfig | null>(
61
- null
63
+ defaultSortConfig ?? null,
62
64
  );
63
65
  const [activeFilterColumn, setActiveFilterColumn] = useState<string | null>(null);
64
66
 
@@ -10,7 +10,6 @@ export interface UseSpreadsheetPinningOptions<T> {
10
10
  columnGroups?: SpreadsheetColumnGroup[];
11
11
  showRowIndex?: boolean;
12
12
  defaultPinnedColumns?: string[];
13
- defaultPinRowIndex?: boolean;
14
13
  }
15
14
 
16
15
  export interface UseSpreadsheetPinningReturn<T> {
@@ -26,14 +25,11 @@ export interface UseSpreadsheetPinningReturn<T> {
26
25
 
27
26
  // Actions
28
27
  handleTogglePin: (columnId: string) => void;
29
- handleToggleRowIndexPin: () => void;
30
28
  handleToggleGroupCollapse: (groupId: string) => void;
31
29
  setPinnedColumnsFromIds: (columnIds: string[]) => void;
32
- setRowIndexPinned: (pinned: boolean) => void;
33
30
 
34
31
  // Offset calculations
35
32
  getColumnLeftOffset: (columnId: string) => number;
36
- getRowIndexLeftOffset: () => number;
37
33
 
38
34
  // Utility
39
35
  isColumnPinned: (columnId: string) => boolean;
@@ -45,26 +41,22 @@ export function useSpreadsheetPinning<T>({
45
41
  columnGroups,
46
42
  showRowIndex = true,
47
43
  defaultPinnedColumns = [],
48
- defaultPinRowIndex = false,
49
44
  }: UseSpreadsheetPinningOptions<T>): UseSpreadsheetPinningReturn<T> {
50
- // Initialize pinned columns from defaults (excluding row index which is handled separately)
45
+ // Initialize pinned columns from defaults (including row index)
51
46
  const [pinnedColumns, setPinnedColumns] = useState<Map<string, 'left' | 'right'>>(() => {
52
47
  const map = new Map<string, 'left' | 'right'>();
53
48
  defaultPinnedColumns.forEach((col) => {
54
- if (col !== ROW_INDEX_COLUMN_ID) {
55
- map.set(col, 'left');
56
- }
49
+ map.set(col, 'left');
57
50
  });
58
51
  return map;
59
52
  });
60
53
 
61
- // Check if row index should be pinned from either defaultPinRowIndex or defaultPinnedColumns
62
- const [isRowIndexPinned, setIsRowIndexPinned] = useState(
63
- defaultPinRowIndex || defaultPinnedColumns.includes(ROW_INDEX_COLUMN_ID)
64
- );
65
54
  const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set());
66
55
 
67
- // Toggle column pin
56
+ // Derive isRowIndexPinned from pinnedColumns for convenience
57
+ const isRowIndexPinned = pinnedColumns.has(ROW_INDEX_COLUMN_ID);
58
+
59
+ // Toggle column pin (works for any column including row index)
68
60
  const handleTogglePin = useCallback((columnId: string) => {
69
61
  setPinnedColumns((prev) => {
70
62
  const newMap = new Map(prev);
@@ -77,27 +69,13 @@ export function useSpreadsheetPinning<T>({
77
69
  });
78
70
  }, []);
79
71
 
80
- // Toggle row index pin
81
- const handleToggleRowIndexPin = useCallback(() => {
82
- setIsRowIndexPinned((prev) => !prev);
83
- }, []);
84
-
85
72
  // Set pinned columns from an array of column IDs
86
73
  const setPinnedColumnsFromIds = useCallback((columnIds: string[]) => {
87
74
  const map = new Map<string, 'left' | 'right'>();
88
75
  columnIds.forEach((col) => {
89
- if (col !== ROW_INDEX_COLUMN_ID) {
90
- map.set(col, 'left');
91
- }
76
+ map.set(col, 'left');
92
77
  });
93
78
  setPinnedColumns(map);
94
- // Also update row index pinned state
95
- setIsRowIndexPinned(columnIds.includes(ROW_INDEX_COLUMN_ID));
96
- }, []);
97
-
98
- // Set row index pinned state directly
99
- const setRowIndexPinned = useCallback((pinned: boolean) => {
100
- setIsRowIndexPinned(pinned);
101
79
  }, []);
102
80
 
103
81
  // Toggle group collapse
@@ -132,8 +110,11 @@ export function useSpreadsheetPinning<T>({
132
110
  });
133
111
  }
134
112
 
135
- // If no columns are pinned, return result as-is to preserve original order
136
- if (pinnedColumns.size === 0) {
113
+ // If no columns are pinned (excluding row index), return result as-is to preserve original order
114
+ const nonRowIndexPinned = Array.from(pinnedColumns.keys()).filter(
115
+ (id) => id !== ROW_INDEX_COLUMN_ID
116
+ );
117
+ if (nonRowIndexPinned.length === 0) {
137
118
  return result;
138
119
  }
139
120
 
@@ -144,10 +125,10 @@ export function useSpreadsheetPinning<T>({
144
125
 
145
126
  // Maintain the order of pinned columns as they were added
146
127
  const pinnedLeftIds = Array.from(pinnedColumns.entries())
147
- .filter(([, side]) => side === 'left')
128
+ .filter(([id, side]) => side === 'left' && id !== ROW_INDEX_COLUMN_ID)
148
129
  .map(([id]) => id);
149
130
  const pinnedRightIds = Array.from(pinnedColumns.entries())
150
- .filter(([, side]) => side === 'right')
131
+ .filter(([id, side]) => side === 'right' && id !== ROW_INDEX_COLUMN_ID)
151
132
  .map(([id]) => id);
152
133
 
153
134
  for (const column of result) {
@@ -168,32 +149,35 @@ export function useSpreadsheetPinning<T>({
168
149
  return [...leftPinned, ...unpinned, ...rightPinned];
169
150
  }, [columns, columnGroups, collapsedGroups, pinnedColumns]);
170
151
 
171
- // Get left offset for row index column
172
- const getRowIndexLeftOffset = useCallback((): number => {
173
- return 0;
174
- }, []);
175
-
176
152
  // Calculate column offset for sticky positioning
177
153
  const getColumnLeftOffset = useCallback(
178
154
  (columnId: string): number => {
155
+ // Row index column is always at left: 0 when pinned
156
+ if (columnId === ROW_INDEX_COLUMN_ID) {
157
+ return 0;
158
+ }
159
+
160
+ // Get left-pinned columns (excluding row index)
179
161
  const pinnedLeft = Array.from(pinnedColumns.entries())
180
- .filter(([, side]) => side === 'left')
162
+ .filter(([id, side]) => side === 'left' && id !== ROW_INDEX_COLUMN_ID)
181
163
  .map(([id]) => id);
182
164
  const index = pinnedLeft.indexOf(columnId);
183
165
 
184
166
  // Base offset includes the row index column if shown and pinned
185
- const baseOffset = showRowIndex && isRowIndexPinned ? ROW_INDEX_COLUMN_WIDTH : 0;
167
+ const isRowIndexPinnedNow = pinnedColumns.has(ROW_INDEX_COLUMN_ID);
168
+ const baseOffset = showRowIndex && isRowIndexPinnedNow ? ROW_INDEX_COLUMN_WIDTH : 0;
186
169
 
187
170
  if (index === -1) return baseOffset;
188
171
 
189
172
  let offset = baseOffset;
190
173
  for (let i = 0; i < index; i++) {
191
174
  const col = columns.find((c) => c.id === pinnedLeft[i]);
192
- offset += col?.width || col?.minWidth || 100;
175
+ // Use minWidth || width to match the rendered cell width
176
+ offset += col?.minWidth || col?.width || 100;
193
177
  }
194
178
  return offset;
195
179
  },
196
- [pinnedColumns, columns, showRowIndex, isRowIndexPinned]
180
+ [pinnedColumns, columns, showRowIndex]
197
181
  );
198
182
 
199
183
  // Check if column is pinned
@@ -218,12 +202,9 @@ export function useSpreadsheetPinning<T>({
218
202
  collapsedGroups,
219
203
  visibleColumns,
220
204
  handleTogglePin,
221
- handleToggleRowIndexPin,
222
205
  handleToggleGroupCollapse,
223
206
  setPinnedColumnsFromIds,
224
- setRowIndexPinned,
225
207
  getColumnLeftOffset,
226
- getRowIndexLeftOffset,
227
208
  isColumnPinned,
228
209
  getColumnPinSide,
229
210
  };