forked from Creditra/Creditra-Frontend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathFormMessage.tsx
More file actions
153 lines (137 loc) · 4.56 KB
/
Copy pathFormMessage.tsx
File metadata and controls
153 lines (137 loc) · 4.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import { useEffect, useState, type CSSProperties, type ReactNode } from "react";
import { AlertCircle, CheckCircle, Info, AlertTriangle } from "lucide-react";
const ANNOUNCEMENT_DELAY_MS = 300;
interface FormMessageProps {
/** Optional stable id so callers can wire `aria-describedby`. */
id?: string;
/** Strong leading line; omit for message-only renders. */
title?: ReactNode;
/** Body text or rich node. Optional so `reserveSpace` can render an empty slot. */
message?: ReactNode;
/**
* Visual treatment. `inline` is compact, used directly under inputs;
* `alert` is the bigger boxed style used above modal forms.
*/
tone?: "inline" | "alert";
/**
* Severity. Drives both the icon and the colour band. `error` is kept
* as an alias of `danger` so callers from other parts of the app can
* use the more conventional name.
*/
type?: "success" | "danger" | "warning" | "info" | "error";
/**
* When true, the slot reserves vertical space (`min-height`) even
* while empty. Used by forms that would otherwise jitter as the
* message appears/disappears.
*/
reserveSpace?: boolean;
/** Override the default reserved height (52 px inline, 88 px alert). */
minHeight?: number;
/** Pass-through inline style for one-off layout tweaks. */
style?: CSSProperties;
/** Pass-through class name appended to the slot container. */
className?: string;
}
function getPlainText(node: ReactNode): string {
if (node == null || typeof node === "boolean") {
return "";
}
if (typeof node === "string" || typeof node === "number") {
return String(node);
}
if (Array.isArray(node)) {
return node.map(getPlainText).filter(Boolean).join(" ");
}
if (typeof node === "object" && "props" in node) {
const props = node.props as { children?: ReactNode };
return getPlainText(props.children);
}
return "";
}
function useDebouncedAnnouncement(text: string, delay: number) {
const [announcedText, setAnnouncedText] = useState("");
useEffect(() => {
if (!text) {
setAnnouncedText("");
return;
}
const timer = window.setTimeout(() => {
setAnnouncedText(text);
}, delay);
return () => {
window.clearTimeout(timer);
};
}, [delay, text]);
return announcedText;
}
/**
* Tone-coded inline message for form fields and form-level alerts.
*
* The visual message is rendered immediately, but the live announcement is
* debounced for 300 ms so assistive technology hears the settled validation
* state instead of every intermediate keystroke.
*
* Use `reserveSpace` on the canonical version below an input to prevent
* layout shift when a message toggles on or off.
*/
export function FormMessage({
id,
title,
message,
tone = "inline",
type = "danger",
reserveSpace = false,
minHeight,
style,
className = "",
}: FormMessageProps) {
const hasContent = Boolean(title) || Boolean(message);
const announcement = useDebouncedAnnouncement(
[getPlainText(title), getPlainText(message)].filter(Boolean).join(" "),
ANNOUNCEMENT_DELAY_MS,
);
if (!hasContent && !reserveSpace) {
return null;
}
const slotClassName = [
"form-message-slot",
tone === "alert" ? "form-message-slot--alert" : "form-message-slot--inline",
className,
]
.filter(Boolean)
.join(" ");
return (
<div
className={slotClassName}
style={{
minHeight: reserveSpace
? minHeight ?? (tone === "alert" ? 88 : 52)
: undefined,
...style,
}}
>
{hasContent ? (
<>
<div
id={id}
className={`form-message form-message--${type === 'error' ? 'danger' : type} form-message--${tone}`}
>
{type === 'success' && <CheckCircle className="form-message__icon" aria-hidden="true" />}
{(type === 'danger' || type === 'error') && <AlertCircle className="form-message__icon" aria-hidden="true" />}
{type === 'warning' && <AlertTriangle className="form-message__icon" aria-hidden="true" />}
{type === 'info' && <Info className="form-message__icon" aria-hidden="true" />}
<div className="form-message__content">
{title ? <strong className="form-message__title">{title}</strong> : null}
{message ? <p className="form-message__text">{message}</p> : null}
</div>
</div>
{announcement ? (
<div className="sr-only" role="alert" aria-live="assertive" aria-atomic="true">
{announcement}
</div>
) : null}
</>
) : null}
</div>
);
}