@scottish-government/designsystem-react 0.1.2 → 0.1.4

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 (73) hide show
  1. package/.svgrrc +15 -0
  2. package/@types/common/Icon.d.ts +3 -5
  3. package/@types/components/Button.d.ts +1 -1
  4. package/@types/components/Pagination.d.ts +21 -0
  5. package/@types/components/TextInput.d.ts +1 -1
  6. package/@types/sgds.d.ts +1 -0
  7. package/README.md +3 -0
  8. package/dist/common/icon.jsx +48 -5
  9. package/dist/components/back-to-top/back-to-top.jsx +1 -1
  10. package/dist/components/confirmation-message/confirmation-message.jsx +1 -1
  11. package/dist/components/date-picker/date-picker.jsx +2 -0
  12. package/dist/components/notification-banner/notification-banner.jsx +4 -5
  13. package/dist/components/pagination/pagination.jsx +97 -0
  14. package/dist/components/select/select.jsx +1 -1
  15. package/dist/components/site-search/site-search.jsx +1 -1
  16. package/dist/components/text-input/text-input.jsx +1 -1
  17. package/dist/components/textarea/textarea.jsx +1 -1
  18. package/dist/icons/ArrowUpward.jsx +41 -0
  19. package/dist/icons/CalendarToday.jsx +41 -0
  20. package/dist/icons/Cancel.jsx +40 -0
  21. package/dist/icons/CheckCircle.jsx +41 -0
  22. package/dist/icons/ChevronLeft.jsx +41 -0
  23. package/dist/icons/ChevronRight.jsx +41 -0
  24. package/dist/icons/Close.jsx +41 -0
  25. package/dist/icons/Description.jsx +41 -0
  26. package/dist/icons/DoubleChevronLeft.jsx +41 -0
  27. package/dist/icons/DoubleChevronRight.jsx +41 -0
  28. package/dist/icons/Error.jsx +41 -0
  29. package/dist/icons/ExpandLess.jsx +41 -0
  30. package/dist/icons/ExpandMore.jsx +41 -0
  31. package/dist/icons/List.jsx +44 -0
  32. package/dist/icons/Menu.jsx +41 -0
  33. package/dist/icons/PriorityHigh.jsx +42 -0
  34. package/dist/icons/Search.jsx +41 -0
  35. package/dist/icons/index.js +40 -0
  36. package/dist/tsconfig.tsbuildinfo +1 -1
  37. package/package.json +3 -1
  38. package/src/common/icon.test.tsx +5 -26
  39. package/src/common/icon.tsx +18 -13
  40. package/src/components/back-to-top/back-to-top.tsx +1 -1
  41. package/src/components/button/button.test.tsx +3 -3
  42. package/src/components/confirmation-message/confirmation-message.tsx +1 -1
  43. package/src/components/date-picker/date-picker.test.tsx +21 -0
  44. package/src/components/date-picker/date-picker.tsx +2 -0
  45. package/src/components/notification-banner/notification-banner.tsx +5 -5
  46. package/src/components/pagination/pagination.test.tsx +366 -0
  47. package/src/components/pagination/pagination.tsx +157 -0
  48. package/src/components/phase-banner/phase-banner.test.tsx +1 -1
  49. package/src/components/select/select.test.tsx +1 -0
  50. package/src/components/select/select.tsx +1 -0
  51. package/src/components/site-search/site-search.tsx +1 -1
  52. package/src/components/text-input/text-input.test.tsx +5 -4
  53. package/src/components/text-input/text-input.tsx +1 -0
  54. package/src/components/textarea/textarea.test.tsx +1 -0
  55. package/src/components/textarea/textarea.tsx +1 -0
  56. package/src/icons/ArrowUpward.tsx +15 -0
  57. package/src/icons/CalendarToday.tsx +15 -0
  58. package/src/icons/Cancel.tsx +13 -0
  59. package/src/icons/CheckCircle.tsx +15 -0
  60. package/src/icons/ChevronLeft.tsx +15 -0
  61. package/src/icons/ChevronRight.tsx +15 -0
  62. package/src/icons/Close.tsx +15 -0
  63. package/src/icons/Description.tsx +15 -0
  64. package/src/icons/DoubleChevronLeft.tsx +19 -0
  65. package/src/icons/DoubleChevronRight.tsx +19 -0
  66. package/src/icons/Error.tsx +15 -0
  67. package/src/icons/ExpandLess.tsx +15 -0
  68. package/src/icons/ExpandMore.tsx +15 -0
  69. package/src/icons/List.tsx +18 -0
  70. package/src/icons/Menu.tsx +15 -0
  71. package/src/icons/PriorityHigh.tsx +16 -0
  72. package/src/icons/Search.tsx +15 -0
  73. package/src/icons/index.ts +17 -0
