Skip to content

Commit 2736b44

Browse files
committed
Feature footer
1 parent e45c740 commit 2736b44

File tree

5 files changed

+359
-3
lines changed

5 files changed

+359
-3
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@
128128
"./Notice": "./dist/Notice.js",
129129
"./Highlight": "./dist/Highlight.js",
130130
"./Header": "./dist/Header/index.js",
131+
"./Footer": "./dist/Footer.js",
131132
"./Display": "./dist/Display.js",
132133
"./Badge": "./dist/Badge.js",
133134
"./Alert": "./dist/Alert.js",

src/Footer.tsx

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
import React, { memo, forwardRef } from "react";
2+
import type { ReactNode } from "react";
3+
import { useLink } from "./lib/routing";
4+
import type { RegisteredLinkProps } from "./lib/routing";
5+
import { symToStr } from "tsafe/symToStr";
6+
import { fr } from "./lib";
7+
import { cx } from "./lib/tools/cx";
8+
import { assert } from "tsafe/assert";
9+
import type { Equals } from "tsafe";
10+
import { createComponentI18nApi } from "./lib/i18n";
11+
12+
export type FooterProps = {
13+
className?: string;
14+
brandTop: ReactNode;
15+
accessibility: "non compliant" | "partially compliant" | "fully compliant";
16+
contentDescription?: ReactNode;
17+
websiteMapLinkProps?: RegisteredLinkProps;
18+
accessibilityLinkProps?: RegisteredLinkProps;
19+
termsLinkProps?: RegisteredLinkProps;
20+
personalDataLinkProps?: RegisteredLinkProps;
21+
cookiesManagementLinkProps?: RegisteredLinkProps;
22+
homeLinkProps: RegisteredLinkProps & { title: string };
23+
classes?: Partial<
24+
Record<
25+
| "root"
26+
| "body"
27+
| "brand"
28+
| "content"
29+
| "contentDesc"
30+
| "contentList"
31+
| "contentItem"
32+
| "contentLink"
33+
| "bottom"
34+
| "bottomList"
35+
| "bottomItem"
36+
| "bottomLink"
37+
| "bottomCopy",
38+
string
39+
>
40+
>;
41+
};
42+
43+
/** @see <https://react-dsfr-components.etalab.studio/?path=/docs/components-footer> */
44+
export const Footer = memo(
45+
forwardRef<HTMLDivElement, FooterProps>((props, ref) => {
46+
const {
47+
className,
48+
classes = {},
49+
brandTop,
50+
contentDescription,
51+
homeLinkProps,
52+
websiteMapLinkProps,
53+
accessibilityLinkProps,
54+
accessibility,
55+
termsLinkProps,
56+
personalDataLinkProps,
57+
cookiesManagementLinkProps,
58+
...rest
59+
} = props;
60+
61+
assert<Equals<keyof typeof rest, never>>();
62+
63+
const { Link } = useLink();
64+
65+
const { t } = useTranslation();
66+
67+
return (
68+
<footer
69+
className={cx(fr.cx("fr-footer"), classes.root, className)}
70+
role="contentinfo"
71+
id="footer"
72+
ref={ref}
73+
{...rest}
74+
>
75+
<div className={fr.cx("fr-container")}>
76+
<div className={cx(fr.cx("fr-footer__body"), classes.body)}>
77+
<div
78+
className={cx(
79+
fr.cx("fr-footer__brand", "fr-enlarge-link"),
80+
classes.brand
81+
)}
82+
>
83+
<Link {...homeLinkProps}>
84+
<p className={fr.cx("fr-logo")}>{brandTop}</p>
85+
</Link>
86+
</div>
87+
<div className={cx(fr.cx("fr-footer__content"), classes.content)}>
88+
{contentDescription !== undefined && (
89+
<p
90+
className={cx(
91+
fr.cx("fr-footer__content-desc"),
92+
classes.contentDesc
93+
)}
94+
>
95+
{contentDescription}
96+
</p>
97+
)}
98+
<ul
99+
className={cx(
100+
fr.cx("fr-footer__content-list"),
101+
classes.contentList
102+
)}
103+
>
104+
{[
105+
"legifrance.gouv.fr",
106+
"gouvernement.fr",
107+
"service-public.fr",
108+
"data.gouv.fr"
109+
].map((domain, i) => (
110+
<li
111+
className={cx(
112+
fr.cx("fr-footer__content-item" as any),
113+
classes.contentItem
114+
)}
115+
key={i}
116+
>
117+
<a
118+
className={cx(
119+
fr.cx("fr-footer__content-link"),
120+
classes.contentLink
121+
)}
122+
target="_blank"
123+
href={`https://${domain}`}
124+
>
125+
{domain}
126+
</a>
127+
</li>
128+
))}
129+
</ul>
130+
</div>
131+
</div>
132+
<div className={cx(fr.cx("fr-footer__bottom"), classes.bottom)}>
133+
<ul className={cx(fr.cx("fr-footer__bottom-list"), classes.bottomList)}>
134+
{(
135+
[
136+
["website map", websiteMapLinkProps],
137+
["accessibility", accessibilityLinkProps],
138+
["terms", termsLinkProps],
139+
["personal data", personalDataLinkProps],
140+
["cookies managment", cookiesManagementLinkProps]
141+
] as const
142+
)
143+
.filter(
144+
([section, linkProps]) =>
145+
section === "accessibility" || linkProps !== undefined
146+
)
147+
.map(([section, linkProps], i) => (
148+
<li
149+
key={i}
150+
className={cx(
151+
fr.cx("fr-footer__bottom-item"),
152+
classes.bottomItem
153+
)}
154+
>
155+
{section === "accessibility"
156+
? (() => {
157+
const text = `${t("accessibility")}: ${t(
158+
accessibility
159+
)}`;
160+
161+
return linkProps === undefined ? (
162+
<a
163+
className={cx(
164+
fr.cx("fr-footer__bottom-link"),
165+
classes.bottomLink
166+
)}
167+
href="#"
168+
>
169+
{text}
170+
</a>
171+
) : (
172+
<Link
173+
{...linkProps}
174+
className={cx(
175+
fr.cx("fr-footer__bottom-link"),
176+
classes.bottomLink,
177+
linkProps.className
178+
)}
179+
>
180+
{text}
181+
</Link>
182+
);
183+
})()
184+
: (assert(linkProps !== undefined),
185+
(
186+
<Link
187+
{...linkProps}
188+
className={cx(
189+
fr.cx("fr-footer__bottom-link"),
190+
classes.bottomLink,
191+
linkProps.className
192+
)}
193+
>
194+
{t(section)}
195+
</Link>
196+
))}
197+
</li>
198+
))}
199+
</ul>
200+
<div className={cx(fr.cx("fr-footer__bottom-copy"), classes.bottomCopy)}>
201+
<p>
202+
{t("license mention")}{" "}
203+
<a
204+
href="https://github.com/etalab/licence-ouverte/blob/master/LO.md"
205+
target="_blank"
206+
>
207+
{t("etalab license")}{" "}
208+
</a>{" "}
209+
</p>
210+
</div>
211+
</div>
212+
</div>
213+
</footer>
214+
);
215+
})
216+
);
217+
218+
Footer.displayName = symToStr({ Footer });
219+
220+
export default Footer;
221+
222+
const { useTranslation, addFooterTranslations } = createComponentI18nApi({
223+
"componentName": symToStr({ Footer }),
224+
"frMessages": {
225+
/* spell-checker: disable */
226+
"hide message": "Masquer le message",
227+
"website map": "Plan du site",
228+
"accessibility": "Accessibilité",
229+
"non compliant": "non conforme",
230+
"partially compliant": "partiellement conforme",
231+
"fully compliant": "totalement conforme",
232+
"terms": "Mentions légales",
233+
"personal data": "Données personnelles",
234+
"cookies managment": "Gestion des cookies",
235+
"license mention": "Sauf mention contraire, tous les contenus de ce site sont sous",
236+
"etalab license": "licence etalab-2.0"
237+
/* spell-checker: enable */
238+
}
239+
});
240+
241+
addFooterTranslations({
242+
"lang": "en",
243+
"messages": {
244+
"hide message": "Hide the message",
245+
"website map": "Website map",
246+
"accessibility": "Accessibility",
247+
"non compliant": "non compliant",
248+
"partially compliant": "partially compliant",
249+
"fully compliant": "fully compliant",
250+
"license mention": "Unless stated otherwise all content of this website are under",
251+
"etalab license": "etalab-2.0 license"
252+
}
253+
});
254+
255+
addFooterTranslations({
256+
"lang": "es",
257+
"messages": {
258+
/* spell-checker: disable */
259+
"hide message": "Occultar el mesage"
260+
/* spell-checker: enable */
261+
}
262+
});
263+
264+
export { addFooterTranslations };

