@windstream/react-shared-components 0.2.67 → 0.2.69
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 +26 -6
- package/dist/contentful/index.esm.js +2 -2
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +2 -2
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/index.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/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/select-plan-button/index.test.tsx +27 -0
- package/src/components/select-plan-button/index.tsx +6 -1
- package/src/contentful/blocks/cards/product-card/index.test.tsx +47 -2
- package/src/contentful/blocks/cards/product-card/index.tsx +46 -44
- package/src/contentful/blocks/cards/product-card/types.ts +3 -0
- package/src/contentful/blocks/carousel/helper.tsx +6 -0
- package/src/contentful/blocks/carousel/index.tsx +1 -1
- package/src/contentful/blocks/dynamic-form/DynamicForm.stories.tsx +3 -0
- package/src/contentful/blocks/dynamic-form/field-map.tsx +1 -1
- package/src/contentful/blocks/dynamic-form/index.test.tsx +96 -0
- package/src/contentful/blocks/dynamic-form/index.tsx +24 -1
- package/src/contentful/blocks/dynamic-form/render.test.tsx +25 -0
- package/src/contentful/blocks/dynamic-form/render.tsx +15 -0
- package/src/contentful/blocks/dynamic-form/types.ts +21 -5
- package/src/contentful/blocks/navigation/index.tsx +4 -1
- package/src/contentful/blocks/primary-hero/index.test.tsx +11 -2
- package/src/contentful/blocks/primary-hero/index.tsx +3 -2
|
@@ -11,12 +11,17 @@ import { SelectPlanButton } from "@shared/components/select-plan-button";
|
|
|
11
11
|
import { Text } from "@shared/components/text";
|
|
12
12
|
import { cx } from "@shared/utils";
|
|
13
13
|
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Renders text where `|...|`-wrapped segments (e.g. the dollar value in a gift
|
|
16
|
+
* badge) use the heavier `label3` weight (900), while the surrounding text keeps
|
|
17
|
+
* the parent `body3` weight (500) — matching residential's gift-reward styling.
|
|
18
|
+
* Pipes are stripped.
|
|
19
|
+
*/
|
|
15
20
|
const renderPipeBold = (value: React.ReactNode): React.ReactNode => {
|
|
16
21
|
if (typeof value !== "string" || !value.includes("|")) return value;
|
|
17
22
|
return value.split("|").map((segment, index) =>
|
|
18
23
|
index % 2 === 1 ? (
|
|
19
|
-
<span key={index} className="
|
|
24
|
+
<span key={index} className="label3">
|
|
20
25
|
{segment}
|
|
21
26
|
</span>
|
|
22
27
|
) : (
|
|
@@ -30,6 +35,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
30
35
|
planSubtext,
|
|
31
36
|
speed,
|
|
32
37
|
price,
|
|
38
|
+
priceUnit = "/mo",
|
|
33
39
|
description,
|
|
34
40
|
bestValue = false,
|
|
35
41
|
bestValueText = "best value",
|
|
@@ -69,9 +75,9 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
69
75
|
// stacks keep the standard sizes at every breakpoint.
|
|
70
76
|
const planNameClass = twoColumn ? "label3 md:subheading2" : "subheading2";
|
|
71
77
|
const priceIntegerClass = twoColumn
|
|
72
|
-
? "heading2 md:
|
|
73
|
-
: "
|
|
74
|
-
const priceUnitClass = twoColumn ? "label3 md:
|
|
78
|
+
? "heading2 md:subheading5"
|
|
79
|
+
: "subheading5";
|
|
80
|
+
const priceUnitClass = twoColumn ? "label3 md:label1" : "label1";
|
|
75
81
|
|
|
76
82
|
return (
|
|
77
83
|
<article className="relative flex h-full w-full max-w-[392px] flex-col">
|
|
@@ -115,15 +121,12 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
115
121
|
)}
|
|
116
122
|
>
|
|
117
123
|
{planName}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
)}
|
|
125
|
-
>
|
|
126
|
-
{planSubtext}
|
|
124
|
+
{planSubtext ? (
|
|
125
|
+
<>
|
|
126
|
+
<br />
|
|
127
|
+
{planSubtext}
|
|
128
|
+
</>
|
|
129
|
+
) : null}
|
|
127
130
|
</Text>
|
|
128
131
|
</div>
|
|
129
132
|
<div className="flex flex-col items-start md:items-end">
|
|
@@ -155,14 +158,14 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
155
158
|
isDark ? "text-text-inverse" : "text-text"
|
|
156
159
|
)}
|
|
157
160
|
>
|
|
158
|
-
{price.split(".")[1]}
|
|
161
|
+
{`${price.split(".")[1]}${priceUnit}`}
|
|
159
162
|
</Text>
|
|
160
163
|
</div>
|
|
161
164
|
{priceSuffixBottom ? (
|
|
162
165
|
<Text
|
|
163
166
|
as="span"
|
|
164
167
|
className={cx(
|
|
165
|
-
"
|
|
168
|
+
"micro text-nowrap",
|
|
166
169
|
isDark ? "text-text-inverse" : "text-text"
|
|
167
170
|
)}
|
|
168
171
|
>
|
|
@@ -176,7 +179,8 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
176
179
|
<section data-pc-description>
|
|
177
180
|
<Text
|
|
178
181
|
className={cx(
|
|
179
|
-
"footnote md:body2
|
|
182
|
+
twoColumn ? "footnote md:body2" : "body2",
|
|
183
|
+
"md:h-[56px]",
|
|
180
184
|
isDark ? "text-text-inverse" : "text-text"
|
|
181
185
|
)}
|
|
182
186
|
>
|
|
@@ -189,18 +193,21 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
189
193
|
<div data-pc-gift className="min-h-[76px]">
|
|
190
194
|
<div
|
|
191
195
|
className={cx(
|
|
192
|
-
"flex-col
|
|
196
|
+
"flex w-full flex-col gap-3 rounded-surface-sm p-2",
|
|
193
197
|
isDark ? "bg-bg-surface-inverse" : "bg-bg-surface-info"
|
|
194
198
|
)}
|
|
195
199
|
>
|
|
196
200
|
{giftBadge?.map((badge: any) => {
|
|
197
201
|
return (
|
|
198
|
-
<div
|
|
202
|
+
<div
|
|
203
|
+
key={badge.title}
|
|
204
|
+
className="flex w-full items-center gap-2"
|
|
205
|
+
>
|
|
199
206
|
<Image
|
|
200
207
|
src={badge.icon}
|
|
201
208
|
alt={badge.title}
|
|
202
|
-
width={
|
|
203
|
-
height={
|
|
209
|
+
width={20}
|
|
210
|
+
height={20}
|
|
204
211
|
/>
|
|
205
212
|
<Text
|
|
206
213
|
as="div"
|
|
@@ -290,38 +297,33 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
290
297
|
iconClassName="mt-0"
|
|
291
298
|
iconColor={isDark ? "yellow" : "green"}
|
|
292
299
|
listContainerClassName="mt-0 space-y-0 flex flex-col gap-3"
|
|
293
|
-
listItemClassName={
|
|
300
|
+
listItemClassName={`body3 ${isDark ? "text-text-inverse" : "text-text"}`}
|
|
294
301
|
/>
|
|
295
302
|
</div>
|
|
296
303
|
{innerBadge?.badgeIcon ? (
|
|
297
|
-
<div className="mt-3 flex items-center gap-
|
|
304
|
+
<div className="mt-3 flex items-center gap-1 text-left">
|
|
298
305
|
<Image
|
|
299
306
|
src={innerBadge.badgeIcon}
|
|
300
307
|
alt="Inner Badge"
|
|
301
308
|
width={24}
|
|
302
309
|
height={24}
|
|
310
|
+
className="ml-1 h-auto pr-2"
|
|
303
311
|
/>
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
) : (
|
|
320
|
-
<Text className="font-bold">
|
|
321
|
-
{innerBadge.badgeText}
|
|
322
|
-
</Text>
|
|
323
|
-
)}
|
|
324
|
-
</span>
|
|
312
|
+
{(innerBadge.badgeText.includes("|")
|
|
313
|
+
? innerBadge.badgeText.split("|")
|
|
314
|
+
: [innerBadge.badgeText]
|
|
315
|
+
).map((badge, index) => (
|
|
316
|
+
<Text
|
|
317
|
+
key={index}
|
|
318
|
+
as="span"
|
|
319
|
+
className={cx(
|
|
320
|
+
index === 0 ? "label3" : "body3",
|
|
321
|
+
isDark ? "text-text-inverse" : "text-text"
|
|
322
|
+
)}
|
|
323
|
+
>
|
|
324
|
+
{badge.trim()}
|
|
325
|
+
</Text>
|
|
326
|
+
))}
|
|
325
327
|
</div>
|
|
326
328
|
) : null}
|
|
327
329
|
</div>
|
|
@@ -10,6 +10,9 @@ export interface ProductCardProps {
|
|
|
10
10
|
techType?: string;
|
|
11
11
|
ismaxSpeed?: boolean;
|
|
12
12
|
price: string;
|
|
13
|
+
/** The price unit rendered after the cents, e.g. "/mo" or "/mo*". Defaults
|
|
14
|
+
* to "/mo". Lets Contentful control a trailing disclaimer star. */
|
|
15
|
+
priceUnit?: string;
|
|
13
16
|
priceSuffixBottom?: string;
|
|
14
17
|
description: string;
|
|
15
18
|
bestValue?: boolean;
|
|
@@ -263,6 +263,11 @@ function ProductCardPanel({
|
|
|
263
263
|
const planSubtext = planContent?.[1] || "";
|
|
264
264
|
const priceCents = item?.priceSuffix?.split("/")?.[0] || "00";
|
|
265
265
|
const formattedPrice = `${item?.price || "0"}.${priceCents}`;
|
|
266
|
+
// Preserve the full unit after the cents (e.g. "/mo*") so a Contentful
|
|
267
|
+
// disclaimer star renders; fall back to "/mo" when no unit is present.
|
|
268
|
+
const suffixParts = item?.priceSuffix?.split("/") || [];
|
|
269
|
+
const priceUnit =
|
|
270
|
+
suffixParts.length > 1 ? `/${suffixParts.slice(1).join("/")}` : "/mo";
|
|
266
271
|
const features = item?.benefits?.items || [];
|
|
267
272
|
const bestValue = item?.highlighted || false;
|
|
268
273
|
const rawBadge = item?.giftRewards?.list?.items || [];
|
|
@@ -308,6 +313,7 @@ function ProductCardPanel({
|
|
|
308
313
|
techType={item?.techType || ""}
|
|
309
314
|
ismaxSpeed={item?.ismaxSpeed || ""}
|
|
310
315
|
price={formattedPrice}
|
|
316
|
+
priceUnit={priceUnit}
|
|
311
317
|
description={item?.productCardDescription || ""}
|
|
312
318
|
giftBadge={rawBadge}
|
|
313
319
|
innerBadge={innerBadge}
|
|
@@ -75,7 +75,7 @@ export const Carousel: React.FC<CarouselProps> = ({
|
|
|
75
75
|
{disclaimerText && (
|
|
76
76
|
<Text
|
|
77
77
|
as="div"
|
|
78
|
-
className="
|
|
78
|
+
className="body3 mt-8 px-4 text-center text-text-secondary"
|
|
79
79
|
>
|
|
80
80
|
{disclaimerText}
|
|
81
81
|
</Text>
|
|
@@ -119,6 +119,102 @@ describe("DynamicForm", () => {
|
|
|
119
119
|
const submit = () =>
|
|
120
120
|
fireEvent.click(screen.getByRole("button", { name: /submit form/i }));
|
|
121
121
|
|
|
122
|
+
describe("non-prod email rerouting", () => {
|
|
123
|
+
const fillAndSubmit = () => {
|
|
124
|
+
fireEvent.change(screen.getByLabelText("First Name"), {
|
|
125
|
+
target: { value: "Jane", name: "firstName" },
|
|
126
|
+
});
|
|
127
|
+
fireEvent.click(screen.getByRole("checkbox"));
|
|
128
|
+
fireEvent.click(screen.getByRole("button", { name: /submit form/i }));
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
it("reroutes email to multiple comma-separated test recipients when isProduction is false", async () => {
|
|
132
|
+
const onEmailSubmit = jest.fn().mockResolvedValue(undefined);
|
|
133
|
+
render(
|
|
134
|
+
<DynamicForm
|
|
135
|
+
fields={fields}
|
|
136
|
+
formSubmitOptions={["email"]}
|
|
137
|
+
emailTemplateName="welcome"
|
|
138
|
+
toEmail="real@customer.com"
|
|
139
|
+
testEmail=" a@test.com , b@test.com "
|
|
140
|
+
isProduction={false}
|
|
141
|
+
onEmailSubmit={onEmailSubmit}
|
|
142
|
+
successMessage={<div>Done</div>}
|
|
143
|
+
/>
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
fillAndSubmit();
|
|
147
|
+
|
|
148
|
+
await waitFor(() => expect(onEmailSubmit).toHaveBeenCalledTimes(1));
|
|
149
|
+
const body = onEmailSubmit.mock.calls[0][0];
|
|
150
|
+
expect(body.toEmail).toBe("a@test.com,b@test.com");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("skips the email action when isProduction is false and testEmail is empty", async () => {
|
|
154
|
+
const onEmailSubmit = jest.fn().mockResolvedValue(undefined);
|
|
155
|
+
render(
|
|
156
|
+
<DynamicForm
|
|
157
|
+
fields={fields}
|
|
158
|
+
formSubmitOptions={["email"]}
|
|
159
|
+
emailTemplateName="welcome"
|
|
160
|
+
toEmail="real@customer.com"
|
|
161
|
+
testEmail=""
|
|
162
|
+
isProduction={false}
|
|
163
|
+
onEmailSubmit={onEmailSubmit}
|
|
164
|
+
successMessage={<div>Done</div>}
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
fillAndSubmit();
|
|
169
|
+
|
|
170
|
+
await waitFor(() => expect(screen.getByText("Done")).toBeInTheDocument());
|
|
171
|
+
expect(onEmailSubmit).not.toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("does not reroute when isProduction is not set (preserves prior behavior)", async () => {
|
|
175
|
+
const onEmailSubmit = jest.fn().mockResolvedValue(undefined);
|
|
176
|
+
render(
|
|
177
|
+
<DynamicForm
|
|
178
|
+
fields={fields}
|
|
179
|
+
formSubmitOptions={["email"]}
|
|
180
|
+
emailTemplateName="welcome"
|
|
181
|
+
toEmail="real@customer.com"
|
|
182
|
+
testEmail="test@test.com"
|
|
183
|
+
onEmailSubmit={onEmailSubmit}
|
|
184
|
+
successMessage={<div>Done</div>}
|
|
185
|
+
/>
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
fillAndSubmit();
|
|
189
|
+
|
|
190
|
+
await waitFor(() => expect(onEmailSubmit).toHaveBeenCalledTimes(1));
|
|
191
|
+
const body = onEmailSubmit.mock.calls[0][0];
|
|
192
|
+
expect(body.toEmail).toBe("real@customer.com");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("does not reroute when isProduction is true", async () => {
|
|
196
|
+
const onEmailSubmit = jest.fn().mockResolvedValue(undefined);
|
|
197
|
+
render(
|
|
198
|
+
<DynamicForm
|
|
199
|
+
fields={fields}
|
|
200
|
+
formSubmitOptions={["email"]}
|
|
201
|
+
emailTemplateName="welcome"
|
|
202
|
+
toEmail="real@customer.com"
|
|
203
|
+
testEmail="test@test.com"
|
|
204
|
+
isProduction={true}
|
|
205
|
+
onEmailSubmit={onEmailSubmit}
|
|
206
|
+
successMessage={<div>Done</div>}
|
|
207
|
+
/>
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
fillAndSubmit();
|
|
211
|
+
|
|
212
|
+
await waitFor(() => expect(onEmailSubmit).toHaveBeenCalledTimes(1));
|
|
213
|
+
const body = onEmailSubmit.mock.calls[0][0];
|
|
214
|
+
expect(body.toEmail).toBe("real@customer.com");
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
122
218
|
it("applies TN/IP preset validation before submitting", async () => {
|
|
123
219
|
const onEmailSubmit = jest.fn().mockResolvedValue(undefined);
|
|
124
220
|
const presetFields: DynamicFormItem[] = [
|
|
@@ -108,6 +108,8 @@ export const DynamicForm: React.FC<DynamicFormProps> = props => {
|
|
|
108
108
|
emailSubject,
|
|
109
109
|
emailTemplateName,
|
|
110
110
|
toEmail,
|
|
111
|
+
testEmail,
|
|
112
|
+
isProduction,
|
|
111
113
|
segmentEventId,
|
|
112
114
|
onEmailSubmit,
|
|
113
115
|
onSmtpSubmit,
|
|
@@ -380,7 +382,28 @@ export const DynamicForm: React.FC<DynamicFormProps> = props => {
|
|
|
380
382
|
|
|
381
383
|
onTrack?.(segmentEventId, payload);
|
|
382
384
|
|
|
383
|
-
|
|
385
|
+
// Non-prod safety: when the app flags a non-production environment,
|
|
386
|
+
// reroute the `email` action to the configured test recipient(s) — which
|
|
387
|
+
// may be multiple comma-separated addresses — or skip `email` entirely
|
|
388
|
+
// when no test recipient is set. Gated on an explicit `isProduction ===
|
|
389
|
+
// false` so prod/undefined behavior is unchanged.
|
|
390
|
+
let effectiveActions = payload.formSubmitOptions || [];
|
|
391
|
+
if (isProduction === false && effectiveActions.includes("email")) {
|
|
392
|
+
const normalizedTestEmail = (testEmail || "")
|
|
393
|
+
.split(",")
|
|
394
|
+
.map(email => email.trim())
|
|
395
|
+
.filter(Boolean)
|
|
396
|
+
.join(",");
|
|
397
|
+
if (normalizedTestEmail) {
|
|
398
|
+
payload.toEmail = normalizedTestEmail;
|
|
399
|
+
} else {
|
|
400
|
+
effectiveActions = effectiveActions.filter(
|
|
401
|
+
action => action !== "email"
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const actions = effectiveActions;
|
|
384
407
|
const hasPost = actions.includes("post");
|
|
385
408
|
const serverActions = actions.filter(a => a !== "post");
|
|
386
409
|
|
|
@@ -57,6 +57,31 @@ describe("dynamicRenderingWithGroupings", () => {
|
|
|
57
57
|
expect(screen.getByText("Custom Content")).toBeInTheDocument();
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
it("renders a full-width divider by default", () => {
|
|
61
|
+
const { container } = renderGroups([{ itemType: "divider" }]);
|
|
62
|
+
const divider = container.querySelector<HTMLElement>('[role="separator"]');
|
|
63
|
+
expect(divider).toBeInTheDocument();
|
|
64
|
+
expect(divider).toHaveClass("w-full");
|
|
65
|
+
expect(divider?.style.height).toBe("1px");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("renders a half-width divider with a custom thickness", () => {
|
|
69
|
+
const { container } = renderGroups([
|
|
70
|
+
{ itemType: "divider", dividerLength: false, dividerThickness: 4 },
|
|
71
|
+
]);
|
|
72
|
+
const divider = container.querySelector<HTMLElement>('[role="separator"]');
|
|
73
|
+
expect(divider).toHaveClass("w-1/2");
|
|
74
|
+
expect(divider?.style.height).toBe("4px");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("clamps a divider thickness below 1 to 1px", () => {
|
|
78
|
+
const { container } = renderGroups([
|
|
79
|
+
{ itemType: "divider", dividerThickness: 0 },
|
|
80
|
+
]);
|
|
81
|
+
const divider = container.querySelector<HTMLElement>('[role="separator"]');
|
|
82
|
+
expect(divider?.style.height).toBe("1px");
|
|
83
|
+
});
|
|
84
|
+
|
|
60
85
|
it("renders a conditional item", () => {
|
|
61
86
|
renderGroups([
|
|
62
87
|
{
|
|
@@ -7,6 +7,7 @@ import FormFieldElement from "./form-field";
|
|
|
7
7
|
import { DynamicFormItem } from "./types";
|
|
8
8
|
import { groupFieldsForLayout } from "./utils";
|
|
9
9
|
|
|
10
|
+
import { Divider } from "@shared/components/divider";
|
|
10
11
|
import { Text } from "@shared/components/text";
|
|
11
12
|
|
|
12
13
|
type FormValues = Record<string, any>;
|
|
@@ -46,6 +47,20 @@ const renderItem = (
|
|
|
46
47
|
/>
|
|
47
48
|
);
|
|
48
49
|
}
|
|
50
|
+
if (item.itemType === "divider") {
|
|
51
|
+
const strokeWidth = Math.max(item.dividerThickness || 1, 1);
|
|
52
|
+
const widthClass = item.dividerLength === false ? "w-1/2" : "w-full";
|
|
53
|
+
return (
|
|
54
|
+
<div key={key} className="flex w-full justify-center">
|
|
55
|
+
<Divider
|
|
56
|
+
role="separator"
|
|
57
|
+
aria-label="Content divider"
|
|
58
|
+
className={widthClass}
|
|
59
|
+
style={{ height: `${strokeWidth}px` }}
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
49
64
|
return <React.Fragment key={key}>{item.content}</React.Fragment>;
|
|
50
65
|
};
|
|
51
66
|
|
|
@@ -69,14 +69,19 @@ export interface ConditionalFieldConfig {
|
|
|
69
69
|
grouped?: boolean;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
export interface DynamicFormDividerConfig {
|
|
73
|
+
dividerLength?: boolean;
|
|
74
|
+
dividerThickness?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
72
77
|
/**
|
|
73
|
-
* A flat list item rendered by the form
|
|
74
|
-
*
|
|
75
|
-
* the app rendered itself and passes through).
|
|
78
|
+
* A flat list item rendered by the form: a form field, conditional field,
|
|
79
|
+
* divider, or custom React node.
|
|
76
80
|
*/
|
|
77
81
|
export type DynamicFormItem =
|
|
78
82
|
| ({ itemType: "formField" } & DynamicFormFieldConfig)
|
|
79
83
|
| ({ itemType: "conditional" } & ConditionalFieldConfig)
|
|
84
|
+
| ({ itemType: "divider"; fieldId?: string } & DynamicFormDividerConfig)
|
|
80
85
|
| { itemType: "custom"; fieldId?: string; content: React.ReactNode };
|
|
81
86
|
|
|
82
87
|
export interface DynamicFormPayload {
|
|
@@ -175,6 +180,15 @@ export interface DynamicFormProps {
|
|
|
175
180
|
emailSubject?: string;
|
|
176
181
|
emailTemplateName?: string;
|
|
177
182
|
toEmail?: string;
|
|
183
|
+
/**
|
|
184
|
+
* Test recipient(s) for the `email` action in non-production environments.
|
|
185
|
+
* When `isProduction === false` and the `email` action runs, the payload's
|
|
186
|
+
* `toEmail` is rerouted here so real recipients never receive non-prod
|
|
187
|
+
* submissions. Supports multiple comma-separated addresses (each is trimmed);
|
|
188
|
+
* when empty, the `email` action is skipped. Mirrors the residential form's
|
|
189
|
+
* `testEmail`.
|
|
190
|
+
*/
|
|
191
|
+
testEmail?: string;
|
|
178
192
|
segmentEventId?: string;
|
|
179
193
|
|
|
180
194
|
/** Injected per-action async submit handlers. */
|
|
@@ -207,8 +221,10 @@ export interface DynamicFormProps {
|
|
|
207
221
|
metadata?: Record<string, any>;
|
|
208
222
|
}) => void;
|
|
209
223
|
|
|
210
|
-
/** Whether the current environment is production
|
|
211
|
-
*
|
|
224
|
+
/** Whether the current environment is production. When explicitly `false`,
|
|
225
|
+
* the `email` action is rerouted to `testEmail` (or skipped when `testEmail`
|
|
226
|
+
* is empty) so non-prod submissions never reach real recipients. Leaving it
|
|
227
|
+
* `undefined` preserves the prior behavior (no rerouting). */
|
|
212
228
|
isProduction?: boolean;
|
|
213
229
|
|
|
214
230
|
/** Field customization + async data. */
|
|
@@ -130,7 +130,10 @@ export const Navigation: React.FC<NavigationProps> = props => {
|
|
|
130
130
|
)}
|
|
131
131
|
>
|
|
132
132
|
<div className="mx-auto flex max-w-120 justify-between">
|
|
133
|
-
<ul
|
|
133
|
+
<ul
|
|
134
|
+
className="flex min-h-11 gap-5"
|
|
135
|
+
aria-label="Utility Navigation"
|
|
136
|
+
>
|
|
134
137
|
{utilityNavigationLinks?.map((links, index) => {
|
|
135
138
|
return (
|
|
136
139
|
<li key={`main-menu-items-${index}`}>
|
|
@@ -4,8 +4,8 @@ import { PrimaryHero } from "./index";
|
|
|
4
4
|
import { fireEvent, render, screen } from "@testing-library/react";
|
|
5
5
|
|
|
6
6
|
jest.mock("@shared/components/checklist", () => ({
|
|
7
|
-
Checklist: ({ items }: any) => (
|
|
8
|
-
<ul data-testid="checklist">
|
|
7
|
+
Checklist: ({ items, iconSize }: any) => (
|
|
8
|
+
<ul data-testid="checklist" data-icon-size={iconSize}>
|
|
9
9
|
{items?.map((i: string, idx: number) => (
|
|
10
10
|
<li key={idx}>{i}</li>
|
|
11
11
|
))}
|
|
@@ -128,6 +128,15 @@ describe("PrimaryHero", () => {
|
|
|
128
128
|
expect(screen.getByText("Free install")).toBeInTheDocument();
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
+
it("passes iconSize of 24 to the checklist", () => {
|
|
132
|
+
const checklist = [{ title: "No data caps" }, { title: "Free install" }];
|
|
133
|
+
render(<PrimaryHero {...baseProps} checklist={checklist} />);
|
|
134
|
+
expect(screen.getByTestId("checklist")).toHaveAttribute(
|
|
135
|
+
"data-icon-size",
|
|
136
|
+
"24"
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
131
140
|
it("does not render checklist when not provided", () => {
|
|
132
141
|
render(<PrimaryHero {...baseProps} />);
|
|
133
142
|
expect(screen.queryByTestId("checklist")).not.toBeInTheDocument();
|
|
@@ -171,6 +171,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
|
|
|
171
171
|
{/* checklist */}
|
|
172
172
|
{checklist && (
|
|
173
173
|
<Checklist
|
|
174
|
+
iconSize={24}
|
|
174
175
|
listItemClassName={`body2 text-left ${textColorClass}`}
|
|
175
176
|
items={checklist.map(item =>
|
|
176
177
|
item.iconUrl
|
|
@@ -230,7 +231,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
|
|
|
230
231
|
{...primaryCta1}
|
|
231
232
|
renderCheckPlans={renderCheckPlans}
|
|
232
233
|
onModalButtonClick={onModalButtonClick}
|
|
233
|
-
fullWidth={true}
|
|
234
|
+
// fullWidth={true}
|
|
234
235
|
buttonClassName="lg:w-auto text-center"
|
|
235
236
|
/>
|
|
236
237
|
</div>
|
|
@@ -243,7 +244,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
|
|
|
243
244
|
{...primaryCta2}
|
|
244
245
|
renderCheckPlans={renderCheckPlans}
|
|
245
246
|
onModalButtonClick={onModalButtonClick}
|
|
246
|
-
fullWidth={true}
|
|
247
|
+
// fullWidth={true}
|
|
247
248
|
buttonClassName="lg:w-auto text-center"
|
|
248
249
|
/>
|
|
249
250
|
</div>
|