@windstream/react-shared-components 0.2.92 → 0.2.94
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/contentful/index.d.ts +46 -9
- package/dist/contentful/index.esm.js +4 -4
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +3 -3
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +6 -3
- package/dist/index.d.ts +4 -1
- package/dist/index.esm.js +2 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.esm.js +1 -1
- package/dist/utils/index.esm.js.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/checklist/index.test.tsx +18 -1
- package/src/components/checklist/index.tsx +25 -21
- package/src/components/checklist/types.ts +8 -2
- package/src/contentful/blocks/address-input-banner/index.test.tsx +14 -0
- package/src/contentful/blocks/address-input-banner/index.tsx +3 -1
- package/src/contentful/blocks/address-input-banner/types.ts +1 -1
- package/src/contentful/blocks/anchored-bottom-banner/index.test.tsx +91 -0
- package/src/contentful/blocks/anchored-bottom-banner/index.tsx +18 -11
- package/src/contentful/blocks/anchored-bottom-banner/types.ts +17 -3
- package/src/contentful/blocks/blogs-grid/index.test.tsx +21 -1
- package/src/contentful/blocks/blogs-grid/index.tsx +1 -0
- package/src/contentful/blocks/blogs-grid/types.ts +3 -0
- package/src/contentful/blocks/blogs-grid-base/index.test.tsx +49 -3
- package/src/contentful/blocks/blogs-grid-base/index.tsx +11 -2
- package/src/contentful/blocks/blogs-grid-base/types.ts +7 -1
- package/src/contentful/blocks/cards/blog-card/index.test.tsx +29 -0
- package/src/contentful/blocks/cards/blog-card/index.tsx +18 -4
- package/src/contentful/blocks/cards/blog-card/types.ts +3 -1
- package/src/contentful/blocks/cards/product-card/index.test.tsx +43 -0
- package/src/contentful/blocks/cards/product-card/index.tsx +37 -9
- package/src/contentful/blocks/cards/product-card/types.ts +3 -0
- package/src/contentful/blocks/carousel/helper.test.tsx +13 -0
- package/src/contentful/blocks/carousel/helper.tsx +1 -0
- package/src/contentful/blocks/carousel/types.ts +2 -0
- package/src/contentful/blocks/dynamic-form/conditional-fields.tsx +1 -0
- package/src/contentful/blocks/dynamic-form/form-field.tsx +1 -0
- package/src/contentful/blocks/dynamic-form/index.test.tsx +48 -0
- package/src/contentful/blocks/dynamic-form/index.tsx +19 -7
- package/src/contentful/blocks/dynamic-form/render.tsx +13 -7
- package/src/contentful/blocks/dynamic-form/types.ts +28 -2
- package/src/contentful/blocks/dynamic-tabs/index.test.tsx +52 -2
- package/src/contentful/blocks/dynamic-tabs/index.tsx +9 -2
- package/src/contentful/blocks/dynamic-tabs/ordered-tab-views.tsx +16 -6
- package/src/contentful/blocks/dynamic-tabs/tab-views.tsx +10 -6
- package/src/contentful/blocks/dynamic-tabs/types.ts +6 -0
- package/src/contentful/blocks/email-input-block/index.test.tsx +51 -3
- package/src/contentful/blocks/email-input-block/index.tsx +88 -31
- package/src/contentful/blocks/email-input-block/types.ts +14 -1
- package/src/contentful/blocks/fiber-form/index.test.tsx +40 -2
- package/src/contentful/blocks/fiber-form/index.tsx +32 -13
- package/src/contentful/blocks/fiber-form/types.ts +18 -0
- package/src/contentful/blocks/iframe-embed/index.test.tsx +44 -7
- package/src/contentful/blocks/iframe-embed/index.tsx +3 -2
- package/src/contentful/blocks/iframe-embed/types.ts +1 -0
- package/src/contentful/blocks/navigation/{Navigation.stories.tsx → navigation.stories.tsx} +1 -1
- package/src/contentful/blocks/ordered-list/index.test.tsx +54 -4
- package/src/contentful/blocks/ordered-list/index.tsx +14 -2
- package/src/contentful/blocks/ordered-list/types.ts +15 -2
- package/src/contentful/blocks/primary-hero/index.tsx +5 -5
- package/src/contentful/blocks/search-block/index.test.tsx +36 -2
- package/src/contentful/blocks/search-block/index.tsx +24 -4
- package/src/contentful/blocks/search-block/types.ts +10 -0
- package/src/utils/index.ts +1 -1
- package/src/utils/replace-tokens.test.ts +40 -0
- package/src/utils/replace-tokens.ts +13 -0
package/package.json
CHANGED
|
@@ -8,11 +8,13 @@ jest.mock("@shared/components/list", () => ({
|
|
|
8
8
|
List: ({
|
|
9
9
|
children,
|
|
10
10
|
className,
|
|
11
|
+
...rest
|
|
11
12
|
}: {
|
|
12
13
|
children: React.ReactNode;
|
|
13
14
|
className?: string;
|
|
15
|
+
[key: string]: unknown;
|
|
14
16
|
}) => (
|
|
15
|
-
<ul data-testid="list" className={className}>
|
|
17
|
+
<ul data-testid="list" className={className} {...rest}>
|
|
16
18
|
{children}
|
|
17
19
|
</ul>
|
|
18
20
|
),
|
|
@@ -117,6 +119,21 @@ describe("Checklist", () => {
|
|
|
117
119
|
});
|
|
118
120
|
|
|
119
121
|
describe("fieldProps", () => {
|
|
122
|
+
it("applies the items fieldProps to the list", () => {
|
|
123
|
+
render(
|
|
124
|
+
<Checklist
|
|
125
|
+
items={["One"]}
|
|
126
|
+
fieldProps={{
|
|
127
|
+
items: { "data-contentful-field-id": "checklist-items" },
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
expect(screen.getByTestId("list")).toHaveAttribute(
|
|
132
|
+
"data-contentful-field-id",
|
|
133
|
+
"checklist-items"
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
120
137
|
it("spreads each structured item's fieldProps onto its own row", () => {
|
|
121
138
|
render(
|
|
122
139
|
<Checklist
|
|
@@ -31,33 +31,37 @@ export const Checklist: React.FC<ChecklistProps> = props => {
|
|
|
31
31
|
iconPosition = "center",
|
|
32
32
|
iconColor = "green",
|
|
33
33
|
iconClassName,
|
|
34
|
+
fieldProps,
|
|
34
35
|
} = props;
|
|
35
36
|
const showIcons = listIconName !== "disc";
|
|
36
37
|
if (!items?.length) return null;
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
39
|
+
const renderContent = (content: React.ReactNode | string) => {
|
|
40
|
+
if (
|
|
41
|
+
typeof content === "string" &&
|
|
42
|
+
content.includes("|") &&
|
|
43
|
+
(content.match(/\|/g) || []).length % 2 === 0
|
|
44
|
+
) {
|
|
45
|
+
const parts = content.split("|");
|
|
46
|
+
const count = (content.match(/\|/g) || []).length;
|
|
47
|
+
return parts.map((part, index) => {
|
|
48
|
+
if (!part) return null;
|
|
49
|
+
const isBold = count > 1 ? index % 2 === 1 : false;
|
|
50
|
+
return isBold ? (
|
|
51
|
+
<Text as="span" key={index} style={{ fontWeight: 900 }}>
|
|
52
|
+
{part}
|
|
53
|
+
</Text>
|
|
54
|
+
) : (
|
|
55
|
+
<span key={index}>{part}</span>
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return content;
|
|
60
|
+
};
|
|
58
61
|
|
|
59
62
|
return (
|
|
60
63
|
<List
|
|
64
|
+
{...fieldProps?.items}
|
|
61
65
|
className={cx("mt-2 space-y-1", listContainerClassName)}
|
|
62
66
|
variant={showIcons ? "unstyled" : "default"}
|
|
63
67
|
>
|
|
@@ -105,7 +109,7 @@ export const Checklist: React.FC<ChecklistProps> = props => {
|
|
|
105
109
|
/>
|
|
106
110
|
</div>
|
|
107
111
|
) : null}
|
|
108
|
-
|
|
112
|
+
<Text as="div">{renderContent(content)}</Text>
|
|
109
113
|
</ListItem>
|
|
110
114
|
);
|
|
111
115
|
})}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import type { FieldProps } from "@shared/types/field-props";
|
|
4
|
+
|
|
1
5
|
export type ChecklistItem = {
|
|
2
|
-
title:
|
|
6
|
+
title: ReactNode;
|
|
3
7
|
iconUrl?: string;
|
|
4
8
|
iconAlt?: string;
|
|
5
9
|
/** Extra DOM attribute props, spread onto this item's list row. */
|
|
@@ -7,7 +11,7 @@ export type ChecklistItem = {
|
|
|
7
11
|
};
|
|
8
12
|
|
|
9
13
|
export type ChecklistProps = {
|
|
10
|
-
items: (
|
|
14
|
+
items: (ReactNode | ChecklistItem)[];
|
|
11
15
|
// TODO: add icon names as needed
|
|
12
16
|
listIconName?: "check" | "disc";
|
|
13
17
|
listItemClassName?: string;
|
|
@@ -16,6 +20,8 @@ export type ChecklistProps = {
|
|
|
16
20
|
iconSize?: 20 | 24 | 32 | 40 | 48 | 52 | undefined;
|
|
17
21
|
iconPosition?: "top" | "center";
|
|
18
22
|
iconColor?: "green" | "yellow" | "blue";
|
|
23
|
+
/** Inspector attributes for the checklist items field. */
|
|
24
|
+
fieldProps?: FieldProps<"items">;
|
|
19
25
|
};
|
|
20
26
|
|
|
21
27
|
export const iconColors = {
|
|
@@ -27,6 +27,20 @@ describe("AddressInputBanner", () => {
|
|
|
27
27
|
);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
it("tags the custom check-plans CTA region", () => {
|
|
31
|
+
render(
|
|
32
|
+
<AddressInputBanner
|
|
33
|
+
{...defaultProps}
|
|
34
|
+
renderCheckPlans={() => <button>Check plans now</button>}
|
|
35
|
+
fieldProps={{ cta: { "data-contentful-field-id": "cta" } }}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
expect(screen.getByText("Check plans now").parentElement).toHaveAttribute(
|
|
39
|
+
"data-contentful-field-id",
|
|
40
|
+
"cta"
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
|
|
30
44
|
it("returns null when isVisible is false", () => {
|
|
31
45
|
const { container } = render(
|
|
32
46
|
<AddressInputBanner {...defaultProps} isVisible={false} />
|
|
@@ -50,7 +50,9 @@ export const AddressInputBanner: FC<AddressInputBannerProps> = props => {
|
|
|
50
50
|
>
|
|
51
51
|
{title}
|
|
52
52
|
</Text>
|
|
53
|
-
{renderedCheckPlans ?
|
|
53
|
+
{renderedCheckPlans ? (
|
|
54
|
+
<div {...fieldProps?.cta}>{renderedCheckPlans}</div>
|
|
55
|
+
) : null}
|
|
54
56
|
</div>
|
|
55
57
|
);
|
|
56
58
|
};
|
|
@@ -13,5 +13,5 @@ export interface AddressInputBannerProps {
|
|
|
13
13
|
isVisible?: boolean;
|
|
14
14
|
renderCheckPlans?: (overrides?: CheckPlansProps) => React.ReactNode;
|
|
15
15
|
/** Extra DOM attribute props, spread onto the respective rendered elements. */
|
|
16
|
-
fieldProps?: FieldProps<"title">;
|
|
16
|
+
fieldProps?: FieldProps<"title" | "cta">;
|
|
17
17
|
}
|
|
@@ -65,6 +65,41 @@ describe("AnchoredBottomBanner", () => {
|
|
|
65
65
|
);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
it("tags the default banner entry, icon, timer, and linked button label", () => {
|
|
69
|
+
render(
|
|
70
|
+
<AnchoredBottomBanner
|
|
71
|
+
enableCountdownTimer={true}
|
|
72
|
+
countdownEndDateTime="2026-05-19T13:00:00Z"
|
|
73
|
+
iconName="local_offer"
|
|
74
|
+
ctaButtonLabel="Shop now"
|
|
75
|
+
fieldProps={{
|
|
76
|
+
entry: { "data-contentful-entry-id": "banner-entry" },
|
|
77
|
+
iconName: { "data-contentful-field-id": "iconName" },
|
|
78
|
+
countdownEndDateTime: {
|
|
79
|
+
"data-contentful-field-id": "countdownEndDateTime",
|
|
80
|
+
},
|
|
81
|
+
buttonLabel: { "data-contentful-field-id": "buttonLabel" },
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(screen.getByText("Shop now")).toHaveAttribute(
|
|
87
|
+
"data-contentful-field-id",
|
|
88
|
+
"buttonLabel"
|
|
89
|
+
);
|
|
90
|
+
expect(screen.getByText("local_offer").parentElement).toHaveAttribute(
|
|
91
|
+
"data-contentful-field-id",
|
|
92
|
+
"iconName"
|
|
93
|
+
);
|
|
94
|
+
expect(screen.getByText(/01H : 00M : 00/)).toHaveAttribute(
|
|
95
|
+
"data-contentful-field-id",
|
|
96
|
+
"countdownEndDateTime"
|
|
97
|
+
);
|
|
98
|
+
expect(
|
|
99
|
+
document.querySelector('[data-contentful-entry-id="banner-entry"]')
|
|
100
|
+
).toBeInTheDocument();
|
|
101
|
+
});
|
|
102
|
+
|
|
68
103
|
it("uses active scheduled-message fieldProps for its CTA content", () => {
|
|
69
104
|
render(
|
|
70
105
|
<AnchoredBottomBanner
|
|
@@ -387,6 +422,62 @@ describe("AnchoredBottomBanner scheduledMessages", () => {
|
|
|
387
422
|
expect(screen.getByText(/01H : 00M : 00/)).toBeInTheDocument();
|
|
388
423
|
});
|
|
389
424
|
|
|
425
|
+
it("uses the active scheduled-message entry tags for banner content", () => {
|
|
426
|
+
render(
|
|
427
|
+
<AnchoredBottomBanner
|
|
428
|
+
enableCountdownTimer={true}
|
|
429
|
+
countdownEndDateTime="2026-05-19T14:00:00Z"
|
|
430
|
+
iconName="local_offer"
|
|
431
|
+
ctaButtonLabel="Default label"
|
|
432
|
+
fieldProps={{
|
|
433
|
+
entry: { "data-contentful-entry-id": "default-entry" },
|
|
434
|
+
iconName: { "data-contentful-field-id": "default-icon" },
|
|
435
|
+
countdownEndDateTime: {
|
|
436
|
+
"data-contentful-field-id": "default-countdown",
|
|
437
|
+
},
|
|
438
|
+
buttonLabel: { "data-contentful-field-id": "default-label" },
|
|
439
|
+
}}
|
|
440
|
+
scheduledMessages={[
|
|
441
|
+
{
|
|
442
|
+
scheduledStart: "2026-05-18",
|
|
443
|
+
scheduleEnd: "2026-05-20",
|
|
444
|
+
enableCountdownTimer: true,
|
|
445
|
+
countdownEndDateTime: "2026-05-19T13:00:00Z",
|
|
446
|
+
iconName: "bolt",
|
|
447
|
+
ctaButtonLabel: "Flash sale",
|
|
448
|
+
fieldProps: {
|
|
449
|
+
entry: { "data-contentful-entry-id": "scheduled-entry" },
|
|
450
|
+
iconName: { "data-contentful-field-id": "scheduled-icon" },
|
|
451
|
+
countdownEndDateTime: {
|
|
452
|
+
"data-contentful-field-id": "scheduled-countdown",
|
|
453
|
+
},
|
|
454
|
+
buttonLabel: { "data-contentful-field-id": "scheduled-label" },
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
]}
|
|
458
|
+
/>
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
expect(screen.getByText("Flash sale")).toHaveAttribute(
|
|
462
|
+
"data-contentful-field-id",
|
|
463
|
+
"scheduled-label"
|
|
464
|
+
);
|
|
465
|
+
expect(screen.getByText("bolt").parentElement).toHaveAttribute(
|
|
466
|
+
"data-contentful-field-id",
|
|
467
|
+
"scheduled-icon"
|
|
468
|
+
);
|
|
469
|
+
expect(screen.getByText(/01H : 00M : 00/)).toHaveAttribute(
|
|
470
|
+
"data-contentful-field-id",
|
|
471
|
+
"scheduled-countdown"
|
|
472
|
+
);
|
|
473
|
+
expect(
|
|
474
|
+
document.querySelector('[data-contentful-entry-id="scheduled-entry"]')
|
|
475
|
+
).toBeInTheDocument();
|
|
476
|
+
expect(
|
|
477
|
+
document.querySelector('[data-contentful-entry-id="default-entry"]')
|
|
478
|
+
).not.toBeInTheDocument();
|
|
479
|
+
});
|
|
480
|
+
|
|
390
481
|
it("treats scheduleEnd as inclusive of the whole end day", () => {
|
|
391
482
|
render(
|
|
392
483
|
<AnchoredBottomBanner
|
|
@@ -343,6 +343,7 @@ export const AnchoredBottomBanner: React.FC<AnchoredBottomBannerProps> = ({
|
|
|
343
343
|
className={`fixed bottom-0 left-0 right-0 z-[30] flex w-full items-center justify-center px-4 py-3 transition-all duration-300 ${bgClass} ${
|
|
344
344
|
boxShadow ? boxShadow : "shadow-[0_-4px_10px_rgba(0,0,0,0.1)]"
|
|
345
345
|
}`}
|
|
346
|
+
{...effectiveFieldProps?.entry}
|
|
346
347
|
>
|
|
347
348
|
<Link
|
|
348
349
|
href={effectiveCtaButtonLink || "#"}
|
|
@@ -354,28 +355,34 @@ export const AnchoredBottomBanner: React.FC<AnchoredBottomBannerProps> = ({
|
|
|
354
355
|
className={`${textColorClass} break-words text-body1 font-black leading-snug md:text-[18px]`}
|
|
355
356
|
>
|
|
356
357
|
{effectiveIconName && (
|
|
357
|
-
<
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
358
|
+
<span {...effectiveFieldProps?.iconName}>
|
|
359
|
+
<MaterialIcon
|
|
360
|
+
name={
|
|
361
|
+
effectiveIconName as React.ComponentProps<
|
|
362
|
+
typeof MaterialIcon
|
|
363
|
+
>["name"]
|
|
364
|
+
}
|
|
365
|
+
size={24}
|
|
366
|
+
fill={1}
|
|
367
|
+
className={`${textColorClass} align-text-bottom`}
|
|
368
|
+
/>
|
|
369
|
+
</span>
|
|
367
370
|
)}
|
|
368
371
|
{countdown.shouldShow && (
|
|
369
372
|
<span
|
|
370
373
|
className="inline-block whitespace-nowrap rounded-lg bg-white px-1 tabular-nums text-text"
|
|
371
374
|
suppressHydrationWarning={true}
|
|
375
|
+
{...effectiveFieldProps?.countdownEndDateTime}
|
|
372
376
|
>
|
|
373
377
|
{countdown.text}
|
|
374
378
|
</span>
|
|
375
379
|
)}
|
|
376
380
|
{countdown.shouldShow ? " " : null}
|
|
377
381
|
{effectiveCtaButtonLabel && (
|
|
378
|
-
<span
|
|
382
|
+
<span
|
|
383
|
+
{...(effectiveFieldProps?.buttonLabel ??
|
|
384
|
+
effectiveFieldProps?.ctaButtonLabel)}
|
|
385
|
+
>
|
|
379
386
|
{effectiveCtaButtonLabel}
|
|
380
387
|
</span>
|
|
381
388
|
)}{" "}
|
|
@@ -51,7 +51,14 @@ export interface ScheduledMessage {
|
|
|
51
51
|
ctaSuffixText?: string;
|
|
52
52
|
ctaButtonLink?: string;
|
|
53
53
|
ctaButtonTarget?: string;
|
|
54
|
-
fieldProps?: FieldProps<
|
|
54
|
+
fieldProps?: FieldProps<
|
|
55
|
+
| "entry"
|
|
56
|
+
| "iconName"
|
|
57
|
+
| "countdownEndDateTime"
|
|
58
|
+
| "buttonLabel"
|
|
59
|
+
| "ctaButtonLabel"
|
|
60
|
+
| "ctaSuffixText"
|
|
61
|
+
>;
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
export interface AnchoredBottomBannerProps {
|
|
@@ -80,6 +87,13 @@ export interface AnchoredBottomBannerProps {
|
|
|
80
87
|
activeMessage?: ScheduledMessage;
|
|
81
88
|
applicableWeekday: string;
|
|
82
89
|
}) => void;
|
|
83
|
-
/** DOM attributes for the
|
|
84
|
-
fieldProps?: FieldProps<
|
|
90
|
+
/** DOM attributes for the banner entry and rendered content fields. */
|
|
91
|
+
fieldProps?: FieldProps<
|
|
92
|
+
| "entry"
|
|
93
|
+
| "iconName"
|
|
94
|
+
| "countdownEndDateTime"
|
|
95
|
+
| "buttonLabel"
|
|
96
|
+
| "ctaButtonLabel"
|
|
97
|
+
| "ctaSuffixText"
|
|
98
|
+
>;
|
|
85
99
|
}
|
|
@@ -70,7 +70,7 @@ jest.mock("@shared/components/text", () => ({
|
|
|
70
70
|
}));
|
|
71
71
|
|
|
72
72
|
jest.mock("@shared/contentful/blocks/cards/blog-card", () => ({
|
|
73
|
-
BlogCard: ({ href, title, category, image, index }: any) => (
|
|
73
|
+
BlogCard: ({ href, title, category, image, index, fieldProps }: any) => (
|
|
74
74
|
<a
|
|
75
75
|
href={href}
|
|
76
76
|
data-testid={`blog-card-${index}`}
|
|
@@ -78,6 +78,7 @@ jest.mock("@shared/contentful/blocks/cards/blog-card", () => ({
|
|
|
78
78
|
data-image-alt={image?.alt || ""}
|
|
79
79
|
data-image-width={image?.width || ""}
|
|
80
80
|
data-image-height={image?.height || ""}
|
|
81
|
+
data-field-props={JSON.stringify(fieldProps)}
|
|
81
82
|
>
|
|
82
83
|
{title} - {category}
|
|
83
84
|
</a>
|
|
@@ -136,6 +137,25 @@ const defaultProps: BlogGridProps = {
|
|
|
136
137
|
};
|
|
137
138
|
|
|
138
139
|
describe("BlogGrid", () => {
|
|
140
|
+
it("forwards each article's inspector slots without tagging another article", () => {
|
|
141
|
+
const fields = { title: { "data-contentful-entry-id": "article-one" } };
|
|
142
|
+
render(
|
|
143
|
+
<BlogGrid
|
|
144
|
+
{...defaultProps}
|
|
145
|
+
paginatedArticles={[
|
|
146
|
+
{ ...mockArticles[0], fieldProps: fields },
|
|
147
|
+
mockArticles[1],
|
|
148
|
+
]}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
expect(screen.getByTestId("blog-card-0")).toHaveAttribute(
|
|
152
|
+
"data-field-props",
|
|
153
|
+
JSON.stringify(fields)
|
|
154
|
+
);
|
|
155
|
+
expect(screen.getByTestId("blog-card-1")).not.toHaveAttribute(
|
|
156
|
+
"data-field-props"
|
|
157
|
+
);
|
|
158
|
+
});
|
|
139
159
|
beforeEach(() => {
|
|
140
160
|
jest.clearAllMocks();
|
|
141
161
|
mockSearchParams = new URLSearchParams("");
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type { BlogCardProps as CardProps } from "@shared/contentful/blocks/cards/blog-card/types";
|
|
2
|
+
|
|
1
3
|
export interface BlogCardProps {
|
|
2
4
|
slug: string;
|
|
5
|
+
fieldProps?: CardProps["fieldProps"];
|
|
3
6
|
title?: string;
|
|
4
7
|
shortDescription?: string;
|
|
5
8
|
blogCreationDate?: string;
|
|
@@ -41,18 +41,29 @@ jest.mock("@shared/components/select", () => ({
|
|
|
41
41
|
}));
|
|
42
42
|
|
|
43
43
|
jest.mock("@shared/components/text", () => ({
|
|
44
|
-
Text: ({ as: Tag = "span", children, className }: any) => (
|
|
45
|
-
<Tag className={className}
|
|
44
|
+
Text: ({ as: Tag = "span", children, className, ...rest }: any) => (
|
|
45
|
+
<Tag className={className} {...rest}>
|
|
46
|
+
{children}
|
|
47
|
+
</Tag>
|
|
46
48
|
),
|
|
47
49
|
}));
|
|
48
50
|
|
|
49
51
|
jest.mock("@shared/contentful/blocks/cards/blog-card", () => ({
|
|
50
|
-
BlogCard: ({
|
|
52
|
+
BlogCard: ({
|
|
53
|
+
href,
|
|
54
|
+
title,
|
|
55
|
+
category,
|
|
56
|
+
image,
|
|
57
|
+
imageComponent,
|
|
58
|
+
index,
|
|
59
|
+
fieldProps,
|
|
60
|
+
}: any) => (
|
|
51
61
|
<a
|
|
52
62
|
href={href}
|
|
53
63
|
data-testid={`blog-card-${index}`}
|
|
54
64
|
data-has-image={!!image}
|
|
55
65
|
data-has-image-component={!!imageComponent}
|
|
66
|
+
data-field-props={JSON.stringify(fieldProps)}
|
|
56
67
|
>
|
|
57
68
|
{title} - {category}
|
|
58
69
|
</a>
|
|
@@ -108,6 +119,41 @@ const defaultProps: BlogGridBaseProps = {
|
|
|
108
119
|
};
|
|
109
120
|
|
|
110
121
|
describe("BlogGridBase", () => {
|
|
122
|
+
it("tags authored headings and forwards child article slots without tagging siblings", () => {
|
|
123
|
+
const articleFields = {
|
|
124
|
+
title: { "data-contentful-entry-id": "article-one" },
|
|
125
|
+
};
|
|
126
|
+
render(
|
|
127
|
+
<BlogGridBase
|
|
128
|
+
{...defaultProps}
|
|
129
|
+
title="Latest Stories"
|
|
130
|
+
description="Updates"
|
|
131
|
+
fieldProps={{
|
|
132
|
+
title: { "data-contentful-field-id": "recentArticlesTitle" },
|
|
133
|
+
description: { "data-contentful-field-id": "description" },
|
|
134
|
+
}}
|
|
135
|
+
paginatedArticles={[
|
|
136
|
+
{ ...mockArticles[0], fieldProps: articleFields },
|
|
137
|
+
mockArticles[1],
|
|
138
|
+
]}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
expect(screen.getByText("Latest Stories")).toHaveAttribute(
|
|
142
|
+
"data-contentful-field-id",
|
|
143
|
+
"recentArticlesTitle"
|
|
144
|
+
);
|
|
145
|
+
expect(screen.getByText("Updates")).toHaveAttribute(
|
|
146
|
+
"data-contentful-field-id",
|
|
147
|
+
"description"
|
|
148
|
+
);
|
|
149
|
+
expect(screen.getByTestId("blog-card-0")).toHaveAttribute(
|
|
150
|
+
"data-field-props",
|
|
151
|
+
JSON.stringify(articleFields)
|
|
152
|
+
);
|
|
153
|
+
expect(screen.getByTestId("blog-card-1")).not.toHaveAttribute(
|
|
154
|
+
"data-field-props"
|
|
155
|
+
);
|
|
156
|
+
});
|
|
111
157
|
beforeEach(() => jest.clearAllMocks());
|
|
112
158
|
|
|
113
159
|
describe("Rendering", () => {
|
|
@@ -19,6 +19,7 @@ export function BlogGridBase({
|
|
|
19
19
|
getPageHref,
|
|
20
20
|
asList = false,
|
|
21
21
|
imageComponent,
|
|
22
|
+
fieldProps,
|
|
22
23
|
}: BlogGridBaseProps) {
|
|
23
24
|
function handleCategoryChange(value: unknown) {
|
|
24
25
|
if (!value || Array.isArray(value)) return;
|
|
@@ -39,11 +40,18 @@ export function BlogGridBase({
|
|
|
39
40
|
{/* Controls bar */}
|
|
40
41
|
<div className="mx-auto flex max-w-[1200px] flex-wrap items-center justify-between gap-3 px-6 pb-6">
|
|
41
42
|
{description && (
|
|
42
|
-
<Text
|
|
43
|
+
<Text
|
|
44
|
+
as="p"
|
|
45
|
+
className="body1 mb-4 w-full text-center md:mb-6"
|
|
46
|
+
{...fieldProps?.description}
|
|
47
|
+
>
|
|
43
48
|
{description}
|
|
44
49
|
</Text>
|
|
45
50
|
)}
|
|
46
|
-
<Text
|
|
51
|
+
<Text
|
|
52
|
+
className="w-full text-center text-heading5 font-black lowercase text-text-secondary md:w-auto md:text-left"
|
|
53
|
+
{...fieldProps?.title}
|
|
54
|
+
>
|
|
47
55
|
{title}
|
|
48
56
|
</Text>
|
|
49
57
|
{categoryOptions.length > 0 && (
|
|
@@ -105,6 +113,7 @@ export function BlogGridBase({
|
|
|
105
113
|
description={article.shortDescription}
|
|
106
114
|
date={article.blogCreationDate}
|
|
107
115
|
category={article.category}
|
|
116
|
+
fieldProps={article.fieldProps}
|
|
108
117
|
image={coverImage}
|
|
109
118
|
imageComponent={imageComponent}
|
|
110
119
|
asGrid={!asList}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
BlogCardImageProps,
|
|
5
|
+
BlogCardProps as CardProps,
|
|
6
|
+
} from "@shared/contentful/blocks/cards/blog-card/types";
|
|
7
|
+
import type { FieldProps } from "@shared/types/field-props";
|
|
4
8
|
|
|
5
9
|
export interface BlogCardProps {
|
|
6
10
|
slug: string;
|
|
11
|
+
fieldProps?: CardProps["fieldProps"];
|
|
7
12
|
title?: string;
|
|
8
13
|
shortDescription?: string;
|
|
9
14
|
blogCreationDate?: string;
|
|
@@ -17,6 +22,7 @@ export interface BlogCardProps {
|
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
export interface BlogGridBaseProps {
|
|
25
|
+
fieldProps?: FieldProps<"title" | "description">;
|
|
20
26
|
title?: string;
|
|
21
27
|
description?: string;
|
|
22
28
|
paginatedArticles: BlogCardProps[];
|
|
@@ -32,6 +32,35 @@ describe("BlogCard", () => {
|
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
describe("Rendering", () => {
|
|
35
|
+
it("tags each authored child field independently", () => {
|
|
36
|
+
const tag = (field: string) => ({ "data-contentful-field-id": field });
|
|
37
|
+
const { container } = render(
|
|
38
|
+
<BlogCard
|
|
39
|
+
{...defaultProps}
|
|
40
|
+
image={{ src: "/cover.jpg", alt: "Cover", width: 400, height: 300 }}
|
|
41
|
+
fieldProps={{
|
|
42
|
+
title: tag("title"),
|
|
43
|
+
description: tag("shortDescription"),
|
|
44
|
+
date: tag("blogCreationDate"),
|
|
45
|
+
category: tag("category"),
|
|
46
|
+
image: tag("cover"),
|
|
47
|
+
href: tag("slug"),
|
|
48
|
+
}}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
for (const field of [
|
|
52
|
+
"title",
|
|
53
|
+
"shortDescription",
|
|
54
|
+
"blogCreationDate",
|
|
55
|
+
"category",
|
|
56
|
+
"cover",
|
|
57
|
+
"slug",
|
|
58
|
+
]) {
|
|
59
|
+
expect(
|
|
60
|
+
container.querySelector(`[data-contentful-field-id="${field}"]`)
|
|
61
|
+
).not.toBeNull();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
35
64
|
it("renders the article element", () => {
|
|
36
65
|
const { container } = render(<BlogCard {...defaultProps} />);
|
|
37
66
|
expect(container.querySelector("article")).toBeInTheDocument();
|
|
@@ -43,7 +43,10 @@ export const BlogCard: React.FC<BlogCardProps> = ({
|
|
|
43
43
|
>
|
|
44
44
|
{/* Image (hidden in list view) */}
|
|
45
45
|
{asGrid && (
|
|
46
|
-
<div
|
|
46
|
+
<div
|
|
47
|
+
className="block h-[232px] w-full flex-shrink-0 overflow-hidden bg-gray-100"
|
|
48
|
+
{...fieldProps?.image}
|
|
49
|
+
>
|
|
47
50
|
{image ? (
|
|
48
51
|
ImageComponent ? (
|
|
49
52
|
<ImageComponent
|
|
@@ -82,7 +85,11 @@ export const BlogCard: React.FC<BlogCardProps> = ({
|
|
|
82
85
|
>
|
|
83
86
|
{/* Meta: category + date */}
|
|
84
87
|
<div className="flex items-center gap-2 text-[13px]">
|
|
85
|
-
{asGrid &&
|
|
88
|
+
{asGrid && (
|
|
89
|
+
<span className="body2 text-text-brand" {...fieldProps?.category}>
|
|
90
|
+
{category}
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
86
93
|
{date && (
|
|
87
94
|
<>
|
|
88
95
|
{asGrid && (
|
|
@@ -90,7 +97,9 @@ export const BlogCard: React.FC<BlogCardProps> = ({
|
|
|
90
97
|
•
|
|
91
98
|
</span>
|
|
92
99
|
)}
|
|
93
|
-
<time className="body2 text-text"
|
|
100
|
+
<time className="body2 text-text" {...fieldProps?.date}>
|
|
101
|
+
{date}
|
|
102
|
+
</time>
|
|
94
103
|
</>
|
|
95
104
|
)}
|
|
96
105
|
</div>
|
|
@@ -100,6 +109,7 @@ export const BlogCard: React.FC<BlogCardProps> = ({
|
|
|
100
109
|
<Text as="h2" className="heading6" {...fieldProps?.title}>
|
|
101
110
|
<Link
|
|
102
111
|
href={href}
|
|
112
|
+
{...fieldProps?.href}
|
|
103
113
|
className="m-0 line-clamp-3 font-black transition-colors duration-150 after:absolute after:inset-0 after:z-10 after:content-[''] hover:text-text-brand group-hover:text-text-brand"
|
|
104
114
|
>
|
|
105
115
|
{title}
|
|
@@ -120,7 +130,11 @@ export const BlogCard: React.FC<BlogCardProps> = ({
|
|
|
120
130
|
|
|
121
131
|
{/* Fallback stretched link keeps the card navigable when no title link is rendered */}
|
|
122
132
|
{!title && (
|
|
123
|
-
<Link
|
|
133
|
+
<Link
|
|
134
|
+
href={href}
|
|
135
|
+
className="absolute inset-0 z-10"
|
|
136
|
+
{...fieldProps?.href}
|
|
137
|
+
>
|
|
124
138
|
<span className="sr-only">{category ?? "Read more"}</span>
|
|
125
139
|
</Link>
|
|
126
140
|
)}
|