@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,47 @@
1
+ import { test, expect } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import HintText from './hint-text';
4
+
5
+ const id = 'hint-text';
6
+ const content = 'Hint text';
7
+
8
+ test('hint test renders correctly', () => {
9
+ render(
10
+ <HintText data-testid="hint-text"
11
+ id={id}
12
+ text={content}
13
+ />
14
+ );
15
+
16
+ const hintText = screen.getByTestId('hint-text');
17
+ expect(hintText).toHaveClass('ds_hint-text');
18
+ expect(hintText).toHaveAttribute('id', id);
19
+ expect(hintText.tagName).toEqual('P');
20
+ expect(hintText.textContent).toEqual(content);
21
+ });
22
+
23
+ test('hint test with children instead of text', () => {
24
+ render(
25
+ <HintText data-testid="hint-text"
26
+ id={id}
27
+ >
28
+ <span>{content}</span>
29
+ </HintText>
30
+ );
31
+
32
+ const hintText = screen.getByTestId('hint-text');
33
+ expect(hintText.innerHTML).toEqual(`<span>${content}</span>`);
34
+ });
35
+
36
+ test('passing additional props', () => {
37
+ render(
38
+ <HintText data-testid="hint-text"
39
+ id={id}
40
+ text={content}
41
+ data-test="foo"
42
+ />
43
+ );
44
+
45
+ const hintText = screen.getByTestId('hint-text');
46
+ expect(hintText?.dataset.test).toEqual('foo');
47
+ });
@@ -0,0 +1,21 @@
1
+ const HintText: React.FC<SGDS.Common.HintText> = ({
2
+ children,
3
+ id,
4
+ text,
5
+ ...props
6
+ }) => {
7
+ return (
8
+ <p
9
+ className="ds_hint-text"
10
+ dangerouslySetInnerHTML={text ? { __html: text } : undefined}
11
+ id={id}
12
+ {...props}
13
+ >
14
+ {!text ? children : null}
15
+ </p>
16
+ );
17
+ };
18
+
19
+ HintText.displayName = 'HintText';
20
+
21
+ export default HintText;
@@ -0,0 +1,100 @@
1
+ import { test, expect } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import Icon from './icon';
4
+
5
+ const defaultIconPath = './icons.stack.svg';
6
+ const iconName = 'search';
7
+
8
+ test('icon renders correctly', () => {
9
+ render(
10
+ <Icon data-testid="icon"
11
+ icon={iconName}
12
+ />
13
+ );
14
+
15
+ const icon = screen.getByRole('img', {hidden: true});
16
+ const use = icon.children[0];
17
+
18
+ expect(icon).toHaveClass('ds_icon');
19
+ expect(icon).toHaveAttribute('aria-hidden');
20
+ expect(icon).toHaveAttribute('role', 'img');
21
+ expect(icon.tagName).toEqual('svg');
22
+
23
+ expect(use).toHaveAttribute('xlink:href', `${defaultIconPath}#${iconName}`)
24
+ expect(use.tagName).toEqual('use')
25
+ });
26
+
27
+ test('icon with class name', () => {
28
+ const className = 'foo';
29
+
30
+ render(
31
+ <Icon data-testid="icon"
32
+ icon={iconName}
33
+ className={className}
34
+ />
35
+ );
36
+
37
+ const icon = screen.getByRole('img', {hidden: true});
38
+
39
+ expect(icon).toHaveClass('foo');
40
+ });
41
+
42
+ test('icon with fill', () => {
43
+ render(
44
+ <Icon data-testid="icon"
45
+ icon={iconName}
46
+ fill
47
+ />
48
+ );
49
+
50
+ const icon = screen.getByRole('img', {hidden: true});
51
+
52
+ expect(icon).toHaveClass('ds_icon--fill');
53
+ });
54
+
55
+ test('icon with custom path', () => {
56
+ const iconPath = '/path/to/icons.stack.svg';
57
+
58
+ render(
59
+ <Icon data-testid="icon"
60
+ icon={iconName}
61
+ iconPath={iconPath}
62
+ />
63
+ );
64
+
65
+ const icon = screen.getByRole('img', {hidden: true});
66
+ const use = icon.children[0];
67
+
68
+ expect(use).toHaveAttribute('xlink:href', `${iconPath}#${iconName}`)
69
+ });
70
+
71
+ test('icon with size', () => {
72
+ const size = 48;
73
+
74
+ render(
75
+ <Icon data-testid="icon"
76
+ icon={iconName}
77
+ iconSize={size}
78
+ />
79
+ );
80
+
81
+ const icon = screen.getByRole('img', {hidden: true});
82
+
83
+ expect(icon).toHaveClass(`ds_icon--${size}`);
84
+ });
85
+
86
+ test('icon with title', () => {
87
+ const title = 'My icon';
88
+
89
+ render(
90
+ <Icon data-testid="icon"
91
+ icon={iconName}
92
+ title={title}
93
+ />
94
+ );
95
+
96
+ const icon = screen.getByRole('img', {hidden: true});
97
+
98
+ expect(icon).toHaveAttribute('aria-label', title);
99
+ expect(icon).not.toHaveAttribute('aria-hidden');
100
+ });
@@ -0,0 +1,28 @@
1
+ const Icon: React.FC<SGDS.Common.Icon> = ({
2
+ className,
3
+ fill,
4
+ icon,
5
+ iconPath = './icons.stack.svg',
6
+ iconSize,
7
+ title
8
+ }) => {
9
+ return (
10
+ <svg
11
+ aria-hidden={title ? undefined : true}
12
+ aria-label={title}
13
+ className={[
14
+ 'ds_icon',
15
+ className,
16
+ fill && 'ds_icon--fill',
17
+ iconSize && `ds_icon--${iconSize}`
18
+ ].join(' ')}
19
+ role="img"
20
+ >
21
+ <use xlinkHref={`${iconPath}#${icon}`} />
22
+ </svg>
23
+ );
24
+ };
25
+
26
+ Icon.displayName = 'Icon';
27
+
28
+ export default Icon;
@@ -0,0 +1,31 @@
1
+ import { test, expect } from 'vitest';
2
+ import { render } from '@testing-library/react';
3
+ import ScreenReaderText from './screen-reader-text';
4
+
5
+ const content = 'My content';
6
+
7
+ test('screen reader text renders correctly', () => {
8
+ render(
9
+ <ScreenReaderText>
10
+ {content}
11
+ </ScreenReaderText>
12
+ );
13
+
14
+ const srtext = document.querySelector('span');
15
+
16
+ expect(srtext).toHaveClass('visually-hidden');
17
+ expect(srtext.textContent).toEqual(content)
18
+ });
19
+
20
+ test('passing additional props', () => {
21
+ render(
22
+ <ScreenReaderText
23
+ data-test="foo"
24
+ >
25
+ {content}
26
+ </ScreenReaderText>
27
+ );
28
+
29
+ const srtext = document.querySelector('span');
30
+ expect(srtext?.dataset.test).toEqual('foo');
31
+ });
@@ -0,0 +1,17 @@
1
+ const ScreenReaderText: React.FC<SGDS.Common.ScreenReaderText> = ({
2
+ children,
3
+ ...props
4
+ }) => {
5
+ return (
6
+ <span
7
+ className="visually-hidden"
8
+ {...props}
9
+ >
10
+ {children}
11
+ </span>
12
+ );
13
+ };
14
+
15
+ ScreenReaderText.displayName = 'ScreenReaderText';
16
+
17
+ export default ScreenReaderText;
@@ -0,0 +1,42 @@
1
+ import { test, expect } from 'vitest';
2
+ import { render } from '@testing-library/react';
3
+ import WrapperTag from './wrapper-tag';
4
+
5
+ const content = 'My content';
6
+
7
+ test('wrapper tag renders correctly', () => {
8
+ render(
9
+ <WrapperTag id="foo">
10
+ {content}
11
+ </WrapperTag>
12
+ );
13
+
14
+ const wrapper = document.querySelector('#foo');
15
+
16
+ expect(wrapper.tagName).toEqual('DIV');
17
+ });
18
+
19
+ test('wrapper tag widh tag name', () => {
20
+ render(
21
+ <WrapperTag id="foo"
22
+ tagName="section"
23
+ >
24
+ {content}
25
+ </WrapperTag>
26
+ );
27
+
28
+ const wrapper = document.querySelector('#foo');
29
+
30
+ expect(wrapper.tagName).toEqual('SECTION');
31
+ });
32
+
33
+ test('passing additional props', () => {
34
+ render(
35
+ <WrapperTag id="foo" data-test="foo">
36
+ {content}
37
+ </WrapperTag>
38
+ );
39
+
40
+ const wrapper = document.querySelector('#foo');
41
+ expect(wrapper?.dataset.test).toEqual('foo');
42
+ });
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Wraps all children in a specified HTML tag.
3
+ */
4
+ const WrapperTag: React.FC<SGDS.Common.WrapperTag> = ({
5
+ children,
6
+ tagName = 'div',
7
+ ...props
8
+ }) => {
9
+ const TagName = tagName;
10
+ return <TagName {...props}>{children}</TagName>;
11
+ };
12
+
13
+ WrapperTag.displayName = 'WrapperTag';
14
+
15
+ export default WrapperTag;
@@ -0,0 +1,212 @@
1
+ import { test, expect } from 'vitest';
2
+ import { render, screen, within } from '@testing-library/react';
3
+ import Accordion, { AccordionItem } from './accordion';
4
+
5
+ const id = 'my-accordion';
6
+ const itemId = 'my-accordion-item';
7
+ const titleText = 'Healthcare for veterans';
8
+ const contentText = `Veterans are entitled to the same healthcare as any citizen. And there
9
+ are health care options and support available specifically for veterans`;
10
+
11
+ test('accordion renders correctly', () => {
12
+ const defaultHeaderLevel = 'h3';
13
+
14
+ render(
15
+ <Accordion id={id} data-testid={id}>
16
+ <AccordionItem id="accordion-1" title="Healthcare for veterans">
17
+ <p>Veterans are entitled to the same healthcare as any citizen. And there
18
+ are health care options and support available specifically for veterans.</p>
19
+ <p>If you have a health condition that’s related to your service, you’re
20
+ entitled to priority treatment based on clinical need.</p>
21
+ </AccordionItem>
22
+ <AccordionItem open id="accordion-2" title="Employability for veterans">
23
+ <p>If you&apos;re looking for a job, there are several organisations that can help
24
+ you <a href="#accordion-link">find a job or develop new skills</a>.</p>
25
+ </AccordionItem>
26
+ <AccordionItem id="accordion-3" title="Housing for veterans">
27
+ <p>If you need <a href="#accordion-link">help finding a place to live</a>{' '}
28
+ there&apos;s support specifically for veterans.</p>
29
+ </AccordionItem>
30
+ </Accordion>
31
+ );
32
+
33
+ // console.log(items.map(item => { console.log(item.id); return item.title; }));
34
+
35
+ const accordion = screen.getByTestId(id);
36
+ const openAllButton = document.querySelector('.ds_accordion__open-all');
37
+ const accordionItems = document.querySelectorAll('.ds_accordion-item');
38
+ const firstAccordionTitle = document.querySelector('.ds_accordion-item__title');
39
+
40
+ expect(accordion).toHaveClass('ds_accordion');
41
+ expect(accordion.tagName).toEqual('DIV');
42
+
43
+ expect(openAllButton).toHaveClass('ds_accordion__open-all', 'ds_link', 'js-open-all');
44
+ expect(openAllButton).toHaveAttribute('type', 'button');
45
+ expect(openAllButton.textContent).toEqual('Open all sections');
46
+ expect(openAllButton.innerHTML).toEqual('Open all <span class="visually-hidden">sections</span>');
47
+
48
+ expect(accordionItems.length).toEqual(3);
49
+
50
+ expect(firstAccordionTitle.tagName).toEqual(defaultHeaderLevel.toUpperCase());
51
+ });
52
+
53
+ test('accordion without open all', () => {
54
+ render(
55
+ <Accordion id={id} data-testid={id} hideOpenAll>
56
+ <AccordionItem id="accordion-1" title="Healthcare for veterans">
57
+ <p>Veterans are entitled to the same healthcare as any citizen. And there
58
+ are health care options and support available specifically for veterans.</p>
59
+ <p>If you have a health condition that’s related to your service, you’re
60
+ entitled to priority treatment based on clinical need.</p>
61
+ </AccordionItem>
62
+ <AccordionItem id="accordion-2" title="Employability for veterans">
63
+ <p>If you&apos;re looking for a job, there are several organisations that can help
64
+ you <a href="#accordion-link">find a job or develop new skills</a>.</p>
65
+ </AccordionItem>
66
+ <AccordionItem id="accordion-3" title="Housing for veterans">
67
+ <p>If you need <a href="#accordion-link">help finding a place to live</a>{' '}
68
+ there&apos;s support specifically for veterans.</p>
69
+ </AccordionItem>
70
+ </Accordion>
71
+ );
72
+
73
+ const openAllButton = document.querySelector('.ds_accordion__open-all');
74
+
75
+ expect(openAllButton).toBeNull();
76
+ });
77
+
78
+ test('empty accordion has no open all', () => {
79
+ render(
80
+ <Accordion id={id} data-testid={id} hideOpenAll>
81
+ </Accordion>
82
+ );
83
+
84
+ const openAllButton = document.querySelector('.ds_accordion__open-all');
85
+
86
+ expect(openAllButton).toBeNull();
87
+ })
88
+
89
+ test('accordion with custom header level', () => {
90
+ const headerLevel = 'h2';
91
+
92
+ render(
93
+ <Accordion id={id} data-testid={id} headerLevel={headerLevel}>
94
+ <AccordionItem id="accordion-1" title="Healthcare for veterans">
95
+ <p>Veterans are entitled to the same healthcare as any citizen. And there
96
+ are health care options and support available specifically for veterans.</p>
97
+ <p>If you have a health condition that’s related to your service, you’re
98
+ entitled to priority treatment based on clinical need.</p>
99
+ </AccordionItem>
100
+ </Accordion>
101
+ );
102
+
103
+ const firstAccordionTitle = document.querySelector('.ds_accordion-item__title');
104
+ expect(firstAccordionTitle.tagName).toEqual(headerLevel.toUpperCase());
105
+ });
106
+
107
+ test('passing additional props to accordion', () => {
108
+ render(
109
+ <Accordion id={id} data-testid={id} data-test="foo">
110
+ </Accordion>
111
+ );
112
+
113
+ const accordion = screen.getByTestId(id);
114
+ expect(accordion?.dataset.test).toEqual('foo');
115
+ });
116
+
117
+ test('accordion item renders correctly', () => {
118
+ render(
119
+ <AccordionItem id={itemId} data-testid={itemId} title={titleText}>
120
+ <p>{contentText}</p>
121
+ </AccordionItem>
122
+ );
123
+
124
+ const accordionItem = screen.getByTestId(itemId);
125
+ const input = within(accordionItem).getByRole('checkbox');
126
+ const header = document.querySelector('.ds_accordion-item__header');
127
+ const title = header?.querySelector('.ds_accordion-item__title');
128
+ const indicator = header?.querySelector('.ds_accordion-item__indicator');
129
+ const label = header.querySelector('.ds_accordion-item__label');
130
+ const body = document.querySelector('.ds_accordion-item__body');
131
+
132
+ expect(accordionItem).toHaveClass('ds_accordion-item');
133
+ expect(accordionItem).toHaveAttribute('id', itemId);
134
+
135
+ expect(input).toHaveClass('ds_accordion-item__control', 'visually-hidden')
136
+ expect(input).toHaveAttribute('id', `${itemId}-control`);
137
+
138
+ expect(header.tagName).toEqual('DIV');
139
+
140
+ expect(title).toHaveAttribute('id', `panel-${itemId}-heading`);
141
+ expect(title.tagName).toEqual('H3');
142
+ expect(title.textContent).toEqual(titleText);
143
+
144
+ expect(indicator.tagName).toEqual('SPAN');
145
+
146
+ expect(label).toHaveAttribute('for', input.id);
147
+ expect(label.tagName).toEqual('LABEL');
148
+ expect(label.textContent).toEqual('Show this section');
149
+ expect(label?.children[0]).toHaveClass('visually-hidden');
150
+
151
+ expect(body.innerHTML).toEqual(`<p>${contentText}</p>`);
152
+ });
153
+
154
+ test('accordion items without ID are given unique IDs', () => {
155
+ render(
156
+ <Accordion id={id} data-testid={id} hideOpenAll>
157
+ <AccordionItem data-testid="item1" title="Healthcare for veterans">
158
+ <p>Veterans are entitled to the same healthcare as any citizen. And there
159
+ are health care options and support available specifically for veterans.</p>
160
+ <p>If you have a health condition that’s related to your service, you’re
161
+ entitled to priority treatment based on clinical need.</p>
162
+ </AccordionItem>
163
+ <AccordionItem data-testid="item2" title="Employability for veterans">
164
+ <p>If you&apos;re looking for a job, there are several organisations that can help
165
+ you <a href="#accordion-link">find a job or develop new skills</a>.</p>
166
+ </AccordionItem>
167
+ <AccordionItem data-testid="item3" title="Housing for veterans">
168
+ <p>If you need <a href="#accordion-link">help finding a place to live</a>{' '}
169
+ there&apos;s support specifically for veterans.</p>
170
+ </AccordionItem>
171
+ </Accordion>
172
+ );
173
+
174
+ const accordionItem1 = screen.getByTestId('item1');
175
+ const accordionItem2 = screen.getByTestId('item2');
176
+ const accordionItem3 = screen.getByTestId('item3');
177
+
178
+ let idModifier = Number(accordionItem1.id.replace('accordion-item-', ''));
179
+
180
+ expect(accordionItem1).toHaveAttribute('id', `accordion-item-${idModifier}`);
181
+ idModifier = idModifier + 1;
182
+ expect(accordionItem2).toHaveAttribute('id', `accordion-item-${idModifier}`);
183
+ idModifier = idModifier + 1;
184
+ expect(accordionItem3).toHaveAttribute('id', `accordion-item-${idModifier}`);
185
+ });
186
+
187
+ test('open accordion item', () => {
188
+ render(
189
+ <AccordionItem open id={itemId} data-testid={itemId} title={titleText}>
190
+ <p>{contentText}</p>
191
+ </AccordionItem>
192
+ );
193
+
194
+ const accordionItem = screen.getByTestId(itemId);
195
+ const input = within(accordionItem).getByRole('checkbox');
196
+
197
+ expect(input).toHaveAttribute('checked');
198
+ });
199
+
200
+ test('passing additional props to accordion item', () => {
201
+ render(
202
+ <AccordionItem id={itemId} data-testid={itemId} title="Healthcare for veterans" data-test="foo">
203
+ <p>Veterans are entitled to the same healthcare as any citizen. And there
204
+ are health care options and support available specifically for veterans.</p>
205
+ <p>If you have a health condition that’s related to your service, you’re
206
+ entitled to priority treatment based on clinical need.</p>
207
+ </AccordionItem>
208
+ );
209
+
210
+ const accordionItem = screen.getByTestId(itemId);
211
+ expect(accordionItem?.dataset.test).toEqual('foo');
212
+ });
@@ -0,0 +1,108 @@
1
+ import React, { Children, useEffect, useRef } from 'react';
2
+ import WrapperTag from '../../common/wrapper-tag';
3
+ // @ts-ignore
4
+ import DSAccordion from '@scottish-government/design-system/src/components/accordion/accordion';
5
+
6
+ let accordionItemCounter = 0;
7
+
8
+ export const AccordionItem: React.FC<SGDS.Component.Accordion.Item> = ({
9
+ children,
10
+ headerLevel = 'h3',
11
+ id: rawId,
12
+ open = false,
13
+ title,
14
+ ...props
15
+ }) => {
16
+ accordionItemCounter = accordionItemCounter + 1;
17
+ const processedId = rawId || `accordion-item-${accordionItemCounter}`;
18
+ return (
19
+ <div
20
+ className="ds_accordion-item"
21
+ id={processedId}
22
+ {...props}
23
+ >
24
+ <input
25
+ aria-labelledby={`panel-${processedId}-heading`}
26
+ className={[
27
+ 'ds_accordion-item__control',
28
+ 'visually-hidden'
29
+ ].join(' ')}
30
+ defaultChecked={open}
31
+ id={`${processedId}-control`}
32
+ type="checkbox"
33
+ />
34
+ <div className="ds_accordion-item__header">
35
+ <WrapperTag
36
+ id={`panel-${processedId}-heading`}
37
+ className="ds_accordion-item__title"
38
+ tagName={headerLevel}
39
+ >
40
+ {title}
41
+ </WrapperTag>
42
+ <span className='ds_accordion-item__indicator' />
43
+ <label
44
+ className="ds_accordion-item__label"
45
+ htmlFor={`${processedId}-control`}
46
+ >
47
+ <span className="visually-hidden">Show this section</span>
48
+ </label>
49
+ </div>
50
+ <div className="ds_accordion-item__body">
51
+ {children}
52
+ </div>
53
+ </div>
54
+ );
55
+ };
56
+
57
+ const Accordion: React.FC<SGDS.Component.Accordion> = ({
58
+ children,
59
+ headerLevel = 'h3',
60
+ hideOpenAll,
61
+ ...props
62
+ }) => {
63
+ const ref = useRef(null);
64
+
65
+ useEffect(() => {
66
+ if (ref.current) {
67
+ new DSAccordion(ref.current).init();
68
+ }
69
+ }, [ref]);
70
+
71
+ if (!children) {
72
+ hideOpenAll = true;
73
+ }
74
+
75
+ function processChild(child: any) {
76
+ return React.cloneElement(child, { headerLevel: headerLevel });
77
+ }
78
+
79
+ return (
80
+ <div
81
+ className='ds_accordion'
82
+ ref={ref}
83
+ {...props}
84
+ >
85
+ { !hideOpenAll && (
86
+ <button
87
+ className={[
88
+ 'ds_accordion__open-all',
89
+ 'ds_link',
90
+ 'js-open-all'
91
+ ].join(' ')}
92
+ type='button'
93
+ >
94
+ Open all
95
+ {' '}
96
+ <span className="visually-hidden">sections</span>
97
+ </button>
98
+ )}
99
+
100
+ {Children.map(children, child => processChild(child))}
101
+ </div>
102
+ );
103
+ };
104
+
105
+ Accordion.displayName = 'Accordion';
106
+ AccordionItem.displayName = 'AccordionItem';
107
+
108
+ export default Accordion;