Skip to content

Commit 6971501

Browse files
committed
Rework ToggleSwitches
1 parent 70262c3 commit 6971501

File tree

8 files changed

+282
-182
lines changed

8 files changed

+282
-182
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@
132132
"./mui": "./dist/mui.js",
133133
"./tools/cx": "./dist/tools/cx.js",
134134
"./dsfr/*": "./dsfr/*",
135+
"./ToggleSwitchGroup": "./dist/ToggleSwitchGroup.js",
135136
"./ToggleSwitch": "./dist/ToggleSwitch.js",
136137
"./Tile": "./dist/Tile.js",
137138
"./Tabs": "./dist/Tabs.js",

src/ButtonsGroup.tsx

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { memo, forwardRef } from "react";
2+
import { Button } from "./Button";
23
import { ButtonProps } from "./Button";
34
import { symToStr } from "tsafe/symToStr";
45
import { assert } from "tsafe/assert";
@@ -18,11 +19,7 @@ export namespace ButtonsGroupProps {
1819
alignment?: "left" | "center" | "right";
1920
/** Default: false */
2021
buttonsEquisized?: boolean;
21-
children: [
22-
React.ReactElement<ButtonProps>,
23-
React.ReactElement<ButtonProps>,
24-
...React.ReactElement<ButtonProps>[]
25-
];
22+
buttons: [ButtonProps, ButtonProps, ...ButtonProps[]];
2623
};
2724

