Skip to content

Commit 78930a0

Browse files
authored
feat(notice): add severity, description and link (#335)
* feat(notice): add severity * feat(notice): add description * feat(notice): add non displayed icon * fix(notice): put back optional severity * feat(notice): add link
1 parent 9998140 commit 78930a0

File tree

2 files changed

+184
-8
lines changed

2 files changed

+184
-8
lines changed

src/Notice.tsx

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,32 @@ import React, {
1010
type CSSProperties
1111
} from "react";
1212
import { symToStr } from "tsafe/symToStr";
13-
import { fr } from "./fr";
13+
import { fr, type FrClassName } from "./fr";
1414
import { cx } from "./tools/cx";
1515
import { assert } from "tsafe/assert";
1616
import type { Equals } from "tsafe";
1717
import { useConstCallback } from "./tools/powerhooks/useConstCallback";
1818
import { createComponentI18nApi } from "./i18n";
1919
import { useAnalyticsId } from "./tools/useAnalyticsId";
20+
import { getLink, type RegisteredLinkProps } from "./link";
2021

21-
export type NoticeProps = NoticeProps.NonClosable | NoticeProps.Closable;
22+
export type NoticeProps = (NoticeProps.NonClosable | NoticeProps.Closable) &
23+
(NoticeProps.OptionalIcon | NoticeProps.MandatoryIcon);
2224

2325
export namespace NoticeProps {
2426
type Common = {
2527
id?: string;
2628
className?: string;
27-
classes?: Partial<Record<"root" | "title" | "close", string>>;
29+
classes?: Partial<Record<"root" | "title" | "description" | "close" | "link", string>>;
2830
title: NonNullable<ReactNode>;
31+
description?: ReactNode;
2932
style?: CSSProperties;
33+
link?: {
34+
linkProps: RegisteredLinkProps;
35+
text: ReactNode;
36+
};
37+
/** Default: "info" */
38+
severity?: NoticeProps.Severity;
3039
};
3140

3241
export type NonClosable = Common & {
@@ -52,6 +61,30 @@ export namespace NoticeProps {
5261
isClosed?: never;
5362
};
5463
}
64+
65+
export type OptionalIcon = {
66+
severity?: Exclude<Severity, RiskyAlertSeverity | WeatherSeverity>;
67+
iconDisplayed?: boolean;
68+
};
69+
70+
export type MandatoryIcon = {
71+
severity: RiskyAlertSeverity | WeatherSeverity;
72+
iconDisplayed?: true;
73+
};
74+
75+
type ExtractSeverity<FrClassName> = FrClassName extends `fr-notice--${infer Severity}`
76+
? Severity
77+
: never;
78+
79+
export type Severity = Exclude<ExtractSeverity<FrClassName>, "no-icon">;
80+
81+
type ExtractWeatherSeverity<Severity> = Severity extends `weather-${infer _WeatherSeverity}`
82+
? Severity
83+
: never;
84+
85+
export type WeatherSeverity = ExtractWeatherSeverity<Severity>;
86+
87+
export type RiskyAlertSeverity = "witness" | "kidnapping" | "attack" | "cyberattack";
5588
}
5689

5790
/** @see <https://components.react-dsfr.codegouv.studio/?path=/docs/components-notice> */
@@ -62,10 +95,14 @@ export const Notice = memo(
6295
className,
6396
classes = {},
6497
title,
98+
description,
99+
link,
65100
isClosable = false,
66101
isClosed: props_isClosed,
67102
onClose,
68103
style,
104+
severity = "info",
105+
iconDisplayed = true,
69106
...rest
70107
} = props;
71108

@@ -82,6 +119,7 @@ export const Notice = memo(
82119

83120
const refShouldButtonGetFocus = useRef(false);
84121
const refShouldSetRole = useRef(false);
122+
const { Link } = getLink();
85123

86124
useEffect(() => {
87125
if (props_isClosed === undefined) {
@@ -124,6 +162,8 @@ export const Notice = memo(
124162
}
125163
);
126164

165+
const doNotDisplayIcon = !iconDisplayed;
166+
127167
const { t } = useTranslation();
128168

129169
if (isClosed) {
@@ -133,15 +173,46 @@ export const Notice = memo(
133173
return (
134174
<div
135175
id={id}
136-
className={cx(fr.cx("fr-notice", `fr-notice--info`), classes.root, className)}
176+
className={cx(
177+
fr.cx(
178+
"fr-notice",
179+
`fr-notice--${severity}`,
180+
doNotDisplayIcon && "fr-notice--no-icon"
181+
),
182+
classes.root,
183+
className
184+
)}
137185
{...(refShouldSetRole.current && { "role": "notice" })}
138186
ref={ref}
139187
style={style}
140188
{...rest}
141189
>
142190
<div className={fr.cx("fr-container")}>
143191
<div className={fr.cx("fr-notice__body")}>
144-
<p className={cx(fr.cx(`fr-notice__title`), classes.title)}>{title}</p>
192+
<p>
193+
<span className={cx(fr.cx(`fr-notice__title`), classes.title)}>
194+
{title}
195+
</span>
196+
{description && (
197+
<span className={cx(fr.cx("fr-notice__desc"), classes.description)}>
198+
{description}
199+
</span>
200+
)}
201+
{link && (
202+
<Link
203+
target="_blank"
204+
rel="noopener external"
205+
{...link.linkProps}
206+
className={cx(
207+
fr.cx("fr-notice__link"),
208+
classes.link,
209+
link.linkProps.className
210+
)}
211+
>
212+
{link.text}
213+
</Link>
214+
)}
215+
</p>
145216
{/* TODO: Use our button once we have one */}
146217
{isClosable && (
147218
<button

stories/Notice.stories.tsx

Lines changed: 108 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { Notice, type NoticeProps } from "../dist/Notice";
22
import { sectionName } from "./sectionName";
33
import { getStoryFactory, logCallbacks } from "./getStory";
4+
import { Equals } from "tsafe/Equals";
5+
import { assert } from "tsafe/assert";
46

57
const { meta, getStory } = getStoryFactory<NoticeProps>({
68
sectionName,
@@ -13,6 +15,38 @@ const { meta, getStory } = getStoryFactory<NoticeProps>({
1315
"description":
1416
'Required message to display, it should not relay a "classic" information, but an important and temporary information.'
1517
},
18+
"description": {
19+
"description": "Optional message to complete title"
20+
},
21+
"link": {
22+
"description": "Optional link to display"
23+
},
24+
"severity": {
25+
"description": 'Default : "info"',
26+
"options": (() => {
27+
const severities = [
28+
"info",
29+
"warning",
30+
"alert",
31+
"weather-orange",
32+
"weather-red",
33+
"weather-purple",
34+
"witness",
35+
"kidnapping",
36+
"attack",
37+
"cyberattack"
38+
] as const;
39+
40+
assert<Equals<typeof severities[number] | undefined, NoticeProps["severity"]>>();
41+
42+
return severities;
43+
})(),
44+
"control": { "type": "radio" }
45+
},
46+
"iconDisplayed": {
47+
"description":
48+
"This option is possible if the notice is not a weather one or an alert one (witness, kidnapping, attack or cyberattack)."
49+
},
1650
"isClosable": {
1751
"description": "If the notice should have a close button"
1852
},
@@ -34,17 +68,88 @@ const { meta, getStory } = getStoryFactory<NoticeProps>({
3468
export default meta;
3569

3670
export const Default = getStory({
37-
"title": "Service maintenance is scheduled today from 12:00 to 14:00",
71+
"title": "Service maintenance is scheduled today from 12:00 to 14:00.",
72+
"description": "All will be ok after 14:00.",
73+
"link": {
74+
"linkProps": {
75+
"href": "#"
76+
},
77+
"text": "More information"
78+
},
3879
"isClosable": true,
3980
"isClosed": undefined,
81+
"severity": "info",
82+
"iconDisplayed": true,
4083
...logCallbacks(["onClose"])
4184
});
4285

4386
export const NonClosableNotice = getStory({
44-
"title": "This is the title"
87+
"title": "This is the title",
88+
"description": "This is the description."
4589
});
4690

4791
export const ClosableNotice = getStory({
48-
"title": "This is the title",
92+
"title": "This is the title.",
93+
"description": "This is the description.",
4994
"isClosable": true
5095
});
96+
97+
export const InfoNotice = getStory({
98+
"title": "This is a Info notice.",
99+
"description": "This is the description.",
100+
"severity": "info"
101+
});
102+
103+
export const WarningNotice = getStory({
104+
"title": "This is a Warning notice.",
105+
"description": "This is the description.",
106+
"severity": "warning"
107+
});
108+
109+
export const AlertNotice = getStory({
110+
"title": "This is an Alert notice.",
111+
"description": "This is the description.",
112+
"severity": "alert"
113+
});
114+
115+
export const WeatherOrangeNotice = getStory({
116+
"title": "This is a WeatherOrange notice.",
117+
"description": "This is the description.",
118+
"severity": "weather-orange"
119+
});
120+
121+
export const WeatherRedNotice = getStory({
122+
"title": "This is a WeatherRed notice.",
123+
"description": "This is the description.",
124+
"severity": "weather-red"
125+
});
126+
127+
export const WeatherPurpleNotice = getStory({
128+
"title": "This is a WeatherPurple notice.",
129+
"description": "This is the description.",
130+
"severity": "weather-purple"
131+
});
132+
133+
export const WitnessNotice = getStory({
134+
"title": "This is a Witness notice.",
135+
"description": "This is the description.",
136+
"severity": "witness"
137+
});
138+
139+
export const KidnappingNotice = getStory({
140+
"title": "This is a Kidnapping notice.",
141+
"description": "This is the description.",
142+
"severity": "kidnapping"
143+
});
144+
145+
export const AttackNotice = getStory({
146+
"title": "This is an Attack notice.",
147+
"description": "This is the description.",
148+
"severity": "attack"
149+
});
150+
151+
export const CyberattackNotice = getStory({
152+
"title": "This is a Cyberattack notice.",
153+
"description": "This is the description.",
154+
"severity": "cyberattack"
155+
});

0 commit comments

Comments
 (0)