Skip to content

Commit e5b6c36

Browse files
skuengneoSandro Küng
andauthored
Added sticky column feature (#75)
Co-authored-by: Sandro Küng <sk@neolution.ch>
1 parent 44bde88 commit e5b6c36

14 files changed

Lines changed: 135 additions & 21 deletions

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- `columnPinning` feature. Allows the pinning of columns.
13+
1014
## [5.8.0] - 2024-12-02
1115

1216
### Added

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"@dnd-kit/modifiers": "^7.0.0",
5151
"@dnd-kit/sortable": "^8.0.0",
5252
"@neolution-ch/react-pattern-ui": "^3.4.0",
53-
"@tanstack/react-table": "^8.10.7",
53+
"@tanstack/react-table": "^8.12.0",
5454
"react-loading-skeleton": "^3.3.1"
5555
},
5656
"devDependencies": {

src/lib/ReactDataTable/ReactDataTable.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { DndContext, KeyboardSensor, MouseSensor, TouchSensor, closestCenter, us
1313
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
1414
import { DraggableRow, InternalTableRow } from "./TableRows";
1515
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
16+
import { getCommonPinningStyles } from "../utils/getCommonPinningStyles";
1617
import { getFilterValue, setFilterValue } from "../utils/customFilterMethods";
1718

1819
interface TableBodyProps<TData> {
@@ -96,6 +97,7 @@ const ReactDataTable = <TData, TFilter extends FilterModel = Record<string, neve
9697
enableExpanding={enableExpanding as boolean | ((row: Row<TData>) => boolean)}
9798
rowStyle={rowStyle && rowStyle(row.original)}
9899
fullRowSelectable={fullRowSelectable}
100+
hasPinnedColumns={table.getIsSomeColumnsPinned()}
99101
/>
100102
))}
101103
</>
@@ -141,6 +143,9 @@ const ReactDataTable = <TData, TFilter extends FilterModel = Record<string, neve
141143
style={{
142144
...header.column.columnDef.meta?.headerStyle,
143145
...(header.column.getCanSort() ? { cursor: "pointer" } : {}),
146+
...(table.getIsSomeColumnsPinned()
147+
? getCommonPinningStyles(header.subHeaders.length > 0 ? header.subHeaders[0].column : header.column)
148+
: {}),
144149
}}
145150
className={header.column.columnDef.meta?.headerClassName}
146151
colSpan={header.colSpan}
@@ -169,7 +174,13 @@ const ReactDataTable = <TData, TFilter extends FilterModel = Record<string, neve
169174
} = header;
170175

