Skip to content

Commit 8a9a8fc

Browse files
committed
Enable to provide ReactNode as header quick access item #141
1 parent e6ed104 commit 8a9a8fc

File tree

5 files changed

+129
-70
lines changed

5 files changed

+129
-70
lines changed

src/Footer.tsx

Lines changed: 56 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -171,13 +171,13 @@ export const Footer = memo(
171171
assert<Equals<keyof typeof rest, never>>();
172172

173173
const { brandTop, homeLinkProps } = (() => {
174-
175174
const wrap = getBrandTopAndHomeLinkProps();
176175

177176
const brandTop = brandTop_prop ?? wrap?.brandTop;
178177
const homeLinkProps = homeLinkProps_prop ?? wrap?.homeLinkProps;
179178

180-
const exceptionMessage = " hasn't been provided to the Footer and we cannot retrieve it from the Header (it's probably client side)";
179+
const exceptionMessage =
180+
" hasn't been provided to the Footer and we cannot retrieve it from the Header (it's probably client side)";
181181

182182
if (brandTop === undefined) {
183183
throw new Error(symToStr({ brandTop }) + exceptionMessage);
@@ -187,11 +187,9 @@ export const Footer = memo(
187187
throw new Error(symToStr({ homeLinkProps }) + exceptionMessage);
188188
}
189189

190-
return { brandTop, homeLinkProps};
191-
190+
return { brandTop, homeLinkProps };
192191
})();
193192

194-
195193
const { Link } = getLink();
196194

197195
const { t } = useTranslation();
@@ -428,76 +426,81 @@ export const Footer = memo(
428426
...(websiteMapLinkProps === undefined
429427
? []
430428
: [
431-
id<FooterProps.BottomItem>({
432-
"text": t("website map"),
433-
"linkProps": websiteMapLinkProps
434-
})
435-
]),
429+
id<FooterProps.BottomItem>({
430+
"text": t("website map"),
431+
"linkProps": websiteMapLinkProps
432+
})
433+
]),
436434
id<FooterProps.BottomItem>({
437435
"text": `${t("accessibility")}: ${t(accessibility)}`,
438436
"linkProps": accessibilityLinkProps ?? {}
439437
}),
440438
...(termsLinkProps === undefined
441439
? []
442440
: [
443-
id<FooterProps.BottomItem>({
444-
"text": t("terms"),
445-
"linkProps": termsLinkProps
446-
})
447-
]),
441+
id<FooterProps.BottomItem>({
442+
"text": t("terms"),
443+
"linkProps": termsLinkProps
444+
})
445+
]),
448446
...(personalDataLinkProps === undefined
449447
? []
450448
: [
451-
id<FooterProps.BottomItem>({
452-
"text": t("personal data"),
453-
"linkProps": personalDataLinkProps
454-
})
455-
]),
449+
id<FooterProps.BottomItem>({
450+
"text": t("personal data"),
451+
"linkProps": personalDataLinkProps
452+
})
453+
]),
456454
...(cookiesManagementButtonProps === undefined
457455
? // one or the other, but not both. Priority to button for consent modal control.
458-
cookiesManagementLinkProps === undefined
456+
cookiesManagementLinkProps === undefined
459457
? []
460458
: [
461-
id<FooterProps.BottomItem>({
462-
"text": t("cookies management"),
463-
"linkProps": cookiesManagementLinkProps
464-
})
465-
]
459+
id<FooterProps.BottomItem>({
460+
"text": t("cookies management"),
461+
"linkProps": cookiesManagementLinkProps
462+
})
463+
]
466464
: [
467-
id<FooterProps.BottomItem>({
468-
"text": t("cookies management"),
469-
"buttonProps":
470-
cookiesManagementButtonProps.nativeButtonProps
471-
})
472-
]),
465+
id<FooterProps.BottomItem>({
466+
"text": t("cookies management"),
467+
"buttonProps":
468+
cookiesManagementButtonProps.nativeButtonProps
469+
})
470+
]),
473471
...bottomItems
474-
].map((bottomItem, i) =>
475-
<li className={cx(fr.cx("fr-footer__bottom-item"), classes.bottomItem, className)} key={i}>
476-
{
477-
!typeGuard<FooterProps.BottomItem>(
478-
bottomItem,
479-
bottomItem instanceof Object && "text" in bottomItem
480-
) ? (
481-
bottomItem
482-
) : (
483-
<FooterBottomItem
484-
classes={{
485-
"bottomLink": classes.bottomLink
486-
}}
487-
bottomItem={bottomItem}
488-
/>
489-
)
490-
}
472+
].map((bottomItem, i) => (
473+
<li
474+
className={cx(
475+
fr.cx("fr-footer__bottom-item"),
476+
classes.bottomItem,
477+
className
478+
)}
479+
key={i}
480+
>
481+
{!typeGuard<FooterProps.BottomItem>(
482+
bottomItem,
483+
bottomItem instanceof Object && "text" in bottomItem
484+
) ? (
485+
bottomItem
486+
) : (
487+
<FooterBottomItem
488+
classes={{
489+
"bottomLink": classes.bottomLink
490+
}}
491+
bottomItem={bottomItem}
492+
/>
493+
)}
491494
</li>
492-
)}
495+
))}
493496
</ul>
494497
<div className={cx(fr.cx("fr-footer__bottom-copy"), classes.bottomCopy)}>
495498
<p>
496499
{license === undefined
497500
? t("license mention", {
498-
"licenseUrl":
499-
"https://github.com/etalab/licence-ouverte/blob/master/LO.md"
500-
})
501+
"licenseUrl":
502+
"https://github.com/etalab/licence-ouverte/blob/master/LO.md"
503+
})
501504
: license}
502505
</p>
503506
</div>

