Skip to content

Commit f994ffc

Browse files
committed
Feature LanguageSelect
1 parent 8c60c0a commit f994ffc

File tree

6 files changed

+406
-6
lines changed

6 files changed

+406
-6
lines changed

src/Header/Header.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,7 @@ export type HeaderQuickAccessItemProps = {
498498
id?: string;
499499
};
500500

501+
/** NOTE: If you wrap this component you should forward the id */
501502
export function HeaderQuickAccessItem(props: HeaderQuickAccessItemProps): JSX.Element {
502503
const { className, quickAccessItem, id } = props;
503504

src/LanguageSelect.tsx

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import React from "react";
2+
import { fr } from "./fr";
3+
import { HeaderQuickAccessItem } from "./Header";
4+
import { useId } from "react";
5+
import { symToStr } from "tsafe/symToStr";
6+
import { createComponentI18nApi } from "./i18n";
7+
import "./assets/language-select.css";
8+
9+
/** NOTE: Can be used as quick access item in the Header */
10+
export function LanguageSelect<Language extends string>(props: {
11+
id?: string;
12+
supportedLangs: readonly Language[];
13+
fullNameByLang: Record<Language, string>;
14+
lang: Language;
15+
setLang: (lang: Language) => void;
16+
}) {
17+
const { supportedLangs, fullNameByLang, lang, setLang } = props;
18+
19+
const id = (function useClosure() {
20+
const id = useId();
21+
22+
return props.id ?? `language-select-${id}`;
23+
})();
24+
25+
const menuId = `dropdown-menu-${id}`;
26+
27+
const { t } = useTranslation();
28+
29+
return (
30+
<HeaderQuickAccessItem
31+
id={id}
32+
className="language-select"
33+
quickAccessItem={{
34+
buttonProps: {
35+
"aria-controls": menuId,
36+
"aria-expanded": false,
37+
title: t("select language"),
38+
className: fr.cx("fr-btn--tertiary", "fr-translate", "fr-nav")
39+
},
40+
iconId: "fr-icon-translate-2",
41+
text: (
42+
<>
43+
<div>
44+
{" "}
45+
<span className="short-label">{lang}</span>
46+
<span className={fr.cx("fr-hidden-lg")}>
47+
{" "}
48+
-{fullNameByLang[lang]}
49+
</span>{" "}
50+
</div>
51+
<div className={fr.cx("fr-collapse", "fr-menu")} id={menuId}>
52+
<ul className={fr.cx("fr-menu__list")}>
53+
{supportedLangs.map(lang_i => (
54+
<li key={lang_i}>
55+
<a
56+
className={fr.cx(
57+
"fr-translate__language",
58+
"fr-nav__link"
59+
)}
60+
href="#"
61+
aria-current={lang_i === lang ? "true" : undefined}
62+
onClick={e => {
63+
e.preventDefault();
64+
setLang(lang_i);
65+
}}
66+
>
67+
<span className="short-label">{lang_i}</span>
68+
&nbsp;-&nbsp;
69+
{fullNameByLang[lang_i]}
70+
</a>
71+
</li>
72+
))}
73+
</ul>
74+
</div>
75+
</>
76+
)
77+
}}
78+
/>
79+
);
80+
}
81+
82+
export const { useTranslation, addLanguageSelectTranslations } = createComponentI18nApi({
83+
"componentName": symToStr({ LanguageSelect }),
84+
"frMessages": {
85+
/* spell-checker: disable */
86+
"select language": "Sélectionner la langue"
87+
/* spell-checker: enable */
88+
}
89+
});
90+
91+
addLanguageSelectTranslations({
92+
"lang": "en",
93+
"messages": {
94+
"select language": "Select language"
95+
}
96+
});

src/assets/language-select.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
2+
3+
4+
.language-select .short-label {
5+
text-transform: uppercase
6+
}
7+
.language-select .fr-menu {
8+
right: 0;
9+
}
10+
11+
.language-select .fr-translate {
12+
display: inline-flex;;
13+
}

stories/Header.stories.tsx

Lines changed: 150 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,13 +201,157 @@ export const HeaderWithQuickAccessItems = getStory(
201201
"onSearchButtonClick": undefined
202202
},
203203
{
204-
"description": `See [\\<Display \\/\\>](https://components.react-dsfr.codegouv.studio/?path=/docs/components-display) for instructions on how to integrate the Dark mode switch.
204+
"description": `Let's see an example usage of quick access items in the Header component.
205205
206-
Note for Next App Router: If you want to have \`quickAccessItems\` client side without having to wrap the whole \`<Header />\`
207-
component within a \`"use client";\` directive you can use the \`<HeaderQuickAccessItem />\` component as demonstrated
208-
[here](https://github.com/garronej/react-dsfr-next-appdir-demo/blob/b485bda99d6140e59584d3134ac9e203ae6b2208/app/layout.tsx#L72) and
209-
[here](https://github.com/garronej/react-dsfr-next-appdir-demo/blob/b485bda99d6140e59584d3134ac9e203ae6b2208/app/LoginHeaderItem.tsx#L1-L24).
210-
`
206+
\`src/Header.tsx\`
207+
208+
\`\`\`tsx
209+
210+
import { Header as DsfrHeader } from "@codegouvfr/react-dsfr/Header";
211+
import { LanguageSelect } from "./LanguageSelect";
212+
import { AuthButtons } from "./AuthButtons";
213+
214+
export function Header() {
215+
216+
return (
217+
<DsfrHeader
218+
quickAccessItems={[
219+
{
220+
iconId: "fr-icon-add-circle-line",
221+
text: "Créer un espace",
222+
linkProps: {
223+
"href": "#" // Link to a page
224+
}
225+
},
226+
{
227+
iconId: "fr-icon-mail-fill",
228+
linkProps: {
229+
href: "mailto:contact@code.gouv.fr"
230+
},
231+
text: "Contact us"
232+
},
233+
<LanguageSelect />, // See "LanguageSelect" component of this website
234+
headerFooterDisplayItem, // See "Display" component of this website
235+
<AuthButtons /> // See below
236+
237+
]}
238+
/>
239+
);
240+
241+
}
242+
243+
\`\`\`
244+
245+
If you need to create a dynamic Header quick action items there is how you can do it.
246+
Let's see an example with the \`AuthButton\` component.
247+
In this example we assume the use of [oidc-spa](https://oidc-spa.dev/) for authentication.
248+
And [i18nifty](for internationalization).
249+
You can see this component live [here](https://vite-insee-starter.demo-domain.ovh/).
250+
251+
\`src/AuthButton.tsx\`
252+
253+
\`\`\`tsx
254+
255+
import { HeaderQuickAccessItem } from "@codegouvfr/react-dsfr/Header";
256+
import { declareComponentKeys, useTranslation } from "i18n"; // i18nifty
257+
import { useOidc } from "oidc"; // oidc-spa
258+
259+
type Props = {
260+
// NOTE: If you component assigns id you must use the one passed as prop.
261+
// If you have multiple id you must prefix them to differentiate them.
262+
// In this example we don't actually need to set ids but I do is so you can see how to do it.
263+
// See this example where it's more relevant:
264+
id?: string;
265+
};
266+
267+
export function AuthButtons(props: Props) {
268+
269+
const { id } = props;
270+
271+
const { isUserLoggedIn, login, logout } = useOidc();
272+
273+
const { t } = useTranslation("AuthButtons");
274+
275+
if (!isUserLoggedIn) {
276+
return (
277+
<>
278+
<HeaderQuickAccessItem
279+
id={\`login-\${id}\`}
280+
quickAccessItem={{
281+
iconId: "fr-icon-lock-line",
282+
buttonProps: {
283+
onClick: () => login({ doesCurrentHrefRequiresAuth: false })
284+
},
285+
text: t("login")
286+
}}
287+
/>
288+
<HeaderQuickAccessItem
289+
id={\`register-\${id}\`}
290+
quickAccessItem={{
291+
iconId: "ri-id-card-line",
292+
buttonProps: {
293+
onClick: () => login({
294+
doesCurrentHrefRequiresAuth: false,
295+
transformUrlBeforeRedirect: url => {
296+
const urlObj = new URL(url);
297+
298+
urlObj.pathname = urlObj.pathname.replace(
299+
/\\/auth$/,
300+
"/registrations"
301+
);
302+
303+
return urlObj.href;
304+
}
305+
})
306+
},
307+
text: t("register")
308+
}}
309+
/>
310+
</>
311+
);
312+
}
313+
314+
return (
315+
<>
316+
<HeaderQuickAccessItem
317+
id={\`account-\${id}\`}
318+
quickAccessItem={{
319+
iconId: "fr-icon-account-fill",
320+
linkProps: {
321+
to: "/account"
322+
},
323+
text: t("my account")
324+
}}
325+
/>
326+
<HeaderQuickAccessItem
327+
id={\`logout-\${id}\`}
328+
quickAccessItem={{
329+
iconId: "ri-logout-box-line",
330+
buttonProps: {
331+
onClick: () =>
332+
logout({
333+
redirectTo: "home"
334+
})
335+
},
336+
text: t("logout")
337+
}}
338+
/>
339+
</>
340+
);
341+
342+
}
343+
344+
const { i18n } = declareComponentKeys<
345+
| "login"
346+
| "register"
347+
| "logout"
348+
| "my account"
349+
>()("AuthButtons");
350+
351+
export type I18n = typeof i18n;
352+
\`\`\`
353+
354+
`
211355
}
212356
);
213357

0 commit comments

Comments
 (0)