Skip to content

Commit

Permalink
feat(data-grid): Add column pinning feature (#3176)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
yusijs authored Dec 6, 2023
1 parent 1904d9f commit e60f98f
Show file tree
Hide file tree
Showing 7 changed files with 166 additions and 16 deletions.
14 changes: 12 additions & 2 deletions packages/eds-data-grid-react/src/EdsDataGrid.docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,27 @@ Allows the user to hide/show columns.

<Canvas of={ComponentStories.HideShowColumns} />

### Column pinning

Columns can be pinned (frozen) to the right / left side of the table by setting the `columnPinState`.

*Note:* This requires `scrollbarHorizontal` to be true

See [Tanstack docs for more](https://tanstack.com/table/v8/docs/api/features/pinning)

<Canvas of={ComponentStories.ColumnPinning} />

### Sorting

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

<Canvas of={ComponentStories.Sortable} />

### External sorting

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

<Canvas of={ComponentStories.ManualSorting} />

Expand Down
25 changes: 25 additions & 0 deletions packages/eds-data-grid-react/src/EdsDataGrid.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,31 @@ ManualSorting.args = {
columns: groupedColumns,
}

export const ColumnPinning: StoryFn<EdsDataGridProps<Photo>> = (args) => {
const { columnPinState } = args
return (
<>
<Typography as={'div'} style={{ whiteSpace: 'pre' }}>
{JSON.stringify(columnPinState, null, 2)}
</Typography>
<EdsDataGrid {...args} />
</>
)
}

ColumnPinning.args = {
columnPinState: {
right: [columns[0].id, columns.at(1).id],
left: [columns.at(2).id],
},
scrollbarHorizontal: true,
stickyHeader: true,
width: 700,
columns: columns,
height: 500,
rows: data,
}

export const ColumnOrdering: StoryFn<EdsDataGridProps<Photo>> = (args) => {
const ids = ['id', 'albumId', 'title', 'url', 'thumbnailUrl']
const [sort, setSort] = useState<string[]>(ids)
Expand Down
29 changes: 27 additions & 2 deletions packages/eds-data-grid-react/src/EdsDataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import {
ColumnDef,
ColumnFiltersState,
ColumnPinningState,
getCoreRowModel,
getFacetedMinMaxValues,
getFacetedRowModel,
Expand Down Expand Up @@ -60,11 +61,18 @@ export function EdsDataGrid<T>({
onSortingChange,
manualSorting,
sortingState,
columnPinState,
scrollbarHorizontal,
width,
height,
}: EdsDataGridProps<T>) {
const [sorting, setSorting] = useState<SortingState>(sortingState ?? [])
const [selection, setSelection] = useState<RowSelectionState>(
selectedRows ?? {},
)
const [columnPin, setColumnPin] = useState<ColumnPinningState>(
columnPinState ?? {},
)
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [visible, setVisible] = useState(columnVisibility ?? {})
const [globalFilter, setGlobalFilter] = useState('')
Expand All @@ -78,6 +86,10 @@ export function EdsDataGrid<T>({
setVisible(columnVisibility ?? {})
}, [columnVisibility, setVisible])

useEffect(() => {
setColumnPin((s) => columnPinState ?? s)
}, [columnPinState])

useEffect(() => {
setSorting(sortingState)
}, [sortingState])
Expand Down Expand Up @@ -138,6 +150,7 @@ export function EdsDataGrid<T>({
columnResizeMode: columnResizeMode,
state: {
sorting,
columnPinning: columnPin,
rowSelection: selection,
columnOrder: columnOrderState,
},
Expand All @@ -158,6 +171,8 @@ export function EdsDataGrid<T>({
debugHeaders: debug,
debugColumns: debug,
enableRowSelection: rowSelection ?? false,
enableColumnPinning: true,
enablePinning: true,
}

useEffect(() => {
Expand Down Expand Up @@ -231,7 +246,7 @@ export function EdsDataGrid<T>({
*/
if (enableVirtual) {
parentRefStyle = {
height: virtualHeight ?? 500,
height: height ?? virtualHeight ?? 500,
overflow: 'auto',
position: 'relative',
}
Expand Down Expand Up @@ -278,7 +293,17 @@ export function EdsDataGrid<T>({
enableColumnFiltering={!!enableColumnFiltering}
stickyHeader={!!stickyHeader}
>
<div className="table-wrapper" style={parentRefStyle} ref={parentRef}>
<div
className="table-wrapper"
style={{
height: height ?? 'auto',
...parentRefStyle,
width: scrollbarHorizontal ? width : 'auto',
tableLayout: scrollbarHorizontal ? 'fixed' : 'auto',
overflow: 'auto',
}}
ref={parentRef}
>
<Table
className={Object.entries(classList)
.filter(([, k]) => k)
Expand Down
22 changes: 22 additions & 0 deletions packages/eds-data-grid-react/src/EdsDataGridProps.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Column,
ColumnDef,
ColumnPinningState,
ColumnResizeMode,
OnChangeFn,
Row,
Expand Down Expand Up @@ -58,6 +59,22 @@ type BaseProps<T> = {
* @default {}
*/
selectedRows?: Record<string | number, boolean>
/**
* Whether there should be horizontal scrolling.
* This must be true for column pinning to work
* @default true
*/
scrollbarHorizontal?: boolean
/**
* Width of the table. Only takes effect if {@link scrollbarHorizontal} is true.
* @default 800
*/
width?: number
/**
* Height of the table.
* @default none
*/
height?: number
}

type StyleProps<T> = {
Expand Down Expand Up @@ -159,11 +176,16 @@ type SortProps = {
sortingState?: SortingState
}

type ColumnProps = {
columnPinState?: ColumnPinningState
}

export type EdsDataGridProps<T> = BaseProps<T> &
StyleProps<T> &
SortProps &
FilterProps &
PagingProps &
ColumnProps &
VirtualProps & {
/**
* Which columns are visible. If not set, all columns are visible. undefined means that the column is visible.
Expand Down
46 changes: 39 additions & 7 deletions packages/eds-data-grid-react/src/components/TableBodyCell.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,62 @@
import { Cell, flexRender } from '@tanstack/react-table'
import { Cell, ColumnPinningPosition, flexRender } from '@tanstack/react-table'
import { Table, Typography } from '@equinor/eds-core-react'
import { useTableContext } from '../EdsDataGridContext'
import { useMemo } from 'react'
import { tokens } from '@equinor/eds-tokens'
import styled from 'styled-components'

type Props<T> = {
cell: Cell<T, unknown>
}

const StyledCell = styled(Table.Cell)<{
$pinned: ColumnPinningPosition
$offset: number
}>`
position: ${(p) => (p.$pinned ? 'sticky' : 'relative')};
${(p) => {
if (p.$pinned) {
return `${p.$pinned}: ${p.$offset}px;`
}
return ''
}}
z-index: ${(p) => (p.$pinned ? 11 : 'auto')};
background-color: ${(p) =>
p.$pinned ? tokens.colors.ui.background__default.hex : 'inherit'};
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`

export function TableBodyCell<T>({ cell }: Props<T>) {
const { cellClass, cellStyle } = useTableContext()
const { cellClass, cellStyle, table } = useTableContext()
const pinned = cell.column.getIsPinned()
const pinnedOffset = useMemo<number>(() => {
if (!pinned) {
return 0
}
const header = table.getFlatHeaders().find((h) => h.id === cell.column.id)
return pinned === 'left'
? header.getStart()
: table.getTotalSize() - header.getStart() - cell.column.getSize()
}, [pinned, cell.column, table])
return (
<Table.Cell
<StyledCell
$pinned={pinned}
$offset={pinnedOffset}
className={cellClass ? cellClass(cell.row, cell.column.id) : ''}
{...{
key: cell.id,
style: {
width: cell.column.getSize(),
maxWidth: cell.column.getSize(),
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
...(cellStyle?.(cell.row, cell.column.id) ?? {}),
},
}}
>
<Typography as="span" group="table" variant="cell_text">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Typography>
</Table.Cell>
</StyledCell>
)
}
44 changes: 39 additions & 5 deletions packages/eds-data-grid-react/src/components/TableHeaderCell.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
ColumnPinningPosition,
ColumnResizeMode,
flexRender,
Header,
Expand All @@ -11,6 +12,7 @@ import { useTableContext } from '../EdsDataGridContext'
import { Filter } from './Filter'
import styled from 'styled-components'
import { tokens } from '@equinor/eds-tokens'
import { useMemo } from 'react'

type Props<T> = {
header: Header<T, unknown>
Expand Down Expand Up @@ -44,6 +46,7 @@ const ResizeInner = styled.div`
const Resizer = styled.div<ResizeProps>`
transform: ${(props) =>
props.$columnResizeMode === 'onEnd' ? 'translateX(0px)' : 'none'};
${ResizeInner} {
opacity: ${(props) => (props.$isResizing ? 1 : 0)};
}
Expand All @@ -60,10 +63,26 @@ const Resizer = styled.div<ResizeProps>`
justify-content: flex-end;
`

const Cell = styled(Table.Cell)<{ sticky: boolean }>`
const Cell = styled(Table.Cell)<{
$sticky: boolean
$pinned: ColumnPinningPosition
$offset: number
}>`
font-weight: bold;
height: 30px;
position: ${(p) => (p.sticky ? 'sticky' : 'relative')};
position: ${(p) => (p.$sticky || p.$pinned ? 'sticky' : 'relative')};
top: 0;
${(p) => {
if (p.$pinned) {
return `${p.$pinned}: ${p.$offset}px;`
}
return ''
}}
z-index: ${(p) => {
if (p.$sticky && p.$pinned) return 13
if (p.$sticky || p.$pinned) return 12
return 'auto'
}};
&:hover ${ResizeInner} {
background: ${tokens.colors.interactive.primary__hover.rgba};
opacity: 1;
Expand All @@ -73,16 +92,31 @@ const Cell = styled(Table.Cell)<{ sticky: boolean }>`
export function TableHeaderCell<T>({ header, columnResizeMode }: Props<T>) {
const ctx = useTableContext()
const table = ctx.table
const pinned = header.column.getIsPinned()
const offset = useMemo<number>(() => {
if (!pinned) {
return null
}
return pinned === 'left'
? header.getStart()
: table.getTotalSize() - header.getStart() - header.getSize()
}, [pinned, header, table])
return header.isPlaceholder ? (
<Cell
sticky={ctx.stickyHeader}
$sticky={ctx.stickyHeader}
$offset={offset}
$pinned={pinned}
className={ctx.headerClass ? ctx.headerClass(header.column) : ''}
style={ctx.headerStyle ? ctx.headerStyle(header.column) : {}}
style={{
...(ctx.headerStyle ? ctx.headerStyle(header.column) : {}),
}}
aria-hidden={true}
/>
) : (
<Cell
sticky={ctx.stickyHeader}
$sticky={ctx.stickyHeader}
$offset={offset}
$pinned={pinned}
className={ctx.headerClass ? ctx.headerClass(header.column) : ''}
aria-sort={getSortLabel(header.column.getIsSorted())}
{...{
Expand Down
2 changes: 2 additions & 0 deletions packages/eds-data-grid-react/src/stories/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const Link = styled.a`
export const columns: Array<ColumnDef<Photo>> = [
helper.accessor('id', {
header: () => <span style={{ fontStyle: 'italic' }}>ID</span>,
size: 100,
id: 'id',
}),
helper.accessor('albumId', {
Expand All @@ -28,6 +29,7 @@ export const columns: Array<ColumnDef<Photo>> = [
helper.accessor('title', {
header: 'Title',
id: 'title',
size: 250,
}),
helper.accessor('url', {
header: 'URL',
Expand Down

0 comments on commit e60f98f

Please sign in to comment.