Skip to content

Commit 4c971ba

Browse files
committed
1 parent b5eb8c0 commit 4c971ba

File tree

2 files changed

+105
-57
lines changed

2 files changed

+105
-57
lines changed

src/SideMenu.tsx

Lines changed: 99 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { memo, forwardRef, type ReactNode, type CSSProperties } from "react";
1+
import React, { memo, forwardRef, type ReactNode, type CSSProperties, useId } from "react";
22
import { symToStr } from "tsafe/symToStr";
33
import { assert } from "tsafe/assert";
44
import type { Equals } from "tsafe";
@@ -14,7 +14,7 @@ export type SideMenuProps = {
1414
style?: CSSProperties;
1515
align?: "left" | "right";
1616
items: SideMenuProps.Item[];
17-
bugerMenuButtonText: ReactNode;
17+
burgerMenuButtonText: ReactNode;
1818
/** Default: false */
1919
sticky?: boolean;
2020
/** Default: false, only relevent when sticky */
@@ -56,63 +56,36 @@ export const SideMenu = memo(
5656
fullHeight,
5757
classes = {},
5858
align = "left",
59-
bugerMenuButtonText,
59+
burgerMenuButtonText,
6060
...rest
6161
} = props;
6262

6363
assert<Equals<keyof typeof rest, never>>();
6464

6565
const { Link } = getLink();
6666

67-
const getItem = (
68-
{ isActive, linkProps, text, items }: SideMenuProps.Item,
69-
key: number,
70-
level = 0
71-
) => {
72-
if (++level > 2) return null;
73-
74-
return (
75-
<li key={key} className={cx(fr.cx("fr-sidemenu__item"), classes.item)}>
76-
{items ? (
77-
<>
78-
<button
79-
aria-expanded="false"
80-
aria-controls={`fr-sidemenu-item-${key}`}
81-
{...(isActive && { ["aria-current"]: true })}
82-
className={cx(fr.cx("fr-sidemenu__btn"), classes.button)}
83-
>
84-
{text}
85-
</button>
86-
<div className={fr.cx("fr-collapse")} id={`fr-sidemenu-item-${key}`}>
87-
<ul className={cx(fr.cx("fr-sidemenu__list"), classes.list)}>
88-
{items.map((item, i) => getItem(item, i, level))}
89-
</ul>
90-
</div>
91-
</>
92-
) : (
93-
<Link
94-
target="_self"
95-
{...linkProps}
96-
{...(isActive && { ["aria-current"]: "page" })}
97-
className={cx(
98-
fr.cx("fr-sidemenu__link"),
99-
classes.link,
100-
linkProps?.className
101-
)}
102-
>
103-
{text}
104-
</Link>
105-
)}
106-
</li>
107-
);
108-
};
67+
const { wrapperId, titleId, getItemId } = (function useClosure() {
68+
const id = useId();
69+
70+
const wrapperId = `fr-sidemenu-wrapper-${id}`;
71+
72+
const titleId = `fr-sidemenu-title-${id}`;
73+
74+
const getItemId = (params: { level: number; key: string }) => {
75+
const { level, key } = params;
76+
77+
return `fr-sidemenu-item-${id}-${level}-${key}`;
78+
};
79+
80+
return { wrapperId, titleId, getItemId };
81+
})();
10982

11083
return (
11184
<nav
11285
{...rest}
11386
ref={ref}
11487
style={style}
115-
aria-labelledby="fr-sidemenu-title"
88+
aria-labelledby={titleId}
11689
className={cx(
11790
fr.cx("fr-sidemenu", {
11891
"fr-sidemenu--right": align === "right",
@@ -127,22 +100,97 @@ export const SideMenu = memo(
127100
<button
128101
hidden
129102
aria-expanded="false"
130-
aria-controls="fr-sidemenu-wrapper"
103+
aria-controls={wrapperId}
131104
className={cx(fr.cx("fr-sidemenu__btn"), classes.button)}
132105
>
133-
{bugerMenuButtonText}
106+
{burgerMenuButtonText}
134107
</button>
135-
<div className={fr.cx("fr-collapse")} id="fr-sidemenu-wrapper">
108+
<div className={fr.cx("fr-collapse")} id={wrapperId}>
136109
{title !== undefined && (
137110
<div
138111
className={cx(fr.cx("fr-sidemenu__title"), classes.title)}
139-
id="fr-sidemenu-title"
112+
id={titleId}
140113
>
141114
{title}
142115
</div>
143116
)}
144117
<ul className={cx(fr.cx("fr-sidemenu__list"), classes.list)}>
145-
{items.map((item, i) => getItem(item, i))}
118+
{items.map((item, i) => {
119+
const getItemRec = (params: {
120+
item: SideMenuProps.Item;
121+
key: string;
122+
level: number;
123+
}) => {
124+
const { item, key, level } = params;
125+
126+
const itemId = getItemId({ key, level });
127+
128+
return (
129+
<li
130+
key={key}
131+
className={cx(fr.cx("fr-sidemenu__item"), classes.item)}
132+
>
133+
{"items" in item ? (
134+
<>
135+
<button
136+
aria-expanded="false"
137+
aria-controls={itemId}
138+
{...(item.isActive && {
139+
["aria-current"]: true
140+
})}
141+
className={cx(
142+
fr.cx("fr-sidemenu__btn"),
143+
classes.button
144+
)}
145+
>
146+
{item.text}
147+
</button>
148+
<div
149+
className={fr.cx("fr-collapse")}
150+
id={itemId}
151+
>
152+
<ul
153+
className={cx(
154+
fr.cx("fr-sidemenu__list"),
155+
classes.list
156+
)}
157+
>
158+
{item.items.map((item, i) =>
159+
getItemRec({
160+
item,
161+
"key": `${i}`,
162+
"level": level + 1
163+
})
164+
)}
165+
</ul>
166+
</div>
167+
</>
168+
) : (
169+
<Link
170+
target="_self"
171+
{...item.linkProps}
172+
{...(item.isActive && {
173+
["aria-current"]: "page"
174+
})}
175+
className={cx(
176+
fr.cx("fr-sidemenu__link"),
177+
classes.link,
178+
item.linkProps?.className
179+
)}
180+
>
181+
{item.text}
182+
</Link>
183+
)}
184+
</li>
185+
);
186+
};
187+
188+
return getItemRec({
189+
"key": `${i}`,
190+
item,
191+
"level": 0
192+
});
193+
})}
146194
</ul>
147195
</div>
148196
</div>

stories/SideMenu.stories.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ const { meta, getStory } = getStoryFactory({
77
wrappedComponent: { SideMenu },
88
defaultContainerWidth: 300,
99
description: `
10-
- [See DSFR documentation](//www.systeme-de-design.gouv.fr/elements-d-interface/composants/menu-lateral)
10+
- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/menu-lateral)
1111
- [See DSFR demos](https://main--ds-gouv.netlify.app/example/component/sidemenu/)
12-
- [See source code](//github.com/codegouvfr/react-dsfr/blob/main/src/Sidemenu.tsx)`,
12+
- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Sidemenu.tsx)`,
1313
disabledProps: ["lang"],
1414
argTypes: {
1515
title: {
@@ -20,7 +20,7 @@ const { meta, getStory } = getStoryFactory({
2020
type: { name: "string", required: true },
2121
description: "Items used to populate the menu"
2222
},
23-
bugerMenuButtonText: {
23+
burgerMenuButtonText: {
2424
type: { name: "string", required: true },
2525
description: "Label to display next to the burger menu button"
2626
},
@@ -43,7 +43,7 @@ export default meta;
4343

4444
export const Default = getStory({
4545
title: "Titre de rubrique",
46-
bugerMenuButtonText: "Dans cette rubrique",
46+
burgerMenuButtonText: "Dans cette rubrique",
4747
items: [
4848
{
4949
isActive: true,
@@ -75,7 +75,7 @@ export const Default = getStory({
7575

7676
export const SideMenuWith2Levels = getStory({
7777
title: "Titre de rubrique",
78-
bugerMenuButtonText: "Dans cette rubrique",
78+
burgerMenuButtonText: "Dans cette rubrique",
7979
items: [
8080
{
8181
text: "Niveau 1",
@@ -147,7 +147,7 @@ export const SideMenuWith2Levels = getStory({
147147

148148
export const SideMenuWith3Levels = getStory({
149149
title: "Titre de rubrique",
150-
bugerMenuButtonText: "Dans cette rubrique",
150+
burgerMenuButtonText: "Dans cette rubrique",
151151
items: [
152152
{
153153
text: "Niveau 1",

0 commit comments

Comments
 (0)