stories/Footer.stories.tsx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import React from "react";
2+
import { Footer } from "../dist/Footer";
3+
import type { FooterProps } from "../dist/Footer";
4+
import { sectionName } from "./sectionName";
5+
import { getStoryFactory } from "./getStory";
6+
import { assert } from "tsafe/assert";
7+
import type { Equals } from "tsafe";
8+
9+
const { meta, getStory } = getStoryFactory({
10+
sectionName,
11+
"wrappedComponent": { Footer },
12+
"description": `
13+
- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/footer)
14+
- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Footer.tsx)`,
15+
"argTypes": {
16+
"brandTop": {
17+
"control": { "type": null }
18+
},
19+
"accessibility": {
20+
"options": (() => {
21+
const accessibility = [
22+
"non compliant",
23+
"partially compliant",
24+
"fully compliant"
25+
] as const;
26+
27+
assert<Equals<typeof accessibility[number], FooterProps["accessibility"]>>();
28+
29+
return accessibility;
30+
})(),
31+
"control": { "type": "radio" }
32+
},
33+
"websiteMapLinkProps": {
34+
"control": { "type": null }
35+
},
36+
"accessibilityLinkProps": {
37+
"control": { "type": null }
38+
},
39+
"termsLinkProps": {
40+
"control": { "type": null }
41+
},
42+
"personalDataLinkProps": {
43+
"control": { "type": null }
44+
},
45+
"cookiesManagementLinkProps": {
46+
"control": { "type": null }
47+
},
48+
"homeLinkProps": {
49+
"control": { "type": null }
50+
}
51+
}
52+
});
53+
54+
export default meta;
55+
56+
export const Default = getStory({
57+
"brandTop": (
58+
<>
59+
INTITULE
60+
<br />
61+
OFFICIEL
62+
</>
63+
),
64+
"accessibility": "fully compliant",
65+
"contentDescription": `
66+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
67+
incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
68+
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
69+
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore
70+
eu fugiat nulla pariatur.
71+
`,
72+
"websiteMapLinkProps": {
73+
"href": "#"
74+
},
75+
"accessibilityLinkProps": {
76+
"href": "#"
77+
},
78+
"termsLinkProps": {
79+
"href": "#"
80+
},
81+
"personalDataLinkProps": {
82+
"href": "#"
83+
},
84+
"cookiesManagementLinkProps": {
85+
"href": "#"
86+
},
87+
"homeLinkProps": {
88+
"href": "/",
89+
"title": "Accueil - Nom de l’entité (ministère, secrétariat d‘état, gouvernement)"
90+
}
91+
});

test/integration/vite/src/Home.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Alert } from "@codegouvfr/react-dsfr/Alert";
22
import { useIsDark, fr } from "@codegouvfr/react-dsfr";
3-
import { DarkModeSwitch } from "@codegouvfr/react-dsfr/DarkModeSwitch";
43

54
export function Home() {
65
const { isDark, setIsDark } = useIsDark();
@@ -30,8 +29,6 @@ export function Home() {
3029
<button onClick={() => setIsDark(false)}>Set color scheme to light</button>
3130
<button onClick={() => setIsDark("system")}>Set color scheme to system</button>
3231

33-
<DarkModeSwitch />
34-
3532
</>
3633
);
3734
}

0 commit comments

Comments
 (0)