Skip to content

Commit

Permalink
Added sticky column feature (#75)
Browse files Browse the repository at this point in the history
Co-authored-by: Sandro Küng <[email protected]>
  • Loading branch information
skuengneo and Sandro Küng authored Dec 17, 2024
1 parent 44bde88 commit e5b6c36
Show file tree
Hide file tree
Showing 14 changed files with 135 additions and 21 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `columnPinning` feature. Allows the pinning of columns.

## [5.8.0] - 2024-12-02

### Added
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"@dnd-kit/modifiers": "^7.0.0",
"@dnd-kit/sortable": "^8.0.0",
"@neolution-ch/react-pattern-ui": "^3.4.0",
"@tanstack/react-table": "^8.10.7",
"@tanstack/react-table": "^8.12.0",
"react-loading-skeleton": "^3.3.1"
},
"devDependencies": {
Expand Down
13 changes: 12 additions & 1 deletion src/lib/ReactDataTable/ReactDataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { DndContext, KeyboardSensor, MouseSensor, TouchSensor, closestCenter, us
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
import { DraggableRow, InternalTableRow } from "./TableRows";
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
import { getCommonPinningStyles } from "../utils/getCommonPinningStyles";
import { getFilterValue, setFilterValue } from "../utils/customFilterMethods";

interface TableBodyProps<TData> {
Expand Down Expand Up @@ -96,6 +97,7 @@ const ReactDataTable = <TData, TFilter extends FilterModel = Record<string, neve
enableExpanding={enableExpanding as boolean | ((row: Row<TData>) => boolean)}
rowStyle={rowStyle && rowStyle(row.original)}
fullRowSelectable={fullRowSelectable}
hasPinnedColumns={table.getIsSomeColumnsPinned()}
/>
))}
</>
Expand Down Expand Up @@ -141,6 +143,9 @@ const ReactDataTable = <TData, TFilter extends FilterModel = Record<string, neve
style={{
...header.column.columnDef.meta?.headerStyle,
...(header.column.getCanSort() ? { cursor: "pointer" } : {}),
...(table.getIsSomeColumnsPinned()
? getCommonPinningStyles(header.subHeaders.length > 0 ? header.subHeaders[0].column : header.column)
: {}),
}}
className={header.column.columnDef.meta?.headerClassName}
colSpan={header.colSpan}
Expand Down Expand Up @@ -169,7 +174,13 @@ const ReactDataTable = <TData, TFilter extends FilterModel = Record<string, neve
} = header;

