uikit-react-public 0.39.0 → 0.40.0

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.
Files changed (70) hide show
  1. package/dist/components/Accordion/Accordion.Heading.d.ts +2 -1
  2. package/dist/components/Accordion/Accordion.d.ts +8 -2
  3. package/dist/components/Accordion/Accordion.stories.d.ts +2 -2
  4. package/dist/components/Alert/Alert.d.ts +2 -2
  5. package/dist/components/Alert/Alert.stories.d.ts +2 -2
  6. package/dist/components/Alert/AlertMessage.d.ts +1 -1
  7. package/dist/components/Alert/AlertTitle.d.ts +1 -1
  8. package/dist/components/ErrorDialog/ErrorDialog.d.ts +26 -0
  9. package/dist/components/ErrorDialog/ErrorDialog.stories.d.ts +14 -0
  10. package/dist/components/ErrorDialog/__tests__/ErrorDialog.test.d.ts +1 -0
  11. package/dist/components/ErrorDialog/index.d.ts +2 -0
  12. package/dist/components/Field/HelperText.d.ts +1 -1
  13. package/dist/components/index.d.ts +2 -0
  14. package/dist/index.js +6648 -6322
  15. package/lib/components/Accordion/Accordion.Heading.tsx +24 -8
  16. package/lib/components/Accordion/Accordion.Panel.tsx +7 -4
  17. package/lib/components/Accordion/Accordion.stories.tsx +8 -8
  18. package/lib/components/Accordion/Accordion.tsx +17 -23
  19. package/lib/components/Accordion/__tests__/__snapshots__/Accordion.test.tsx.snap +14 -14
  20. package/lib/components/Alert/Alert.tsx +0 -1
  21. package/lib/components/Alert/AlertMessage.tsx +10 -14
  22. package/lib/components/Alert/AlertTitle.tsx +11 -8
  23. package/lib/components/Alert/__tests__/__snapshots__/Alert.test.tsx.snap +29 -14
  24. package/lib/components/AppHeader/AppHeader.tsx +0 -1
  25. package/lib/components/AppHeader/__tests__/__snapshots__/AppHeader.test.tsx.snap +1 -1
  26. package/lib/components/AppMenu/AppMenu.tsx +2 -9
  27. package/lib/components/AppMenu/AppMenuItem.tsx +6 -2
  28. package/lib/components/AppMenu/__tests__/__snapshots__/AppMenu.test.tsx.snap +6 -6
  29. package/lib/components/Breadcrumbs/Breadcrumbs.tsx +0 -4
  30. package/lib/components/Breadcrumbs/__tests__/__snapshots__/Breadcrumbs.test.tsx.snap +10 -10
  31. package/lib/components/Calendar/__tests__/__snapshots__/Calendar.test.tsx.snap +85 -85
  32. package/lib/components/Calendar/subcomponents/Day.tsx +7 -3
  33. package/lib/components/Datepicker/__tests__/__snapshots__/Datepicker.test.tsx.snap +2 -2
  34. package/lib/components/Datepicker/subcomponents/DatepickerInput.tsx +6 -3
  35. package/lib/components/Dialog/BaseDialog.tsx +6 -2
  36. package/lib/components/ErrorDialog/ErrorDialog.stories.tsx +85 -0
  37. package/lib/components/ErrorDialog/ErrorDialog.tsx +433 -0
  38. package/lib/components/ErrorDialog/__tests__/ErrorDialog.test.tsx +145 -0
  39. package/lib/components/ErrorDialog/index.tsx +2 -0
  40. package/lib/components/Field/HelperText.tsx +9 -10
  41. package/lib/components/Footer/__tests__/__snapshots__/Footer.test.tsx.snap +25 -25
  42. package/lib/components/FooterNew/__tests__/__snapshots__/Footer.test.tsx.snap +28 -28
  43. package/lib/components/Header/Header.tsx +6 -3
  44. package/lib/components/Header/__tests__/__snapshots__/Header.test.tsx.snap +4 -4
  45. package/lib/components/HeaderNew/Header.tsx +0 -1
  46. package/lib/components/HeaderNew/__tests__/__snapshots__/Header.test.tsx.snap +2 -2
  47. package/lib/components/Input/Input.tsx +6 -3
  48. package/lib/components/Input/__tests__/__snapshots__/Input.test.tsx.snap +4 -4
  49. package/lib/components/Label/Label.tsx +7 -7
  50. package/lib/components/Label/__tests__/__snapshots__/Label.test.tsx.snap +7 -7
  51. package/lib/components/Link/BaseLink.tsx +8 -16
  52. package/lib/components/Link/__tests__/__snapshots__/link.test.tsx.snap +2 -2
  53. package/lib/components/Menu/Menu.tsx +0 -4
  54. package/lib/components/Menu/MenuItem.tsx +7 -5
  55. package/lib/components/Search/__tests__/__snapshots__/Search.test.tsx.snap +2 -2
  56. package/lib/components/Select/__tests__/__snapshots__/Select.test.tsx.snap +2 -2
  57. package/lib/components/Select/subcomponents/CustomOption.tsx +6 -3
  58. package/lib/components/Select/subcomponents/CustomSelect.tsx +6 -2
  59. package/lib/components/Select/subcomponents/NativeSelect.tsx +6 -3
  60. package/lib/components/Select/subcomponents/VisibleField.tsx +6 -1
  61. package/lib/components/Sidebar/Sidebar.tsx +0 -1
  62. package/lib/components/Sidebar/__tests__/__snapshots__/Sidebar.test.tsx.snap +2 -2
  63. package/lib/components/StandaloneLink/__tests__/__snapshots__/StandaloneLink.test.tsx.snap +2 -2
  64. package/lib/components/Textarea/Textarea.tsx +6 -3
  65. package/lib/components/Textarea/__tests__/__snapshots__/Textarea.test.tsx.snap +4 -4
  66. package/lib/components/Timepicker/Timepicker.tsx +6 -2
  67. package/lib/components/Timepicker/__tests__/__snapshots__/Timepicker.test.tsx.snap +2 -2
  68. package/lib/components/WeekPicker/subcomponents/DatepickerInput.tsx +6 -3
  69. package/lib/components/index.ts +3 -0
  70. package/package.json +1 -1
