Skip to content

Commit 3f4e420

Browse files
committed
2 parents 1f744ff + 7f627d9 commit 3f4e420

File tree

4 files changed

+221
-1
lines changed

4 files changed

+221
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ jspm_packages
3737
.node_repl_history
3838

3939
.vscode
40+
.idea
4041

4142
.DS_Store
4243

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@codegouvfr/react-dsfr",
3-
"version": "0.0.74",
3+
"version": "0.0.73",
44
"description": "French State Design System React integration library",
55
"repository": {
66
"type": "git",
@@ -124,6 +124,7 @@
124124
"./next": "./dist/next.js",
125125
"./mui": "./dist/mui.js",
126126
"./Tabs": "./dist/Tabs.js",
127+
"./Notice": "./dist/Notice.js",
127128
"./Header": "./dist/Header.js",
128129
"./DarkModeSwitch": "./dist/DarkModeSwitch.js",
129130
"./Alert": "./dist/Alert.js",

src/Notice.tsx

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import React, { memo, forwardRef, useState, useEffect, useRef, ReactNode } from "react";
2+
import { symToStr } from "tsafe/symToStr";
3+
import { fr } from "./lib";
4+
import { cx } from "./lib/tools/cx";
5+
import { assert } from "tsafe/assert";
6+
import type { Equals } from "tsafe";
7+
import { useConstCallback } from "./lib/tools/powerhooks/useConstCallback";
8+
import { createComponentI18nApi } from "./lib/i18n";
9+
// We make users import dsfr.css so we don't need to import the scoped CSS
10+
// but in the future if we have a complete component coverage it
11+
// we could stop requiring users to import the hole CSS and only import on a
12+
// per component basis.
13+
import "./dsfr/component/notice/notice.css";
14+
15+
export type NoticeProps = {
16+
className?: string;
17+
classes?: Partial<Record<"root" | "title" | "close", string>>;
18+
title: NonNullable<ReactNode>;
19+
} & (NoticeProps.NonClosable | NoticeProps.Closable);
20+
21+
export namespace NoticeProps {
22+
export type NonClosable = {
23+
isClosable?: false;
24+
isClosed?: undefined;
25+
onClose?: undefined;
26+
};
27+
28+
export type Closable = {
29+
isClosable: true;
30+
} & (Closable.Controlled | Closable.Uncontrolled);
31+
32+
export namespace Closable {
33+
export type Controlled = {
34+
isClosed: boolean;
35+
onClose: () => void;
36+
};
37+
38+
export type Uncontrolled = {
39+
isClosed?: undefined;
40+
onClose?: () => void;
41+
};
42+
}
43+
}
44+
45+
/** @see <https://react-dsfr-components.etalab.studio/?path=/docs/components-notice> */
46+
export const Notice = memo(
47+
forwardRef<HTMLDivElement, NoticeProps>((props, ref) => {
48+
const {
49+
className,
50+
classes = {},
51+
title,
52+
isClosable = false,
53+
isClosed: props_isClosed,
54+
onClose,
55+
...rest
56+
} = props;
57+
58+
assert<Equals<keyof typeof rest, never>>();
59+
60+
const [isClosed, setIsClosed] = useState(props_isClosed ?? false);
61+
62+
const [buttonElement, setButtonElement] = useState<HTMLButtonElement | null>(null);
63+
64+
const refShouldButtonGetFocus = useRef(false);
65+
const refShouldSetRole = useRef(false);
66+
67+
useEffect(() => {
68+
if (props_isClosed === undefined) {
69+
return;
70+
}
71+
setIsClosed((isClosed: boolean) => {
72+
if (isClosed && !props_isClosed) {
73+
refShouldButtonGetFocus.current = true;
74+
refShouldSetRole.current = true;
75+
}
76+
77+
return props_isClosed;
78+
});
79+
}, [props_isClosed]);
80+
81+
useEffect(() => {
82+
if (!refShouldButtonGetFocus.current) {
83+
return;
84+
}
85+
86+
if (buttonElement === null) {
87+
//NOTE: This should not be reachable
88+
return;
89+
}
90+
91+
refShouldButtonGetFocus.current = false;
92+
buttonElement.focus();
93+
}, [buttonElement]);
94+
95+
const onCloseButtonClick = useConstCallback(() => {
96+
if (props_isClosed === undefined) {
97+
//Uncontrolled
98+
setIsClosed(true);
99+
onClose?.();
100+
} else {
101+
//Controlled
102+
onClose();
103+
}
104+
});
105+
106+
const { t } = useTranslation();
107+
108+
if (isClosed) {
109+
return null;
110+
}
111+
112+
return (
113+
<div
114+
className={cx(fr.cx("fr-notice", `fr-notice--info`), classes.root, className)}
115+
{...(refShouldSetRole.current && { "role": "notice" })}
116+
ref={ref}
117+
{...rest}
118+
>
119+
<div className="fr-container">
120+
<div className="fr-notice__body">
121+
<p className={classes.title}>{title}</p>
122+
{/* TODO: Use our button once we have one */}
123+
{isClosable && (
124+
<button
125+
ref={setButtonElement}
126+
className={cx(fr.cx("fr-btn--close", "fr-btn"), classes.close)}
127+
onClick={onCloseButtonClick}
128+
>
129+
{t("hide message")}
130+
</button>
131+
)}
132+
</div>
133+
</div>
134+
</div>
135+
);
136+
})
137+
);
138+
139+
Notice.displayName = symToStr({ Notice });
140+
141+
export default Notice;
142+
143+
const { useTranslation, addNoticeTranslations } = createComponentI18nApi({
144+
"componentName": symToStr({ Notice }),
145+
"frMessages": {
146+
/* spell-checker: disable */
147+
"hide message": "Masquer le message"
148+
/* spell-checker: enable */
149+
}
150+
});
151+
152+
addNoticeTranslations({
153+
"lang": "en",
154+
"messages": {
155+
"hide message": "Hide the message"
156+
}
157+
});
158+
159+
addNoticeTranslations({
160+
"lang": "es",
161+
"messages": {
162+
/* spell-checker: disable */
163+
"hide message": "Occultar el mesage"
164+
/* spell-checker: enable */
165+
}
166+
});
167+
168+
export { addNoticeTranslations };

stories/Notice.stories.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Notice } from "../dist/Notice";
2+
import { sectionName } from "./sectionName";
3+
import { getStoryFactory, logCallbacks } from "./getStory";
4+
5+
const { meta, getStory } = getStoryFactory({
6+
sectionName,
7+
"wrappedComponent": { Notice },
8+
"description": `
9+
- [See DSFR documentation](https://www.systeme-de-design.gouv.fr/elements-d-interface/composants/bandeau-d-information-importante)
10+
- [See source code](https://github.com/codegouvfr/react-dsfr/blob/main/src/Notice.tsx)`,
11+
"argTypes": {
12+
"title": {
13+
"description":
14+
'Required message to display, it should not relay a "classic" information, but an important and temporary information.'
15+
},
16+
"isClosable": {
17+
"description": "If the notice should have a close button"
18+
},
19+
"onClose": {
20+
"description": "Called when the user clicks the close button"
21+
},
22+
"isClosed": {
23+
"description": `If specified the \`<Notice />\` is in
24+
[controlled mode](https://reactjs.org/docs/forms.html#controlled-components)
25+
this means that when the close button is clicked
26+
the \`onClose()\` callback will be called but you are responsible
27+
for setting \`isClosed\` to \`false\`, the \`<Notice />\` wont close itself.`,
28+
"control": { "type": null }
29+
}
30+
},
31+
"disabledProps": ["lang"]
32+
});
33+
34+
export default meta;
35+
36+
export const Default = getStory({
37+
"title": "Service maintenance is scheduled today from 12:00 to 14:00",
38+
"isClosable": true,
39+
"isClosed": undefined,
40+
...logCallbacks(["onClose"])
41+
});
42+
43+
export const NonClosableNotice = getStory({
44+
"title": "This is the title"
45+
});
46+
47+
export const ClosableNotice = getStory({
48+
"title": "This is the title",
49+
"isClosable": true
50+
});

0 commit comments

Comments
 (0)