Skip to content

Commit 4906719

Browse files
gary-van-woerkensJulien Bouquillongarronej
authored
feat: add table component (#84)
* feat: add table component * add story arguments types * add examples * mark table component as done * add classes * fix color variants type * Update src/Table.tsx Co-authored-by: Julien Bouquillon <contact@revolunet.com> Signed-off-by: Gary van Woerkens <gary.van-woerkens@sg.social.gouv.fr> * Update src/Table.tsx Co-authored-by: Julien Bouquillon <contact@revolunet.com> Signed-off-by: Gary van Woerkens <gary.van-woerkens@sg.social.gouv.fr> * Update src/Table.tsx Co-authored-by: Joseph Garrone <joseph.garrone.gj@gmail.com> Signed-off-by: Gary van Woerkens <gary.van-woerkens@sg.social.gouv.fr> * Update src/Table.tsx Co-authored-by: Joseph Garrone <joseph.garrone.gj@gmail.com> Signed-off-by: Gary van Woerkens <gary.van-woerkens@sg.social.gouv.fr> * Update src/Table.tsx Co-authored-by: Joseph Garrone <joseph.garrone.gj@gmail.com> Signed-off-by: Gary van Woerkens <gary.van-woerkens@sg.social.gouv.fr> * Update src/Table.tsx Co-authored-by: Joseph Garrone <joseph.garrone.gj@gmail.com> Signed-off-by: Gary van Woerkens <gary.van-woerkens@sg.social.gouv.fr> * Update src/Table.tsx Co-authored-by: Joseph Garrone <joseph.garrone.gj@gmail.com> Signed-off-by: Gary van Woerkens <gary.van-woerkens@sg.social.gouv.fr> * use only boolean values * remove classes * add quotes * add quotes again --------- Signed-off-by: Gary van Woerkens <gary.van-woerkens@sg.social.gouv.fr> Co-authored-by: Julien Bouquillon <contact@revolunet.com> Co-authored-by: Joseph Garrone <joseph.garrone.gj@gmail.com>
1 parent bcace1f commit 4906719

File tree

8 files changed

+363
-1
lines changed

8 files changed

+363
-1
lines changed

COMPONENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535
- [x] Footer
3636
- [ ] Translate
3737
- [x] Summary
38-
- [ ] Table
3938
- [x] Tag
39+
- [x] Table
4040
- [x] Download
4141
- [ ] Transcription
4242
- [x] Tile

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@
139139
"./Tile": "./dist/Tile.js",
140140
"./Tag": "./dist/Tag.js",
141141
"./Tabs": "./dist/Tabs.js",
142+
"./Table": "./dist/Table.js",
142143
"./Summary": "./dist/Summary.js",
143144
"./Stepper": "./dist/Stepper.js",
144145
"./SkipLinks": "./dist/SkipLinks.js",

src/Table.tsx

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import React, { forwardRef, memo, type ReactNode, type CSSProperties } from "react";
2+
import { assert } from "tsafe/assert";
3+
import type { Equals } from "tsafe";
4+
import { fr } from "./fr";
5+
import { cx } from "./tools/cx";
6+
import { symToStr } from "tsafe/symToStr";
7+
import type { FrClassName } from "./fr/generatedFromCss/classNames";
8+
9+
export type TableProps = {
10+
data: ReactNode[][];
11+
className?: string;
12+
caption?: ReactNode;
13+
headers?: ReactNode[];
14+
/** Default: false */
15+
fixed?: boolean;
16+
/** Default: false */
17+
noScroll?: boolean;
18+
/** Default: false */
19+
bordered?: boolean;
20+
/** Default: false */
21+
noCaption?: boolean;
22+
/** Default: false */
23+
bottomCaption?: boolean;
24+
style?: CSSProperties;
25+
colorVariant?: TableProps.ColorVariant;
26+
};
27+
28+
export namespace TableProps {
29+
type ExtractColorVariant<FrClassName> = FrClassName extends `fr-table--${infer AccentColor}`
30+
? Exclude<
31+
AccentColor,
32+
"no-scroll" | "no-caption" | "caption-bottom" | "layout-fixed" | "bordered"
33+
>
34+
: never;
35+
36+
export type ColorVariant = ExtractColorVariant<FrClassName>;
37+
}
38+
39+
/** @see <https://react-dsfr-components.etalab.studio/?path=/docs/tableau> */
40+
export const Table = memo(
41+
forwardRef<HTMLDivElement, TableProps>((props, ref) => {
42+
const {
43+
data,
44+
headers,
45+
caption,
46+
bordered = false,
47+
noScroll = false,
48+
fixed = false,
49+
noCaption = false,
50+
bottomCaption = false,
51+
colorVariant,
52+
className,
53+
style,
54+
...rest
55+
} = props;
56+
57+
assert<Equals<keyof typeof rest, never>>();
58+
59+
return (
60+
<div
61+
ref={ref}
62+
style={style}
63+
className={cx(
64+
fr.cx(
65+
"fr-table",
66+
{
67+
"fr-table--bordered": bordered,
68+
"fr-table--no-scroll": noScroll,
69+
"fr-table--layout-fixed": fixed,
70+
"fr-table--no-caption": noCaption,
71+
"fr-table--caption-bottom": bottomCaption
72+
},
73+
colorVariant !== undefined && `fr-table--${colorVariant}`
74+
),
75+
className
76+
)}
77+
>
78+
<table>
79+
{caption !== undefined && <caption>{caption}</caption>}
80+
{headers !== undefined && (
81+
<thead>
82+
<tr>
83+
{headers.map((header, i) => (
84+
<th key={i} scope="col">
85+
{header}
86+
</th>
87+
))}
88+
</tr>
89+
</thead>
90+
)}
91+
<tbody>
92+
{data.map((row, i) => (
93+
<tr key={i}>
94+
{row.map((col, j) => (
95+
<td key={j}>{col}</td>
96+
))}
97+
</tr>
98+
))}
99+
</tbody>
100+
</table>
101+
</div>
102+
);
103+
})
104+
);
105+
106+
Table.displayName = symToStr({ Table });
107+
108+
export default Table;

stories/Table.stories.tsx

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { Table, type TableProps } from "../dist/Table";
2+
import { getStoryFactory } from "./getStory";
3+
import { sectionName } from "./sectionName";
4+
import { assert } from "tsafe/assert";
5+
import type { Equals } from "tsafe";
6+
7+
const { meta, getStory } = getStoryFactory({
8+
sectionName,
9+
"wrappedComponent": { Table },
10+
"description": `
11+
- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/tableau)
12+
- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Table.tsx)`,
13+
"disabledProps": ["lang"],
14+
"argTypes": {
15+
"caption": {
16+
"description": "Table caption"
17+
},
18+
"bordered": {
19+
"description": "Add borders between lines",
20+
"type": { "name": "boolean" }
21+
},
22+
"noScroll": {
23+
"description": "Prevent horizontal scrolling",
24+
"type": { "name": "boolean" }
25+
},
26+
"fixed": {
27+
"description": "Fix columns width",
28+
"type": { "name": "boolean" }
29+
},
30+
"noCaption": {
31+
"description": "Hide caption",
32+
"type": { "name": "boolean" }
33+
},
34+
"bottomCaption": {
35+
"description": "Move caption to bottom",
36+
"type": { "name": "boolean" }
37+
},
38+
"colorVariant": {
39+
"options": (() => {
40+
const options = [
41+
"green-tilleul-verveine",
42+
"green-bourgeon",
43+
"green-emeraude",
44+
"green-menthe",
45+
"green-archipel",
46+
"blue-ecume",
47+
"blue-cumulus",
48+
"purple-glycine",
49+
"pink-macaron",
50+
"pink-tuile",
51+
"brown-cafe-creme",
52+
"brown-caramel",
53+
"brown-opera",
54+
"orange-terre-battue",
55+
"yellow-moutarde",
56+
"yellow-tournesol",
57+
"beige-gris-galet",
58+
undefined
59+
] as const;
60+
61+
assert<Equals<typeof options[number], TableProps["colorVariant"]>>();
62+
63+
return options;
64+
})(),
65+
"control": { "type": "select", "labels": { "null": "no color variant" } }
66+
}
67+
}
68+
});
69+
70+
export default meta;
71+
72+
export const Default = getStory({
73+
"caption": "Résumé du tableau (accessibilité)",
74+
"headers": ["Titre", "Titre", "Titre", "Titre", "Titre"],
75+
"data": [
76+
["Donnée", "Donnée", "Donnée", "Donnée", "Donnée"],
77+
["Donnée", "Donnée", "Donnée", "Donnée", "Donnée"],
78+
["Donnée", "Donnée", "Donnée", "Donnée", "Donnée"],
79+
["Donnée", "Donnée", "Donnée", "Donnée", "Donnée"],
80+
["Donnée", "Donnée", "Donnée", "Donnée", "Donnée"]
81+
]
82+
});
83+
84+
export const TableWithBorders = getStory({
85+
"bordered": true,
86+
"caption": "Résumé du tableau (accessibilité)",
87+
"headers": ["th0", "th1", "th2", "th3"],
88+
"data": [
89+
[
90+
"Lorem [...] elit ut.",
91+
"Lorem [...] elit ut.",
92+
"Lorem [...] elit ut.",
93+
"Lorem [...] elit ut."
94+
],
95+
["Lorem [...] elit ut.", "Lorem", "Lorem [..", "Lor"],
96+
["Lorem [...] elit ut.", "Lorem [...] elit ut.", "Lorem [...]", "Lorem [...]"]
97+
]
98+
});
99+
100+
export const TableNoScroll = getStory({
101+
"noScroll": true,
102+
"caption": "Résumé du tableau (accessibilité)",
103+
"headers": ["Titre", "Titre"],
104+
"data": [
105+
["Donnée", "Donnée"],
106+
["Donnée", "Donnée"]
107+
]
108+
});
109+
110+
export const TableWithFixedLayout = getStory({
111+
"fixed": true,
112+
"caption": "Caption tableau fixé",
113+
"headers": ["td", "titre"],
114+
"data": [
115+
[
116+
"Lorem ipsum dolor sit amet consectetur adipisicin",
117+
"Lorem ipsum dolor sit amet consectetur"
118+
],
119+
["Lorem ipsum d", "Lorem ipsu"]
120+
]
121+
});
122+
123+
export const TableWithoutCaption = getStory({
124+
"noCaption": true,
125+
"caption": "Titre du tableau",
126+
"headers": ["td", "titre"],
127+
"data": [
128+
[
129+
"Lorem ipsum dolor sit amet consectetur adipisicin",
130+
"Lorem ipsum dolor sit amet consectetur"
131+
],
132+
["Lorem ipsum d", "Lorem ipsu"],
133+
[
134+
"Lorem ipsum dolor sit amet consectetur adipisicin",
135+
"Lorem ipsum dolor sit amet consectetur"
136+
],
137+
["Lorem ipsum d", "Lorem ipsu"]
138+
]
139+
});
140+
141+
export const TableWithBottomCaption = getStory({
142+
"bottomCaption": true,
143+
"caption": "Titre du tableau en bas",
144+
"headers": ["td", "titre"],
145+
"data": [
146+
[
147+
"Lorem ipsum dolor sit amet consectetur adipisicin",
148+
"Lorem ipsum dolor sit amet consectetur"
149+
],
150+
["Lorem ipsum d", "Lorem ipsu"],
151+
[
152+
"Lorem ipsum dolor sit amet consectetur adipisicin",
153+
"Lorem ipsum dolor sit amet consectetur"
154+
],
155+
["Lorem ipsum d", "Lorem ipsu"]
156+
]
157+
});
158+
159+
export const TableWithColorVariant = getStory({
160+
"colorVariant": "green-emeraude",
161+
"caption": "Titre du tableau",
162+
"headers": ["td", "titre"],
163+
"data": [
164+
[
165+
"Lorem ipsum dolor sit amet consectetur adipisicin",
166+
"Lorem ipsum dolor sit amet consectetur"
167+
],
168+
["Lorem ipsum d", "Lorem ipsu"],
169+
[
170+
"Lorem ipsum dolor sit amet consectetur adipisicin",
171+
"Lorem ipsum dolor sit amet consectetur"
172+
],
173+
["Lorem ipsum d", "Lorem ipsu"]
174+
]
175+
});

test/integration/cra/src/Home.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Alert } from "@codegouvfr/react-dsfr/Alert";
22
import { fr } from "@codegouvfr/react-dsfr";
33
import { useIsDark } from "@codegouvfr/react-dsfr/useIsDark";
44
import { SideMenu } from "@codegouvfr/react-dsfr/SideMenu";
5+
import { Table } from "@codegouvfr/react-dsfr/Table";
56

