@windstream/react-shared-components 0.2.92 → 0.2.93
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 -6
- 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 +4 -1
- package/dist/index.d.ts +6 -3
- 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/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 +7 -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/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
|
@@ -5,23 +5,42 @@ import { Button } from "../button";
|
|
|
5
5
|
import { EmailInputBlockProps } from "./types";
|
|
6
6
|
|
|
7
7
|
const LogoCream = ({ fill = "#FFFEEF" }: { fill?: string }) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
8
|
+
const clipPathId = `logo-cream-clip-${React.useId()}`;
|
|
9
|
+
return (
|
|
10
|
+
<svg
|
|
11
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
12
|
+
width="100%"
|
|
13
|
+
height="100%"
|
|
14
|
+
fill="none"
|
|
15
|
+
viewBox="0 0 100 99"
|
|
16
|
+
preserveAspectRatio="xMidYMid meet"
|
|
17
|
+
>
|
|
18
|
+
<g fill={fill} clipPath={`url(#${clipPathId})`}>
|
|
19
|
+
<path
|
|
20
|
+
d="M23.877 73.439c2.829 9.044 7.288 13.706 25.03 18.07 16.735 4.111 43.31 12.356 48.798 3.65 3.454-5.095 1.534-12.395.415-18.244-.875-3.853-1.756-7.3-2.534-11.074-.255-1.237-2.034-1.22-2.278.022-1.284 6.552-3.528 11.26-8.55 14.482-8.572 6.125-28.7 2.295-39.453-2.272-11.487-4.656-14.696-13.509-15.486-23.126-.296-5.157.437-10.123 1.818-14.392.409-1.254-1.273-2.097-2.045-1.023-6.113 8.464-8.073 23.88-5.71 33.913"
|
|
21
|
+
opacity=".45"
|
|
22
|
+
/>
|
|
23
|
+
<path
|
|
24
|
+
d="M74.18 75.362c9.135-2.8 13.845-7.215 18.253-24.779 4.153-16.569 12.481-42.878 3.687-48.31-5.147-3.42-12.52-1.52-18.429-.411-3.89.866-7.373 1.738-11.185 2.508-1.25.253-1.233 2.014.023 2.255 6.618 1.271 11.373 3.493 14.628 8.465 6.186 8.486 2.318 28.412-2.295 39.059-4.704 11.371-13.646 14.549-23.36 15.33-5.21.293-10.226-.432-14.537-1.799-1.267-.405-2.12 1.26-1.034 2.025 8.55 6.051 24.12 7.992 34.255 5.652"
|
|
25
|
+
opacity=".45"
|
|
26
|
+
/>
|
|
27
|
+
<path
|
|
28
|
+
d="M76.124 25.562c-2.83-9.044-7.289-13.706-25.03-18.07C34.358 3.38 7.783-4.865 2.3 3.842-1.153 8.937.767 16.237 1.886 22.086c.875 3.852 1.756 7.3 2.534 11.074.256 1.237 2.034 1.22 2.278-.023 1.284-6.552 3.528-11.26 8.55-14.482 8.572-6.124 28.7-2.294 39.453 2.272 11.487 4.657 14.696 13.51 15.486 23.126.296 5.158-.437 10.124-1.818 14.392-.409 1.255 1.273 2.098 2.045 1.024 6.113-8.464 8.073-23.88 5.71-33.913"
|
|
29
|
+
opacity=".45"
|
|
30
|
+
/>
|
|
31
|
+
<path
|
|
32
|
+
d="M25.82 23.638c-9.135 2.8-13.845 7.216-18.253 24.78-4.153 16.568-12.48 42.877-3.687 48.31 5.147 3.42 12.52 1.519 18.429.41 3.891-.865 7.374-1.737 11.186-2.508 1.25-.253 1.232-2.013-.023-2.255-6.619-1.27-11.373-3.492-14.629-8.464-6.186-8.487-2.317-28.413 2.296-39.059 4.703-11.372 13.645-14.55 23.36-15.331 5.209-.293 10.225.433 14.537 1.8 1.266.404 2.118-1.26 1.033-2.025-8.55-6.052-24.12-7.992-34.255-5.652"
|
|
33
|
+
opacity=".45"
|
|
34
|
+
/>
|
|
35
|
+
</g>
|
|
36
|
+
<defs>
|
|
37
|
+
<clipPath id={clipPathId}>
|
|
38
|
+
<path fill="#fff" d="M0 0h100v99H0z" />
|
|
39
|
+
</clipPath>
|
|
40
|
+
</defs>
|
|
41
|
+
</svg>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
25
44
|
|
|
26
45
|
export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
27
46
|
const {
|
|
@@ -41,6 +60,7 @@ export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
|
41
60
|
padding = "large",
|
|
42
61
|
requiredErrorMessage = "Email is required",
|
|
43
62
|
validationErrorMessage = "Invalid email address",
|
|
63
|
+
fieldProps,
|
|
44
64
|
} = props || {};
|
|
45
65
|
|
|
46
66
|
const buttonLabel = ctaButtonText || ctaText || "Sign me up";
|
|
@@ -61,12 +81,18 @@ export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
|
61
81
|
|
|
62
82
|
const normalizedBg =
|
|
63
83
|
((backgroundColor as string) || "").trim().toLowerCase() || "white";
|
|
64
|
-
const outerBgClass =
|
|
65
|
-
|
|
84
|
+
const outerBgClass =
|
|
85
|
+
backgroundColorClasses[
|
|
86
|
+
normalizedBg as keyof typeof backgroundColorClasses
|
|
87
|
+
] || backgroundColorClasses.white;
|
|
88
|
+
const captionTextColorClass =
|
|
89
|
+
normalizedBg === "white" ? "text-text" : "text-white";
|
|
66
90
|
|
|
67
91
|
const normalizedTheme =
|
|
68
|
-
|
|
69
|
-
const containerBgClass =
|
|
92
|
+
((themeColor as string) || "").trim().toLowerCase() || "green";
|
|
93
|
+
const containerBgClass =
|
|
94
|
+
themeColorClasses[normalizedTheme as keyof typeof themeColorClasses] ||
|
|
95
|
+
themeColorClasses.green;
|
|
70
96
|
|
|
71
97
|
const darkThemeColors = ["green", "blue", "purple"];
|
|
72
98
|
const isDarkTheme = darkThemeColors.includes(normalizedTheme);
|
|
@@ -74,7 +100,8 @@ export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
|
74
100
|
const errorTextColor = isDarkTheme ? "text-text-inverse" : "text-text";
|
|
75
101
|
const pinwheelFill = normalizedTheme === "white" ? "#E0E0E0" : "#FFFEEF";
|
|
76
102
|
|
|
77
|
-
const paddingClass =
|
|
103
|
+
const paddingClass =
|
|
104
|
+
padding === "none" ? "!py-0 md:!py-0" : "!py-8 md:!py-20";
|
|
78
105
|
|
|
79
106
|
const [email, setEmail] = useState("");
|
|
80
107
|
const [formSubmitted, setFormSubmitted] = useState(false);
|
|
@@ -99,28 +126,45 @@ export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
|
99
126
|
};
|
|
100
127
|
|
|
101
128
|
return (
|
|
102
|
-
<section
|
|
129
|
+
<section
|
|
130
|
+
id={anchorId}
|
|
131
|
+
className={`w-full px-4 ${paddingClass} md:px-10 ${outerBgClass}`}
|
|
132
|
+
>
|
|
103
133
|
<div
|
|
104
134
|
className={`mx-auto max-w-[1200px] overflow-hidden !rounded-[20px] md:rounded-[20px] flex flex-col items-start !px-5 !pt-6 !pb-10 md:!p-10 md:!pt-12 min-h-[344px] md:!w-auto md:min-h-[367px] relative ${containerBgClass}`}
|
|
105
135
|
>
|
|
106
136
|
{showPinWheel && (
|
|
107
|
-
<div
|
|
137
|
+
<div
|
|
138
|
+
className="absolute right-[-50px] top-[-30px] hidden h-[420px] w-[420px] pointer-events-none lg:block"
|
|
139
|
+
aria-hidden="true"
|
|
140
|
+
>
|
|
108
141
|
<LogoCream fill={pinwheelFill} />
|
|
109
142
|
</div>
|
|
110
143
|
)}
|
|
111
144
|
<div className="w-full">
|
|
112
145
|
<div className="lg:w-[548px]">
|
|
113
146
|
{title && (
|
|
114
|
-
<h2
|
|
147
|
+
<h2
|
|
148
|
+
className={`mb-3 text-[2.25rem] leading-[2.5rem] font-black md:text-heading2 md:font-black ${textColor}`}
|
|
149
|
+
{...fieldProps?.title}
|
|
150
|
+
>
|
|
151
|
+
{title}
|
|
152
|
+
</h2>
|
|
115
153
|
)}
|
|
116
154
|
{subTitle && (
|
|
117
|
-
<h3
|
|
155
|
+
<h3
|
|
156
|
+
className={`subheading3 mb-3 font-black ${textColor}`}
|
|
157
|
+
{...fieldProps?.subTitle}
|
|
158
|
+
>
|
|
118
159
|
{subTitle}
|
|
119
160
|
</h3>
|
|
120
161
|
)}
|
|
121
162
|
|
|
122
163
|
{emailInputDescription && (
|
|
123
|
-
<p
|
|
164
|
+
<p
|
|
165
|
+
className={`!mb-4 md:!mb-16 text-xl leading-7 font-medium md:text-subheading1 md:!leading-8 md:!font-bold opacity-95 ${textColor}`}
|
|
166
|
+
{...fieldProps?.emailInputDescription}
|
|
167
|
+
>
|
|
124
168
|
{emailInputDescription}
|
|
125
169
|
</p>
|
|
126
170
|
)}
|
|
@@ -145,6 +189,7 @@ export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
|
145
189
|
}
|
|
146
190
|
}}
|
|
147
191
|
className="w-full flex-grow bg-transparent !pl-4 pr-6 py-3 text-[1rem] leading-none md:text-base font-normal text-text outline-none placeholder:input-text-placeholder md:py-4"
|
|
192
|
+
{...fieldProps?.email}
|
|
148
193
|
/>
|
|
149
194
|
</div>
|
|
150
195
|
<Button
|
|
@@ -152,25 +197,37 @@ export const EmailInputBlock: React.FC<EmailInputBlockProps> = props => {
|
|
|
152
197
|
buttonVariant="primary_inverse"
|
|
153
198
|
buttonClassName="capitalize-first whitespace-nowrap rounded-2xl px-8 !text-lg mt-3 w-full sm:mt-0 sm:w-auto sm:absolute sm:right-[8px] sm:top-[calc(50%-28px)]"
|
|
154
199
|
onClick={handleOnSubmit}
|
|
200
|
+
fieldProps={fieldProps?.cta}
|
|
155
201
|
/>
|
|
156
202
|
</div>
|
|
157
203
|
{emailError && (
|
|
158
|
-
<p
|
|
204
|
+
<p
|
|
205
|
+
className={`mt-2 text-label4 font-semibold ${errorTextColor}`}
|
|
206
|
+
{...fieldProps?.errorMessage}
|
|
207
|
+
>
|
|
159
208
|
{emailError}
|
|
160
209
|
</p>
|
|
161
210
|
)}
|
|
162
211
|
{successFeedback && formSubmitted && !emailError && (
|
|
163
|
-
<p
|
|
212
|
+
<p
|
|
213
|
+
className={`mt-2 text-label4 font-semibold ${textColor}`}
|
|
214
|
+
{...fieldProps?.successFeedback}
|
|
215
|
+
>
|
|
164
216
|
{successFeedback}
|
|
165
217
|
</p>
|
|
166
218
|
)}
|
|
167
219
|
</div>
|
|
168
220
|
</div>
|
|
169
221
|
{caption && (
|
|
170
|
-
<div
|
|
222
|
+
<div
|
|
223
|
+
className={`mx-auto max-w-[1200px] px-6 md:px-8 ${captionTextColorClass}`}
|
|
224
|
+
{...fieldProps?.caption}
|
|
225
|
+
>
|
|
226
|
+
{caption}
|
|
227
|
+
</div>
|
|
171
228
|
)}
|
|
172
229
|
</section>
|
|
173
230
|
);
|
|
174
231
|
};
|
|
175
232
|
|
|
176
|
-
export default EmailInputBlock;
|
|
233
|
+
export default EmailInputBlock;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
+
import type { FieldProps } from "@shared/types/field-props";
|
|
4
|
+
|
|
3
5
|
export interface EmailInputBlockProps {
|
|
4
6
|
anchorId?: string;
|
|
5
7
|
title?: string;
|
|
@@ -10,13 +12,24 @@ export interface EmailInputBlockProps {
|
|
|
10
12
|
ctaButtonText?: string;
|
|
11
13
|
ctaText?: string;
|
|
12
14
|
caption?: React.ReactNode;
|
|
13
|
-
backgroundColor?: "green" | "white" | "yellow" | "blue" | "navy" | "gray90";
|
|
15
|
+
backgroundColor?: "green" | "white" | "yellow" | "blue" | "navy" | "gray90";
|
|
14
16
|
themeColor?: "blue" | "green" | "orange" | "purple" | "white";
|
|
15
17
|
showPinWheel?: boolean;
|
|
16
18
|
padding?: "none" | "large";
|
|
17
19
|
validationErrorMessage?: string;
|
|
18
20
|
requiredErrorMessage?: string;
|
|
19
21
|
onSubmit: (values: EmailInputFormValues) => void;
|
|
22
|
+
/** Inspector attributes for rendered content and email form fields. */
|
|
23
|
+
fieldProps?: FieldProps<
|
|
24
|
+
| "title"
|
|
25
|
+
| "subTitle"
|
|
26
|
+
| "emailInputDescription"
|
|
27
|
+
| "email"
|
|
28
|
+
| "cta"
|
|
29
|
+
| "errorMessage"
|
|
30
|
+
| "successFeedback"
|
|
31
|
+
| "caption"
|
|
32
|
+
>;
|
|
20
33
|
}
|
|
21
34
|
|
|
22
35
|
export interface EmailInputFormValues {
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
-
import * as Yup from "yup";
|
|
3
1
|
import { FiberForm } from ".";
|
|
4
2
|
import type { FiberFormValues } from "./types";
|
|
3
|
+
import * as Yup from "yup";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
act,
|
|
7
|
+
fireEvent,
|
|
8
|
+
render,
|
|
9
|
+
screen,
|
|
10
|
+
waitFor,
|
|
11
|
+
} from "@testing-library/react";
|
|
5
12
|
|
|
6
13
|
jest.mock("@vimeo/player", () => jest.fn(), { virtual: true });
|
|
7
14
|
|
|
@@ -47,6 +54,37 @@ const renderForm = (onSubmit: (values: FiberFormValues) => Promise<void>) =>
|
|
|
47
54
|
);
|
|
48
55
|
|
|
49
56
|
describe("FiberForm submit loading state", () => {
|
|
57
|
+
it("tags configured fields and the submit CTA", () => {
|
|
58
|
+
const { container } = render(
|
|
59
|
+
<FiberForm
|
|
60
|
+
AddressAutocomplete={AddressAutocomplete}
|
|
61
|
+
initialValues={initialValues}
|
|
62
|
+
inputList={{
|
|
63
|
+
firstName: {},
|
|
64
|
+
lastName: {},
|
|
65
|
+
phoneNumber: {},
|
|
66
|
+
emailAddress: {},
|
|
67
|
+
}}
|
|
68
|
+
formSchema={Yup.object({})}
|
|
69
|
+
onSubmit={async () => Promise.resolve()}
|
|
70
|
+
fieldProps={{
|
|
71
|
+
firstName: { "data-contentful-field-id": "firstName" },
|
|
72
|
+
lastName: { "data-contentful-field-id": "lastName" },
|
|
73
|
+
phoneNumber: { "data-contentful-field-id": "phoneNumber" },
|
|
74
|
+
emailAddress: { "data-contentful-field-id": "emailAddress" },
|
|
75
|
+
submit: { "data-contentful-field-id": "submit" },
|
|
76
|
+
}}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
["firstName", "lastName", "phoneNumber", "emailAddress", "submit"].forEach(
|
|
80
|
+
field => {
|
|
81
|
+
expect(
|
|
82
|
+
container.querySelector(`[data-contentful-field-id="${field}"]`)
|
|
83
|
+
).toBeInTheDocument();
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
50
88
|
it("shows a spinner and disables submit until submission resolves", async () => {
|
|
51
89
|
const submission = deferred();
|
|
52
90
|
const onSubmit = jest.fn(() => submission.promise);
|
|
@@ -67,6 +67,7 @@ export const FiberForm: React.FC<FiberFormProps> = ({
|
|
|
67
67
|
copyRightLinks,
|
|
68
68
|
className,
|
|
69
69
|
onAddressEvent,
|
|
70
|
+
fieldProps,
|
|
70
71
|
}) => {
|
|
71
72
|
const FormInput = InputComponent ?? Input;
|
|
72
73
|
const [candidates, setCandidates] = useState<FiberAddressCandidate[]>([]);
|
|
@@ -164,7 +165,7 @@ export const FiberForm: React.FC<FiberFormProps> = ({
|
|
|
164
165
|
<Form className={cx("fiber-form w-full", className)}>
|
|
165
166
|
<div className="flex flex-col gap-5 sm:flex-row">
|
|
166
167
|
{inputList.firstName && (
|
|
167
|
-
<div className="flex-1">
|
|
168
|
+
<div className="flex-1" {...fieldProps?.firstName}>
|
|
168
169
|
<FieldLabel required>First name</FieldLabel>
|
|
169
170
|
<Field
|
|
170
171
|
as={FormInput}
|
|
@@ -180,7 +181,7 @@ export const FiberForm: React.FC<FiberFormProps> = ({
|
|
|
180
181
|
</div>
|
|
181
182
|
)}
|
|
182
183
|
{inputList.lastName && (
|
|
183
|
-
<div className="flex-1">
|
|
184
|
+
<div className="flex-1" {...fieldProps?.lastName}>
|
|
184
185
|
<FieldLabel required>Last name</FieldLabel>
|
|
185
186
|
<Field
|
|
186
187
|
as={FormInput}
|
|
@@ -199,7 +200,7 @@ export const FiberForm: React.FC<FiberFormProps> = ({
|
|
|
199
200
|
|
|
200
201
|
{inputList.address && (
|
|
201
202
|
<div className="mt-8 flex flex-col gap-5 sm:flex-row">
|
|
202
|
-
<div className="sm:flex-[3]">
|
|
203
|
+
<div className="sm:flex-[3]" {...fieldProps?.address}>
|
|
203
204
|
<FieldLabel required>Address</FieldLabel>
|
|
204
205
|
<Field name="address">
|
|
205
206
|
{({ field }: FieldProps) => (
|
|
@@ -285,7 +286,10 @@ export const FiberForm: React.FC<FiberFormProps> = ({
|
|
|
285
286
|
</ul>
|
|
286
287
|
)}
|
|
287
288
|
</div>
|
|
288
|
-
<div
|
|
289
|
+
<div
|
|
290
|
+
className="w-full sm:w-[216px] sm:flex-none"
|
|
291
|
+
{...fieldProps?.unit}
|
|
292
|
+
>
|
|
289
293
|
<div className="mb-1 hidden h-[20px] sm:block" />
|
|
290
294
|
<Select
|
|
291
295
|
options={unitOptions}
|
|
@@ -401,7 +405,7 @@ export const FiberForm: React.FC<FiberFormProps> = ({
|
|
|
401
405
|
|
|
402
406
|
<div className="mt-8 flex flex-col gap-5 sm:flex-row">
|
|
403
407
|
{inputList.phoneNumber && (
|
|
404
|
-
<div className="flex-1">
|
|
408
|
+
<div className="flex-1" {...fieldProps?.phoneNumber}>
|
|
405
409
|
<FieldLabel required>Phone number</FieldLabel>
|
|
406
410
|
<Field name="phone">
|
|
407
411
|
{({ field, form }: FieldProps) => (
|
|
@@ -423,7 +427,7 @@ export const FiberForm: React.FC<FiberFormProps> = ({
|
|
|
423
427
|
</div>
|
|
424
428
|
)}
|
|
425
429
|
{inputList.emailAddress && (
|
|
426
|
-
<div className="flex-1">
|
|
430
|
+
<div className="flex-1" {...fieldProps?.emailAddress}>
|
|
427
431
|
<FieldLabel required>Email address</FieldLabel>
|
|
428
432
|
<Field
|
|
429
433
|
as={FormInput}
|
|
@@ -439,7 +443,7 @@ export const FiberForm: React.FC<FiberFormProps> = ({
|
|
|
439
443
|
|
|
440
444
|
<div className="mt-8 flex flex-col gap-5 sm:flex-row">
|
|
441
445
|
{inputList.referralCode && (
|
|
442
|
-
<div className="flex-1">
|
|
446
|
+
<div className="flex-1" {...fieldProps?.referralCode}>
|
|
443
447
|
<FieldLabel>Referral code</FieldLabel>
|
|
444
448
|
<Field
|
|
445
449
|
as={FormInput}
|
|
@@ -455,7 +459,7 @@ export const FiberForm: React.FC<FiberFormProps> = ({
|
|
|
455
459
|
</div>
|
|
456
460
|
)}
|
|
457
461
|
{inputList.notes && (
|
|
458
|
-
<div className="flex-1">
|
|
462
|
+
<div className="flex-1" {...fieldProps?.notes}>
|
|
459
463
|
<FieldLabel>Notes or comments</FieldLabel>
|
|
460
464
|
<Field
|
|
461
465
|
as={FormInput}
|
|
@@ -468,7 +472,10 @@ export const FiberForm: React.FC<FiberFormProps> = ({
|
|
|
468
472
|
</div>
|
|
469
473
|
|
|
470
474
|
{checkBoxEnabled && !hideLegalCopy && (
|
|
471
|
-
<div
|
|
475
|
+
<div
|
|
476
|
+
className="mt-16 text-neutral-600"
|
|
477
|
+
{...fieldProps?.isMarketingCom}
|
|
478
|
+
>
|
|
472
479
|
<Field name="isMarketingCom">
|
|
473
480
|
{({ field }: FieldProps) => (
|
|
474
481
|
<div className="mb-7 mt-4 flex items-center gap-1 pl-1">
|
|
@@ -503,7 +510,10 @@ export const FiberForm: React.FC<FiberFormProps> = ({
|
|
|
503
510
|
</div>
|
|
504
511
|
)}
|
|
505
512
|
{!hideLegalCopy && (
|
|
506
|
-
<Text
|
|
513
|
+
<Text
|
|
514
|
+
className="body3 my-[32px] text-neutral-600 lg:mb-[64px] lg:mt-[32px]"
|
|
515
|
+
{...fieldProps?.legalCopy}
|
|
516
|
+
>
|
|
507
517
|
By submitting this form you consent to receive marketing messages
|
|
508
518
|
from Kinetic by text, email or phone to the contact provided.
|
|
509
519
|
Consent is not a condition of purchase. Msg & data rates may
|
|
@@ -512,12 +522,18 @@ export const FiberForm: React.FC<FiberFormProps> = ({
|
|
|
512
522
|
</Text>
|
|
513
523
|
)}
|
|
514
524
|
{bottomTextDisclaimer && !hideLegalCopy && (
|
|
515
|
-
<Text
|
|
525
|
+
<Text
|
|
526
|
+
className="mt-2 text-center text-text-secondary"
|
|
527
|
+
{...fieldProps?.bottomTextDisclaimer}
|
|
528
|
+
>
|
|
516
529
|
{bottomTextDisclaimer}
|
|
517
530
|
</Text>
|
|
518
531
|
)}
|
|
519
532
|
{copyRightLinks && !hideLegalCopy && (
|
|
520
|
-
<ul
|
|
533
|
+
<ul
|
|
534
|
+
className="mt-16 flex list-disc justify-center gap-8 text-text-secondary"
|
|
535
|
+
{...fieldProps?.copyRightLinks}
|
|
536
|
+
>
|
|
521
537
|
{copyRightLinks.map(link => (
|
|
522
538
|
<li key={link.title}>
|
|
523
539
|
<a
|
|
@@ -532,7 +548,10 @@ export const FiberForm: React.FC<FiberFormProps> = ({
|
|
|
532
548
|
))}
|
|
533
549
|
</ul>
|
|
534
550
|
)}
|
|
535
|
-
<div
|
|
551
|
+
<div
|
|
552
|
+
className="mt-8 flex items-center justify-center lg:mt-[64px]"
|
|
553
|
+
{...fieldProps?.submit}
|
|
554
|
+
>
|
|
536
555
|
<Button
|
|
537
556
|
type="submit"
|
|
538
557
|
disabled={isSubmitting}
|
|
@@ -2,6 +2,8 @@ import type { ComponentType, KeyboardEvent, ReactNode } from "react";
|
|
|
2
2
|
import type { FormikErrors, FormikTouched, FormikValues } from "formik";
|
|
3
3
|
import type * as Yup from "yup";
|
|
4
4
|
|
|
5
|
+
import type { FieldProps } from "@shared/types/field-props";
|
|
6
|
+
|
|
5
7
|
export type FiberAddressUnit = {
|
|
6
8
|
addressLine2: string;
|
|
7
9
|
dfAddressId?: string;
|
|
@@ -83,6 +85,22 @@ export type FiberFormProps = {
|
|
|
83
85
|
copyRightLinks?: Array<{ title: string; href: string }>;
|
|
84
86
|
className?: string;
|
|
85
87
|
onAddressEvent?: (address: string) => void;
|
|
88
|
+
/** Inspector attributes for each rendered form field, disclaimer and CTA. */
|
|
89
|
+
fieldProps?: FieldProps<
|
|
90
|
+
| "firstName"
|
|
91
|
+
| "lastName"
|
|
92
|
+
| "address"
|
|
93
|
+
| "unit"
|
|
94
|
+
| "phoneNumber"
|
|
95
|
+
| "emailAddress"
|
|
96
|
+
| "referralCode"
|
|
97
|
+
| "notes"
|
|
98
|
+
| "isMarketingCom"
|
|
99
|
+
| "legalCopy"
|
|
100
|
+
| "bottomTextDisclaimer"
|
|
101
|
+
| "copyRightLinks"
|
|
102
|
+
| "submit"
|
|
103
|
+
>;
|
|
86
104
|
};
|
|
87
105
|
|
|
88
106
|
export type FiberFormErrorProps = {
|
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { IframeEmbed } from "./index";
|
|
3
3
|
|
|
4
|
-
import { render, screen
|
|
4
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
5
5
|
|
|
6
6
|
describe("IframeEmbed", () => {
|
|
7
|
+
it("tags the iframe URL outside the embedded document and accepts updated props", () => {
|
|
8
|
+
const { container, rerender } = render(
|
|
9
|
+
<IframeEmbed
|
|
10
|
+
iframeUrl="https://example.com/one"
|
|
11
|
+
fieldProps={{ iframeUrl: { "data-contentful-field-id": "iframeUrl" } }}
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
expect(container.querySelector("section")).toHaveAttribute(
|
|
15
|
+
"data-contentful-field-id",
|
|
16
|
+
"iframeUrl"
|
|
17
|
+
);
|
|
18
|
+
rerender(<IframeEmbed iframeUrl="https://example.com/two" />);
|
|
19
|
+
expect(screen.getByTitle("Iframe Embed")).toHaveAttribute(
|
|
20
|
+
"src",
|
|
21
|
+
"https://example.com/two"
|
|
22
|
+
);
|
|
23
|
+
expect(container.querySelector("section")).not.toHaveAttribute(
|
|
24
|
+
"data-contentful-field-id"
|
|
25
|
+
);
|
|
26
|
+
});
|
|
7
27
|
it("renders nothing when iframeUrl is not provided", () => {
|
|
8
28
|
const { container } = render(<IframeEmbed iframeUrl="" />);
|
|
9
29
|
expect(container.querySelector("section")).not.toBeInTheDocument();
|
|
@@ -18,14 +38,20 @@ describe("IframeEmbed", () => {
|
|
|
18
38
|
|
|
19
39
|
it("applies anchorId to the section", () => {
|
|
20
40
|
const { container } = render(
|
|
21
|
-
<IframeEmbed
|
|
41
|
+
<IframeEmbed
|
|
42
|
+
iframeUrl="https://example.com/embed"
|
|
43
|
+
anchorId="embed-section"
|
|
44
|
+
/>
|
|
22
45
|
);
|
|
23
46
|
expect(container.querySelector("#embed-section")).toBeInTheDocument();
|
|
24
47
|
});
|
|
25
48
|
|
|
26
49
|
it("applies background color when backgroundColor is provided", () => {
|
|
27
50
|
render(
|
|
28
|
-
<IframeEmbed
|
|
51
|
+
<IframeEmbed
|
|
52
|
+
iframeUrl="https://example.com/embed"
|
|
53
|
+
backgroundColor="green"
|
|
54
|
+
/>
|
|
29
55
|
);
|
|
30
56
|
const iframe = screen.getByTitle("Iframe Embed");
|
|
31
57
|
expect(iframe.style.background).toBe("rgb(36, 167, 106)");
|
|
@@ -51,7 +77,9 @@ describe("IframeEmbed", () => {
|
|
|
51
77
|
|
|
52
78
|
it("removes message listener on unmount", () => {
|
|
53
79
|
const removeEventListenerSpy = jest.spyOn(window, "removeEventListener");
|
|
54
|
-
const { unmount } = render(
|
|
80
|
+
const { unmount } = render(
|
|
81
|
+
<IframeEmbed iframeUrl="https://example.com/embed" />
|
|
82
|
+
);
|
|
55
83
|
|
|
56
84
|
unmount();
|
|
57
85
|
|
|
@@ -83,7 +111,10 @@ describe("IframeEmbed", () => {
|
|
|
83
111
|
render(<IframeEmbed iframeUrl="https://example.com/embed" />);
|
|
84
112
|
const iframe = screen.getByTitle("Iframe Embed");
|
|
85
113
|
|
|
86
|
-
fireEvent(
|
|
114
|
+
fireEvent(
|
|
115
|
+
window,
|
|
116
|
+
new MessageEvent("message", { data: { iframeHeight: "abc" } })
|
|
117
|
+
);
|
|
87
118
|
expect(iframe.style.height).toBe("");
|
|
88
119
|
});
|
|
89
120
|
|
|
@@ -91,7 +122,10 @@ describe("IframeEmbed", () => {
|
|
|
91
122
|
render(<IframeEmbed iframeUrl="https://example.com/embed" />);
|
|
92
123
|
const iframe = screen.getByTitle("Iframe Embed");
|
|
93
124
|
|
|
94
|
-
fireEvent(
|
|
125
|
+
fireEvent(
|
|
126
|
+
window,
|
|
127
|
+
new MessageEvent("message", { data: { iframeHeight: 99999 } })
|
|
128
|
+
);
|
|
95
129
|
expect(iframe.style.height).toBe("");
|
|
96
130
|
});
|
|
97
131
|
|
|
@@ -99,7 +133,10 @@ describe("IframeEmbed", () => {
|
|
|
99
133
|
render(<IframeEmbed iframeUrl="https://example.com/embed" />);
|
|
100
134
|
const iframe = screen.getByTitle("Iframe Embed");
|
|
101
135
|
|
|
102
|
-
fireEvent(
|
|
136
|
+
fireEvent(
|
|
137
|
+
window,
|
|
138
|
+
new MessageEvent("message", { data: { iframeHeight: -100 } })
|
|
139
|
+
);
|
|
103
140
|
expect(iframe.style.height).toBe("");
|
|
104
141
|
});
|
|
105
142
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React, { useEffect, useRef } from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { bgColorMap, IframeEmbedProps } from "./types";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* IframeEmbed — A generic, responsive iframe component for embedding third-party content
|
|
@@ -30,6 +30,7 @@ export const IframeEmbed: React.FC<IframeEmbedProps> = ({
|
|
|
30
30
|
anchorId,
|
|
31
31
|
backgroundColor,
|
|
32
32
|
height,
|
|
33
|
+
fieldProps,
|
|
33
34
|
}) => {
|
|
34
35
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
35
36
|
|
|
@@ -56,7 +57,7 @@ export const IframeEmbed: React.FC<IframeEmbedProps> = ({
|
|
|
56
57
|
if (!iframeUrl) return null;
|
|
57
58
|
|
|
58
59
|
return (
|
|
59
|
-
<section id={anchorId} className="w-full">
|
|
60
|
+
<section id={anchorId} className="w-full" {...fieldProps?.iframeUrl}>
|
|
60
61
|
<iframe
|
|
61
62
|
ref={iframeRef}
|
|
62
63
|
src={iframeUrl}
|
|
@@ -6,16 +6,24 @@ import { OrderedList } from "./index";
|
|
|
6
6
|
import { render, screen, within } from "@testing-library/react";
|
|
7
7
|
|
|
8
8
|
jest.mock("@shared/components/link", () => ({
|
|
9
|
-
Link: ({ children, href, title, className }: any) => (
|
|
10
|
-
<a
|
|
9
|
+
Link: ({ children, href, title, className, ...rest }: any) => (
|
|
10
|
+
<a
|
|
11
|
+
href={href}
|
|
12
|
+
title={title}
|
|
13
|
+
className={className}
|
|
14
|
+
data-testid="ol-link"
|
|
15
|
+
{...rest}
|
|
16
|
+
>
|
|
11
17
|
{children}
|
|
12
18
|
</a>
|
|
13
19
|
),
|
|
14
20
|
}));
|
|
15
21
|
|
|
16
22
|
jest.mock("@shared/components/text", () => ({
|
|
17
|
-
Text: ({ children, as: Tag = "span", className }: any) => (
|
|
18
|
-
<Tag className={className}
|
|
23
|
+
Text: ({ children, as: Tag = "span", className, ...rest }: any) => (
|
|
24
|
+
<Tag className={className} {...rest}>
|
|
25
|
+
{children}
|
|
26
|
+
</Tag>
|
|
19
27
|
),
|
|
20
28
|
}));
|
|
21
29
|
|
|
@@ -83,6 +91,48 @@ describe("OrderedList", () => {
|
|
|
83
91
|
expect(screen.getByText("Find availability near you")).toBeInTheDocument();
|
|
84
92
|
});
|
|
85
93
|
|
|
94
|
+
it("tags headings and child names and links independently", () => {
|
|
95
|
+
renderComponent({
|
|
96
|
+
fieldProps: {
|
|
97
|
+
title: { "data-contentful-field-id": "title" },
|
|
98
|
+
description: { "data-contentful-field-id": "description" },
|
|
99
|
+
},
|
|
100
|
+
tabs: [
|
|
101
|
+
{
|
|
102
|
+
items: {
|
|
103
|
+
tabName: "Cities",
|
|
104
|
+
tabContent: [
|
|
105
|
+
{
|
|
106
|
+
itemName: "new york",
|
|
107
|
+
itemSlug: "/ny",
|
|
108
|
+
fieldProps: {
|
|
109
|
+
itemName: { "data-contentful-field-id": "cityName" },
|
|
110
|
+
itemSlug: { "data-contentful-field-id": "slug" },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
});
|
|
118
|
+
expect(screen.getByText("Browse locations")).toHaveAttribute(
|
|
119
|
+
"data-contentful-field-id",
|
|
120
|
+
"title"
|
|
121
|
+
);
|
|
122
|
+
expect(screen.getByText("Find availability near you")).toHaveAttribute(
|
|
123
|
+
"data-contentful-field-id",
|
|
124
|
+
"description"
|
|
125
|
+
);
|
|
126
|
+
expect(screen.getByText("New York")).toHaveAttribute(
|
|
127
|
+
"data-contentful-field-id",
|
|
128
|
+
"cityName"
|
|
129
|
+
);
|
|
130
|
+
expect(screen.getByTestId("ol-link")).toHaveAttribute(
|
|
131
|
+
"data-contentful-field-id",
|
|
132
|
+
"slug"
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
86
136
|
it("renders the section with the provided anchor id and testid", () => {
|
|
87
137
|
const { container } = renderComponent({ anchorId: "locations" });
|
|
88
138
|
const section = container.querySelector(
|