diff --git a/package.json b/package.json index bdc41e235..bc5048d99 100755 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "./Header": "./dist/Header/index.js", "./Footer": "./dist/Footer.js", "./Display": "./dist/Display.js", + "./ButtonsGroup": "./dist/ButtonsGroup.js", "./Button": "./dist/Button.js", "./Breadcrumb": "./dist/Breadcrumb.js", "./Badge": "./dist/Badge.js", diff --git a/src/ButtonsGroup.tsx b/src/ButtonsGroup.tsx new file mode 100644 index 000000000..053956eab --- /dev/null +++ b/src/ButtonsGroup.tsx @@ -0,0 +1,74 @@ +import React, { memo, forwardRef } from "react"; +import { ButtonProps } from "./Button"; +import { symToStr } from "tsafe/symToStr"; +import { assert } from "tsafe/assert"; +import type { Equals } from "tsafe"; +import { fr } from "./lib"; +import { cx } from "./lib/tools/cx"; + +// We make users import dsfr.css, so we don't need to import the scoped CSS +// but in the future if we have a complete component coverage it +// we could stop requiring users to import the hole CSS and only import on a +// per component basis. +import "./dsfr/component/button/button.css"; + +export type ButtonsGroupCommonProps = { + className?: string; + classes?: Partial>; + children: [ + // this component (ul) should have at least 2 children (RGAA) + React.ReactElement, + React.ReactElement, + ...React.ReactElement[] + ]; + size?: ButtonsGroupCommonProps.Size; +}; + +export namespace ButtonsGroupCommonProps { + export type Size = "sm" | "lg"; + export type Mode = "inline" | "inline-sm" | "inline-md" | "inline-lg"; + export type Align = "center" | "right" | "inline-reverse" | "equisized"; +} + +type ButtonAlignProps = // align will take effect only on inline placements + + | { + mode?: false; + align?: never; + } + | { + mode: ButtonsGroupCommonProps.Mode; + align?: ButtonsGroupCommonProps.Align; + }; + +export type ButtonsGroupProps = ButtonsGroupCommonProps & ButtonAlignProps; + +/** @see */ +export const ButtonsGroup = memo( + forwardRef((props, ref) => { + const { className, classes, mode, children, size, align, ...rest } = props; + + assert>(); + + const buttonsGroupClassName = cx( + fr.cx("fr-btns-group"), + mode && fr.cx(`fr-btns-group--${mode}`), + align && fr.cx(`fr-btns-group--${align}`), + size && fr.cx(`fr-btns-group--${size}`), + className, + classes?.root + ); + + return ( +
    + {children.map(child => ( +
  • {child}
  • + ))} +
+ ); + }) +); + +ButtonsGroup.displayName = symToStr({ ButtonsGroup }); + +export default ButtonsGroup; diff --git a/stories/ButtonsGroup.stories.tsx b/stories/ButtonsGroup.stories.tsx new file mode 100644 index 000000000..1531618b1 --- /dev/null +++ b/stories/ButtonsGroup.stories.tsx @@ -0,0 +1,91 @@ +import React from "react"; +import { ButtonsGroup } from "../dist/ButtonsGroup"; +import { Button } from "../dist/Button"; +import type { ButtonsGroupProps, ButtonsGroupCommonProps } from "../dist/ButtonsGroup"; +import { sectionName } from "./sectionName"; +import { getStoryFactory } from "./getStory"; +import { assert } from "tsafe/assert"; +import type { Equals } from "tsafe"; + +const { meta, getStory } = getStoryFactory({ + sectionName, + wrappedComponent: { ButtonsGroup }, + description: ` +- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/badge) +- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Badge.tsx)`, + argTypes: { + size: { + options: (() => { + const sizes = ["sm", "lg"] as const; + + assert>(); + + return [null, ...sizes]; + })(), + control: { type: "select", labels: { null: "no size (md)" } } + }, + mode: { + options: (() => { + const modes = ["inline", "inline-sm", "inline-md", "inline-lg"] as const; + + assert>(); + + return [null, ...modes]; + })(), + control: { type: "select", labels: { null: "no mode (vertical)" } } + }, + align: { + options: (() => { + const aligns = ["center", "right", "inline-reverse", "equisized"] as const; + + assert>(); + + return [null, ...aligns]; + })(), + control: { type: "select", labels: { null: "no align (default to left)" } } + } + }, + disabledProps: ["lang"] +}); + +export default meta; + +export const Default = getStory({ + children: [ +