@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,208 @@
1
+ import { test, expect, vi } from 'vitest';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import Select from './select';
4
+
5
+ const id = 'select-component';
6
+ const labelText = 'choose a component';
7
+ const options = [
8
+ {
9
+ text: 'Accordion',
10
+ value: 'accordion'
11
+ },
12
+ {
13
+ text: 'Breadcrumbs',
14
+ value: 'breadcrumbs'
15
+ },
16
+ {
17
+ text: 'Button',
18
+ value: 'button'
19
+ }
20
+ ];
21
+
22
+ test('select renders correctly', () => {
23
+ render(
24
+ <Select
25
+ id={id}
26
+ label={labelText}
27
+ options={options}
28
+ />
29
+ );
30
+
31
+ const select = screen.getByRole('combobox');
32
+ const selectWrapper = select.parentNode;
33
+ const label = selectWrapper?.previousSibling;
34
+ const selectArrow = select.nextSibling;
35
+
36
+ expect(select).toHaveClass('ds_select');
37
+ expect(select.id).toEqual(id);
38
+ expect(select).toHaveAttribute('name', id);
39
+
40
+ expect(selectWrapper).toHaveClass('ds_select-wrapper');
41
+ expect(selectWrapper.tagName).toEqual('DIV');
42
+
43
+ expect(label).toHaveClass('ds_label');
44
+ expect(label).toHaveAttribute('for', id);
45
+
46
+ expect(selectArrow).toHaveClass('ds_select-arrow');
47
+ expect(selectArrow).toHaveAttribute('aria-hidden');
48
+ expect(selectArrow.textContent).toEqual('');
49
+ });
50
+
51
+ test('select with width', () => {
52
+ const width = 'fixed-10';
53
+
54
+ render(
55
+ <Select
56
+ id={id}
57
+ label={labelText}
58
+ options={options}
59
+ width={width}
60
+ />
61
+ );
62
+
63
+ const selectWrapper = screen.getByRole('combobox').parentNode;
64
+ expect(selectWrapper).toHaveClass(`ds_input--${width}`);
65
+ });
66
+
67
+ test('select with hint text', () => {
68
+ const hintText = 'hint text';
69
+
70
+ render(
71
+ <Select
72
+ id={id}
73
+ label={labelText}
74
+ options={options}
75
+ hintText={hintText}
76
+ />
77
+ );
78
+
79
+ const hintTextEl = screen.getByText(hintText);
80
+ const select = screen.getByRole('combobox');
81
+
82
+ expect(hintTextEl).toBeInTheDocument();
83
+ expect(select).toHaveAttribute('aria-describedby', hintTextEl.id);
84
+ });
85
+
86
+ test('select with custom name', () => {
87
+ const name = 'foo';
88
+
89
+ render(
90
+ <Select
91
+ id={id}
92
+ label={labelText}
93
+ options={options}
94
+ name={name}
95
+ />
96
+ );
97
+
98
+ const select = screen.getByRole('combobox');
99
+ expect(select).toHaveAttribute('name', name);
100
+ });
101
+
102
+ test('select with blur function', () => {
103
+ const onBlurFn = vi.fn();
104
+ render(
105
+ <Select
106
+ id={id}
107
+ label={labelText}
108
+ options={options}
109
+ onBlur={onBlurFn}
110
+ />
111
+ );
112
+
113
+ const select = screen.getByRole('combobox');
114
+
115
+ fireEvent.blur(select);
116
+
117
+ expect(onBlurFn).toHaveBeenCalled();
118
+ });
119
+
120
+ test('select with change function', () => {
121
+ const onChangeFn = vi.fn();
122
+ render(
123
+ <Select
124
+ id={id}
125
+ label={labelText}
126
+ options={options}
127
+ onChange={onChangeFn}
128
+ />
129
+ );
130
+
131
+ const select = screen.getByRole('combobox');
132
+
133
+ fireEvent.change(select, {target: {value: 'button'}});
134
+
135
+ expect(onChangeFn).toHaveBeenCalled();
136
+ });
137
+
138
+ test('select with placeholder option', () => {
139
+ const placeholder = 'foo';
140
+
141
+ render(
142
+ <Select
143
+ id={id}
144
+ label={labelText}
145
+ options={options}
146
+ placeholder={placeholder}
147
+ />
148
+ );
149
+
150
+ const select = screen.getByRole('combobox');
151
+ const firstOption = select.childNodes[0];
152
+ expect(firstOption.textContent).toEqual(placeholder);
153
+ expect(firstOption).toHaveAttribute('value', '');
154
+ });
155
+
156
+ test('select with initial value', () => {
157
+ const initialValue = 'button';
158
+
159
+ render(
160
+ <Select
161
+ id={id}
162
+ label={labelText}
163
+ options={options}
164
+ defaultValue={initialValue}
165
+ />
166
+ );
167
+
168
+ const select = screen.getByRole('combobox');
169
+ const selectedOption = [].slice.call(select.childNodes).filter(childNode => childNode.selected)[0];
170
+ expect(selectedOption).toHaveAttribute('value', initialValue);
171
+ });
172
+
173
+ test('select with error message', () => {
174
+ const errorMessage = 'This is a required field';
175
+ render(
176
+ <Select
177
+ id={id}
178
+ label={labelText}
179
+ options={options}
180
+ error
181
+ errorMessage={errorMessage}
182
+ />
183
+ );
184
+
185
+ const select = screen.getByRole('combobox');
186
+ const selectWrapper = select.parentNode;
187
+ const errorMessageElement = screen.getByText(errorMessage);
188
+
189
+ expect(selectWrapper).toHaveClass('ds_input--error')
190
+ expect(select).toHaveAttribute('aria-describedby', errorMessageElement.id);
191
+ expect(errorMessageElement).toBeInTheDocument();
192
+ expect(errorMessageElement).toHaveClass('ds_question__error-message');
193
+ });
194
+
195
+ test('passing additional props', () => {
196
+ render(
197
+ <Select
198
+ id={id}
199
+ label={labelText}
200
+ options={options}
201
+ data-test="foo"
202
+ />
203
+ );
204
+
205
+ const select = screen.getByRole('combobox');
206
+ const selectWrapper = select.parentNode;
207
+ expect(selectWrapper?.dataset.test).toEqual('foo');
208
+ });
@@ -0,0 +1,86 @@
1
+ import ErrorMessage from '../error-message/error-message';
2
+ import HintText from '../../common/hint-text';
3
+
4
+ const Option: React.FC<SGDS.Component.Select.Option> = function ({
5
+ text,
6
+ value
7
+ }) {
8
+ return (
9
+ <option value={value}>{text}</option>
10
+ );
11
+ };
12
+
13
+ const Select: React.FC<SGDS.Component.Select> = function ({
14
+ defaultValue,
15
+ error,
16
+ errorMessage,
17
+ hintText,
18
+ id,
19
+ label,
20
+ name,
21
+ onBlur,
22
+ onChange,
23
+ options,
24
+ placeholder,
25
+ width,
26
+ ...props
27
+ }) {
28
+ const errorMessageId = `error-message-${id}`;
29
+ const hintTextId = `hint-text-${id}`;
30
+ const describedbys: string[] = [];
31
+
32
+ if (hintText) { describedbys.push(hintTextId) };
33
+ if (errorMessage) { describedbys.push(errorMessageId) };
34
+
35
+ function handleBlur(event: React.FocusEvent) {
36
+ if (typeof onBlur === 'function') {
37
+ onBlur(event);
38
+ }
39
+ }
40
+
41
+ function handleChange(event: React.ChangeEvent) {
42
+ if (typeof onChange === 'function') {
43
+ onChange(event);
44
+ }
45
+ }
46
+
47
+ return (
48
+ <>
49
+ <label className="ds_label" htmlFor={id}>{label}</label>
50
+ {hintText && <HintText id={hintTextId} text={hintText} />}
51
+ {errorMessage && <ErrorMessage id={errorMessageId} text={errorMessage}/>}
52
+ <div
53
+ className={[
54
+ "ds_select-wrapper",
55
+ error && 'ds_input--error',
56
+ width && `ds_input--${width}`,
57
+ ].join(' ')}
58
+ {...props}
59
+ >
60
+ <select
61
+ aria-describedby={describedbys.join(' ')}
62
+ className="ds_select"
63
+ defaultValue={defaultValue}
64
+ id={id}
65
+ name={name || id}
66
+ onBlur={handleBlur}
67
+ onChange={handleChange}
68
+ >
69
+ <option value="">{placeholder}</option>
70
+ {options && options.map((option, index: number) => (
71
+ <Option
72
+ value={option.value}
73
+ text={option.text}
74
+ key={`option-${index}`}
75
+ />
76
+ ))}
77
+ </select>
78
+ <span className="ds_select-arrow" aria-hidden="true"></span>
79
+ </div>
80
+ </>
81
+ );
82
+ };
83
+
84
+ Select.displayName = 'Select';
85
+
86
+ export default Select;
@@ -0,0 +1,67 @@
1
+ import { test, expect } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import SequentialNavigation from './sequential-navigation';
4
+
5
+ const nextLinkObj = { title: 'Apply for or renew a Blue Badge?', href: '#prev' }
6
+ const prevLinkObj = { title: 'Apply for or renew a Blue Badge?', href: '#prev' }
7
+
8
+ test('sequential navigation renders correctly', () => {
9
+ render(
10
+ <SequentialNavigation
11
+ next={nextLinkObj}
12
+ previous={prevLinkObj}
13
+ />
14
+ );
15
+
16
+ const sequentialNavigation = screen.getByRole('navigation');
17
+ const prevLink = screen.getAllByRole('link')[0];
18
+ const prevLinkWrapper = prevLink.parentNode;
19
+ const nextLink = screen.getAllByRole('link')[1];
20
+ const nextLinkWrapper = nextLink.parentNode;
21
+
22
+ expect(sequentialNavigation).toHaveClass('ds_sequential-nav');
23
+ expect(sequentialNavigation).toHaveAttribute('aria-label', 'Article navigation');
24
+
25
+ expect(prevLink).toHaveClass('ds_sequential-nav__button', 'ds_sequential-nav__button--left');
26
+ expect(prevLink).toHaveAttribute('href', prevLinkObj.href);
27
+ expect(prevLink.textContent).toEqual(prevLinkObj.title);
28
+ expect(prevLinkWrapper).toHaveClass('ds_sequential-nav__item', 'ds_sequential-nav__item--prev');
29
+ expect(prevLinkWrapper.tagName).toEqual('DIV');
30
+ expect(prevLink.childNodes[0]).toHaveAttribute('data-label', 'Previous')
31
+
32
+ expect(nextLink).toHaveClass('ds_sequential-nav__button', 'ds_sequential-nav__button--right');
33
+ expect(nextLink).toHaveAttribute('href', nextLinkObj.href);
34
+ expect(nextLink.textContent).toEqual(nextLinkObj.title);
35
+ expect(nextLinkWrapper).toHaveClass('ds_sequential-nav__item', 'ds_sequential-nav__item--next');
36
+ expect(nextLinkWrapper.tagName).toEqual('DIV');
37
+ expect(nextLink.childNodes[0]).toHaveAttribute('data-label', 'Next')
38
+ });
39
+
40
+ test('with custom aria label', () => {
41
+ const ariaLabel = 'My label';
42
+
43
+ render(
44
+ <SequentialNavigation
45
+ ariaLabel={ariaLabel}
46
+ next={nextLinkObj}
47
+ previous={prevLinkObj}
48
+ />
49
+ );
50
+
51
+ const sequentialNavigation = screen.getByRole('navigation');
52
+
53
+ expect(sequentialNavigation).toHaveAttribute('aria-label', ariaLabel);
54
+ });
55
+
56
+ test('passing additional props', () => {
57
+ render(
58
+ <SequentialNavigation
59
+ data-test="foo"
60
+ next={nextLinkObj}
61
+ previous={prevLinkObj}
62
+ />
63
+ );
64
+
65
+ const sequentialNavigation = screen.getByRole('navigation');
66
+ expect(sequentialNavigation?.dataset.test).toEqual('foo');
67
+ });
@@ -0,0 +1,55 @@
1
+ const NextLink: React.FC<SGDS.Component.SequentialNavigation.Link> = ({
2
+ href,
3
+ title
4
+ }) => {
5
+ return (
6
+ <div
7
+ className="ds_sequential-nav__item ds_sequential-nav__item--next"
8
+ >
9
+ <a href={href} className="ds_sequential-nav__button ds_sequential-nav__button--right">
10
+ <span className="ds_sequential-nav__text" data-label="Next">
11
+ {title}
12
+ </span>
13
+ </a>
14
+ </div>
15
+ );
16
+ };
17
+
18
+ const PrevLink: React.FC<SGDS.Component.SequentialNavigation.Link> = ({
19
+ href,
20
+ title,
21
+ }) => {
22
+ return (
23
+ <div
24
+ className="ds_sequential-nav__item ds_sequential-nav__item--prev"
25
+ >
26
+ <a href={href} className="ds_sequential-nav__button ds_sequential-nav__button--left">
27
+ <span className="ds_sequential-nav__text" data-label="Previous">
28
+ {title}
29
+ </span>
30
+ </a>
31
+ </div>
32
+ );
33
+ };
34
+
35
+ const SequentialNavigation: React.FC<SGDS.Component.SequentialNavigation> = ({
36
+ ariaLabel = 'Article navigation',
37
+ next,
38
+ previous,
39
+ ...props
40
+ }) => {
41
+ return (
42
+ <nav
43
+ className="ds_sequential-nav"
44
+ aria-label={ariaLabel}
45
+ {...props}
46
+ >
47
+ {previous && <PrevLink href={previous.href} title={previous.title}></PrevLink>}
48
+ {next && <NextLink href={next.href} title={next.title}></NextLink>}
49
+ </nav>
50
+ );
51
+ };
52
+
53
+ SequentialNavigation.displayName = 'SequentialNavigation';
54
+
55
+ export default SequentialNavigation;
@@ -0,0 +1,156 @@
1
+ import { test, expect } from 'vitest';
2
+ import { render, screen, within } from '@testing-library/react';
3
+ import SideNavigation, { List, Link } from './side-navigation';
4
+
5
+ const items = [
6
+ {
7
+ title: 'Apples',
8
+ href: '#apples',
9
+ items: [
10
+ {
11
+ title: 'Green apples',
12
+ href: '#green-apples',
13
+ items: [
14
+ {
15
+ title: 'Bramley',
16
+ current: true
17
+ },
18
+ {
19
+ title: 'Granny Smith',
20
+ href: '#granny-smith'
21
+ }
22
+ ]
23
+ },
24
+ {
25
+ title: 'Red apples',
26
+ href: '#red-apples'
27
+ }
28
+ ]
29
+ },
30
+ {
31
+ title: 'Bananas',
32
+ href: '#bananas'
33
+ },
34
+ {
35
+ title: 'Cherries',
36
+ href: '#cherries'
37
+ },
38
+ {
39
+ title: 'Dates',
40
+ href: '#dates'
41
+ }
42
+ ];
43
+
44
+ test('side navigation renders correctly', () => {
45
+ render(
46
+ <SideNavigation items={items} />
47
+ );
48
+
49
+ const sideNavigation = screen.getByRole('navigation');
50
+ const toggle = within(sideNavigation).getByRole('checkbox');
51
+ const label = document.querySelector('.ds_side-navigation__expand');
52
+ const rootList = within(sideNavigation).getAllByRole('list')[0];
53
+
54
+ expect(sideNavigation).toHaveClass('ds_side-navigation');
55
+ expect(sideNavigation).toHaveAttribute('aria-label', 'Sections');
56
+
57
+ expect(toggle).toHaveClass('fully-hidden', 'js-toggle-side-navigation');
58
+ expect(toggle).toHaveAttribute('id', 'show-side-navigation');
59
+ expect(toggle).toHaveAttribute('aria-controls', 'side-navigation-root');
60
+
61
+ expect(label).toHaveClass('ds_link');
62
+ expect(label).toHaveAttribute('for', toggle.id);
63
+ expect(label.textContent).toEqual('Show all Pages in this section');
64
+
65
+ expect(rootList).toHaveAttribute('id', 'side-navigation-root');
66
+
67
+ // some specifics of this case
68
+ expect(rootList.children.length).toEqual(4);
69
+ expect(document.querySelectorAll('.ds_side-navigation__link').length).toEqual(8);
70
+ expect(document.querySelectorAll('.ds_side-navigation__list').length).toEqual(3);
71
+ });
72
+
73
+ test('side nav link renders correctly', () => {
74
+ render(
75
+ <Link title="Green apples" href="#green-apples" />
76
+ );
77
+
78
+ const item = screen.getByRole('listitem');
79
+ const link = within(item).getByRole('link');
80
+
81
+ expect(item).toHaveClass('ds_side-navigation__item');
82
+
83
+ expect(link).toHaveClass('ds_side-navigation__link');
84
+ expect(link).toHaveAttribute('href', '#green-apples');
85
+ expect(link.textContent).toEqual('Green apples');
86
+ });
87
+
88
+ test('current side nav item without link renders correctly', () => {
89
+ render(
90
+ <Link title="Green apples" href="#green-apples" current />
91
+ );
92
+
93
+ const item = screen.getByRole('listitem');
94
+ const span = within(item).getByText('Green apples');
95
+
96
+ expect(span).toHaveClass('ds_side-navigation__link', 'ds_current');
97
+ expect(span.tagName).toEqual('SPAN');
98
+ });
99
+
100
+ test('side nav link with children', () => {
101
+ render(
102
+ <Link title="Green apples" href="#green-apples" items={[{
103
+ title: 'Bramley',
104
+ href: '#bramley'
105
+ },
106
+ {
107
+ title: 'Granny Smith',
108
+ href: '#granny-smith'
109
+ }]} />
110
+ );
111
+
112
+ const childList = screen.getByRole('list');
113
+ const childItem = screen.getAllByRole('listitem')[1];
114
+ const childLink = within(childItem).getByRole('link');
115
+
116
+ expect(childList).toHaveClass('ds_side-navigation__list');
117
+ expect(childList.children.length).toEqual(2);
118
+
119
+ // check properties of first child link
120
+ expect(childItem).toHaveClass('ds_side-navigation__item');
121
+ expect(childLink).toHaveClass('ds_side-navigation__link');
122
+ expect(childLink).toHaveAttribute('href', '#bramley');
123
+ expect(childLink.textContent).toEqual('Bramley');
124
+ });
125
+
126
+ test('side nav list renders correctly', () => {
127
+ const items = [
128
+ {
129
+ title: 'Bramley',
130
+ href: '#bramley'
131
+ },
132
+ {
133
+ title: 'Granny Smith',
134
+ href: '#granny-smith'
135
+ }
136
+ ];
137
+
138
+ render(
139
+ <List items={items}/>
140
+ );
141
+
142
+ const list = screen.getByRole('list');
143
+ const item = screen.getAllByRole('listitem')[0];
144
+ const link = within(item).getByRole('link');
145
+
146
+ expect(list).toHaveClass('ds_side-navigation__list');
147
+ expect(list.tagName).toEqual('UL');
148
+
149
+ expect(list.children.length).toEqual(items.length);
150
+
151
+ // check properties of first link
152
+ expect(item).toHaveClass('ds_side-navigation__item');
153
+ expect(link).toHaveClass('ds_side-navigation__link');
154
+ expect(link).toHaveAttribute('href', '#bramley');
155
+ expect(link.textContent).toEqual('Bramley');
156
+ });
@@ -0,0 +1,85 @@
1
+ import { useEffect, useRef } from 'react';
2
+ // @ts-ignore
3
+ import DSSideNavigation from '@scottish-government/design-system/src/components/side-navigation/side-navigation';
4
+
5
+ export const List: React.FC<SGDS.Component.SideNavigation.List> = function ({
6
+ items,
7
+ root
8
+ }) {
9
+ return (
10
+ <ul className="ds_side-navigation__list"
11
+ id={root ? 'side-navigation-root' : undefined }
12
+ >
13
+ {items && items.map((item, index: number) => (
14
+ <Link
15
+ title={item.title}
16
+ href={item.href}
17
+ items={item.items}
18
+ current={item.current}
19
+ key={'sidenavitem' + index}
20
+ />
21
+ ))}
22
+ </ul>
23
+ );
24
+ };
25
+
26
+ export const Link: React.FC<SGDS.Component.SideNavigation.Link> = function ({
27
+ current = false,
28
+ href,
29
+ items,
30
+ title
31
+ }) {
32
+ return (
33
+ <li
34
+ className={[
35
+ 'ds_side-navigation__item',
36
+ items && 'ds_side-navigation__item--has-children'
37
+ ].join(' ')}
38
+ >
39
+ {current ?
40
+ <span className="ds_side-navigation__link ds_current">{title}</span> :
41
+ <a href={href} className="ds_side-navigation__link">{title}</a>
42
+ }
43
+
44
+ {items && <List items={items} />}
45
+ </li>
46
+ );
47
+ };
48
+
49
+ const SideNavigation: React.FC<SGDS.Component.SideNavigation> = function ({
50
+ children,
51
+ items,
52
+ ...props
53
+ }) {
54
+ const ref = useRef(null);
55
+
56
+ useEffect(() => {
57
+ if (ref.current) {
58
+ new DSSideNavigation(ref.current).init();
59
+ }
60
+ }, [ref]);
61
+
62
+ return (
63
+ <nav
64
+ aria-label="Sections"
65
+ className="ds_side-navigation"
66
+ data-module="ds-side-navigation"
67
+ ref={ref}
68
+ {...props}
69
+ >
70
+ <input type="checkbox" className="fully-hidden js-toggle-side-navigation" id="show-side-navigation" aria-controls="side-navigation-root" />
71
+ <label className="ds_side-navigation__expand ds_link" htmlFor="show-side-navigation">
72
+ <span className="visually-hidden">Show all</span> Pages in this section
73
+ <span className="ds_side-navigation__expand-indicator"></span>
74
+ </label>
75
+
76
+ {items && <List root items={items} />}
77
+ </nav>
78
+ );
79
+ };
80
+
81
+ SideNavigation.displayName = 'SideNavigation';
82
+ Link.displayName = 'SideNavLink';
83
+ List.displayName = 'SideNavList';
84
+
85
+ export default SideNavigation;