@scottish-government/designsystem-react 1.0.1 → 1.1.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 (83) hide show
  1. package/.storybook/sgdsArgTypes.ts +19 -3
  2. package/CHANGELOG.md +19 -0
  3. package/dist/common/AbstractNotificationMessage/AbstractNotificationMessage.d.ts +6 -0
  4. package/dist/common/AbstractNotificationMessage/AbstractNotificationMessage.jsx +34 -0
  5. package/dist/common/AbstractNotificationMessage/index.d.ts +1 -0
  6. package/dist/common/AbstractNotificationMessage/index.js +8 -0
  7. package/dist/common/AbstractNotificationMessage/types.d.ts +9 -0
  8. package/dist/common/AbstractNotificationMessage/types.js +2 -0
  9. package/dist/common/WrapperTag/WrapperTag.d.ts +11 -11
  10. package/dist/components/ConfirmationMessage/ConfirmationMessage.d.ts +18 -2
  11. package/dist/components/ConfirmationMessage/ConfirmationMessage.jsx +21 -17
  12. package/dist/components/ConfirmationNotification/ConfirmationNotification.d.ts +6 -0
  13. package/dist/components/ConfirmationNotification/ConfirmationNotification.jsx +26 -0
  14. package/dist/components/ConfirmationNotification/index.d.ts +1 -0
  15. package/dist/components/ConfirmationNotification/index.js +8 -0
  16. package/dist/components/ErrorNotification/ErrorNotification.d.ts +6 -0
  17. package/dist/components/ErrorNotification/ErrorNotification.jsx +26 -0
  18. package/dist/components/ErrorNotification/index.d.ts +1 -0
  19. package/dist/components/ErrorNotification/index.js +8 -0
  20. package/dist/components/FileUpload/FileUpload.d.ts +6 -0
  21. package/dist/components/FileUpload/FileUpload.jsx +55 -0
  22. package/dist/components/FileUpload/index.d.ts +1 -0
  23. package/dist/components/FileUpload/index.js +8 -0
  24. package/dist/components/FileUpload/types.d.ts +24 -0
  25. package/dist/components/FileUpload/types.js +2 -0
  26. package/dist/components/InfoNotification/InfoNotification.d.ts +6 -0
  27. package/dist/components/InfoNotification/InfoNotification.jsx +26 -0
  28. package/dist/components/InfoNotification/index.d.ts +1 -0
  29. package/dist/components/InfoNotification/index.js +8 -0
  30. package/dist/components/NotificationBanner/NotificationBanner.jsx +1 -1
  31. package/dist/components/WarningNotification/WarningNotification.d.ts +6 -0
  32. package/dist/components/WarningNotification/WarningNotification.jsx +26 -0
  33. package/dist/components/WarningNotification/index.d.ts +1 -0
  34. package/dist/components/WarningNotification/index.js +8 -0
  35. package/dist/components/WarningText/WarningText.jsx +4 -1
  36. package/dist/images/icons/index.d.ts +2 -0
  37. package/dist/images/icons/index.js +5 -1
  38. package/dist/images/icons/info.d.ts +4 -0
  39. package/dist/images/icons/info.jsx +40 -0
  40. package/dist/images/icons/warning.d.ts +4 -0
  41. package/dist/images/icons/warning.jsx +40 -0
  42. package/dist/shared-types.d.ts +1 -1
  43. package/dist/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +22 -16
  45. package/src/common/AbstractNotificationMessage/AbstractNotificationMessage.test.tsx +101 -0
  46. package/src/common/AbstractNotificationMessage/AbstractNotificationMessage.tsx +56 -0
  47. package/src/common/AbstractNotificationMessage/index.ts +1 -0
  48. package/src/common/AbstractNotificationMessage/types.ts +10 -0
  49. package/src/components/Button/Button.stories.tsx +1 -1
  50. package/src/components/ConfirmationMessage/ConfirmationMessage.stories.tsx +11 -3
  51. package/src/components/ConfirmationMessage/ConfirmationMessage.test.tsx +58 -23
  52. package/src/components/ConfirmationMessage/ConfirmationMessage.tsx +32 -25
  53. package/src/components/ConfirmationMessage/confirmationmessage.mdx +17 -0
  54. package/src/components/ConfirmationNotification/ConfirmationNotification.stories.tsx +47 -0
  55. package/src/components/ConfirmationNotification/ConfirmationNotification.test.tsx +101 -0
  56. package/src/components/ConfirmationNotification/ConfirmationNotification.tsx +46 -0
  57. package/src/components/ConfirmationNotification/index.ts +1 -0
  58. package/src/components/ErrorNotification/ErrorNotification.stories.tsx +47 -0
  59. package/src/components/ErrorNotification/ErrorNotification.test.tsx +101 -0
  60. package/src/components/ErrorNotification/ErrorNotification.tsx +46 -0
  61. package/src/components/ErrorNotification/index.ts +1 -0
  62. package/src/components/FileDownload/FileDownload.test.tsx +1 -1
  63. package/src/components/FileUpload/FileUpload.stories.tsx +77 -0
  64. package/src/components/FileUpload/FileUpload.test.tsx +185 -0
  65. package/src/components/FileUpload/FileUpload.tsx +87 -0
  66. package/src/components/FileUpload/index.ts +1 -0
  67. package/src/components/FileUpload/types.ts +25 -0
  68. package/src/components/InfoNotification/InfoNotification.stories.tsx +47 -0
  69. package/src/components/InfoNotification/InfoNotification.test.tsx +101 -0
  70. package/src/components/InfoNotification/InfoNotification.tsx +46 -0
  71. package/src/components/InfoNotification/index.ts +1 -0
  72. package/src/components/NotificationBanner/NotificationBanner.tsx +1 -1
  73. package/src/components/TextInput/TextInput.stories.tsx +1 -1
  74. package/src/components/WarningNotification/WarningNotification.stories.tsx +47 -0
  75. package/src/components/WarningNotification/WarningNotification.test.tsx +101 -0
  76. package/src/components/WarningNotification/WarningNotification.tsx +46 -0
  77. package/src/components/WarningNotification/index.ts +1 -0
  78. package/src/components/WarningText/WarningText.tsx +4 -1
  79. package/src/images/icons/index.ts +2 -0
  80. package/src/images/icons/info.tsx +14 -0
  81. package/src/images/icons/warning.tsx +14 -0
  82. package/src/shared-types.ts +1 -1
  83. package/src/components/ConfirmationMessage/types.ts +0 -7
