@scottish-government/designsystem-react 0.0.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.
- package/.editorconfig +12 -0
- package/.github/workflows/release-package.yml +96 -0
- package/@types/common/ConditionalWrapper.d.ts +6 -0
- package/@types/common/HintText.d.ts +6 -0
- package/@types/common/Icon.d.ts +11 -0
- package/@types/common/ScreenReaderText.d.ts +4 -0
- package/@types/common/WrapperTag.d.ts +5 -0
- package/@types/components/Accordion.d.ts +15 -0
- package/@types/components/AspectBox.d.ts +5 -0
- package/@types/components/BackToTop.d.ts +5 -0
- package/@types/components/Breadcrumbs.d.ts +14 -0
- package/@types/components/Button.d.ts +17 -0
- package/@types/components/Checkbox.d.ts +13 -0
- package/@types/components/ConfirmationMessage.d.ts +7 -0
- package/@types/components/ContentsNav.d.ts +15 -0
- package/@types/components/DatePicker.d.ts +19 -0
- package/@types/components/Details.d.ts +6 -0
- package/@types/components/ErrorMessage.d.ts +6 -0
- package/@types/components/Metadata.d.ts +11 -0
- package/@types/components/NotificationBanner.d.ts +9 -0
- package/@types/components/NotificationPanel.d.ts +7 -0
- package/@types/components/PageHeader.d.ts +6 -0
- package/@types/components/PhaseBanner.d.ts +5 -0
- package/@types/components/Question.d.ts +11 -0
- package/@types/components/RadioButton.d.ts +15 -0
- package/@types/components/Select.d.ts +14 -0
- package/@types/components/SequentialNavigation.d.ts +14 -0
- package/@types/components/SideNavigation.d.ts +19 -0
- package/@types/components/SiteNavigation.d.ts +13 -0
- package/@types/components/SiteSearch.d.ts +14 -0
- package/@types/components/SkipLinks.d.ts +14 -0
- package/@types/components/Tag.d.ts +7 -0
- package/@types/components/TaskList.d.ts +21 -0
- package/@types/components/TextInput.d.ts +12 -0
- package/@types/components/Textarea.d.ts +4 -0
- package/@types/global.d.ts +1 -0
- package/@types/sgds.d.ts +35 -0
- package/package.json +36 -0
- package/src/common/conditional-wrapper.test.tsx +36 -0
- package/src/common/conditional-wrapper.tsx +9 -0
- package/src/common/hint-text.test.tsx +47 -0
- package/src/common/hint-text.tsx +21 -0
- package/src/common/icon.test.tsx +100 -0
- package/src/common/icon.tsx +28 -0
- package/src/common/screen-reader-text.test.tsx +31 -0
- package/src/common/screen-reader-text.tsx +17 -0
- package/src/common/wrapper-tag.test.tsx +42 -0
- package/src/common/wrapper-tag.tsx +15 -0
- package/src/components/accordion/accordion.test.tsx +212 -0
- package/src/components/accordion/accordion.tsx +108 -0
- package/src/components/aspect-box/aspect-box.test.tsx +81 -0
- package/src/components/aspect-box/aspect-box.tsx +57 -0
- package/src/components/back-to-top/back-to-top.test.tsx +45 -0
- package/src/components/back-to-top/back-to-top.tsx +33 -0
- package/src/components/breadcrumbs/breadcrumbs.test.tsx +77 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +53 -0
- package/src/components/button/button.test.tsx +125 -0
- package/src/components/button/button.tsx +48 -0
- package/src/components/checkbox/checkbox.test.tsx +180 -0
- package/src/components/checkbox/checkbox.tsx +107 -0
- package/src/components/confirmation-message/confirmation-message.test.tsx +46 -0
- package/src/components/confirmation-message/confirmation-message.tsx +32 -0
- package/src/components/contents-nav/contents-nav.test.tsx +136 -0
- package/src/components/contents-nav/contents-nav.tsx +54 -0
- package/src/components/date-picker/date-picker.test.tsx +209 -0
- package/src/components/date-picker/date-picker.tsx +129 -0
- package/src/components/details/details.test.tsx +38 -0
- package/src/components/details/details.tsx +25 -0
- package/src/components/error-message/error-message.test.tsx +40 -0
- package/src/components/error-message/error-message.tsx +23 -0
- package/src/components/inset-text/inset-text.test.tsx +33 -0
- package/src/components/inset-text/inset-text.tsx +19 -0
- package/src/components/notification-banner/notification-banner.test.tsx +93 -0
- package/src/components/notification-banner/notification-banner.tsx +70 -0
- package/src/components/notification-panel/notification-panel.test.tsx +77 -0
- package/src/components/notification-panel/notification-panel.tsx +31 -0
- package/src/components/page-header/page-header.test.tsx +48 -0
- package/src/components/page-header/page-header.tsx +22 -0
- package/src/components/page-metadata/page-metadata.test.tsx +56 -0
- package/src/components/page-metadata/page-metadata.tsx +39 -0
- package/src/components/phase-banner/phase-banner.test.tsx +67 -0
- package/src/components/phase-banner/phase-banner.tsx +27 -0
- package/src/components/question/question.test.tsx +69 -0
- package/src/components/question/question.tsx +33 -0
- package/src/components/radio-button/radio-button.test.tsx +190 -0
- package/src/components/radio-button/radio-button.tsx +88 -0
- package/src/components/select/select.test.tsx +208 -0
- package/src/components/select/select.tsx +86 -0
- package/src/components/sequential-navigation/sequential-navigation.test.tsx +67 -0
- package/src/components/sequential-navigation/sequential-navigation.tsx +55 -0
- package/src/components/side-navigation/side-navigation.test.tsx +156 -0
- package/src/components/side-navigation/side-navigation.tsx +85 -0
- package/src/components/site-navigation/site-navigation.test.tsx +63 -0
- package/src/components/site-navigation/site-navigation.tsx +40 -0
- package/src/components/site-search/site-search.test.tsx +153 -0
- package/src/components/site-search/site-search.tsx +97 -0
- package/src/components/skip-links/skip-links.test.tsx +84 -0
- package/src/components/skip-links/skip-links.tsx +39 -0
- package/src/components/tag/tag.test.tsx +45 -0
- package/src/components/tag/tag.tsx +23 -0
- package/src/components/task-list/task-list.test.tsx +409 -0
- package/src/components/task-list/task-list.tsx +132 -0
- package/src/components/text-input/text-input.test.tsx +307 -0
- package/src/components/text-input/text-input.tsx +98 -0
- package/src/components/textarea/textarea.test.tsx +212 -0
- package/src/components/textarea/textarea.tsx +82 -0
- package/src/components/warning-text/warning-text.test.tsx +40 -0
- package/src/components/warning-text/warning-text.tsx +21 -0
- package/tsconfig.json +45 -0
- package/vite.config.ts +12 -0
- package/vitest-setup.ts +13 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { test, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen, within, fireEvent } from '@testing-library/react';
|
|
3
|
+
import TextInput from './text-input';
|
|
4
|
+
|
|
5
|
+
const id = 'text-input';
|
|
6
|
+
const labelText = 'First name';
|
|
7
|
+
|
|
8
|
+
test('text input renders correctly', () => {
|
|
9
|
+
render(
|
|
10
|
+
<TextInput
|
|
11
|
+
id={id}
|
|
12
|
+
label={labelText}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const textInput = screen.getByRole('textbox');
|
|
17
|
+
const label = screen.getByText(labelText);
|
|
18
|
+
|
|
19
|
+
expect(textInput).toHaveClass('ds_input');
|
|
20
|
+
expect(textInput).toHaveAttribute('id', id);
|
|
21
|
+
expect(textInput).toHaveAttribute('name', id);
|
|
22
|
+
expect(textInput).toHaveAttribute('type', 'text');
|
|
23
|
+
expect(textInput.tagName).toEqual('INPUT');
|
|
24
|
+
|
|
25
|
+
expect(label).toHaveClass('ds_label');
|
|
26
|
+
expect(label).toHaveAttribute('for', id);
|
|
27
|
+
expect(label.tagName).toEqual('LABEL');
|
|
28
|
+
expect(label.textContent).toEqual(labelText);
|
|
29
|
+
|
|
30
|
+
expect(textInput.previousSibling).toEqual(label);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('text input with custom class(es)', () => {
|
|
34
|
+
render(
|
|
35
|
+
<TextInput
|
|
36
|
+
id={id}
|
|
37
|
+
label={labelText}
|
|
38
|
+
className="foo bar"
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const textInput = screen.getByRole('textbox');
|
|
43
|
+
|
|
44
|
+
expect(textInput).toHaveClass('ds_input', 'foo', 'bar');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('text input with character count', () => {
|
|
48
|
+
const maxLength = 100;
|
|
49
|
+
|
|
50
|
+
render(
|
|
51
|
+
<TextInput
|
|
52
|
+
id={id}
|
|
53
|
+
label={labelText}
|
|
54
|
+
maxlength={maxLength}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const textInput = screen.getByRole('textbox');
|
|
59
|
+
const textInputWrapper = textInput.parentNode;
|
|
60
|
+
|
|
61
|
+
expect(textInputWrapper).toHaveAttribute('data-maxlength', maxLength.toString());
|
|
62
|
+
expect(textInputWrapper).toHaveAttribute('data-module', 'ds-character-count');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('text input with character count and threshold', () => {
|
|
66
|
+
const maxLength = 100;
|
|
67
|
+
const countThreshold = 80;
|
|
68
|
+
|
|
69
|
+
render(
|
|
70
|
+
<TextInput
|
|
71
|
+
id={id}
|
|
72
|
+
label={labelText}
|
|
73
|
+
maxlength={maxLength}
|
|
74
|
+
countThreshold={countThreshold}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const textInput = screen.getByRole('textbox');
|
|
79
|
+
const textInputWrapper = textInput.parentNode;
|
|
80
|
+
|
|
81
|
+
expect(textInputWrapper).toHaveAttribute('data-threshold', countThreshold.toString());
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('text input with width', () => {
|
|
85
|
+
const width = 'fixed-10';
|
|
86
|
+
|
|
87
|
+
render(
|
|
88
|
+
<TextInput
|
|
89
|
+
id={id}
|
|
90
|
+
label={labelText}
|
|
91
|
+
width={width}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const textInput = screen.getByRole('textbox');
|
|
96
|
+
expect(textInput).toHaveClass(`ds_input--${width}`);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('text input with currency', () => {
|
|
100
|
+
render(
|
|
101
|
+
<TextInput
|
|
102
|
+
id={id}
|
|
103
|
+
label={labelText}
|
|
104
|
+
currency
|
|
105
|
+
/>
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const textInput = screen.getByRole('textbox');
|
|
109
|
+
const textInputWrapper = textInput.parentNode;
|
|
110
|
+
|
|
111
|
+
expect(textInputWrapper.tagName).toEqual('DIV')
|
|
112
|
+
expect(textInputWrapper).toHaveClass('ds_currency-wrapper');
|
|
113
|
+
expect(textInputWrapper).not.toHaveAttribute('data-symbol');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('text input with custom currency symbol', () => {
|
|
117
|
+
const symbol = '@';
|
|
118
|
+
|
|
119
|
+
render(
|
|
120
|
+
<TextInput
|
|
121
|
+
id={id}
|
|
122
|
+
label={labelText}
|
|
123
|
+
currency
|
|
124
|
+
currencySymbol={symbol}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const textInput = screen.getByRole('textbox');
|
|
129
|
+
const textInputWrapper = textInput.parentNode;
|
|
130
|
+
|
|
131
|
+
expect(textInputWrapper).toHaveAttribute('data-symbol', symbol);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('text input with button', () => {
|
|
135
|
+
const buttonText = 'Search';
|
|
136
|
+
const buttonIcon = 'search';
|
|
137
|
+
render(
|
|
138
|
+
<TextInput
|
|
139
|
+
id={id}
|
|
140
|
+
label={labelText}
|
|
141
|
+
buttonIcon="search"
|
|
142
|
+
buttonText="Search"
|
|
143
|
+
hasButton
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const textInput = screen.getByRole('textbox');
|
|
148
|
+
const textInputWrapper = textInput.parentNode;
|
|
149
|
+
const button = screen.getByRole('button');
|
|
150
|
+
const buttonTextElement = within(button).getByText(buttonText);
|
|
151
|
+
const buttonIconElement = within(button).getByRole('img', { hidden: true });
|
|
152
|
+
|
|
153
|
+
expect(textInputWrapper).toHaveClass('ds_input__wrapper', 'ds_input__wrapper--has-icon ');
|
|
154
|
+
|
|
155
|
+
expect(button).toBeInTheDocument();
|
|
156
|
+
expect(button).toHaveClass('ds_button');
|
|
157
|
+
expect(button).toHaveAttribute('type', 'button');
|
|
158
|
+
expect(button.tagName).toEqual('BUTTON');
|
|
159
|
+
expect(buttonTextElement).toHaveClass('visually-hidden');
|
|
160
|
+
expect(buttonTextElement.tagName).toEqual('SPAN');
|
|
161
|
+
|
|
162
|
+
// todo: check for correct icon
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('text input with hint text', () => {
|
|
166
|
+
const hintText = 'hint text';
|
|
167
|
+
render(
|
|
168
|
+
<TextInput
|
|
169
|
+
id={id}
|
|
170
|
+
label={labelText}
|
|
171
|
+
hintText={hintText}
|
|
172
|
+
/>
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const hintTextEl = screen.getByText(hintText);
|
|
176
|
+
const textInput = screen.getByRole('textbox');
|
|
177
|
+
|
|
178
|
+
expect(hintTextEl).toBeInTheDocument();
|
|
179
|
+
expect(textInput).toHaveAttribute('aria-describedby', hintTextEl.id);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('text input with custom name', () => {
|
|
183
|
+
const name = 'foo';
|
|
184
|
+
|
|
185
|
+
render(
|
|
186
|
+
<TextInput
|
|
187
|
+
id={id}
|
|
188
|
+
label={labelText}
|
|
189
|
+
name={name}
|
|
190
|
+
/>
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const textInput = screen.getByRole('textbox');
|
|
194
|
+
expect(textInput).toHaveAttribute('name', name);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('text input with blur function', () => {
|
|
198
|
+
const onBlurFn = vi.fn();
|
|
199
|
+
render(
|
|
200
|
+
<TextInput
|
|
201
|
+
id={id}
|
|
202
|
+
label={labelText}
|
|
203
|
+
onBlur={onBlurFn}
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const textInput = screen.getByRole('textbox');
|
|
208
|
+
|
|
209
|
+
fireEvent.blur(textInput);
|
|
210
|
+
|
|
211
|
+
expect(onBlurFn).toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('text input with change function', () => {
|
|
215
|
+
const onChangeFn = vi.fn();
|
|
216
|
+
render(
|
|
217
|
+
<TextInput
|
|
218
|
+
id={id}
|
|
219
|
+
label={labelText}
|
|
220
|
+
onChange={onChangeFn}
|
|
221
|
+
/>
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const textInput = screen.getByRole('textbox');
|
|
225
|
+
|
|
226
|
+
fireEvent.change(textInput, {target: {value: 'foo'}});
|
|
227
|
+
|
|
228
|
+
expect(onChangeFn).toHaveBeenCalled();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('text input with placeholder text', () => {
|
|
232
|
+
const placeholder = 'foo';
|
|
233
|
+
|
|
234
|
+
render(
|
|
235
|
+
<TextInput
|
|
236
|
+
id={id}
|
|
237
|
+
label={labelText}
|
|
238
|
+
placeholder={placeholder}
|
|
239
|
+
/>
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const textInput = screen.getByRole('textbox');
|
|
243
|
+
expect(textInput).toHaveAttribute('placeholder', placeholder);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test('text input with different type', () => {
|
|
247
|
+
const type = 'foo';
|
|
248
|
+
|
|
249
|
+
render(
|
|
250
|
+
<TextInput
|
|
251
|
+
id={id}
|
|
252
|
+
label={labelText}
|
|
253
|
+
type={type}
|
|
254
|
+
/>
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const textInput = screen.getByRole('textbox');
|
|
258
|
+
expect(textInput).toHaveAttribute('type', type);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test('text input with initial value', () => {
|
|
262
|
+
const initialValue = 'initial value';
|
|
263
|
+
|
|
264
|
+
render(
|
|
265
|
+
<TextInput
|
|
266
|
+
id={id}
|
|
267
|
+
label={labelText}
|
|
268
|
+
value={initialValue}
|
|
269
|
+
/>
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const textInput = screen.getByRole('textbox');
|
|
273
|
+
expect(textInput).toHaveAttribute('value', initialValue);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('text input with error message', () => {
|
|
277
|
+
const errorMessage = 'This is a required field';
|
|
278
|
+
render(
|
|
279
|
+
<TextInput
|
|
280
|
+
id={id}
|
|
281
|
+
label={labelText}
|
|
282
|
+
error
|
|
283
|
+
errorMessage={errorMessage}
|
|
284
|
+
/>
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
const textInput = screen.getByRole('textbox');
|
|
288
|
+
const errorMessageElement = screen.getByText(errorMessage);
|
|
289
|
+
|
|
290
|
+
expect(textInput).toHaveClass('ds_input--error')
|
|
291
|
+
expect(textInput).toHaveAttribute('aria-describedby', errorMessageElement.id);
|
|
292
|
+
expect(errorMessageElement).toBeInTheDocument();
|
|
293
|
+
expect(errorMessageElement).toHaveClass('ds_question__error-message');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('passing additional props', () => {
|
|
297
|
+
render(
|
|
298
|
+
<TextInput
|
|
299
|
+
id={id}
|
|
300
|
+
label={labelText}
|
|
301
|
+
data-test="foo"
|
|
302
|
+
/>
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
const textInput = screen.getByRole('textbox');
|
|
306
|
+
expect(textInput?.dataset.test).toEqual('foo');
|
|
307
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import DSCharacterCount from '@scottish-government/design-system/src/forms/character-count/character-count';
|
|
4
|
+
import Button from '../button/button';
|
|
5
|
+
import ConditionalWrapper from '../../common/conditional-wrapper';
|
|
6
|
+
import ErrorMessage from '../error-message/error-message';
|
|
7
|
+
import HintText from '../../common/hint-text';
|
|
8
|
+
|
|
9
|
+
const TextInput: React.FC<SGDS.Component.TextInput> = ({
|
|
10
|
+
buttonIcon,
|
|
11
|
+
buttonText,
|
|
12
|
+
children,
|
|
13
|
+
className,
|
|
14
|
+
countThreshold,
|
|
15
|
+
width,
|
|
16
|
+
currency,
|
|
17
|
+
currencySymbol,
|
|
18
|
+
error,
|
|
19
|
+
errorMessage,
|
|
20
|
+
hasButton = false,
|
|
21
|
+
hintText,
|
|
22
|
+
id,
|
|
23
|
+
label,
|
|
24
|
+
maxlength,
|
|
25
|
+
name,
|
|
26
|
+
onBlur,
|
|
27
|
+
onChange,
|
|
28
|
+
placeholder,
|
|
29
|
+
type = 'text',
|
|
30
|
+
value,
|
|
31
|
+
...props
|
|
32
|
+
}) => {
|
|
33
|
+
const errorMessageId = `error-message-${id}`;
|
|
34
|
+
const hintTextId = `hint-text-${id}`;
|
|
35
|
+
const ref = useRef(null);
|
|
36
|
+
const inputWrapperClasses = `${hasButton ? 'ds_input__wrapper ds_input__wrapper--has-icon' : ''} ${currency ? 'ds_currency-wrapper' : ''}`;
|
|
37
|
+
const describedbys: string[] = [];
|
|
38
|
+
|
|
39
|
+
if (hintText) { describedbys.push(hintTextId) };
|
|
40
|
+
if (errorMessage) { describedbys.push(errorMessageId) };
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (ref.current) {
|
|
44
|
+
new DSCharacterCount(ref.current).init();
|
|
45
|
+
}
|
|
46
|
+
}, [ref]);
|
|
47
|
+
|
|
48
|
+
function handleBlur(event: React.FocusEvent) {
|
|
49
|
+
if (typeof onBlur === 'function') {
|
|
50
|
+
onBlur(event);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function handleChange(event: React.ChangeEvent) {
|
|
55
|
+
if (typeof onChange === 'function') {
|
|
56
|
+
onChange(event);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<ConditionalWrapper
|
|
62
|
+
condition={typeof maxlength !== 'undefined' && maxlength > 0}
|
|
63
|
+
wrapper={(children: React.JSX.Element) => <div ref={ref} data-threshold={countThreshold} data-module="ds-character-count">{children}</div>}
|
|
64
|
+
>
|
|
65
|
+
<label className="ds_label" htmlFor={id}>{label}</label>
|
|
66
|
+
{hintText && <HintText id={hintTextId} text={hintText} />}
|
|
67
|
+
{errorMessage && <ErrorMessage id={errorMessageId} text={errorMessage}/>}
|
|
68
|
+
<ConditionalWrapper
|
|
69
|
+
condition={hasButton || typeof currency !== 'undefined' && currency}
|
|
70
|
+
wrapper={(children: React.JSX.Element) => <div className={inputWrapperClasses} data-symbol={currencySymbol}>{children}</div>}
|
|
71
|
+
>
|
|
72
|
+
<input
|
|
73
|
+
aria-describedby={describedbys.join(' ')}
|
|
74
|
+
className={[
|
|
75
|
+
'ds_input',
|
|
76
|
+
className,
|
|
77
|
+
error ? 'ds_input--error' : '',
|
|
78
|
+
width ? `ds_input--${width}` : '',
|
|
79
|
+
].join(' ')}
|
|
80
|
+
defaultValue={value}
|
|
81
|
+
id={id}
|
|
82
|
+
maxLength={maxlength}
|
|
83
|
+
name={name || id}
|
|
84
|
+
onBlur={handleBlur}
|
|
85
|
+
onChange={handleChange}
|
|
86
|
+
placeholder={placeholder}
|
|
87
|
+
type={type}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
{hasButton && (buttonText || buttonIcon) && <Button iconOnly icon={buttonIcon}>{buttonText}</Button>}
|
|
91
|
+
</ConditionalWrapper>
|
|
92
|
+
</ConditionalWrapper>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
TextInput.displayName = 'TextInput';
|
|
97
|
+
|
|
98
|
+
export default TextInput;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { test, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen, within, fireEvent } from '@testing-library/react';
|
|
3
|
+
import Textarea from './textarea';
|
|
4
|
+
|
|
5
|
+
const id = 'textarea';
|
|
6
|
+
const labelText = 'Description';
|
|
7
|
+
|
|
8
|
+
test('text input renders correctly', () => {
|
|
9
|
+
render(
|
|
10
|
+
<Textarea
|
|
11
|
+
id={id}
|
|
12
|
+
label={labelText}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const textarea = screen.getByRole('textbox');
|
|
17
|
+
const label = screen.getByText(labelText);
|
|
18
|
+
|
|
19
|
+
expect(textarea).toHaveClass('ds_input');
|
|
20
|
+
expect(textarea).toHaveAttribute('id', id);
|
|
21
|
+
expect(textarea).toHaveAttribute('name', id);
|
|
22
|
+
expect(textarea).toHaveAttribute('rows', String(4));
|
|
23
|
+
|
|
24
|
+
expect(label).toHaveClass('ds_label');
|
|
25
|
+
expect(label).toHaveAttribute('for', id);
|
|
26
|
+
expect(label.tagName).toEqual('LABEL');
|
|
27
|
+
expect(label.textContent).toEqual(labelText);
|
|
28
|
+
|
|
29
|
+
expect(textarea.previousSibling).toEqual(label);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('textarea with character count', () => {
|
|
33
|
+
const maxLength = 250;
|
|
34
|
+
|
|
35
|
+
render(
|
|
36
|
+
<Textarea
|
|
37
|
+
id={id}
|
|
38
|
+
label={labelText}
|
|
39
|
+
maxlength={maxLength}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const textarea = screen.getByRole('textbox');
|
|
44
|
+
const textareaWrapper = textarea.parentNode;
|
|
45
|
+
|
|
46
|
+
expect(textareaWrapper).toHaveAttribute('data-maxlength', maxLength.toString());
|
|
47
|
+
expect(textareaWrapper).toHaveAttribute('data-module', 'ds-character-count');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('text input with character count and threshold', () => {
|
|
51
|
+
const maxLength = 250;
|
|
52
|
+
const countThreshold = 80;
|
|
53
|
+
|
|
54
|
+
render(
|
|
55
|
+
<Textarea
|
|
56
|
+
id={id}
|
|
57
|
+
label={labelText}
|
|
58
|
+
maxlength={maxLength}
|
|
59
|
+
countThreshold={countThreshold}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const textarea = screen.getByRole('textbox');
|
|
64
|
+
const textareaWrapper = textarea.parentNode;
|
|
65
|
+
|
|
66
|
+
expect(textareaWrapper).toHaveAttribute('data-threshold', countThreshold.toString());
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('textarea with hint text', () => {
|
|
70
|
+
const hintText = 'hint text';
|
|
71
|
+
render(
|
|
72
|
+
<Textarea
|
|
73
|
+
id={id}
|
|
74
|
+
label={labelText}
|
|
75
|
+
hintText={hintText}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const hintTextEl = screen.getByText(hintText);
|
|
80
|
+
const textarea = screen.getByRole('textbox');
|
|
81
|
+
|
|
82
|
+
expect(hintTextEl).toBeInTheDocument();
|
|
83
|
+
expect(textarea).toHaveAttribute('aria-describedby', hintTextEl.id);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('textarea with custom name', () => {
|
|
87
|
+
const name = 'foo';
|
|
88
|
+
|
|
89
|
+
render(
|
|
90
|
+
<Textarea
|
|
91
|
+
id={id}
|
|
92
|
+
label={labelText}
|
|
93
|
+
name={name}
|
|
94
|
+
/>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const textarea = screen.getByRole('textbox');
|
|
98
|
+
expect(textarea).toHaveAttribute('name', name);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('textarea with blur function', () => {
|
|
102
|
+
const onBlurFn = vi.fn();
|
|
103
|
+
render(
|
|
104
|
+
<Textarea
|
|
105
|
+
id={id}
|
|
106
|
+
label={labelText}
|
|
107
|
+
onBlur={onBlurFn}
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const textarea = screen.getByRole('textbox');
|
|
112
|
+
|
|
113
|
+
fireEvent.blur(textarea);
|
|
114
|
+
|
|
115
|
+
expect(onBlurFn).toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('textarea with change function', () => {
|
|
119
|
+
const onChangeFn = vi.fn();
|
|
120
|
+
render(
|
|
121
|
+
<Textarea
|
|
122
|
+
id={id}
|
|
123
|
+
label={labelText}
|
|
124
|
+
onChange={onChangeFn}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const textarea = screen.getByRole('textbox');
|
|
129
|
+
|
|
130
|
+
fireEvent.change(textarea, {target: {value: 'foo'}});
|
|
131
|
+
|
|
132
|
+
expect(onChangeFn).toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('textarea with placeholder text', () => {
|
|
136
|
+
const placeholder = 'foo';
|
|
137
|
+
|
|
138
|
+
render(
|
|
139
|
+
<Textarea
|
|
140
|
+
id={id}
|
|
141
|
+
label={labelText}
|
|
142
|
+
placeholder={placeholder}
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const textarea = screen.getByRole('textbox');
|
|
147
|
+
expect(textarea).toHaveAttribute('placeholder', placeholder);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('textarea with custom rows', () => {
|
|
151
|
+
const rows = 2;
|
|
152
|
+
|
|
153
|
+
render(
|
|
154
|
+
<Textarea
|
|
155
|
+
id={id}
|
|
156
|
+
label={labelText}
|
|
157
|
+
rows={rows}
|
|
158
|
+
/>
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const textarea = screen.getByRole('textbox');
|
|
162
|
+
|
|
163
|
+
expect(textarea).toHaveAttribute('rows', String(rows));
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('textarea with initial value', () => {
|
|
167
|
+
const content = 'Mygov.scot gives people and businesses information about and access to public services in Scotland. We work closely with public sector organisations to make public services easy to find and understand.';
|
|
168
|
+
|
|
169
|
+
render(
|
|
170
|
+
<Textarea
|
|
171
|
+
defaultValue={content}
|
|
172
|
+
id={id}
|
|
173
|
+
label={labelText}
|
|
174
|
+
/>
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const textarea = screen.getByRole('textbox');
|
|
178
|
+
expect(textarea.textContent).toEqual(content);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('textarea with error message', () => {
|
|
182
|
+
const errorMessage = 'This is a required field';
|
|
183
|
+
render(
|
|
184
|
+
<Textarea
|
|
185
|
+
id={id}
|
|
186
|
+
label={labelText}
|
|
187
|
+
error
|
|
188
|
+
errorMessage={errorMessage}
|
|
189
|
+
/>
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const textarea = screen.getByRole('textbox');
|
|
193
|
+
const errorMessageElement = screen.getByText(errorMessage);
|
|
194
|
+
|
|
195
|
+
expect(textarea).toHaveClass('ds_input--error')
|
|
196
|
+
expect(textarea).toHaveAttribute('aria-describedby', errorMessageElement.id);
|
|
197
|
+
expect(errorMessageElement).toBeInTheDocument();
|
|
198
|
+
expect(errorMessageElement).toHaveClass('ds_question__error-message');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('passing additional props', () => {
|
|
202
|
+
render(
|
|
203
|
+
<Textarea
|
|
204
|
+
id={id}
|
|
205
|
+
label={labelText}
|
|
206
|
+
data-test="foo"
|
|
207
|
+
/>
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const textarea = screen.getByRole('textbox');
|
|
211
|
+
expect(textarea?.dataset.test).toEqual('foo');
|
|
212
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import DSCharacterCount from '@scottish-government/design-system/src/forms/character-count/character-count';
|
|
4
|
+
import ConditionalWrapper from '../../common/conditional-wrapper';
|
|
5
|
+
import ErrorMessage from '../error-message/error-message';
|
|
6
|
+
import HintText from '../../common/hint-text';
|
|
7
|
+
|
|
8
|
+
const Textarea: React.FC<SGDS.Component.Textarea> = ({
|
|
9
|
+
countThreshold,
|
|
10
|
+
error,
|
|
11
|
+
errorMessage,
|
|
12
|
+
hintText,
|
|
13
|
+
id,
|
|
14
|
+
label,
|
|
15
|
+
maxlength,
|
|
16
|
+
name,
|
|
17
|
+
onBlur,
|
|
18
|
+
onChange,
|
|
19
|
+
placeholder,
|
|
20
|
+
rows = 4,
|
|
21
|
+
value,
|
|
22
|
+
...props
|
|
23
|
+
}) => {
|
|
24
|
+
const errorMessageId = `error-message-${id}`;
|
|
25
|
+
const hintTextId = `hint-text-${id}`;
|
|
26
|
+
const ref = useRef(null);
|
|
27
|
+
const describedbys: string[] = [];
|
|
28
|
+
|
|
29
|
+
if (hintText) { describedbys.push(hintTextId) };
|
|
30
|
+
if (errorMessage) { describedbys.push(errorMessageId) };
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (ref.current) {
|
|
34
|
+
new DSCharacterCount(ref.current).init();
|
|
35
|
+
}
|
|
36
|
+
}, [ref]);
|
|
37
|
+
|
|
38
|
+
function handleBlur(event: React.FocusEvent) {
|
|
39
|
+
if (typeof onBlur === 'function') {
|
|
40
|
+
onBlur(event);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function handleChange(event: React.ChangeEvent) {
|
|
45
|
+
if (typeof onChange === 'function') {
|
|
46
|
+
onChange(event);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<ConditionalWrapper
|
|
52
|
+
condition={typeof maxlength !== 'undefined' && maxlength > 0}
|
|
53
|
+
wrapper={(children: React.JSX.Element) => <div ref={ref} data-threshold={countThreshold} data-module="ds-character-count">{children}</div>}
|
|
54
|
+
>
|
|
55
|
+
<label className="ds_label" htmlFor={id}>{label}</label>
|
|
56
|
+
{hintText && <HintText id={hintTextId} text={hintText} />}
|
|
57
|
+
{errorMessage && <ErrorMessage id={errorMessageId} text={errorMessage}/>}
|
|
58
|
+
|
|
59
|
+
<textarea
|
|
60
|
+
aria-describedby={describedbys.join(' ')}
|
|
61
|
+
className={[
|
|
62
|
+
'ds_input',
|
|
63
|
+
error && 'ds_input--error',
|
|
64
|
+
].join(' ')}
|
|
65
|
+
defaultValue={value}
|
|
66
|
+
id={id}
|
|
67
|
+
maxLength={maxlength}
|
|
68
|
+
name={name || id}
|
|
69
|
+
onBlur={handleBlur}
|
|
70
|
+
onChange={handleChange}
|
|
71
|
+
placeholder={placeholder}
|
|
72
|
+
rows={rows}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
|
|
76
|
+
</ConditionalWrapper>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
Textarea.displayName = 'Textarea';
|
|
81
|
+
|
|
82
|
+
export default Textarea;
|