Skip to content

Commit f2d84ce

Browse files
committed
Enable to provide custom nodes in Footer bottomItem #141
1 parent 784cfcd commit f2d84ce

File tree

3 files changed

+115
-71
lines changed

3 files changed

+115
-71
lines changed

src/Footer.tsx

Lines changed: 93 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type { FrIconClassName, RiIconClassName } from "./fr/generatedFromCss/cla
1111
import { id } from "tsafe/id";
1212
import { ModalProps } from "./Modal";
1313
import { getBrandTopAndHomeLinkProps } from "./zz_internal/brandTopAndHomeLinkProps";
14+
import { typeGuard } from "tsafe/typeGuard";
1415

1516
export type FooterProps = {
1617
className?: string;
@@ -22,7 +23,7 @@ export type FooterProps = {
2223
personalDataLinkProps?: RegisteredLinkProps;
2324
cookiesManagementLinkProps?: RegisteredLinkProps;
2425
cookiesManagementButtonProps?: ModalProps.ModalButtonProps;
25-
bottomItems?: FooterProps.BottomItem[];
26+
bottomItems?: (FooterProps.BottomItem | ReactNode)[];
2627
partnersLogos?: FooterProps.PartnersLogos;
2728
operatorLogo?: {
2829
orientation: "horizontal" | "vertical";
@@ -395,98 +396,76 @@ export const Footer = memo(
395396
...(websiteMapLinkProps === undefined
396397
? []
397398
: [
398-
id<FooterProps.BottomItem>({
399-
"text": t("website map"),
400-
"linkProps": websiteMapLinkProps
401-
})
402-
]),
399+
id<FooterProps.BottomItem>({
400+
"text": t("website map"),
401+
"linkProps": websiteMapLinkProps
402+
})
403+
]),
403404
id<FooterProps.BottomItem>({
404405
"text": `${t("accessibility")}: ${t(accessibility)}`,
405406
"linkProps": accessibilityLinkProps ?? {}
406407
}),
407408
...(termsLinkProps === undefined
408409
? []
409410
: [
410-
id<FooterProps.BottomItem>({
411-
"text": t("terms"),
412-
"linkProps": termsLinkProps
413-
})
414-
]),
411+
id<FooterProps.BottomItem>({
412+
"text": t("terms"),
413+
"linkProps": termsLinkProps
414+
})
415+
]),
415416
...(personalDataLinkProps === undefined
416417
? []
417418
: [
418-
id<FooterProps.BottomItem>({
419-
"text": t("personal data"),
420-
"linkProps": personalDataLinkProps
421-
})
422-
]),
419+
id<FooterProps.BottomItem>({
420+
"text": t("personal data"),
421+
"linkProps": personalDataLinkProps
422+
})
423+
]),
423424
...(cookiesManagementButtonProps === undefined
424425
? // one or the other, but not both. Priority to button for consent modal control.
425-
cookiesManagementLinkProps === undefined
426+
cookiesManagementLinkProps === undefined
426427
? []
427428
: [
428-
id<FooterProps.BottomItem>({
429-
"text": t("cookies management"),
430-
"linkProps": cookiesManagementLinkProps
431-
})
432-
]
429+
id<FooterProps.BottomItem>({
430+
"text": t("cookies management"),
431+
"linkProps": cookiesManagementLinkProps
432+
})
433+
]
433434
: [
434-
id<FooterProps.BottomItem>({
435-
"text": t("cookies management"),
436-
"buttonProps":
437-
cookiesManagementButtonProps.nativeButtonProps
438-
})
439-
]),
435+
id<FooterProps.BottomItem>({
436+
"text": t("cookies management"),
437+
"buttonProps":
438+
cookiesManagementButtonProps.nativeButtonProps
439+
})
440+
]),
440441
...bottomItems
441-
].map(({ iconId, text, buttonProps, linkProps }, i) => (
442-
<li
443-
key={i}
444-
className={cx(
445-
fr.cx("fr-footer__bottom-item"),
446-
classes.bottomItem
447-
)}
448-
>
449-
{(() => {
450-
const className = cx(
451-
fr.cx(
452-
"fr-footer__bottom-link",
453-
...(iconId !== undefined
454-
? ([iconId, "fr-link--icon-left"] as const)
455-
: [])
456-
),
457-
classes.bottomLink
458-
);
459-
460-
return linkProps !== undefined ? (
461-
Object.keys(linkProps).length === 0 ? (
462-
<span className={className}>{text}</span>
463-
) : (
464-
<Link
465-
{...linkProps}
466-
className={cx(className, linkProps.className)}
467-
>
468-
{text}
469-
</Link>
470-
)
442+
].map((bottomItem, i) =>
443+
<li className={cx(fr.cx("fr-footer__bottom-item"), classes.bottomItem, className)} key={i}>
444+
{
445+
!typeGuard<FooterProps.BottomItem>(
446+
bottomItem,
447+
bottomItem instanceof Object && "text" in bottomItem
448+
) ? (
449+
bottomItem
471450
) : (
472-
<button
473-
{...buttonProps}
474-
className={cx(className, buttonProps.className)}
475-
>
476-
{text}
477-
</button>
478-
);
479-
})()}
451+
<FooterBottomItem
452+
classes={{
453+
"bottomLink": classes.bottomLink
454+
}}
455+
bottomItem={bottomItem}
456+
/>
457+
)
458+
}
480459
</li>
481-
))}
460+
)}
482461
</ul>
483462
<div className={cx(fr.cx("fr-footer__bottom-copy"), classes.bottomCopy)}>
484463
<p>
485464
{license === undefined
486465
? t("license mention", {
487-
"licenseUrl":
488-
"https://github.com/etalab/licence-ouverte/blob/master/LO.md"
489-
})
466+
"licenseUrl":
467+
"https://github.com/etalab/licence-ouverte/blob/master/LO.md"
468+
})
490469
: license}
491470
</p>
492471
</div>
@@ -557,3 +536,47 @@ addFooterTranslations({
557536
});
558537