@@ -0,0 +1,46 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import AbstractNotificationMessage from '../../common/AbstractNotificationMessage';
3
+ import DSNotificationMessage from '@scottish-government/design-system/src/components/notification-message/notification-message';
4
+ import { AbstractNotificationMessageProps } from '../../common/AbstractNotificationMessage/types';
5
+ import clsx from 'clsx';
6
+
7
+ const ConfirmationNotification = ({
8
+ ariaLive,
9
+ children,
10
+ className,
11
+ headingLevel = 'h3',
12
+ isDismissable,
13
+ title,
14
+ ...props
15
+ }: AbstractNotificationMessageProps) => {
16
+ const ref = useRef(null);
17
+
18
+ useEffect(() => {
19
+ /* istanbul ignore else */
20
+ if (ref.current) {
21
+ new DSNotificationMessage(ref.current).init();
22
+ }
23
+ }, [ref]);
24
+
25
+ return (
26
+ <AbstractNotificationMessage
27
+ ariaLive={ariaLive}
28
+ className={clsx([
29
+ 'ds_notification-message--success',
30
+ className
31
+ ])}
32
+ headingLevel={headingLevel}
33
+ icon="CheckCircle"
34
+ isDismissable={isDismissable}
35
+ ref={ref}
36
+ title={title}
37
+ {...props}
38
+ >
39
+ {children}
40
+ </AbstractNotificationMessage>
41
+ );
42
+ };
43
+
44
+ ConfirmationNotification.displayName = 'ConfirmationNotification';
45
+
46
+ export default ConfirmationNotification;
@@ -0,0 +1 @@
1
+ export { default } from './ConfirmationNotification';
@@ -0,0 +1,47 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import argTypes from '../../../.storybook/sgdsArgTypes';
3
+
4
+ import ErrorNotification from './ErrorNotification';
5
+
6
+ const meta = {
7
+ title: 'Components/NotificationMessage/ErrorNotification',
8
+ component: ErrorNotification,
9
+ argTypes: {
10
+ ariaLive: argTypes.ariaLive(),
11
+ children: argTypes.children(),
12
+ headingLevel: argTypes.headingLevel(),
13
+ icon: { table: { disable: true } },
14
+ isDismissable: argTypes.boolean()
15
+ },
16
+ args: {
17
+ ariaLive: 'polite',
18
+ children: (<p>That link does not seem to be working.<br />Please try again.</p>),
19
+ headingLevel: 'h3',
20
+ isDismissable: false,
21
+ title: 'Something went wrong',
22
+ }
23
+ } satisfies Meta<typeof ErrorNotification>;
24
+
25
+ export default meta;
26
+ type Story = StoryObj<typeof meta>;
27
+
28
+ export const Default: Story = {
29
+ };
30
+
31
+ export const NoChildren: Story = {
32
+ args: {
33
+ children: undefined
34
+ }
35
+ };
36
+
37
+ export const DifferentHeadingLevel: Story = {
38
+ args: {
39
+ headingLevel: 'h2'
40
+ }
41
+ };
42
+
43
+ export const Dismissable: Story = {
44
+ args: {
45
+ isDismissable: true
46
+ }
47
+ };
@@ -0,0 +1,101 @@
1
+ import { test, expect } from 'vitest';
2
+ import { render, screen, within } from '@testing-library/react';
3
+ import ErrorNotification from './ErrorNotification';
4
+
5
+ const NOTIFICATION_TEXT = 'Further details of the notification message';
6
+ const TITLE_TEXT = 'Important information';
7
+
8
+ test('error notification message renders correctly', () => {
9
+ render(
10
+ <ErrorNotification title={TITLE_TEXT}>
11
+ {NOTIFICATION_TEXT}
12
+ </ErrorNotification>
13
+ );
14
+
15
+ const container = document.querySelector('.ds_notification-message');
16
+ const heading = screen.getByRole('heading');
17
+ const content = heading.nextElementSibling;
18
+
19
+ expect(container?.ariaLive).toEqual('polite');
20
+
21
+ expect(heading.tagName).toEqual('H3');
22
+ expect(heading.textContent).toEqual(TITLE_TEXT)
23
+ expect(content?.textContent).toEqual(NOTIFICATION_TEXT)
24
+ });
25
+
26
+ test('error notification message with close button', () => {
27
+ render(
28
+ <ErrorNotification isDismissable>
29
+ {NOTIFICATION_TEXT}
30
+ </ErrorNotification>
31
+ );
32
+
33
+ const closeButton = screen.getByRole('button');
34
+ const closeButtonLabel = within(closeButton).getByText('Close this notification');
35
+ const closeButtonIcon = within(closeButton).getByRole('img', { hidden: true });
36
+
37
+ expect(closeButton).toHaveClass('ds_notification-message__close', 'js-close-notification-message');
38
+ expect(closeButton).toHaveAttribute('type', 'button');
39
+
40
+ expect(closeButtonLabel).toBeInTheDocument();
41
+ expect(closeButtonLabel).toHaveClass('visually-hidden');
42
+
43
+ expect(closeButtonIcon).toHaveClass('ds_icon', 'ds_icon--fill');
44
+ });
45
+
46
+ test('error notification message with icon', () => {
47
+ render(
48
+ <ErrorNotification icon="Search">
49
+ {NOTIFICATION_TEXT}
50
+ </ErrorNotification>
51
+ );
52
+
53
+
54
+ const notificationIcon = screen.getByRole('img', { hidden: true });
55
+
56
+ expect(notificationIcon).toHaveClass('ds_icon', 'ds_notification-message__icon', 'ds_icon--24');
57
+ expect(notificationIcon).toHaveAttribute('aria-hidden');
58
+ });
59
+
60
+ test("does not render body when no children specified", () => {
61
+ const { container } = render(<ErrorNotification title={TITLE_TEXT} />);
62
+
63
+ expect(
64
+ container.querySelector(".ds_notification-message__body"),
65
+ ).not.toBeInTheDocument();
66
+ });
67
+
68
+ test('error notification message with custom aria live and custom header level', () => {
69
+ render(
70
+ <ErrorNotification headingLevel="h2" ariaLive="assertive" title={TITLE_TEXT}/>
71
+ );
72
+
73
+ const container = document.querySelector('.ds_notification-message');
74
+ const heading = screen.getByRole('heading');
75
+
76
+ expect(container?.ariaLive).toEqual('assertive');
77
+
78
+ expect(heading.tagName).toEqual('H2');
79
+ });
80
+
81
+ test('passing additional props', () => {
82
+ render(
83
+ <ErrorNotification data-test="foo">
84
+ {NOTIFICATION_TEXT}
85
+ </ErrorNotification>
86
+ )
87
+
88
+ const container = document.querySelector('.ds_notification-message') as HTMLElement;
89
+ expect(container?.dataset.test).toEqual('foo');
90
+ });
91
+
92
+ test('passing additional CSS classes', () => {
93
+ render(
94
+ <ErrorNotification className="foo">
95
+ {NOTIFICATION_TEXT}
96
+ </ErrorNotification>
97
+ )
98
+
99
+ const container = document.querySelector('.ds_notification-message') as HTMLElement;
100
+ expect(container).toHaveClass('foo', 'ds_notification-message');
101
+ });
@@ -0,0 +1,46 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import AbstractNotificationMessage from '../../common/AbstractNotificationMessage';
3
+ import DSNotificationMessage from '@scottish-government/design-system/src/components/notification-message/notification-message';
4
+ import { AbstractNotificationMessageProps } from '../../common/AbstractNotificationMessage/types';
5
+ import clsx from 'clsx';
6
+
7
+ const ErrorNotification = ({
8
+ ariaLive,
9
+ children,
10
+ className,
11
+ headingLevel = 'h3',
12
+ isDismissable,
13
+ title,
14
+ ...props
15
+ }: AbstractNotificationMessageProps) => {
16
+ const ref = useRef(null);
17
+
18
+ useEffect(() => {
19
+ /* istanbul ignore else */
20
+ if (ref.current) {
21
+ new DSNotificationMessage(ref.current).init();
22
+ }
23
+ }, [ref]);
24
+
25
+ return (
26
+ <AbstractNotificationMessage
27
+ ariaLive={ariaLive}
28
+ className={clsx([
29
+ 'ds_notification-message--error',
30
+ className
31
+ ])}
32
+ headingLevel={headingLevel}
33
+ icon="Error"
34
+ isDismissable={isDismissable}
35
+ ref={ref}
36
+ title={title}
37
+ {...props}
38
+ >
39
+ {children}
40
+ </AbstractNotificationMessage>
41
+ );
42
+ };
43
+
44
+ ErrorNotification.displayName = 'ErrorNotification';
45
+
46
+ export default ErrorNotification;
@@ -0,0 +1 @@
1
+ export { default } from './ErrorNotification';
@@ -5,7 +5,7 @@ import FileDownload from './FileDownload';
5
5
  const FILE_TITLE = 'Scotland\'s Artificial Intelligence Strategy - Trustworthy, Ethical and Inclusive';
