@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.
Files changed (44) hide show
  1. package/dist/index.cjs.js +1227 -859
  2. package/dist/index.es.js +10902 -10144
  3. package/package.json +1 -1
  4. package/src/App.styles.js +2 -2
  5. package/src/components/Atoms/Button/Button.js +13 -3
  6. package/src/components/Atoms/Button/Button.style.js +19 -1
  7. package/src/components/Atoms/Button/Button.test.js +1 -1
  8. package/src/components/Atoms/Button/ButtonExample.jsx +24 -0
  9. package/src/components/Atoms/ButtonWithStates/ButtonWithStates.js +1 -1
  10. package/src/components/Atoms/Icons/Cross.js +0 -1
  11. package/src/components/Atoms/Icons/Download.js +4 -4
  12. package/src/components/Atoms/Icons/External.js +4 -4
  13. package/src/components/Atoms/Icons/Filter.js +50 -0
  14. package/src/components/Atoms/Icons/IconsExample.jsx +9 -5
  15. package/src/components/Atoms/Icons/Undo.js +44 -0
  16. package/src/components/Atoms/Icons/index.js +2 -0
  17. package/src/components/Atoms/Link/Link.test.js +1 -1
  18. package/src/components/Atoms/Link/LinkExample.jsx +4 -2
  19. package/src/components/Atoms/Text/TextExample.jsx +338 -130
  20. package/src/components/Molecules/CardDs/__snapshots__/CardDs.test.js.snap +2 -2
  21. package/src/components/Molecules/Promo/__snapshots__/Promo.test.js.snap +2 -2
  22. package/src/components/Molecules/QuoteSlice/__snapshots__/QuoteSlice.test.js.snap +1 -1
  23. package/src/components/Molecules/SingleMessage/__snapshots__/SingleMessage.test.js.snap +2 -2
  24. package/src/components/Molecules/SingleMessageDS/__snapshots__/SingleMessageDs.test.js.snap +1 -3
  25. package/src/components/Organisms/CookieBanner/CookieBanner.test.js +1 -1
  26. package/src/components/Organisms/EmailBanner/__snapshots__/EmailBanner.test.js.snap +12 -12
  27. package/src/components/Organisms/EmailSignUp/__snapshots__/EmailSignUp.test.js.snap +3 -3
  28. package/src/components/Organisms/FilterCard/FilterCard.js +286 -0
  29. package/src/components/Organisms/FilterCard/FilterCard.style.js +425 -0
  30. package/src/components/Organisms/FilterCard/FilterCard.test.js +60 -0
  31. package/src/components/Organisms/FilterCard/FilterCardExample.jsx +48 -0
  32. package/src/components/Organisms/FilterCard/FilterCardNodes.js +155 -0
  33. package/src/components/Organisms/FilterCard/__snapshots__/FilterCard.test.js.snap +1006 -0
  34. package/src/components/Organisms/FilterCard/_utils/_utils.js +57 -0
  35. package/src/components/Organisms/FilterCard/assets/Rectangle.svg +3 -0
  36. package/src/components/Organisms/FooterNew/__snapshots__/FooterNew.test.js.snap +4 -4
  37. package/src/components/Organisms/MarketingPreferencesDS/MarketingPreferencesDS.style.js +1 -1
  38. package/src/data/TestFilterCardNodeData.js +930 -0
  39. package/src/data/data.js +64 -1
  40. package/src/data/test-icons/Children--black.svg +5 -0
  41. package/src/data/test-icons/Children--white.svg +5 -0
  42. package/src/demos/index.jsx +31 -9
  43. package/src/index.js +1 -0
  44. 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;