2825
export type AlwaysStacked = Common & {
@@ -54,11 +51,11 @@ export const ButtonsGroup = memo(
5451
className,
5552
buttonsSize = "medium",
5653
buttonsIconPosition = "left",
57-
children,
5854
inlineLayoutWhen = "never",
5955
alignment = "left",
6056
buttonsEquisized = false,
6157
isReverseOrder = false,
58+
buttons,
6259
...rest
6360
} = props;
6461

@@ -99,8 +96,10 @@ export const ButtonsGroup = memo(
9996

10097
return (
10198
<ul className={buttonsGroupClassName} ref={ref} {...rest}>
102-
{children.map((button, i) => (
103-
<li key={i}>{button}</li>
99+
{buttons.map((buttonProps, i) => (
100+
<li key={i}>
101+
<Button {...buttonProps} />
102+
</li>
104103
))}
105104
</ul>
106105
);

src/ToggleSwitch.tsx

Lines changed: 45 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import React, { memo, forwardRef, ReactNode, useId, useState } from "react";
1+
import React, { memo, forwardRef, ReactNode, useId, useState, useEffect } from "react";
22
import { symToStr } from "tsafe/symToStr";
33
import { assert } from "tsafe/assert";
44
import type { Equals } from "tsafe";
5-
65
import { cx } from "./tools/cx";
76
import { fr } from "./fr";
87
import { createComponentI18nApi } from "./i18n";
@@ -14,10 +13,10 @@ export namespace ToggleSwitchProps {
1413
export type Common = {
1514
className?: string;
1615
label: ReactNode;
17-
text?: ReactNode;
18-
/** Default: "true" */
16+
helperText?: ReactNode;
17+
/** Default: true */
1918
showCheckedHint?: boolean;
20-
/** Default: "false" */
19+
/** Default: false */
2120
disabled?: boolean;
2221
/** Default: "left" */
2322
labelPosition?: "left" | "right";
@@ -27,90 +26,70 @@ export namespace ToggleSwitchProps {
2726
export type Uncontrolled = Common & {
2827
/** Default: "false" */
2928
defaultChecked?: boolean;
30-
checked?: undefined;
31-
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
29+
checked?: never;
30+
onChange?: (checked: boolean, e: React.ChangeEvent<HTMLInputElement>) => void;
31+
inputTitle: string;
3232
};
3333

3434
export type Controlled = Common & {
35-
/** Default: "false" */
36-
defaultChecked?: undefined;
35+
defaultChecked?: never;
3736
checked: boolean;
38-
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
37+
onChange: (checked: boolean, e: React.ChangeEvent<HTMLInputElement>) => void;
38+
inputTitle?: string;
3939
};
4040
}
4141

42-
export type ToggleSwitchGroupProps = {
43-
className?: string;
44-
/** Needs at least one ToggleSwitch */
45-
togglesProps: [ToggleSwitchProps, ...ToggleSwitchProps[]];
46-
/** Default: "true" */
47-
showCheckedHint?: boolean;
48-
/** Default: "left" */
49-
labelPosition?: "left" | "right";
50-
classes?: Partial<Record<"root" | "li", string>>;
51-
};
52-
53-
/** @see <https://react-dsfr-components.etalab.studio/?path=/docs/components-toggleswitch> */
54-
export const ToggleSwitchGroup = memo<ToggleSwitchGroupProps>(props => {
55-
const {
56-
className,
57-
togglesProps,
58-
showCheckedHint = true,
59-
labelPosition = "right",
60-
classes = {},
61-
...rest
62-
} = props;
63-
64-
assert<Equals<keyof typeof rest, never>>();
65-
66-
return (
67-
<ul className={cx(fr.cx("fr-toggle__list"), classes.root, className)} {...rest}>
68-
{togglesProps &&
69-
togglesProps.map((toggleProps, i) => (
70-
<li key={i} className={classes.li}>
71-
<ToggleSwitch
72-
{...{
73-
...toggleProps,
74-
showCheckedHint,
75-
labelPosition
76-
}}
77-
/>
78-
</li>
79-
))}
80-
</ul>
81-
);
82-
});
83-
8442
/** @see <https://react-dsfr-components.etalab.studio/?path=/docs/components-toggleswitch> */
8543
export const ToggleSwitch = memo(
8644
forwardRef<HTMLDivElement, ToggleSwitchProps>((props, ref) => {
8745
const {
8846
className,
8947
label,
90-
text,
48+
helperText,
9149
defaultChecked = false,
92-
checked,
50+
checked: props_checked,
9351
showCheckedHint = true,
9452
disabled = false,
9553
labelPosition = "right",
9654
classes = {},
9755
onChange,
56+
inputTitle,
9857
...rest
9958
} = props;
10059

101-
const [checkedState, setCheckState] = useState(defaultChecked);
60+
const [checked, setChecked] = useState(defaultChecked);
61+
62+
useEffect(() => {
63+
if (defaultChecked === undefined) {
64+
return;
65+
}
10266

103-
const checkedValue = checked !== undefined ? checked : checkedState;
67+
setChecked(defaultChecked);
68+
}, [defaultChecked]);
10469

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

107-
const inputId = useId();
72+
const { inputId, hintId } = (function useClosure() {
73+
const id = useId();
74+
75+
const inputId = `toggle-${id}`;
76+
77+
const hintId = `toggle-${id}-hint-text`;
78+
79+
return { inputId, hintId };
80+
})();
10881

10982
const { t } = useTranslation();
11083

111-
const onInputChange = useConstCallback((event: React.ChangeEvent<HTMLInputElement>) => {
112-
setCheckState(event.currentTarget.checked);
113-
onChange?.(event);
84+
const onInputChange = useConstCallback((e: React.ChangeEvent<HTMLInputElement>) => {
85+
const checked = e.currentTarget.checked;
86+
87+
if (props_checked === undefined) {
88+
setChecked(checked);
89+
onChange?.(checked, e);
90+
} else {
91+
onChange(checked, e);
92+
}
11493
});
11594

11695
return (
@@ -127,9 +106,10 @@ export const ToggleSwitch = memo(
127106
type="checkbox"
128107
disabled={disabled || undefined}
129108
className={cx(fr.cx("fr-toggle__input"), classes.input)}
130-
aria-describedby={`${inputId}-hint-text`}
109+
aria-describedby={hintId}
131110
id={inputId}
132-
checked={checkedValue}
111+
title={inputTitle}
112+
checked={props_checked ?? checked}
133113
/>
134114
<label
135115
className={cx(fr.cx("fr-toggle__label"), classes.label)}
@@ -141,12 +121,9 @@ export const ToggleSwitch = memo(
141121
>
142122
{label}
143123
</label>
144-
{text && (
145-
<p
146-
className={cx(fr.cx("fr-hint-text"), classes.hint)}
147-
id={`${inputId}-hint-text`}
148-
>
149-
{text}
124+
{helperText && (
125+
<p className={cx(fr.cx("fr-hint-text"), classes.hint)} id={hintId}>
126+
{helperText}
150127
</p>
151128
)}
152129
</div>

src/ToggleSwitchGroup.tsx

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import React, { memo } from "react";
2+
import { assert } from "tsafe/assert";
3+
import type { Equals } from "tsafe";
4+
import { cx } from "./tools/cx";
5+
import { fr } from "./fr";
6+
import { ToggleSwitch } from "./ToggleSwitch";
7+
import type { ToggleSwitchProps } from "./ToggleSwitch";
8+
9+
export type ToggleSwitchGroupProps = {
10+
className?: string;
11+
/** Default: true */
12+
showCheckedHint?: ToggleSwitchProps["showCheckedHint"];
13+
/** Default: right */
14+
labelPosition?: ToggleSwitchProps["labelPosition"];
15+
classes?: Partial<Record<"root" | "li", string>>;
16+
/** Needs at least one ToggleSwitch */
17+
toggles: [ToggleSwitchPropsWithoutSharedProps, ...ToggleSwitchPropsWithoutSharedProps[]];
18+
};
19+
20+
/** @see <https://react-dsfr-components.etalab.studio/?path=/docs/components-toggleswitchgroup> */
21+
export const ToggleSwitchGroup = memo<ToggleSwitchGroupProps>(props => {
22+
const {
23+
className,
24+
toggles,
25+
showCheckedHint = true,
26+
labelPosition = "right",
27+
classes = {},
28+
...rest
29+
} = props;
30+
31+
assert<Equals<keyof typeof rest, never>>();
32+
33+
return (
34+
<ul className={cx(fr.cx("fr-toggle__list"), classes.root, className)} {...rest}>
35+
{toggles.map((toggleSwitchProps, i) => (
36+
<li key={i} className={classes.li}>
37+
<ToggleSwitch
38+
{...toggleSwitchProps}
39+
showCheckedHint={showCheckedHint}
40+
labelPosition={labelPosition}
41+
/>
42+
</li>
43+
))}
44+
</ul>
45+
);
46+
});
47+
48+
export default ToggleSwitchGroup;
49+
50+
type ToggleSwitchPropsWithoutSharedProps =
51+
| ToggleSwitchPropsWithoutSharedProps.Controlled
52+
| ToggleSwitchPropsWithoutSharedProps.Uncontrolled;
53+
54+
namespace ToggleSwitchPropsWithoutSharedProps {
55+
export type Common = Omit<ToggleSwitchProps, "showCheckedHint" | "labelPosition">;
56+
57+
export type Uncontrolled = Common &
58+
Omit<ToggleSwitchProps.Uncontrolled, keyof ToggleSwitchProps.Common>;
59+
export type Controlled = Common &
60+
Omit<ToggleSwitchProps.Controlled, keyof ToggleSwitchProps.Common>;
61+
}

stories/ButtonsGroup.stories.tsx

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import React from "react";
21
import { ButtonsGroup } from "../dist/ButtonsGroup";
3-
import { Button } from "../dist/Button";
42
import type { ButtonsGroupProps } from "../dist/ButtonsGroup";
53
import { sectionName } from "./sectionName";
64
import { getStoryFactory } from "./getStory";
@@ -90,8 +88,8 @@ const { meta, getStory } = getStoryFactory({
9088
`,
9189
"control": { "type": "select" }
9290
},
93-
"children": {
94-
"description": `This component (ul) should have at least 2 children (RGAA)`,
91+
"buttons": {
92+
"description": `An array of ButtonProps (at least 2, RGAA)`,
9593
"control": { "type": null }
9694
}
9795
},
@@ -102,20 +100,22 @@ const { meta, getStory } = getStoryFactory({
102100
export default meta;
103101

104102
export const Default = getStory({
105-
"children": [
106-
<Button key={0} linkProps={{ href: "#" }} iconId="fr-icon-git-commit-fill">
107-
Button 1 label
108-
</Button>,
109-
<Button
110-
key={1}
111-
priority="secondary"
112-
linkProps={{ href: "#" }}
113-
iconId="fr-icon-chat-check-fill"
114-
>
115-
Button 2 label (longer)
116-
</Button>,
117-
<Button key={2} linkProps={{ href: "#" }} iconId="fr-icon-bank-card-line">
118-
Button 3 label
119-
</Button>
103+
"buttons": [
104+
{
105+
"linkProps": { "href": "#" },
106+
"iconId": "fr-icon-git-commit-fill",
107+
"children": "Button 1 label"
108+
},
109+
{
110+
"priority": "secondary",
111+
"linkProps": { "href": "#" },
112+
"iconId": "fr-icon-chat-check-fill",
113+
"children": "Button 2 label (longer)"
114+
},
115+
{
116+
"linkProps": { "href": "#" },
117+
"iconId": "fr-icon-bank-card-line",
118+
"children": "Button 3 label"
119+
}
120120
]
121121
});

stories/Tabs.stories.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ function ControlledTabs() {
4848
);
4949
5050
}
51-
\`\`\``,
51+
\`\`\`
52+
53+
`,
5254
"disabledProps": ["lang"]
5355
});
5456

0 commit comments

Comments
 (0)