171176
return (
172-
<th key={`${header.id}-col-filter`} style={header.column.columnDef.meta?.headerFilterStyle}>
177+
<th
178+
key={`${header.id}-col-filter`}
179+
style={{
180+
...header.column.columnDef.meta?.headerFilterStyle,
181+
...(table.getIsSomeColumnsPinned() ? getCommonPinningStyles(header.column) : {}),
182+
}}
183+
>
173184
{header.index === 0 && (
174185
<>
175186
{onEnter && (

src/lib/ReactDataTable/TableRows.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { Row, flexRender } from "@tanstack/react-table";
33
import { CSSProperties } from "react";
44
import { CSS } from "@dnd-kit/utilities";
5+
import { getCommonPinningStyles } from "../utils/getCommonPinningStyles";
56
import { FilterModel } from "../types/TableState";
67
import { ReactDataTableProps } from "./ReactDataTableProps";
78

@@ -13,10 +14,20 @@ interface TableRowProps<TData, TFilter extends FilterModel = Record<string, neve
1314
enableExpanding?: boolean | ((row: Row<TData>) => boolean);
1415
rowStyle?: CSSProperties;
1516
setNodeRef?: (node: HTMLElement | null) => void;
17+
hasPinnedColumns?: boolean;
1618
}
1719

1820
const InternalTableRow = <TData, TFilter extends FilterModel = Record<string, never>>(props: TableRowProps<TData, TFilter>) => {
19-
const { row, rowStyle, setNodeRef, enableRowSelection = false, fullRowSelectable = true, onRowClick, enableRowClick } = props;
21+
const {
22+
row,
23+
rowStyle,
24+
setNodeRef,
25+
enableRowSelection = false,
26+
fullRowSelectable = true,
27+
onRowClick,
28+
enableRowClick,
29+
hasPinnedColumns,
30+
} = props;
2031
const isRowSelectionEnabled =
2132
(typeof enableRowSelection === "function" ? enableRowSelection(row) : enableRowSelection) && fullRowSelectable;
2233
const isRowClickable = typeof enableRowClick === "function" ? enableRowClick(row) : enableRowClick;
@@ -37,7 +48,14 @@ const InternalTableRow = <TData, TFilter extends FilterModel = Record<string, ne
3748
style={rowStyle}
3849
>
3950
{row.getVisibleCells().map((cell) => (
40-
<td key={cell.id} style={cell.column.columnDef.meta?.cellStyle} className={cell.column.columnDef.meta?.cellClassName}>
51+
<td
52+
key={cell.id}
53+
style={{
54+
...cell.column.columnDef.meta?.cellStyle,
55+
...(hasPinnedColumns ? getCommonPinningStyles(cell.column) : {}),
56+
}}
57+
className={cell.column.columnDef.meta?.cellClassName}
58+
>
4159
{flexRender(cell.column.columnDef.cell, cell.getContext())}
4260
</td>
4361
))}

src/lib/types/TableState.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CoreOptions } from "@tanstack/react-table";
1+
import { ColumnPinningState, CoreOptions } from "@tanstack/react-table";
22
import { SortingState } from "./SortingState";
33

44
/**
@@ -20,6 +20,10 @@ interface TableState<TData, TFilter extends FilterModel>
2020
* The sorting state
2121
*/
2222
sorting?: SortingState<TData>;
23+
/**
24+
* The column pinning state
25+
*/
26+
columnPinning?: ColumnPinningState;
2327
}
2428

2529
export { TableState, FilterModel };

src/lib/useFullyControlledReactDataTable/useFullyControlledReactDataTable.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface useFullyControlledReactDataTableProps<TData, TFilter extends FilterMod
1616
sorting: TableState<TData, TFilter>["sorting"];
1717
expanded: TableState<TData, TFilter>["expanded"];
1818
rowSelection?: TableState<TData, TFilter>["rowSelection"];
19+
columnPinning?: TableState<TData, TFilter>["columnPinning"];
1920
};
2021
}
2122

src/lib/useReactDataTable/useReactDataTable.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import {
1+
/* eslint-disable max-lines */
2+
import {
23
getCoreRowModel,
34
getExpandedRowModel,
45
getFilteredRowModel,
@@ -38,6 +39,7 @@ const useReactDataTable = <TData, TFilter extends FilterModel = Record<string, n
3839
onSortingChange,
3940
onRowSelectionChange,
4041
onExpandedChange,
42+
onColumnPinningChange,
4143
reactTableOptions,
4244
} = props;
4345

@@ -47,13 +49,15 @@ const useReactDataTable = <TData, TFilter extends FilterModel = Record<string, n
4749
pagination: paginationInitial,
4850
rowSelection: rowSelectionInitial,
4951
expanded: expandedInitial,
52+
columnPinning: columnPinningInitial,
5053
} = initialState ?? {};
5154
const {
5255
columnFilters: columnFiltersExternal,
5356
pagination: paginationExternal,
5457
sorting: sortingExternal,
5558
rowSelection: rowSelectionExternal,
5659
expanded: expandedExternal,
60+
columnPinning: columnPinningExternal,
5761
} = state ?? {};
5862

5963
const {
@@ -62,29 +66,34 @@ const useReactDataTable = <TData, TFilter extends FilterModel = Record<string, n
6266
sorting: sortingInternal,
6367
rowSelection: rowSelectionInteral,
6468
expanded: expandedInternal,
69+
columnPinning: columnPinningInternal,
6570
setColumnFilters: setColumnFiltersInternal,
6671
setPagination: setPaginationInternal,
6772
setSorting: setSortingInternal,
6873
setRowSelection: setRowSelectionInternal,
6974
setExpanded: setExpandedInternal,
75+
setColumnPinning: setColumnPinningInternal,
7076
} = useReactDataTableState<TData, TFilter>({
7177
initialColumnFilters: columnFiltersInitial as TFilter,
7278
initialPagination: paginationInitial,
7379
initialSorting: sortingInitial,
7480
rowSelection: rowSelectionInitial,
7581
expanded: expandedInitial,
82+
columnPinning: columnPinningInitial,
7683
} as unknown as OptionalNullable<useReactDataTableStateProps<TData, TFilter>>);
7784

7885
const effectiveColumnFilters = columnFiltersExternal ?? columnFiltersInternal;
7986
const effectivePagination = paginationExternal ?? paginationInternal;
8087
const effectiveSorting = sortingExternal ?? sortingInternal;
8188
const effectiveRowSelection = rowSelectionExternal ?? rowSelectionInteral;
8289
const effectiveExpanded = expandedExternal ?? expandedInternal;
90+
const effectiveColumnPinning = columnPinningExternal ?? columnPinningInternal;
8391
const effectiveOnColumnFiltersChange = onColumnFiltersChange ?? setColumnFiltersInternal;
8492
const effectiveOnPaginationChange = onPaginationChange ?? setPaginationInternal;
8593
const effectiveOnSortingChange = onSortingChange ?? setSortingInternal;
8694
const effectiveOnRowSelectionChange = onRowSelectionChange ?? setRowSelectionInternal;
8795
const effectiveOnExpandedChange = onExpandedChange ?? setExpandedInternal;
96+
const effectiveOnColumnPinningChange = onColumnPinningChange ?? setColumnPinningInternal;
8897

8998
// If we active the manual filtering, we have to unset the filter function, else it still does automatic filtering
9099
if (manualFiltering) columns.forEach((x) => (x.filterFn = undefined));
@@ -124,20 +133,27 @@ const useReactDataTable = <TData, TFilter extends FilterModel = Record<string, n
124133
const newExpanded = typeof expandedOrUpdaterFn !== "function" ? expandedOrUpdaterFn : expandedOrUpdaterFn(effectiveExpanded);
125134
return effectiveOnExpandedChange(newExpanded);
126135
},
136+
onColumnPinningChange: (columnPinningOrUpdaterFn) => {
137+
const newColumnPinning =
138+
typeof columnPinningOrUpdaterFn !== "function" ? columnPinningOrUpdaterFn : columnPinningOrUpdaterFn(effectiveColumnPinning);
139+
return effectiveOnColumnPinningChange(newColumnPinning);
140+
},
127141

