@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/dist/index.js +95 -99
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +95 -99
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/ColumnHeaderActions.tsx +2 -16
- package/src/components/Spreadsheet.stories.tsx +1 -0
- package/src/components/Spreadsheet.tsx +32 -1
- package/src/components/SpreadsheetCell.tsx +1 -1
- package/src/hooks/useSpreadsheetFiltering.ts +3 -1
- package/src/hooks/useSpreadsheetPinning.ts +26 -45
package/package.json
CHANGED
|
@@ -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
|
-
<
|
|
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
|
-
</
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
};
|