From 73ae1d556d7701193b5bbd261e435acef5b5fe2a Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Fri, 16 Dec 2022 09:50:31 +0100 Subject: [PATCH 01/10] feat: add Card component --- package.json | 1 + src/Card.tsx | 176 ++++++++++++++++++++++++++ stories/Card.stories.tsx | 258 +++++++++++++++++++++++++++++++++++++++ stories/getStory.tsx | 16 ++- 4 files changed, 447 insertions(+), 4 deletions(-) create mode 100644 src/Card.tsx create mode 100644 stories/Card.stories.tsx diff --git a/package.json b/package.json index fc6d7b3c2..156b0332f 100755 --- a/package.json +++ b/package.json @@ -131,6 +131,7 @@ "./Header": "./dist/Header/index.js", "./Footer": "./dist/Footer.js", "./Display": "./dist/Display.js", + "./Card": "./dist/Card.js", "./Button": "./dist/Button.js", "./Breadcrumb": "./dist/Breadcrumb.js", "./Badge": "./dist/Badge.js", diff --git a/src/Card.tsx b/src/Card.tsx new file mode 100644 index 000000000..dd7d14117 --- /dev/null +++ b/src/Card.tsx @@ -0,0 +1,176 @@ +import React, { memo, forwardRef } from "react"; +import type { ReactNode } from "react"; +import { symToStr } from "tsafe/symToStr"; +import { assert } from "tsafe/assert"; +import type { Equals } from "tsafe"; + +import { FrIconClassName, RiIconClassName } from "./lib/generatedFromCss/classNames"; +import { fr, RegisteredLinkProps } from "./lib"; +import { useLink } from "./lib/routing"; +import { cx } from "./lib/tools/cx"; + +import "./dsfr/component/card/card.css"; + +//https://main--ds-gouv.netlify.app/example/component/card/ +export type CardProps = { + className?: string; + title: ReactNode; + linkProps: RegisteredLinkProps; + desc?: ReactNode; + imageUrl?: string; + imageAlt?: string; + start?: ReactNode; + detail?: ReactNode; + end?: ReactNode; + endDetail?: ReactNode; + badges?: ReactNode[]; // todo: restrict to badge component ? these badges are display on the image + footer?: ReactNode; // where actions can be placed + horizontal?: boolean; + size?: "small" | "medium" | "large"; // only affect the text + enlargeLink?: boolean; // make the whole card clickable + iconId?: FrIconClassName | RiIconClassName; // only needed when enlargeMode=true + shadow?: boolean; + background?: boolean; + border?: boolean; + grey?: boolean; + classes?: Partial< + Record< + | "root" + | "title" + | "card" + | "link" + | "body" + | "content" + | "desc" + | "header" + | "img" + | "imgTag" + | "start" + | "detail" + | "end" + | "endDetail" + | "badges" + | "footer", + string + > + >; +} & (CardProps.Default | CardProps.Horizontal); + +export namespace CardProps { + export type Default = {}; + + export type Horizontal = { + horizontal: true; + }; +} + +/** @see */ +export const Card = memo( + forwardRef((props, ref) => { + const { + className, + title, + linkProps, + desc, + imageUrl, + imageAlt, + start, + detail, + end, + endDetail, + badges, + footer, + horizontal = false, + size = "medium", + classes = {}, + enlargeLink = true, + background = true, + border = true, + shadow = false, + grey = false, + iconId, + ...rest + } = props; + + assert>(); + + const { Link } = useLink(); + + return ( +
+
+
+

+ + {title} + +

