Skip to content

Commit e60f98f

Browse files
authored
feat(data-grid): Add column pinning feature (#3176)
* feat(data-grid): Add column pinning feature * feat(data-grid): Add column pinning feature fix eslint error.. * feat(data-grid): Add column pinning feature fix horizontal scrollbar appearing when it shouldn't * feat(data-grid): Add column pinning feature Fix for styled-component warning * feat(data-grid): Add column pinning feature z-index to 'auto' by default. * feat(data-grid): Add column pinning feature Fix rowStyle method Closes #3042
1 parent 1904d9f commit e60f98f

File tree

7 files changed

+166
-16
lines changed

7 files changed

+166
-16
lines changed

packages/eds-data-grid-react/src/EdsDataGrid.docs.mdx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,27 @@ Allows the user to hide/show columns.
8484

8585
<Canvas of={ComponentStories.HideShowColumns} />
8686

87+
### Column pinning
88+
89+
Columns can be pinned (frozen) to the right / left side of the table by setting the `columnPinState`.
90+
91+
*Note:* This requires `scrollbarHorizontal` to be true
92+
93+
See [Tanstack docs for more](https://tanstack.com/table/v8/docs/api/features/pinning)
94+
95+
<Canvas of={ComponentStories.ColumnPinning} />
96+
8797
### Sorting
8898

8999
Comes with sorting built-in, and uses default sort functions. Can be overridden on a per-column basis.
90-
See [https://tanstack.com/table/v8/docs/api/features/sorting](Tanstack docs for more)
100+
See [Tanstack docs for more](https://tanstack.com/table/v8/docs/api/features/sorting)
91101

92102
<Canvas of={ComponentStories.Sortable} />
93103

94104
### External sorting
95105

96106
It's also possible to handle sorting manually by setting manualSorting to `true` and listening on the onSortingChange prop.
97-
See [https://tanstack.com/table/v8/docs/api/features/sorting](Tanstack docs for more)
107+
See [Tanstack docs for more](https://tanstack.com/table/v8/docs/api/features/sorting)
98108

99109
<Canvas of={ComponentStories.ManualSorting} />
100110

packages/eds-data-grid-react/src/EdsDataGrid.stories.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,31 @@ ManualSorting.args = {
137137
columns: groupedColumns,
138138
}
139139

140+
export const ColumnPinning: StoryFn<EdsDataGridProps<Photo>> = (args) => {
141+
const { columnPinState } = args
142+
return (
143+
<>
144+
<Typography as={'div'} style={{ whiteSpace: 'pre' }}>
145+
{JSON.stringify(columnPinState, null, 2)}
146+
</Typography>
147+
<EdsDataGrid {...args} />
148+
</>
149+
)
150+
}
151+
152+
ColumnPinning.args = {
153+
columnPinState: {
154+
right: [columns[0].id, columns.at(1).id],
155+
left: [columns.at(2).id],
156+
},
157+
scrollbarHorizontal: true,
158+
stickyHeader: true,
159+
width: 700,
160+
columns: columns,
161+
height: 500,
162+
rows: data,
163+
}
164+
140165
export const ColumnOrdering: StoryFn<EdsDataGridProps<Photo>> = (args) => {
141166
const ids = ['id', 'albumId', 'title', 'url', 'thumbnailUrl']
142167
const [sort, setSort] = useState<string[]>(ids)

packages/eds-data-grid-react/src/EdsDataGrid.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import {
33
ColumnDef,
44
ColumnFiltersState,
5+
ColumnPinningState,
56
getCoreRowModel,
67
getFacetedMinMaxValues,
78
getFacetedRowModel,
@@ -60,11 +61,18 @@ export function EdsDataGrid<T>({
6061
onSortingChange,
6162
manualSorting,
6263
sortingState,
64+
columnPinState,
65+
scrollbarHorizontal,
66+
width,
67+
height,
6368
}: EdsDataGridProps<T>) {
6469
const [sorting, setSorting] = useState<SortingState>(sortingState ?? [])
6570
const [selection, setSelection] = useState<RowSelectionState>(
6671
selectedRows ?? {},
6772
)
73+
const [columnPin, setColumnPin] = useState<ColumnPinningState>(
74+
columnPinState ?? {},
75+
)
6876
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
6977
const [visible, setVisible] = useState(columnVisibility ?? {})
7078
const [globalFilter, setGlobalFilter] = useState('')
@@ -78,6 +86,10 @@ export function EdsDataGrid<T>({
7886
setVisible(columnVisibility ?? {})
7987
}, [columnVisibility, setVisible])
8088

89+
useEffect(() => {
90+
setColumnPin((s) => columnPinState ?? s)
91+
}, [columnPinState])
92+
8193
useEffect(() => {
8294
setSorting(sortingState)
8395
}, [sortingState])
@@ -138,6 +150,7 @@ export function EdsDataGrid<T>({
138150
columnResizeMode: columnResizeMode,
139151
state: {
140152
sorting,
153+
columnPinning: columnPin,
141154
rowSelection: selection,
142155
columnOrder: columnOrderState,
143156
},
@@ -158,6 +171,8 @@ export function EdsDataGrid<T>({
158171
debugHeaders: debug,
159172
debugColumns: debug,
160173
enableRowSelection: rowSelection ?? false,
174+
enableColumnPinning: true,
175+
enablePinning: true,
161176
}
162177

163178
useEffect(() => {
@@ -231,7 +246,7 @@ export function EdsDataGrid<T>({
231246
*/
232247
if (enableVirtual) {
233248
parentRefStyle = {
234-
height: virtualHeight ?? 500,
249+
height: height ?? virtualHeight ?? 500,
235250
overflow: 'auto',
236251
position: 'relative',
237252
}
@@ -278,7 +293,17 @@ export function EdsDataGrid<T>({
278293
enableColumnFiltering={!!enableColumnFiltering}
279294
stickyHeader={!!stickyHeader}
280295
>
281-
<div className="table-wrapper" style={parentRefStyle} ref={parentRef}>
296+
<div
297+
className="table-wrapper"
298+
style={{
299+
height: height ?? 'auto',
300+
...parentRefStyle,
301+
width: scrollbarHorizontal ? width : 'auto',
302+
tableLayout: scrollbarHorizontal ? 'fixed' : 'auto',
303+
overflow: 'auto',
304+
}}
305+
ref={parentRef}
306+
>
282307
<Table
283308
className={Object.entries(classList)
284309
.filter(([, k]) => k)

packages/eds-data-grid-react/src/EdsDataGridProps.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
Column,
33
ColumnDef,
4+
ColumnPinningState,
45
ColumnResizeMode,
56
OnChangeFn,
67
Row,
@@ -58,6 +59,22 @@ type BaseProps<T> = {
5859
* @default {}
5960
*/
6061
selectedRows?: Record<string | number, boolean>
62+
/**
63+
* Whether there should be horizontal scrolling.
64+
* This must be true for column pinning to work
65+
* @default true
66+
*/
67+
scrollbarHorizontal?: boolean
68+
/**
69+
* Width of the table. Only takes effect if {@link scrollbarHorizontal} is true.
70+
* @default 800
71+
*/
72+
width?: number
73+
/**
74+
* Height of the table.
75+
* @default none
76+
*/
77+
height?: number
6178
}
6279

6380
type StyleProps<T> = {
@@ -159,11 +176,16 @@ type SortProps = {
159176
sortingState?: SortingState
160177
}
161178

179+
type ColumnProps = {
180+
columnPinState?: ColumnPinningState
181+
}
182+
162183
export type EdsDataGridProps<T> = BaseProps<T> &
163184
StyleProps<T> &
164185
SortProps &
165186
FilterProps &
166187
PagingProps &
188+
ColumnProps &
167189
VirtualProps & {
168190
/**
169191
* Which columns are visible. If not set, all columns are visible. undefined means that the column is visible.
Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,62 @@
1-
import { Cell, flexRender } from '@tanstack/react-table'
1+
import { Cell, ColumnPinningPosition, flexRender } from '@tanstack/react-table'
22
import { Table, Typography } from '@equinor/eds-core-react'
33
import { useTableContext } from '../EdsDataGridContext'
4+
import { useMemo } from 'react'
5+
import { tokens } from '@equinor/eds-tokens'
6+
import styled from 'styled-components'
47

58
type Props<T> = {
69
cell: Cell<T, unknown>
710
}
11+
12+
const StyledCell = styled(Table.Cell)<{
13+
$pinned: ColumnPinningPosition
14+
$offset: number
15+
}>`
16+
position: ${(p) => (p.$pinned ? 'sticky' : 'relative')};
17+
${(p) => {
18+
if (p.$pinned) {
19+
return `${p.$pinned}: ${p.$offset}px;`
20+
}
21+
return ''
22+
}}
23+
z-index: ${(p) => (p.$pinned ? 11 : 'auto')};
24+
background-color: ${(p) =>
25+
p.$pinned ? tokens.colors.ui.background__default.hex : 'inherit'};
26+
overflow: hidden;
27+
white-space: nowrap;
28+
text-overflow: ellipsis;
29+
`
30+
831
export function TableBodyCell<T>({ cell }: Props<T>) {
9-
const { cellClass, cellStyle } = useTableContext()
32+
const { cellClass, cellStyle, table } = useTableContext()
33+
const pinned = cell.column.getIsPinned()
34+
const pinnedOffset = useMemo<number>(() => {
35+
if (!pinned) {
36+
return 0
37+
}
38+
const header = table.getFlatHeaders().find((h) => h.id === cell.column.id)
39+
return pinned === 'left'
40+
? header.getStart()
41+
: table.getTotalSize() - header.getStart() - cell.column.getSize()
42+
}, [pinned, cell.column, table])
1043
return (
11-
<Table.Cell
44+
<StyledCell
45+
$pinned={pinned}
46+
$offset={pinnedOffset}
1247
className={cellClass ? cellClass(cell.row, cell.column.id) : ''}
1348
{...{
1449
key: cell.id,
1550
style: {
1651
width: cell.column.getSize(),
1752
maxWidth: cell.column.getSize(),
18-
overflow: 'hidden',
19-
whiteSpace: 'nowrap',
20-
textOverflow: 'ellipsis',
2153
...(cellStyle?.(cell.row, cell.column.id) ?? {}),
2254
},
2355
}}
2456
>
2557
<Typography as="span" group="table" variant="cell_text">
2658
{flexRender(cell.column.columnDef.cell, cell.getContext())}
2759
</Typography>
28-
</Table.Cell>
60+
</StyledCell>
2961
)
3062
}

packages/eds-data-grid-react/src/components/TableHeaderCell.tsx

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
ColumnPinningPosition,
23
ColumnResizeMode,
34
flexRender,
45
Header,
@@ -11,6 +12,7 @@ import { useTableContext } from '../EdsDataGridContext'
1112
import { Filter } from './Filter'
1213
import styled from 'styled-components'
1314
import { tokens } from '@equinor/eds-tokens'
15+
import { useMemo } from 'react'
1416

1517
type Props<T> = {
1618
header: Header<T, unknown>
@@ -44,6 +46,7 @@ const ResizeInner = styled.div`
4446
const Resizer = styled.div<ResizeProps>`
4547
transform: ${(props) =>
4648
props.$columnResizeMode === 'onEnd' ? 'translateX(0px)' : 'none'};
49+
4750
${ResizeInner} {
4851
opacity: ${(props) => (props.$isResizing ? 1 : 0)};
4952
}
@@ -60,10 +63,26 @@ const Resizer = styled.div<ResizeProps>`
6063
justify-content: flex-end;
6164
`
6265

63-
const Cell = styled(Table.Cell)<{ sticky: boolean }>`
66+
const Cell = styled(Table.Cell)<{
67+
$sticky: boolean
68+
$pinned: ColumnPinningPosition
69+
$offset: number
70+
}>`
6471
font-weight: bold;
6572
height: 30px;
66-
position: ${(p) => (p.sticky ? 'sticky' : 'relative')};
73+
position: ${(p) => (p.$sticky || p.$pinned ? 'sticky' : 'relative')};
74+
top: 0;
75+
${(p) => {
76+
if (p.$pinned) {
77+
return `${p.$pinned}: ${p.$offset}px;`
78+
}
79+
return ''
80+
}}
81+
z-index: ${(p) => {
82+
if (p.$sticky && p.$pinned) return 13
83+
if (p.$sticky || p.$pinned) return 12
84+
return 'auto'
85+
}};
6786
&:hover ${ResizeInner} {
6887
background: ${tokens.colors.interactive.primary__hover.rgba};
6988
opacity: 1;
@@ -73,16 +92,31 @@ const Cell = styled(Table.Cell)<{ sticky: boolean }>`
7392
export function TableHeaderCell<T>({ header, columnResizeMode }: Props<T>) {
7493
const ctx = useTableContext()
7594
const table = ctx.table
95+
const pinned = header.column.getIsPinned()
96+
const offset = useMemo<number>(() => {
97+
if (!pinned) {
98+
return null
99+
}
100+
return pinned === 'left'
101+
? header.getStart()
102+
: table.getTotalSize() - header.getStart() - header.getSize()
103+
}, [pinned, header, table])
76104
return header.isPlaceholder ? (
77105
<Cell
78-
sticky={ctx.stickyHeader}
106+
$sticky={ctx.stickyHeader}
107+
$offset={offset}
108+
$pinned={pinned}
79109
className={ctx.headerClass ? ctx.headerClass(header.column) : ''}
80-
style={ctx.headerStyle ? ctx.headerStyle(header.column) : {}}
110+
style={{
111+
...(ctx.headerStyle ? ctx.headerStyle(header.column) : {}),
112+
}}
81113
aria-hidden={true}
82114
/>
83115
) : (
84116
<Cell
85-
sticky={ctx.stickyHeader}
117+
$sticky={ctx.stickyHeader}
118+
$offset={offset}
119+
$pinned={pinned}
86120
className={ctx.headerClass ? ctx.headerClass(header.column) : ''}
87121
aria-sort={getSortLabel(header.column.getIsSorted())}
88122
{...{

packages/eds-data-grid-react/src/stories/columns.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const Link = styled.a`
1919
export const columns: Array<ColumnDef<Photo>> = [
2020
helper.accessor('id', {
2121
header: () => <span style={{ fontStyle: 'italic' }}>ID</span>,
22+
size: 100,
2223
id: 'id',
2324
}),
2425
helper.accessor('albumId', {
@@ -28,6 +29,7 @@ export const columns: Array<ColumnDef<Photo>> = [
2829
helper.accessor('title', {
2930
header: 'Title',
3031
id: 'title',
32+
size: 250,
3133
}),
3234
helper.accessor('url', {
3335
header: 'URL',

0 commit comments

Comments
 (0)