diff --git a/src/Table.tsx b/src/Table.tsx index 7a67b7a84..7c8a26c97 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef, memo, type ReactNode, type CSSProperties } from "react"; +import React, { forwardRef, memo, type ReactNode, type CSSProperties, useState } from "react"; import { assert } from "tsafe/assert"; import type { Equals } from "tsafe"; import { fr } from "./fr"; @@ -14,6 +14,10 @@ export type TableProps = { caption?: ReactNode; headers?: ReactNode[]; /** Default: false */ + headColumn?: boolean; + /** Default: false */ + selectableRows?: boolean; + /** Default: false */ fixed?: boolean; /** Default: false */ noScroll?: boolean; @@ -23,19 +27,20 @@ export type TableProps = { noCaption?: boolean; /** Default: false */ bottomCaption?: boolean; + cellsAlignment?: (TableProps.Alignment | undefined)[][] | (TableProps.Alignment | undefined)[]; + size?: TableProps.Size; style?: CSSProperties; - colorVariant?: TableProps.ColorVariant; }; export namespace TableProps { - type ExtractColorVariant = FrClassName extends `fr-table--${infer AccentColor}` - ? Exclude< - AccentColor, - "no-scroll" | "no-caption" | "caption-bottom" | "layout-fixed" | "bordered" - > + export type Size = "sm" | "md" | "lg"; + + type ExtractCellClasses = FrClassName extends `fr-cell--${infer Alignment}` + ? Alignment : never; - export type ColorVariant = ExtractColorVariant; + export type Alignment = ExtractCellClasses & + ("center" | "top" | "bottom" | "right"); } /** @see */ @@ -45,13 +50,16 @@ export const Table = memo( id: id_props, data, headers, + headColumn = false, + selectableRows = false, caption, bordered = false, noScroll = false, fixed = false, noCaption = false, bottomCaption = false, - colorVariant, + size = "md", + cellsAlignment = undefined, className, style, ...rest @@ -59,54 +67,131 @@ export const Table = memo( assert>(); + const [checkedIds, setCheckedIds] = useState([]); + const id = useAnalyticsId({ "defaultIdPrefix": "fr-table", "explicitlyProvidedId": id_props }); + const getCellAlignment = (i: number, j: number): undefined | string => { + if (Array.isArray(cellsAlignment)) { + const rowCellsAlignement = cellsAlignment[i]; + if (Array.isArray(rowCellsAlignement)) { + const cellAlignement = rowCellsAlignement[j]; + return cellAlignement === undefined ? undefined : `fr-cell--${cellAlignement}`; + } + + const cellAlignement = cellsAlignment[j]; + return cellAlignement === undefined || Array.isArray(cellAlignement) + ? undefined + : `fr-cell--${cellAlignement}`; + } + return undefined; + }; + + const getRole = (headColumn: boolean, i: number): React.AriaRole | undefined => { + return headColumn && i === 0 ? "rowheader" : undefined; + }; + return (
- - {caption !== undefined && } - {headers !== undefined && ( - - - {headers.map((header, i) => ( - - ))} - - - )} - - {data.map((row, i) => ( - - {row.map((col, j) => ( - - ))} - - ))} - -
{caption}
- {header} -
{col}
+
+
+
+ + {caption !== undefined && } + {headers !== undefined && ( + + + {headers.map((header, i) => ( + + ))} + + + )} + + {data.map((row, i) => { + const isChecked = checkedIds.includes(i); + return ( + + {row.map((col, j) => { + const role = getRole(headColumn, j); + const HtmlElement = + role === undefined ? "td" : "th"; + const isSelectable = selectableRows && j === 0; + if (isSelectable) { + return ( + +
{ + setCheckedIds( + isChecked + ? checkedIds.filter( + id => id !== i + ) + : [...checkedIds, i] + ); + }} + > + + +
+
+ ); + } + + return ( + + {col} + + ); + })} + + ); + })} + +
{caption}
+ {header} +
+
+
+
); }) diff --git a/stories/Table.stories.tsx b/stories/Table.stories.tsx index aaa936ee1..0fcd9a199 100644 --- a/stories/Table.stories.tsx +++ b/stories/Table.stories.tsx @@ -1,8 +1,7 @@ -import { Table, type TableProps } from "../dist/Table"; +import { Table } from "../dist/Table"; import { getStoryFactory } from "./getStory"; import { sectionName } from "./sectionName"; -import { assert } from "tsafe/assert"; -import type { Equals } from "tsafe"; +import React from "react"; const { meta, getStory } = getStoryFactory({ sectionName, @@ -37,34 +36,13 @@ const { meta, getStory } = getStoryFactory({ "description": "Move caption to bottom", "type": { "name": "boolean" } }, - "colorVariant": { - "options": (() => { - const options = [ - "green-tilleul-verveine", - "green-bourgeon", - "green-emeraude", - "green-menthe", - "green-archipel", - "blue-ecume", - "blue-cumulus", - "purple-glycine", - "pink-macaron", - "pink-tuile", - "brown-cafe-creme", - "brown-caramel", - "brown-opera", - "orange-terre-battue", - "yellow-moutarde", - "yellow-tournesol", - "beige-gris-galet", - undefined - ] as const; - - assert>(); - - return options; - })(), - "control": { "type": "select", "labels": { "null": "no color variant" } } + "headColumn": { + "description": "Add a header column", + "type": { "name": "boolean" } + }, + "selectableRows": { + "description": "Add a checkbox column", + "type": { "name": "boolean" } } } }); @@ -158,8 +136,32 @@ export const TableWithBottomCaption = getStory({ ] }); -export const TableWithColorVariant = getStory({ - "colorVariant": "green-emeraude", +export const TableWithHeadColumn = getStory({ + "headColumn": true, + "caption": "Titre du tableau", + "headers": ["", "titre"], + "data": [ + ["ligne 1", "Lorem ipsum dolor sit amet consectetur"], + ["ligne 2", "Lorem ipsu"], + ["ligne 3", "Lorem ipsum dolor sit amet consectetur"], + ["ligne 4", "Lorem ipsu"] + ] +}); + +export const SelectableRowsTableWithHeadColumn = getStory({ + "headColumn": true, + "selectableRows": true, + "caption": "Titre du tableau", + "headers": ["", "titre"], + "data": [ + ["ligne 1", "Lorem ipsum dolor sit amet consectetur"], + ["ligne 2", "Lorem ipsu"], + ["ligne 3", "Lorem ipsum dolor sit amet consectetur"], + ["ligne 4", "Lorem ipsu"] + ] +}); + +export const SmallTable = getStory({ "caption": "Titre du tableau", "headers": ["td", "titre"], "data": [ @@ -173,5 +175,63 @@ export const TableWithColorVariant = getStory({ "Lorem ipsum dolor sit amet consectetur" ], ["Lorem ipsum d", "Lorem ipsu"] + ], + "size": "sm" +}); + +export const LargeTable = getStory({ + "caption": "Titre du tableau", + "headers": ["td", "titre"], + "data": [ + [ + "Lorem ipsum dolor sit amet consectetur adipisicin", + "Lorem ipsum dolor sit amet consectetur" + ], + ["Lorem ipsum d", "Lorem ipsu"], + [ + "Lorem ipsum dolor sit amet consectetur adipisicin", + "Lorem ipsum dolor sit amet consectetur" + ], + ["Lorem ipsum d", "Lorem ipsu"] + ], + "size": "lg" +}); + +const CellWithBr = ( + + Lorem
+ ipsu +
d +
+); + +export const TableWithSomeColumnAlignement = getStory({ + "caption": "Titre du tableau", + "headers": ["aligné à droite", "aligné au centre", "aligné en haut", "aligné en bas"], + "data": [ + [CellWithBr, "Lorem ipsum d", "Lorem ipsum d", "Lorem ipsum d"], + ["Lorem ipsum d", CellWithBr, "Lorem ipsu", "Lorem ipsum d"], + ["Lorem ipsum d", "Lorem ipsum d", CellWithBr, "Lorem ipsum d"], + ["Lorem ipsum d", "Lorem ipsu", "Lorem ipsum d", CellWithBr] + ], + "size": "lg", + "cellsAlignment": ["right", "center", "top", "bottom"] +}); + +export const TableWithSomeCellAlignement = getStory({ + "caption": "Titre du tableau", + "headers": ["colonne 1", "colonne 2", "colonne 3", "colonne 4"], + "data": [ + ["aligné à droite", "Lorem ipsum d", "Lorem ipsum d", CellWithBr], + ["Lorem ipsum d", "aligné au centre", "Lorem ipsum d", CellWithBr], + ["Lorem ipsum d", "Lorem ipsum d", "aligné en haut", CellWithBr], + ["Lorem ipsum d", "Lorem ipsu", CellWithBr, "aligné en bas"] + ], + "size": "lg", + "cellsAlignment": [ + ["right", undefined, undefined, undefined], + [undefined, "center", undefined, undefined], + [undefined, undefined, "top", undefined], + [undefined, undefined, undefined, "bottom"] ] });