+ {desc !== undefined && ( +

{desc}

+ )} +
+ {start} + {detail !== undefined && ( +

+ {detail} +

+ )} +
+
+ {end} + {endDetail !== undefined && ( +

+ {endDetail} +

+ )} +
+
+ {footer !== undefined && ( +

{footer}

+ )} +
+ {/* ensure we dont have an empty imageUrl string */} + {(imageUrl !== undefined && imageUrl.length && ( +
+
+ {imageAlt} +
+ {(badges !== undefined && badges.length && ( +
    + {badges.map((badge, i) => ( +
  • {badge}
  • + ))} +
+ )) || + null} +
+ )) || + null} +
+ ); + }) +); + +Card.displayName = symToStr({ Card }); + +export default Card; diff --git a/stories/Card.stories.tsx b/stories/Card.stories.tsx new file mode 100644 index 000000000..d22d90e89 --- /dev/null +++ b/stories/Card.stories.tsx @@ -0,0 +1,258 @@ +import React from "react"; +import { Card, CardProps } from "../dist/Card"; +import { Badge } from "../dist/Badge"; +import { sectionName } from "./sectionName"; +import { getStoryFactory } from "./getStory"; +import { assert } from "tsafe/assert"; +import type { Equals } from "tsafe"; + +import "../dsfr/utility/icons/icons-system/icons-system.css"; +import { fr } from "../dist/lib"; + +const { meta, getStory } = getStoryFactory({ + sectionName, + defaultContainerWidth: 360, + "wrappedComponent": { Card }, + "description": ` +- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/carte) +- [See DSFR demos](https://main--ds-gouv.netlify.app/example/component/card/) +- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Card.tsx)`, + "argTypes": { + "title": { "description": `Required.` }, + "desc": { "description": `` }, + linkProps: { + "description": `Required. the Card Link props` + }, + enlargeLink: { + "description": `default: true. Set to false to restrict the link area to the Card title only.`, + "control": { "type": "boolean" } + }, + size: { + "description": "Card title text sizing", + "options": (() => { + const sizes = ["small", "medium", "large"] as const; + + assert>(); + + return sizes; + })(), + "control": { "type": "radio" } + }, + imageUrl: { + "description": "Use any image URL, or none" + } + }, + "disabledProps": ["lang"] +}); + +export default meta; + +const defaultProps = { + enlargeLink: true, + title: "Intitulé de la carte (sur lequel se trouve le lien)", + linkProps: { + href: "#" + }, + desc: "Lorem ipsum dolor sit amet, consectetur adipiscing, incididunt, ut labore et dolore magna aliqua. Vitae sapien pellentesque habitant morbi tristique senectus et", + imageUrl: "https://www.systeme-de-design.gouv.fr/img/placeholder.16x9.png", + imageAlt: "texte alternatif de l’image" +}; + +export const Default = getStory({ ...defaultProps }); + +// todo: wrap with grid +export const CardWithoutEnlargeLink = getStory( + { ...defaultProps, enlargeLink: false }, + { description: "Carte sans lien étendu à la carte" } +); + +export const CardWithIcon = getStory( + { ...defaultProps, iconId: "fr-icon-warning-fill" }, + { description: "Carte avec icône personnalisée" } +); + +export const CardWithoutBorder = getStory( + { ...defaultProps, border: false }, + { description: "Carte sans bordure" } +); + +export const CardWithShadow = getStory( + { ...defaultProps, shadow: true }, + { description: "Carte avec ombre portée" } +); + +export const CardWithoutImage = getStory( + { ...defaultProps, imageUrl: undefined }, + { description: "Carte sans image" } +); + +export const CardWithImageRatio = getStory( + { ...defaultProps, classes: { imgTag: "fr-ratio-3x4" } }, + { description: "Carte verticale avec image au ratio d'aspect 3x4" } +); + +export const CardWithBadgeInTheHeader = getStory( + { + ...defaultProps, + badges: [, ] + }, + { description: "Carte verticale avec badge dans l'image" } +); + +export const CardWithBadgeInTheContent = getStory( + { + ...defaultProps, + detail: ( +
    + + +
