@scottish-government/designsystem-react 0.10.2 → 0.11.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 (66) hide show
  1. package/@types/components/Accordion.d.ts +3 -2
  2. package/@types/components/ButtonGroup.d.ts +5 -0
  3. package/@types/components/RadioButton.d.ts +2 -2
  4. package/@types/components/SearchFacets.d.ts +18 -0
  5. package/@types/components/SearchFilters.d.ts +14 -0
  6. package/@types/components/SearchResult.d.ts +30 -0
  7. package/@types/components/SearchSort.d.ts +9 -0
  8. package/@types/components/SideNavigation.d.ts +1 -1
  9. package/CHANGELOG.md +31 -5
  10. package/dist/components/Accordion/Accordion.jsx +8 -3
  11. package/dist/components/ButtonGroup/ButtonGroup.jsx +13 -0
  12. package/dist/components/RadioButton/RadioGroup.jsx +1 -1
  13. package/dist/components/SearchFacets/SearchFacets.jsx +101 -0
  14. package/dist/components/SearchFilters/SearchFilters.jsx +63 -0
  15. package/dist/components/SearchResult/SearchResult.jsx +93 -0
  16. package/dist/components/SearchSort/SearchSort.jsx +28 -0
  17. package/dist/components/SequentialNavigation/SequentialNavigation.jsx +0 -1
  18. package/dist/components/SideNavigation/SideNavigation.jsx +2 -2
  19. package/dist/tsconfig.tsbuildinfo +1 -1
  20. package/package.json +2 -2
  21. package/src/components/Accordion/Accordion.Item.stories.tsx +10 -9
  22. package/src/components/Accordion/Accordion.stories.tsx +4 -4
  23. package/src/components/Accordion/Accordion.test.tsx +48 -14
  24. package/src/components/Accordion/Accordion.tsx +12 -1
  25. package/src/components/Breadcrumbs/Breadcrumbs.Item.stories.tsx +8 -1
  26. package/src/components/Button/Button.stories.tsx +1 -1
  27. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +41 -0
  28. package/src/components/ButtonGroup/ButtonGroup.test.tsx +45 -0
  29. package/src/components/ButtonGroup/ButtonGroup.tsx +20 -0
  30. package/src/components/ContentsNav/ContentsNav.Item.stories.tsx +8 -0
  31. package/src/components/ErrorSummary/ErrorSummary.Item.stories.tsx +7 -0
  32. package/src/components/PageMetadata/PageMetadata.Item.stories.tsx +9 -0
  33. package/src/components/RadioButton/RadioGroup.tsx +2 -2
  34. package/src/components/SearchFacets/SearchFacets.Group.stories.tsx +56 -0
  35. package/src/components/SearchFacets/SearchFacets.Item.stories.tsx +53 -0
  36. package/src/components/SearchFacets/SearchFacets.stories.tsx +38 -0
  37. package/src/components/SearchFacets/SearchFacets.test.tsx +214 -0
  38. package/src/components/SearchFacets/SearchFacets.tsx +99 -0
  39. package/src/components/SearchFilters/SearchFilters.Panel.stories.tsx +201 -0
  40. package/src/components/SearchFilters/SearchFilters.stories.tsx +137 -0
  41. package/src/components/SearchFilters/SearchFilters.test.tsx +161 -0
  42. package/src/components/SearchFilters/SearchFilters.tsx +89 -0
  43. package/src/components/SearchResult/SearchResult.stories.tsx +111 -0
  44. package/src/components/SearchResult/SearchResult.test.tsx +215 -0
  45. package/src/components/SearchResult/SearchResult.tsx +137 -0
  46. package/src/components/SearchSort/SearchSort.stories.tsx +32 -0
  47. package/src/components/SearchSort/SearchSort.test.tsx +129 -0
  48. package/src/components/SearchSort/SearchSort.tsx +45 -0
  49. package/src/components/SequentialNavigation/SequentialNavigation.Next.stories.tsx +1 -1
  50. package/src/components/SequentialNavigation/SequentialNavigation.Previous.stories.tsx +1 -1
  51. package/src/components/SequentialNavigation/SequentialNavigation.tsx +0 -1
  52. package/src/components/SideNavigation/SideNavigation.Item.stories.tsx +9 -0
  53. package/src/components/SideNavigation/SideNavigation.List.stories.tsx +7 -0
  54. package/src/components/SideNavigation/SideNavigation.tsx +2 -1
  55. package/src/components/SiteFooter/SiteFooter.License.stories.tsx +7 -0
  56. package/src/components/SiteFooter/SiteFooter.Link.stories.tsx +9 -0
  57. package/src/components/SiteFooter/SiteFooter.Org.stories.tsx +7 -0
  58. package/src/components/SiteNavigation/SiteNavigation.Item.stories.tsx +10 -0
  59. package/src/components/SkipLinks/SkipLinks.Item.stories.tsx +11 -1
  60. package/src/components/SummaryCard/SummaryCard.Action.stories.tsx +7 -0
  61. package/src/components/SummaryCard/SummaryCard.stories.tsx +7 -0
  62. package/src/components/SummaryList/SummaryList.Item.stories.tsx +15 -0
  63. package/src/components/SummaryList/SummaryList.Value.stories.tsx +5 -2
  64. package/src/components/Tabs/Tabs.Item.stories.tsx +4 -1
  65. package/src/components/TaskList/TaskList.Group.stories.tsx +9 -0
  66. package/src/components/TaskList/TaskList.Item.stories.tsx +7 -0