return (
<th key={`${header.id}-col-filter`} style={header.column.columnDef.meta?.headerFilterStyle}>
<th
key={`${header.id}-col-filter`}
style={{
...header.column.columnDef.meta?.headerFilterStyle,
...(table.getIsSomeColumnsPinned() ? getCommonPinningStyles(header.column) : {}),
}}
>
{header.index === 0 && (
<>
{onEnter && (
Expand Down
22 changes: 20 additions & 2 deletions src/lib/ReactDataTable/TableRows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { Row, flexRender } from "@tanstack/react-table";
import { CSSProperties } from "react";
import { CSS } from "@dnd-kit/utilities";
import { getCommonPinningStyles } from "../utils/getCommonPinningStyles";
import { FilterModel } from "../types/TableState";
import { ReactDataTableProps } from "./ReactDataTableProps";

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

const InternalTableRow = <TData, TFilter extends FilterModel = Record<string, never>>(props: TableRowProps<TData, TFilter>) => {
const { row, rowStyle, setNodeRef, enableRowSelection = false, fullRowSelectable = true, onRowClick, enableRowClick } = props;
const {
row,
rowStyle,
setNodeRef,
enableRowSelection = false,
fullRowSelectable = true,
onRowClick,
enableRowClick,
hasPinnedColumns,
} = props;
const isRowSelectionEnabled =
(typeof enableRowSelection === "function" ? enableRowSelection(row) : enableRowSelection) && fullRowSelectable;
const isRowClickable = typeof enableRowClick === "function" ? enableRowClick(row) : enableRowClick;
Expand All @@ -37,7 +48,14 @@ const InternalTableRow = <TData, TFilter extends FilterModel = Record<string, ne
style={rowStyle}
>
{row.getVisibleCells().map((cell) => (
<td key={cell.id} style={cell.column.columnDef.meta?.cellStyle} className={cell.column.columnDef.meta?.cellClassName}>
<td
key={cell.id}
style={{
...cell.column.columnDef.meta?.cellStyle,
...(hasPinnedColumns ? getCommonPinningStyles(cell.column) : {}),
}}
className={cell.column.columnDef.meta?.cellClassName}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
Expand Down
6 changes: 5 additions & 1 deletion src/lib/types/TableState.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CoreOptions } from "@tanstack/react-table";
import { ColumnPinningState, CoreOptions } from "@tanstack/react-table";
import { SortingState } from "./SortingState";

/**
Expand All @@ -20,6 +20,10 @@ interface TableState<TData, TFilter extends FilterModel>
* The sorting state
*/
sorting?: SortingState<TData>;
/**
* The column pinning state
*/
columnPinning?: ColumnPinningState;
}

export { TableState, FilterModel };
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface useFullyControlledReactDataTableProps<TData, TFilter extends FilterMod
sorting: TableState<TData, TFilter>["sorting"];
expanded: TableState<TData, TFilter>["expanded"];
rowSelection?: TableState<TData, TFilter>["rowSelection"];
columnPinning?: TableState<TData, TFilter>["columnPinning"];
};
}

Expand Down
20 changes: 19 additions & 1 deletion src/lib/useReactDataTable/useReactDataTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
/* eslint-disable max-lines */
import {
getCoreRowModel,
getExpandedRowModel,
getFilteredRowModel,
Expand Down Expand Up @@ -38,6 +39,7 @@ const useReactDataTable = <TData, TFilter extends FilterModel = Record<string, n
onSortingChange,
onRowSelectionChange,
onExpandedChange,
onColumnPinningChange,
reactTableOptions,
} = props;

Expand All @@ -47,13 +49,15 @@ const useReactDataTable = <TData, TFilter extends FilterModel = Record<string, n
pagination: paginationInitial,
rowSelection: rowSelectionInitial,
expanded: expandedInitial,
columnPinning: columnPinningInitial,
} = initialState ?? {};
const {
columnFilters: columnFiltersExternal,
pagination: paginationExternal,
sorting: sortingExternal,
rowSelection: rowSelectionExternal,
expanded: expandedExternal,
columnPinning: columnPinningExternal,
} = state ?? {};

const {
Expand All @@ -62,29 +66,34 @@ const useReactDataTable = <TData, TFilter extends FilterModel = Record<string, n
sorting: sortingInternal,
rowSelection: rowSelectionInteral,
expanded: expandedInternal,
columnPinning: columnPinningInternal,
setColumnFilters: setColumnFiltersInternal,
setPagination: setPaginationInternal,
setSorting: setSortingInternal,
setRowSelection: setRowSelectionInternal,
setExpanded: setExpandedInternal,
setColumnPinning: setColumnPinningInternal,
} = useReactDataTableState<TData, TFilter>({
initialColumnFilters: columnFiltersInitial as TFilter,
initialPagination: paginationInitial,
initialSorting: sortingInitial,
rowSelection: rowSelectionInitial,
expanded: expandedInitial,
columnPinning: columnPinningInitial,
} as unknown as OptionalNullable<useReactDataTableStateProps<TData, TFilter>>);

const effectiveColumnFilters = columnFiltersExternal ?? columnFiltersInternal;
const effectivePagination = paginationExternal ?? paginationInternal;
const effectiveSorting = sortingExternal ?? sortingInternal;
const effectiveRowSelection = rowSelectionExternal ?? rowSelectionInteral;
const effectiveExpanded = expandedExternal ?? expandedInternal;
const effectiveColumnPinning = columnPinningExternal ?? columnPinningInternal;
const effectiveOnColumnFiltersChange = onColumnFiltersChange ?? setColumnFiltersInternal;
const effectiveOnPaginationChange = onPaginationChange ?? setPaginationInternal;
const effectiveOnSortingChange = onSortingChange ?? setSortingInternal;
const effectiveOnRowSelectionChange = onRowSelectionChange ?? setRowSelectionInternal;
const effectiveOnExpandedChange = onExpandedChange ?? setExpandedInternal;
const effectiveOnColumnPinningChange = onColumnPinningChange ?? setColumnPinningInternal;

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

state: {
columnFilters,
pagination: effectivePagination,
sorting,
rowSelection: effectiveRowSelection,
expanded: effectiveExpanded,
columnPinning: effectiveColumnPinning,
},

initialState: {
columnFilters: getColumnFilterFromModel(columnFiltersInitial ?? columnFiltersExternal ?? {}),
pagination: paginationInitial ?? paginationExternal,
sorting: getSortingStateFromModel(sortingInitial ?? sortingExternal),
expanded: expandedInitial ?? expandedExternal,
columnPinning: columnPinningInitial ?? columnPinningExternal,
},

getCoreRowModel: getCoreRowModel(),
Expand Down Expand Up @@ -173,11 +189,13 @@ const useReactDataTable = <TData, TFilter extends FilterModel = Record<string, n
sorting: effectiveSorting,
rowSelection: effectiveRowSelection,
expanded: effectiveExpanded,
columnPinning: effectiveColumnPinning,
setColumnFilters: effectiveOnColumnFiltersChange,
setPagination: effectiveOnPaginationChange,
setSorting: effectiveOnSortingChange,
setRowSelection: effectiveOnRowSelectionChange,
setExpanded: effectiveOnExpandedChange,
setColumnPinning: effectiveOnColumnPinningChange,
};
};

Expand Down
15 changes: 14 additions & 1 deletion src/lib/useReactDataTable/useReactDataTableProps.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { ColumnDef, ExpandedState, OnChangeFn, PaginationState, RowSelectionState, TableOptions } from "@tanstack/react-table";
import {
ColumnDef,
ColumnPinningState,
ExpandedState,
OnChangeFn,
PaginationState,
RowSelectionState,
TableOptions,
} from "@tanstack/react-table";
import { FilterModel, TableState } from "../types/TableState";
import { SortingState } from "../types/SortingState";

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

/**
* event handler for when the column pinning changes
*/
onColumnPinningChange?: OnChangeFn<ColumnPinningState>;

/**
* the react table options that will be passed to the `useReactTable` hook.
* Omits the `state` property. Use the `state` property instead.
Expand Down
12 changes: 11 additions & 1 deletion src/lib/useReactDataTable/useReactDataTableResult.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExpandedState, PaginationState, RowSelectionState, Table } from "@tanstack/react-table";
import { ColumnPinningState, ExpandedState, PaginationState, RowSelectionState, Table } from "@tanstack/react-table";
import { Dispatch, SetStateAction } from "react";
import { FilterModel } from "../types/TableState";
import { SortingState } from "../types/SortingState";
Expand Down Expand Up @@ -62,4 +62,14 @@ export interface useReactDataTableResult<TData, TFilter extends FilterModel> {
* the expanded state setter. Only makes sense to use this if you are not supplying the `state.expanded` property
*/
setExpanded: Dispatch<SetStateAction<ExpandedState>>;

/**
* the column pinning state. Only makes sense to use this if you are not supplying the `state.columnPinning` property
*/
columnPinning: ColumnPinningState;

/**
* the column pinning state setter. Only makes sense to use this if you are not supplying the `state.columnPinning` property
*/
setColumnPinning: Dispatch<SetStateAction<ColumnPinningState>>;
}
7 changes: 5 additions & 2 deletions src/lib/useReactDataTableState/useReactDataTableState.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from "react";
import { ExpandedState, PaginationState, RowSelectionState } from "@tanstack/react-table";
import { ColumnPinningState, ExpandedState, PaginationState, RowSelectionState } from "@tanstack/react-table";
import { useReactDataTableStateProps } from "./useReactDataTableStateProps";
import { useReactDataTableStateResult } from "./useReactDataTableStateResult";
import { FilterModel } from "../types/TableState";
Expand All @@ -13,14 +13,15 @@ import { OptionalNullable } from "../types/NullableTypes";
const useReactDataTableState = <TData, TFilter extends FilterModel = Record<string, never>>(
props: OptionalNullable<useReactDataTableStateProps<TData, TFilter>>,
): useReactDataTableStateResult<TData, TFilter> => {
const { initialColumnFilters, initialSorting, initialPagination, initialRowSelection, initialExpanded } =
const { initialColumnFilters, initialSorting, initialPagination, initialRowSelection, initialExpanded, initialColumnPinning } =
props as useReactDataTableStateProps<TData, TFilter>;

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

const [pagination, setPagination] = useState<PaginationState>({
pageIndex: initialPagination?.pageIndex ?? 0,
Expand All @@ -34,12 +35,14 @@ const useReactDataTableState = <TData, TFilter extends FilterModel = Record<stri
afterSearchFilter,
rowSelection,
expanded,
columnPinning,
setSorting,
setColumnFilters,
setPagination,
setAfterSearchFilter,
setRowSelection,
setExpanded,
setColumnPinning,
};
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ExpandedState, PaginationState, RowSelectionState } from "@tanstack/react-table";
import { ColumnPinningState, ExpandedState, PaginationState, RowSelectionState } from "@tanstack/react-table";
import { SortingState } from "../types/SortingState";
import { FilterModel } from "../types/TableState";
import { ColumnFilterState } from "../types/ColumnFilterState";
Expand Down Expand Up @@ -31,4 +31,9 @@ export interface useReactDataTableStateProps<TData, TFilter extends FilterModel>
* the initial expanded
*/
initialExpanded?: ExpandedState;

/**
* the initial column pinning
*/
initialColumnPinning?: ColumnPinningState;
}
12 changes: 11 additions & 1 deletion src/lib/useReactDataTableState/useReactDataTableStateResult.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Dispatch, SetStateAction } from "react";
import { ExpandedState, PaginationState, RowSelectionState } from "@tanstack/react-table";
import { ColumnPinningState, ExpandedState, PaginationState, RowSelectionState } from "@tanstack/react-table";
import { SortingState } from "../types/SortingState";

