Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 215 additions & 0 deletions src/Share.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLButtonElement>;
linkProps?: never;
disabled?: never;
};

export type Button = LinkButton | NativeButton;
}

/** @see <https://components.react-dsfr.codegouv.studio/?path=/docs/components-share> */
export const Share = memo(
forwardRef<HTMLDivElement, ShareProps>((props, ref) => {
const { t } = useTranslation();

const {
id: id_props,
className,
classes = {},
style,
title = t("share this page"),
text,
buttons,
...rest
} = props;

assert<Equals<keyof typeof rest, never>>();

const { Link } = getLink();

const id = useAnalyticsId({
defaultIdPrefix: "fr-share",
explicitlyProvidedId: id_props
});

return (
<div
id={id}
ref={ref}
className={cx(fr.cx("fr-share"), classes.root, className)}
style={style}
>
<p className={cx(fr.cx("fr-share__title"), classes.title)}>{title}</p>

{text !== undefined && (
<p className={cx(fr.cx("fr-share__text"), classes.text)}>{text}</p>
)}

<ul className={cx(fr.cx("fr-btns-group"), classes["buttons-group"])}>
{buttons.map((button, i) => {
const buttonLabel = button.children ?? t(button.type);

return (
<li key={i} className={cx(classes["buttons-group-item"])}>
{button.buttonProps !== undefined
? (() => {
const { buttonProps } = button;

return (
<button
{...buttonProps}
className={cx(
fr.cx("fr-btn", `fr-btn--${button.type}`),
classes.button,
buttonProps.className
)}
>
{buttonLabel}
</button>
);
})()
: (() => {
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 (
<Link
{...restLinkProps}
target={target}
rel={rel}
title={title}
role={button.disabled ? "link" : undefined}
aria-disabled={button.disabled || undefined}
onClick={
button.disabled
? event => {
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}
</Link>
);
})()}
</li>
);
})}
</ul>
</div>
);
})
);

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 };
1 change: 1 addition & 0 deletions src/bin/only-include-css-of-used-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export const REACT_DSFR_MODULE_TO_DSFR_COMPONENTS: Record<string, DsfrComponentN
"SegmentedControl": ["segmented", "form"],
"Select": ["select", "form"],
"SelectNext": ["select", "form"],
"Share": ["share", "link"],
"SideMenu": ["sidemenu", "link"],
"SkipLinks": ["skiplink", "link"],
"Stepper": ["stepper"],
Expand Down
129 changes: 129 additions & 0 deletions stories/Share.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import React from "react";
import { action } from "@storybook/addon-actions";
import { Share, type ShareProps } from "../dist/Share";
import { getStoryFactory } from "./getStory";
import { sectionName } from "./sectionName";

const { meta, getStory } = getStoryFactory<ShareProps>({
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 <a href="#consent">autoriser le depot de cookies</a> 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
});
Loading