src/Header.tsx

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { MainNavigationProps } from "./MainNavigation";
1212
import { MainNavigation } from "./MainNavigation";
1313
import { Display } from "./Display/Display";
1414
import { setBrandTopAndHomeLinkProps } from "./zz_internal/brandTopAndHomeLinkProps";
15+
import { typeGuard } from "tsafe/typeGuard";
1516

1617
export type HeaderProps = {
1718
className?: string;
@@ -21,7 +22,7 @@ export type HeaderProps = {
2122
serviceTagline?: ReactNode;
2223
navigation?: MainNavigationProps.Item[] | ReactNode;
2324
/** There should be at most three of them */
24-
quickAccessItems?: HeaderProps.QuickAccessItem[];
25+
quickAccessItems?: (HeaderProps.QuickAccessItem | ReactNode)[];
2526
operatorLogo?: {
2627
orientation: "horizontal" | "vertical";
2728
/**
@@ -125,22 +126,15 @@ export const Header = memo(
125126

126127
const quickAccessNode = (
127128
<ul className={fr.cx("fr-btns-group")}>
128-
{quickAccessItems.map(({ iconId, text, buttonProps, linkProps }, i) => (
129+
{quickAccessItems.map((quickAccessItem, i) => (
129130
<li key={i}>
130-
{linkProps !== undefined ? (
131-
<Link
132-
{...linkProps}
133-
className={cx(fr.cx("fr-btn", iconId), linkProps.className)}
134-
>
135-
{text}
136-
</Link>
131+
{!typeGuard<HeaderProps.QuickAccessItem>(
132+
quickAccessItem,
133+
quickAccessItem instanceof Object && "text" in quickAccessItem
134+
) ? (
135+
quickAccessItem
137136
) : (
138-
<button
139-
{...buttonProps}
140-
className={cx(fr.cx("fr-btn", iconId), buttonProps.className)}
141-
>
142-
{text}
143-
</button>
137+
<HeaderQuickAccessItem quickAccessItem={quickAccessItem} />
144138
)}
145139
</li>
146140
))}
@@ -406,3 +400,38 @@ addHeaderTranslations({
406400
});
407401

408402
export { addHeaderTranslations };
403+
404+
export type HeaderQuickAccessItemProps = {
405+
className?: string;
406+
quickAccessItem: HeaderProps.QuickAccessItem;
407+
};
408+
409+
export function HeaderQuickAccessItem(props: HeaderQuickAccessItemProps): JSX.Element {
410+
const { className, quickAccessItem } = props;
411+
412+
const { Link } = getLink();
413+
414+
return quickAccessItem.linkProps !== undefined ? (
415+
<Link
416+
{...quickAccessItem.linkProps}
417+
className={cx(
418+
fr.cx("fr-btn", quickAccessItem.iconId),
419+
quickAccessItem.linkProps.className,
420+
className
421+
)}
422+
>
423+
{quickAccessItem.text}
424+
</Link>
425+
) : (
426+
<button
427+
{...quickAccessItem.buttonProps}
428+
className={cx(
429+
fr.cx("fr-btn", quickAccessItem.iconId),
430+
quickAccessItem.buttonProps.className,
431+
className
432+
)}
433+
>
434+
{quickAccessItem.text}
435+
</button>
436+
);
437+
}

stories/Header.stories.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,14 @@ const { meta, getStory } = getStoryFactory({
1313
- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/en-tete)
1414
- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Header.tsx)
1515
16-
See also [\\<MainNavigation \\/\\>](https://react-dsfr-components.etalab.studio/?path=/docs/components-mainnavigation)`,
16+
See also [\\<MainNavigation \\/\\>](https://react-dsfr-components.etalab.studio/?path=/docs/components-mainnavigation)
17+
18+
> Note for Next App Router: If you want to have \`quickAccessItems\` client side without having to wrap the whole \`<Header />\`
19+
> component within a \`"use client";\` directive you can use the \`<HeaderQuickAccessItem />\` component as demonstrated
20+
[here](https://github.com/codegouvfr/react-dsfr/blob/703961e480eb5f8d39e571fdd64de725aa1d4ff9/test/integration/next-appdir/app/layout.tsx#L91) and
21+
[here](https://github.com/codegouvfr/react-dsfr/blob/703961e480eb5f8d39e571fdd64de725aa1d4ff9/test/integration/next-appdir/ui/ClientHeaderQuickAccessItem.tsx#L1-L18).
22+
23+
`,
1724
"argTypes": {
1825
"brandTop": {
1926
"control": { "type": null },

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { fr } from "@codegouvfr/react-dsfr";
1313
import { Navigation } from "./Navigation";
1414
import Link from "next/link";
1515
import { ClientFooterItem } from "../ui/ClientFooterItem";
16+
import { ClientHeaderQuickAccessItem } from "../ui/ClientHeaderQuickAccessItem";
1617

1718
declare module "@codegouvfr/react-dsfr/gdpr" {
1819
interface RegisterGdprServices {
@@ -86,7 +87,8 @@ export default function RootLayout({ children }: { children: JSX.Element; }) {
8687
href: `mailto:${"joseph.garrone@code.gouv.fr"}`,
8788
},
8889
text: "Nous contacter",
89-
}
90+
},
91+
<ClientHeaderQuickAccessItem />
9092
]}
9193
navigation={<Navigation />}
9294
/>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
"use client";
2+
3+
import { HeaderQuickAccessItem } from "@codegouvfr/react-dsfr/Header";
4+
5+
export function ClientHeaderQuickAccessItem() {
6+
return (
7+
<HeaderQuickAccessItem
8+
quickAccessItem={{
9+
iconId: "fr-icon-article-fill",
10+
linkProps: {
11+
href: `/some-page`,
12+
},
13+
text: "A client side item",
14+
15+
}}
16+
/>
17+
);
18+
}

0 commit comments

Comments
 (0)