6
6
  const FILE_URL = 'my-file.file';
7
7
 
8
- test('inset text renders correctly', () => {
8
+ test('File download renders correctly', () => {
9
9
  render(
10
10
  <FileDownload fileUrl={FILE_URL} title={FILE_TITLE} data-testid="file-download" />
11
11
  );
@@ -0,0 +1,77 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import FileUpload from './FileUpload';
3
+ import argTypes from '../../../.storybook/sgdsArgTypes';
4
+ import { defaultText } from '@scottish-government/design-system/src/components/file-upload/file-upload';
5
+
6
+ const meta = {
7
+ title: 'Components/FileUpload',
8
+ component: FileUpload,
9
+ args: {
10
+ // capture?: boolean | 'user' | 'environment'
11
+ id: 'file-upload',
12
+ label: 'Upload a file',
13
+ text: JSON.parse(JSON.stringify(defaultText))
14
+ },
15
+ argTypes: {
16
+ accept: {
17
+ control: { type: 'text' },
18
+ description: 'Value of the "accept" attribute on the input element',
19
+ table: {
20
+ type: {
21
+ summary: 'string'
22
+ }
23
+ }
24
+ },
25
+ capture: argTypes.select({options: ['', 'user', 'environment']}),
26
+ errorMessage: argTypes.errorMessage(),
27
+ files: argTypes.hidden(),
28
+ hasError: argTypes.boolean(),
29
+ hintText: argTypes.hintText(),
30
+ id: argTypes.id(),
31
+ isMultiple: argTypes.boolean(),
32
+ label: argTypes.label(),
33
+ name: {type: 'string'},
34
+ text: argTypes.hidden()
35
+ }
36
+ } satisfies Meta<typeof FileUpload>;
37
+
38
+ export default meta;
39
+ type Story = StoryObj<typeof meta>;
40
+
41
+ export const Default: Story = {
42
+
43
+ };
44
+
45
+ export const Multiple: Story = {
46
+ args: {
47
+ isMultiple: true
48
+ }
49
+ };
50
+
51
+ export const KitchenSink: Story = {
52
+ args: {
53
+ errorMessage: 'Please select at least one file',
54
+ hasError: true,
55
+ hintText: 'The files must be no larger than 10MB each',
56
+ isMultiple: true,
57
+ label: 'Upload your files'
58
+ }
59
+ }
60
+
61
+ export const DifferentText: Story = {
62
+ args: {
63
+ label: 'Upload a banana 🍌',
64
+ text: {
65
+ buttonText: 'Choose a banana',
66
+ buttonTextPlural: 'Choose some bananas',
67
+ defaultStatusText: 'No banana chosen',
68
+ defaultStatusTextPlural: 'No bananas chosen',
69
+ enteredDropzone: 'Entered banana zone',
70
+ filesAddedText: '$NUMBER bananas',
71
+ filesListHeading: 'Bananas selected for upload',
72
+ instructionText: 'or drag and drop a banana here',
73
+ instructionTextPlural: 'or drag and drop some bananas here',
74
+ leftDropzone: 'Left banana zone'
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,185 @@
1
+ import { test, expect, vi } from 'vitest';
2
+ import { screen, render, fireEvent } from '@testing-library/react';
3
+ import FileUpload from './FileUpload';
4
+
5
+ const ID = 'my-file-upload'
6
+ const LABEL_TEXT = 'Upload a file';
7
+
8
+ test('file upload renders correctly', () => {
9
+ render(
10
+ <FileUpload data-testid="file-upload" id={ID} label={LABEL_TEXT} />
11
+ );
12
+
13
+ const fileUploadElement = screen.getByTestId('file-upload');
14
+ const label = screen.getByText(LABEL_TEXT);
15
+ const dropzoneButton = document.getElementById(ID + '-dropzone');
16
+ const statusSpan = document.getElementById(ID + '-status');
17
+ const commaSpan = document.getElementById(ID + '-comma');
18
+ const buttonContainer = document.querySelector('.ds_file-upload__button-container');
19
+ const pseudoButton = document.querySelector('.ds_file-upload__button');
20
+ const instructionSpan = document.getElementById(ID + '-instruction');
21
+ const fileInput = document.getElementById(ID);
22
+
23
+ expect(fileUploadElement).toHaveClass('ds_file-upload');
24
+ expect(fileUploadElement.tagName).toEqual('DIV');
25
+
26
+ expect(label).toHaveClass('ds_label');
27
+ expect(label).toHaveAttribute('for', ID);
28
+ expect(label).toHaveAttribute('id', ID + '-label');
29
+ expect(label.tagName).toEqual('LABEL');
30
+ expect(label.textContent).toEqual(LABEL_TEXT);
31
+ expect(label.parentElement).toEqual(fileUploadElement);
32
+
33
+ expect(dropzoneButton).toHaveClass('ds_file-upload__dropzone');
34
+ expect(dropzoneButton).toHaveAttribute('id', ID + '-dropzone');
35
+ expect(dropzoneButton).toHaveAttribute('type', 'button');
36
+ expect(dropzoneButton?.tagName).toEqual('BUTTON');
37
+ expect(dropzoneButton).toHaveAttribute('aria-labelledby', `${label.id} ${commaSpan?.id} ${dropzoneButton?.id}`);
38
+ expect(dropzoneButton?.parentElement).toEqual(fileUploadElement);
39
+ expect(dropzoneButton?.previousElementSibling).toEqual(label);
40
+
41
+ expect(statusSpan).toHaveClass('ds_file-upload__status');
42
+ expect(statusSpan).toHaveAttribute('aria-live', 'polite');
43
+ expect(statusSpan?.tagName).toEqual('SPAN');
44
+ expect(statusSpan?.textContent).toEqual('No file chosen');
45
+ expect(statusSpan?.parentElement).toEqual(dropzoneButton);
46
+
47
+ expect(commaSpan).toHaveClass('visually-hidden');
48
+ expect(commaSpan?.tagName).toEqual('SPAN');
49
+ expect(commaSpan?.parentElement).toEqual(dropzoneButton);
50
+ expect(commaSpan?.previousElementSibling).toEqual(statusSpan);
51
+
52
+ expect(buttonContainer?.tagName).toEqual('SPAN');
53
+ expect(buttonContainer?.parentElement).toEqual(dropzoneButton);
54
+ expect(buttonContainer?.previousElementSibling).toEqual(commaSpan);
55
+
56
+ expect(pseudoButton?.tagName).toEqual('SPAN');
57
+ expect(pseudoButton?.textContent).toEqual('Choose file');
58
+ expect(pseudoButton?.parentElement).toEqual(buttonContainer);
59
+
60
+ expect(instructionSpan).toHaveClass('ds_file-upload__instruction');
61
+ expect(instructionSpan?.tagName).toEqual('SPAN');
62
+ expect(instructionSpan?.textContent).toEqual('or drag and drop file here');
63
+ expect(instructionSpan?.parentElement).toEqual(buttonContainer);
64
+ expect(instructionSpan?.previousElementSibling).toEqual(pseudoButton);
65
+
66
+ expect(fileInput).not.toHaveAttribute('aria-describedby');
67
+ expect(fileInput).toHaveAttribute('aria-hidden', 'true');
68
+ expect(fileInput).toHaveClass('ds_file-upload__input');
69
+ expect(fileInput).toHaveAttribute('hidden', 'true');
70
+ expect(fileInput).toHaveAttribute('name', ID);
71
+ expect(fileInput).toHaveAttribute('tabindex', '-1');
72
+ expect(fileInput).toHaveAttribute('type', 'file');
73
+ });
74
+
75
+ test('multiple file upload', () => {
76
+ const LABEL_TEXT = 'Upload files';
77
+
78
+ render(
79
+ <FileUpload isMultiple data-testid="file-upload" id={ID} label={LABEL_TEXT} />
80
+ );
81
+
82
+ const label = screen.getByText(LABEL_TEXT);
83
+ const statusSpan = document.getElementById(ID + '-status');
84
+ const pseudoButton = document.querySelector('.ds_file-upload__button');
85
+ const instructionSpan = document.getElementById(ID + '-instruction');
86
+ const fileInput = document.getElementById(ID);
87
+
88
+ expect(label.textContent).toEqual(LABEL_TEXT);
89
+ expect(statusSpan?.textContent).toEqual('No files chosen');
90
+ expect(pseudoButton?.textContent).toEqual('Choose files');
91
+ expect(instructionSpan?.textContent).toEqual('or drag and drop files here');
92
+ expect(fileInput).toHaveAttribute('multiple');
93
+ });
94
+
95
+ test('with hint text', () => {
96
+ const HINT_TEXT = 'My hint text'
97
+
98
+ render(
99
+ <FileUpload hintText={HINT_TEXT} data-testid="file-upload" id={ID} label={LABEL_TEXT} />
100
+ );
101
+
102
+ const label = screen.getByText(LABEL_TEXT);
103
+ const hintText = document.getElementById('hint-text-' + ID);
104
+
105
+ expect(hintText).toBeInTheDocument();
106
+ expect(hintText).toHaveClass('ds_hint-text');
107
+ expect(hintText?.textContent).toEqual(HINT_TEXT);
108
+ expect(hintText?.previousElementSibling).toEqual(label);
109
+ });
110
+
111
+ test('with error text', () => {
112
+ const ERROR_TEXT = 'My error text'
113
+
114
+ render(
115
+ <FileUpload errorMessage={ERROR_TEXT} hasError data-testid="file-upload" id={ID} label={LABEL_TEXT} />
116
+ );
117
+
118
+ const label = screen.getByText(LABEL_TEXT);
119
+ const errorText = document.getElementById('error-message-' + ID);
120
+
121
+ expect(errorText).toBeInTheDocument();
122
+ expect(errorText).toHaveClass('ds_question__error-message');
123
+ expect(errorText?.textContent).toEqual(ERROR_TEXT);
124
+ expect(errorText?.previousElementSibling).toEqual(label);
125
+ });
126
+
127
+ test('with blur fn', () => {
128
+ const ONBLUR_FUNCTION = vi.fn();
129
+
130
+ render(
131
+ <FileUpload onBlur={ONBLUR_FUNCTION} id={ID} label={LABEL_TEXT} />
132
+ );
133
+
134
+ const fileInput = document.getElementById(ID);
135
+
136
+ if (fileInput) {
137
+ fireEvent.blur(fileInput);
138
+ }
139
+
140
+ expect(ONBLUR_FUNCTION).toHaveBeenCalled();
141
+ });
142
+
143
+ test('with change fn', () => {
144
+ const ONCHANGE_FUNCTION = vi.fn();
145
+
146
+ render(
147
+ <FileUpload onChange={ONCHANGE_FUNCTION} id={ID} label={LABEL_TEXT} />
148
+ );
149
+
150
+ const fileInput = document.getElementById(ID);
151
+
152
+ if (fileInput) {
153
+ fireEvent.change(fileInput);
154
+ }
155
+
156
+ expect(ONCHANGE_FUNCTION).toHaveBeenCalled();
157
+ });
158
+
159
+ test('passing additional props', () => {
160
+ render(
161
+ <FileUpload
162
+ data-testid="file-upload"
163
+ id={ID}
164
+ label={LABEL_TEXT}
165
+ data-test="foo"
166
+ />
167
+ );
168
+
169
+ const fileUploadElement = screen.getByTestId('file-upload');
170
+ expect(fileUploadElement?.dataset.test).toEqual('foo');
171
+ });
172
+
173
+ test('passing additional CSS classes', () => {
174
+ render(
175
+ <FileUpload
176
+ data-testid="file-upload"
177
+ id={ID}
178
+ label={LABEL_TEXT}
179
+ className="foo"
180
+ />
181
+ );
182
+
183
+ const fileUploadElement = screen.getByTestId('file-upload');
184
+ expect(fileUploadElement).toHaveClass('foo');
185
+ });
@@ -0,0 +1,87 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { FileUploadProps } from './types';
3
+ import ErrorMessage from '../ErrorMessage';
4
+ import HintText from '../../common/HintText';
5
+ import DSFileUpload from '@scottish-government/design-system/src/components/file-upload/file-upload'
6
+
7
+ const FileUpload = ({
8
+ accept,
9
+ capture,
10
+ className,
11
+ errorMessage,
12
+ hasError,
13
+ hintText,
14
+ id,
15
+ isMultiple,
16
+ label,
17
+ name,
18
+ onBlur,
19
+ onChange,
20
+ text,
21
+ ...props
22
+ }: FileUploadProps) => {
23
+ const ref = useRef(null);
24
+
25
+ const errorMessageId = `error-message-${id}`;
26
+ const hintTextId = `hint-text-${id}`;
27
+ const describedbys: string[] = [];
28
+
29
+ if (hintText) { describedbys.push(hintTextId) };
30
+ if (errorMessage) { describedbys.push(errorMessageId) };
31
+
32
+ const options = {
33
+ text: text
34
+ }
35
+
36
+ useEffect(() => {
37
+ if (ref.current) {
38
+ new DSFileUpload(ref.current, options).init();
39
+ }
40
+ }, [ref]);
41
+
42
+ function handleBlur(event: React.FocusEvent<HTMLInputElement>) {
43
+ if (typeof onBlur === 'function') {
44
+ onBlur(event);
45
+ }
46
+ }
47
+
48
+ function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
49
+ if (typeof onChange === 'function') {
50
+ onChange(event);
51
+ }
52
+ }
53
+
54
+ return (
55
+ <div className={[
56
+ 'ds_file-upload',
57
+ className
58
+ ].join(' ')}
59
+ ref={ref}
60
+ {...props}
61
+ >
62
+ <label className="ds_label" htmlFor={id}>{label}</label>
63
+ {hintText && <HintText id={hintTextId}>{hintText}</HintText>}
64
+ {errorMessage && <ErrorMessage id={errorMessageId}>{errorMessage}</ErrorMessage>}
65
+ <input
66
+ accept={accept}
67
+ aria-describedby={describedbys.length ? describedbys.join(' ') : undefined}
68
+ aria-invalid={hasError}
69
+ capture={capture}
70
+ className={[
71
+ 'ds_file-upload__input',
72
+ hasError ? 'ds_file-upload__input--error' : ''
73
+ ].join(' ')}
74
+ id={id}
75
+ multiple={isMultiple}
76
+ name={name || id}
77
+ onBlur={handleBlur}
78
+ onChange={handleChange}
79
+ type="file"
80
+ />
81
+ </div>
82
+ );
83
+ };
84
+
85
+ FileUpload.displayName = 'FileUpload';
86
+
87
+ export default FileUpload;
@@ -0,0 +1 @@
1
+ export { default } from './FileUpload';
@@ -0,0 +1,25 @@
1
+ import { FormFieldBase } from '../../shared-types';
2
+
3
+ type TextArgs = {
4
+ buttonText: string
5
+ buttonTextPlural: string
6
+ defaultStatusText: string
7
+ defaultStatusTextPlural: string
8
+ enteredDropzone: string
9
+ filesAddedText: string
10
+ filesListHeading: string
11
+ instructionText: string
12
+ instructionTextPlural: string
13
+ leftDropzone: string
14
+ }
15
+
16
+ export interface FileUploadProps extends FormFieldBase<HTMLElement> {
17
+ accept?: string,
18
+ capture?: boolean | 'user' | 'environment'
19
+ className?: string
20
+ files?: File[]
21
+ id: string
22
+ isMultiple?: boolean
23
+ name?: string
24
+ text?: Partial<TextArgs>
25
+ }
@@ -0,0 +1,47 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import argTypes from '../../../.storybook/sgdsArgTypes';
3
+
4
+ import InfoNotification from './InfoNotification';
5
+
6
+ const meta = {
7
+ title: 'Components/NotificationMessage/InfoNotification',
8
+ component: InfoNotification,
9
+ argTypes: {
10
+ ariaLive: argTypes.ariaLive(),
11
+ children: argTypes.children(),
12
+ headingLevel: argTypes.headingLevel(),
13
+ icon: { table: { disable: true } },
14
+ isDismissable: argTypes.boolean()
15
+ },
16
+ args: {
17
+ ariaLive: 'polite',
18
+ children: (<p>You have added the landlord <strong>John Smith</strong> to the application.</p>),
19
+ headingLevel: 'h3',
20
+ isDismissable: false,
21
+ title: 'Landlord added successfully',
22
+ }
23
+ } satisfies Meta<typeof InfoNotification>;
24
+
25
+ export default meta;
26
+ type Story = StoryObj<typeof meta>;
27
+
28
+ export const Default: Story = {
29
+ };
30
+
31
+ export const NoChildren: Story = {
32
+ args: {
33
+ children: undefined
34
+ }
35
+ };
36
+
37
+ export const DifferentHeadingLevel: Story = {
38
+ args: {
39
+ headingLevel: 'h2'
40
+ }
41
+ };
42
+
43
+ export const Dismissable: Story = {
44
+ args: {
45
+ isDismissable: true
46
+ }
47
+ };