From ae92dc8d1b54c54d235682e8489de703da7e30e4 Mon Sep 17 00:00:00 2001 From: maximallain Date: Thu, 7 Nov 2024 16:46:08 +0100 Subject: [PATCH 1/4] feat(table): add table size --- src/Table.tsx | 59 +++++++++++++++++++++++---------------- stories/Table.stories.tsx | 36 ++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 24 deletions(-) diff --git a/src/Table.tsx b/src/Table.tsx index 7a67b7a84..ed0a7258d 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -23,15 +23,18 @@ export type TableProps = { noCaption?: boolean; /** Default: false */ bottomCaption?: boolean; + size?: TableProps.Size; style?: CSSProperties; colorVariant?: TableProps.ColorVariant; }; export namespace TableProps { + export type Size = "sm" | "md" | "lg"; + type ExtractColorVariant = FrClassName extends `fr-table--${infer AccentColor}` ? Exclude< AccentColor, - "no-scroll" | "no-caption" | "caption-bottom" | "layout-fixed" | "bordered" + "no-scroll" | "no-caption" | "caption-bottom" | "layout-fixed" | "bordered" | Size > : never; @@ -51,6 +54,7 @@ export const Table = memo( fixed = false, noCaption = false, bottomCaption = false, + size = "md", colorVariant, className, style, @@ -71,6 +75,7 @@ export const Table = memo( style={style} className={cx( fr.cx( + size !== "md" && `fr-table--${size}`, "fr-table", { "fr-table--bordered": bordered, @@ -84,29 +89,35 @@ export const Table = memo( className )} > - - {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) => ( + + {row.map((col, j) => ( + + ))} + + ))} + +
{caption}
+ {header} +
{col}
+
+
+
); }) diff --git a/stories/Table.stories.tsx b/stories/Table.stories.tsx index aaa936ee1..e70cd2215 100644 --- a/stories/Table.stories.tsx +++ b/stories/Table.stories.tsx @@ -175,3 +175,39 @@ export const TableWithColorVariant = getStory({ ["Lorem ipsum d", "Lorem ipsu"] ] }); + +export const SmallTable = 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": "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" +}); From 2b213f62b9430d4f9386510a63c8d3ecfd9a53ef Mon Sep 17 00:00:00 2001 From: maximallain Date: Thu, 7 Nov 2024 18:10:58 +0100 Subject: [PATCH 2/4] feat(table): remove color variant and add cell alignment --- src/Table.tsx | 53 ++++++++++++++--------- stories/Table.stories.tsx | 91 ++++++++++++++++++--------------------- 2 files changed, 73 insertions(+), 71 deletions(-) diff --git a/src/Table.tsx b/src/Table.tsx index ed0a7258d..a50977eff 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -23,22 +23,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 { export type Size = "sm" | "md" | "lg"; - type ExtractColorVariant = FrClassName extends `fr-table--${infer AccentColor}` - ? Exclude< - AccentColor, - "no-scroll" | "no-caption" | "caption-bottom" | "layout-fixed" | "bordered" | Size - > + type ExtractCellClasses = FrClassName extends `fr-cell--${infer Alignment}` + ? Alignment : never; - export type ColorVariant = ExtractColorVariant; + export type Alignment = ExtractCellClasses & + ("center" | "top" | "bottom" | "right"); } /** @see */ @@ -55,7 +53,7 @@ export const Table = memo( noCaption = false, bottomCaption = false, size = "md", - colorVariant, + cellsAlignment = undefined, className, style, ...rest @@ -68,24 +66,35 @@ export const Table = memo( "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; + }; + return (
@@ -109,7 +118,9 @@ export const Table = memo( {data.map((row, i) => ( {row.map((col, j) => ( - {col} + + {col} + ))} ))} diff --git a/stories/Table.stories.tsx b/stories/Table.stories.tsx index e70cd2215..06b22e8e0 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, @@ -36,35 +35,6 @@ const { meta, getStory } = getStoryFactory({ "bottomCaption": { "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" } } } } }); @@ -158,24 +128,6 @@ export const TableWithBottomCaption = getStory({ ] }); -export const TableWithColorVariant = getStory({ - "colorVariant": "green-emeraude", - "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"] - ] -}); - export const SmallTable = getStory({ "caption": "Titre du tableau", "headers": ["td", "titre"], @@ -211,3 +163,42 @@ export const LargeTable = getStory({ ], "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"] + ] +}); From fc3d008fe87262d74f8e2850cc983958a692b9d6 Mon Sep 17 00:00:00 2001 From: maximallain Date: Fri, 8 Nov 2024 16:40:13 +0100 Subject: [PATCH 3/4] feat(table): add column header --- src/Table.tsx | 33 +++++++++++++++++++++++++++------ stories/Table.stories.tsx | 16 ++++++++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/Table.tsx b/src/Table.tsx index a50977eff..d8fd9c3f8 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -14,6 +14,8 @@ export type TableProps = { caption?: ReactNode; headers?: ReactNode[]; /** Default: false */ + headColumn?: boolean; + /** Default: false */ fixed?: boolean; /** Default: false */ noScroll?: boolean; @@ -46,6 +48,7 @@ export const Table = memo( id: id_props, data, headers, + headColumn = false, caption, bordered = false, noScroll = false, @@ -82,6 +85,10 @@ export const Table = memo( return undefined; }; + const getRole = (headColumn: boolean, i: number): React.AriaRole | undefined => { + return headColumn && i === 0 ? "rowheader" : undefined; + }; + return (
{headers.map((header, i) => ( - + {header} ))} @@ -117,11 +128,21 @@ export const Table = memo( {data.map((row, i) => ( - {row.map((col, j) => ( - - {col} - - ))} + {row.map((col, j) => { + const role = getRole(headColumn, j); + const HtmlElement = + role === undefined ? "td" : "th"; + + return ( + + {col} + + ); + })} ))} diff --git a/stories/Table.stories.tsx b/stories/Table.stories.tsx index 06b22e8e0..0778b0a20 100644 --- a/stories/Table.stories.tsx +++ b/stories/Table.stories.tsx @@ -35,6 +35,10 @@ const { meta, getStory } = getStoryFactory({ "bottomCaption": { "description": "Move caption to bottom", "type": { "name": "boolean" } + }, + "headColumn": { + "description": "Add a header column", + "type": { "name": "boolean" } } } }); @@ -128,6 +132,18 @@ export const TableWithBottomCaption = getStory({ ] }); +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 SmallTable = getStory({ "caption": "Titre du tableau", "headers": ["td", "titre"], From 98b9ce2cbad3ae3c712cc7bd0ae37a198670fc9d Mon Sep 17 00:00:00 2001 From: maximallain Date: Wed, 13 Nov 2024 10:40:03 +0100 Subject: [PATCH 4/4] feat(table): add selectable row --- src/Table.tsx | 80 +++++++++++++++++++++++++++++---------- stories/Table.stories.tsx | 17 +++++++++ 2 files changed, 78 insertions(+), 19 deletions(-) diff --git a/src/Table.tsx b/src/Table.tsx index d8fd9c3f8..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"; @@ -16,6 +16,8 @@ export type TableProps = { /** Default: false */ headColumn?: boolean; /** Default: false */ + selectableRows?: boolean; + /** Default: false */ fixed?: boolean; /** Default: false */ noScroll?: boolean; @@ -49,6 +51,7 @@ export const Table = memo( data, headers, headColumn = false, + selectableRows = false, caption, bordered = false, noScroll = false, @@ -64,6 +67,8 @@ export const Table = memo( assert>(); + const [checkedIds, setCheckedIds] = useState([]); + const id = useAnalyticsId({ "defaultIdPrefix": "fr-table", "explicitlyProvidedId": id_props @@ -126,25 +131,62 @@ export const Table = memo( )} - {data.map((row, i) => ( - - {row.map((col, j) => { - const role = getRole(headColumn, j); - const HtmlElement = - role === undefined ? "td" : "th"; + {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} - - ); - })} - - ))} + return ( + + {col} + + ); + })} + + ); + })}
diff --git a/stories/Table.stories.tsx b/stories/Table.stories.tsx index 0778b0a20..0fcd9a199 100644 --- a/stories/Table.stories.tsx +++ b/stories/Table.stories.tsx @@ -39,6 +39,10 @@ const { meta, getStory } = getStoryFactory({ "headColumn": { "description": "Add a header column", "type": { "name": "boolean" } + }, + "selectableRows": { + "description": "Add a checkbox column", + "type": { "name": "boolean" } } } }); @@ -144,6 +148,19 @@ export const TableWithHeadColumn = getStory({ ] }); +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"],