/**
Expand Down Expand Up @@ -36,6 +36,11 @@ export interface useReactDataTableStateResult<TData, TFilter> {
*/
expanded: ExpandedState;

/**
* the column pinning state
*/
columnPinning: ColumnPinningState;

/**
* the setter for the sorting state
*/
Expand Down Expand Up @@ -65,4 +70,9 @@ export interface useReactDataTableStateResult<TData, TFilter> {
* the setter for the expanded state
*/
setExpanded: Dispatch<SetStateAction<ExpandedState>>;

/**
* the setter for the column pinning state
*/
setColumnPinning: Dispatch<SetStateAction<ColumnPinningState>>;
}
17 changes: 17 additions & 0 deletions src/lib/utils/getCommonPinningStyles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Column } from "@tanstack/react-table";
import { CSSProperties } from "react";

// These are the important styles to make sticky column pinning work!
const getCommonPinningStyles = <TData>(column: Column<TData>): CSSProperties => {
const isPinned = column.getIsPinned();

return {
left: isPinned === "left" ? `${column.getStart("left")}px` : undefined,
right: isPinned === "right" ? `${column.getAfter("right")}px` : undefined,
position: isPinned ? "sticky" : "relative",
width: column.getSize(),
zIndex: isPinned ? 1 : 0,
};
};

export { getCommonPinningStyles };
Loading

0 comments on commit e5b6c36

Please sign in to comment.