559538
export { addFooterTranslations };
539+
540+
export type FooterBottomItemProps = {
541+
className?: string;
542+
bottomItem: FooterProps.BottomItem;
543+
classes?: Partial<Record<"root" | "bottomLink", string>>;
544+
};
545+
546+
export function FooterBottomItem(props: FooterBottomItemProps): JSX.Element {
547+
const { className: className_props, bottomItem, classes = {} } = props;
548+
549+
const { Link } = getLink();
550+
551+
const className = cx(
552+
fr.cx(
553+
"fr-footer__bottom-link",
554+
...(bottomItem.iconId !== undefined
555+
? ([bottomItem.iconId, "fr-link--icon-left"] as const)
556+
: [])
557+
),
558+
classes.bottomLink,
559+
classes.root,
560+
className_props
561+
);
562+
563+
return bottomItem.linkProps !== undefined ? (
564+
Object.keys(bottomItem.linkProps).length === 0 ? (
565+
<span className={className}>{bottomItem.text}</span>
566+
) : (
567+
<Link
568+
{...bottomItem.linkProps}
569+
className={cx(className, bottomItem.linkProps.className)}
570+
>
571+
{bottomItem.text}
572+
</Link>
573+
)
574+
) : (
575+
<button
576+
{...bottomItem.buttonProps}
577+
className={cx(className, bottomItem.buttonProps.className)}
578+
>
579+
{bottomItem.text}
580+
</button>
581+
);
582+
}

test/integration/next-appdir/app/layout.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { headerFooterDisplayItem } from "@codegouvfr/react-dsfr/Display";
1212
import { fr } from "@codegouvfr/react-dsfr";
1313
import { Navigation } from "./Navigation";
1414
import Link from "next/link";
15+
import { ClientFooterItem } from "../ui/ClientFooterItem";
1516

1617
declare module "@codegouvfr/react-dsfr/gdpr" {
1718
interface RegisterGdprServices {
@@ -108,7 +109,10 @@ export default function RootLayout({ children }: { children: JSX.Element; }) {
108109
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
109110
eu fugiat nulla pariatur.
110111
`}
111-
bottomItems={[headerFooterDisplayItem]}
112+
bottomItems={[
113+
headerFooterDisplayItem,
114+
<ClientFooterItem />
115+
]}
112116
/>
113117
</MuiDsfrThemeProvider>
114118
</NextAppDirEmotionCacheProvider>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"use client";
2+
import { FooterBottomItem } from "@codegouvfr/react-dsfr/Footer";
3+
4+
export function ClientFooterItem() {
5+
return (
6+
<FooterBottomItem
7+
bottomItem={{
8+
iconId: "fr-icon-arrow-down-line",
9+
linkProps: {
10+
href: `https://example.com`,
11+
},
12+
text: "A client side bottom item",
13+
14+
}}
15+
/>
16+
);
17+
}

0 commit comments

Comments
 (0)