@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.
Files changed (111) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/workflows/release-package.yml +96 -0
  3. package/@types/common/ConditionalWrapper.d.ts +6 -0
  4. package/@types/common/HintText.d.ts +6 -0
  5. package/@types/common/Icon.d.ts +11 -0
  6. package/@types/common/ScreenReaderText.d.ts +4 -0
  7. package/@types/common/WrapperTag.d.ts +5 -0
  8. package/@types/components/Accordion.d.ts +15 -0
  9. package/@types/components/AspectBox.d.ts +5 -0
  10. package/@types/components/BackToTop.d.ts +5 -0
  11. package/@types/components/Breadcrumbs.d.ts +14 -0
  12. package/@types/components/Button.d.ts +17 -0
  13. package/@types/components/Checkbox.d.ts +13 -0
  14. package/@types/components/ConfirmationMessage.d.ts +7 -0
  15. package/@types/components/ContentsNav.d.ts +15 -0
  16. package/@types/components/DatePicker.d.ts +19 -0
  17. package/@types/components/Details.d.ts +6 -0
  18. package/@types/components/ErrorMessage.d.ts +6 -0
  19. package/@types/components/Metadata.d.ts +11 -0
  20. package/@types/components/NotificationBanner.d.ts +9 -0
  21. package/@types/components/NotificationPanel.d.ts +7 -0
  22. package/@types/components/PageHeader.d.ts +6 -0
  23. package/@types/components/PhaseBanner.d.ts +5 -0
  24. package/@types/components/Question.d.ts +11 -0
  25. package/@types/components/RadioButton.d.ts +15 -0
  26. package/@types/components/Select.d.ts +14 -0
  27. package/@types/components/SequentialNavigation.d.ts +14 -0
  28. package/@types/components/SideNavigation.d.ts +19 -0
  29. package/@types/components/SiteNavigation.d.ts +13 -0
  30. package/@types/components/SiteSearch.d.ts +14 -0
  31. package/@types/components/SkipLinks.d.ts +14 -0
  32. package/@types/components/Tag.d.ts +7 -0
  33. package/@types/components/TaskList.d.ts +21 -0
  34. package/@types/components/TextInput.d.ts +12 -0
  35. package/@types/components/Textarea.d.ts +4 -0
  36. package/@types/global.d.ts +1 -0
  37. package/@types/sgds.d.ts +35 -0
  38. package/package.json +36 -0
  39. package/src/common/conditional-wrapper.test.tsx +36 -0
  40. package/src/common/conditional-wrapper.tsx +9 -0
  41. package/src/common/hint-text.test.tsx +47 -0
  42. package/src/common/hint-text.tsx +21 -0
  43. package/src/common/icon.test.tsx +100 -0
  44. package/src/common/icon.tsx +28 -0
  45. package/src/common/screen-reader-text.test.tsx +31 -0
  46. package/src/common/screen-reader-text.tsx +17 -0
  47. package/src/common/wrapper-tag.test.tsx +42 -0
  48. package/src/common/wrapper-tag.tsx +15 -0
  49. package/src/components/accordion/accordion.test.tsx +212 -0
  50. package/src/components/accordion/accordion.tsx +108 -0
  51. package/src/components/aspect-box/aspect-box.test.tsx +81 -0
  52. package/src/components/aspect-box/aspect-box.tsx +57 -0
  53. package/src/components/back-to-top/back-to-top.test.tsx +45 -0
  54. package/src/components/back-to-top/back-to-top.tsx +33 -0
  55. package/src/components/breadcrumbs/breadcrumbs.test.tsx +77 -0
  56. package/src/components/breadcrumbs/breadcrumbs.tsx +53 -0
  57. package/src/components/button/button.test.tsx +125 -0
  58. package/src/components/button/button.tsx +48 -0
  59. package/src/components/checkbox/checkbox.test.tsx +180 -0
  60. package/src/components/checkbox/checkbox.tsx +107 -0
  61. package/src/components/confirmation-message/confirmation-message.test.tsx +46 -0
  62. package/src/components/confirmation-message/confirmation-message.tsx +32 -0
  63. package/src/components/contents-nav/contents-nav.test.tsx +136 -0
  64. package/src/components/contents-nav/contents-nav.tsx +54 -0
  65. package/src/components/date-picker/date-picker.test.tsx +209 -0
  66. package/src/components/date-picker/date-picker.tsx +129 -0
  67. package/src/components/details/details.test.tsx +38 -0
  68. package/src/components/details/details.tsx +25 -0
  69. package/src/components/error-message/error-message.test.tsx +40 -0
  70. package/src/components/error-message/error-message.tsx +23 -0
  71. package/src/components/inset-text/inset-text.test.tsx +33 -0
  72. package/src/components/inset-text/inset-text.tsx +19 -0
  73. package/src/components/notification-banner/notification-banner.test.tsx +93 -0
  74. package/src/components/notification-banner/notification-banner.tsx +70 -0
  75. package/src/components/notification-panel/notification-panel.test.tsx +77 -0
  76. package/src/components/notification-panel/notification-panel.tsx +31 -0
  77. package/src/components/page-header/page-header.test.tsx +48 -0
  78. package/src/components/page-header/page-header.tsx +22 -0
  79. package/src/components/page-metadata/page-metadata.test.tsx +56 -0
  80. package/src/components/page-metadata/page-metadata.tsx +39 -0
  81. package/src/components/phase-banner/phase-banner.test.tsx +67 -0
  82. package/src/components/phase-banner/phase-banner.tsx +27 -0
  83. package/src/components/question/question.test.tsx +69 -0
  84. package/src/components/question/question.tsx +33 -0
  85. package/src/components/radio-button/radio-button.test.tsx +190 -0
  86. package/src/components/radio-button/radio-button.tsx +88 -0
  87. package/src/components/select/select.test.tsx +208 -0
  88. package/src/components/select/select.tsx +86 -0
  89. package/src/components/sequential-navigation/sequential-navigation.test.tsx +67 -0
  90. package/src/components/sequential-navigation/sequential-navigation.tsx +55 -0
  91. package/src/components/side-navigation/side-navigation.test.tsx +156 -0
  92. package/src/components/side-navigation/side-navigation.tsx +85 -0
  93. package/src/components/site-navigation/site-navigation.test.tsx +63 -0
  94. package/src/components/site-navigation/site-navigation.tsx +40 -0
  95. package/src/components/site-search/site-search.test.tsx +153 -0
  96. package/src/components/site-search/site-search.tsx +97 -0
  97. package/src/components/skip-links/skip-links.test.tsx +84 -0
  98. package/src/components/skip-links/skip-links.tsx +39 -0
  99. package/src/components/tag/tag.test.tsx +45 -0
  100. package/src/components/tag/tag.tsx +23 -0
  101. package/src/components/task-list/task-list.test.tsx +409 -0
  102. package/src/components/task-list/task-list.tsx +132 -0
  103. package/src/components/text-input/text-input.test.tsx +307 -0
  104. package/src/components/text-input/text-input.tsx +98 -0
  105. package/src/components/textarea/textarea.test.tsx +212 -0
  106. package/src/components/textarea/textarea.tsx +82 -0
  107. package/src/components/warning-text/warning-text.test.tsx +40 -0
  108. package/src/components/warning-text/warning-text.tsx +21 -0
  109. package/tsconfig.json +45 -0
  110. package/vite.config.ts +12 -0
  111. package/vitest-setup.ts +13 -0
