Skip to content

Commit c560412

Browse files
authored
Merge pull request #190 from codegouvfr/feature/tooltip-component
Feature/tooltip component
2 parents 3ca65f1 + 0154782 commit c560412

File tree

3 files changed

+187
-0
lines changed

3 files changed

+187
-0
lines changed

src/Tooltip.tsx

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import React, { forwardRef, memo } from "react";
2+
import type { ReactNode, CSSProperties } from "react";
3+
import type { Equals } from "tsafe";
4+
import { assert } from "tsafe/assert";
5+
import { symToStr } from "tsafe/symToStr";
6+
import { useAnalyticsId } from "./tools/useAnalyticsId";
7+
import { createComponentI18nApi } from "./i18n";
8+
import { fr } from "./fr";
9+
import { cx } from "./tools/cx";
10+
11+
export type TooltipProps = TooltipProps.WithClickAction | TooltipProps.WithHoverAction;
12+
13+
export namespace TooltipProps {
14+
export type Common = {
15+
title: ReactNode;
16+
id?: string;
17+
className?: string;
18+
style?: CSSProperties;
19+
};
20+
21+
export type WithClickAction = Common & {
22+
kind: "click";
23+
children?: undefined;
24+
};
25+
26+
export type WithHoverAction = Common & {
27+
kind?: "hover";
28+
children?: ReactNode;
29+
};
30+
}
31+
32+
/** @see <https://components.react-dsfr.codegouv.studio/?path=/docs/components-tooltip> */
33+
export const Tooltip = memo(
34+
forwardRef<HTMLSpanElement, TooltipProps>((props, ref) => {
35+
const { id: id_prop, className, title, kind, style, children, ...rest } = props;
36+
assert<Equals<keyof typeof rest, never>>();
37+
38+
const { t } = useTranslation();
39+
40+
const id = useAnalyticsId({
41+
"defaultIdPrefix": "fr-tooltip",
42+
"explicitlyProvidedId": id_prop
43+
});
44+
45+
const TooltipSpan = () => (
46+
<span
47+
className={cx(fr.cx("fr-tooltip", "fr-placement"), className)}
48+
id={id}
49+
ref={ref}
50+
style={style}
51+
role="tooltip"
52+
aria-hidden="true"
53+
>
54+
{title}
55+
</span>
56+
);
57+
58+
return (
59+
<>
60+
{kind === "click" ? (
61+
<button
62+
className={fr.cx("fr-btn--tooltip", "fr-btn")}
63+
aria-describedby={id}
64+
id={`tooltip-owner-${id}`}
65+
>
66+
{t("tooltip-button-text")}
67+
</button>
68+
) : typeof children === "undefined" ? (
69+
// mimic default tooltip style
70+
<i
71+
className={fr.cx("fr-icon--sm", "fr-icon-question-line")}
72+
style={{ color: fr.colors.decisions.text.actionHigh.blueFrance.default }}
73+
aria-describedby={id}
74+
id={`tooltip-owner-${id}`}
75+
></i>
76+
) : (
77+
<span aria-describedby={id} id={`tooltip-owner-${id}`}>
78+
{children}
79+
</span>
80+
)}
81+
<TooltipSpan />
82+
</>
83+
);
84+
})
85+
);
86+
87+
Tooltip.displayName = symToStr({ Tooltip });
88+
89+
const { useTranslation, addTooltipTranslations } = createComponentI18nApi({
90+
"componentName": symToStr({ Tooltip }),
91+
"frMessages": {
92+
"tooltip-button-text": "Information contextuelle"
93+
}
94+
});
95+
96+
addTooltipTranslations({
97+
"lang": "en",
98+
"messages": {
99+
"tooltip-button-text": "Contextual information"
100+
}
101+
});
102+
103+
export { addTooltipTranslations };
104+
105+
export default Tooltip;

stories/Tooltip.stories.tsx

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from "react";
2+
import { assert, Equals } from "tsafe/assert";
3+
4+
import { Tooltip, type TooltipProps } from "../src/Tooltip";
5+
6+
import { sectionName } from "./sectionName";
7+
import { getStoryFactory } from "./getStory";
8+
9+
const { meta, getStory } = getStoryFactory({
10+
sectionName,
11+
"wrappedComponent": { Tooltip },
12+
"description": `
13+
- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/infobulle)
14+
- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Tooltip.tsx)`,
15+
"argTypes": {
16+
"id": {
17+
"control": { "type": "text" },
18+
"description":
19+
"Optional: tootlip Id, which is also use as aria-describedby for hovered/clicked element"
20+
},
21+
"className": {
22+
"control": { "type": "text" },
23+
"description": "Optional"
24+
},
25+
"kind": {
26+
"control": { "type": "select" },
27+
"options": (() => {
28+
const options = ["hover", "click"] as const;
29+
30+
assert<Equals<typeof options[number] | undefined, TooltipProps["kind"]>>();
31+
32+
return options;
33+
})(),
34+
"description": "Optional."
35+
},
36+
"title": {
37+
"control": { "type": "text" }
38+
},
39+
"children": {
40+
"control": { "type": "text" }
41+
}
42+
}
43+
});
44+
45+
export default meta;
46+
47+
const defaultOnHoverProps: TooltipProps.WithHoverAction = {
48+
"kind": "hover",
49+
"title": "lorem ipsum"
50+
};
51+
52+
export const Default = getStory(defaultOnHoverProps);
53+
54+
export const TooltipOnHover = getStory(defaultOnHoverProps);
55+
56+
export const TooltipOnHoverWithChild = getStory({
57+
...defaultOnHoverProps,
58+
children: <a href="#">Some link</a>
59+
});
60+
61+
export const TooltipOnClick = getStory({
62+
"kind": "click",
63+
"title": "lorem ipsum"
64+
});

test/types/Tooltip.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Tooltip } from "../../src/Tooltip";
2+
3+
{
4+
<Tooltip description="lorem ipsum">Exemple</Tooltip>;
5+
}
6+
{
7+
<Tooltip kind="hover" description="lorem ipsum">
8+
Exemple
9+
</Tooltip>;
10+
}
11+
{
12+
<Tooltip kind="click" description="lorem ipsum" />;
13+
}
14+
{
15+
<Tooltip kind="click" description="lorem ipsum">
16+
Exemple
17+
</Tooltip>;
18+
}

0 commit comments

Comments
 (0)