@comicrelief/component-library 8.83.1 → 8.84.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/dist/index.cjs.js +1227 -859
- package/dist/index.es.js +10902 -10144
- package/package.json +1 -1
- package/src/App.styles.js +2 -2
- package/src/components/Atoms/Button/Button.js +13 -3
- package/src/components/Atoms/Button/Button.style.js +19 -1
- package/src/components/Atoms/Button/Button.test.js +1 -1
- package/src/components/Atoms/Button/ButtonExample.jsx +24 -0
- package/src/components/Atoms/ButtonWithStates/ButtonWithStates.js +1 -1
- package/src/components/Atoms/Icons/Cross.js +0 -1
- package/src/components/Atoms/Icons/Download.js +4 -4
- package/src/components/Atoms/Icons/External.js +4 -4
- package/src/components/Atoms/Icons/Filter.js +50 -0
- package/src/components/Atoms/Icons/IconsExample.jsx +9 -5
- package/src/components/Atoms/Icons/Undo.js +44 -0
- package/src/components/Atoms/Icons/index.js +2 -0
- package/src/components/Atoms/Link/Link.test.js +1 -1
- package/src/components/Atoms/Link/LinkExample.jsx +4 -2
- package/src/components/Atoms/Text/TextExample.jsx +338 -130
- package/src/components/Molecules/CardDs/__snapshots__/CardDs.test.js.snap +2 -2
- package/src/components/Molecules/Promo/__snapshots__/Promo.test.js.snap +2 -2
- package/src/components/Molecules/QuoteSlice/__snapshots__/QuoteSlice.test.js.snap +1 -1
- package/src/components/Molecules/SingleMessage/__snapshots__/SingleMessage.test.js.snap +2 -2
- package/src/components/Molecules/SingleMessageDS/__snapshots__/SingleMessageDs.test.js.snap +1 -3
- package/src/components/Organisms/CookieBanner/CookieBanner.test.js +1 -1
- package/src/components/Organisms/EmailBanner/__snapshots__/EmailBanner.test.js.snap +12 -12
- package/src/components/Organisms/EmailSignUp/__snapshots__/EmailSignUp.test.js.snap +3 -3
- package/src/components/Organisms/FilterCard/FilterCard.js +286 -0
- package/src/components/Organisms/FilterCard/FilterCard.style.js +425 -0
- package/src/components/Organisms/FilterCard/FilterCard.test.js +60 -0
- package/src/components/Organisms/FilterCard/FilterCardExample.jsx +48 -0
- package/src/components/Organisms/FilterCard/FilterCardNodes.js +155 -0
- package/src/components/Organisms/FilterCard/__snapshots__/FilterCard.test.js.snap +1006 -0
- package/src/components/Organisms/FilterCard/_utils/_utils.js +57 -0
- package/src/components/Organisms/FilterCard/assets/Rectangle.svg +3 -0
- package/src/components/Organisms/FooterNew/__snapshots__/FooterNew.test.js.snap +4 -4
- package/src/components/Organisms/MarketingPreferencesDS/MarketingPreferencesDS.style.js +1 -1
- package/src/data/TestFilterCardNodeData.js +930 -0
- package/src/data/data.js +64 -1
- package/src/data/test-icons/Children--black.svg +5 -0
- package/src/data/test-icons/Children--white.svg +5 -0
- package/src/demos/index.jsx +31 -9
- package/src/index.js +1 -0
- package/src/theme/crTheme/buttonColors.js +3 -4
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import styled, { css, keyframes } from 'styled-components';
|
|
2
|
+
import Picture from '../../Atoms/Picture/Picture';
|
|
3
|
+
import Text from '../../Atoms/Text/Text';
|
|
4
|
+
import Button from '../../Atoms/Button/Button';
|
|
5
|
+
import { Copywrapper, IconWrapper } from '../../Atoms/Button/Button.style';
|
|
6
|
+
import defaultBoxShadow from '../../../theme/shared/boxShadows';
|
|
7
|
+
import Link from '../../Atoms/Link/Link';
|
|
8
|
+
import { IconWrapper as LinkIconWrapper } from '../../Atoms/Link/Link.style';
|
|
9
|
+
import Rectangle from './assets/Rectangle.svg';
|
|
10
|
+
|
|
11
|
+
// To reduce icon flash when switching between images:
|
|
12
|
+
const fadeIn = keyframes`
|
|
13
|
+
from { opacity: 0; }
|
|
14
|
+
to { opacity: 1; }
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
// Just for the lovely clean markup:
|
|
18
|
+
const OuterWrapper = styled.div`
|
|
19
|
+
//
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const Title = styled(Text)`
|
|
23
|
+
@media ${({ theme }) => theme.allBreakpoints('L')} {
|
|
24
|
+
margin-bottom: 2rem;
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const Container = styled.div`
|
|
29
|
+
width: 100%;
|
|
30
|
+
height: auto;
|
|
31
|
+
display: flex;
|
|
32
|
+
position: relative;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
background: ${({ theme, $pageBackgroundColour }) => theme.color($pageBackgroundColour)};
|
|
35
|
+
justify-content: center;
|
|
36
|
+
${({ $paddingAbove, $paddingBelow }) => css`padding: ${$paddingAbove} 1rem ${$paddingBelow};`}
|
|
37
|
+
|
|
38
|
+
@media ${({ theme }) => theme.allBreakpoints('M')} {
|
|
39
|
+
padding-inline: 2rem;
|
|
40
|
+
}
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
const HeaderWrapper = styled.div`
|
|
44
|
+
background: ${({ theme }) => theme.color('white')};
|
|
45
|
+
border-radius: 1rem;
|
|
46
|
+
padding: 2rem;
|
|
47
|
+
position: relative;
|
|
48
|
+
${defaultBoxShadow()};
|
|
49
|
+
|
|
50
|
+
// Down 'arrow':
|
|
51
|
+
&:after {
|
|
52
|
+
--size: 3rem;
|
|
53
|
+
position: absolute;
|
|
54
|
+
content: "";
|
|
55
|
+
width: var(--size);
|
|
56
|
+
height: var(--size);
|
|
57
|
+
bottom: calc((var(--size) * -0.5));
|
|
58
|
+
left: calc(50% - (var(--size) / 2));
|
|
59
|
+
background: center / contain no-repeat url("${Rectangle}");
|
|
60
|
+
// Since the boxShadow won't extend to cover this, adding a little dropshadow to help it blend better:
|
|
61
|
+
filter: drop-shadow(0px 6px 3px rgba(0, 0, 0, 0.05));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@media ${({ theme }) => theme.allBreakpoints('M')} {
|
|
65
|
+
&:after {
|
|
66
|
+
left: 3rem;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@media ${({ theme }) => theme.allBreakpoints('L')} {
|
|
71
|
+
padding: 4rem;
|
|
72
|
+
|
|
73
|
+
&:after {
|
|
74
|
+
left: 4rem;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
const Body = styled.div`
|
|
80
|
+
margin-bottom: 40px;
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
const ControlsWrapper = styled.div`
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-direction: column;
|
|
86
|
+
gap: 1rem;
|
|
87
|
+
|
|
88
|
+
@media ${({ theme }) => theme.allBreakpoints('M')} {
|
|
89
|
+
flex-direction: row;
|
|
90
|
+
}
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
// Tweaks to suit this context:
|
|
94
|
+
const CustomisedButton = styled(Button)`
|
|
95
|
+
// Override 'Button w/icon' styles for this use-case:
|
|
96
|
+
display: inline-flex;
|
|
97
|
+
grid-column-gap: 0.5rem;
|
|
98
|
+
width: auto;
|
|
99
|
+
|
|
100
|
+
@media ${({ theme }) => theme.allBreakpoints('L')} {
|
|
101
|
+
justify-content: left;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
${({ theme }) => css`
|
|
105
|
+
color: ${theme.color('black')};
|
|
106
|
+
box-shadow: 0px 0px 0px 1px ${theme.color('black')} inset;
|
|
107
|
+
`};
|
|
108
|
+
|
|
109
|
+
> ${Copywrapper} {
|
|
110
|
+
padding-right: 0;
|
|
111
|
+
text-align: left;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
> ${IconWrapper} {
|
|
115
|
+
margin-left: 0;
|
|
116
|
+
height: calc(2.25rem - (0.6rem * 2));
|
|
117
|
+
}
|
|
118
|
+
`;
|
|
119
|
+
|
|
120
|
+
const ShowHideFiltersButton = styled(CustomisedButton)`
|
|
121
|
+
// Override all normal button styles to make this work more as a 'toggle':
|
|
122
|
+
&,
|
|
123
|
+
&:hover,
|
|
124
|
+
&:focus,
|
|
125
|
+
&:focus-within,
|
|
126
|
+
&:focus-visible {
|
|
127
|
+
background-color: ${({ theme, $isActive }) => ($isActive ? `${theme.color('grey_medium')}` : `${theme.color('white')}`)};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
> ${Copywrapper} {
|
|
131
|
+
// Fixed label width to prevent squirming when the label copy changes:
|
|
132
|
+
width: 7rem;
|
|
133
|
+
text-align: center;
|
|
134
|
+
|
|
135
|
+
@media ${({ theme }) => theme.allBreakpoints('L')} {
|
|
136
|
+
width: 8rem;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// More squirm-prevention when switching icons:
|
|
141
|
+
> ${IconWrapper} {
|
|
142
|
+
width: 1rem;
|
|
143
|
+
}
|
|
144
|
+
`;
|
|
145
|
+
|
|
146
|
+
const ResultsWrapper = styled.div`
|
|
147
|
+
text-align: center;
|
|
148
|
+
|
|
149
|
+
@media ${({ theme }) => theme.allBreakpoints('M')} {
|
|
150
|
+
display: flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
}
|
|
153
|
+
`;
|
|
154
|
+
|
|
155
|
+
const ClearSelectionButton = styled(CustomisedButton)`
|
|
156
|
+
display: ${({ $show }) => ($show ? 'flex' : 'none')};
|
|
157
|
+
`;
|
|
158
|
+
|
|
159
|
+
const FilterButtonsWrapper = styled.div`
|
|
160
|
+
display: ${({ $show }) => ($show ? 'flex' : 'none')};
|
|
161
|
+
margin-top: 2rem;
|
|
162
|
+
flex-wrap: wrap;
|
|
163
|
+
gap: 0.5rem;
|
|
164
|
+
animation: ${fadeIn} 0.1s ease-in;
|
|
165
|
+
|
|
166
|
+
@media ${({ theme }) => theme.allBreakpoints('M')} {
|
|
167
|
+
gap: 1rem;
|
|
168
|
+
}
|
|
169
|
+
`;
|
|
170
|
+
|
|
171
|
+
const FilterButton = styled(CustomisedButton)`
|
|
172
|
+
box-shadow: 0px 0px 0px 1px ${({ theme }) => theme.color('grey_medium')} inset;
|
|
173
|
+
display: inline-flex;
|
|
174
|
+
flex-grow: 1;
|
|
175
|
+
background-color: ${({ theme }) => theme.color('grey_light')};
|
|
176
|
+
|
|
177
|
+
${({ $isSelected }) => ($isSelected && css`
|
|
178
|
+
&,
|
|
179
|
+
&:hover,
|
|
180
|
+
&:focus {
|
|
181
|
+
background-color: ${({ theme }) => theme.color('red')};
|
|
182
|
+
color: ${({ theme }) => theme.color('white')};
|
|
183
|
+
box-shadow: none;
|
|
184
|
+
}
|
|
185
|
+
`)};
|
|
186
|
+
|
|
187
|
+
// Increase specificity to override style defaults:
|
|
188
|
+
${FilterButtonsWrapper} & {
|
|
189
|
+
|
|
190
|
+
// Match the pre-existing transition with the icon animation so it looks as natural as poss:
|
|
191
|
+
transition: all 0.2s ease-in;
|
|
192
|
+
|
|
193
|
+
> ${IconWrapper} {
|
|
194
|
+
// Squirm-reduction
|
|
195
|
+
width: 1.25rem;
|
|
196
|
+
margin: 0;
|
|
197
|
+
animation: ${fadeIn} 0.2s ease-in;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
> ${Copywrapper} {
|
|
201
|
+
padding-inline: 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Match the focus and hover states to default, as it makes for confusing UX:
|
|
205
|
+
&:hover,
|
|
206
|
+
&:focus {
|
|
207
|
+
box-shadow: 0px 0px 0px 1px ${({ theme }) => theme.color('grey')} inset;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@media ${({ theme }) => theme.allBreakpoints('M')} {
|
|
212
|
+
flex-grow: 0;
|
|
213
|
+
}
|
|
214
|
+
`;
|
|
215
|
+
|
|
216
|
+
const NodeWrapper = styled.ul`
|
|
217
|
+
display: flex;
|
|
218
|
+
flex-direction: row;
|
|
219
|
+
flex-wrap: wrap;
|
|
220
|
+
margin: 3rem 0 0;
|
|
221
|
+
gap: 1.5rem;
|
|
222
|
+
padding: 0;
|
|
223
|
+
|
|
224
|
+
@media ${({ theme }) => theme.allBreakpoints('M')} {
|
|
225
|
+
gap: 2rem;
|
|
226
|
+
margin-top: 4rem;
|
|
227
|
+
}
|
|
228
|
+
`;
|
|
229
|
+
|
|
230
|
+
const NodeImageWrapper = styled.div`
|
|
231
|
+
flex: 0 0 20%;
|
|
232
|
+
aspect-ratio: 1;
|
|
233
|
+
align-self: start;
|
|
234
|
+
min-width: 75px;
|
|
235
|
+
|
|
236
|
+
@media ${({ theme }) => theme.allBreakpoints('M')} {
|
|
237
|
+
flex: 0 0 20%;
|
|
238
|
+
min-width: 165px;
|
|
239
|
+
}
|
|
240
|
+
`;
|
|
241
|
+
|
|
242
|
+
const NodeCopyWrapper = styled.div`
|
|
243
|
+
flex: 0 0 calc(auto - var(--s-gap));
|
|
244
|
+
align-self: start;
|
|
245
|
+
|
|
246
|
+
@media ${({ theme }) => theme.allBreakpoints('M')} {
|
|
247
|
+
flex: 0 0 calc(auto - var(--m-gap));
|
|
248
|
+
}
|
|
249
|
+
`;
|
|
250
|
+
|
|
251
|
+
const Node = styled.li`
|
|
252
|
+
--s-gap: 1rem;
|
|
253
|
+
--m-gap: 2rem;
|
|
254
|
+
--l-gap: 4rem;
|
|
255
|
+
position: relative;
|
|
256
|
+
display: flex;
|
|
257
|
+
flex: 0 0 100%;
|
|
258
|
+
flex-direction: row;
|
|
259
|
+
align-items: center;
|
|
260
|
+
gap: var(--s-gap);
|
|
261
|
+
border-radius: 0.5rem;
|
|
262
|
+
height: fit-content;
|
|
263
|
+
animation: ${fadeIn} 0.2s ease-in;
|
|
264
|
+
|
|
265
|
+
// Hero-only customisations:
|
|
266
|
+
${({ $isHero }) => ($isHero ? css`
|
|
267
|
+
flex-direction: column;
|
|
268
|
+
margin-bottom: 1.5rem;
|
|
269
|
+
|
|
270
|
+
${NodeImageWrapper},
|
|
271
|
+
${NodeCopyWrapper} {
|
|
272
|
+
width: 100%;
|
|
273
|
+
}`
|
|
274
|
+
: css`
|
|
275
|
+
// Add a funky underline accent to all non-Hero, non-last nodes:
|
|
276
|
+
&:not(:last-child) {
|
|
277
|
+
// Space for funky accent
|
|
278
|
+
padding-bottom: 1.5rem;
|
|
279
|
+
|
|
280
|
+
// The titular funky accent
|
|
281
|
+
&:after {
|
|
282
|
+
position: absolute;
|
|
283
|
+
content: "";
|
|
284
|
+
width: 100%;
|
|
285
|
+
height: 1px;
|
|
286
|
+
bottom: 0;
|
|
287
|
+
left: 0;
|
|
288
|
+
background-color: ${({ theme }) => theme.color('grey_medium')};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
`)};
|
|
292
|
+
|
|
293
|
+
@media ${({ theme }) => theme.allBreakpoints('M')} {
|
|
294
|
+
gap: var(--m-gap);
|
|
295
|
+
|
|
296
|
+
${({ $isHero }) => ($isHero ? css`
|
|
297
|
+
flex-direction: row;
|
|
298
|
+
margin-bottom: 2rem;
|
|
299
|
+
|
|
300
|
+
${NodeImageWrapper} {
|
|
301
|
+
flex: 0 0 calc(40% - var(--s-gap));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
${NodeCopyWrapper} {
|
|
305
|
+
flex: 0 0 calc(60% - var(--s-gap));
|
|
306
|
+
align-self: center;
|
|
307
|
+
}
|
|
308
|
+
` : css`
|
|
309
|
+
// Remove funky underline from all non-Hero nodes on this breakpoint
|
|
310
|
+
&& {
|
|
311
|
+
padding-bottom: 0;
|
|
312
|
+
|
|
313
|
+
&:after {
|
|
314
|
+
content: none;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
`)}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
@media ${({ theme }) => theme.allBreakpoints('L')} {
|
|
321
|
+
flex-direction: row;
|
|
322
|
+
|
|
323
|
+
${({ $isHero }) => ($isHero
|
|
324
|
+
? css`
|
|
325
|
+
gap: var(--l-gap);
|
|
326
|
+
flex: 0 0 100%;
|
|
327
|
+
|
|
328
|
+
${NodeImageWrapper} {
|
|
329
|
+
flex: 0 0 40%;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
${NodeCopyWrapper} {
|
|
333
|
+
flex: 0 0 calc(60% - var(--m-gap));
|
|
334
|
+
}`
|
|
335
|
+
: css`
|
|
336
|
+
flex: 0 0 calc(50% - 1rem);
|
|
337
|
+
`)};
|
|
338
|
+
}
|
|
339
|
+
`;
|
|
340
|
+
|
|
341
|
+
const NodeImage = styled(Picture)`
|
|
342
|
+
img {
|
|
343
|
+
${defaultBoxShadow()};
|
|
344
|
+
border-radius: ${({ $isHero }) => ($isHero ? '1rem' : '0.5rem')};
|
|
345
|
+
|
|
346
|
+
@media ${({ theme }) => theme.allBreakpoints('M')} {
|
|
347
|
+
border-radius: 1rem;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
`;
|
|
351
|
+
|
|
352
|
+
const NodeCopyLabel = styled(Text)`
|
|
353
|
+
font-size: 14px;
|
|
354
|
+
|
|
355
|
+
${({ $isHero }) => (!$isHero && css`
|
|
356
|
+
margin-bottom: 6px;
|
|
357
|
+
|
|
358
|
+
@media ${({ theme }) => theme.allBreakpoints('M')} {
|
|
359
|
+
margin-bottom: 12px;
|
|
360
|
+
}`
|
|
361
|
+
)}
|
|
362
|
+
`;
|
|
363
|
+
|
|
364
|
+
const NodeCopyDescription = styled(Text)`
|
|
365
|
+
margin-bottom: ${({ $isHero }) => ($isHero ? '2rem' : '1.5rem')};
|
|
366
|
+
`;
|
|
367
|
+
|
|
368
|
+
const NodeCopyHeading = styled(Text)`
|
|
369
|
+
${({ $isHero }) => ($isHero ? css`
|
|
370
|
+
${({ $hasDescription }) => (!$hasDescription && css`margin-bottom: 2rem;`)};
|
|
371
|
+
font-weight: normal;
|
|
372
|
+
margin-top: 0;`
|
|
373
|
+
: css`
|
|
374
|
+
// Replicates the larger, standard margin Description usually has, allowing
|
|
375
|
+
// spacing above the CTA to be consistent when Desciption isn't present:
|
|
376
|
+
margin-bottom: ${({ $hasDescription }) => ($hasDescription ? '6px' : '1.5rem')};
|
|
377
|
+
|
|
378
|
+
@media ${({ theme }) => theme.allBreakpoints('M')} {
|
|
379
|
+
margin-bottom: ${({ $hasDescription }) => ($hasDescription ? '12px' : '1.5rem')};
|
|
380
|
+
}
|
|
381
|
+
`)}
|
|
382
|
+
`;
|
|
383
|
+
|
|
384
|
+
// Override 'interesting' styling choices made within the underlying Link
|
|
385
|
+
// component in the past to ensure icon doesn't escape constraints of the parent:
|
|
386
|
+
// without accidentally borking anything in other contexts:
|
|
387
|
+
const NodeCopyLink = styled(Link)`
|
|
388
|
+
display: inline-block;
|
|
389
|
+
padding-right: 2rem;
|
|
390
|
+
|
|
391
|
+
${LinkIconWrapper} {
|
|
392
|
+
right: 0;
|
|
393
|
+
margin: 0;
|
|
394
|
+
}
|
|
395
|
+
`;
|
|
396
|
+
|
|
397
|
+
const ShowMoreButtonWrapper = styled.div`
|
|
398
|
+
display: flex;
|
|
399
|
+
margin-top: 2rem;
|
|
400
|
+
justify-content: center;
|
|
401
|
+
`;
|
|
402
|
+
|
|
403
|
+
export {
|
|
404
|
+
Container,
|
|
405
|
+
OuterWrapper,
|
|
406
|
+
HeaderWrapper,
|
|
407
|
+
Title,
|
|
408
|
+
Body,
|
|
409
|
+
ControlsWrapper,
|
|
410
|
+
ShowHideFiltersButton,
|
|
411
|
+
ClearSelectionButton,
|
|
412
|
+
ResultsWrapper,
|
|
413
|
+
FilterButtonsWrapper,
|
|
414
|
+
FilterButton,
|
|
415
|
+
NodeWrapper,
|
|
416
|
+
Node,
|
|
417
|
+
NodeImageWrapper,
|
|
418
|
+
NodeImage,
|
|
419
|
+
NodeCopyWrapper,
|
|
420
|
+
NodeCopyLabel,
|
|
421
|
+
NodeCopyHeading,
|
|
422
|
+
NodeCopyDescription,
|
|
423
|
+
NodeCopyLink,
|
|
424
|
+
ShowMoreButtonWrapper
|
|
425
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import 'jest-styled-components';
|
|
3
|
+
import renderWithTheme from '../../../../tests/hoc/shallowWithTheme';
|
|
4
|
+
import FilterCard from './FilterCard';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
filterCardTestDataStandard,
|
|
9
|
+
filterCardTestDataHero,
|
|
10
|
+
filterCardTestDataExpanded,
|
|
11
|
+
filterCardTestDataExpandedLoader,
|
|
12
|
+
filterCardTestDataExpandedLoaderHero,
|
|
13
|
+
} from '../../../data/data';
|
|
14
|
+
|
|
15
|
+
it('renders standard FilterCard correctly', () => {
|
|
16
|
+
const tree = renderWithTheme(
|
|
17
|
+
<FilterCard
|
|
18
|
+
data={filterCardTestDataStandard}
|
|
19
|
+
/>
|
|
20
|
+
).toJSON();
|
|
21
|
+
expect(tree).toMatchSnapshot();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renders standard FilterCard w/hero correctly', () => {
|
|
25
|
+
const tree = renderWithTheme(
|
|
26
|
+
<FilterCard
|
|
27
|
+
data={filterCardTestDataHero}
|
|
28
|
+
/>
|
|
29
|
+
).toJSON();
|
|
30
|
+
expect(tree).toMatchSnapshot();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
it('renders standard FilterCard w/expanded content', () => {
|
|
35
|
+
const tree = renderWithTheme(
|
|
36
|
+
<FilterCard
|
|
37
|
+
data={filterCardTestDataExpanded}
|
|
38
|
+
/>
|
|
39
|
+
).toJSON();
|
|
40
|
+
expect(tree).toMatchSnapshot();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
it('renders standard FilterCard w/expanded content + loader', () => {
|
|
45
|
+
const tree = renderWithTheme(
|
|
46
|
+
<FilterCard
|
|
47
|
+
data={filterCardTestDataExpandedLoader}
|
|
48
|
+
/>
|
|
49
|
+
).toJSON();
|
|
50
|
+
expect(tree).toMatchSnapshot();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('renders standard FilterCard w/expanded content, loader + hero', () => {
|
|
54
|
+
const tree = renderWithTheme(
|
|
55
|
+
<FilterCard
|
|
56
|
+
data={filterCardTestDataExpandedLoaderHero}
|
|
57
|
+
/>
|
|
58
|
+
).toJSON();
|
|
59
|
+
expect(tree).toMatchSnapshot();
|
|
60
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ExampleContainer } from '../../../demos/SharedStyles';
|
|
3
|
+
import FilterCard from './FilterCard';
|
|
4
|
+
import {
|
|
5
|
+
filterCardTestDataStandard,
|
|
6
|
+
filterCardTestDataHero,
|
|
7
|
+
filterCardTestDataExpanded,
|
|
8
|
+
filterCardTestDataExpandedLoader,
|
|
9
|
+
filterCardTestDataExpandedLoaderHero
|
|
10
|
+
|
|
11
|
+
} from '../../../data/data';
|
|
12
|
+
|
|
13
|
+
export default function FilterCardExample() {
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
<ExampleContainer $bg="#F4F3F5" style={{ padding: 0 }}>
|
|
17
|
+
<FilterCard
|
|
18
|
+
data={filterCardTestDataStandard}
|
|
19
|
+
/>
|
|
20
|
+
</ExampleContainer>
|
|
21
|
+
|
|
22
|
+
<ExampleContainer $bg="#F4F3F5" style={{ padding: 0 }}>
|
|
23
|
+
<FilterCard
|
|
24
|
+
data={filterCardTestDataHero}
|
|
25
|
+
/>
|
|
26
|
+
</ExampleContainer>
|
|
27
|
+
|
|
28
|
+
<ExampleContainer $bg="#F4F3F5" style={{ padding: 0 }}>
|
|
29
|
+
<FilterCard
|
|
30
|
+
data={filterCardTestDataExpanded}
|
|
31
|
+
/>
|
|
32
|
+
</ExampleContainer>
|
|
33
|
+
|
|
34
|
+
<ExampleContainer $bg="#F4F3F5" style={{ padding: 0 }}>
|
|
35
|
+
<FilterCard
|
|
36
|
+
data={filterCardTestDataExpandedLoader}
|
|
37
|
+
/>
|
|
38
|
+
</ExampleContainer>
|
|
39
|
+
|
|
40
|
+
<ExampleContainer $bg="#F4F3F5" style={{ padding: 0 }}>
|
|
41
|
+
<FilterCard
|
|
42
|
+
data={filterCardTestDataExpandedLoaderHero}
|
|
43
|
+
/>
|
|
44
|
+
</ExampleContainer>
|
|
45
|
+
|
|
46
|
+
</>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import Text from '../../Atoms/Text/Text';
|
|
4
|
+
import { getIcon } from './_utils/_utils';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
Node,
|
|
8
|
+
NodeWrapper,
|
|
9
|
+
NodeImageWrapper,
|
|
10
|
+
NodeImage,
|
|
11
|
+
NodeCopyWrapper,
|
|
12
|
+
NodeCopyLabel,
|
|
13
|
+
NodeCopyHeading,
|
|
14
|
+
NodeCopyDescription,
|
|
15
|
+
NodeCopyLink
|
|
16
|
+
} from './FilterCard.style';
|
|
17
|
+
|
|
18
|
+
const FilterCardNodes = ({
|
|
19
|
+
updateTotalResult,
|
|
20
|
+
processedNodes,
|
|
21
|
+
firstFilterCardNodeAsHero,
|
|
22
|
+
nodeRenderLimit,
|
|
23
|
+
currentFilters,
|
|
24
|
+
hideShowMoreButton
|
|
25
|
+
}) => {
|
|
26
|
+
// Track of how many nodes we successfully rendered, plus how
|
|
27
|
+
// many the current filter has associated with it in total:
|
|
28
|
+
let currentNodeCount = 0;
|
|
29
|
+
let totalNodeCount = 0;
|
|
30
|
+
|
|
31
|
+
const filtersActive = currentFilters.length > 0;
|
|
32
|
+
|
|
33
|
+
// Step through all content this FilterCard component has been fed from the CMS:
|
|
34
|
+
const createNodes = processedNodes.map((thisNode, index) => {
|
|
35
|
+
// Whether or not this node contains a tag we're currently filtering for:
|
|
36
|
+
const tagMatch = currentFilters.some(thisTag => thisNode.tags.includes(thisTag));
|
|
37
|
+
|
|
38
|
+
// Only insist on a tag match when filtering, otherwise just count _every_ node;
|
|
39
|
+
if ((filtersActive && tagMatch) || !filtersActive) totalNodeCount += 1;
|
|
40
|
+
|
|
41
|
+
// Render this node when all filters are inactive, or when it matches 1+ of our active filters:
|
|
42
|
+
const renderThisNode = (!filtersActive || tagMatch);
|
|
43
|
+
|
|
44
|
+
// Finally, check if we've even got a render limit before comparing to the current counter:
|
|
45
|
+
const underLimit = nodeRenderLimit === 0
|
|
46
|
+
|| (nodeRenderLimit > 0 && currentNodeCount < nodeRenderLimit);
|
|
47
|
+
|
|
48
|
+
// If we meet both criteria, go forth and render:
|
|
49
|
+
if (renderThisNode && underLimit) {
|
|
50
|
+
const {
|
|
51
|
+
id, label, heading, description,
|
|
52
|
+
image, imageLow, imageSet, imageAltText,
|
|
53
|
+
url, ctaText, ctaIcon
|
|
54
|
+
} = thisNode;
|
|
55
|
+
|
|
56
|
+
// Applies the Hero styling to the first node, when the CMS is set to do so:
|
|
57
|
+
const heroFirstNode = index === 0 && firstFilterCardNodeAsHero;
|
|
58
|
+
|
|
59
|
+
// Bump the counter for this successful render:
|
|
60
|
+
currentNodeCount += 1;
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Node
|
|
64
|
+
key={id}
|
|
65
|
+
$isHero={heroFirstNode}
|
|
66
|
+
data-testid="FilterCard-Node"
|
|
67
|
+
>
|
|
68
|
+
<NodeImageWrapper>
|
|
69
|
+
<NodeImage
|
|
70
|
+
image={image}
|
|
71
|
+
images={imageSet}
|
|
72
|
+
imageLow={imageLow}
|
|
73
|
+
objectFit="cover"
|
|
74
|
+
width="100%"
|
|
75
|
+
height="100%"
|
|
76
|
+
alt={imageAltText}
|
|
77
|
+
$isHero={heroFirstNode}
|
|
78
|
+
/>
|
|
79
|
+
</NodeImageWrapper>
|
|
80
|
+
|
|
81
|
+
<NodeCopyWrapper tabIndex="0">
|
|
82
|
+
<NodeCopyLabel
|
|
83
|
+
tag="p"
|
|
84
|
+
color="grey"
|
|
85
|
+
weight="500"
|
|
86
|
+
$isHero={heroFirstNode}
|
|
87
|
+
>
|
|
88
|
+
{label}
|
|
89
|
+
</NodeCopyLabel>
|
|
90
|
+
|
|
91
|
+
<NodeCopyHeading
|
|
92
|
+
tag={heroFirstNode ? 'h2' : 'p'}
|
|
93
|
+
weight="700"
|
|
94
|
+
$isHero={heroFirstNode}
|
|
95
|
+
$hasDescription={description}
|
|
96
|
+
data-testid="FilterCard-Node--Heading"
|
|
97
|
+
>
|
|
98
|
+
{heading}
|
|
99
|
+
</NodeCopyHeading>
|
|
100
|
+
|
|
101
|
+
{description && (
|
|
102
|
+
<NodeCopyDescription
|
|
103
|
+
tag="p"
|
|
104
|
+
$isHero={heroFirstNode}
|
|
105
|
+
>
|
|
106
|
+
{description}
|
|
107
|
+
</NodeCopyDescription>
|
|
108
|
+
)}
|
|
109
|
+
|
|
110
|
+
<Text tag="p">
|
|
111
|
+
<NodeCopyLink
|
|
112
|
+
href={url}
|
|
113
|
+
$underline
|
|
114
|
+
target="blank"
|
|
115
|
+
icon={getIcon(ctaIcon)}
|
|
116
|
+
>
|
|
117
|
+
{ctaText}
|
|
118
|
+
</NodeCopyLink>
|
|
119
|
+
</Text>
|
|
120
|
+
</NodeCopyWrapper>
|
|
121
|
+
</Node>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Remove any nulls from our map-function created createNodes array:
|
|
128
|
+
const nodesToRender = createNodes.filter(Boolean);
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
// Let the parent know how many _total_ nodes this filter combination has associated
|
|
132
|
+
// with it (for the 'Results' readout), rather than how many we're _currently_ rendering:
|
|
133
|
+
updateTotalResult(totalNodeCount);
|
|
134
|
+
|
|
135
|
+
// And also let it know if we've rendered everything we can:
|
|
136
|
+
hideShowMoreButton(currentNodeCount === totalNodeCount);
|
|
137
|
+
}, [nodesToRender, updateTotalResult, hideShowMoreButton, currentNodeCount, totalNodeCount]);
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<NodeWrapper>
|
|
141
|
+
{nodesToRender}
|
|
142
|
+
</NodeWrapper>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
FilterCardNodes.propTypes = {
|
|
147
|
+
processedNodes: PropTypes.arrayOf(PropTypes.shape()).isRequired,
|
|
148
|
+
currentFilters: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
149
|
+
firstFilterCardNodeAsHero: PropTypes.bool.isRequired,
|
|
150
|
+
updateTotalResult: PropTypes.func.isRequired,
|
|
151
|
+
nodeRenderLimit: PropTypes.number.isRequired,
|
|
152
|
+
hideShowMoreButton: PropTypes.func.isRequired
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export default FilterCardNodes;
|