@scottish-government/designsystem-react 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +12 -0
- package/.github/workflows/release-package.yml +96 -0
- package/@types/common/ConditionalWrapper.d.ts +6 -0
- package/@types/common/HintText.d.ts +6 -0
- package/@types/common/Icon.d.ts +11 -0
- package/@types/common/ScreenReaderText.d.ts +4 -0
- package/@types/common/WrapperTag.d.ts +5 -0
- package/@types/components/Accordion.d.ts +15 -0
- package/@types/components/AspectBox.d.ts +5 -0
- package/@types/components/BackToTop.d.ts +5 -0
- package/@types/components/Breadcrumbs.d.ts +14 -0
- package/@types/components/Button.d.ts +17 -0
- package/@types/components/Checkbox.d.ts +13 -0
- package/@types/components/ConfirmationMessage.d.ts +7 -0
- package/@types/components/ContentsNav.d.ts +15 -0
- package/@types/components/DatePicker.d.ts +19 -0
- package/@types/components/Details.d.ts +6 -0
- package/@types/components/ErrorMessage.d.ts +6 -0
- package/@types/components/Metadata.d.ts +11 -0
- package/@types/components/NotificationBanner.d.ts +9 -0
- package/@types/components/NotificationPanel.d.ts +7 -0
- package/@types/components/PageHeader.d.ts +6 -0
- package/@types/components/PhaseBanner.d.ts +5 -0
- package/@types/components/Question.d.ts +11 -0
- package/@types/components/RadioButton.d.ts +15 -0
- package/@types/components/Select.d.ts +14 -0
- package/@types/components/SequentialNavigation.d.ts +14 -0
- package/@types/components/SideNavigation.d.ts +19 -0
- package/@types/components/SiteNavigation.d.ts +13 -0
- package/@types/components/SiteSearch.d.ts +14 -0
- package/@types/components/SkipLinks.d.ts +14 -0
- package/@types/components/Tag.d.ts +7 -0
- package/@types/components/TaskList.d.ts +21 -0
- package/@types/components/TextInput.d.ts +12 -0
- package/@types/components/Textarea.d.ts +4 -0
- package/@types/global.d.ts +1 -0
- package/@types/sgds.d.ts +35 -0
- package/package.json +36 -0
- package/src/common/conditional-wrapper.test.tsx +36 -0
- package/src/common/conditional-wrapper.tsx +9 -0
- package/src/common/hint-text.test.tsx +47 -0
- package/src/common/hint-text.tsx +21 -0
- package/src/common/icon.test.tsx +100 -0
- package/src/common/icon.tsx +28 -0
- package/src/common/screen-reader-text.test.tsx +31 -0
- package/src/common/screen-reader-text.tsx +17 -0
- package/src/common/wrapper-tag.test.tsx +42 -0
- package/src/common/wrapper-tag.tsx +15 -0
- package/src/components/accordion/accordion.test.tsx +212 -0
- package/src/components/accordion/accordion.tsx +108 -0
- package/src/components/aspect-box/aspect-box.test.tsx +81 -0
- package/src/components/aspect-box/aspect-box.tsx +57 -0
- package/src/components/back-to-top/back-to-top.test.tsx +45 -0
- package/src/components/back-to-top/back-to-top.tsx +33 -0
- package/src/components/breadcrumbs/breadcrumbs.test.tsx +77 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +53 -0
- package/src/components/button/button.test.tsx +125 -0
- package/src/components/button/button.tsx +48 -0
- package/src/components/checkbox/checkbox.test.tsx +180 -0
- package/src/components/checkbox/checkbox.tsx +107 -0
- package/src/components/confirmation-message/confirmation-message.test.tsx +46 -0
- package/src/components/confirmation-message/confirmation-message.tsx +32 -0
- package/src/components/contents-nav/contents-nav.test.tsx +136 -0
- package/src/components/contents-nav/contents-nav.tsx +54 -0
- package/src/components/date-picker/date-picker.test.tsx +209 -0
- package/src/components/date-picker/date-picker.tsx +129 -0
- package/src/components/details/details.test.tsx +38 -0
- package/src/components/details/details.tsx +25 -0
- package/src/components/error-message/error-message.test.tsx +40 -0
- package/src/components/error-message/error-message.tsx +23 -0
- package/src/components/inset-text/inset-text.test.tsx +33 -0
- package/src/components/inset-text/inset-text.tsx +19 -0
- package/src/components/notification-banner/notification-banner.test.tsx +93 -0
- package/src/components/notification-banner/notification-banner.tsx +70 -0
- package/src/components/notification-panel/notification-panel.test.tsx +77 -0
- package/src/components/notification-panel/notification-panel.tsx +31 -0
- package/src/components/page-header/page-header.test.tsx +48 -0
- package/src/components/page-header/page-header.tsx +22 -0
- package/src/components/page-metadata/page-metadata.test.tsx +56 -0
- package/src/components/page-metadata/page-metadata.tsx +39 -0
- package/src/components/phase-banner/phase-banner.test.tsx +67 -0
- package/src/components/phase-banner/phase-banner.tsx +27 -0
- package/src/components/question/question.test.tsx +69 -0
- package/src/components/question/question.tsx +33 -0
- package/src/components/radio-button/radio-button.test.tsx +190 -0
- package/src/components/radio-button/radio-button.tsx +88 -0
- package/src/components/select/select.test.tsx +208 -0
- package/src/components/select/select.tsx +86 -0
- package/src/components/sequential-navigation/sequential-navigation.test.tsx +67 -0
- package/src/components/sequential-navigation/sequential-navigation.tsx +55 -0
- package/src/components/side-navigation/side-navigation.test.tsx +156 -0
- package/src/components/side-navigation/side-navigation.tsx +85 -0
- package/src/components/site-navigation/site-navigation.test.tsx +63 -0
- package/src/components/site-navigation/site-navigation.tsx +40 -0
- package/src/components/site-search/site-search.test.tsx +153 -0
- package/src/components/site-search/site-search.tsx +97 -0
- package/src/components/skip-links/skip-links.test.tsx +84 -0
- package/src/components/skip-links/skip-links.tsx +39 -0
- package/src/components/tag/tag.test.tsx +45 -0
- package/src/components/tag/tag.tsx +23 -0
- package/src/components/task-list/task-list.test.tsx +409 -0
- package/src/components/task-list/task-list.tsx +132 -0
- package/src/components/text-input/text-input.test.tsx +307 -0
- package/src/components/text-input/text-input.tsx +98 -0
- package/src/components/textarea/textarea.test.tsx +212 -0
- package/src/components/textarea/textarea.tsx +82 -0
- package/src/components/warning-text/warning-text.test.tsx +40 -0
- package/src/components/warning-text/warning-text.tsx +21 -0
- package/tsconfig.json +45 -0
- package/vite.config.ts +12 -0
- package/vitest-setup.ts +13 -0
|
@@ -0,0 +1,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;
|