@madie/madie-design-system 1.2.56 → 1.2.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -165,7 +165,11 @@ const AutoComplete = ({
165
165
  disabled={disabled}
166
166
  multiple={multiple}
167
167
  disableCloseOnSelect={multiple}
168
- disableClearable={!rest.value?.length || rest.value?.[0] === "" || rest.value?.[0] === "-"}
168
+ disableClearable={
169
+ !rest.value?.length ||
170
+ rest.value?.[0] === "" ||
171
+ rest.value?.[0] === "-"
172
+ }
169
173
  sx={
170
174
  multiple
171
175
  ? {
@@ -1,11 +1,12 @@
1
- import React, { useEffect, useState } from "react";
1
+ import React, { useEffect, useState, useMemo } from "react";
2
2
 
3
3
  import ErrorIcon from "@mui/icons-material/Error"; // warning
4
4
  import InfoIcon from "@mui/icons-material/Info"; // info
5
5
  import CancelIcon from "@mui/icons-material/Cancel"; // error
6
6
  import CheckCircleIcon from "@mui/icons-material/CheckCircle"; //success
7
7
  import ContentCopyIcon from "@mui/icons-material/ContentCopy"; //copy
8
- import FullscreenExitRoundedIcon from '@mui/icons-material/FullscreenExitRounded';
8
+ import FullscreenExitRoundedIcon from "@mui/icons-material/FullscreenExitRounded";
9
+ import WarningRoundedIcon from "@mui/icons-material/WarningRounded";
9
10
  import Tooltip from "@mui/material/Tooltip";
10
11
  import { IconButton } from "@mui/material";
11
12
  import ClearIcon from "@mui/icons-material/Clear";
@@ -13,171 +14,362 @@ import Toast from "../Toast/index";
13
14
  import classNames from "classnames";
14
15
  import PropTypes from "prop-types";
15
16
 
16
- // warning, info, error, success
17
+ // Define icon mapping outside component to avoid recreation on each render
18
+ const typeToIconMap = {
19
+ warning: ErrorIcon,
20
+ info: InfoIcon,
21
+ error: CancelIcon,
22
+ success: CheckCircleIcon,
23
+ };
24
+
25
+ // Common button styles
26
+ const buttonBaseSx = {
27
+ marginLeft: "auto",
28
+ };
29
+
30
+ const buttonWithDividerSx = {
31
+ ...buttonBaseSx,
32
+ "&:after": {
33
+ content: `''`,
34
+ position: "absolute",
35
+ left: "0px",
36
+ width: "1px",
37
+ height: "40px",
38
+ backgroundColor: "#B0B0B0",
39
+ pointerEvents: "none",
40
+ },
41
+ };
42
+
43
+ // Extract alert content processing to a separate function
44
+ const processContent = (content) => {
45
+ // Counts errors/warnings in ul/li elements
46
+ const countUlLiChildren = (element) => {
47
+ const counts = { ul: 0, li: 0 };
48
+
49
+ const traverse = (el) => {
50
+ if (!el || typeof el !== "object") return;
51
+
52
+ if (el.type === "ul" || el.type === "li") {
53
+ const children = el.props?.children;
54
+ const childCount = Array.isArray(children)
55
+ ? children.length
56
+ : children
57
+ ? 1
58
+ : 0;
59
+ counts[el.type] += childCount;
60
+ }
61
+
62
+ const children = el.props?.children;
63
+ if (Array.isArray(children)) {
64
+ children.forEach(traverse);
65
+ } else if (children && typeof children === "object") {
66
+ traverse(children);
67
+ }
68
+ };
69
+
70
+ traverse(element);
71
+
72
+ return counts.li > counts.ul ? counts.li : counts.ul;
73
+ };
74
+
75
+ // Converts content to text for copying - improved to avoid unwanted characters
76
+ const buildCopyText = (contentNode, parentNode = true) => {
77
+ if (!contentNode) return "";
78
+
79
+ // Initialize result string
80
+ let result = "";
81
+
82
+ // Handle specific node types that need special formatting
83
+ if (contentNode.type === "ul") {
84
+ // Start ul with a newline
85
+ result = "\n";
86
+ }
87
+
88
+ // Process children based on their type
89
+ if (Array.isArray(contentNode.props?.children)) {
90
+ contentNode.props.children.forEach((child, index) => {
91
+ const childText = buildCopyText(child, false);
92
+ // Only add non-empty text
93
+ if (childText.trim()) {
94
+ result += childText;
95
+
96
+ // Add line breaks after certain elements
97
+ if (
98
+ (parentNode && index === 1) ||
99
+ contentNode.type === "ul"
100
+ ) {
101
+ result += "\n";
102
+ }
103
+ }
104
+ });
105
+ } else if (typeof contentNode.props?.children === "object") {
106
+ result += buildCopyText(contentNode.props.children, false);
107
+ } else if (
108
+ typeof contentNode.props?.children === "string" ||
109
+ typeof contentNode.props?.children === "number"
110
+ ) {
111
+ // Clean the string by trimming and normalizing whitespace
112
+ result += contentNode.props.children
113
+ .toString()
114
+ .replace(/\s+/g, " ")
115
+ .trim();
116
+ } else if (
117
+ typeof contentNode === "string" ||
118
+ typeof contentNode === "number"
119
+ ) {
120
+ // Clean the string by trimming and normalizing whitespace
121
+ result += contentNode.toString().replace(/\s+/g, " ").trim();
122
+ }
123
+
124
+ return result;
125
+ };
126
+
127
+ // Process content and ensure we trim the final result
128
+ const rawText = buildCopyText(content, false);
129
+ return {
130
+ errorCount: countUlLiChildren(content),
131
+ copyText: rawText
132
+ .replace(/\s+\n/g, "\n")
133
+ .replace(/\n\s+/g, "\n")
134
+ .trim(),
135
+ };
136
+ };
137
+
138
+ // Define ActionButton as a separate component outside MadieAlert
139
+ const ActionButton = ({ tooltip, onClick, icon, testId, sx }) => (
140
+ <Tooltip title={tooltip} data-testid={`${testId}-tooltip`} arrow>
141
+ <IconButton onClick={onClick} sx={sx} data-testid={testId}>
142
+ {icon}
143
+ </IconButton>
144
+ </Tooltip>
145
+ );
146
+
147
+ // Add PropTypes for ActionButton
148
+ ActionButton.propTypes = {
149
+ tooltip: PropTypes.string.isRequired,
150
+ onClick: PropTypes.func.isRequired,
151
+ icon: PropTypes.node.isRequired,
152
+ testId: PropTypes.string.isRequired,
153
+ sx: PropTypes.object,
154
+ };
155
+
17
156
  const MadieAlert = ({
18
157
  type = "warning",
19
158
  visible = true,
20
159
  content,
21
160
  canClose = true,
22
- minimizeAlerts = false, // pass the minimizeAlerts featureflag for now, since it will be standard later
23
- alertProps, // props to pass to outer component
24
- closeButtonProps, // props to pass to close button
161
+ minimizeAlerts = false,
162
+ alertProps,
163
+ closeButtonProps,
25
164
  copyButton,
165
+ alerts = null,
26
166
  }) => {
27
- const [copyText, setCopyText] = useState("");
28
- const [toastOpen, setToastOpen] = useState(false);
29
- const [minimized, setMinimized] = useState(false);
30
-
31
- const copyButtonBuilder = (content) => {
32
- const traversal = (contentNode, parentNode = true) => {
33
- if (!contentNode) return "";
34
- let result = contentNode.type === "ul" ? "\n" : "";
35
- if (Array.isArray(contentNode.props?.children)) {
36
- contentNode.props.children.forEach((child, index) => {
37
- result += traversal(child, false);
38
- if (
39
- (parentNode && index == 1) ||
40
- contentNode.type === "ul"
41
- ) {
42
- result += "\n";
43
- }
44
- });
45
- } else if (typeof contentNode.props?.children === "object") {
46
- result += traversal(contentNode.props.children, false);
47
- } else if (
48
- typeof contentNode.props?.children === "string" ||
49
- typeof contentNode.props?.children === "number"
50
- ) {
51
- result += contentNode.props.children.toString();
52
- } else if (
53
- typeof contentNode === "string" ||
54
- typeof contentNode === "number"
55
- ) {
56
- result += contentNode.toString();
57
- }
58
- return result;
59
- };
60
- return traversal(content, false).trim();
61
- };
167
+ // Consolidate related state
168
+ const [state, setState] = useState({
169
+ toastOpen: false,
170
+ copyText: "",
171
+ minimizedAlerts: {},
172
+ individualErrors: {},
173
+ });
62
174
 
63
- useEffect(() => {
64
- if (content && copyButton) {
65
- setCopyText(copyButtonBuilder(content));
66
- }
67
- }, [content]);
68
- // we have four states to render for
69
- const typeSelect = {
70
- warning: ErrorIcon,
71
- info: InfoIcon,
72
- error: CancelIcon,
73
- success: CheckCircleIcon,
175
+ // Create alias for readability
176
+ const { toastOpen, copyText, minimizedAlerts, individualErrors } = state;
177
+
178
+ // Use a single update function
179
+ const updateState = (updates) => {
180
+ setState((prev) => ({ ...prev, ...updates }));
74
181
  };
75
- const Icon = typeSelect[type];
76
- const alertClass = classNames("madie-alert", type);
77
- const iconClass = classNames("alert-icon", type);
78
182
 
79
- return (
80
- visible && !minimized && (
81
- <div className={alertClass} {...alertProps}>
82
- <Toast
83
- toastKey="copy-success-toast"
84
- data-testid="copy-success"
85
- toastType="success"
86
- open={toastOpen}
87
- message="Copied to clipboard!"
88
- onClose={() => {
89
- setToastOpen(false);
90
- }}
91
- autoHideDuration={1500}
92
- />
93
- <Icon className={iconClass} />
94
- <div id="content">{content && content}</div>
183
+ // Prepare alerts array once
184
+ const alertsArray = useMemo(() => {
185
+ return (
186
+ alerts || [
95
187
  {
96
- // minimizeAlerts is a feature flag for now, since it will be standard later
188
+ type,
189
+ visible,
190
+ content,
191
+ canClose,
192
+ alertProps,
193
+ closeButtonProps,
194
+ copyButton,
195
+ },
196
+ ]
197
+ );
198
+ }, [
199
+ alerts,
200
+ type,
201
+ visible,
202
+ content,
203
+ canClose,
204
+ alertProps,
205
+ closeButtonProps,
206
+ copyButton,
207
+ ]);
208
+
209
+ // Process all alerts content once
210
+ useEffect(() => {
211
+ const newIndividualErrors = {};
212
+ let totalCopyText = "";
213
+
214
+ alertsArray.forEach((alert, index) => {
215
+ if (alert.content && alert.visible !== false) {
216
+ const processed = processContent(alert.content);
217
+ newIndividualErrors[index] = processed.errorCount;
218
+
219
+ if (alert.copyButton) {
220
+ totalCopyText += processed.copyText + "\n\n";
97
221
  }
222
+ }
223
+ });
224
+
225
+ updateState({
226
+ individualErrors: newIndividualErrors,
227
+ copyText: totalCopyText.trim(),
228
+ });
229
+ }, [alertsArray]);
230
+
231
+ // Utility functions for managing minimized state
232
+ const minimizeAlert = (index) => {
233
+ updateState({
234
+ minimizedAlerts: { ...minimizedAlerts, [index]: true },
235
+ });
236
+ };
237
+
238
+ const restoreAllAlerts = () => {
239
+ updateState({ minimizedAlerts: {} });
240
+ };
241
+
242
+ // Calculate minimized alerts info
243
+ const minimizedIndices = Object.keys(minimizedAlerts)
244
+ .filter((key) => minimizedAlerts[key])
245
+ .map((key) => parseInt(key));
246
+
247
+ const totalMinimizedErrors = minimizedIndices.reduce(
248
+ (sum, index) => sum + (individualErrors[index] || 0),
249
+ 0
250
+ );
251
+
252
+ // Render alert content
253
+ const renderAlert = (alert, index) => {
254
+ const {
255
+ type = "warning",
256
+ visible = true,
257
+ content,
258
+ canClose = true,
259
+ alertProps = {},
260
+ closeButtonProps = {},
261
+ copyButton = false,
262
+ } = alert;
263
+
264
+ if (!visible || minimizedAlerts[index]) return null;
265
+
266
+ const Icon = typeToIconMap[type];
267
+ const alertClass = classNames("madie-alert", type);
268
+
269
+ return (
270
+ <div key={index} className={alertClass} {...alertProps}>
271
+ <Icon className={classNames("alert-icon", type)} />
272
+ <div id="content" data-alert-index={index}>
273
+ {content}
274
+ </div>
275
+
98
276
  {minimizeAlerts && (
99
- <Tooltip title={"Minimize"} arrow>
100
- <IconButton sx={{
101
- marginLeft: "auto",
102
- "&:after": {
103
- content: `''`,
104
- position: "absolute",
105
- left: "0px",
106
- width: "1px",
107
- height: "40px",
108
- backgroundColor: "#B0B0B0",
109
- pointerEvents: "none",
110
- },
111
- }}
112
- >
113
- <FullscreenExitRoundedIcon
114
- onClick={(e) => {
115
- e.preventDefault();
116
- setMinimized(true);
117
- }}
277
+ <ActionButton
278
+ tooltip="Minimize"
279
+ onClick={(e) => {
280
+ e.preventDefault();
281
+ minimizeAlert(index);
282
+ }}
283
+ icon={
284
+ <FullscreenExitRoundedIcon
118
285
  sx={{ color: "#242424" }}
119
286
  />
120
- </IconButton>
121
- </Tooltip>
287
+ }
288
+ testId={`minimize-button-${index}`}
289
+ sx={buttonWithDividerSx}
290
+ />
122
291
  )}
292
+
123
293
  {copyButton && (
124
- <Tooltip
125
- data-testid="copy-button-tooltip"
126
- title={"Copy"}
127
- arrow
128
- >
129
- <IconButton
130
- sx={{
131
- marginLeft: "auto",
132
- ...(!minimizeAlerts && {
133
- "&:after": {
134
- content: `''`,
135
- position: "absolute",
136
- left: "0px",
137
- width: "1px",
138
- height: "40px",
139
- backgroundColor: "#B0B0B0",
140
- pointerEvents: "none",
141
- }
142
- })
143
- }}
144
- >
145
- <ContentCopyIcon
146
- onClick={(e) => {
147
- e.preventDefault();
148
- navigator.clipboard.writeText(copyText);
149
- setToastOpen(true);
150
- }}
151
- sx={{ color: "#242424" }}
152
- />
153
- </IconButton>
154
- </Tooltip>
294
+ <ActionButton
295
+ tooltip="Copy"
296
+ onClick={(e) => {
297
+ e.preventDefault();
298
+ navigator.clipboard.writeText(
299
+ copyButton
300
+ ? processContent(content).copyText
301
+ : copyText
302
+ );
303
+ updateState({ toastOpen: true });
304
+ }}
305
+ icon={<ContentCopyIcon sx={{ color: "#242424" }} />}
306
+ testId="copy-button"
307
+ sx={
308
+ !minimizeAlerts ? buttonWithDividerSx : buttonBaseSx
309
+ }
310
+ />
155
311
  )}
156
312
 
157
313
  {canClose && (
158
- <IconButton
159
- sx={{
160
- marginLeft: "auto",
161
- "&:after": {
162
- content: `''`,
163
- position: "absolute",
164
- left: "0px",
165
- width: "1px",
166
- height: "40px",
167
- backgroundColor: "#B0B0B0",
168
- pointerEvents: "none",
169
- },
170
- }}
171
- {...closeButtonProps}
172
- >
314
+ <IconButton sx={buttonWithDividerSx} {...closeButtonProps}>
173
315
  <ClearIcon sx={{ color: "#242424" }} />
174
316
  </IconButton>
175
317
  )}
176
318
  </div>
177
- )
319
+ );
320
+ };
321
+
322
+ return (
323
+ <div>
324
+ <Toast
325
+ toastKey="copy-success-toast"
326
+ data-testid="copy-success"
327
+ toastType="success"
328
+ open={toastOpen}
329
+ message="Copied to clipboard!"
330
+ onClose={() => updateState({ toastOpen: false })}
331
+ autoHideDuration={1500}
332
+ />
333
+
334
+ {/* Render visible alerts */}
335
+ {alertsArray.map((alert, index) => renderAlert(alert, index))}
336
+
337
+ {/* Show minimized indicator if any alerts are minimized */}
338
+ {minimizedIndices.length > 0 && (
339
+ <div
340
+ style={{
341
+ position: "absolute",
342
+ right: "32px",
343
+ top: "10px",
344
+ display: "flex",
345
+ alignItems: "center",
346
+ cursor: "pointer",
347
+ }}
348
+ data-testid="minimized-alert"
349
+ onClick={(e) => {
350
+ e.preventDefault();
351
+ restoreAllAlerts();
352
+ }}
353
+ >
354
+ <WarningRoundedIcon
355
+ sx={{ color: "yellow", marginRight: "5px" }}
356
+ />
357
+ <span
358
+ data-testid="minimized-alert-text"
359
+ style={{ color: "white", fontSize: "16px" }}
360
+ >
361
+ Display Alerts{" "}
362
+ {totalMinimizedErrors > 0
363
+ ? `(${totalMinimizedErrors})`
364
+ : ""}
365
+ </span>
366
+ </div>
367
+ )}
368
+ </div>
178
369
  );
179
370
  };
180
371
 
372
+ // PropTypes definition
181
373
  MadieAlert.propTypes = {
182
374
  type: PropTypes.string,
183
375
  visible: PropTypes.bool,
@@ -187,10 +379,23 @@ MadieAlert.propTypes = {
187
379
  closeButtonProps: PropTypes.object,
188
380
  copyButton: PropTypes.bool,
189
381
  minimizeAlerts: PropTypes.bool,
382
+ alerts: PropTypes.arrayOf(
383
+ PropTypes.shape({
384
+ type: PropTypes.string,
385
+ visible: PropTypes.bool,
386
+ content: PropTypes.node,
387
+ canClose: PropTypes.bool,
388
+ alertProps: PropTypes.object,
389
+ closeButtonProps: PropTypes.object,
390
+ copyButton: PropTypes.bool,
391
+ })
392
+ ),
190
393
  };
394
+
191
395
  MadieAlert.defaultProps = {
192
396
  copyButton: false,
193
397
  minimizeAlerts: false,
398
+ alerts: null,
194
399
  };
195
400
 
196
401
  export default MadieAlert;
@@ -179,6 +179,7 @@ const RichTextEditor = ({
179
179
  {
180
180
  extensions: [
181
181
  StarterKit,
182
+ Markdown,
182
183
  Gapcursor,
183
184
  Table.configure({
184
185
  resizable: true,
@@ -187,7 +188,6 @@ const RichTextEditor = ({
187
188
  TableHeader,
188
189
  TableCell,
189
190
  Underline,
190
- Markdown,
191
191
  ],
192
192
  shouldRerenderOnTransaction: false,
193
193
  content,
@@ -200,7 +200,7 @@ const RichTextEditor = ({
200
200
  [content]
201
201
  );
202
202
  return (
203
- <>
203
+ <div className="rich-text-editor">
204
204
  <InputLabel
205
205
  shrink
206
206
  required={required}
@@ -243,7 +243,7 @@ const RichTextEditor = ({
243
243
  </InputLabel>
244
244
  <MenuBar editor={editor} />
245
245
  <EditorContent editor={editor} />
246
- </>
246
+ </div>
247
247
  );
248
248
  };
249
249
 
@@ -1,6 +1,6 @@
1
1
  import React, { useState } from "react";
2
2
  import { withKnobs } from "@storybook/addon-knobs";
3
-
3
+ import "./tabs.scss";
4
4
  import Tabs from "./index";
5
5
  import Tab from "./Tab";
6
6
 
@@ -147,44 +147,46 @@ export const TypeA = () => {
147
147
  />
148
148
  </Tabs>
149
149
  </div>
150
- <div>
151
- Vertical Standard: C
152
- <Tabs
153
- type="C"
154
- value={selected}
155
- onChange={handleChange}
156
- orientation="vertical"
157
- >
158
- <Tab
150
+ <div className={"vertical-standard-type-c-wrapper"}>
151
+ <div className={"vertical-standard-type-c"}>
152
+ <h4>Vertical Standard: C</h4>
153
+ <Tabs
159
154
  type="C"
160
- size="standard"
161
- label="Item One"
162
- value={1}
155
+ value={selected}
156
+ onChange={handleChange}
163
157
  orientation="vertical"
164
- />
165
- <Tab
166
- type="C"
167
- size="standard"
168
- label="Item Two"
169
- value={2}
170
- orientation="vertical"
171
- />
172
- <Tab
173
- type="C"
174
- size="standard"
175
- label="Item Three"
176
- value={3}
177
- orientation="vertical"
178
- />
179
- <Tab
180
- type="C"
181
- size="standard"
182
- label="disabled"
183
- value={4}
184
- disabled
185
- orientation="vertical"
186
- />
187
- </Tabs>
158
+ >
159
+ <Tab
160
+ type="C"
161
+ size="standard"
162
+ label="Item One"
163
+ value={1}
164
+ orientation="vertical"
165
+ />
166
+ <Tab
167
+ type="C"
168
+ size="standard"
169
+ label="Item Two"
170
+ value={2}
171
+ orientation="vertical"
172
+ />
173
+ <Tab
174
+ type="C"
175
+ size="standard"
176
+ label="Item Three"
177
+ value={3}
178
+ orientation="vertical"
179
+ />
180
+ <Tab
181
+ type="C"
182
+ size="standard"
183
+ label="disabled"
184
+ value={4}
185
+ disabled
186
+ orientation="vertical"
187
+ />
188
+ </Tabs>
189
+ </div>
188
190
  </div>
189
191
  <div style={{ width: "fit-content" }}>
190
192
  Standard: D