Skip to content

Commit

Permalink
Merge pull request #190 from codegouvfr/feature/tooltip-component
Browse files Browse the repository at this point in the history
Feature/tooltip component
  • Loading branch information
garronej authored Oct 16, 2024
2 parents 3ca65f1 + 0154782 commit c560412
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 0 deletions.
105 changes: 105 additions & 0 deletions src/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import React, { forwardRef, memo } from "react";
import type { ReactNode, CSSProperties } from "react";
import type { Equals } from "tsafe";
import { assert } from "tsafe/assert";
import { symToStr } from "tsafe/symToStr";
import { useAnalyticsId } from "./tools/useAnalyticsId";
import { createComponentI18nApi } from "./i18n";
import { fr } from "./fr";
import { cx } from "./tools/cx";

export type TooltipProps = TooltipProps.WithClickAction | TooltipProps.WithHoverAction;

export namespace TooltipProps {
export type Common = {
title: ReactNode;
id?: string;
className?: string;
style?: CSSProperties;
};

export type WithClickAction = Common & {
kind: "click";
children?: undefined;
};

export type WithHoverAction = Common & {
kind?: "hover";
children?: ReactNode;
};
}

/** @see <https://components.react-dsfr.codegouv.studio/?path=/docs/components-tooltip> */
export const Tooltip = memo(
forwardRef<HTMLSpanElement, TooltipProps>((props, ref) => {
const { id: id_prop, className, title, kind, style, children, ...rest } = props;
assert<Equals<keyof typeof rest, never>>();

const { t } = useTranslation();

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

const TooltipSpan = () => (
<span
className={cx(fr.cx("fr-tooltip", "fr-placement"), className)}
id={id}
ref={ref}
style={style}
role="tooltip"
aria-hidden="true"
>
{title}
</span>
);

return (
<>
{kind === "click" ? (
<button
className={fr.cx("fr-btn--tooltip", "fr-btn")}
aria-describedby={id}
id={`tooltip-owner-${id}`}
>
{t("tooltip-button-text")}
</button>
) : typeof children === "undefined" ? (
// mimic default tooltip style
<i
className={fr.cx("fr-icon--sm", "fr-icon-question-line")}
style={{ color: fr.colors.decisions.text.actionHigh.blueFrance.default }}
aria-describedby={id}
id={`tooltip-owner-${id}`}
></i>
) : (
<span aria-describedby={id} id={`tooltip-owner-${id}`}>
{children}
</span>
)}
<TooltipSpan />
</>
);
})
);

Tooltip.displayName = symToStr({ Tooltip });

const { useTranslation, addTooltipTranslations } = createComponentI18nApi({
"componentName": symToStr({ Tooltip }),
"frMessages": {
"tooltip-button-text": "Information contextuelle"
}
});

addTooltipTranslations({
"lang": "en",
"messages": {
"tooltip-button-text": "Contextual information"
}
});

export { addTooltipTranslations };

export default Tooltip;
64 changes: 64 additions & 0 deletions stories/Tooltip.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from "react";
import { assert, Equals } from "tsafe/assert";

import { Tooltip, type TooltipProps } from "../src/Tooltip";

import { sectionName } from "./sectionName";
import { getStoryFactory } from "./getStory";

const { meta, getStory } = getStoryFactory({
sectionName,
"wrappedComponent": { Tooltip },
"description": `
- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/infobulle)
- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Tooltip.tsx)`,
"argTypes": {
"id": {
"control": { "type": "text" },
"description":
"Optional: tootlip Id, which is also use as aria-describedby for hovered/clicked element"
},
"className": {
"control": { "type": "text" },
"description": "Optional"
},
"kind": {
"control": { "type": "select" },
"options": (() => {
const options = ["hover", "click"] as const;

assert<Equals<typeof options[number] | undefined, TooltipProps["kind"]>>();

return options;
})(),
"description": "Optional."
},
"title": {
"control": { "type": "text" }
},
"children": {
"control": { "type": "text" }
}
}
});

export default meta;

const defaultOnHoverProps: TooltipProps.WithHoverAction = {
"kind": "hover",
"title": "lorem ipsum"
};

export const Default = getStory(defaultOnHoverProps);

export const TooltipOnHover = getStory(defaultOnHoverProps);

export const TooltipOnHoverWithChild = getStory({
...defaultOnHoverProps,
children: <a href="#">Some link</a>
});

export const TooltipOnClick = getStory({
"kind": "click",
"title": "lorem ipsum"
});
18 changes: 18 additions & 0 deletions test/types/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Tooltip } from "../../src/Tooltip";

{
<Tooltip description="lorem ipsum">Exemple</Tooltip>;
}
{
<Tooltip kind="hover" description="lorem ipsum">
Exemple
</Tooltip>;
}
{
<Tooltip kind="click" description="lorem ipsum" />;
}
{
<Tooltip kind="click" description="lorem ipsum">
Exemple
</Tooltip>;
}

0 comments on commit c560412

Please sign in to comment.