128142
state: {
129143
columnFilters,
130144
pagination: effectivePagination,
131145
sorting,
132146
rowSelection: effectiveRowSelection,
133147
expanded: effectiveExpanded,
148+
columnPinning: effectiveColumnPinning,
134149
},
135150

136151
initialState: {
137152
columnFilters: getColumnFilterFromModel(columnFiltersInitial ?? columnFiltersExternal ?? {}),
138153
pagination: paginationInitial ?? paginationExternal,
139154
sorting: getSortingStateFromModel(sortingInitial ?? sortingExternal),
140155
expanded: expandedInitial ?? expandedExternal,
156+
columnPinning: columnPinningInitial ?? columnPinningExternal,
141157
},
142158

143159
getCoreRowModel: getCoreRowModel(),
@@ -173,11 +189,13 @@ const useReactDataTable = <TData, TFilter extends FilterModel = Record<string, n
173189
sorting: effectiveSorting,
174190
rowSelection: effectiveRowSelection,
175191
expanded: effectiveExpanded,
192+
columnPinning: effectiveColumnPinning,
176193
setColumnFilters: effectiveOnColumnFiltersChange,
177194
setPagination: effectiveOnPaginationChange,
178195
setSorting: effectiveOnSortingChange,
179196
setRowSelection: effectiveOnRowSelectionChange,
180197
setExpanded: effectiveOnExpandedChange,
198+
setColumnPinning: effectiveOnColumnPinningChange,
181199
};
182200
};
183201

src/lib/useReactDataTable/useReactDataTableProps.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { ColumnDef, ExpandedState, OnChangeFn, PaginationState, RowSelectionState, TableOptions } from "@tanstack/react-table";
1+
import {
2+
ColumnDef,
3+
ColumnPinningState,
4+
ExpandedState,
5+
OnChangeFn,
6+
PaginationState,
7+
RowSelectionState,
8+
TableOptions,
9+
} from "@tanstack/react-table";
210
import { FilterModel, TableState } from "../types/TableState";
311
import { SortingState } from "../types/SortingState";
412

