Skip to content

Commit 9914ffa

Browse files
committed
Add best effort auto generated id for analytics tracking
1 parent af8c042 commit 9914ffa

File tree

13 files changed

+135
-41
lines changed

13 files changed

+135
-41
lines changed

src/Header/Header.tsx

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
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";
29
import { fr } from "../fr";
310
import { createComponentI18nApi } from "../i18n";
411
import { symToStr } from "tsafe/symToStr";
@@ -15,6 +22,7 @@ import { setBrandTopAndHomeLinkProps } from "../zz_internal/brandTopAndHomeLinkP
1522
import { typeGuard } from "tsafe/typeGuard";
1623
import { SearchButton } from "../SearchBar/SearchButton";
1724
import { useTranslation as useSearchBarTranslation } from "../SearchBar/SearchBar";
25+
import { generateValidHtmlId } from "../tools/generateValidHtmlId";
1826

1927
export 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

440445
export type HeaderQuickAccessItemProps = {
446+
id?: string;
441447
className?: string;
442448
quickAccessItem: HeaderProps.QuickAccessItem;
443449
};
444450

445451
export 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,

src/MainNavigation/MainNavigation.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { memo, forwardRef, useId, type ReactNode, type CSSProperties } from "react";
1+
import React, { memo, forwardRef, type ReactNode, type CSSProperties } from "react";
22
import { createComponentI18nApi } from "../i18n";
33
import { symToStr } from "tsafe/symToStr";
44
import { assert } from "tsafe/assert";
@@ -84,19 +84,13 @@ export const MainNavigation = memo(
8484

8585
const { Link } = getLink();
8686

87-
const { getMenuId } = (function useClosure() {
88-
const id = useId();
89-
90-
const getMenuId = (i: number) => `menu-${id}-${i}`;
91-
92-
return { getMenuId };
93-
})();
94-
9587
const id = useAnalyticsId({
9688
"explicitlyProvidedId": id_props,
9789
"defaultIdPrefix": "main-navigation"
9890
});
9991

92+
const getMenuId = (i: number) => `${id}-menu-${i}`;
93+
10094
return (
10195
<nav
10296
id={id}
@@ -128,6 +122,7 @@ export const MainNavigation = memo(
128122
{linkProps !== undefined ? (
129123
<Link
130124
{...linkProps}
125+
id={linkProps.id ?? `${id}-link-${i}`}
131126
className={cx(
132127
fr.cx("fr-nav__link"),
133128
classes.link,
@@ -141,6 +136,7 @@ export const MainNavigation = memo(
141136
<>
142137
<button
143138
{...buttonProps}
139+
id={buttonProps.id ?? `${id}-button-${i}`}
144140
className={cx(
145141
fr.cx("fr-nav__btn"),
146142
buttonProps.className,

src/MainNavigation/MegaMenu.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { assert } from "tsafe/assert";
77
import type { Equals } from "tsafe";
88
import { getLink } from "../link";
99
import type { RegisteredLinkProps } from "../link";
10+
import { generateValidHtmlId } from "../tools/generateValidHtmlId";
1011

1112
export type MegaMenuProps = {
1213
classes?: Partial<Record<"root" | "leader" | "category" | "list", string>>;
@@ -81,6 +82,12 @@ export const MegaMenu = memo(
8182
{leader.link !== undefined && (
8283
<Link
8384
{...leader.link.linkProps}
85+
id={
86+
leader.link.linkProps.id ??
87+
`${id}-leader-link${generateValidHtmlId({
88+
"text": leader.link.text
89+
})}`
90+
}
8491
className={cx(
8592
fr.cx(
8693
"fr-link",
@@ -107,6 +114,12 @@ export const MegaMenu = memo(
107114
>
108115
<Link
109116
{...categoryMainLink.linkProps}
117+
id={
118+
categoryMainLink.linkProps.id ??
119+
`${id}-category-link${generateValidHtmlId({
120+
"text": categoryMainLink.text
121+
})}-${i}`
122+
}
110123
className={cx(
111124
fr.cx("fr-nav__link"),
112125
categoryMainLink.linkProps.className
@@ -116,10 +129,16 @@ export const MegaMenu = memo(
116129
</Link>
117130
</h5>
118131
<ul className={cx(fr.cx("fr-mega-menu__list"), classes.list)}>
119-
{links.map(({ linkProps, text, isActive }, i) => (
120-
<li key={i}>
132+
{links.map(({ linkProps, text, isActive }, j) => (
133+
<li key={j}>
121134
<Link
122135
{...linkProps}
136+
id={
137+
linkProps.id ??
138+
`${id}-link${generateValidHtmlId({
139+
"text": text
140+
})}-${i}-${j}`
141+
}
123142
className={cx(
124143
fr.cx("fr-nav__link"),
125144
linkProps.className

src/MainNavigation/Menu.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { assert } from "tsafe/assert";
88
import type { Equals } from "tsafe";
99
import type { RegisteredLinkProps } from "../link";
1010
import { getLink } from "../link";
11+
import { generateValidHtmlId } from "../tools/generateValidHtmlId";
1112

1213
export type MenuProps = {
1314
classes?: Partial<Record<"root" | "list", string>>;
@@ -44,6 +45,12 @@ export const Menu = memo(
4445
<li key={i}>
4546
<Link
4647
{...linkProps}
48+
id={
49+
linkProps.id ??
50+
`${id}-link${generateValidHtmlId({
51+
text
52+
})}-${i}`
53+
}
4754
className={cx(fr.cx("fr-nav__link"), linkProps.className)}
4855
{...(isActive && { ["aria-current"]: "page" })}
4956
>

src/Modal/Modal.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,8 @@ export { addModalTranslations };
198198
/** @see <https://components.react-dsfr.fr/?path=/docs/components-modal> */
199199
export function createModal(params: { isOpenedByDefault: boolean; id: string }): {
200200
buttonProps: {
201+
/** Only for analytics, feel free to overwrite */
202+
id: string;
201203
"aria-controls": string;
202204
"data-fr-opened": boolean;
203205
};
@@ -210,6 +212,7 @@ export function createModal(params: { isOpenedByDefault: boolean; id: string }):
210212
const { isOpenedByDefault, id } = params;
211213

212214
const buttonProps = {
215+
"id": `${id}-control-button`,
213216
"aria-controls": id,
214217
"data-fr-opened": isOpenedByDefault
215218
};

src/SearchBar/SearchBar.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,15 @@ export const SearchBar = memo(
8282
<label className={cx(fr.cx("fr-label"), classes.label)} htmlFor={inputId}>
8383
{label}
8484
</label>
85+
{/* NOTE: It is crucial that renderInput be called
86+
one time and only one time in each render to allow useState to be used inline*/}
8587
{renderInput({
8688
"className": fr.cx("fr-input"),
8789
"placeholder": label,
8890
"type": "search",
8991
"id": inputId
9092
})}
91-
<SearchButton searchInputId={inputId} onClick={onButtonClick} />
93+
<SearchButton id={`${id}-button`} searchInputId={inputId} onClick={onButtonClick} />
9294
</div>
9395
);
9496
})

src/SearchBar/SearchButton.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import { observeInputValue } from "../tools/observeInputValue";
1010
import { id } from "tsafe/id";
1111

1212
export type SearchButtonProps = {
13+
id: string;
1314
searchInputId: string;
1415
onClick: ((text: string) => void) | undefined;
1516
};
1617

1718
export function SearchButton(props: SearchButtonProps) {
18-
const { searchInputId, onClick: onClick_props } = props;
19+
const { searchInputId, onClick: onClick_props, id: id_props } = props;
1920

2021
const { t } = useTranslation();
2122

@@ -148,6 +149,7 @@ export function SearchButton(props: SearchButtonProps) {
148149

149150
return (
150151
<button
152+
id={id_props}
151153
className={fr.cx("fr-btn")}
152154
title={t("label")}
153155
onClick={onClick}

src/Summary.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { fr } from "./fr";
88
import { getLink } from "./link";
99
import type { RegisteredLinkProps } from "./link";
1010
import { useAnalyticsId } from "./tools/useAnalyticsId";
11+
import { generateValidHtmlId } from "./tools/generateValidHtmlId";
1112

1213
export type SummaryProps = {
1314
id?: string;
@@ -70,11 +71,12 @@ export const Summary = memo(
7071
)}
7172
<ol>
7273
{links.map(
73-
(link, idx) =>
74+
(link, i) =>
7475
link.linkProps.href !== undefined && (
75-
<li key={idx}>
76+
<li key={i}>
7677
<Link
7778
{...link.linkProps}
79+
id={link.linkProps.id ?? `${id}-link${generateValidHtmlId({ "text": link.text })}-${i}`}
7880
className={cx(
7981
fr.cx("fr-summary__link"),
8082
classes.link,

src/consentManagement/ConsentBannerAndConsentManagement/ConsentBanner.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ export function createConsentBanner<Finality extends string>(params: {
3030
setHostname(location.host);
3131
}, []);
3232

33+
const id = "fr-consent-banner";
34+
3335
return (
3436
<>
35-
<div id="fr-consent-banner" className={fr.cx("fr-consent-banner")}>
37+
<div id={id} className={fr.cx("fr-consent-banner")}>
3638
<h2 className={fr.cx("fr-h6")}>{t("about cookies", { hostname })}</h2>
3739
<div /*className={fr.cx("fr-consent-banner__content")}*/>
3840
<p className={fr.cx("fr-text--sm")}>
@@ -50,6 +52,7 @@ export function createConsentBanner<Finality extends string>(params: {
5052
>
5153
<li>
5254
<button
55+
id={`${id}-button-accept-all`}
5356
className={fr.cx("fr-btn")}
5457
title={t("accept all - title")}
5558
onClick={async () => {
@@ -64,6 +67,7 @@ export function createConsentBanner<Finality extends string>(params: {
6467
</li>
6568
<li>
6669
<button
70+
id={`${id}-button-refuse-app`}
6771
className={fr.cx("fr-btn")}
6872
title={t("refuse all - title")}
6973
onClick={() => {
@@ -78,6 +82,7 @@ export function createConsentBanner<Finality extends string>(params: {
7882
</li>
7983
<li>
8084
<button
85+
id={`${id}-button-customize`}
8186
className={fr.cx("fr-btn", "fr-btn--secondary")}
8287
title={t("customize cookies - title")}
8388
disabled={isProcessingChanges}

0 commit comments

Comments
 (0)