@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.
- package/@types/components/Accordion.d.ts +3 -2
- package/@types/components/ButtonGroup.d.ts +5 -0
- package/@types/components/RadioButton.d.ts +2 -2
- package/@types/components/SearchFacets.d.ts +18 -0
- package/@types/components/SearchFilters.d.ts +14 -0
- package/@types/components/SearchResult.d.ts +30 -0
- package/@types/components/SearchSort.d.ts +9 -0
- package/@types/components/SideNavigation.d.ts +1 -1
- package/CHANGELOG.md +31 -5
- package/dist/components/Accordion/Accordion.jsx +8 -3
- package/dist/components/ButtonGroup/ButtonGroup.jsx +13 -0
- package/dist/components/RadioButton/RadioGroup.jsx +1 -1
- package/dist/components/SearchFacets/SearchFacets.jsx +101 -0
- package/dist/components/SearchFilters/SearchFilters.jsx +63 -0
- package/dist/components/SearchResult/SearchResult.jsx +93 -0
- package/dist/components/SearchSort/SearchSort.jsx +28 -0
- package/dist/components/SequentialNavigation/SequentialNavigation.jsx +0 -1
- package/dist/components/SideNavigation/SideNavigation.jsx +2 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/components/Accordion/Accordion.Item.stories.tsx +10 -9
- package/src/components/Accordion/Accordion.stories.tsx +4 -4
- package/src/components/Accordion/Accordion.test.tsx +48 -14
- package/src/components/Accordion/Accordion.tsx +12 -1
- package/src/components/Breadcrumbs/Breadcrumbs.Item.stories.tsx +8 -1
- package/src/components/Button/Button.stories.tsx +1 -1
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +41 -0
- package/src/components/ButtonGroup/ButtonGroup.test.tsx +45 -0
- package/src/components/ButtonGroup/ButtonGroup.tsx +20 -0
- package/src/components/ContentsNav/ContentsNav.Item.stories.tsx +8 -0
- package/src/components/ErrorSummary/ErrorSummary.Item.stories.tsx +7 -0
- package/src/components/PageMetadata/PageMetadata.Item.stories.tsx +9 -0
- package/src/components/RadioButton/RadioGroup.tsx +2 -2
- package/src/components/SearchFacets/SearchFacets.Group.stories.tsx +56 -0
- package/src/components/SearchFacets/SearchFacets.Item.stories.tsx +53 -0
- package/src/components/SearchFacets/SearchFacets.stories.tsx +38 -0
- package/src/components/SearchFacets/SearchFacets.test.tsx +214 -0
- package/src/components/SearchFacets/SearchFacets.tsx +99 -0
- package/src/components/SearchFilters/SearchFilters.Panel.stories.tsx +201 -0
- package/src/components/SearchFilters/SearchFilters.stories.tsx +137 -0
- package/src/components/SearchFilters/SearchFilters.test.tsx +161 -0
- package/src/components/SearchFilters/SearchFilters.tsx +89 -0
- package/src/components/SearchResult/SearchResult.stories.tsx +111 -0
- package/src/components/SearchResult/SearchResult.test.tsx +215 -0
- package/src/components/SearchResult/SearchResult.tsx +137 -0
- package/src/components/SearchSort/SearchSort.stories.tsx +32 -0
- package/src/components/SearchSort/SearchSort.test.tsx +129 -0
- package/src/components/SearchSort/SearchSort.tsx +45 -0
- package/src/components/SequentialNavigation/SequentialNavigation.Next.stories.tsx +1 -1
- package/src/components/SequentialNavigation/SequentialNavigation.Previous.stories.tsx +1 -1
- package/src/components/SequentialNavigation/SequentialNavigation.tsx +0 -1
- package/src/components/SideNavigation/SideNavigation.Item.stories.tsx +9 -0
- package/src/components/SideNavigation/SideNavigation.List.stories.tsx +7 -0
- package/src/components/SideNavigation/SideNavigation.tsx +2 -1
- package/src/components/SiteFooter/SiteFooter.License.stories.tsx +7 -0
- package/src/components/SiteFooter/SiteFooter.Link.stories.tsx +9 -0
- package/src/components/SiteFooter/SiteFooter.Org.stories.tsx +7 -0
- package/src/components/SiteNavigation/SiteNavigation.Item.stories.tsx +10 -0
- package/src/components/SkipLinks/SkipLinks.Item.stories.tsx +11 -1
- package/src/components/SummaryCard/SummaryCard.Action.stories.tsx +7 -0
- package/src/components/SummaryCard/SummaryCard.stories.tsx +7 -0
- package/src/components/SummaryList/SummaryList.Item.stories.tsx +15 -0
- package/src/components/SummaryList/SummaryList.Value.stories.tsx +5 -2
- package/src/components/Tabs/Tabs.Item.stories.tsx +4 -1
- package/src/components/TaskList/TaskList.Group.stories.tsx +9 -0
- 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',
|
|
@@ -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=
|
|
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: {
|