@@ -0,0 +1,215 @@
1
+ import { test, expect } from 'vitest';
2
+ import { render, screen, within } from '@testing-library/react';
3
+ import SearchResult from './SearchResult';
4
+
5
+ const RESULT_TITLE = 'My title';
6
+ const RESULT_HREF = '#foo';
7
+ const RESULT_CONTENT = 'My content';
8
+ const META_KEY = 'My meta key';
9
+ const META_VALUE = 'My meta value';
10
+ const CONTEXT_VALUE = 'My context value';
11
+
12
+ test('search result renders correctly', () => {
13
+ render(
14
+ <SearchResult href={RESULT_HREF} title={RESULT_TITLE} data-testid="searchresult">
15
+ <SearchResult.Content>
16
+ {RESULT_CONTENT}
17
+ </SearchResult.Content>
18
+ <SearchResult.Meta>
19
+ <SearchResult.MetaItem name={META_KEY}>
20
+ {META_VALUE}
21
+ </SearchResult.MetaItem>
22
+ </SearchResult.Meta>
23
+ <SearchResult.Context>
24
+ <SearchResult.ContextItem>{CONTEXT_VALUE}</SearchResult.ContextItem>
25
+ </SearchResult.Context>
26
+ </SearchResult>
27
+ );
28
+
29
+ const searchResult = screen.getByTestId('searchresult');
30
+ expect(searchResult).toHaveClass('ds_search-result');
31
+ expect(searchResult).not.toHaveClass('ds_search-result--promoted');
32
+
33
+ const title = screen.getByRole('heading');
34
+ expect(title).toHaveClass('ds_search-result__title');
35
+ expect(title).toHaveTextContent(RESULT_TITLE);
36
+ expect(title?.parentElement).toEqual(searchResult);
37
+
38
+ const link = within(title).getByRole('link');
39
+ expect(link).toHaveClass('ds_search-result__link');
40
+ expect(link).toHaveAttribute('href', RESULT_HREF);
41
+ expect(link.tagName).toBe('A');
42
+
43
+ const content = screen.getByText(RESULT_CONTENT);
44
+ expect(content).toBeInTheDocument();
45
+ expect(content).toHaveClass('ds_search-result__summary');
46
+ expect(content?.previousElementSibling).toEqual(title);
47
+
48
+ const meta = searchResult.querySelector('.ds_metadata');
49
+ expect(meta).toBeInTheDocument();
50
+ expect(meta).toHaveClass('ds_metadata--inline', 'ds_search-result__metadata');
51
+ expect(meta?.previousElementSibling).toEqual(content);
52
+ expect(meta?.tagName).toBe('DL');
53
+
54
+ const metaItem = meta.querySelector('.ds_metadata__item');
55
+ expect(metaItem).toBeInTheDocument();
56
+ expect(metaItem).toHaveClass('ds_metadata__item');
57
+ expect(metaItem?.parentElement).toEqual(meta);
58
+
59
+ const metaTerm = meta.querySelector('dt');
60
+ expect(metaTerm).toBeInTheDocument();
61
+ expect(metaTerm).toHaveClass('ds_metadata__key');
62
+ expect(metaTerm?.textContent).toBe(META_KEY);
63
+ expect(metaTerm?.parentElement).toEqual(metaItem);
64
+
65
+ const metaDesc = meta.querySelector('dd');
66
+ expect(metaDesc).toBeInTheDocument();
67
+ expect(metaDesc).toHaveClass('ds_metadata__value');
68
+ expect(metaDesc?.textContent.trim()).toBe(META_VALUE);
69
+ expect(metaDesc?.parentElement).toEqual(metaItem);
70
+ expect(metaDesc?.previousElementSibling).toEqual(metaTerm);
71
+
72
+ const context = searchResult.querySelector('.ds_search-result__context');
73
+ expect(context).toBeInTheDocument();
74
+ expect(context).toHaveClass('ds_search-result__context');
75
+ expect(context?.tagName).toBe('DL');
76
+ expect(context?.previousElementSibling).toEqual(meta);
77
+
78
+ const contextTitle = context?.querySelector('dt');
79
+ expect(contextTitle).toBeInTheDocument();
80
+ expect(contextTitle).toHaveClass('ds_search-result__context-key');
81
+ expect(contextTitle?.textContent).toBe('Part of:');
82
+ expect(contextTitle?.parentElement).toEqual(context);
83
+
84
+ const contextValue = context?.querySelector('dd');
85
+ expect(contextValue).toBeInTheDocument();
86
+ expect(contextValue).toHaveClass('ds_search-result__context-value');
87
+ expect(contextValue?.textContent).toBe(CONTEXT_VALUE);
88
+ expect(contextValue?.previousElementSibling).toEqual(contextTitle);
89
+ expect(contextValue?.parentElement).toEqual(context);
90
+ });
91
+
92
+ test('promoted search result renders correctly', () => {
93
+ render(
94
+ <SearchResult isPromoted href="#foo" title={RESULT_TITLE} data-testid="searchresult">
95
+ <SearchResult.Content>
96
+ {RESULT_CONTENT}
97
+ </SearchResult.Content>
98
+ </SearchResult>
99
+ );
100
+
101
+ const searchResult = screen.getByTestId('searchresult');
102
+ expect(searchResult).toHaveClass('ds_search-result', 'ds_search-result--promoted');
103
+
104
+ const promotedContent = searchResult.querySelector('.ds_search-result--promoted-content');
105
+ expect(promotedContent).toBeInTheDocument();
106
+ expect(promotedContent?.parentElement).toEqual(searchResult);
107
+ expect(promotedContent?.tagName).toBe('DIV');
108
+
109
+ const promotedTitle = promotedContent?.querySelector('.ds_search-result--promoted-title');
110
+ expect(promotedTitle).toBeInTheDocument();
111
+ expect(promotedTitle?.textContent).toBe('Recommended');
112
+ expect(promotedTitle?.parentElement).toEqual(promotedContent);
113
+
114
+ const title = screen.getByRole('heading');
115
+ expect(title).toHaveClass('ds_search-result__title');
116
+ expect(title).toHaveTextContent(RESULT_TITLE);
117
+ expect(title?.parentElement).toEqual(promotedContent);
118
+
119
+ const content = screen.getByText(RESULT_CONTENT);
120
+ expect(content).toBeInTheDocument();
121
+ expect(content?.previousElementSibling).toEqual(title);
122
+ expect(content?.parentElement).toEqual(promotedContent);
123
+ });
124
+
125
+ test('search result with media renders correctly', () => {
126
+ const IMAGE = <img alt=""/>;
127
+
128
+ render(
129
+ <SearchResult href={RESULT_HREF} title={RESULT_TITLE} data-testid="searchresult">
130
+ <SearchResult.Content>
131
+ <SearchResult.Media>
132
+ {IMAGE}
133
+ </SearchResult.Media>
134
+ {RESULT_CONTENT}
135
+ </SearchResult.Content>
136
+ </SearchResult>
137
+ );
138
+
139
+ const searchResult = screen.getByTestId('searchresult');
140
+ const title = screen.getByRole('heading');
141
+
142
+ const summary = searchResult.querySelector('.ds_search-result__has-media');
143
+ expect(summary).toBeInTheDocument();
144
+ expect(summary?.tagName).toBe('DIV');
145
+ expect(summary?.parentElement).toEqual(searchResult);
146
+ expect(summary?.previousElementSibling).toEqual(title);
147
+
148
+ const mediaWrapper = summary?.querySelector('.ds_search-result__media-wrapper');
149
+ expect(mediaWrapper).toBeInTheDocument();
150
+ expect(mediaWrapper?.parentElement).toEqual(summary);
151
+
152
+ const mediaLink = mediaWrapper?.querySelector('.ds_search-result__media-link');
153
+ expect(mediaLink).toBeInTheDocument();
154
+ expect(mediaLink).toHaveAttribute('aria-hidden', 'true');
155
+ expect(mediaLink).toHaveAttribute('href', RESULT_HREF);
156
+ expect(mediaLink).toHaveAttribute('tabindex', '-1');
157
+ expect(mediaLink?.parentElement).toEqual(mediaWrapper);
158
+ expect(mediaLink?.tagName).toBe('A');
159
+
160
+ const media = summary?.querySelector('.ds_search-result__media');
161
+ expect(media).toBeInTheDocument();
162
+ expect(media).toHaveClass('ds_aspect-box', 'ds_aspect-box--square');
163
+ expect(media?.parentElement).toEqual(mediaLink);
164
+ expect(media?.tagName).toBe('DIV');
165
+
166
+ const img = media?.querySelector('img');
167
+ expect(img).toBeInTheDocument();
168
+ expect(img?.parentElement).toEqual(media);
169
+
170
+ const content = screen.getByText(RESULT_CONTENT);
171
+ expect(content).toBeInTheDocument();
172
+ expect(content).toHaveClass('ds_search-result__summary');
173
+ expect(content?.parentElement).toEqual(summary);
174
+ expect(content?.previousElementSibling).toEqual(mediaWrapper);
175
+ });
176
+
177
+ test('linkComponent is used for title link', () => {
178
+ render(
179
+ <SearchResult href={RESULT_HREF} title={RESULT_TITLE} data-testid="searchresult" linkComponent={
180
+ ({ className, ...props }) => (
181
+ <span role="link" className={className} {...props}/>
182
+ )}>
183
+ <SearchResult.Content>
184
+ {RESULT_CONTENT}
185
+ </SearchResult.Content>
186
+ </SearchResult>
187
+ );
188
+
189
+ const title = screen.getByRole('heading');
190
+
191
+ const link = within(title).getByRole('link');
192
+ expect(link).toHaveClass('ds_search-result__link');
193
+ expect(link).toHaveAttribute('href', RESULT_HREF);
194
+ expect(link.tagName).toBe('SPAN');
195
+ expect(link?.parentElement).toEqual(title);
196
+ expect(link?.previousElementSibling).toBeNull();
197
+ });
198
+
199
+ test('passing additional props', () => {
200
+ render(
201
+ <SearchResult data-test="foo" data-testid="searchresult"/>
202
+ );
203
+
204
+ const searchResult = screen.getByTestId('searchresult');
205
+ expect(searchResult?.dataset.test).toEqual('foo');
206
+ });
207
+
208
+ test('passing additional CSS classes', () => {
209
+ render(
210
+ <SearchResult className="foo" data-testid="searchresult"/>
211
+ );
212
+
213
+ const searchResult = screen.getByTestId('searchresult');
214
+ expect(searchResult).toHaveClass('foo');
215
+ });
@@ -0,0 +1,137 @@
1
+ import { Children, createContext, useContext } from 'react';
2
+ import ConditionalWrapper from '../../common/ConditionalWrapper';
3
+ import AspectBox from '../AspectBox/AspectBox';
4
+ import Metadata from '../PageMetadata/PageMetadata';
5
+
6
+ const SearchResultLinkHrefContext = createContext('');
7
+
8
+ const SearchResultContent = ({
9
+ children
10
+ }: SGDS.Component.SearchResult.Content) => {
11
+ const otherChildren: any[] = [];
12
+ let imageChild: React.ReactNode = null;
13
+
14
+ // assign to slots
15
+ Children.forEach(children, (child: React.ReactNode) => {
16
+ const thisChild = child as React.ReactElement<any>;
17
+ if (thisChild && thisChild.type === SearchResultMedia) {
18
+ imageChild = thisChild;
19
+ } else {
20
+ otherChildren.push(thisChild);
21
+ }
22
+ });
23
+
24
+ return (
25
+ imageChild ?
26
+ (<div className="ds_search-result__has-media">
27
+ {imageChild}
28
+ <div className="ds_search-result__summary">{otherChildren}</div>
29
+ </div>)
30
+ :
31
+ (<div className="ds_search-result__summary">
32
+ {otherChildren}
33
+ </div>)
34
+ )
35
+ };
36
+
37
+ const SearchResultContext = ({
38
+ children,
39
+ title = 'Part of'
40
+ }: SGDS.Component.SearchResult.Context) => {
41
+ return (
42
+ <dl className="ds_search-result__context">
43
+ <dt className="ds_search-result__context-key">{title}:</dt>
44
+ {children}
45
+ </dl>
46
+ )
47
+ };
48
+
49
+ const SearchResultContextItem = ({
50
+ children
51
+ }: SGDS.Component.SearchResult.ContextItem) => {
52
+ return (
53
+ <dd className="ds_search-result__context-value">
54
+ {children}
55
+ </dd>
56
+ )
57
+ };
58
+
59
+ const SearchResultMedia = ({
60
+ children
61
+ }: SGDS.Component.SearchResult.Media) => {
62
+ return (
63
+ <div className="ds_search-result__media-wrapper">
64
+ <a className="ds_search-result__media-link" href={useContext(SearchResultLinkHrefContext)} tabIndex={-1} aria-hidden="true">
65
+ <AspectBox className="ds_search-result__media" ratio="1:1">
66
+ {children}
67
+ </AspectBox>
68
+ </a>
69
+ </div>
70
+ )
71
+ };
72
+
73
+ const SearchResultMeta = ({
74
+ children
75
+ }: SGDS.Component.SearchResult.Meta) => {
76
+ return (
77
+ <Metadata className="ds_search-result__metadata" isInline>
78
+ {children}
79
+ </Metadata>
80
+ )
81
+ };
82
+
83
+ const SearchResult = ({
84
+ children,
85
+ href,
86
+ isPromoted,
87
+ linkComponent,
88
+ promotedTitle = 'Recommended',
89
+ title,
90
+ ...props
91
+ }: SGDS.Component.SearchResult) => {
92
+ const LINK_CLASS = 'ds_search-result__link';
93
+
94
+ return (
95
+ <div className={[
96
+ 'ds_search-result',
97
+ isPromoted ? 'ds_search-result--promoted' : ''
98
+ ].join(' ')}
99
+ {...props}
100
+ >
101
+ <ConditionalWrapper
102
+ condition={!!isPromoted}
103
+ wrapper={(children: React.JSX.Element) => <div className="ds_search-result--promoted-content">
104
+ <header className="ds_search-result--promoted-title">{promotedTitle}</header>
105
+ {children}
106
+ </div>}
107
+ >
108
+ <SearchResultLinkHrefContext value={href}>
109
+ <h3 className="ds_search-result__title">
110
+ {linkComponent ?
111
+ linkComponent({ className: LINK_CLASS, children: title, href }) :
112
+ <a className={LINK_CLASS} href={href}>{title}</a>
113
+ }
114
+ </h3>
115
+
116
+ {children}
117
+ </SearchResultLinkHrefContext>
118
+ </ConditionalWrapper>
119
+ </div>
120
+ );
121
+ };
122
+
123
+ SearchResult.Content = SearchResultContent;
124
+ SearchResult.Context = SearchResultContext;
125
+ SearchResult.ContextItem = SearchResultContextItem;
126
+ SearchResult.Media = SearchResultMedia;
127
+ SearchResult.Meta = SearchResultMeta;
128
+ SearchResult.MetaItem = Metadata.Item;
129
+
130
+ SearchResultContent.displayName = 'SearchResult.Content';
131
+ SearchResultContext.displayName = 'SearchResult.Context';
132
+ SearchResultContextItem.displayName = 'SearchResult.ContextItem';
133
+ SearchResultMedia.displayName = 'SearchResult.Media';
134
+ SearchResultMeta.displayName = 'SearchResult.Meta';
135
+ SearchResult.MetaItem.displayName = 'SearchResult.MetaItem';
136
+
137
+ export default SearchResult;
@@ -0,0 +1,32 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import argTypes from '../../../.storybook/sgdsArgTypes';
3
+
4
+ import SearchSort from './SearchSort';
5
+
6
+ const meta = {
7
+ title: 'Components/Search results/Sort',
8
+ component: SearchSort,
9
+ argTypes: {
10
+ children: argTypes.children(),
11
+ id: argTypes.id(),
12
+ label: argTypes.label({ defaultValue: 'Sort by' }),
13
+ onApply: argTypes.onClick({ description: 'Callback function to be called when the Apply sort button is clicked' }),
14
+ },
15
+ args: {
16
+ id: 'sort-by',
17
+ label: 'Sort by'
18
+ }
19
+ } satisfies Meta<typeof SearchSort>;
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof meta>;
23
+
24
+ export const Default: Story = {
25
+ render: (args) => (
26
+ <SearchSort {...args}>
27
+ <SearchSort.Option value="relevance">Most relevant</SearchSort.Option>
28
+ <SearchSort.Option value="date">Updated (newest)</SearchSort.Option>
29
+ <SearchSort.Option value="adate">Updated (oldest)</SearchSort.Option>
30
+ </SearchSort>
31
+ )
32
+ };
@@ -0,0 +1,129 @@
1
+ import { test, expect, vi } from 'vitest';
2
+ import { render, screen, within } from '@testing-library/react';
3
+ import SearchSort from './SearchSort';
4
+
5
+ const SELECT_ID = 'sort-by';
6
+ const LABEL_TEXT = 'Sort by';
7
+
8
+ test('renders correctly', () => {
9
+ render(
10
+ <SearchSort data-testid="search-sort">
11
+ </SearchSort>
12
+ );
13
+
14
+ const searchSort = screen.getByTestId('search-sort');
15
+ const select = screen.getByRole('combobox');
16
+ const selectWrapper = select.parentElement;
17
+ const label = selectWrapper?.previousElementSibling;
18
+ const selectArrow = select.nextElementSibling;
19
+ const button = within(searchSort).getByRole('button');
20
+
21
+ expect(select).toHaveClass('ds_select');
22
+ expect(select.id).toEqual(SELECT_ID);
23
+ expect(select).toHaveAttribute('name', SELECT_ID);
24
+
25
+ expect(selectWrapper).toHaveClass('ds_select-wrapper');
26
+ expect(selectWrapper?.tagName).toEqual('DIV');
27
+
28
+ expect(label).toHaveClass('ds_label');
29
+ expect(label).toHaveAttribute('for', SELECT_ID);
30
+ expect(label).toHaveTextContent(LABEL_TEXT);
31
+
32
+ expect(selectArrow).toHaveClass('ds_select-arrow');
33
+ expect(selectArrow).toHaveAttribute('aria-hidden');
34
+ expect(selectArrow?.textContent).toEqual('');
35
+
36
+ expect(searchSort).toBeInTheDocument();
37
+ expect(searchSort).toHaveClass('ds_sort-options');
38
+ expect(searchSort.tagName).toEqual('DIV');
39
+
40
+ expect(button).toHaveClass('ds_button', 'ds_button--small', 'ds_button--secondary');
41
+ expect(button).toHaveTextContent('Apply sort');
42
+ expect(button).toHaveAttribute('type', 'submit');
43
+ expect(button.previousElementSibling).toEqual(selectWrapper);
44
+ });
45
+
46
+ test('custom id and label', () => {
47
+ const CUSTOM_ID = 'custom-sort-by';
48
+ const CUSTOM_LABEL = 'Custom sort by';
49
+
50
+ render(
51
+ <SearchSort id={CUSTOM_ID} label={CUSTOM_LABEL} data-testid="search-sort">
52
+ </SearchSort>
53
+ );
54
+
55
+ const select = screen.getByRole('combobox');
56
+ const selectWrapper = select.parentElement;
57
+ const label = selectWrapper?.previousElementSibling;
58
+
59
+ expect(select.id).toEqual(CUSTOM_ID);
60
+ expect(select).toHaveAttribute('name', CUSTOM_ID);
61
+ expect(label).toHaveAttribute('for', CUSTOM_ID);
62
+ expect(label).toHaveTextContent(CUSTOM_LABEL);
63
+ });
64
+
65
+ test('event handler onApply', async () => {
66
+ const onApply = vi.fn();
67
+ render(
68
+ <SearchSort onApply={onApply} data-testid="search-sort">
69
+ </SearchSort>
70
+ );
71
+
72
+ const button = within(screen.getByTestId('search-sort')).getByRole('button');
73
+ await button.click();
74
+ expect(onApply).toHaveBeenCalled();
75
+ });
76
+
77
+ test('passing additional props', () => {
78
+ render(
79
+ <SearchSort data-test="foo" data-testid="search-sort">
80
+ </SearchSort>
81
+ );
82
+
83
+ const searchSort = screen.getByTestId('search-sort');
84
+ expect(searchSort.dataset.test).toEqual('foo');
85
+ });
86
+
87
+ test('passing additional CSS classes', () => {
88
+ render(
89
+ <SearchSort className="foo" data-testid="search-sort">
90
+ </SearchSort>
91
+ );
92
+
93
+ const searchSort = screen.getByTestId('search-sort');
94
+ expect(searchSort).toHaveClass('foo', 'ds_sort-options');
95
+ });
96
+
97
+ test('renders options correctly', () => {
98
+ const OPTION_VALUE_1 = 'relevance';
99
+ const OPTION_TEXT_1 = 'Most relevant';
100
+ const OPTION_VALUE_2 = 'date';
101
+ const OPTION_TEXT_2 = 'Updated (newest)';
102
+ const OPTION_VALUE_3 = 'adate';
103
+ const OPTION_TEXT_3 = 'Updated (oldest)';
104
+
105
+ render(
106
+ <SearchSort data-testid="search-sort">
107
+ <SearchSort.Option value={OPTION_VALUE_1}>{OPTION_TEXT_1}</SearchSort.Option>
108
+ <SearchSort.Option value={OPTION_VALUE_2}>{OPTION_TEXT_2}</SearchSort.Option>
109
+ <SearchSort.Option value={OPTION_VALUE_3}>{OPTION_TEXT_3}</SearchSort.Option>
110
+ </SearchSort>
111
+ );
112
+
113
+ const select = screen.getByRole('combobox');
114
+ const options = within(select).getAllByRole('option');
115
+
116
+ expect(options).toHaveLength(4);
117
+
118
+ expect(options[0]).toHaveTextContent('');
119
+ expect(options[0]).toHaveAttribute('value', '');
120
+
121
+ expect(options[1]).toHaveTextContent(OPTION_TEXT_1);
122
+ expect(options[1]).toHaveAttribute('value', OPTION_VALUE_1);
123
+
124
+ expect(options[2]).toHaveTextContent(OPTION_TEXT_2);
125
+ expect(options[2]).toHaveAttribute('value', OPTION_VALUE_2);
126
+
127
+ expect(options[3]).toHaveTextContent(OPTION_TEXT_3);
128
+ expect(options[3]).toHaveAttribute('value', OPTION_VALUE_3);
129
+ });
@@ -0,0 +1,45 @@
1
+ import { AllHTMLAttributes } from "react";
2
+ import Button from "../Button/Button";
3
+ import Select from "../Select/Select";
4
+
5
+ const Option = ({
6
+ children,
7
+ value
8
+ }: AllHTMLAttributes<HTMLOptionElement>) => {
9
+ return (
10
+ <option value={value}>
11
+ {children}
12
+ </option>
13
+ );
14
+ }
15
+
16
+ const SearchSort = ({
17
+ children,
18
+ className,
19
+ id = 'sort-by',
20
+ label = 'Sort by',
21
+ onApply,
22
+ ...props
23
+ }: SGDS.Component.SearchSort) => {
24
+ return (
25
+ <div
26
+ className={[
27
+ 'ds_sort-options',
28
+ className
29
+ ].join(' ')}
30
+ {...props}
31
+ >
32
+ <Select id={id} label={label}>
33
+ {children}
34
+ </Select>
35
+
36
+ <Button onClick={onApply} isSmall buttonStyle="secondary" type="submit">Apply sort</Button>
37
+ </div>
38
+ );
39
+ };
40
+
41
+ SearchSort.displayName = 'SearchSort';
42
+ Option.displayName = 'SearchSort.Option';
43
+ SearchSort.Option = Option;
44
+
45
+ export default SearchSort;
@@ -7,7 +7,7 @@ const meta = {
7
7
  title: 'Components/SequentialNavigation/SequentialNavigation.Next',
8
8
  component: SequentialNavigation.Next,
9
9
  argTypes: {
10
- href: argTypes.href(),
10
+ href: argTypes.href({type: {name: 'string', required: true}}),
11
11
  linkComponent: argTypes.linkComponent(),
12
12
  textLabel: {
13
13
  description: 'String to use for the label that precedes the link text',
@@ -7,7 +7,7 @@ const meta = {
7
7
  title: 'Components/SequentialNavigation/SequentialNavigation.Previous',
8
8
  component: SequentialNavigation.Previous,
9
9
  argTypes: {
10
- href: argTypes.href(),
10
+ href: argTypes.href({type: {name: 'string', required: true}}),
11
11
  linkComponent: argTypes.linkComponent(),
12
12
  textLabel: {
13
13
  description: 'String to use for the label that precedes the link text',
@@ -5,7 +5,6 @@ const SeqNavLink = ({
5
5
  linkComponent,
6
6
  textLabel
7
7
  }: SGDS.Component.SequentialNavigation.Link) => {
8
- console.log(textLabel)
9
8
  const LINK_CLASSES = [
10
9
  'ds_sequential-nav__button',
11
10
  isPrev ? 'ds_sequential-nav__button--left' : 'ds_sequential-nav__button--right'
@@ -6,6 +6,15 @@ import SideNavigation from './SideNavigation';
6
6
  const meta = {
7
7
  title: 'Components/SideNavigation/SideNavigation.Item',
8
8
  component: SideNavigation.Item,
9
+ decorators: [
10
+ Story => (
11
+ <nav className="ds_side-navigation">
12
+ <ul className="ds_side-navigation__list">
13
+ <Story />
14
+ </ul>
15
+ </nav>
16
+ )
17
+ ],
9
18
  argTypes: {
10
19
  href: argTypes.href(),
11
20
  isCurrent: argTypes.isCurrent(),
@@ -6,6 +6,13 @@ import SideNavigation from './SideNavigation';
6
6
  const meta = {
7
7
  title: 'Components/SideNavigation/SideNavigation.List',
8
8
  component: SideNavigation.List,
9
+ decorators: [
10
+ Story => (
11
+ <nav className="ds_side-navigation">
12
+ <Story />
13
+ </nav>
14
+ )
15
+ ],
9
16
  argTypes: {
10
17
  isRoot: {
11
18
  description: 'Indicates that this is the root list in a nested structure. Required for mobile navigation.',
@@ -43,6 +43,7 @@ const SideNavigationItem = function ({
43
43
  };
44
44
 
45
45
  const SideNavigation = function ({
46
+ ariaLabel = 'Sections',
46
47
  children,
47
48
  className,
48
49
  ...props
@@ -57,7 +58,7 @@ const SideNavigation = function ({
57
58
 
58
59
  return (
59
60
  <nav
60
- aria-label="Sections"
61
+ aria-label={ariaLabel}
61
62
  className={[
62
63
  'ds_side-navigation',
63
64
  className
@@ -6,6 +6,13 @@ import SiteFooter from './SiteFooter';
6
6
  const meta = {
7
7
  title: 'Components/SiteFooter/SiteFooter.License',
8
8
  component: SiteFooter.License,
9
+ decorators: [
10
+ Story => (
11
+ <nav className="ds_site-footer" style={{borderTop: 0}}>
12
+ <Story />
13
+ </nav>
14
+ )
15
+ ],
9
16
  argTypes: {
10
17
  children: argTypes.children()
11
18
  },
@@ -6,6 +6,15 @@ import SiteFooter from './SiteFooter';
6
6
  const meta = {
7
7
  title: 'Components/SiteFooter/SiteFooter.Link',
8
8
  component: SiteFooter.Link,
9
+ decorators: [
10
+ Story => (
11
+ <div className="ds_site-footer__site-items" style={{ borderBottom: 0 }}>
12
+ <ul>
13
+ <Story />
14
+ </ul>
15
+ </div>
16
+ )
17
+ ],
9
18
  argTypes: {
10
19
  href: argTypes.href(),
11
20
  linkComponent: argTypes.linkComponent(),
@@ -6,6 +6,13 @@ import SiteFooter from './SiteFooter';
6
6
  const meta = {
7
7
  title: 'Components/SiteFooter/SiteFooter.Org',
8
8
  component: SiteFooter.Org,
9
+ decorators: [
10
+ Story => (
11
+ <div className="ds_site-footer__content" style={{borderTop: 0}}>
12
+ <Story />
13
+ </div>
14
+ )
15
+ ],
9
16
  argTypes: {
10
17
  href: argTypes.href(),
11
18
  title: {