From 6c75ec97de60491124c4585399a4bf982e733ceb Mon Sep 17 00:00:00 2001 From: Tom Jonkman Date: Mon, 16 Dec 2024 10:03:02 +0100 Subject: [PATCH] feat(components): table restyle (#953) * Improve Table styling * Add sorting functionality * Make table layout fixed * Pass all props to Table component --------- Co-authored-by: sushitommy <> --- .changeset/gentle-buses-scream.md | 8 ++ components/table/src/defaultArgs.ts | 6 +- components/table/src/icons.tsx | 47 ++++++++++ components/table/src/index.scss | 24 +++++ components/table/src/template.tsx | 93 ++++++++++++++----- .../src/components/rvo/table.tokens.json | 7 +- 6 files changed, 156 insertions(+), 29 deletions(-) create mode 100644 .changeset/gentle-buses-scream.md create mode 100644 components/table/src/icons.tsx diff --git a/.changeset/gentle-buses-scream.md b/.changeset/gentle-buses-scream.md new file mode 100644 index 0000000000..63684b06ef --- /dev/null +++ b/.changeset/gentle-buses-scream.md @@ -0,0 +1,8 @@ +--- +"@nl-rvo/design-tokens": minor +"@nl-rvo/css-table": minor +"@nl-rvo/component-library-css": minor +"@nl-rvo/component-library-react": minor +--- + +Improved Table styling diff --git a/components/table/src/defaultArgs.ts b/components/table/src/defaultArgs.ts index ff2b01883d..a7e3f35f82 100644 --- a/components/table/src/defaultArgs.ts +++ b/components/table/src/defaultArgs.ts @@ -3,10 +3,10 @@ import { ITableProps } from './template'; export const defaultArgs: ITableProps = { description: 'Table description.', columns: [ - { label: 'Title' }, + { label: 'Title', sortable: true }, { label: 'Text', sortable: true, sortDirection: 'ASC' }, - { label: 'Price ($)', type: 'numeric' }, - { label: 'Link' }, + { label: 'Price ($)', sortable: true, type: 'numeric' }, + { label: 'Link', sortable: true }, ], rows: [ ['Title value 1', 'Text value 1', '57', 'Link 1'], diff --git a/components/table/src/icons.tsx b/components/table/src/icons.tsx new file mode 100644 index 0000000000..e0a34a6868 --- /dev/null +++ b/components/table/src/icons.tsx @@ -0,0 +1,47 @@ +import { SVGProps } from 'react'; + +export const SortDescendingIcon = (props: SVGProps) => ( + + + + +); + +export const SortAscendingIcon = (props: SVGProps) => ( + + + + +); + +export const SortDefaultIcon = (props: SVGProps) => ( + + + + + + + + + + + +); diff --git a/components/table/src/index.scss b/components/table/src/index.scss index d1fd5853b8..a57c271ec3 100644 --- a/components/table/src/index.scss +++ b/components/table/src/index.scss @@ -5,6 +5,7 @@ .rvo-table { border-collapse: collapse; border-spacing: 0; + table-layout: fixed; width: 100%; } @@ -21,6 +22,9 @@ .rvo-table-header { background-color: var(--rvo-table-header-background-color); + border-bottom-color: var(--rvo-table-header-border-bottom-color); + border-bottom-style: var(--rvo-table-header-border-bottom-style); + border-bottom-width: var(--rvo-table-header-border-bottom-width); color: var(--rvo-table-header-color); font-weight: var(--rvo-table-header-font-weight); padding-block-end: var(--rvo-table-header-padding-block-end); @@ -30,6 +34,26 @@ text-align: start; } +.rvo-table-header__sortable-container { + align-items: center; + display: flex; + gap: var(--rvo-space-sm); +} + +.rvo-table-header__sortable-button { + min-inline-size: 0; + padding-block-end: 0; + padding-block-start: 0; + padding-inline-end: 0; + padding-inline-start: 0; +} + +.rvo--table-header__sorting-icon { + display: flex; + height: var(--rvo-icon-md-height); + width: var(--rvo-icon-md-width); +} + .rvo-table-header--sortable:hover { background-color: var(--rvo-table-header-sortable-hover-background-color); cursor: pointer; diff --git a/components/table/src/template.tsx b/components/table/src/template.tsx index 0d6543bf20..28d8aebf91 100644 --- a/components/table/src/template.tsx +++ b/components/table/src/template.tsx @@ -3,23 +3,25 @@ * Copyright (c) 2021 Community for NL Design System */ import clsx from 'clsx'; -import React from 'react'; +import React, { HTMLAttributes, useCallback, useEffect, useState } from 'react'; import { defaultArgs } from './defaultArgs'; -import { Button } from '../../button/src/template'; +import { SortAscendingIcon, SortDescendingIcon } from './icons'; import validateHTML from '../../utils/validateHTML'; import './index.scss'; -export interface ITableColumnProps { +export interface ITableColumnProps extends HTMLAttributes { label: string; type?: 'numeric'; sortable?: boolean; sortDirection?: 'ASC' | 'DESC' | ''; + key?: string; } export interface ITableProps { description?: string; columns: ITableColumnProps[]; rows: string[][]; + onSort?: (columnIndex: number, direction: 'ASC' | 'DESC' | '') => void; } export const argTypes = { @@ -40,27 +42,67 @@ export const argTypes = { }, }; +const sortData = (rows: string[][], columnIndex: number, direction: 'ASC' | 'DESC' | '', type?: string): string[][] => { + if (!direction) return rows; + + return [...rows].sort((a, b) => { + const aValue = a[columnIndex]; + const bValue = b[columnIndex]; + + if (type === 'numeric') { + const aNum = parseFloat(aValue); + const bNum = parseFloat(bValue); + return direction === 'ASC' ? aNum - bNum : bNum - aNum; + } + + return direction === 'ASC' ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue); + }); +}; + export const Table: React.FC = ({ description, columns = defaultArgs.columns, rows = defaultArgs.rows, + onSort, + ...props }: ITableProps) => { + const [internalColumns, setInternalColumns] = useState(columns); + const [internalRows, setInternalRows] = useState(rows); + + useEffect(() => { + setInternalColumns(columns); + setInternalRows(rows); + }, [columns, rows]); + + const handleSort = useCallback( + (columnIndex: number) => { + const newColumns = [...internalColumns]; + const currentDirection = newColumns[columnIndex].sortDirection || ''; + const newDirection: 'ASC' | 'DESC' | '' = + currentDirection === '' ? 'ASC' : currentDirection === 'ASC' ? 'DESC' : ''; + + // Reset all other columns + newColumns.forEach((col, i) => { + if (i !== columnIndex) { + col.sortDirection = ''; + } + }); + + newColumns[columnIndex].sortDirection = newDirection; + setInternalColumns(newColumns); + setInternalRows(sortData(rows, columnIndex, newDirection, columns[columnIndex].type)); + onSort?.(columnIndex, newDirection); + }, + [internalColumns, rows, columns, onSort], + ); + return ( -
+
{description && } - {columns.map((column, index) => { - let icon: string; - if (column.sortDirection === 'ASC') { - icon = 'delta-omhoog'; - } else if (column.sortDirection === 'DESC') { - icon = 'delta-omlaag'; - } else { - icon = ''; - } - + {internalColumns.map((column, index) => { return ( ); })} - {rows.map((row, rowIndex) => ( + {internalRows.map((row, rowIndex) => ( - {columns.map((column, columnIndex) => { + {internalColumns.map((column, columnIndex) => { const cellValue = row[columnIndex]; const cellClassNames = clsx('rvo-table-cell', column.type === 'numeric' && 'rvo-table-cell--numeric'); - // Parse cell markup (value is either string, HTML string or React node) let cellMarkup = (
{description}
= ({ className={clsx( 'rvo-table-header', column.sortable && 'rvo-table-header--sortable', - column.sortable && - column.sortDirection && - column.sortDirection.length > 1 && ['rvo-layout-row', 'rvo-layout-gap--sm'], column.type === 'numeric' && 'rvo-table-header--numeric', )} + onClick={column.sortable ? () => handleSort(index) : undefined} + style={column.sortable ? { cursor: 'pointer' } : undefined} > - {column.label} - {column.sortable && column.sortDirection && column.sortDirection.length > 0 && ( -
{cellValue} diff --git a/proprietary/design-tokens/src/components/rvo/table.tokens.json b/proprietary/design-tokens/src/components/rvo/table.tokens.json index 21ca644481..6796257b5c 100644 --- a/proprietary/design-tokens/src/components/rvo/table.tokens.json +++ b/proprietary/design-tokens/src/components/rvo/table.tokens.json @@ -6,7 +6,10 @@ }, "table": { "header": { - "background-color": { "value": "{rvo.color.grijs-050}" }, + "background-color": { "value": "{rvo.color.grijs-200}" }, + "border-bottom-width": { "value": "{rvo.size.3xs}" }, + "border-bottom-style": { "value": "solid" }, + "border-bottom-color": { "value": "{rvo.color.grijs-500}" }, "color": { "value": "{rvo.color.grijs-600}" }, "font-weight": { "value": "{rvo.font-weight.normal}" }, "padding-block-end": { "value": "{rvo.space.xs}" }, @@ -25,7 +28,7 @@ "cell": { "border-bottom-width": { "value": "0.05rem" }, "border-bottom-style": { "value": "solid" }, - "border-bottom-color": { "value": "{rvo.color.grijs-300}" }, + "border-bottom-color": { "value": "{rvo.color.grijs-500}" }, "padding-block-end": { "value": "{rvo.space.xs}" }, "padding-block-start": { "value": "{rvo.space.xs}" }, "padding-inline-end": { "value": "{rvo.space.md}" },