@@ -0,0 +1,366 @@
1
+ import { test, expect, vi } from 'vitest';
2
+ import { render, screen, within, fireEvent } from '@testing-library/react';
3
+ import Pagination, { Page, Ellipsis } from './pagination';
4
+
5
+ const pageAriaLabel = 'Page 1';
6
+ const pageHref = '#foo';
7
+ const pageText = '1';
8
+ const currentPage = 10;
9
+ const totalPages = 21;
10
+
11
+ test('pagination page renders correctly', () => {
12
+ render(
13
+ <Page
14
+ ariaLabel={pageAriaLabel}
15
+ href={pageHref}
16
+ text={pageText}
17
+ />
18
+ );
19
+
20
+ const item = screen.getByRole('listitem');
21
+ const link = within(item).getByRole('link');
22
+ const span = within(link).getByText(pageText);
23
+
24
+ expect(item).toHaveClass('ds_pagination__item');
25
+ expect(link).toHaveClass('ds_pagination__link');
26
+ expect(link).toHaveAttribute('aria-label', pageAriaLabel);
27
+ expect(link).toHaveAttribute('href', pageHref);
28
+ expect(span).toHaveClass('ds_pagination__link-label');
29
+ });
30
+
31
+ test('current pagination page', () => {
32
+ render(
33
+ <Page
34
+ ariaLabel={pageAriaLabel}
35
+ href={pageHref}
36
+ text={pageText}
37
+ current
38
+ />
39
+ );
40
+
41
+ const item = screen.getByRole('listitem');
42
+ const link = within(item).getByRole('link');
43
+
44
+ expect(link).toHaveClass('ds_current');
45
+ expect(link).toHaveAttribute('aria-current', 'page');
46
+ });
47
+
48
+ test('pagination page with click event', () => {
49
+ const onClickFn = vi.fn();
50
+
51
+ render(
52
+ <Page
53
+ ariaLabel={pageAriaLabel}
54
+ href={pageHref}
55
+ text={pageText}
56
+ onClick={onClickFn}
57
+ />
58
+ );
59
+
60
+ const item = screen.getByRole('listitem');
61
+ const link = within(item).getByRole('link');
62
+
63
+ fireEvent.click(link);
64
+
65
+ expect(onClickFn).toHaveBeenCalled();
66
+ });
67
+
68
+ test('Ellipsis item renders correctly', () => {
69
+ render(
70
+ <Ellipsis/>
71
+ );
72
+
73
+ const item = screen.getByRole('listitem', {hidden: true});
74
+ const link = within(item).getByText('…');
75
+
76
+ expect(item).toHaveClass('ds_pagination__item');
77
+ expect(item).toHaveAttribute('aria-hidden', 'true');
78
+ expect(link).toHaveClass('ds_pagination__link', 'ds_pagination__link--ellipsis');
79
+ });
80
+
81
+ test('pagination renders correctly', () => {
82
+ const currentPage = 10;
83
+ const totalPages = 21;
84
+
85
+ render(
86
+ <Pagination page={currentPage} totalPages={totalPages} />
87
+ );
88
+
89
+ const paginationNav = screen.getByRole('navigation');
90
+ const paginationList = within(paginationNav).getByRole('list');
91
+ const prevLabel = within(paginationList).getByText('Previous');
92
+ const prevLink = prevLabel.parentNode;
93
+ const prevIcon = prevLabel.previousSibling;
94
+ const prevItem = prevLink?.parentNode;
95
+ const nextLabel = within(paginationList).getByText('Next');
96
+ const nextLink = nextLabel.parentNode;
97
+ const nextIcon = nextLabel.nextSibling;
98
+ const nextItem = nextLink?.parentNode;
99
+
100
+ const firstPageLabel = within(paginationList).getByText('1');
101
+ const firstPageLink = firstPageLabel.parentNode;
102
+ const firstPageItem = firstPageLink?.parentNode;
103
+ const lastPageLabel = within(paginationList).getByText(totalPages);
104
+ const lastPageLink = lastPageLabel.parentNode;
105
+ const lastPageItem = lastPageLink?.parentNode;
106
+
107
+ const firstPageEllipsisItem = firstPageItem?.nextSibling;
108
+ const firstPageEllipsis = firstPageEllipsisItem?.children[0];
109
+ const lastPageEllipsisItem = lastPageItem?.previousSibling;
110
+ const lastPageEllipsis = lastPageEllipsisItem?.children[0];
111
+
112
+ const currentPageLink = document.querySelector('.ds_current');
113
+ const currentPageItem = currentPageLink?.parentNode;
114
+
115
+ const paginationItems = within(paginationList).getAllByRole('listitem', { hidden: true });
116
+
117
+ expect(paginationNav).toHaveClass('ds_pagination');
118
+ expect(paginationNav).toHaveAttribute('aria-label', 'Pages');
119
+
120
+ expect(paginationList).toHaveClass('ds_pagination__list');
121
+ expect(paginationList.tagName).toEqual('UL');
122
+
123
+ expect(prevItem).toHaveClass('ds_pagination__item');
124
+ expect(prevItem?.tagName).toEqual('LI');
125
+ expect(prevItem?.parentNode).toEqual(paginationList);
126
+ expect(prevLink).toHaveClass('ds_pagination__link', 'ds_pagination__link--text', 'ds_pagination__link--icon')
127
+ expect(prevLink).toHaveAttribute('aria-label', 'Previous page');
128
+ expect(prevLink).toHaveAttribute('href', `/search?page=${currentPage - 1}`);
129
+ expect(prevLink?.tagName).toEqual('A');
130
+ expect(prevIcon).toHaveClass('ds_icon');
131
+ expect(prevIcon).toHaveAttribute('aria-hidden', 'true')
132
+ expect(prevIcon?.tagName).toEqual('svg');
133
+ expect(prevLabel).toHaveClass('ds_pagination__link-label');
134
+ expect(prevLabel?.tagName).toEqual('SPAN');
135
+
136
+ expect(nextItem).toHaveClass('ds_pagination__item');
137
+ expect(nextItem?.tagName).toEqual('LI');
138
+ expect(nextItem?.parentNode).toEqual(paginationList);
139
+ expect(nextLink).toHaveClass('ds_pagination__link', 'ds_pagination__link--text', 'ds_pagination__link--icon')
140
+ expect(nextLink).toHaveAttribute('aria-label', 'Next page');
141
+ expect(nextLink).toHaveAttribute('href', `/search?page=${currentPage + 1}`);
142
+ expect(nextLink?.tagName).toEqual('A');
143
+ expect(nextIcon).toHaveClass('ds_icon');
144
+ expect(nextIcon).toHaveAttribute('aria-hidden', 'true')
145
+ expect(nextIcon?.tagName).toEqual('svg');
146
+ expect(nextLabel).toHaveClass('ds_pagination__link-label');
147
+ expect(nextLabel?.tagName).toEqual('SPAN');
148
+
149
+ expect(firstPageItem).toHaveClass('ds_pagination__item');
150
+ expect(firstPageItem?.tagName).toEqual('LI');
151
+ expect(firstPageItem?.parentNode).toEqual(paginationList);
152
+ expect(firstPageLink).toHaveClass('ds_pagination__link')
153
+ expect(firstPageLink).toHaveAttribute('aria-label', 'Page 1');
154
+ expect(firstPageLink).toHaveAttribute('href', `/search?page=1`);
155
+ expect(firstPageLink?.tagName).toEqual('A');
156
+ expect(firstPageLabel).toHaveClass('ds_pagination__link-label');
157
+ expect(firstPageLabel?.tagName).toEqual('SPAN');
158
+
159
+ expect(lastPageItem).toHaveClass('ds_pagination__item');
160
+ expect(lastPageItem?.tagName).toEqual('LI');
161
+ expect(lastPageItem?.parentNode).toEqual(paginationList);
162
+ expect(lastPageLink).toHaveClass('ds_pagination__link')
163
+ expect(lastPageLink).toHaveAttribute('aria-label', `Page ${totalPages}`);
164
+ expect(lastPageLink).toHaveAttribute('href', `/search?page=${totalPages}`);
165
+ expect(lastPageLink?.tagName).toEqual('A');
166
+ expect(lastPageLabel).toHaveClass('ds_pagination__link-label');
167
+ expect(lastPageLabel?.tagName).toEqual('SPAN');
168
+
169
+ // dev note: by this point I'd started to wonder if it would just be better to do a single string compare of the rendered output versus the expected output
170
+
171
+ expect(firstPageEllipsisItem).toHaveClass('ds_pagination__item');
172
+ expect(firstPageEllipsisItem).toHaveAttribute('aria-hidden', 'true');
173
+ expect(firstPageEllipsisItem.tagName).toEqual('LI');
174
+ expect(firstPageEllipsis).toHaveClass('ds_pagination__link', 'ds_pagination__link--ellipsis');
175
+ expect(firstPageEllipsis.tagName).toEqual('SPAN');
176
+ expect(firstPageEllipsis.textContent).toEqual('…');
177
+
178
+ expect(lastPageEllipsisItem).toHaveClass('ds_pagination__item');
179
+ expect(lastPageEllipsisItem).toHaveAttribute('aria-hidden', 'true');
180
+ expect(lastPageEllipsisItem.tagName).toEqual('LI');
181
+ expect(lastPageEllipsis).toHaveClass('ds_pagination__link', 'ds_pagination__link--ellipsis');
182
+ expect(lastPageEllipsis.tagName).toEqual('SPAN');
183
+ expect(lastPageEllipsis.textContent).toEqual('…');
184
+
185
+ expect(currentPageItem).toHaveClass('ds_pagination__item');
186
+ expect(currentPageLink).toHaveClass('ds_pagination__link', 'ds_current');
187
+ expect(currentPageLink.textContent).toEqual(currentPage.toString());
188
+
189
+ // expect one link either side of the current (default padding)
190
+ expect(currentPageItem.previousSibling.querySelector('a')).toHaveAttribute('aria-label', 'Page 9');
191
+ expect(currentPageItem.previousSibling.previousSibling.querySelector('a')).toBeNull();
192
+
193
+ expect(currentPageItem.nextSibling.querySelector('a')).toHaveAttribute('aria-label', 'Page 11');
194
+ expect(currentPageItem.nextSibling.nextSibling.querySelector('a')).toBeNull();
195
+
196
+ // 9 is: previous, first, ellipsis, current page and 1 padding either side, ellipsis, last, next
197
+ expect(paginationItems.length).toEqual(9);
198
+ });
199
+
200
+ test('pagination with 2 padding', () => {
201
+ const padding = 2;
202
+
203
+ render(
204
+ <Pagination
205
+ page={currentPage}
206
+ totalPages={totalPages}
207
+ padding={padding}
208
+ />
209
+ );
210
+
211
+ const paginationNav = screen.getByRole('navigation');
212
+ const paginationList = within(paginationNav).getByRole('list');
213
+ const paginationItems = within(paginationList).getAllByRole('listitem', { hidden: true });
214
+
215
+ // 11 is: previous, first, ellipsis, current page and 2 padding either side, ellipsis, last, next
216
+ expect(paginationItems.length).toEqual(11);
217
+ });
218
+
219
+ test('pagination with custom aria label', () => {
220
+ const ariaLabel = 'My label';
221
+
222
+ render(
223
+ <Pagination
224
+ page={currentPage}
225
+ totalPages={totalPages}
226
+ ariaLabel={ariaLabel}
227
+ />
228
+ );
229
+
230
+ const paginationNav = screen.getByRole('navigation');
231
+
232
+ expect(paginationNav).toHaveAttribute('aria-label', ariaLabel);
233
+ });
234
+
235
+ test('pagination passes onclick event to child links', () => {
236
+ const onClickFn = vi.fn();
237
+
238
+ render(
239
+ <Pagination
240
+ page={currentPage}
241
+ totalPages={totalPages}
242
+ onClick={onClickFn}
243
+ />
244
+ );
245
+
246
+ const paginationNav = screen.getByRole('navigation');
247
+
248
+ // pick an arbitrary link
249
+ const link = [].slice.call(document.querySelectorAll('.ds_pagination__link'))[4];
250
+ link.setAttribute('href', '#foo');
251
+
252
+ fireEvent.click(link);
253
+
254
+ expect(onClickFn).toHaveBeenCalled();
255
+ });
256
+
257
+ test('pagination modifies an input pattern for its link format', () => {
258
+ render(
259
+ <Pagination
260
+ page={currentPage}
261
+ totalPages={totalPages}
262
+ pattern='My/Link/Format?Page=$1#foo'
263
+ />
264
+ );
265
+
266
+ // pick an arbitrary link
267
+ const link = document.querySelector('.ds_pagination__link.ds_current');
268
+ expect(link).toHaveAttribute('href', 'My/Link/Format?Page=10#foo');
269
+ });
270
+
271
+
272
+ test('pagination at an early link in the list', () => {
273
+ render(
274
+ <Pagination
275
+ page="1"
276
+ totalPages={totalPages}
277
+ />
278
+ );
279
+
280
+ const paginationNav = screen.getByRole('navigation');
281
+ const paginationList = within(paginationNav).getByRole('list');
282
+ const paginationItems = within(paginationList).getAllByRole('listitem', { hidden: true });
283
+
284
+ // 7 is: current page and 3 subsequent items (padding plus 2), ellipsis, last, next
285
+ expect(paginationItems.length).toEqual(7);
286
+ expect(paginationNav.textContent).toEqual('1234…21Next');
287
+ });
288
+
289
+ test('pagination at an early link in the list, increased padding', () => {
290
+ render(
291
+ <Pagination
292
+ padding={2}
293
+ page="1"
294
+ totalPages={totalPages}
295
+ />
296
+ );
297
+
298
+ const paginationNav = screen.getByRole('navigation');
299
+ const paginationList = within(paginationNav).getByRole('list');
300
+ const paginationItems = within(paginationList).getAllByRole('listitem', { hidden: true });
301
+
302
+ // 8 is: current page and 4 subsequent items (padding plus 2), ellipsis, last, next
303
+ expect(paginationItems.length).toEqual(8);
304
+ expect(paginationNav.textContent).toEqual('12345…21Next');
305
+ });
306
+
307
+ test('pagination at late link in the list', () => {
308
+ render(
309
+ <Pagination
310
+ page={totalPages}
311
+ totalPages={totalPages}
312
+ />
313
+ );
314
+
315
+ const paginationNav = screen.getByRole('navigation');
316
+ const paginationList = within(paginationNav).getByRole('list');
317
+ const paginationItems = within(paginationList).getAllByRole('listitem', { hidden: true });
318
+
319
+ // 7 is: current page and 3 subsequent items (padding plus 2), ellipsis, last, next
320
+ expect(paginationItems.length).toEqual(7);
321
+ expect(paginationNav.textContent).toEqual('Previous1…18192021');
322
+ });
323
+
324
+ test('pagination at late link in the list, increased padding', () => {
325
+ render(
326
+ <Pagination
327
+ padding={2}
328
+ page={totalPages}
329
+ totalPages={totalPages}
330
+ />
331
+ );
332
+
333
+ const paginationNav = screen.getByRole('navigation');
334
+ const paginationList = within(paginationNav).getByRole('list');
335
+ const paginationItems = within(paginationList).getAllByRole('listitem', { hidden: true });
336
+
337
+ // 8 is: current page and 4 subsequent items (padding plus 2), ellipsis, last, next
338
+ expect(paginationItems.length).toEqual(8);
339
+ expect(paginationNav.textContent).toEqual('Previous1…1718192021');
340
+ });
341
+
342
+ test('passing additional props', () => {
343
+ render(
344
+ <Pagination
345
+ page={currentPage}
346
+ totalPages={totalPages}
347
+ data-test="foo"
348
+ />
349
+ );
350
+
351
+ const paginationNav = screen.getByRole('navigation');
352
+ expect(paginationNav?.dataset.test).toEqual('foo');
353
+ });
354
+
355
+ test('passing additional CSS classes', () => {
356
+ render(
357
+ <Pagination
358
+ page={currentPage}
359
+ totalPages={totalPages}
360
+ className="foo"
361
+ />
362
+ )
363
+
364
+ const paginationNav = screen.getByRole('navigation');
365
+ expect(paginationNav).toHaveClass('foo');
366
+ });
@@ -0,0 +1,157 @@
1
+ import Icon from "../../common/icon";
2
+
3
+ export const Page: React.FC<SGDS.Component.Pagination.Page> = ({
4
+ ariaLabel,
5
+ current = false,
6
+ href,
7
+ onClick,
8
+ text
9
+ }) => {
10
+ function handleClick(event: React.MouseEvent) {
11
+ if (typeof onClick === 'function') {
12
+ onClick(event);
13
+ }
14
+ }
15
+
16
+ return (
17
+ <li className="ds_pagination__item">
18
+ <a aria-label={ariaLabel}
19
+ className={[
20
+ 'ds_pagination__link',
21
+ current ? 'ds_current' : undefined
22
+ ].join(' ')}
23
+ href={href}
24
+ aria-current={current ? "page" : undefined}
25
+ onClick={handleClick}
26
+ >
27
+ <span className="ds_pagination__link-label">{text}</span>
28
+ </a>
29
+ </li>
30
+ );
31
+ };
32
+
33
+ export const Ellipsis = () => {
34
+ return (
35
+ <li className="ds_pagination__item" aria-hidden="true">
36
+ <span className="ds_pagination__link ds_pagination__link--ellipsis">&hellip;</span>
37
+ </li>
38
+ );
39
+ };
40
+
41
+ const Pagination: React.FC<SGDS.Component.Pagination> = ({
42
+ ariaLabel = 'Pages',
43
+ className,
44
+ onClick,
45
+ padding = 1,
46
+ page = 1,
47
+ pattern = '/search?page=$1',
48
+ totalPages,
49
+ ...props
50
+ }) => {
51
+ padding = Number(padding);
52
+ page = Number(page);
53
+
54
+ const minToShow = padding + 2;
55
+ let includeFirst, includeLast;
56
+ let pages = [];
57
+
58
+ if (page <= minToShow) {
59
+ for (let i = 1; i <= minToShow + 1; i++) {
60
+ pages.push(Number(i));
61
+ }
62
+ } else if (page <= totalPages - minToShow) {
63
+ pages = [page];
64
+ for (let i = 0; i < padding; i++) {
65
+ pages.unshift(page - 1 - i);
66
+ pages.push(Number(page) + 1 + i);
67
+ }
68
+ } else {
69
+ for (let i = totalPages; i > totalPages - minToShow - 1; i--) {
70
+ pages.push(Number(i));
71
+ }
72
+ pages.reverse();
73
+ }
74
+
75
+ // filter out pages that are out of bounds
76
+ pages = pages.filter(item => item > 0 && item <= totalPages);
77
+
78
+ if ((page - padding) > 2) {
79
+ includeFirst = true;
80
+ }
81
+
82
+ if ((page + padding < totalPages - 1)) {
83
+ includeLast = true;
84
+ }
85
+
86
+ return (
87
+ <nav className={[
88
+ 'ds_pagination',
89
+ className
90
+ ].join(' ')}
91
+ aria-label={ariaLabel}
92
+ {...props}
93
+ >
94
+ <ul className="ds_pagination__list">
95
+ {page > 1 && (
96
+ <li className="ds_pagination__item">
97
+ <a aria-label="Previous page" className="ds_pagination__link ds_pagination__link--text ds_pagination__link--icon" href={pattern.replace('$1', String(page - 1))} data-search="pagination-previous" onClick={onClick}>
98
+ <Icon icon="ChevronLeft" />
99
+ <span className="ds_pagination__link-label">Previous</span>
100
+ </a>
101
+ </li>
102
+ )}
103
+
104
+ {includeFirst && (
105
+ <>
106
+ <Page
107
+ ariaLabel="Page 1"
108
+ href={pattern.replace('$1', String(1))}
109
+ onClick={onClick}
110
+ text="1"
111
+ />
112
+ <Ellipsis/>
113
+ </>
114
+ )}
115
+
116
+ {pages && pages.map((item, index: number) => (
117
+ <Page
118
+ ariaLabel={`Page ${item}`}
119
+ current={item === page}
120
+ href={pattern.replace('$1', String(item))}
121
+ key={`pagination${index}`}
122
+ onClick={onClick}
123
+ pattern={pattern}
124
+ text={item.toString()}
125
+ />
126
+ ))}
127
+
128
+ {includeLast && (
129
+ <>
130
+ <Ellipsis/>
131
+ <Page
132
+ ariaLabel={`Page ${totalPages}`}
133
+ href={pattern.replace('$1', String(totalPages))}
134
+ onClick={onClick}
135
+ pattern={pattern}
136
+ text={totalPages.toString()}
137
+ />
138
+ </>
139
+ )}
140
+
141
+ {page < totalPages && (
142
+ <li className="ds_pagination__item">
143
+ <a aria-label="Next page" className="ds_pagination__link ds_pagination__link--text ds_pagination__link--icon" href={pattern.replace('$1', String(page + 1))} data-search="pagination-next" onClick={onClick}>
144
+ <span className="ds_pagination__link-label">Next</span>
145
+ <Icon icon="ChevronRight" />
146
+ </a>
147
+ </li>
148
+ )}
149
+
150
+ </ul>
151
+ </nav>
152
+ )
153
+ }
154
+
155
+ Pagination.displayName = 'Pagination';
156
+
157
+ export default Pagination;
@@ -5,7 +5,7 @@ import PhaseBanner from './phase-banner';
5
5
  const text = 'This is a new service. Your feedback will help us to improve it.';
