From 63715ef5c7b89fdc7c428bd3b18212698560ae7b Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Mon, 2 Jan 2023 15:03:55 +0100 Subject: [PATCH 1/3] feat: add ToggleSwitch --- package.json | 1 + src/ToggleSwitch.tsx | 149 ++++++++++++++++++++++++++ stories/ToggleSwitch.stories.tsx | 59 ++++++++++ stories/ToggleSwitchGroup.stories.tsx | 73 +++++++++++++ 4 files changed, 282 insertions(+) create mode 100644 src/ToggleSwitch.tsx create mode 100644 stories/ToggleSwitch.stories.tsx create mode 100644 stories/ToggleSwitchGroup.stories.tsx diff --git a/package.json b/package.json index ba6001d73..fbe78d7ac 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "./mui": "./dist/mui.js", "./tools/cx": "./dist/tools/cx.js", "./dsfr/*": "./dsfr/*", + "./ToggleSwitch": "./dist/ToggleSwitch.js", "./Tile": "./dist/Tile.js", "./Tabs": "./dist/Tabs.js", "./Summary": "./dist/Summary.js", diff --git a/src/ToggleSwitch.tsx b/src/ToggleSwitch.tsx new file mode 100644 index 000000000..6b510eab9 --- /dev/null +++ b/src/ToggleSwitch.tsx @@ -0,0 +1,149 @@ +import React, { memo, forwardRef, ReactNode, useId } from "react"; +import { symToStr } from "tsafe/symToStr"; +import { assert } from "tsafe/assert"; +import type { Equals } from "tsafe"; + +import { cx } from "./tools/cx"; +import { fr } from "./fr"; +import { createComponentI18nApi } from "./i18n"; + +export type ToggleSwitchProps = { + className?: string; + label: ReactNode; + text?: ReactNode; + checked?: boolean; + /** Default: "true" */ + showCheckedHint?: boolean; + /** Default: "false" */ + disabled?: boolean; + /** Default: "left" */ + labelPosition?: "left" | "right"; + classes?: Partial>; +}; + +export type ToggleSwitchGroupProps = { + className?: string; + /** Needs at least one ToggleSwitch */ + togglesProps: [ToggleSwitchProps, ...ToggleSwitchProps[]]; + /** Default: "true" */ + showCheckedHint?: boolean; + /** Default: "left" */ + labelPosition?: "left" | "right"; + classes?: Partial>; +}; + +/** @see */ +export const ToggleSwitchGroup = memo(props => { + const { + className, + togglesProps, + showCheckedHint = true, + labelPosition = "right", + classes = {}, + ...rest + } = props; + + assert>(); + + return ( +
    + {togglesProps && + togglesProps.map((toggleProps, i) => ( +
  • + +
  • + ))} +
+ ); +}); + +/** @see */ +export const ToggleSwitch = memo( + forwardRef((props, ref) => { + const { + className, + label, + text, + checked = false, + showCheckedHint = true, + disabled = false, + labelPosition = "right", + classes = {}, + ...rest + } = props; + + assert>(); + + const inputId = useId(); + + const { t } = useTranslation(); + + return ( +
+ + + {text && ( +

+ {text} +

+ )} +
+ ); + }) +); + +ToggleSwitch.displayName = symToStr({ ToggleSwitch }); + +const { useTranslation, addToggleSwitchTranslations } = createComponentI18nApi({ + "componentName": symToStr({ ToggleSwitch }), + "frMessages": { + /* spell-checker: disable */ + "checked": "Activé", + "unchecked": "Désactivé" + /* spell-checker: enable */ + } +}); + +addToggleSwitchTranslations({ + "lang": "en", + "messages": { + "checked": "Active", + "unchecked": "Inactive" + } +}); + +export { addToggleSwitchTranslations }; + +export default ToggleSwitch; diff --git a/stories/ToggleSwitch.stories.tsx b/stories/ToggleSwitch.stories.tsx new file mode 100644 index 000000000..d80d21963 --- /dev/null +++ b/stories/ToggleSwitch.stories.tsx @@ -0,0 +1,59 @@ +import { ToggleSwitch } from "../dist/ToggleSwitch"; +import { sectionName } from "./sectionName"; +import { getStoryFactory } from "./getStory"; + +const { meta, getStory } = getStoryFactory({ + sectionName, + "wrappedComponent": { ToggleSwitch }, + "description": ` +- [See DSFR documentation](//www.systeme-de-design.gouv.fr/elements-d-interface/composants/interrupteur) +- [See DSFR demo](https://main--ds-gouv.netlify.app/example/component/toggle/) +- [See source code](//github.com/codegouvfr/react-dsfr/blob/main/src/ToggleSwitch.tsx)`, + "disabledProps": ["lang"] +}); + +export default meta; + +export const Default = getStory({ + label: "Label action interrupteur", + text: "Texte d’aide pour clarifier l’action", + disabled: false, + labelPosition: "right", + showCheckedHint: true, + checked: false +}); + +export const ToggleSwitchNoTextNoHint = getStory({ + label: "Label action interrupteur", + disabled: false, + labelPosition: "right", + showCheckedHint: false +}); + +export const ToggleSwitchDisabled = getStory({ + label: "Label action interrupteur", + text: "Texte d’aide pour clarifier l’action", + disabled: true, + labelPosition: "right" +}); + +export const ToggleSwitchLabelLeft = getStory({ + label: "Label action interrupteur", + text: "Texte d’aide pour clarifier l’action", + labelPosition: "left" +}); + +export const ToggleSwitchLabelLeftChecked = getStory({ + label: "Label action interrupteur", + text: "Texte d’aide pour clarifier l’action", + labelPosition: "left", + checked: true +}); + +export const ToggleSwitchLabelLeftCheckedDisabled = getStory({ + label: "Label action interrupteur", + text: "Texte d’aide pour clarifier l’action", + labelPosition: "left", + checked: true, + disabled: true +}); diff --git a/stories/ToggleSwitchGroup.stories.tsx b/stories/ToggleSwitchGroup.stories.tsx new file mode 100644 index 000000000..1d2994c9a --- /dev/null +++ b/stories/ToggleSwitchGroup.stories.tsx @@ -0,0 +1,73 @@ +import { ToggleSwitchGroup } from "../dist/ToggleSwitch"; +import { sectionName } from "./sectionName"; +import { getStoryFactory } from "./getStory"; + +const { meta, getStory } = getStoryFactory({ + sectionName, + "wrappedComponent": { ToggleSwitchGroup }, + "description": ` +- [See DSFR documentation](//www.systeme-de-design.gouv.fr/elements-d-interface/composants/interrupteur) +- [See DSFR demo](https://main--ds-gouv.netlify.app/example/component/toggle/) +- [See source code](//github.com/codegouvfr/react-dsfr/blob/main/src/ToggleSwitchGroup.tsx)`, + "disabledProps": ["lang"] +}); + +export default meta; + +export const Default = getStory({ + showCheckedHint: false, + labelPosition: "right", + togglesProps: [ + { + label: "Toggle 1", + text: "Text toggle 1", + checked: true + }, + { + label: "Toggle 2", + text: "Text toggle 2", + checked: true + }, + { + label: "Toggle 3", + text: "Text toggle 3" + }, + { + label: "Toggle 4", + text: "Text toggle 4" + }, + { + label: "Toggle 5", + text: "Text toggle 5" + } + ] +}); + +export const ToggleSwitchGroupLeftWithHint = getStory({ + showCheckedHint: true, + labelPosition: "left", + togglesProps: [ + { + label: "Toggle 1", + text: "Text toggle 1", + checked: true + }, + { + label: "Toggle 2", + text: "Text toggle 2", + checked: true + }, + { + label: "Toggle 3", + text: "Text toggle 3" + }, + { + label: "Toggle 4", + text: "Text toggle 4" + }, + { + label: "Toggle 5", + text: "Text toggle 5" + } + ] +}); From c326d6a444be440fd15a89ad59fb932eccf69ab7 Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Mon, 2 Jan 2023 15:20:36 +0100 Subject: [PATCH 2/3] fix(ToggleSwitch): add onChange --- src/ToggleSwitch.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ToggleSwitch.tsx b/src/ToggleSwitch.tsx index 6b510eab9..f9b870f33 100644 --- a/src/ToggleSwitch.tsx +++ b/src/ToggleSwitch.tsx @@ -11,6 +11,7 @@ export type ToggleSwitchProps = { className?: string; label: ReactNode; text?: ReactNode; + onChange?: () => void; checked?: boolean; /** Default: "true" */ showCheckedHint?: boolean; @@ -75,6 +76,7 @@ export const ToggleSwitch = memo( disabled = false, labelPosition = "right", classes = {}, + onChange, ...rest } = props; @@ -94,6 +96,7 @@ export const ToggleSwitch = memo( ref={ref} > Date: Mon, 2 Jan 2023 17:18:33 +0100 Subject: [PATCH 3/3] fix --- COMPONENTS.md | 2 +- src/ToggleSwitch.tsx | 65 +++++++++++++++++++-------- stories/ToggleSwitch.stories.tsx | 19 ++++++-- stories/ToggleSwitchGroup.stories.tsx | 16 ++++--- 4 files changed, 71 insertions(+), 31 deletions(-) diff --git a/COMPONENTS.md b/COMPONENTS.md index 43e1ef292..71cc79064 100644 --- a/COMPONENTS.md +++ b/COMPONENTS.md @@ -19,7 +19,7 @@ - [ ] Consent banner - [ ] Favicon (?) - [x] Stepper -- [ ] Toggle switch +- [x] Toggle switch - [ ] Follow - [ ] Link - [x] SkipLinks diff --git a/src/ToggleSwitch.tsx b/src/ToggleSwitch.tsx index f9b870f33..21cf6960c 100644 --- a/src/ToggleSwitch.tsx +++ b/src/ToggleSwitch.tsx @@ -1,4 +1,4 @@ -import React, { memo, forwardRef, ReactNode, useId } from "react"; +import React, { memo, forwardRef, ReactNode, useId, useState } from "react"; import { symToStr } from "tsafe/symToStr"; import { assert } from "tsafe/assert"; import type { Equals } from "tsafe"; @@ -6,21 +6,38 @@ import type { Equals } from "tsafe"; import { cx } from "./tools/cx"; import { fr } from "./fr"; import { createComponentI18nApi } from "./i18n"; - -export type ToggleSwitchProps = { - className?: string; - label: ReactNode; - text?: ReactNode; - onChange?: () => void; - checked?: boolean; - /** Default: "true" */ - showCheckedHint?: boolean; - /** Default: "false" */ - disabled?: boolean; - /** Default: "left" */ - labelPosition?: "left" | "right"; - classes?: Partial>; -}; +import { useConstCallback } from "./tools/powerhooks/useConstCallback"; + +export type ToggleSwitchProps = ToggleSwitchProps.Controlled | ToggleSwitchProps.Uncontrolled; + +export namespace ToggleSwitchProps { + export type Common = { + className?: string; + label: ReactNode; + text?: ReactNode; + /** Default: "true" */ + showCheckedHint?: boolean; + /** Default: "false" */ + disabled?: boolean; + /** Default: "left" */ + labelPosition?: "left" | "right"; + classes?: Partial>; + }; + + export type Uncontrolled = Common & { + /** Default: "false" */ + defaultChecked?: boolean; + checked?: undefined; + onChange?: (event: React.ChangeEvent) => void; + }; + + export type Controlled = Common & { + /** Default: "false" */ + defaultChecked?: undefined; + checked: boolean; + onChange: (event: React.ChangeEvent) => void; + }; +} export type ToggleSwitchGroupProps = { className?: string; @@ -71,7 +88,8 @@ export const ToggleSwitch = memo( className, label, text, - checked = false, + defaultChecked = false, + checked, showCheckedHint = true, disabled = false, labelPosition = "right", @@ -80,12 +98,21 @@ export const ToggleSwitch = memo( ...rest } = props; + const [checkedState, setCheckState] = useState(defaultChecked); + + const checkedValue = checked !== undefined ? checked : checkedState; + assert>(); const inputId = useId(); const { t } = useTranslation(); + const onInputChange = useConstCallback((event: React.ChangeEvent) => { + setCheckState(event.currentTarget.checked); + onChange?.(event); + }); + return (