@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,63 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import { render, screen, within } from '@testing-library/react';
|
|
3
|
+
import SiteNavigation from './site-navigation';
|
|
4
|
+
|
|
5
|
+
const items = [
|
|
6
|
+
{title: 'About', href: '#about'},
|
|
7
|
+
{title: 'Get started', href: '#get-started'},
|
|
8
|
+
{title: 'Styles', href: '#styles'},
|
|
9
|
+
{title: 'Components', href: '#components'},
|
|
10
|
+
{title: 'Patterns', href: '#patterns'},
|
|
11
|
+
{title: 'Guidance', href: '#guidance'},
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
test('renders correctly', () => {
|
|
15
|
+
render(
|
|
16
|
+
<SiteNavigation items={items}/>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const nav = screen.getByRole('navigation');
|
|
20
|
+
const list = within(nav).getByRole('list');
|
|
21
|
+
const listItems = within(list).getAllByRole('listitem');
|
|
22
|
+
|
|
23
|
+
// check nav
|
|
24
|
+
expect(nav).toHaveClass('ds_site-navigation');
|
|
25
|
+
expect(nav.tagName).toEqual('NAV');
|
|
26
|
+
|
|
27
|
+
// check list
|
|
28
|
+
expect(list.tagName).toEqual('UL');
|
|
29
|
+
expect(list).toHaveClass('ds_site-navigation__list');
|
|
30
|
+
|
|
31
|
+
// check items
|
|
32
|
+
expect(listItems.length).toEqual(items.length);
|
|
33
|
+
|
|
34
|
+
listItems.forEach((item, index) => {
|
|
35
|
+
expect(item).toHaveClass('ds_site-navigation__item');
|
|
36
|
+
|
|
37
|
+
const link = within(item).getByRole('link');
|
|
38
|
+
|
|
39
|
+
expect(link).toHaveClass('ds_site-navigation__link');
|
|
40
|
+
expect(link).not.toHaveClass('ds_current');
|
|
41
|
+
expect(link.textContent).toEqual(items[index].title);
|
|
42
|
+
expect(link).toHaveAttribute('href', items[index].href)
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('highlights current item', () => {
|
|
47
|
+
render(
|
|
48
|
+
<SiteNavigation data-test="foo" items={[{title: 'About', href: '#about', current: true}]}/>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const link = screen.getByRole('link');
|
|
52
|
+
|
|
53
|
+
expect(link).toHaveClass('ds_current');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('passing additional props', () => {
|
|
57
|
+
render(
|
|
58
|
+
<SiteNavigation data-test="foo" items={items}/>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const nav = screen.getByRole('navigation');
|
|
62
|
+
expect(nav.dataset.test).toEqual('foo');
|
|
63
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const SiteNavLink: React.FC<SGDS.Component.SiteNavigation.Link> = ({
|
|
2
|
+
current=false,
|
|
3
|
+
href,
|
|
4
|
+
title
|
|
5
|
+
}) => {
|
|
6
|
+
return (
|
|
7
|
+
<li
|
|
8
|
+
className="ds_site-navigation__item"
|
|
9
|
+
>
|
|
10
|
+
<a
|
|
11
|
+
href={href}
|
|
12
|
+
className={[
|
|
13
|
+
'ds_site-navigation__link',
|
|
14
|
+
current ? 'ds_current' : undefined
|
|
15
|
+
].join(' ')}>{title}</a>
|
|
16
|
+
</li>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const SiteNavigation: React.FC<SGDS.Component.SiteNavigation> = ({
|
|
21
|
+
items,
|
|
22
|
+
...props
|
|
23
|
+
}) => {
|
|
24
|
+
return (
|
|
25
|
+
<nav
|
|
26
|
+
className="ds_site-navigation"
|
|
27
|
+
{...props}
|
|
28
|
+
>
|
|
29
|
+
<ul className="ds_site-navigation__list">
|
|
30
|
+
{items && items.map((item, index: number) => (
|
|
31
|
+
<SiteNavLink current={item.current} href={item.href} title={item.title} key={`link-${index}`} />
|
|
32
|
+
))}
|
|
33
|
+
</ul>
|
|
34
|
+
</nav>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
SiteNavigation.displayName = 'SiteNavigation';
|
|
39
|
+
|
|
40
|
+
export default SiteNavigation;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { test, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen, within } from '@testing-library/react';
|
|
3
|
+
import SiteSearch from './site-search';
|
|
4
|
+
|
|
5
|
+
const id = 'site-search';
|
|
6
|
+
const labelText = 'Search';
|
|
7
|
+
const placeholderText = 'Search';
|
|
8
|
+
|
|
9
|
+
test('site search renders correctly', () => {
|
|
10
|
+
render(
|
|
11
|
+
<SiteSearch />
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const searchForm = screen.getByRole('search');
|
|
15
|
+
const searchFormContainer = searchForm.parentNode;
|
|
16
|
+
const searchLabel = document.querySelector('label');
|
|
17
|
+
const searchInput = within(searchForm).getByRole('searchbox');
|
|
18
|
+
const inputWrapper = searchInput.parentNode;
|
|
19
|
+
const searchButton = within(searchForm).getByRole('button');
|
|
20
|
+
|
|
21
|
+
expect(searchFormContainer).toHaveClass('ds_site-search');
|
|
22
|
+
expect(searchFormContainer.tagName).toEqual('DIV');
|
|
23
|
+
expect(searchFormContainer).not.toHaveAttribute('id', 'site-search-autocomplete');
|
|
24
|
+
|
|
25
|
+
expect(searchForm).toHaveClass('ds_site-search__form');
|
|
26
|
+
expect(searchForm).toHaveAttribute('method', 'GET');
|
|
27
|
+
expect(searchForm).toHaveAttribute('action', '/search');
|
|
28
|
+
|
|
29
|
+
expect(searchLabel.textContent).toEqual(labelText);
|
|
30
|
+
expect(searchLabel).toHaveAttribute('for', id);
|
|
31
|
+
expect(searchLabel).toHaveAttribute('id', `${id}-label`);
|
|
32
|
+
expect(searchLabel).toHaveClass('ds_label', 'visually-hidden');
|
|
33
|
+
|
|
34
|
+
expect(inputWrapper).toHaveClass('ds_input__wrapper ds_input__wrapper--has-icon');
|
|
35
|
+
expect(inputWrapper.tagName).toEqual('DIV');
|
|
36
|
+
|
|
37
|
+
expect(searchInput).toHaveClass('ds_input', 'ds_site-search__input');
|
|
38
|
+
expect(searchInput).toHaveAttribute('id', id);
|
|
39
|
+
expect(searchInput).toHaveAttribute('placeholder', placeholderText);
|
|
40
|
+
expect(searchInput).toHaveAttribute('required');
|
|
41
|
+
expect(searchInput).toHaveAttribute('spellcheck', 'false');
|
|
42
|
+
expect(searchInput).toHaveAttribute('type', 'search');
|
|
43
|
+
expect(searchInput).toHaveAttribute('name', 'q');
|
|
44
|
+
expect(searchInput).not.toHaveAttribute('autocomplete');
|
|
45
|
+
|
|
46
|
+
expect(searchButton).toHaveClass('ds_button');
|
|
47
|
+
expect(searchButton).toHaveAttribute('type', 'submit');
|
|
48
|
+
expect(searchButton.textContent).toEqual('Search');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('custom action', () => {
|
|
52
|
+
render(
|
|
53
|
+
<SiteSearch action="/foo" />
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const searchForm = screen.getByRole('search');
|
|
57
|
+
|
|
58
|
+
expect(searchForm).toHaveAttribute('action', '/foo');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('custom id', () => {
|
|
62
|
+
render(
|
|
63
|
+
<SiteSearch id="foo" />
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const searchForm = screen.getByRole('search');
|
|
67
|
+
const searchInput = within(searchForm).getByRole('searchbox');
|
|
68
|
+
|
|
69
|
+
expect(searchInput).toHaveAttribute('id', 'foo');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('custom method', () => {
|
|
73
|
+
render(
|
|
74
|
+
<SiteSearch method="POST" />
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const searchForm = screen.getByRole('search');
|
|
78
|
+
|
|
79
|
+
expect(searchForm).toHaveAttribute('method', 'POST');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('custom name', () => {
|
|
83
|
+
render(
|
|
84
|
+
<SiteSearch name="foo" />
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const searchForm = screen.getByRole('search');
|
|
88
|
+
const searchInput = within(searchForm).getByRole('searchbox');
|
|
89
|
+
|
|
90
|
+
expect(searchInput).toHaveAttribute('name', 'foo');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('custom placeholder', () => {
|
|
94
|
+
render(
|
|
95
|
+
<SiteSearch placeholder="foo" />
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const searchForm = screen.getByRole('search');
|
|
99
|
+
const searchInput = within(searchForm).getByRole('searchbox');
|
|
100
|
+
|
|
101
|
+
expect(searchInput).toHaveAttribute('placeholder', 'foo');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('autocomplete', () => {
|
|
105
|
+
const autocompleteSuggestionMappingFunction = vi.fn();
|
|
106
|
+
const suggestionsId = 'autocomplete-suggestions';
|
|
107
|
+
|
|
108
|
+
render(
|
|
109
|
+
<SiteSearch
|
|
110
|
+
autocompleteEndpoint="/endpoint"
|
|
111
|
+
autocompleteSuggestionMappingFunction={autocompleteSuggestionMappingFunction}
|
|
112
|
+
/>
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
const searchForm = screen.getByRole('search');
|
|
116
|
+
const searchFormContainer = searchForm.parentNode;
|
|
117
|
+
const searchInput = within(searchForm).getByRole('searchbox');
|
|
118
|
+
const autocompleteStatus = within(searchForm).getByRole('status');
|
|
119
|
+
const suggestionsContainer = document.querySelector('.ds_autocomplete__suggestions');
|
|
120
|
+
const suggestionsList = within(searchForm).getByRole('listbox');
|
|
121
|
+
|
|
122
|
+
expect(searchFormContainer).toHaveClass('ds_autocomplete');
|
|
123
|
+
expect(searchFormContainer).toHaveAttribute('id', `${id}-autocomplete`);
|
|
124
|
+
|
|
125
|
+
expect(autocompleteStatus).toBeInTheDocument();
|
|
126
|
+
expect(autocompleteStatus).toHaveClass('visually-hidden');
|
|
127
|
+
expect(autocompleteStatus).toHaveAttribute('aria-live', 'polite');
|
|
128
|
+
expect(autocompleteStatus).toHaveAttribute('id', 'autocomplete-status');
|
|
129
|
+
expect(autocompleteStatus.tagName).toEqual('DIV');
|
|
130
|
+
|
|
131
|
+
expect(searchInput).toHaveAttribute('aria-autocomplete', 'list');
|
|
132
|
+
expect(searchInput).toHaveAttribute('aria-owns', suggestionsId);
|
|
133
|
+
expect(searchInput).toHaveAttribute('autocomplete', 'off');
|
|
134
|
+
expect(searchInput).toHaveClass('js-autocomplete-input');
|
|
135
|
+
|
|
136
|
+
expect(suggestionsContainer).toHaveAttribute('id', suggestionsId);
|
|
137
|
+
expect(suggestionsContainer.tagName).toEqual('DIV');
|
|
138
|
+
|
|
139
|
+
expect(suggestionsList).toHaveClass('ds_autocomplete__suggestions-list');
|
|
140
|
+
expect(suggestionsList).toHaveAttribute('aria-labelledby', `${id}-label`);
|
|
141
|
+
expect(suggestionsList.tagName).toEqual('OL');
|
|
142
|
+
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('passing additional props', () => {
|
|
146
|
+
render(
|
|
147
|
+
<SiteSearch data-test="foo" />
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const searchForm = screen.getByRole('search');
|
|
151
|
+
const searchFormContainer = searchForm.parentNode;
|
|
152
|
+
expect(searchFormContainer?.dataset.test).toEqual('foo');
|
|
153
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
// @ts-ignore
|
|
3
|
+
import DSAutocomplete from '@scottish-government/design-system/src/components/autocomplete/autocomplete';
|
|
4
|
+
import Button from '../button/button';
|
|
5
|
+
|
|
6
|
+
const SiteSearch: React.FC<SGDS.Component.SiteSearch> = function ({
|
|
7
|
+
action = '/search',
|
|
8
|
+
autocompleteEndpoint,
|
|
9
|
+
autocompleteSuggestionMappingFunction,
|
|
10
|
+
id = 'site-search',
|
|
11
|
+
method = 'GET',
|
|
12
|
+
minLength = 3,
|
|
13
|
+
name = 'q',
|
|
14
|
+
placeholder = 'Search',
|
|
15
|
+
...props
|
|
16
|
+
}) {
|
|
17
|
+
const ref = useRef(null);
|
|
18
|
+
const hasAutocomplete = !!autocompleteEndpoint;
|
|
19
|
+
let autocompleteId = hasAutocomplete ? id + '-autocomplete' : '';
|
|
20
|
+
|
|
21
|
+
type AutoCompleteOptions = {
|
|
22
|
+
minLength?: number,
|
|
23
|
+
suggestionMappingFunction?: Function,
|
|
24
|
+
throttleDelay?: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (hasAutocomplete && ref.current) {
|
|
29
|
+
const options: AutoCompleteOptions = {};
|
|
30
|
+
if (minLength) {
|
|
31
|
+
options.minLength = minLength;
|
|
32
|
+
}
|
|
33
|
+
if (autocompleteSuggestionMappingFunction) {
|
|
34
|
+
options.suggestionMappingFunction = autocompleteSuggestionMappingFunction
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const autocomplete = new DSAutocomplete(
|
|
38
|
+
document.getElementById(autocompleteId),
|
|
39
|
+
autocompleteEndpoint,
|
|
40
|
+
options
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
autocomplete.init();
|
|
44
|
+
}
|
|
45
|
+
}, [ref, autocompleteEndpoint, autocompleteId, hasAutocomplete, minLength, autocompleteSuggestionMappingFunction]);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
className={[
|
|
50
|
+
'ds_site-search',
|
|
51
|
+
hasAutocomplete ? 'ds_autocomplete' : undefined
|
|
52
|
+
].join(' ')}
|
|
53
|
+
id={autocompleteId ? autocompleteId : undefined}
|
|
54
|
+
ref={ref}
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
|
|
58
|
+
<form role="search" className="ds_site-search__form" method={method} action={action}>
|
|
59
|
+
<label className="ds_label visually-hidden" htmlFor={id} id={id + '-label'}>Search</label>
|
|
60
|
+
|
|
61
|
+
{hasAutocomplete && (
|
|
62
|
+
<div role="status" aria-live="polite" id="autocomplete-status" className="visually-hidden"></div>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
<div className="ds_input__wrapper ds_input__wrapper--has-icon">
|
|
66
|
+
<input aria-autocomplete={hasAutocomplete ? 'list' : undefined}
|
|
67
|
+
aria-owns={hasAutocomplete ? 'autocomplete-suggestions' : undefined}
|
|
68
|
+
autoComplete={hasAutocomplete ? 'off' : undefined}
|
|
69
|
+
className={[
|
|
70
|
+
'ds_input',
|
|
71
|
+
'ds_site-search__input',
|
|
72
|
+
hasAutocomplete ? 'js-autocomplete-input' : undefined
|
|
73
|
+
].join(' ')}
|
|
74
|
+
id={id}
|
|
75
|
+
name={name}
|
|
76
|
+
placeholder={placeholder}
|
|
77
|
+
required
|
|
78
|
+
spellCheck="false"
|
|
79
|
+
type="search"
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
<Button type="submit" icon="search" iconOnly>Search</Button>
|
|
83
|
+
|
|
84
|
+
{hasAutocomplete && (
|
|
85
|
+
<div id="autocomplete-suggestions" className="ds_autocomplete__suggestions">
|
|
86
|
+
<ol className="ds_autocomplete__suggestions-list" role="listbox" aria-labelledby="site-search-label"></ol>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
</form>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
SiteSearch.displayName = 'SiteSearch';
|
|
96
|
+
|
|
97
|
+
export default SiteSearch;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import { render, screen, within } from '@testing-library/react';
|
|
3
|
+
import SkipLinks from './skip-links';
|
|
4
|
+
|
|
5
|
+
const mainContentId = 'main-content';
|
|
6
|
+
const linkText = 'Skip to main content';
|
|
7
|
+
|
|
8
|
+
test('skip links renders correctly', () => {
|
|
9
|
+
render(
|
|
10
|
+
<SkipLinks />
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const skipLinksList = screen.getByRole('list');
|
|
14
|
+
const skipLinksContainer = skipLinksList.parentNode;
|
|
15
|
+
const skipLinksListItem = within(skipLinksList).getByRole('listitem');
|
|
16
|
+
const skipLinksLink = within(skipLinksList).getByRole('link');
|
|
17
|
+
|
|
18
|
+
expect(skipLinksContainer).toHaveClass('ds_skip-links');
|
|
19
|
+
expect(skipLinksContainer.tagName).toEqual('DIV');
|
|
20
|
+
|
|
21
|
+
expect(skipLinksList).toHaveClass('ds_skip-links__list');
|
|
22
|
+
expect(skipLinksList.tagName).toEqual('UL');
|
|
23
|
+
|
|
24
|
+
expect(skipLinksListItem).toHaveClass('ds_skip-links__item');
|
|
25
|
+
|
|
26
|
+
expect(skipLinksLink).toHaveClass('ds_skip-links__link');
|
|
27
|
+
expect(skipLinksLink).toHaveAttribute('href', `#${mainContentId}`);
|
|
28
|
+
expect(skipLinksLink.textContent).toEqual(linkText);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('custom link text', () => {
|
|
32
|
+
const mainLinkText = 'foo';
|
|
33
|
+
|
|
34
|
+
render(
|
|
35
|
+
<SkipLinks mainLinkText={mainLinkText} />
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const skipLinksList = screen.getByRole('list');
|
|
39
|
+
const skipLinksLink = within(skipLinksList).getByRole('link');
|
|
40
|
+
|
|
41
|
+
expect(skipLinksLink.textContent).toEqual(mainLinkText);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('custom link target', () => {
|
|
45
|
+
const customId = 'bar'
|
|
46
|
+
|
|
47
|
+
render(
|
|
48
|
+
<SkipLinks mainContentId={customId} />
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const skipLinksList = screen.getByRole('list');
|
|
52
|
+
const skipLinksLink = within(skipLinksList).getByRole('link');
|
|
53
|
+
|
|
54
|
+
expect(skipLinksLink).toHaveAttribute('href', `#${customId}`)
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('additional links', () => {
|
|
58
|
+
const items = [
|
|
59
|
+
{ title: 'foo', targetId: 'bar' }
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
render(
|
|
63
|
+
<SkipLinks items={items} />
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const skipLinksList = screen.getByRole('list');
|
|
67
|
+
const skipLinksListItems = within(skipLinksList).getAllByRole('listitem');
|
|
68
|
+
const skipLinksSecondLink = within(skipLinksList).getAllByRole('link')[1];
|
|
69
|
+
|
|
70
|
+
expect(skipLinksListItems.length).toEqual(2);
|
|
71
|
+
expect(skipLinksSecondLink).toHaveAttribute('href', `#${items[0].targetId}`);
|
|
72
|
+
expect(skipLinksSecondLink.textContent).toEqual(items[0].title);
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('passing additional props', () => {
|
|
76
|
+
render(
|
|
77
|
+
<SkipLinks data-test="foo" />
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const skipLinksList = screen.getByRole('list');
|
|
81
|
+
const skipLinksContainer = skipLinksList.parentNode;
|
|
82
|
+
|
|
83
|
+
expect(skipLinksContainer?.dataset.test).toEqual('foo');
|
|
84
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const SkipLink: React.FC<SGDS.Component.SkipLinks.Link> = ({
|
|
2
|
+
targetId,
|
|
3
|
+
title
|
|
4
|
+
}) => {
|
|
5
|
+
return (
|
|
6
|
+
<li
|
|
7
|
+
className="ds_skip-links__item"
|
|
8
|
+
>
|
|
9
|
+
<a href={`#${targetId}`} className="ds_skip-links__link">{ title }</a>
|
|
10
|
+
</li>
|
|
11
|
+
);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const SkipLinks: React.FC<SGDS.Component.SkipLinks> = ({
|
|
15
|
+
items,
|
|
16
|
+
mainContentId = 'main-content',
|
|
17
|
+
mainLinkText = 'Skip to main content',
|
|
18
|
+
...props
|
|
19
|
+
}) => {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className="ds_skip-links"
|
|
23
|
+
{...props}
|
|
24
|
+
>
|
|
25
|
+
<ul className="ds_skip-links__list">
|
|
26
|
+
<SkipLink title={mainLinkText} targetId={mainContentId}/>
|
|
27
|
+
|
|
28
|
+
{items && items.map((item, index: number) => (
|
|
29
|
+
<SkipLink title={item.title} targetId={item.targetId} key={`skiplink-${index}`}/>
|
|
30
|
+
))}
|
|
31
|
+
</ul>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
SkipLinks.displayName = 'SkipLinks';
|
|
37
|
+
SkipLink.displayName = 'SkipLink';
|
|
38
|
+
|
|
39
|
+
export default SkipLinks;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { test, expect } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import Tag from './tag';
|
|
4
|
+
|
|
5
|
+
const tagText = 'Beta';
|
|
6
|
+
|
|
7
|
+
test('tag renders correctly', () => {
|
|
8
|
+
render(
|
|
9
|
+
<Tag title={tagText}/>
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const tag = screen.getByText(tagText);
|
|
13
|
+
|
|
14
|
+
expect(tag).toHaveClass('ds_tag');
|
|
15
|
+
expect(tag.nodeName).toEqual('SPAN');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('tag with custom colour', () => {
|
|
19
|
+
render(
|
|
20
|
+
<Tag colour="red" title={tagText}/>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const tag = screen.getByText(tagText);
|
|
24
|
+
|
|
25
|
+
expect(tag).toHaveClass('ds_tag--red');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('tag with additional CSS class', () => {
|
|
29
|
+
render(
|
|
30
|
+
<Tag className="foo" title={tagText}/>
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const tag = screen.getByText(tagText);
|
|
34
|
+
|
|
35
|
+
expect(tag).toHaveClass('foo');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('passing additional props', () => {
|
|
39
|
+
render(
|
|
40
|
+
<Tag data-test="foo" title={tagText}/>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const tag = screen.getByText(tagText);
|
|
44
|
+
expect(tag?.dataset.test).toEqual('foo');
|
|
45
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const Tag: React.FC<SGDS.Component.Tag> = ({
|
|
2
|
+
className,
|
|
3
|
+
colour,
|
|
4
|
+
title,
|
|
5
|
+
...props
|
|
6
|
+
}) => {
|
|
7
|
+
return (
|
|
8
|
+
<span
|
|
9
|
+
className={[
|
|
10
|
+
'ds_tag',
|
|
11
|
+
className,
|
|
12
|
+
colour && `ds_tag--${colour}`,
|
|
13
|
+
].join(' ')}
|
|
14
|
+
{...props}
|
|
15
|
+
>
|
|
16
|
+
{title}
|
|
17
|
+
</span>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
Tag.displayName = 'Tag';
|
|
22
|
+
|
|
23
|
+
export default Tag;
|