67
const sideMenuItems = [
78
{
@@ -104,6 +105,24 @@ export function Home() {
104105
burgerMenuButtonText="Dans cette rubrique"
105106
/>
106107

108+
<TableExample />
107109
</>
108110
);
109111
}
112+
113+
function TableExample() {
114+
return (
115+
<Table
116+
caption = "Titre du tableau"
117+
colorVariant = "green-emeraude"
118+
headers = {["Titre", "Titre", "Titre", "Titre", "Titre"]}
119+
data = {[
120+
["Donnée", "Donnée", "Donnée", "Donnée", "Donnée"],
121+
["Donnée", "Donnée", "Donnée", "Donnée", "Donnée"],
122+
["Donnée", "Donnée", "Donnée", "Donnée", "Donnée"],
123+
["Donnée", "Donnée", "Donnée", "Donnée", "Donnée"],
124+
["Donnée", "Donnée", "Donnée", "Donnée", "Donnée"]
125+
]}
126+
/>
127+
);
128+
}

test/integration/next-appdir/app/page.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ClientComponent } from "#/ui/ClientComponent";
22
import { Alert } from "@codegouvfr/react-dsfr/Alert";
33
import { Tabs } from "@codegouvfr/react-dsfr/Tabs";
4+
import { Table } from "@codegouvfr/react-dsfr/Table";
45
import { Summary } from "@codegouvfr/react-dsfr/Summary";
56
import { Button } from "@codegouvfr/react-dsfr/Button";
67
import { createModal } from "@codegouvfr/react-dsfr/Modal";
@@ -156,7 +157,25 @@ export default function Page() {
156157
title="Titre de rubrique"
157158
burgerMenuButtonText="Dans cette rubrique"
158159
/>
160+
<TableExample />
159161
</>
160162
);
161163

162164
}
165+
166+
function TableExample() {
167+
return (
168+
<Table
169+
caption = "Titre du tableau"
170+
colorVariant = "green-emeraude"
171+
headers = {["Titre", "Titre", "Titre", "Titre", "Titre"]}
172+
data = {[
173+
["Donnée", "Donnée", "Donnée", "Donnée", "Donnée"],
174+
["Donnée", "Donnée", "Donnée", "Donnée", "Donnée"],
175+
["Donnée", "Donnée", "Donnée", "Donnée", "Donnée"],
176+
["Donnée", "Donnée", "Donnée", "Donnée", "Donnée"],
177+
["Donnée", "Donnée", "Donnée", "Donnée", "Donnée"]
178+
]}
179+
/>
180+
);
181+
}

0 commit comments

Comments
 (0)