+ ) + }, + { description: "Carte verticale avec badges dans le contenu" } +); + +export const CardWithDetail = getStory( + { + ...defaultProps, + detail: "détail(optionnel)" + }, + { description: "Carte verticale avec détail" } +); + +export const CardWithEndDetail = getStory( + { + ...defaultProps, + endDetail: "détail(optionnel)" + }, + { description: "Carte verticale avec détail en bas" } +); + +export const CardWithActionLinks = getStory( + { + ...defaultProps, + enlargeLink: false, + footer: ( + + ) + }, + { description: "Carte verticale avec liens d'action" } +); + +export const CardWithActionButtons = getStory( + { + ...defaultProps, + enlargeLink: false, + footer: ( +
    +
  • + +
  • +
  • + +
  • +
+ ) + }, + { description: "Carte verticale avec buttons d'action" } +); + +export const CardHorizontale = getStory( + { + ...defaultProps, + horizontal: true + }, + { description: "Carte horizontale", containerWidth: 700 } +); + +export const CardHorizontaleSM = getStory( + { + ...defaultProps, + horizontal: true, + size: "small" + }, + { description: "Carte horizontale", containerWidth: 500 } +); + +export const CardHorizontaleLG = getStory( + { + ...defaultProps, + horizontal: true, + size: "large" + }, + { description: "Carte horizontale", containerWidth: 900 } +); + +export const CardHorizontaleWithoutImage = getStory( + { + ...defaultProps, + horizontal: true, + size: "large", + imageUrl: undefined, + detail: ( +
    + + +
+ ) + }, + { description: "Carte horizontale sans image", containerWidth: 900 } +); + +export const CardHorizontaleWithoutImageAndEnlargeLink = getStory( + { + ...defaultProps, + horizontal: true, + enlargeLink: false, + size: "large", + imageUrl: undefined + }, + { description: "Carte horizontale sans image", containerWidth: 900 } +); + +export const CardHorizontaleWithActions = getStory( + { + ...defaultProps, + enlargeLink: false, + horizontal: true, + size: "large", + badges: [], + detail: ( +
    + + +
+ ), + footer: ( +
    +
  • + +
  • +
  • + +
  • +
+ ) + }, + { description: "Carte horizontale", containerWidth: 900 } +); + +export const CardGrey = getStory( + { + ...defaultProps, + horizontal: true, + grey: false + }, + { description: "Carte horizontale grey", containerWidth: 900 } +); diff --git a/stories/getStory.tsx b/stories/getStory.tsx index 318c16449..024f6aa29 100644 --- a/stories/getStory.tsx +++ b/stories/getStory.tsx @@ -90,14 +90,17 @@ export function getStoryFactory>(params: { let isFirstStory = true; - function getStory(props: Props, params?: { description?: string }): typeof Template { - const { description } = params ?? {}; + function getStory( + props: Props, + params?: { containerWidth?: number; description?: string } + ): typeof Template { + const { containerWidth, description } = params ?? {}; const out = Template.bind({}); out.args = { "darkMode": window.matchMedia("(prefers-color-scheme: dark)").matches, - "containerWidth": defaultContainerWidth ?? 0, + "containerWidth": containerWidth ?? defaultContainerWidth ?? 0, "lang": "fr", isFirstStory, ...props @@ -107,8 +110,13 @@ export function getStoryFactory>(params: { out.parameters = { "docs": { + sectionName: "plop", + componentName: "koko", + "description": { - "story": description + "story": description, + sectionName: "plop", + componentName: "koko2" } } }; From 7945e6377df97379a4ee6992dd7281d2c01c2dc3 Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Fri, 16 Dec 2022 09:52:02 +0100 Subject: [PATCH 02/10] fix --- stories/getStory.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/stories/getStory.tsx b/stories/getStory.tsx index 024f6aa29..43811cb28 100644 --- a/stories/getStory.tsx +++ b/stories/getStory.tsx @@ -110,13 +110,8 @@ export function getStoryFactory>(params: { out.parameters = { "docs": { - sectionName: "plop", - componentName: "koko", - "description": { - "story": description, - sectionName: "plop", - componentName: "koko2" + "story": description } } }; From 4f28dbfc3569386dfa6ceb7608064424a19fe45e Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Wed, 21 Dec 2022 12:11:34 +0100 Subject: [PATCH 03/10] fix: use getLink Signed-off-by: Julien Bouquillon --- src/Card.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Card.tsx b/src/Card.tsx index dd7d14117..d66a05857 100644 --- a/src/Card.tsx +++ b/src/Card.tsx @@ -6,7 +6,7 @@ import type { Equals } from "tsafe"; import { FrIconClassName, RiIconClassName } from "./lib/generatedFromCss/classNames"; import { fr, RegisteredLinkProps } from "./lib"; -import { useLink } from "./lib/routing"; +import { getLink } from "./lib/routing"; import { cx } from "./lib/tools/cx"; import "./dsfr/component/card/card.css"; @@ -94,7 +94,7 @@ export const Card = memo( assert>(); - const { Link } = useLink(); + const { Link } = getLink(); return (
Date: Wed, 21 Dec 2022 12:57:38 +0100 Subject: [PATCH 04/10] Update src/Card.tsx Co-authored-by: Joseph Garrone Signed-off-by: Julien Bouquillon --- src/Card.tsx | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Card.tsx b/src/Card.tsx index d66a05857..425196fb8 100644 --- a/src/Card.tsx +++ b/src/Card.tsx @@ -24,11 +24,14 @@ export type CardProps = { end?: ReactNode; endDetail?: ReactNode; badges?: ReactNode[]; // todo: restrict to badge component ? these badges are display on the image - footer?: ReactNode; // where actions can be placed - horizontal?: boolean; - size?: "small" | "medium" | "large"; // only affect the text - enlargeLink?: boolean; // make the whole card clickable - iconId?: FrIconClassName | RiIconClassName; // only needed when enlargeMode=true + /** where actions can be placed */ + footer?: ReactNode; + /** only affect the text */ + size?: "small" | "medium" | "large"; + /** make the whole card clickable */ + enlargeLink?: boolean; + /** only needed when enlargeMode=true */ + iconId?: FrIconClassName | RiIconClassName; shadow?: boolean; background?: boolean; border?: boolean; @@ -57,7 +60,9 @@ export type CardProps = { } & (CardProps.Default | CardProps.Horizontal); export namespace CardProps { - export type Default = {}; + export type Default = { + horizontal?: never; + }; export type Horizontal = { horizontal: true; From 82c6a4c310847c90b6335bd52eb7c269c136086b Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Wed, 21 Dec 2022 12:58:16 +0100 Subject: [PATCH 05/10] Update stories/getStory.tsx Co-authored-by: Joseph Garrone Signed-off-by: Julien Bouquillon --- stories/getStory.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stories/getStory.tsx b/stories/getStory.tsx index caf7ba887..3c6c52b48 100644 --- a/stories/getStory.tsx +++ b/stories/getStory.tsx @@ -102,7 +102,7 @@ export function getStoryFactory>(params: { out.args = { "darkMode": window.matchMedia("(prefers-color-scheme: dark)").matches, - "containerWidth": containerWidth ?? defaultContainerWidth ?? 0, + "containerWidth": defaultContainerWidthStoryLevel ?? defaultContainerWidth ?? 0, "lang": "fr", isFirstStory, ...props From 0dd260199cfbc8f27db6995ca1f7a4696560f433 Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Wed, 21 Dec 2022 12:58:25 +0100 Subject: [PATCH 06/10] Update stories/getStory.tsx Co-authored-by: Joseph Garrone Signed-off-by: Julien Bouquillon --- stories/getStory.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stories/getStory.tsx b/stories/getStory.tsx index 3c6c52b48..2ec6744c1 100644 --- a/stories/getStory.tsx +++ b/stories/getStory.tsx @@ -94,9 +94,9 @@ export function getStoryFactory>(params: { function getStory( props: Props, - params?: { containerWidth?: number; description?: string } + params?: { defaultContainerWidth?: number; description?: string } ): typeof Template { - const { containerWidth, description } = params ?? {}; + const { defaultContainerWidth: defaultContainerWidthStoryLevel, description } = params ?? {}; const out = Template.bind({}); From 1f31fb15747fb8b6f53f0693b4cfe42c0ebf01b1 Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Wed, 21 Dec 2022 12:58:47 +0100 Subject: [PATCH 07/10] fix --- src/Card.tsx | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/Card.tsx b/src/Card.tsx index 425196fb8..3dd50048b 100644 --- a/src/Card.tsx +++ b/src/Card.tsx @@ -105,14 +105,14 @@ export const Card = memo(

- + {title}

@@ -151,7 +151,7 @@ export const Card = memo( )}
{/* ensure we dont have an empty imageUrl string */} - {(imageUrl !== undefined && imageUrl.length && ( + {imageUrl !== undefined && imageUrl.length && (
{imageAlt}
- {(badges !== undefined && badges.length && ( + {badges !== undefined && badges.length && (
    {badges.map((badge, i) => (
  • {badge}
  • ))}
- )) || - null} + )}
- )) || - null} + )}
); }) From f5c5cb699b7e3314804a6b661e48010f2adef636 Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Wed, 21 Dec 2022 13:00:37 +0100 Subject: [PATCH 08/10] fix --- src/Card.tsx | 12 ++++++------ stories/Card.stories.tsx | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Card.tsx b/src/Card.tsx index 3dd50048b..496059beb 100644 --- a/src/Card.tsx +++ b/src/Card.tsx @@ -25,13 +25,13 @@ export type CardProps = { endDetail?: ReactNode; badges?: ReactNode[]; // todo: restrict to badge component ? these badges are display on the image /** where actions can be placed */ - footer?: ReactNode; + footer?: ReactNode; /** only affect the text */ - size?: "small" | "medium" | "large"; + size?: "small" | "medium" | "large"; /** make the whole card clickable */ - enlargeLink?: boolean; - /** only needed when enlargeMode=true */ - iconId?: FrIconClassName | RiIconClassName; + enlargeLink?: boolean; + /** only needed when enlargeLink=true */ + iconId?: FrIconClassName | RiIconClassName; shadow?: boolean; background?: boolean; border?: boolean; @@ -61,7 +61,7 @@ export type CardProps = { export namespace CardProps { export type Default = { - horizontal?: never; + horizontal?: never; }; export type Horizontal = { diff --git a/stories/Card.stories.tsx b/stories/Card.stories.tsx index d22d90e89..746f30bc4 100644 --- a/stories/Card.stories.tsx +++ b/stories/Card.stories.tsx @@ -173,7 +173,7 @@ export const CardHorizontale = getStory( ...defaultProps, horizontal: true }, - { description: "Carte horizontale", containerWidth: 700 } + { description: "Carte horizontale", defaultContainerWidth: 700 } ); export const CardHorizontaleSM = getStory( @@ -182,7 +182,7 @@ export const CardHorizontaleSM = getStory( horizontal: true, size: "small" }, - { description: "Carte horizontale", containerWidth: 500 } + { description: "Carte horizontale", defaultContainerWidth: 500 } ); export const CardHorizontaleLG = getStory( @@ -191,7 +191,7 @@ export const CardHorizontaleLG = getStory( horizontal: true, size: "large" }, - { description: "Carte horizontale", containerWidth: 900 } + { description: "Carte horizontale", defaultContainerWidth: 900 } ); export const CardHorizontaleWithoutImage = getStory( @@ -207,7 +207,7 @@ export const CardHorizontaleWithoutImage = getStory( ) }, - { description: "Carte horizontale sans image", containerWidth: 900 } + { description: "Carte horizontale sans image", defaultContainerWidth: 900 } ); export const CardHorizontaleWithoutImageAndEnlargeLink = getStory( @@ -218,7 +218,7 @@ export const CardHorizontaleWithoutImageAndEnlargeLink = getStory( size: "large", imageUrl: undefined }, - { description: "Carte horizontale sans image", containerWidth: 900 } + { description: "Carte horizontale sans image", defaultContainerWidth: 900 } ); export const CardHorizontaleWithActions = getStory( @@ -245,7 +245,7 @@ export const CardHorizontaleWithActions = getStory( ) }, - { description: "Carte horizontale", containerWidth: 900 } + { description: "Carte horizontale", defaultContainerWidth: 900 } ); export const CardGrey = getStory( @@ -254,5 +254,5 @@ export const CardGrey = getStory( horizontal: true, grey: false }, - { description: "Carte horizontale grey", containerWidth: 900 } + { description: "Carte horizontale grey", defaultContainerWidth: 900 } ); From 860aa892c874d979f2dfaf92d46ccb88259bb383 Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Wed, 21 Dec 2022 13:07:13 +0100 Subject: [PATCH 09/10] lint --- stories/getStory.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/stories/getStory.tsx b/stories/getStory.tsx index 2ec6744c1..8be444f68 100644 --- a/stories/getStory.tsx +++ b/stories/getStory.tsx @@ -96,7 +96,8 @@ export function getStoryFactory>(params: { props: Props, params?: { defaultContainerWidth?: number; description?: string } ): typeof Template { - const { defaultContainerWidth: defaultContainerWidthStoryLevel, description } = params ?? {}; + const { defaultContainerWidth: defaultContainerWidthStoryLevel, description } = + params ?? {}; const out = Template.bind({}); From b328f4b77fdbf3f184f3e464cccf7451268e1503 Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Wed, 21 Dec 2022 13:09:02 +0100 Subject: [PATCH 10/10] Update COMPONENTS.md Signed-off-by: Julien Bouquillon --- COMPONENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/COMPONENTS.md b/COMPONENTS.md index accd798fd..c48c557b6 100644 --- a/COMPONENTS.md +++ b/COMPONENTS.md @@ -11,7 +11,7 @@ - [ ] Radio button - [ ] Radio rich - [ ] Checkbox -- [ ] Cards +- [x] Cards - [x] Quote - [ ] Media - [x] Header