diff --git a/src/Share.tsx b/src/Share.tsx new file mode 100644 index 000000000..98da96b8f --- /dev/null +++ b/src/Share.tsx @@ -0,0 +1,215 @@ +"use client"; + +import React, { CSSProperties, ReactNode, forwardRef, memo } from "react"; +import { assert } from "tsafe/assert"; +import type { Equals } from "tsafe"; +import { symToStr } from "tsafe/symToStr"; +import { CxArg } from "tss-react"; +import { fr } from "./fr"; +import { createComponentI18nApi } from "./i18n"; +import { getLink, RegisteredLinkProps } from "./link"; +import { cx } from "./tools/cx"; +import { useAnalyticsId } from "./tools/useAnalyticsId"; + +export type ShareProps = { + id?: string; + className?: string; + classes?: Partial< + Record<"root" | "title" | "text" | "buttons-group" | "buttons-group-item" | "button", CxArg> + >; + style?: CSSProperties; + title?: ReactNode; + text?: ReactNode; + buttons: [ShareProps.Button, ...ShareProps.Button[]]; +}; + +export namespace ShareProps { + export type Type = "facebook" | "twitter-x" | "linkedin" | "mail" | "copy"; + + export type CommonButton = { + type: Type; + children?: ReactNode; + }; + + export type LinkButton = CommonButton & { + linkProps: RegisteredLinkProps; + disabled?: boolean; + buttonProps?: never; + }; + + export type NativeButton = CommonButton & { + buttonProps: React.ButtonHTMLAttributes; + linkProps?: never; + disabled?: never; + }; + + export type Button = LinkButton | NativeButton; +} + +/** @see */ +export const Share = memo( + forwardRef((props, ref) => { + const { t } = useTranslation(); + + const { + id: id_props, + className, + classes = {}, + style, + title = t("share this page"), + text, + buttons, + ...rest + } = props; + + assert>(); + + const { Link } = getLink(); + + const id = useAnalyticsId({ + defaultIdPrefix: "fr-share", + explicitlyProvidedId: id_props + }); + + return ( +
+

{title}

+ + {text !== undefined && ( +

{text}

+ )} + +
    + {buttons.map((button, i) => { + const buttonLabel = button.children ?? t(button.type); + + return ( +
  • + {button.buttonProps !== undefined + ? (() => { + const { buttonProps } = button; + + return ( + + ); + })() + : (() => { + const { + target = button.type === "mail" + ? undefined + : "_blank", + rel = target === "_blank" + ? "noopener external" + : undefined, + title = target === "_blank" + ? `${buttonLabelToString(buttonLabel)} - ${t( + "new window" + )}` + : undefined, + ...restLinkProps + } = button.linkProps; + + return ( + { + event.preventDefault(); + ( + button.linkProps as RegisteredLinkProps & { + onClick?: ( + event: React.MouseEvent + ) => void; + } + ).onClick?.(event); + } + : ( + button.linkProps as RegisteredLinkProps & { + onClick?: ( + event: React.MouseEvent + ) => void; + } + ).onClick + } + className={cx( + fr.cx("fr-btn", `fr-btn--${button.type}`), + classes.button, + button.linkProps.className + )} + > + {buttonLabel} + + ); + })()} +
  • + ); + })} +
+
+ ); + }) +); + +Share.displayName = symToStr({ Share }); + +export default Share; + +function buttonLabelToString(label: ReactNode): string { + if (typeof label === "string") { + return label; + } + + if (typeof label === "number") { + return `${label}`; + } + + return ""; +} + +const { useTranslation, addShareTranslations } = createComponentI18nApi({ + componentName: symToStr({ Share }), + frMessages: { + "share this page": "Partager la page", + "new window": "nouvelle fenetre", + "facebook": "Partager sur Facebook", + "twitter-x": "Partager sur X (anciennement Twitter)", + "linkedin": "Partager sur LinkedIn", + "mail": "Partager par email", + "copy": "Copier dans le presse-papier" + } +}); + +addShareTranslations({ + lang: "en", + messages: { + "share this page": "Share this page", + "new window": "new window", + "facebook": "Share on Facebook", + "twitter-x": "Share on X (formerly Twitter)", + "linkedin": "Share on LinkedIn", + "mail": "Share by email", + "copy": "Copy to clipboard" + } +}); + +export { addShareTranslations }; diff --git a/src/bin/only-include-css-of-used-components.ts b/src/bin/only-include-css-of-used-components.ts index 00773b1a4..f8fbb10ce 100644 --- a/src/bin/only-include-css-of-used-components.ts +++ b/src/bin/only-include-css-of-used-components.ts @@ -166,6 +166,7 @@ export const REACT_DSFR_MODULE_TO_DSFR_COMPONENTS: Record({ + sectionName, + wrappedComponent: { Share }, + description: ` +- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/partage) +- [See DSFR demos](https://main--ds-gouv.netlify.app/example/component/share/) +- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Share.tsx) + +The component renders a DSFR share block with social/email links and optional custom actions. +`, + argTypes: { + classes: { + control: { type: null }, + description: + 'Add custom classes for inner elements. Possible keys are "root", "title", "text", "buttons-group", "buttons-group-item", "button".' + } + }, + disabledProps: ["lang"] +}); + +export default meta; + +const defaultButtons: [ShareProps.Button, ...ShareProps.Button[]] = [ + { + type: "facebook", + linkProps: { + href: "https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fwww.systeme-de-design.gouv.fr", + target: "_blank", + rel: "noopener noreferrer" + } + }, + { + type: "twitter-x", + linkProps: { + href: "https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.systeme-de-design.gouv.fr", + target: "_blank", + rel: "noopener noreferrer" + } + }, + { + type: "linkedin", + linkProps: { + href: "https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.systeme-de-design.gouv.fr", + target: "_blank", + rel: "noopener noreferrer" + } + }, + { + type: "mail", + linkProps: { + href: "mailto:?subject=DSFR&body=Decouvrez%20le%20DSFR%20https%3A%2F%2Fwww.systeme-de-design.gouv.fr" + } + }, + { + type: "copy", + buttonProps: { + type: "button", + onClick: action("copy-click") + } + } +]; + +export const Default = getStory({ + buttons: defaultButtons +}); + +export const InactiveSocialButtons = getStory({ + text: ( + <> + Veuillez autoriser le depot de cookies pour partager sur + Facebook, X et LinkedIn. + + ), + buttons: [ + { + type: "facebook", + disabled: true, + linkProps: { + href: "https://www.facebook.com/sharer/sharer.php?u=https%3A%2F%2Fwww.systeme-de-design.gouv.fr", + target: "_blank", + rel: "noopener external" + } + }, + { + type: "twitter-x", + disabled: true, + linkProps: { + href: "https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.systeme-de-design.gouv.fr", + target: "_blank", + rel: "noopener external" + } + }, + { + type: "linkedin", + disabled: true, + linkProps: { + href: "https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.systeme-de-design.gouv.fr", + target: "_blank", + rel: "noopener external" + } + }, + { + type: "mail", + linkProps: { + href: "mailto:?subject=Partage&body=Regardez%20cette%20page%20https%3A%2F%2Fwww.systeme-de-design.gouv.fr", + target: "_blank", + rel: "noopener external" + } + }, + { + type: "copy", + buttonProps: { + type: "button", + onClick: action("copy-click") + } + } + ] +}); + +export const CustomTitle = getStory({ + title: "Partager cet article", + buttons: defaultButtons +}); diff --git a/test/integration/vite/src/Home.tsx b/test/integration/vite/src/Home.tsx index a74746c06..96bc10ce4 100644 --- a/test/integration/vite/src/Home.tsx +++ b/test/integration/vite/src/Home.tsx @@ -11,6 +11,7 @@ import { useIsDark } from "@codegouvfr/react-dsfr/useIsDark"; import { useState } from "react"; import { Table } from "@codegouvfr/react-dsfr/Table"; import { Tile } from "@codegouvfr/react-dsfr/Tile"; +import { Share } from "@codegouvfr/react-dsfr/Share"; import { Accordion } from "@codegouvfr/react-dsfr/Accordion"; import { Book, Money, Police, Sun, LocationFrance } from '@codegouvfr/react-dsfr/picto'; @@ -84,6 +85,9 @@ export function Home() {
+
+ +
); } @@ -264,4 +268,54 @@ function HighlightExample() { bodyAs="p" /> ) -} \ No newline at end of file +} + +function ShareExample() { + return ( + { + navigator.clipboard + .writeText(window.location.href) + .then(() => alert("Adresse copiee dans le presse-papier.")); + } + } + } + ]} + /> + ); +}