@xcelsior/ui-spreadsheets 1.1.7 → 1.1.9
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.d.mts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +30 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +30 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/Spreadsheet.stories.tsx +56 -0
- package/src/components/Spreadsheet.tsx +9 -0
- package/src/components/SpreadsheetCell.tsx +3 -3
- package/src/components/SpreadsheetHeader.tsx +1 -1
- package/src/hooks/useSpreadsheetPinning.ts +30 -0
- package/src/types.ts +8 -1
package/package.json
CHANGED
|
@@ -1511,3 +1511,59 @@ export const WithCheckboxColumns: Story = {
|
|
|
1511
1511
|
);
|
|
1512
1512
|
},
|
|
1513
1513
|
};
|
|
1514
|
+
|
|
1515
|
+
// With Right-Pinned Columns
|
|
1516
|
+
export const WithRightPinnedColumns: Story = {
|
|
1517
|
+
render: () => {
|
|
1518
|
+
const [data, setData] = useState(sampleUsers);
|
|
1519
|
+
|
|
1520
|
+
const handleCellsEdit = (edits: CellEdit[]) => {
|
|
1521
|
+
setData((prev) =>
|
|
1522
|
+
prev.map((row) => {
|
|
1523
|
+
const editsForRow = edits.filter((e) => e.rowId === row.id);
|
|
1524
|
+
if (editsForRow.length > 0) {
|
|
1525
|
+
const updates = editsForRow.reduce(
|
|
1526
|
+
(acc, edit) => ({ ...acc, [edit.columnId]: edit.value }),
|
|
1527
|
+
{}
|
|
1528
|
+
);
|
|
1529
|
+
return { ...row, ...updates };
|
|
1530
|
+
}
|
|
1531
|
+
return row;
|
|
1532
|
+
})
|
|
1533
|
+
);
|
|
1534
|
+
};
|
|
1535
|
+
|
|
1536
|
+
return (
|
|
1537
|
+
<div className="p-4">
|
|
1538
|
+
<div className="mb-4 p-4 bg-purple-50 rounded-lg border border-purple-200">
|
|
1539
|
+
<h3 className="font-semibold text-purple-900 mb-2">Right-Pinned Columns Demo</h3>
|
|
1540
|
+
<p className="text-sm text-purple-700 mb-3">
|
|
1541
|
+
This example demonstrates columns pinned to the right side of the spreadsheet.
|
|
1542
|
+
The "Status" and "Active" columns are pinned to the right and will remain visible
|
|
1543
|
+
when scrolling horizontally.
|
|
1544
|
+
</p>
|
|
1545
|
+
<ul className="text-sm text-purple-700 space-y-1 ml-4 list-disc">
|
|
1546
|
+
<li><strong>ID column</strong> is pinned to the left</li>
|
|
1547
|
+
<li><strong>Status and Active columns</strong> are pinned to the right</li>
|
|
1548
|
+
<li>Scroll horizontally to see the pinned columns stay in place</li>
|
|
1549
|
+
</ul>
|
|
1550
|
+
</div>
|
|
1551
|
+
|
|
1552
|
+
<Spreadsheet
|
|
1553
|
+
data={data}
|
|
1554
|
+
columns={userColumns}
|
|
1555
|
+
getRowId={(row) => row.id}
|
|
1556
|
+
onCellsEdit={handleCellsEdit}
|
|
1557
|
+
settings={{
|
|
1558
|
+
defaultPinnedColumns: ['id'],
|
|
1559
|
+
defaultPinnedRightColumns: ['status', 'isActive'],
|
|
1560
|
+
}}
|
|
1561
|
+
showToolbar
|
|
1562
|
+
showPagination
|
|
1563
|
+
enableRowSelection
|
|
1564
|
+
enableCellEditing
|
|
1565
|
+
/>
|
|
1566
|
+
</div>
|
|
1567
|
+
);
|
|
1568
|
+
},
|
|
1569
|
+
};
|
|
@@ -188,12 +188,14 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
188
188
|
handleToggleGroupCollapse,
|
|
189
189
|
setPinnedColumnsFromIds,
|
|
190
190
|
getColumnLeftOffset,
|
|
191
|
+
getColumnRightOffset,
|
|
191
192
|
isColumnPinned,
|
|
192
193
|
getColumnPinSide,
|
|
193
194
|
} = useSpreadsheetPinning({
|
|
194
195
|
columns,
|
|
195
196
|
columnGroups,
|
|
196
197
|
defaultPinnedColumns: initialSettings?.defaultPinnedColumns,
|
|
198
|
+
defaultPinnedRightColumns: initialSettings?.defaultPinnedRightColumns,
|
|
197
199
|
});
|
|
198
200
|
|
|
199
201
|
// Comments hook
|
|
@@ -755,6 +757,9 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
755
757
|
const isPinnedLeft =
|
|
756
758
|
isColumnPinned(column.id) &&
|
|
757
759
|
getColumnPinSide(column.id) === 'left';
|
|
760
|
+
const isPinnedRight =
|
|
761
|
+
isColumnPinned(column.id) &&
|
|
762
|
+
getColumnPinSide(column.id) === 'right';
|
|
758
763
|
return (
|
|
759
764
|
<SpreadsheetHeader
|
|
760
765
|
key={column.id}
|
|
@@ -766,6 +771,9 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
766
771
|
leftOffset={
|
|
767
772
|
isPinnedLeft ? getColumnLeftOffset(column.id) : 0
|
|
768
773
|
}
|
|
774
|
+
rightOffset={
|
|
775
|
+
isPinnedRight ? getColumnRightOffset(column.id) : 0
|
|
776
|
+
}
|
|
769
777
|
highlightColor={getColumnHighlight(column.id)}
|
|
770
778
|
compactMode={effectiveCompactMode}
|
|
771
779
|
onClick={() => handleSort(column.id)}
|
|
@@ -1047,6 +1055,7 @@ export function Spreadsheet<T extends Record<string, any>>({
|
|
|
1047
1055
|
isPinned={isColPinned}
|
|
1048
1056
|
pinSide={colPinSide}
|
|
1049
1057
|
leftOffset={getColumnLeftOffset(column.id)}
|
|
1058
|
+
rightOffset={getColumnRightOffset(column.id)}
|
|
1050
1059
|
onClick={(e) =>
|
|
1051
1060
|
handleCellClick(rowId, column.id, e)
|
|
1052
1061
|
}
|
|
@@ -297,7 +297,7 @@ const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
|
|
|
297
297
|
{!isInSelection && !isFocused && (
|
|
298
298
|
<div className="flex items-center gap-0.5 shrink-0">
|
|
299
299
|
{/* Highlight button - hover only */}
|
|
300
|
-
{onHighlight && (
|
|
300
|
+
{column.highlightable !== false && onHighlight && (
|
|
301
301
|
<button
|
|
302
302
|
type="button"
|
|
303
303
|
onClick={(e) => {
|
|
@@ -317,7 +317,7 @@ const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
|
|
|
317
317
|
)}
|
|
318
318
|
|
|
319
319
|
{/* Comment button - always visible when has comments, hover only when adding */}
|
|
320
|
-
{hasComments && onViewComments ? (
|
|
320
|
+
{column.commentable !== false && hasComments && onViewComments ? (
|
|
321
321
|
<button
|
|
322
322
|
type="button"
|
|
323
323
|
onClick={(e) => {
|
|
@@ -336,7 +336,7 @@ const SpreadsheetCell: React.FC<SpreadsheetCellProps> = ({
|
|
|
336
336
|
</span>
|
|
337
337
|
)}
|
|
338
338
|
</button>
|
|
339
|
-
) : onAddComment ? (
|
|
339
|
+
) : column.commentable !== false && onAddComment ? (
|
|
340
340
|
<button
|
|
341
341
|
type="button"
|
|
342
342
|
onClick={(e) => {
|
|
@@ -93,7 +93,7 @@ export const SpreadsheetHeader: React.FC<
|
|
|
93
93
|
{/* Action buttons using unified ColumnHeaderActions */}
|
|
94
94
|
<ColumnHeaderActions
|
|
95
95
|
enableFiltering={column.filterable}
|
|
96
|
-
enableHighlighting={!!onHighlightClick}
|
|
96
|
+
enableHighlighting={column.highlightable !== false && !!onHighlightClick}
|
|
97
97
|
enablePinning={column.pinnable !== false}
|
|
98
98
|
hasActiveFilter={hasActiveFilter}
|
|
99
99
|
hasActiveHighlight={!!highlightColor}
|
|
@@ -10,6 +10,7 @@ export interface UseSpreadsheetPinningOptions<T> {
|
|
|
10
10
|
columnGroups?: SpreadsheetColumnGroup[];
|
|
11
11
|
showRowIndex?: boolean;
|
|
12
12
|
defaultPinnedColumns?: string[];
|
|
13
|
+
defaultPinnedRightColumns?: string[];
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export interface UseSpreadsheetPinningReturn<T> {
|
|
@@ -30,6 +31,7 @@ export interface UseSpreadsheetPinningReturn<T> {
|
|
|
30
31
|
|
|
31
32
|
// Offset calculations
|
|
32
33
|
getColumnLeftOffset: (columnId: string) => number;
|
|
34
|
+
getColumnRightOffset: (columnId: string) => number;
|
|
33
35
|
|
|
34
36
|
// Utility
|
|
35
37
|
isColumnPinned: (columnId: string) => boolean;
|
|
@@ -41,6 +43,7 @@ export function useSpreadsheetPinning<T>({
|
|
|
41
43
|
columnGroups,
|
|
42
44
|
showRowIndex = true,
|
|
43
45
|
defaultPinnedColumns = [],
|
|
46
|
+
defaultPinnedRightColumns = [],
|
|
44
47
|
}: UseSpreadsheetPinningOptions<T>): UseSpreadsheetPinningReturn<T> {
|
|
45
48
|
// Initialize pinned columns from defaults (including row index)
|
|
46
49
|
const [pinnedColumns, setPinnedColumns] = useState<Map<string, 'left' | 'right'>>(() => {
|
|
@@ -48,6 +51,9 @@ export function useSpreadsheetPinning<T>({
|
|
|
48
51
|
defaultPinnedColumns.forEach((col) => {
|
|
49
52
|
map.set(col, 'left');
|
|
50
53
|
});
|
|
54
|
+
defaultPinnedRightColumns.forEach((col) => {
|
|
55
|
+
map.set(col, 'right');
|
|
56
|
+
});
|
|
51
57
|
return map;
|
|
52
58
|
});
|
|
53
59
|
|
|
@@ -196,6 +202,29 @@ export function useSpreadsheetPinning<T>({
|
|
|
196
202
|
[pinnedColumns]
|
|
197
203
|
);
|
|
198
204
|
|
|
205
|
+
// Calculate column offset for right sticky positioning
|
|
206
|
+
const getColumnRightOffset = useCallback(
|
|
207
|
+
(columnId: string): number => {
|
|
208
|
+
// Get right-pinned columns
|
|
209
|
+
const pinnedRight = Array.from(pinnedColumns.entries())
|
|
210
|
+
.filter(([, side]) => side === 'right')
|
|
211
|
+
.map(([id]) => id);
|
|
212
|
+
|
|
213
|
+
const index = pinnedRight.indexOf(columnId);
|
|
214
|
+
if (index === -1) return 0;
|
|
215
|
+
|
|
216
|
+
// Calculate offset from the right edge
|
|
217
|
+
// Start from 0 and add widths of columns to the right of this one
|
|
218
|
+
let offset = 0;
|
|
219
|
+
for (let i = pinnedRight.length - 1; i > index; i--) {
|
|
220
|
+
const col = columns.find((c) => c.id === pinnedRight[i]);
|
|
221
|
+
offset += col?.minWidth || col?.width || 100;
|
|
222
|
+
}
|
|
223
|
+
return offset;
|
|
224
|
+
},
|
|
225
|
+
[pinnedColumns, columns]
|
|
226
|
+
);
|
|
227
|
+
|
|
199
228
|
return {
|
|
200
229
|
pinnedColumns,
|
|
201
230
|
isRowIndexPinned,
|
|
@@ -205,6 +234,7 @@ export function useSpreadsheetPinning<T>({
|
|
|
205
234
|
handleToggleGroupCollapse,
|
|
206
235
|
setPinnedColumnsFromIds,
|
|
207
236
|
getColumnLeftOffset,
|
|
237
|
+
getColumnRightOffset,
|
|
208
238
|
isColumnPinned,
|
|
209
239
|
getColumnPinSide,
|
|
210
240
|
};
|
package/src/types.ts
CHANGED
|
@@ -22,6 +22,10 @@ export interface SpreadsheetColumn<T = any> {
|
|
|
22
22
|
editable?: boolean;
|
|
23
23
|
/** Whether the column can be pinned */
|
|
24
24
|
pinnable?: boolean;
|
|
25
|
+
/** Whether the column can be highlighted (defaults to true) */
|
|
26
|
+
highlightable?: boolean;
|
|
27
|
+
/** Whether the column can have comments (defaults to true) */
|
|
28
|
+
commentable?: boolean;
|
|
25
29
|
/** Type of data in the column (for filtering/formatting) */
|
|
26
30
|
type?: 'text' | 'number' | 'date' | 'select' | 'boolean' | 'checkbox';
|
|
27
31
|
/** Options for select type columns */
|
|
@@ -385,8 +389,10 @@ export interface SpreadsheetProps<T = any> {
|
|
|
385
389
|
onSave?: () => void | Promise<void>;
|
|
386
390
|
/** Initial settings (optional). All settings can be changed by user in the settings modal. */
|
|
387
391
|
settings?: {
|
|
388
|
-
/** Default pinned column IDs */
|
|
392
|
+
/** Default pinned column IDs (pinned to left) */
|
|
389
393
|
defaultPinnedColumns?: string[];
|
|
394
|
+
/** Default pinned column IDs (pinned to right) */
|
|
395
|
+
defaultPinnedRightColumns?: string[];
|
|
390
396
|
/** Default sort configuration */
|
|
391
397
|
defaultSort?: SpreadsheetSortConfig | null;
|
|
392
398
|
/** Default page size */
|
|
@@ -405,6 +411,7 @@ export interface SpreadsheetProps<T = any> {
|
|
|
405
411
|
defaultPageSize?: number;
|
|
406
412
|
defaultZoom?: number;
|
|
407
413
|
defaultPinnedColumns?: string[];
|
|
414
|
+
defaultPinnedRightColumns?: string[];
|
|
408
415
|
defaultSort?: SpreadsheetSortConfig | null;
|
|
409
416
|
}) => void;
|
|
410
417
|
/** Loading state */
|