@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,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;