From e60f98fcfdb3e8e8d6b7a48fc25a9c391cd220b8 Mon Sep 17 00:00:00 2001 From: Ronnie Laugen Date: Wed, 6 Dec 2023 07:20:53 +0100 Subject: [PATCH] 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 --- .../src/EdsDataGrid.docs.mdx | 14 +++++- .../src/EdsDataGrid.stories.tsx | 25 ++++++++++ .../eds-data-grid-react/src/EdsDataGrid.tsx | 29 +++++++++++- .../src/EdsDataGridProps.ts | 22 +++++++++ .../src/components/TableBodyCell.tsx | 46 ++++++++++++++++--- .../src/components/TableHeaderCell.tsx | 44 ++++++++++++++++-- .../src/stories/columns.tsx | 2 + 7 files changed, 166 insertions(+), 16 deletions(-) diff --git a/packages/eds-data-grid-react/src/EdsDataGrid.docs.mdx b/packages/eds-data-grid-react/src/EdsDataGrid.docs.mdx index 91da6a1af0..b6bbf026f7 100644 --- a/packages/eds-data-grid-react/src/EdsDataGrid.docs.mdx +++ b/packages/eds-data-grid-react/src/EdsDataGrid.docs.mdx @@ -84,17 +84,27 @@ Allows the user to hide/show columns. +### 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) + + + ### 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) ### 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) diff --git a/packages/eds-data-grid-react/src/EdsDataGrid.stories.tsx b/packages/eds-data-grid-react/src/EdsDataGrid.stories.tsx index 85c056ff83..39b5f243dd 100644 --- a/packages/eds-data-grid-react/src/EdsDataGrid.stories.tsx +++ b/packages/eds-data-grid-react/src/EdsDataGrid.stories.tsx @@ -137,6 +137,31 @@ ManualSorting.args = { columns: groupedColumns, } +export const ColumnPinning: StoryFn> = (args) => { + const { columnPinState } = args + return ( + <> + + {JSON.stringify(columnPinState, null, 2)} + + + + ) +} + +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> = (args) => { const ids = ['id', 'albumId', 'title', 'url', 'thumbnailUrl'] const [sort, setSort] = useState(ids) diff --git a/packages/eds-data-grid-react/src/EdsDataGrid.tsx b/packages/eds-data-grid-react/src/EdsDataGrid.tsx index fdae15a753..c5417eabf8 100644 --- a/packages/eds-data-grid-react/src/EdsDataGrid.tsx +++ b/packages/eds-data-grid-react/src/EdsDataGrid.tsx @@ -2,6 +2,7 @@ import { ColumnDef, ColumnFiltersState, + ColumnPinningState, getCoreRowModel, getFacetedMinMaxValues, getFacetedRowModel, @@ -60,11 +61,18 @@ export function EdsDataGrid({ onSortingChange, manualSorting, sortingState, + columnPinState, + scrollbarHorizontal, + width, + height, }: EdsDataGridProps) { const [sorting, setSorting] = useState(sortingState ?? []) const [selection, setSelection] = useState( selectedRows ?? {}, ) + const [columnPin, setColumnPin] = useState( + columnPinState ?? {}, + ) const [columnFilters, setColumnFilters] = useState([]) const [visible, setVisible] = useState(columnVisibility ?? {}) const [globalFilter, setGlobalFilter] = useState('') @@ -78,6 +86,10 @@ export function EdsDataGrid({ setVisible(columnVisibility ?? {}) }, [columnVisibility, setVisible]) + useEffect(() => { + setColumnPin((s) => columnPinState ?? s) + }, [columnPinState]) + useEffect(() => { setSorting(sortingState) }, [sortingState]) @@ -138,6 +150,7 @@ export function EdsDataGrid({ columnResizeMode: columnResizeMode, state: { sorting, + columnPinning: columnPin, rowSelection: selection, columnOrder: columnOrderState, }, @@ -158,6 +171,8 @@ export function EdsDataGrid({ debugHeaders: debug, debugColumns: debug, enableRowSelection: rowSelection ?? false, + enableColumnPinning: true, + enablePinning: true, } useEffect(() => { @@ -231,7 +246,7 @@ export function EdsDataGrid({ */ if (enableVirtual) { parentRefStyle = { - height: virtualHeight ?? 500, + height: height ?? virtualHeight ?? 500, overflow: 'auto', position: 'relative', } @@ -278,7 +293,17 @@ export function EdsDataGrid({ enableColumnFiltering={!!enableColumnFiltering} stickyHeader={!!stickyHeader} > -
+
k) diff --git a/packages/eds-data-grid-react/src/EdsDataGridProps.ts b/packages/eds-data-grid-react/src/EdsDataGridProps.ts index 4b7b6bbce9..8a1ee2ad4c 100644 --- a/packages/eds-data-grid-react/src/EdsDataGridProps.ts +++ b/packages/eds-data-grid-react/src/EdsDataGridProps.ts @@ -1,6 +1,7 @@ import { Column, ColumnDef, + ColumnPinningState, ColumnResizeMode, OnChangeFn, Row, @@ -58,6 +59,22 @@ type BaseProps = { * @default {} */ selectedRows?: Record + /** + * 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 = { @@ -159,11 +176,16 @@ type SortProps = { sortingState?: SortingState } +type ColumnProps = { + columnPinState?: ColumnPinningState +} + export type EdsDataGridProps = BaseProps & StyleProps & SortProps & FilterProps & PagingProps & + ColumnProps & VirtualProps & { /** * Which columns are visible. If not set, all columns are visible. undefined means that the column is visible. diff --git a/packages/eds-data-grid-react/src/components/TableBodyCell.tsx b/packages/eds-data-grid-react/src/components/TableBodyCell.tsx index 889f90e656..bed5c50a01 100644 --- a/packages/eds-data-grid-react/src/components/TableBodyCell.tsx +++ b/packages/eds-data-grid-react/src/components/TableBodyCell.tsx @@ -1,23 +1,55 @@ -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 = { cell: Cell } + +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({ cell }: Props) { - const { cellClass, cellStyle } = useTableContext() + const { cellClass, cellStyle, table } = useTableContext() + const pinned = cell.column.getIsPinned() + const pinnedOffset = useMemo(() => { + 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 ( - ({ cell }: Props) { {flexRender(cell.column.columnDef.cell, cell.getContext())} - + ) } diff --git a/packages/eds-data-grid-react/src/components/TableHeaderCell.tsx b/packages/eds-data-grid-react/src/components/TableHeaderCell.tsx index 8a3b56f84e..4e80f8fed1 100644 --- a/packages/eds-data-grid-react/src/components/TableHeaderCell.tsx +++ b/packages/eds-data-grid-react/src/components/TableHeaderCell.tsx @@ -1,4 +1,5 @@ import { + ColumnPinningPosition, ColumnResizeMode, flexRender, Header, @@ -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 = { header: Header @@ -44,6 +46,7 @@ const ResizeInner = styled.div` const Resizer = styled.div` transform: ${(props) => props.$columnResizeMode === 'onEnd' ? 'translateX(0px)' : 'none'}; + ${ResizeInner} { opacity: ${(props) => (props.$isResizing ? 1 : 0)}; } @@ -60,10 +63,26 @@ const Resizer = styled.div` 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; @@ -73,16 +92,31 @@ const Cell = styled(Table.Cell)<{ sticky: boolean }>` export function TableHeaderCell({ header, columnResizeMode }: Props) { const ctx = useTableContext() const table = ctx.table + const pinned = header.column.getIsPinned() + const offset = useMemo(() => { + if (!pinned) { + return null + } + return pinned === 'left' + ? header.getStart() + : table.getTotalSize() - header.getStart() - header.getSize() + }, [pinned, header, table]) return header.isPlaceholder ? ( ) : ( > = [ helper.accessor('id', { header: () => ID, + size: 100, id: 'id', }), helper.accessor('albumId', { @@ -28,6 +29,7 @@ export const columns: Array> = [ helper.accessor('title', { header: 'Title', id: 'title', + size: 250, }), helper.accessor('url', { header: 'URL',