@@ -25,6 +25,7 @@ const Day = ({
25
25
  events = [],
26
26
  }: DayProps) => {
27
27
  const [theme] = useTheme();
28
+ const { md, mdSemibold } = theme.typography.body;
28
29
 
29
30
  // More than 3 dots displayed breaks the layout
30
31
  const displayedEvents = events.slice(0, 3);
@@ -79,9 +80,12 @@ const Day = ({
79
80
  `;
80
81
 
81
82
  const foregroundStyle = css`
82
- font-family: ${theme.font.family.primary};
83
+ font-family: ${md.fontFamily};
84
+ font-feature-settings: ${md.fontSettings};
83
85
  user-select: none;
84
- font-size: 16px;
86
+ font-size: ${md.fontSize}px;
87
+ font-weight: ${md.fontWeight};
88
+ line-height: ${md.lineHeight}%;
85
89
 
86
90
  ${
87
91
  !isInCurrentMonth &&
@@ -92,7 +96,7 @@ const Day = ({
92
96
  ${
93
97
  isToday &&
94
98
  css`
95
- font-weight: 700;
99
+ font-weight: ${mdSemibold.fontWeight};
96
100
  `
97
101
  }
98
102
  ${
@@ -10,7 +10,7 @@ exports[`Datepicker > Snapshot: no date provided 1`] = `
10
10
  data-testid="ucl-uikit-datepicker__visible-field"
11
11
  >
12
12
  <input
13
- class="ucl-uikit-datepicker__input css-1xl7vs4"
13
+ class="ucl-uikit-datepicker__input css-19gsrgi"
14
14
  data-testid="ucl-uikit-datepicker__input"
15
15
  inputmode="numeric"
16
16
  placeholder="DD/MM/YYYY"
@@ -65,7 +65,7 @@ exports[`Datepicker > Snapshot: with date provided 1`] = `
65
65
  data-testid="ucl-uikit-datepicker__visible-field"
66
66
  >
67
67
  <input
68
- class="ucl-uikit-datepicker__input css-1xl7vs4"
68
+ class="ucl-uikit-datepicker__input css-19gsrgi"
69
69
  data-testid="ucl-uikit-datepicker__input"
70
70
  inputmode="numeric"
71
71
  placeholder="DD/MM/YYYY"
@@ -38,6 +38,7 @@ const DatepickerInput = ({
38
38
  ...props
39
39
  }: DatepickerInputProps) => {
40
40
  const [theme] = useTheme();
41
+ const { md } = theme.typography.body;
41
42
 
42
43
  const baseStyle = css`
43
44
  width: 100%;
@@ -46,9 +47,11 @@ const DatepickerInput = ({
46
47
  border: none;
47
48
  color: ${theme.colour.text.default};
48
49
  background-color: ${theme.colour.fill.inverse};
49
- font-family: ${theme.font.family.primary};
50
- font-size: ${theme.font.size.f16};
51
- letter-spacing: 1px;
50
+ font-family: ${md.fontFamily};
51
+ font-feature-settings: ${md.fontSettings};
52
+ font-size: ${md.fontSize}px;
53
+ font-weight: ${md.fontWeight};
54
+ line-height: ${md.lineHeight}%;
52
55
  caret-color: ${theme.colour.text.default};
53
56
 
54
57
  &:focus {
@@ -63,6 +63,7 @@ const BaseDialog = forwardRef<HTMLDialogElement, BaseDialogProps>(
63
63
  }[size];
64
64
 
65
65
  const [theme] = useTheme();
66
+ const { md } = theme.typography.body;
66
67
 
67
68
  const dialogRef = useRef<HTMLDialogElement>(null);
68
69
  const previousActiveElement = useRef<HTMLElement | null>(null);
@@ -179,8 +180,11 @@ const BaseDialog = forwardRef<HTMLDialogElement, BaseDialogProps>(
179
180
  border: none;
180
181
  background: ${theme.colour.surface.default};
181
182
  color: ${theme.colour.text.default};
182
- font-family: ${theme.font.family.primary};
183
- font-size: ${theme.font.size.f16};
183
+ font-family: ${md.fontFamily};
184
+ font-feature-settings: ${md.fontSettings};
185
+ font-size: ${md.fontSize}px;
186
+ font-weight: ${md.fontWeight};
187
+ line-height: ${md.lineHeight}%;
184
188
  width: 100vw;
185
189
  height: 100vh;
186
190
  margin: auto;
@@ -0,0 +1,85 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import ErrorDialog from './ErrorDialog';
3
+
4
+ const meta = {
5
+ title: 'Components/ErrorDialog',
6
+ component: ErrorDialog,
7
+ parameters: {
8
+ docs: {
9
+ story: 'Default',
10
+ },
11
+ },
12
+ } satisfies Meta<typeof ErrorDialog>;
13
+
14
+ export default meta;
15
+
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ export const Default: Story = {
19
+ args: {
20
+ open: true,
21
+ modal: true,
22
+ heading: 'Something went wrong',
23
+ errorCode: '1601',
24
+ referenceCode: 'UCLCID-20e935c3',
25
+ children:
26
+ 'We couldn’t complete your request right now. Please try again in a moment.',
27
+ details: [
28
+ {
29
+ label: 'HTTP Status Code',
30
+ value: 'HTTP status code: 404 (Not Found)',
31
+ },
32
+ {
33
+ label: 'Online Status',
34
+ value: 'Network connection: online',
35
+ },
36
+ {
37
+ label: 'Username',
38
+ value: 'username: j.bloggs',
39
+ },
40
+ {
41
+ label: 'Login Time',
42
+ value: 'login time: Fri Jan 16 2026 10:01:43 GMT+0000',
43
+ },
44
+ ],
45
+ primaryButton: 'Primary',
46
+ },
47
+ };
48
+
49
+ export const LongContent: Story = {
50
+ args: {
51
+ open: true,
52
+ modal: true,
53
+ heading: 'The file could not be uploaded',
54
+ errorCode: '1601',
55
+ referenceCode: 'UCLCID-20e935c3',
56
+ details: [
57
+ {
58
+ label: 'HTTP Status Code',
59
+ value:
60
+ 'HTTP status code: 404 (Not Found)\nAPI request method: GET\nNetwork connection: online',
61
+ },
62
+ {
63
+ label: 'User Details',
64
+ value:
65
+ 'username: j.bloggs\nlogin time: Fri Jan 16 2026 10:01:43 GMT+0000\nonline status: online',
66
+ },
67
+ {
68
+ label: 'Request Details',
69
+ value:
70
+ 'path: /rooms/201/312\nrequest URL: https://timetable.mesh.ucl.ac.uk/online-timetable-api-proxy/v0.1/location',
71
+ },
72
+ {
73
+ label: 'Browser Details',
74
+ value: 'browser: Chrome\nOS: macOS\ndevice type: desktop',
75
+ },
76
+ ],
77
+ children: Array(20)
78
+ .fill(
79
+ 'The upload service did not respond before the request timed out. Check the file and try again.'
80
+ )
81
+ .join(' '),
82
+ primaryButton: 'Primary',
83
+ footerDivider: true,
84
+ },
85
+ };
@@ -0,0 +1,433 @@
1
+ import { ReactNode, useCallback, useId } from 'react';
2
+ import { css, cx } from '@emotion/css';
3
+ import useTheme from '../../theme/useTheme';
4
+ import BaseDialog, { BaseDialogProps } from '../Dialog/BaseDialog';
5
+ import Heading from '../Heading';
6
+ import Paragraph from '../Paragraph';
7
+ import Text from '../Text';
8
+ import Button, { ButtonProps } from '../Button';
9
+ import Icon from '../Icon';
10
+ import IconButton, { IconButtonProps } from '../IconButton';
11
+ import Accordion from '../Accordion';
12
+
13
+ export const NAME = 'ucl-uikit-error-dialog';
14
+
15
+ export interface ErrorDialogDetail {
16
+ label: string;
17
+ value: string;
18
+ }
19
+
20
+ export interface ErrorDialogProps extends BaseDialogProps {
21
+ heading: ReactNode;
22
+ errorCode?: ReactNode;
23
+ referenceCode?: ReactNode;
24
+ details?: ErrorDialogDetail[];
25
+ primaryButton?: ReactNode;
26
+ secondaryButton?: ReactNode;
27
+ primaryButtonProps?: ButtonProps;
28
+ secondaryButtonProps?: ButtonProps;
29
+ closeButtonProps?: IconButtonProps;
30
+ closeButtonAriaLabel?: string;
31
+ footerDivider?: boolean;
32
+ onAction?: () => void;
33
+ onSecondaryAction?: () => void;
34
+ }
35
+
36
+ const ErrorDialog = ({
37
+ heading,
38
+ errorCode,
39
+ referenceCode,
40
+ details = [],
41
+ primaryButton = 'Close',
42
+ secondaryButton,
43
+ primaryButtonProps,
44
+ secondaryButtonProps,
45
+ closeButtonProps,
46
+ closeButtonAriaLabel = 'Close error dialog',
47
+ footerDivider = false,
48
+ onAction,
49
+ onSecondaryAction,
50
+ onClose,
51
+ size = 'medium',
52
+ modal = true,
53
+ role = 'alertdialog',
54
+ testId = NAME,
55
+ className,
56
+ children,
57
+ ...props
58
+ }: ErrorDialogProps) => {
59
+ const [theme] = useTheme();
60
+ const dialogHeaderId = useId();
61
+ const dialogBodyId = useId();
62
+ const hasBody = children !== undefined && children !== null;
63
+ const hasErrorCode = errorCode !== undefined && errorCode !== null;
64
+ const hasReferenceCode =
65
+ referenceCode !== undefined && referenceCode !== null;
66
+ const hasFooterMetadata = hasErrorCode || hasReferenceCode;
67
+ const hasDetails = details.length > 0;
68
+ const hasPrimaryButton =
69
+ primaryButton !== undefined && primaryButton !== null;
70
+ const hasSecondaryButton =
71
+ secondaryButton !== undefined && secondaryButton !== null;
72
+ const hasFooterActions = hasPrimaryButton || hasSecondaryButton;
73
+
74
+ const handleCloseButtonClick = useCallback(
75
+ (ev: React.MouseEvent<HTMLButtonElement>) => {
76
+ closeButtonProps?.onClick?.(ev);
77
+
78
+ if (!ev.defaultPrevented) {
79
+ onClose?.(ev);
80
+ }
81
+ },
82
+ [closeButtonProps, onClose]
83
+ );
84
+
85
+ const handlePrimaryButtonClick = useCallback(
86
+ (ev: React.MouseEvent<HTMLButtonElement>) => {
87
+ primaryButtonProps?.onClick?.(ev);
88
+
89
+ if (ev.defaultPrevented) return;
90
+
91
+ if (onAction) {
92
+ onAction();
93
+ } else {
94
+ onClose?.(ev);
95
+ }
96
+ },
97
+ [onAction, onClose, primaryButtonProps]
98
+ );
99
+
100
+ const handleSecondaryButtonClick = useCallback(
101
+ (ev: React.MouseEvent<HTMLButtonElement>) => {
102
+ secondaryButtonProps?.onClick?.(ev);
103
+
104
+ if (ev.defaultPrevented) return;
105
+
106
+ if (onSecondaryAction) {
107
+ onSecondaryAction();
108
+ } else {
109
+ onClose?.(ev);
110
+ }
111
+ },
112
+ [onClose, onSecondaryAction, secondaryButtonProps]
113
+ );
114
+
115
+ const baseStyle = css`
116
+ box-shadow: ${theme.boxShadow.x2y4};
117
+ border-top: ${theme.padding.p4} solid ${theme.colour.border.critical};
118
+ display: flex;
119
+ flex-direction: column;
120
+ overflow: hidden;
121
+ `;
122
+
123
+ const style = cx(NAME, baseStyle, className);
124
+
125
+ const headerBaseStyle = css`
126
+ flex: 0 0 auto;
127
+ display: flex;
128
+ align-items: flex-start;
129
+ gap: ${theme.padding.p8};
130
+ padding: ${theme.padding.p24} ${theme.padding.p24} 0;
131
+
132
+ @media (min-width: ${theme.breakpoints.tablet}px) {
133
+ padding: ${theme.padding.p32} ${theme.padding.p32} 0;
134
+ }
135
+ `;
136
+
137
+ const headerStyle = cx(`${NAME}__header`, headerBaseStyle);
138
+
139
+ const headerContentBaseStyle = css`
140
+ flex: 1 1 auto;
141
+ min-width: 0;
142
+ display: flex;
143
+ flex-direction: column;
144
+ align-items: flex-start;
145
+ gap: ${theme.padding.p16};
146
+ `;
147
+
148
+ const headerContentStyle = cx(
149
+ `${NAME}__header-content`,
150
+ headerContentBaseStyle
151
+ );
152
+
153
+ const closeButtonBaseStyle = css`
154
+ flex: 0 0 auto;
155
+ color: ${theme.colour.icon.secondary};
156
+ `;
157
+
158
+ const closeButtonStyle = cx(
159
+ `${NAME}__close-button`,
160
+ closeButtonBaseStyle,
161
+ closeButtonProps?.className
162
+ );
163
+
164
+ const iconBaseStyle = css`
165
+ color: ${theme.colour.icon.critical};
166
+ flex: 0 0 auto;
167
+ `;
168
+
169
+ const iconStyle = cx(`${NAME}__icon`, iconBaseStyle);
170
+
171
+ const bodyBaseStyle = css`
172
+ flex: 1 1 auto;
173
+ min-height: 0;
174
+ overflow-y: auto;
175
+ padding: ${theme.padding.p16} ${theme.padding.p24};
176
+
177
+ @media (min-width: ${theme.breakpoints.tablet}px) {
178
+ padding: ${theme.padding.p16} ${theme.padding.p32};
179
+ }
180
+ `;
181
+
182
+ const bodyStyle = cx(`${NAME}__body`, bodyBaseStyle);
183
+
184
+ const bodyContentBaseStyle = css`
185
+ display: flex;
186
+ flex-direction: column;
187
+ gap: ${theme.padding.p8};
188
+ `;
189
+
190
+ const bodyContentStyle = cx(`${NAME}__body-content`, bodyContentBaseStyle);
191
+
192
+ const detailsBaseStyle = css`
193
+ display: flex;
194
+ flex-direction: column;
195
+ gap: ${theme.padding.p8};
196
+ margin-top: ${theme.margin.m16};
197
+ `;
198
+
199
+ const detailsStyle = cx(`${NAME}__details`, detailsBaseStyle);
200
+
201
+ const detailAccordionBaseStyle = css`
202
+ width: 100%;
203
+ max-width: none;
204
+ `;
205
+
206
+ const detailAccordionStyle = cx(`${NAME}__detail`, detailAccordionBaseStyle);
207
+
208
+ const detailValueBaseStyle = css`
209
+ white-space: pre-line;
210
+ `;
211
+
212
+ const detailValueStyle = cx(`${NAME}__detail-value`, detailValueBaseStyle);
213
+
214
+ const errorCodeBaseStyle = css`
215
+ color: ${theme.colour.text.default};
216
+ opacity: 0.9;
217
+ `;
218
+
219
+ const errorCodeStyle = cx(`${NAME}__error-code`, errorCodeBaseStyle);
220
+
221
+ const referenceCodeBaseStyle = css`
222
+ color: ${theme.colour.text.default};
223
+ opacity: 0.9;
224
+ `;
225
+
226
+ const referenceCodeStyle = cx(
227
+ `${NAME}__reference-code`,
228
+ referenceCodeBaseStyle
229
+ );
230
+
231
+ const footerBaseStyle = css`
232
+ flex: 0 0 auto;
233
+ display: flex;
234
+ flex-direction: column-reverse;
235
+ gap: ${theme.padding.p12};
236
+ padding: ${theme.padding.p16} ${theme.padding.p24} ${theme.padding.p24};
237
+
238
+ @media (min-width: ${theme.breakpoints.tablet}px) {
239
+ flex-direction: row;
240
+ justify-content: space-between;
241
+ align-items: center;
242
+ padding: ${theme.padding.p16} ${theme.padding.p32} ${theme.padding.p32};
243
+ }
244
+ `;
245
+
246
+ const footerDividerStyle = css`
247
+ border-top: ${theme.border.b1} solid ${theme.colour.border.default};
248
+ `;
249
+
250
+ const footerStyle = cx(
251
+ `${NAME}__footer`,
252
+ footerBaseStyle,
253
+ footerDivider && footerDividerStyle
254
+ );
255
+
256
+ const footerMetadataBaseStyle = css`
257
+ display: flex;
258
+ flex-direction: column;
259
+ gap: ${theme.padding.p4};
260
+ `;
261
+
262
+ const footerMetadataStyle = cx(
263
+ `${NAME}__footer-metadata`,
264
+ footerMetadataBaseStyle
265
+ );
266
+
267
+ const footerActionsBaseStyle = css`
268
+ display: flex;
269
+ flex-direction: column-reverse;
270
+ gap: ${theme.padding.p12};
271
+
272
+ @media (min-width: ${theme.breakpoints.tablet}px) {
273
+ flex-direction: row;
274
+ justify-content: flex-end;
275
+ align-items: center;
276
+ }
277
+ `;
278
+
279
+ const footerActionsStyle = cx(
280
+ `${NAME}__footer-actions`,
281
+ footerActionsBaseStyle
282
+ );
283
+
284
+ const buttonBaseStyle = css`
285
+ width: 100%;
286
+
287
+ @media (min-width: ${theme.breakpoints.tablet}px) {
288
+ width: auto;
289
+ }
290
+ `;
291
+
292
+ const primaryButtonStyle = cx(
293
+ `${NAME}__primary-button`,
294
+ buttonBaseStyle,
295
+ primaryButtonProps?.className
296
+ );
297
+
298
+ const secondaryButtonStyle = cx(
299
+ `${NAME}__secondary-button`,
300
+ buttonBaseStyle,
301
+ secondaryButtonProps?.className
302
+ );
303
+
304
+ return (
305
+ <BaseDialog
306
+ size={size}
307
+ modal={modal}
308
+ role={role}
309
+ aria-labelledby={dialogHeaderId}
310
+ aria-describedby={dialogBodyId}
311
+ onClose={onClose}
312
+ testId={testId}
313
+ className={style}
314
+ skipCloseOnInitialFocus={true}
315
+ {...props}
316
+ >
317
+ <header className={headerStyle}>
318
+ <div className={headerContentStyle}>
319
+ <Icon.AlertCircle
320
+ size={32}
321
+ className={iconStyle}
322
+ role='presentation'
323
+ aria-hidden='true'
324
+ />
325
+ <Heading
326
+ id={dialogHeaderId}
327
+ level='lg'
328
+ as='h3'
329
+ noMargins
330
+ >
331
+ {heading}
332
+ </Heading>
333
+ </div>
334
+ <IconButton
335
+ aria-label={closeButtonAriaLabel}
336
+ {...closeButtonProps}
337
+ className={closeButtonStyle}
338
+ onClick={handleCloseButtonClick}
339
+ >
340
+ <Icon.X
341
+ role='presentation'
342
+ aria-hidden='true'
343
+ />
344
+ </IconButton>
345
+ </header>
346
+ <div
347
+ id={dialogBodyId}
348
+ className={bodyStyle}
349
+ role='document'
350
+ >
351
+ <div className={bodyContentStyle}>
352
+ {hasBody && (
353
+ <Paragraph
354
+ as='div'
355
+ level='md'
356
+ noMargins
357
+ >
358
+ {children}
359
+ </Paragraph>
360
+ )}
361
+ {hasDetails && (
362
+ <div className={detailsStyle}>
363
+ {details.map(({ label, value }, index) => (
364
+ <Accordion
365
+ key={`${label}-${index}`}
366
+ className={detailAccordionStyle}
367
+ >
368
+ <Accordion.Heading>{label}</Accordion.Heading>
369
+ <Accordion.Panel>
370
+ <span className={detailValueStyle}>{value}</span>
371
+ </Accordion.Panel>
372
+ </Accordion>
373
+ ))}
374
+ </div>
375
+ )}
376
+ </div>
377
+ </div>
378
+ {(hasFooterActions || hasFooterMetadata) && (
379
+ <footer
380
+ className={footerStyle}
381
+ aria-label='Error dialog actions'
382
+ >
383
+ {hasFooterMetadata && (
384
+ <div className={footerMetadataStyle}>
385
+ {hasErrorCode && (
386
+ <Text
387
+ className={errorCodeStyle}
388
+ level='md-semibold'
389
+ >
390
+ Error code: &nbsp; {errorCode}
391
+ </Text>
392
+ )}
393
+ {hasReferenceCode && (
394
+ <Text
395
+ className={referenceCodeStyle}
396
+ level='md-semibold'
397
+ >
398
+ Reference code: &nbsp; {referenceCode}
399
+ </Text>
400
+ )}
401
+ </div>
402
+ )}
403
+ {hasFooterActions && (
404
+ <div className={footerActionsStyle}>
405
+ {hasSecondaryButton && (
406
+ <Button
407
+ variant='secondary-destructive'
408
+ {...secondaryButtonProps}
409
+ className={secondaryButtonStyle}
410
+ onClick={handleSecondaryButtonClick}
411
+ >
412
+ {secondaryButton}
413
+ </Button>
414
+ )}
415
+ {hasPrimaryButton && (
416
+ <Button
417
+ variant='primary-destructive'
418
+ {...primaryButtonProps}
419
+ className={primaryButtonStyle}
420
+ onClick={handlePrimaryButtonClick}
421
+ >
422
+ {primaryButton}
423
+ </Button>
424
+ )}
425
+ </div>
426
+ )}
427
+ </footer>
428
+ )}
429
+ </BaseDialog>
430
+ );
431
+ };
432
+
433
+ export default ErrorDialog;