@@ -0,0 +1,409 @@
1
+ import { test, expect } from 'vitest';
2
+ import { render, screen, within } from '@testing-library/react';
3
+ import TaskList, {Task, TaskGroup} from './task-list';
4
+
5
+ const taskListHeadingText = 'Application incomplete';
6
+
7
+ const tasks = [
8
+ { id: 'task-conditions', statusText: 'Cannot start yet', title: 'Conditions' },
9
+ { id: 'task-medications', statusText: 'Cannot start yet', title: 'Medications' },
10
+ { id: 'task-contacts', statusText: 'Cannot start yet', title: 'Contacts and supporting information' },
11
+ ];
12
+ const taskItem = {
13
+ id: 'task-conditions',
14
+ statusText: 'Cannot start yet',
15
+ title: 'Conditions',
16
+ href: '#foo'
17
+ };
18
+ const taskSummaryContent = 'Tell us about your conditions, symptoms and any sensory issues you have.';
19
+
20
+
21
+ test('task list renders correctly', () => {
22
+ render(
23
+ <TaskList
24
+ title={taskListHeadingText}
25
+ >
26
+ <Task
27
+ id={tasks[0].id}
28
+ statusText={tasks[0].statusText}
29
+ title={tasks[0].title}
30
+ >
31
+ Tell us about your conditions, symptoms and any sensory issues you have.
32
+ </Task>
33
+ <Task
34
+ id={tasks[1].id}
35
+ statusText={tasks[1].statusText}
36
+ title={tasks[1].title}
37
+ >
38
+ Tell us about any medication you need.
39
+ </Task>
40
+ <Task
41
+ id={tasks[2].id}
42
+ statusText={tasks[2].statusText}
43
+ title={tasks[2].title}
44
+ >
45
+ Share any supporting documents and provide details of people we can talk to about you.
46
+ </Task>
47
+ </TaskList>
48
+ );
49
+
50
+ const taskList = screen.getByRole('list');
51
+ const taskListStatus = screen.getByRole('navigation');
52
+ const taskListStatus1 = taskListStatus.children[0];
53
+ const taskListStatus2 = taskListStatus.children[1];
54
+
55
+ const taskListStatusLink = within(taskListStatus).getByRole('link');
56
+ const taskListHeading = screen.getAllByRole('heading')[0];
57
+
58
+ expect(taskListHeading).toHaveClass('ds_task-list-status-heading');
59
+ expect(taskListHeading).toHaveAttribute('id', 'task-list-status');
60
+ expect(taskListHeading.textContent).toEqual(taskListHeadingText);
61
+
62
+ expect(taskListStatus).toHaveClass('ds_task-list-status');
63
+ expect(taskListStatus).toHaveAttribute('aria-labelledby', taskListHeading.id);
64
+
65
+ expect(taskListStatus1.textContent).toEqual('You have completed 0 of 3 sections.');
66
+ expect(taskListStatus1.tagName).toEqual('P');
67
+ expect(taskListStatus2.tagName).toEqual('P');
68
+ expect(taskListStatusLink.tagName).toEqual('A');
69
+ expect(taskListStatusLink.textContent).toEqual('Skip to first incomplete section');
70
+ expect(taskListStatusLink).toHaveAttribute('href', `#${tasks[0].id}`);
71
+
72
+ expect(taskList).toHaveClass('ds_task-list');
73
+ expect(taskList.tagName).toEqual('UL');
74
+ expect(taskList.children.length).toEqual(tasks.length);
75
+ });
76
+
77
+ test('task list with custom ID', () => {
78
+ const headingId = 'my-id';
79
+ render(
80
+ <TaskList
81
+ title={taskListHeadingText}
82
+ headingId={headingId}
83
+ >
84
+ </TaskList>
85
+ );
86
+
87
+ const taskListStatus = screen.getByRole('navigation');
88
+ const taskListHeading = screen.getAllByRole('heading')[0];
89
+
90
+ expect(taskListHeading).toHaveAttribute('id', `${headingId}-status`);
91
+ expect(taskListStatus).toHaveAttribute('aria-labelledby', taskListHeading.id);
92
+ });
93
+
94
+ test('task list with completed tasks', () => {
95
+ render(
96
+ <TaskList
97
+ title={taskListHeadingText}
98
+ >
99
+ <Task
100
+ id={tasks[0].id}
101
+ statusText={tasks[0].statusText}
102
+ title={tasks[0].title}
103
+ isComplete
104
+ >
105
+ Tell us about your conditions, symptoms and any sensory issues you have.
106
+ </Task>
107
+ <Task
108
+ id={tasks[1].id}
109
+ statusText={tasks[1].statusText}
110
+ title={tasks[1].title}
111
+ isComplete
112
+ >
113
+ Tell us about any medication you need.
114
+ </Task>
115
+ <Task
116
+ id={tasks[2].id}
117
+ statusText={tasks[2].statusText}
118
+ title={tasks[2].title}
119
+ >
120
+ Share any supporting documents and provide details of people we can talk to about you.
121
+ </Task>
122
+ </TaskList>
123
+ );
124
+
125
+ const taskListStatus = screen.getByRole('navigation');
126
+ const taskListStatus1 = taskListStatus.children[0];
127
+
128
+ const taskListStatusLink = within(taskListStatus).getByRole('link');
129
+
130
+ expect(taskListStatus1.textContent).toEqual('You have completed 2 of 3 sections.');
131
+ expect(taskListStatusLink).toHaveAttribute('href', `#${tasks[2].id}`);
132
+ });
133
+
134
+ test('task renders correctly', () => {
135
+ render(
136
+ <Task
137
+ id={taskItem.id}
138
+ statusText={taskItem.statusText}
139
+ title={taskItem.title}
140
+ >
141
+ {taskSummaryContent}
142
+ </Task>
143
+ );
144
+
145
+ const task = screen.getByRole('listitem');
146
+ const tag = document.querySelector('.ds_tag');
147
+ const taskHeading = within(task).getByRole('heading');
148
+ const taskDetails = taskHeading.parentNode;
149
+ const taskSummary = taskHeading.nextSibling;
150
+
151
+ expect(task).toHaveClass('ds_task-list__task');
152
+ expect(task).toHaveAttribute('id', taskItem.id);
153
+
154
+ expect(taskDetails).toHaveClass('ds_task-list__task-details');
155
+ expect(taskDetails.parentNode).toEqual(task);
156
+ expect(taskDetails.tagName).toEqual('DIV');
157
+
158
+ expect(taskHeading).toHaveClass('ds_task-list__task-heading');
159
+ expect(taskHeading.tagName).toEqual('H3');
160
+ expect(taskHeading.textContent).toEqual(`${taskItem.title}(${taskItem.statusText})`);
161
+
162
+ expect(taskSummary).toHaveClass('ds_task-list__task-summary');
163
+ expect(taskSummary.tagName).toEqual('P');
164
+ expect(taskSummary.textContent).toEqual(taskSummaryContent);
165
+
166
+ expect(tag).toHaveAttribute('aria-hidden', 'true');
167
+ expect(tag.textContent).toEqual(taskItem.statusText);
168
+ });
169
+
170
+ test('task with link', () => {
171
+ const href = '#foo';
172
+
173
+ render(
174
+ <Task
175
+ id={taskItem.id}
176
+ statusText={taskItem.statusText}
177
+ title={taskItem.title}
178
+ href={href}
179
+ >
180
+ {taskSummaryContent}
181
+ </Task>
182
+ );
183
+
184
+ const task = screen.getByRole('listitem');
185
+ const taskHeading = within(task).getByRole('heading');
186
+ const link = within(task).getByRole('link');
187
+
188
+ expect(link).toHaveClass('ds_task-list__task-link');
189
+ expect(link).toHaveAttribute('href', href);
190
+ expect(link.textContent).toEqual(`${taskItem.title}(${taskItem.statusText})`);
191
+ expect(link.parentNode).toEqual(taskHeading);
192
+ expect(link.textContent).toEqual(taskHeading.textContent);
193
+ });
194
+
195
+ test('completed task has green tag', () => {
196
+ render(
197
+ <Task
198
+ id={taskItem.id}
199
+ statusText={taskItem.statusText}
200
+ title={taskItem.title}
201
+ isComplete
202
+ >
203
+ {taskSummaryContent}
204
+ </Task>
205
+ );
206
+
207
+ const tag = document.querySelector('.ds_tag');
208
+
209
+ expect(tag).toHaveClass('ds_tag--green');
210
+ });
211
+
212
+ test('specific tag colour', () => {
213
+ const tagColour = 'red';
214
+
215
+ render(
216
+ <Task
217
+ id={taskItem.id}
218
+ statusText={taskItem.statusText}
219
+ title={taskItem.title}
220
+ tagColour={tagColour}
221
+ >
222
+ {taskSummaryContent}
223
+ </Task>
224
+ );
225
+
226
+ const tag = document.querySelector('.ds_tag');
227
+
228
+ expect(tag).toHaveClass(`ds_tag--${tagColour}`);
229
+ });
230
+
231
+ test('no status tag when no status text provided', () => {
232
+ render(
233
+ <Task
234
+ id={taskItem.id}
235
+ title={taskItem.title}
236
+ >
237
+ {taskSummaryContent}
238
+ </Task>
239
+ );
240
+
241
+ const tag = document.querySelector('.ds_tag');
242
+
243
+ expect(tag).toBeNull();
244
+ });
245
+
246
+ test('task group renders correctly', () => {
247
+ const taskGroupHeadingText = 'Provide your health details';
248
+
249
+ render(
250
+ <TaskGroup title={taskGroupHeadingText}>
251
+ <Task
252
+ id={tasks[0].id}
253
+ statusText={tasks[0].statusText}
254
+ title={tasks[0].title}
255
+ >
256
+ Tell us about your conditions, symptoms and any sensory issues you have.
257
+ </Task>
258
+ <Task
259
+ id={tasks[1].id}
260
+ statusText={tasks[1].statusText}
261
+ title={tasks[1].title}
262
+ >
263
+ Tell us about any medication you need.
264
+ </Task>
265
+ <Task
266
+ id={tasks[2].id}
267
+ statusText={tasks[2].statusText}
268
+ title={tasks[2].title}
269
+ >
270
+ Share any supporting documents and provide details of people we can talk to about you.
271
+ </Task>
272
+ </TaskGroup>
273
+ );
274
+
275
+ const taskGroup = document.querySelector('.ds_task-list-group__section');
276
+ const taskGroupHeading = taskGroup.querySelector('.ds_task-list-heading');
277
+ const innerTaskList = taskGroup.querySelector('.ds_task-list');
278
+
279
+ expect(taskGroup).toHaveClass('ds_task-list-group__section');
280
+ expect(taskGroup.tagName).toEqual('LI');
281
+
282
+ expect(taskGroupHeading.tagName).toEqual('H2');
283
+ expect(taskGroupHeading.textContent).toEqual(taskGroupHeadingText);
284
+
285
+ // a bit hacky, but list should be last child
286
+ expect(taskGroup.children[taskGroup.children.length - 1]).toEqual(innerTaskList);
287
+ });
288
+
289
+ test('task group with intro text', () => {
290
+ const taskGroupIntroText = 'This information will be used to check what additional benefits you may be able to apply for.';
291
+
292
+ render(
293
+ <TaskGroup title="Provide your health details" intro={taskGroupIntroText}>
294
+ <Task
295
+ id={tasks[0].id}
296
+ statusText={tasks[0].statusText}
297
+ title={tasks[0].title}
298
+ >
299
+ Tell us about your conditions, symptoms and any sensory issues you have.
300
+ </Task>
301
+ <Task
302
+ id={tasks[1].id}
303
+ statusText={tasks[1].statusText}
304
+ title={tasks[1].title}
305
+ >
306
+ Tell us about any medication you need.
307
+ </Task>
308
+ <Task
309
+ id={tasks[2].id}
310
+ statusText={tasks[2].statusText}
311
+ title={tasks[2].title}
312
+ >
313
+ Share any supporting documents and provide details of people we can talk to about you.
314
+ </Task>
315
+ </TaskGroup>
316
+ );
317
+
318
+ const taskGroup = document.querySelector('.ds_task-list-group__section');
319
+ const taskGroupHeading = taskGroup?.querySelector('.ds_task-list-heading');
320
+ const taskGroupIntro = taskGroup?.querySelector('.ds_task-list-intro');
321
+
322
+ expect(taskGroupIntro?.textContent).toEqual(taskGroupIntroText);
323
+ expect(taskGroupIntro?.previousSibling).toEqual(taskGroupHeading);
324
+ });
325
+
326
+ test('task list counts completed items from all groups for its status text and the first incomplete section wraps to the next group sensibly', () => {
327
+ render(
328
+ <TaskList>
329
+ <TaskGroup title="Provide your health details">
330
+ <Task
331
+ id={tasks[0].id}
332
+ statusText={tasks[0].statusText}
333
+ title={tasks[0].title}
334
+ isComplete
335
+ >
336
+ Tell us about your conditions, symptoms and any sensory issues you have.
337
+ </Task>
338
+ <Task
339
+ id={tasks[1].id}
340
+ statusText={tasks[1].statusText}
341
+ title={tasks[1].title}
342
+ isComplete
343
+ >
344
+ Tell us about any medication you need.
345
+ </Task>
346
+ <Task
347
+ id={tasks[2].id}
348
+ statusText={tasks[2].statusText}
349
+ title={tasks[2].title}
350
+ isComplete
351
+ >
352
+ Share any supporting documents and provide details of people we can talk to about you.
353
+ </Task>
354
+ </TaskGroup>
355
+ <TaskGroup title="Provide your health details">
356
+ <Task
357
+ id={tasks[0].id + '2'}
358
+ statusText={tasks[0].statusText}
359
+ title={tasks[0].title}
360
+ >
361
+ Tell us about your conditions, symptoms and any sensory issues you have.
362
+ </Task>
363
+ <Task
364
+ id={tasks[1].id + '2'}
365
+ statusText={tasks[1].statusText}
366
+ title={tasks[1].title}
367
+ >
368
+ Tell us about any medication you need.
369
+ </Task>
370
+ <Task
371
+ id={tasks[2].id + '2'}
372
+ statusText={tasks[2].statusText}
373
+ title={tasks[2].title}
374
+ isComplete
375
+ >
376
+ Share any supporting documents and provide details of people we can talk to about you.
377
+ </Task>
378
+ </TaskGroup>
379
+ </TaskList>
380
+ );
381
+
382
+ const taskListStatus = screen.getByRole('navigation');
383
+ const taskListStatus1 = taskListStatus.children[0];
384
+
385
+ const taskListStatusLink = within(taskListStatus).getByRole('link');
386
+
387
+ expect(taskListStatus1.textContent).toEqual('You have completed 4 of 6 sections.');
388
+ expect(taskListStatusLink).toHaveAttribute('href', `#${tasks[0].id + '2'}`);
389
+ });
390
+
391
+ test('passing additional props to task group', () => {
392
+ render(
393
+ <TaskGroup data-test="foo">
394
+ </TaskGroup>
395
+ );
396
+
397
+ const taskGroup = document.querySelector('.ds_task-list-group__section');
398
+ expect(taskGroup?.dataset.test).toEqual('foo');
399
+ });
400
+
401
+ test('passing additional props to task', () => {
402
+ render(
403
+ <Task data-test="foo">
404
+ </Task>
405
+ );
406
+
407
+ const task = document.querySelector('.ds_task-list__task');
408
+ expect(task?.dataset.test).toEqual('foo');
409
+ });
@@ -0,0 +1,132 @@
1
+ import { Children } from 'react';
2
+ import ConditionalWrapper from '../../common/conditional-wrapper';
3
+ import HintText from '../../common/hint-text';
4
+ import ScreenReaderText from '../../common/screen-reader-text';
5
+ import Tag from '../tag/tag';
6
+
7
+ export const Task: React.FC<SGDS.Component.TaskList.Task> = ({
8
+ children,
9
+ href,
10
+ id,
11
+ isComplete = false,
12
+ statusText,
13
+ tagColour = 'grey',
14
+ title,
15
+ ...props
16
+ }) => {
17
+ if (isComplete) {
18
+ tagColour = 'green';
19
+ }
20
+
21
+ return (
22
+ <li
23
+ className="ds_task-list__task"
24
+ id={id}
25
+ {...props}
26
+ >
27
+ <div className="ds_task-list__task-details">
28
+ <h3 className="ds_task-list__task-heading">
29
+ <ConditionalWrapper
30
+ condition={typeof href !== 'undefined'}
31
+ wrapper={(children: React.JSX.Element) => <a className="ds_task-list__task-link" href={href}>{children}</a>}
32
+ >
33
+ {title}
34
+ {statusText && <ScreenReaderText>({statusText})</ScreenReaderText>}
35
+ </ConditionalWrapper>
36
+ </h3>
37
+ <HintText className="ds_task-list__task-summary">{children}</HintText>
38
+ </div>
39
+
40
+ {typeof statusText !== 'undefined' &&
41
+ <Tag
42
+ aria-hidden="true"
43
+ colour={tagColour}
44
+ title={statusText}
45
+ />
46
+ }
47
+ </li>
48
+ );
49
+ };
50
+
51
+ /**
52
+ * @param {Object} props
53
+ * @param {string} props.intro - Intro text
54
+ * @param {string} props.title - The title of the task group
55
+ * @returns {JSX.Element} - The element
56
+ */
57
+ export const TaskGroup: React.FC<SGDS.Component.TaskList.Group> = ({
58
+ children,
59
+ intro,
60
+ title,
61
+ ...props
62
+ }) => {
63
+ return (
64
+ <li
65
+ className="ds_task-list-group__section"
66
+ {...props}
67
+ >
68
+ <h2 className="ds_task-list-heading">{title}</h2>
69
+ {intro && <p className="ds_task-list-intro">{intro}</p>}
70
+ <ul className="ds_task-list">
71
+ {children}
72
+ </ul>
73
+ </li>
74
+ );
75
+ };
76
+
77
+ const TaskList: React.FC<SGDS.Component.TaskList> = ({
78
+ children,
79
+ headingId = 'task-list',
80
+ title
81
+ }) => {
82
+ let taskCount = 0;
83
+ let incompleteTaskIds: string[] = [];
84
+ let completedTasksCount = 0;
85
+
86
+ function processChild(item: any) {
87
+ if (item.type.displayName === 'Task') {
88
+ taskCount = taskCount + 1;
89
+
90
+ if (item.props.isComplete) {
91
+ completedTasksCount = completedTasksCount + 1;
92
+ } else {
93
+ incompleteTaskIds.push(item.props.id);
94
+ }
95
+ } else if (item.type.displayName === 'TaskGroup') {
96
+ Children.forEach(item.props.children, child => {
97
+ processChild(child);
98
+ });
99
+ }
100
+ }
101
+
102
+ function firstIncompleteTaskLink() {
103
+ if (incompleteTaskIds.length > 0) {
104
+ return (
105
+ <p><a href={'#' + incompleteTaskIds[0]} className="js-task-list-skip-link">Skip to first incomplete section</a></p>
106
+ )
107
+ }
108
+ }
109
+
110
+ Children.forEach(children, child => {
111
+ processChild(child);
112
+ });
113
+
114
+ return (
115
+ <>
116
+ <h2 id={`${headingId}-status`} className="ds_task-list-status-heading">{title}</h2>
117
+ <nav aria-labelledby={`${headingId}-status`} className="ds_task-list-status">
118
+ <p>You have completed {completedTasksCount} of {taskCount} sections.</p>
119
+ {firstIncompleteTaskLink()}
120
+ </nav>
121
+ <ul className="ds_task-list">
122
+ {children}
123
+ </ul>
124
+ </>
125
+ );
126
+ };
127
+
128
+ TaskList.displayName = 'TaskList';
129
+ Task.displayName = 'Task';
130
+ TaskGroup.displayName = 'TaskGroup';
131
+
132
+ export default TaskList;