@windstream/react-shared-components 0.2.86 → 0.2.88
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 +20 -4
- package/dist/contentful/index.esm.js +4 -4
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +4 -4
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/checklist/index.test.tsx +31 -1
- package/src/components/checklist/index.tsx +1 -0
- package/src/components/checklist/types.ts +2 -0
- package/src/contentful/blocks/accordion/index.test.tsx +53 -0
- package/src/contentful/blocks/accordion/index.tsx +7 -2
- package/src/contentful/blocks/accordion/types.ts +4 -0
- package/src/contentful/blocks/callout/{callout.stories.tsx → Callout.stories.tsx} +1 -1
- package/src/contentful/blocks/callout/field-props.test.tsx +184 -0
- package/src/contentful/blocks/callout/index.tsx +26 -4
- package/src/contentful/blocks/callout/types.ts +17 -1
- package/src/contentful/blocks/cards/blog-card/index.tsx +6 -2
- package/src/contentful/blocks/cards/blog-card/types.ts +4 -0
- package/src/contentful/blocks/cards/floating-image-card/index.tsx +4 -2
- package/src/contentful/blocks/cards/floating-image-card/types.ts +4 -0
- package/src/contentful/blocks/cards/full-image-card/index.tsx +24 -5
- package/src/contentful/blocks/cards/full-image-card/types.ts +4 -0
- package/src/contentful/blocks/cards/product-card/index.test.tsx +29 -4
- package/src/contentful/blocks/cards/product-card/index.tsx +46 -44
- package/src/contentful/blocks/cards/product-card/types.ts +1 -1
- package/src/contentful/blocks/cards/simple-card/index.tsx +21 -3
- package/src/contentful/blocks/cards/simple-card/types.ts +4 -0
- package/src/contentful/blocks/carousel/helper.test.tsx +24 -0
- package/src/contentful/blocks/carousel/helper.tsx +1 -1
- package/src/contentful/blocks/comparison-table/index.test.tsx +75 -0
- package/src/contentful/blocks/comparison-table/index.tsx +15 -3
- package/src/contentful/blocks/comparison-table/types.ts +8 -2
- package/src/contentful/blocks/cta-callout/index.test.tsx +93 -1
- package/src/contentful/blocks/cta-callout/index.tsx +9 -2
- package/src/contentful/blocks/cta-callout/types.ts +3 -0
- package/src/contentful/blocks/modal/index.test.tsx +75 -2
- package/src/contentful/blocks/modal/index.tsx +11 -2
- package/src/contentful/blocks/modal/types.ts +4 -0
- package/src/contentful/blocks/navigation/{navigation.stories.tsx → Navigation.stories.tsx} +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ButtonProps } from "../../button/types";
|
|
2
2
|
|
|
3
|
+
import type { FieldProps } from "@shared/types/field-props";
|
|
4
|
+
|
|
3
5
|
export type FullImageAsset = {
|
|
4
6
|
href: string;
|
|
5
7
|
title?: string;
|
|
@@ -24,6 +26,8 @@ export type FullImageCardItem = {
|
|
|
24
26
|
imageClassName?: string;
|
|
25
27
|
/** Applies `shadow-card` to the outer card element. */
|
|
26
28
|
shadow?: boolean;
|
|
29
|
+
/** Per-field DOM attributes supplied by the consumer or parent Callout item. */
|
|
30
|
+
fieldProps?: FieldProps<"title" | "description" | "caption" | "cta">;
|
|
27
31
|
};
|
|
28
32
|
|
|
29
33
|
export type FullImageCardProps = {
|
|
@@ -179,6 +179,29 @@ describe("ProductCard", () => {
|
|
|
179
179
|
});
|
|
180
180
|
});
|
|
181
181
|
|
|
182
|
+
describe("Missing price", () => {
|
|
183
|
+
it.each([
|
|
184
|
+
["empty", ""],
|
|
185
|
+
["undefined", undefined],
|
|
186
|
+
])("hides the price block when price is %s", (_label, price) => {
|
|
187
|
+
const { container } = render(
|
|
188
|
+
<ProductCard
|
|
189
|
+
{...defaultProps}
|
|
190
|
+
price={price}
|
|
191
|
+
priceSuffixBottom="for 12 months"
|
|
192
|
+
/>
|
|
193
|
+
);
|
|
194
|
+
expect(screen.queryByText("$")).not.toBeInTheDocument();
|
|
195
|
+
expect(screen.queryByText(/\/mo/)).not.toBeInTheDocument();
|
|
196
|
+
expect(screen.queryByText("for 12 months")).not.toBeInTheDocument();
|
|
197
|
+
expect(container.textContent).not.toContain("0.00");
|
|
198
|
+
expect(screen.getByRole("heading", { level: 3 })).toHaveTextContent(
|
|
199
|
+
"Kinetic 500"
|
|
200
|
+
);
|
|
201
|
+
expect(screen.getByTestId("select-plan-btn")).toBeInTheDocument();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
182
205
|
describe("Best Value", () => {
|
|
183
206
|
it("renders best value banner when bestValue is true", () => {
|
|
184
207
|
render(<ProductCard {...defaultProps} bestValue={true} />);
|
|
@@ -300,11 +323,13 @@ describe("ProductCard", () => {
|
|
|
300
323
|
});
|
|
301
324
|
|
|
302
325
|
it("renders the real gift slot (not the empty placeholder) when the card has its own gift", () => {
|
|
303
|
-
const giftBadge = [
|
|
304
|
-
{ title: "Free router", icon: "/router.png" },
|
|
305
|
-
] as any;
|
|
326
|
+
const giftBadge = [{ title: "Free router", icon: "/router.png" }] as any;
|
|
306
327
|
const { container } = render(
|
|
307
|
-
<ProductCard
|
|
328
|
+
<ProductCard
|
|
329
|
+
{...defaultProps}
|
|
330
|
+
giftBadge={giftBadge}
|
|
331
|
+
hasAnyGift={true}
|
|
332
|
+
/>
|
|
308
333
|
);
|
|
309
334
|
expect(
|
|
310
335
|
container.querySelector("[data-pc-gift-empty]")
|
|
@@ -137,51 +137,53 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
137
137
|
) : null}
|
|
138
138
|
</Text>
|
|
139
139
|
</div>
|
|
140
|
-
|
|
141
|
-
<div className="flex items-start"
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
140
|
+
{price ? (
|
|
141
|
+
<div className="flex flex-col items-start md:items-end">
|
|
142
|
+
<div className="flex items-start" {...fieldProps?.price}>
|
|
143
|
+
<Text
|
|
144
|
+
as="span"
|
|
145
|
+
className={cx(
|
|
146
|
+
priceUnitClass,
|
|
147
|
+
"mr-1 mt-[2px]",
|
|
148
|
+
isDark ? "text-text-inverse" : "text-text"
|
|
149
|
+
)}
|
|
150
|
+
>
|
|
151
|
+
$
|
|
152
|
+
</Text>
|
|
153
|
+
<Text
|
|
154
|
+
as="span"
|
|
155
|
+
className={cx(
|
|
156
|
+
priceIntegerClass,
|
|
157
|
+
"leading-none",
|
|
158
|
+
isDark ? "text-text-inverse" : "text-text"
|
|
159
|
+
)}
|
|
160
|
+
>
|
|
161
|
+
{price.split(".")[0]}
|
|
162
|
+
</Text>
|
|
163
|
+
<Text
|
|
164
|
+
as="span"
|
|
165
|
+
className={cx(
|
|
166
|
+
priceUnitClass,
|
|
167
|
+
isDark ? "text-text-inverse" : "text-text"
|
|
168
|
+
)}
|
|
169
|
+
>
|
|
170
|
+
{`${price.split(".")[1]}${priceUnit}`}
|
|
171
|
+
</Text>
|
|
172
|
+
</div>
|
|
173
|
+
{priceSuffixBottom ? (
|
|
174
|
+
<Text
|
|
175
|
+
as="span"
|
|
176
|
+
{...fieldProps?.priceSuffixBottom}
|
|
177
|
+
className={cx(
|
|
178
|
+
"micro text-nowrap",
|
|
179
|
+
isDark ? "text-text-inverse" : "text-text"
|
|
180
|
+
)}
|
|
181
|
+
>
|
|
182
|
+
{priceSuffixBottom}
|
|
183
|
+
</Text>
|
|
184
|
+
) : null}
|
|
171
185
|
</div>
|
|
172
|
-
|
|
173
|
-
<Text
|
|
174
|
-
as="span"
|
|
175
|
-
{...fieldProps?.priceSuffixBottom}
|
|
176
|
-
className={cx(
|
|
177
|
-
"micro text-nowrap",
|
|
178
|
-
isDark ? "text-text-inverse" : "text-text"
|
|
179
|
-
)}
|
|
180
|
-
>
|
|
181
|
-
{priceSuffixBottom}
|
|
182
|
-
</Text>
|
|
183
|
-
) : null}
|
|
184
|
-
</div>
|
|
186
|
+
) : null}
|
|
185
187
|
</header>
|
|
186
188
|
|
|
187
189
|
{/* Description */}
|
|
@@ -10,7 +10,7 @@ export interface ProductCardProps {
|
|
|
10
10
|
speed: string;
|
|
11
11
|
techType?: string;
|
|
12
12
|
ismaxSpeed?: boolean;
|
|
13
|
-
price
|
|
13
|
+
price?: string;
|
|
14
14
|
/** The price unit rendered after the cents, e.g. "/mo" or "/mo*". Defaults
|
|
15
15
|
* to "/mo". Lets Contentful control a trailing disclaimer star. */
|
|
16
16
|
priceUnit?: string;
|
|
@@ -97,7 +97,13 @@ export const SimpleCard: React.FC<SimpleCardProps> = ({
|
|
|
97
97
|
>
|
|
98
98
|
<NextImage
|
|
99
99
|
src={card.image}
|
|
100
|
-
alt={
|
|
100
|
+
alt={
|
|
101
|
+
card.imageAltText ??
|
|
102
|
+
card.imageDescription ??
|
|
103
|
+
card.imageAlt ??
|
|
104
|
+
card.title ??
|
|
105
|
+
"card-image"
|
|
106
|
+
}
|
|
101
107
|
width={card.imageWidth || 400}
|
|
102
108
|
height={card.imageHeight || 280}
|
|
103
109
|
className={cx("w-full", !margin && "rounded-t-3xl")}
|
|
@@ -124,7 +130,13 @@ export const SimpleCard: React.FC<SimpleCardProps> = ({
|
|
|
124
130
|
>
|
|
125
131
|
<NextImage
|
|
126
132
|
src={card.image}
|
|
127
|
-
alt={
|
|
133
|
+
alt={
|
|
134
|
+
card.imageAltText ??
|
|
135
|
+
card.imageDescription ??
|
|
136
|
+
card.imageAlt ??
|
|
137
|
+
card.title ??
|
|
138
|
+
"card-icon"
|
|
139
|
+
}
|
|
128
140
|
width={imgWidth}
|
|
129
141
|
height={imgHeight}
|
|
130
142
|
className={cx(
|
|
@@ -144,6 +156,7 @@ export const SimpleCard: React.FC<SimpleCardProps> = ({
|
|
|
144
156
|
as="h3"
|
|
145
157
|
className="heading6 mb-2 text-text md:heading6"
|
|
146
158
|
style={textColorStyle}
|
|
159
|
+
{...card.fieldProps?.title}
|
|
147
160
|
>
|
|
148
161
|
{card.title}
|
|
149
162
|
</Text>
|
|
@@ -300,6 +313,7 @@ export const SimpleCard: React.FC<SimpleCardProps> = ({
|
|
|
300
313
|
as="div"
|
|
301
314
|
className="body1 text-text"
|
|
302
315
|
style={textColorStyle}
|
|
316
|
+
{...card.fieldProps?.body}
|
|
303
317
|
>
|
|
304
318
|
{card.body}
|
|
305
319
|
</Text>
|
|
@@ -309,6 +323,7 @@ export const SimpleCard: React.FC<SimpleCardProps> = ({
|
|
|
309
323
|
as="div"
|
|
310
324
|
className="body1 text-text"
|
|
311
325
|
style={textColorStyle}
|
|
326
|
+
{...card.fieldProps?.richText}
|
|
312
327
|
>
|
|
313
328
|
{card.richText}
|
|
314
329
|
</Text>
|
|
@@ -318,7 +333,10 @@ export const SimpleCard: React.FC<SimpleCardProps> = ({
|
|
|
318
333
|
</div>
|
|
319
334
|
|
|
320
335
|
{card.cta ? (
|
|
321
|
-
<div
|
|
336
|
+
<div
|
|
337
|
+
className={cx(!isCtaOnly && "mt-2", ctaAlignmentClass)}
|
|
338
|
+
{...card.fieldProps?.cta}
|
|
339
|
+
>
|
|
322
340
|
<Button
|
|
323
341
|
linkVariant="unstyled"
|
|
324
342
|
linkClassName={cx(
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CSSProperties, ReactNode } from "react";
|
|
2
2
|
import type { ButtonProps } from "../../button/types";
|
|
3
3
|
|
|
4
|
+
import type { FieldProps } from "@shared/types/field-props";
|
|
5
|
+
|
|
4
6
|
export type PinwheelColor =
|
|
5
7
|
| "Blue-#307998"
|
|
6
8
|
| "Green-#209A61"
|
|
@@ -54,6 +56,8 @@ export type Item = {
|
|
|
54
56
|
/** Each entry uses a `label|value` separator. */
|
|
55
57
|
workingHours?: string[];
|
|
56
58
|
shadow?: boolean;
|
|
59
|
+
/** Per-field DOM attributes supplied by the consumer or parent Callout item. */
|
|
60
|
+
fieldProps?: FieldProps<"title" | "body" | "richText" | "cta">;
|
|
57
61
|
};
|
|
58
62
|
|
|
59
63
|
export type SimpleCardProps = {
|
|
@@ -34,11 +34,13 @@ jest.mock("@shared/contentful/blocks/cards/product-card", () => ({
|
|
|
34
34
|
enableBusinessSettings,
|
|
35
35
|
giftBadge,
|
|
36
36
|
hasAnyGift,
|
|
37
|
+
price,
|
|
37
38
|
}: any) => (
|
|
38
39
|
<article
|
|
39
40
|
data-testid="product-card"
|
|
40
41
|
data-plan={planName}
|
|
41
42
|
data-speed={speed}
|
|
43
|
+
data-price={price}
|
|
42
44
|
data-expanded={isExpanded}
|
|
43
45
|
data-business={enableBusinessSettings ? "true" : "false"}
|
|
44
46
|
>
|
|
@@ -373,6 +375,28 @@ describe("ProductCardCarousel", () => {
|
|
|
373
375
|
expect(card).toHaveAttribute("data-plan", "500 Mbps");
|
|
374
376
|
});
|
|
375
377
|
|
|
378
|
+
it("passes the formatted price when a price is authored", () => {
|
|
379
|
+
const fields = createProductFields([sampleProduct]);
|
|
380
|
+
render(<ProductCardCarousel fields={fields} />);
|
|
381
|
+
expect(screen.getByTestId("product-card")).toHaveAttribute(
|
|
382
|
+
"data-price",
|
|
383
|
+
"49.99"
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it.each([
|
|
388
|
+
["undefined", undefined],
|
|
389
|
+
["empty", ""],
|
|
390
|
+
])("passes an empty price (not 0.00) when price is %s", (_label, price) => {
|
|
391
|
+
const item = { ...sampleProduct, price };
|
|
392
|
+
const fields = createProductFields([item]);
|
|
393
|
+
render(<ProductCardCarousel fields={fields} />);
|
|
394
|
+
expect(screen.getByTestId("product-card")).toHaveAttribute(
|
|
395
|
+
"data-price",
|
|
396
|
+
""
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
|
|
376
400
|
it("renders Next arrow and hides Previous on first slide in carousel mode", async () => {
|
|
377
401
|
const items = Array.from({ length: 4 }, (_, i) => ({
|
|
378
402
|
...sampleProduct,
|
|
@@ -380,7 +380,7 @@ function ProductCardPanel({
|
|
|
380
380
|
const planName = planContent?.[0] || "";
|
|
381
381
|
const planSubtext = planContent?.[1] || "";
|
|
382
382
|
const priceCents = item?.priceSuffix?.split("/")?.[0] || "00";
|
|
383
|
-
const formattedPrice =
|
|
383
|
+
const formattedPrice = item?.price ? `${item.price}.${priceCents}` : "";
|
|
384
384
|
// Preserve the full unit after the cents (e.g. "/mo*") so a Contentful
|
|
385
385
|
// disclaimer star renders; fall back to "/mo" when no unit is present.
|
|
386
386
|
const suffixParts = item?.priceSuffix?.split("/") || [];
|
|
@@ -41,6 +41,81 @@ describe("ComparisonTable", () => {
|
|
|
41
41
|
expect(container.querySelector(".component-container")).toBeInTheDocument();
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
describe("fieldProps", () => {
|
|
45
|
+
it("tags the block fields and preserves consumer-supplied row tags", () => {
|
|
46
|
+
const tag = (entry: string, field: string) => ({
|
|
47
|
+
"data-contentful-entry-id": entry,
|
|
48
|
+
"data-contentful-field-id": field,
|
|
49
|
+
"data-contentful-locale": "en-US",
|
|
50
|
+
});
|
|
51
|
+
const { container } = render(
|
|
52
|
+
<ComparisonTable
|
|
53
|
+
{...defaultProps}
|
|
54
|
+
fieldProps={{
|
|
55
|
+
title: tag("comparison", "title"),
|
|
56
|
+
table: tag("comparison", "table"),
|
|
57
|
+
disclaimer: tag("comparison", "disclaimer"),
|
|
58
|
+
}}
|
|
59
|
+
table={
|
|
60
|
+
<table>
|
|
61
|
+
<tbody>
|
|
62
|
+
<tr {...tag("row-1", "feature")}>
|
|
63
|
+
<td>Speed</td>
|
|
64
|
+
</tr>
|
|
65
|
+
<tr {...tag("row-2", "feature")}>
|
|
66
|
+
<td>Price</td>
|
|
67
|
+
</tr>
|
|
68
|
+
</tbody>
|
|
69
|
+
</table>
|
|
70
|
+
}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
expect(screen.getByText("Compare Plans")).toHaveAttribute(
|
|
74
|
+
"data-contentful-field-id",
|
|
75
|
+
"title"
|
|
76
|
+
);
|
|
77
|
+
expect(
|
|
78
|
+
container.querySelector(".comparison-table-container")
|
|
79
|
+
).toHaveAttribute("data-contentful-field-id", "table");
|
|
80
|
+
expect(
|
|
81
|
+
screen.getByText("* Terms and conditions apply").parentElement
|
|
82
|
+
).toHaveAttribute("data-contentful-field-id", "disclaimer");
|
|
83
|
+
expect(screen.getByText("Speed").closest("tr")).toHaveAttribute(
|
|
84
|
+
"data-contentful-entry-id",
|
|
85
|
+
"row-1"
|
|
86
|
+
);
|
|
87
|
+
expect(screen.getByText("Price").closest("tr")).toHaveAttribute(
|
|
88
|
+
"data-contentful-entry-id",
|
|
89
|
+
"row-2"
|
|
90
|
+
);
|
|
91
|
+
container
|
|
92
|
+
.querySelectorAll("[data-contentful-field-id]")
|
|
93
|
+
.forEach(element => {
|
|
94
|
+
expect(element).toHaveAttribute("data-contentful-locale", "en-US");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("supports partial fieldProps", () => {
|
|
99
|
+
const { container } = render(
|
|
100
|
+
<ComparisonTable
|
|
101
|
+
{...defaultProps}
|
|
102
|
+
fieldProps={{ table: { "data-contentful-field-id": "table" } }}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
expect(
|
|
106
|
+
container.querySelectorAll("[data-contentful-field-id]")
|
|
107
|
+
).toHaveLength(1);
|
|
108
|
+
expect(screen.getByText("Compare Plans")).not.toHaveAttribute(
|
|
109
|
+
"data-contentful-field-id"
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("does not add tagging attributes when fieldProps is omitted", () => {
|
|
114
|
+
const { container } = render(<ComparisonTable {...defaultProps} />);
|
|
115
|
+
expect(container.querySelector("[data-contentful-field-id]")).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
44
119
|
it("renders the title as h2", () => {
|
|
45
120
|
render(<ComparisonTable {...defaultProps} />);
|
|
46
121
|
const title = screen.getByText("Compare Plans");
|
|
@@ -8,19 +8,31 @@ export const ComparisonTable: React.FC<ComparisonTableProps> = ({
|
|
|
8
8
|
disclaimer,
|
|
9
9
|
table,
|
|
10
10
|
maxWidth = true,
|
|
11
|
+
fieldProps,
|
|
11
12
|
}) => {
|
|
12
13
|
return (
|
|
13
14
|
<div className="component-container">
|
|
14
15
|
<div
|
|
15
16
|
className={`mx-5 mb-5 mt-8 md:mt-18 ${maxWidth ? "max-w-120 xl:mx-auto" : ""}`}
|
|
16
17
|
>
|
|
17
|
-
<Text
|
|
18
|
+
<Text
|
|
19
|
+
as="h2"
|
|
20
|
+
className="heading2 md:heading1 md:text-center"
|
|
21
|
+
{...fieldProps?.title}
|
|
22
|
+
>
|
|
18
23
|
{title}
|
|
19
24
|
</Text>
|
|
20
|
-
<div
|
|
25
|
+
<div
|
|
26
|
+
className="comparison-table-container mt-5 p-5 pt-0 md:mt-10 md:px-10 md:py-9"
|
|
27
|
+
{...fieldProps?.table}
|
|
28
|
+
>
|
|
21
29
|
{table}
|
|
22
30
|
</div>
|
|
23
|
-
<Text
|
|
31
|
+
<Text
|
|
32
|
+
as="div"
|
|
33
|
+
className="micro mt-8 text-center"
|
|
34
|
+
{...fieldProps?.disclaimer}
|
|
35
|
+
>
|
|
24
36
|
{disclaimer}
|
|
25
37
|
</Text>
|
|
26
38
|
</div>
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import type { FieldProps } from "@shared/types/field-props";
|
|
4
|
+
|
|
1
5
|
export type ComparisonTableProps = {
|
|
2
6
|
title?: string;
|
|
3
|
-
disclaimer?:
|
|
4
|
-
table:
|
|
7
|
+
disclaimer?: ReactNode;
|
|
8
|
+
table: ReactNode;
|
|
5
9
|
maxWidth?: boolean;
|
|
10
|
+
/** DOM attributes for the block fields. Tag individual rows in the consumer-supplied `table` node. */
|
|
11
|
+
fieldProps?: FieldProps<"title" | "table" | "disclaimer">;
|
|
6
12
|
};
|
|
@@ -19,8 +19,12 @@ jest.mock("@shared/contentful/blocks/button", () => ({
|
|
|
19
19
|
data-testid="cta-button"
|
|
20
20
|
data-size={JSON.stringify(props.size)}
|
|
21
21
|
data-href={props.href}
|
|
22
|
+
{...props.fieldProps}
|
|
22
23
|
>
|
|
23
|
-
{props.
|
|
24
|
+
{props.preDefinedFunctionExecution === "check availability" &&
|
|
25
|
+
props.renderCheckPlans
|
|
26
|
+
? props.renderCheckPlans()
|
|
27
|
+
: props.buttonLabel || props.children}
|
|
24
28
|
</button>
|
|
25
29
|
),
|
|
26
30
|
}));
|
|
@@ -37,6 +41,94 @@ describe("CtaCallout", () => {
|
|
|
37
41
|
descriptionAlignment: "center",
|
|
38
42
|
};
|
|
39
43
|
|
|
44
|
+
describe("fieldProps", () => {
|
|
45
|
+
it("tags all block fields without changing nested button tags", () => {
|
|
46
|
+
const tag = (field: string) => ({
|
|
47
|
+
"data-contentful-field-id": field,
|
|
48
|
+
"data-contentful-entry-id": "cta-callout",
|
|
49
|
+
"data-contentful-locale": "en-US",
|
|
50
|
+
});
|
|
51
|
+
const { container } = render(
|
|
52
|
+
<CtaCallout
|
|
53
|
+
{...defaultProps}
|
|
54
|
+
button={{
|
|
55
|
+
...defaultProps.button,
|
|
56
|
+
fieldProps: { "data-contentful-field-id": "buttonLabel" },
|
|
57
|
+
}}
|
|
58
|
+
fieldProps={{
|
|
59
|
+
title: tag("title"),
|
|
60
|
+
subTitle: tag("subTitle"),
|
|
61
|
+
description: tag("description"),
|
|
62
|
+
button: tag("button"),
|
|
63
|
+
}}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
expect(screen.getByText(defaultProps.title!)).toHaveAttribute(
|
|
67
|
+
"data-contentful-field-id",
|
|
68
|
+
"title"
|
|
69
|
+
);
|
|
70
|
+
expect(screen.getByText(defaultProps.subTitle!)).toHaveAttribute(
|
|
71
|
+
"data-contentful-field-id",
|
|
72
|
+
"subTitle"
|
|
73
|
+
);
|
|
74
|
+
expect(
|
|
75
|
+
screen.getByText("Choose the plan that works for you.").parentElement
|
|
76
|
+
).toHaveAttribute("data-contentful-field-id", "description");
|
|
77
|
+
expect(screen.getByTestId("cta-button").parentElement).toHaveAttribute(
|
|
78
|
+
"data-contentful-field-id",
|
|
79
|
+
"button"
|
|
80
|
+
);
|
|
81
|
+
expect(screen.getByTestId("cta-button")).toHaveAttribute(
|
|
82
|
+
"data-contentful-field-id",
|
|
83
|
+
"buttonLabel"
|
|
84
|
+
);
|
|
85
|
+
container
|
|
86
|
+
.querySelectorAll("[data-contentful-entry-id]")
|
|
87
|
+
.forEach(element => {
|
|
88
|
+
expect(element).toHaveAttribute(
|
|
89
|
+
"data-contentful-entry-id",
|
|
90
|
+
"cta-callout"
|
|
91
|
+
);
|
|
92
|
+
expect(element).toHaveAttribute("data-contentful-locale", "en-US");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("tags the CTA region for custom renderers", () => {
|
|
97
|
+
render(
|
|
98
|
+
<CtaCallout
|
|
99
|
+
button={{ preDefinedFunctionExecution: "check availability" }}
|
|
100
|
+
renderCheckPlans={() => <span>Custom check plans</span>}
|
|
101
|
+
fieldProps={{ button: { "data-contentful-field-id": "button" } }}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
expect(screen.getByTestId("cta-button").parentElement).toHaveAttribute(
|
|
105
|
+
"data-contentful-field-id",
|
|
106
|
+
"button"
|
|
107
|
+
);
|
|
108
|
+
expect(screen.getByText("Custom check plans")).toBeInTheDocument();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("does not add tagging attributes when fieldProps is omitted", () => {
|
|
112
|
+
const { container } = render(<CtaCallout {...defaultProps} />);
|
|
113
|
+
expect(container.querySelector("[data-contentful-field-id]")).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("supports partial fieldProps and does not create absent fields", () => {
|
|
117
|
+
const { container } = render(
|
|
118
|
+
<CtaCallout
|
|
119
|
+
title="Only title"
|
|
120
|
+
fieldProps={{
|
|
121
|
+
title: { "data-contentful-field-id": "title" },
|
|
122
|
+
description: { "data-contentful-field-id": "description" },
|
|
123
|
+
}}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
expect(
|
|
127
|
+
container.querySelectorAll("[data-contentful-field-id]")
|
|
128
|
+
).toHaveLength(1);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
40
132
|
it("renders the component", () => {
|
|
41
133
|
const { container } = render(<CtaCallout {...defaultProps} />);
|
|
42
134
|
expect(container.firstChild).toBeInTheDocument();
|
|
@@ -17,6 +17,7 @@ export const CtaCallout: React.FC<CtaCalloutProps> = ({
|
|
|
17
17
|
maxWidth = true,
|
|
18
18
|
onModalButtonClick,
|
|
19
19
|
renderCheckPlans,
|
|
20
|
+
fieldProps,
|
|
20
21
|
}) => {
|
|
21
22
|
const bgColorClasses: Record<ThemeKey, string> = {
|
|
22
23
|
navy: "bg-[#00002D]",
|
|
@@ -28,7 +29,7 @@ export const CtaCallout: React.FC<CtaCalloutProps> = ({
|
|
|
28
29
|
};
|
|
29
30
|
return (
|
|
30
31
|
<div
|
|
31
|
-
className={`${bgColorClasses[background]} component-container px-5 py-16 lg:px-13 lg:py-24
|
|
32
|
+
className={`${bgColorClasses[background]} component-container w-full px-5 py-16 lg:px-13 lg:py-24`}
|
|
32
33
|
>
|
|
33
34
|
<div
|
|
34
35
|
className={`${maxWidth ? "mx-auto max-w-120" : ""} color-${color} flex flex-col ${color == "dark" ? "text-text" : "text-text-inverse"}`}
|
|
@@ -37,6 +38,7 @@ export const CtaCallout: React.FC<CtaCalloutProps> = ({
|
|
|
37
38
|
<Text
|
|
38
39
|
as={enableHeading ? "h1" : "h2"}
|
|
39
40
|
className={`heading2 lg:heading1 text-${contentAlignment}`}
|
|
41
|
+
{...fieldProps?.title}
|
|
40
42
|
>
|
|
41
43
|
{title}
|
|
42
44
|
</Text>
|
|
@@ -45,6 +47,7 @@ export const CtaCallout: React.FC<CtaCalloutProps> = ({
|
|
|
45
47
|
<Text
|
|
46
48
|
as="h3"
|
|
47
49
|
className={`subheading3 pt-2 lg:subheading1 lg:pt-3 text-${contentAlignment}`}
|
|
50
|
+
{...fieldProps?.subTitle}
|
|
48
51
|
>
|
|
49
52
|
{subTitle}
|
|
50
53
|
</Text>
|
|
@@ -52,12 +55,16 @@ export const CtaCallout: React.FC<CtaCalloutProps> = ({
|
|
|
52
55
|
{description && (
|
|
53
56
|
<div
|
|
54
57
|
className={`pt-2 text-body1 lg:pt-3 text-${descriptionAlignment} break-words [&_table]:w-full`}
|
|
58
|
+
{...fieldProps?.description}
|
|
55
59
|
>
|
|
56
60
|
{description}
|
|
57
61
|
</div>
|
|
58
62
|
)}
|
|
59
63
|
{button || renderCheckPlans ? (
|
|
60
|
-
<div
|
|
64
|
+
<div
|
|
65
|
+
className="flex justify-center pt-5 lg:pt-14"
|
|
66
|
+
{...fieldProps?.button}
|
|
67
|
+
>
|
|
61
68
|
<Button
|
|
62
69
|
{...button}
|
|
63
70
|
renderCheckPlans={renderCheckPlans}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
+
import type { FieldProps } from "@shared/types/field-props";
|
|
3
4
|
import { CheckPlansProps } from "@shared/types/micro-components";
|
|
4
5
|
|
|
5
6
|
export type CtaCalloutProps = {
|
|
@@ -15,6 +16,8 @@ export type CtaCalloutProps = {
|
|
|
15
16
|
maxWidth?: boolean;
|
|
16
17
|
onModalButtonClick?: (id?: string) => void;
|
|
17
18
|
renderCheckPlans?: (overrides?: CheckPlansProps) => React.ReactNode;
|
|
19
|
+
/** Consumer-supplied DOM attributes; `button` tags the CTA region, including custom renderers. */
|
|
20
|
+
fieldProps?: FieldProps<"title" | "subTitle" | "description" | "button">;
|
|
18
21
|
};
|
|
19
22
|
|
|
20
23
|
export type ThemeKey =
|