1- import React , { memo , forwardRef , type ReactNode , type CSSProperties } from "react" ;
1+ import React , { memo , forwardRef , type ReactNode , type CSSProperties , useId } from "react" ;
22import { symToStr } from "tsafe/symToStr" ;
33import { assert } from "tsafe/assert" ;
44import 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 >
0 commit comments