Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(data-grid): Add column pinning feature #3176

Merged
merged 6 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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