@@ -76,6 +84,11 @@ export interface useReactDataTableProps<TData, TFilter extends FilterModel> {
7684
*/
7785
onExpandedChange?: OnChangeFn<ExpandedState>;
7886

87+
/**
88+
* event handler for when the column pinning changes
89+
*/
90+
onColumnPinningChange?: OnChangeFn<ColumnPinningState>;
91+
7992
/**
8093
* the react table options that will be passed to the `useReactTable` hook.
8194
* Omits the `state` property. Use the `state` property instead.

src/lib/useReactDataTable/useReactDataTableResult.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ExpandedState, PaginationState, RowSelectionState, Table } from "@tanstack/react-table";
1+
import { ColumnPinningState, ExpandedState, PaginationState, RowSelectionState, Table } from "@tanstack/react-table";
22
import { Dispatch, SetStateAction } from "react";
33
import { FilterModel } from "../types/TableState";
44
import { SortingState } from "../types/SortingState";
@@ -62,4 +62,14 @@ export interface useReactDataTableResult<TData, TFilter extends FilterModel> {
6262
* the expanded state setter. Only makes sense to use this if you are not supplying the `state.expanded` property
6363
*/
6464
setExpanded: Dispatch<SetStateAction<ExpandedState>>;
65+
66+
/**
67+
* the column pinning state. Only makes sense to use this if you are not supplying the `state.columnPinning` property
68+
*/
69+
columnPinning: ColumnPinningState;
70+
71+
/**
72+
* the column pinning state setter. Only makes sense to use this if you are not supplying the `state.columnPinning` property
73+
*/
74+
setColumnPinning: Dispatch<SetStateAction<ColumnPinningState>>;
6575
}

src/lib/useReactDataTableState/useReactDataTableState.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState } from "react";
2-
import { ExpandedState, PaginationState, RowSelectionState } from "@tanstack/react-table";
2+
import { ColumnPinningState, ExpandedState, PaginationState, RowSelectionState } from "@tanstack/react-table";
33
import { useReactDataTableStateProps } from "./useReactDataTableStateProps";
44
import { useReactDataTableStateResult } from "./useReactDataTableStateResult";
55
import { FilterModel } from "../types/TableState";
@@ -13,14 +13,15 @@ import { OptionalNullable } from "../types/NullableTypes";
1313
const useReactDataTableState = <TData, TFilter extends FilterModel = Record<string, never>>(
1414
props: OptionalNullable<useReactDataTableStateProps<TData, TFilter>>,
1515
): useReactDataTableStateResult<TData, TFilter> => {
16-
const { initialColumnFilters, initialSorting, initialPagination, initialRowSelection, initialExpanded } =
16+
const { initialColumnFilters, initialSorting, initialPagination, initialRowSelection, initialExpanded, initialColumnPinning } =
1717
props as useReactDataTableStateProps<TData, TFilter>;
1818

1919
const [columnFilters, setColumnFilters] = useState<TFilter>((initialColumnFilters ?? {}) as TFilter);
2020
const [afterSearchFilter, setAfterSearchFilter] = useState<TFilter>((initialColumnFilters ?? {}) as TFilter);
2121
const [sorting, setSorting] = useState<SortingState<TData> | undefined>(initialSorting);
2222
const [rowSelection, setRowSelection] = useState<RowSelectionState>(initialRowSelection ?? ({} as RowSelectionState));
2323
const [expanded, setExpanded] = useState<ExpandedState>(initialExpanded ?? ({} as ExpandedState));
24+
const [columnPinning, setColumnPinning] = useState<ColumnPinningState>(initialColumnPinning ?? ({} as ColumnPinningState));
2425

2526
const [pagination, setPagination] = useState<PaginationState>({
2627
pageIndex: initialPagination?.pageIndex ?? 0,
@@ -34,12 +35,14 @@ const useReactDataTableState = <TData, TFilter extends FilterModel = Record<stri
3435
afterSearchFilter,
3536
rowSelection,
3637
expanded,
38+
columnPinning,
3739
setSorting,
3840
setColumnFilters,
3941
setPagination,
4042
setAfterSearchFilter,
4143
setRowSelection,
4244
setExpanded,
45+
setColumnPinning,
4346
};
4447
};
4548

0 commit comments

Comments
 (0)