1- import React , { memo , forwardRef , useId , type ReactNode , type CSSProperties } from "react" ;
1+ import React , {
2+ memo ,
3+ forwardRef ,
4+ useId ,
5+ type ReactNode ,
6+ type CSSProperties ,
7+ type ComponentProps
8+ } from "react" ;
29import { fr } from "../fr" ;
310import { createComponentI18nApi } from "../i18n" ;
411import { symToStr } from "tsafe/symToStr" ;
@@ -15,6 +22,7 @@ import { setBrandTopAndHomeLinkProps } from "../zz_internal/brandTopAndHomeLinkP
1522import { typeGuard } from "tsafe/typeGuard" ;
1623import { SearchButton } from "../SearchBar/SearchButton" ;
1724import { useTranslation as useSearchBarTranslation } from "../SearchBar/SearchBar" ;
25+ import { generateValidHtmlId } from "../tools/generateValidHtmlId" ;
1826
1927export type HeaderProps = {
2028 className ?: string ;
@@ -89,10 +97,8 @@ export namespace HeaderProps {
8997
9098 export type Button = Common & {
9199 linkProps ?: never ;
92- buttonProps : React . DetailedHTMLProps <
93- React . ButtonHTMLAttributes < HTMLButtonElement > ,
94- HTMLButtonElement
95- > ;
100+ buttonProps : ComponentProps < "button" > &
101+ Record < `data-${string } `, string | boolean | null | undefined > ;
96102 } ;
97103 }
98104}
@@ -123,27 +129,16 @@ export const Header = memo(
123129
124130 const id = id_props ?? "fr-header" ;
125131
132+ const menuModalId = `${ headerMenuModalIdPrefix } -${ id } ` ;
133+ const menuButtonId = `${ id } -menu-button` ;
134+ const searchModalId = `${ id } -search-modal` ;
135+ const searchInputId = `${ id } -search-input` ;
136+
126137 const isSearchBarEnabled =
127138 renderSearchInput !== undefined || onSearchButtonClick !== undefined ;
128139
129140 setBrandTopAndHomeLinkProps ( { brandTop, homeLinkProps } ) ;
130141
131- const { menuModalId, menuButtonId, searchModalId, searchInputId } = ( function useClosure ( ) {
132- const id = useId ( ) ;
133-
134- const menuModalId = `${ headerMenuModalIdPrefix } -${ id } ` ;
135- const menuButtonId = `button-${ id } ` ;
136- const searchModalId = `modal-${ id } ` ;
137- const searchInputId = `search-${ id } -input` ;
138-
139- return {
140- menuModalId,
141- menuButtonId,
142- searchModalId,
143- searchInputId
144- } ;
145- } ) ( ) ;
146-
147142 const { t } = useTranslation ( ) ;
148143 const { t : tSearchBar } = useSearchBarTranslation ( ) ;
149144
@@ -159,7 +154,13 @@ export const Header = memo(
159154 ) ? (
160155 quickAccessItem
161156 ) : (
162- < HeaderQuickAccessItem quickAccessItem = { quickAccessItem } />
157+ < HeaderQuickAccessItem
158+ id = { `${ id } -quick-access-item-${ generateValidHtmlId ( {
159+ "fallback" : "" ,
160+ "text" : quickAccessItem . text
161+ } ) } -${ i } `}
162+ quickAccessItem = { quickAccessItem }
163+ />
163164 ) }
164165 </ li >
165166 ) ) }
@@ -246,6 +247,7 @@ export const Header = memo(
246247 >
247248 { isSearchBarEnabled && (
248249 < button
250+ id = { `${ id } -search-button` }
249251 className = { fr . cx (
250252 "fr-btn--search" ,
251253 "fr-btn"
@@ -326,6 +328,7 @@ export const Header = memo(
326328 ) }
327329 >
328330 < button
331+ id = { `${ id } -search-button` }
329332 className = { fr . cx ( "fr-btn--close" , "fr-btn" ) }
330333 aria-controls = { searchModalId }
331334 title = { t ( "close" ) }
@@ -364,6 +367,7 @@ export const Header = memo(
364367 "type" : "search"
365368 } ) }
366369 < SearchButton
370+ id = { `${ id } -search-bar-button` }
367371 searchInputId = { searchInputId }
368372 onClick = { onSearchButtonClick }
369373 />
@@ -384,6 +388,7 @@ export const Header = memo(
384388 >
385389 < div className = { fr . cx ( "fr-container" ) } >
386390 < button
391+ id = { `${ id } -mobile-overlay-button-close` }
387392 className = { fr . cx ( "fr-btn--close" , "fr-btn" ) }
388393 aria-controls = { menuModalId }
389394 title = { t ( "close" ) }
@@ -438,18 +443,35 @@ addHeaderTranslations({
438443} ) ;
439444
440445export type HeaderQuickAccessItemProps = {
446+ id ?: string ;
441447 className ?: string ;
442448 quickAccessItem : HeaderProps . QuickAccessItem ;
443449} ;
444450
445451export function HeaderQuickAccessItem ( props : HeaderQuickAccessItemProps ) : JSX . Element {
446- const { className, quickAccessItem } = props ;
452+ const { id : id_props , className, quickAccessItem } = props ;
447453
448454 const { Link } = getLink ( ) ;
449455
456+ const id = ( function useClosure ( ) {
457+ const id = useId ( ) ;
458+
459+ return (
460+ id_props ??
461+ ( quickAccessItem . linkProps !== undefined
462+ ? quickAccessItem . linkProps . id
463+ : quickAccessItem . buttonProps . id ) ??
464+ `fr-header-quick-access-item${ generateValidHtmlId ( {
465+ "text" : quickAccessItem . text ,
466+ "fallback" : id
467+ } ) } `
468+ ) ;
469+ } ) ( ) ;
470+
450471 return quickAccessItem . linkProps !== undefined ? (
451472 < Link
452473 { ...quickAccessItem . linkProps }
474+ id = { id }
453475 className = { cx (
454476 fr . cx ( "fr-btn" , quickAccessItem . iconId ) ,
455477 quickAccessItem . linkProps . className ,
@@ -461,6 +483,7 @@ export function HeaderQuickAccessItem(props: HeaderQuickAccessItemProps): JSX.El
461483 ) : (
462484 < button
463485 { ...quickAccessItem . buttonProps }
486+ id = { id }
464487 className = { cx (
465488 fr . cx ( "fr-btn" , quickAccessItem . iconId ) ,
466489 quickAccessItem . buttonProps . className ,
0 commit comments