6
6
  const defaultText = 'This is a new service';
7
7
 
8
- test('inset text renders correctly', () => {
8
+ test('phase banner renders correctly', () => {
9
9
  render(
10
10
  <PhaseBanner>
11
11
  {text}
@@ -188,6 +188,7 @@ test('select with error message', () => {
188
188
 
189
189
  expect(selectWrapper).toHaveClass('ds_input--error')
190
190
  expect(select).toHaveAttribute('aria-describedby', errorMessageElement.id);
191
+ expect(select).toHaveAttribute('aria-invalid', 'true');
191
192
  expect(errorMessageElement).toBeInTheDocument();
192
193
  expect(errorMessageElement).toHaveClass('ds_question__error-message');
193
194
  });
@@ -61,6 +61,7 @@ const Select: React.FC<SGDS.Component.Select> = function ({
61
61
  >
62
62
  <select
63
63
  aria-describedby={describedbys.join(' ')}
64
+ aria-invalid={error}
64
65
  className="ds_select"
65
66
  defaultValue={defaultValue}
66
67
  id={id}
@@ -81,7 +81,7 @@ const SiteSearch: React.FC<SGDS.Component.SiteSearch> = function ({
81
81
  type="search"
82
82
  />
83
83
 
84
- <Button type="submit" icon="search" iconOnly>Search</Button>
84
+ <Button type="submit" icon="Search" iconOnly>Search</Button>
85
85
 
86
86
  {hasAutocomplete && (
87
87
  <div id="autocomplete-suggestions" className="ds_autocomplete__suggestions">
@@ -133,13 +133,13 @@ test('text input with custom currency symbol', () => {
133
133
 
134
134
  test('text input with button', () => {
135
135
  const buttonText = 'Search';
136
- const buttonIcon = 'search';
136
+ const buttonIcon = 'Search';
137
137
  render(
138
138
  <TextInput
139
139
  id={id}
140
140
  label={labelText}
141
- buttonIcon="search"
142
- buttonText="Search"
141
+ buttonIcon={buttonIcon}
142
+ buttonText={buttonText}
143
143
  hasButton
144
144
  />
145
145
  );
@@ -159,7 +159,7 @@ test('text input with button', () => {
159
159
  expect(buttonTextElement).toHaveClass('visually-hidden');
160
160
  expect(buttonTextElement.tagName).toEqual('SPAN');
161
161
 
162
- // todo: check for correct icon
162
+ expect(buttonIconElement).toBeInTheDocument();
163
163
  });
164
164
 
165
165
  test('text input with hint text', () => {
@@ -289,6 +289,7 @@ test('text input with error message', () => {
289
289
 
290
290
  expect(textInput).toHaveClass('ds_input--error')
291
291
  expect(textInput).toHaveAttribute('aria-describedby', errorMessageElement.id);
292
+ expect(textInput).toHaveAttribute('aria-invalid', 'true');
292
293
  expect(errorMessageElement).toBeInTheDocument();
293
294
  expect(errorMessageElement).toHaveClass('ds_question__error-message');
294
295
  });
@@ -71,6 +71,7 @@ const TextInput: React.FC<SGDS.Component.TextInput> = ({
71
71
  >
72
72
  <input
73
73
  aria-describedby={describedbys.join(' ')}
74
+ aria-invalid={error}
74
75
  className={[
75
76
  'ds_input',
76
77
  className,
@@ -194,6 +194,7 @@ test('textarea with error message', () => {
194
194
 
195
195
  expect(textarea).toHaveClass('ds_input--error')
196
196
  expect(textarea).toHaveAttribute('aria-describedby', errorMessageElement.id);
197
+ expect(textarea).toHaveAttribute('aria-invalid', 'true');
197
198
  expect(errorMessageElement).toBeInTheDocument();
198
199
  expect(errorMessageElement).toHaveClass('ds_question__error-message');
199
200
  });
@@ -59,6 +59,7 @@ const Textarea: React.FC<SGDS.Component.Textarea> = ({
59
59
 
60
60
  <textarea
61
61
  aria-describedby={describedbys.join(' ')}
62
+ aria-invalid={error}
62
63
  className={[
63
64
  'ds_input',
64
65
  error && 'ds_input--error',
@@ -0,0 +1,15 @@
1
+ import * as React from "react";
2
+ import type { SVGProps } from "react";
3
+ const SvgArrowUpward = (props: SVGProps<SVGSVGElement>) => (
4
+ <svg
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ viewBox="0 0 24 24"
7
+ fill="#000000"
8
+ role="img"
9
+ {...props}
10
+ >
11
+ <path d="M0 0h24v24H0V0z" fill="none" />
12
+ <path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z" />
13
+ </svg>
14
+ );
15
+ export default SvgArrowUpward;
@@ -0,0 +1,15 @@
1
+ import * as React from "react";
2
+ import type { SVGProps } from "react";
3
+ const SvgCalendarToday = (props: SVGProps<SVGSVGElement>) => (
4
+ <svg
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ viewBox="0 0 24 24"
7
+ fill="#000000"
8
+ role="img"
9
+ {...props}
10
+ >
11
+ <path d="M0 0h24v24H0z" fill="none" />
12
+ <path d="M20 3h-1V1h-2v2H7V1H5v2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 18H4V8h16v13z" />
13
+ </svg>
14
+ );
15
+ export default SvgCalendarToday;
@@ -0,0 +1,13 @@
1
+ import * as React from "react";
2
+ import type { SVGProps } from "react";
3
+ const SvgCancel = (props: SVGProps<SVGSVGElement>) => (
4
+ <svg
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ viewBox="0 -960 960 960"
7
+ role="img"
8
+ {...props}
9
+ >
10
+ <path d="m336-280 144-144 144 144 56-56-144-144 144-144-56-56-144 144-144-144-56 56 144 144-144 144 56 56ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Z" />
11
+ </svg>
12
+ );
13
+ export default SvgCancel;