@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.
Files changed (72) hide show
  1. package/dist/contentful/index.d.ts +46 -9
  2. package/dist/contentful/index.esm.js +4 -4
  3. package/dist/contentful/index.esm.js.map +1 -1
  4. package/dist/contentful/index.js +3 -3
  5. package/dist/contentful/index.js.map +1 -1
  6. package/dist/core.d.ts +6 -3
  7. package/dist/index.d.ts +4 -1
  8. package/dist/index.esm.js +2 -2
  9. package/dist/index.esm.js.map +1 -1
  10. package/dist/index.js +2 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/utils/index.d.ts +2 -1
  13. package/dist/utils/index.esm.js +1 -1
  14. package/dist/utils/index.esm.js.map +1 -1
  15. package/dist/utils/index.js +1 -1
  16. package/dist/utils/index.js.map +1 -1
  17. package/package.json +1 -1
  18. package/src/components/checklist/index.test.tsx +18 -1
  19. package/src/components/checklist/index.tsx +25 -21
  20. package/src/components/checklist/types.ts +8 -2
  21. package/src/contentful/blocks/address-input-banner/index.test.tsx +14 -0
  22. package/src/contentful/blocks/address-input-banner/index.tsx +3 -1
  23. package/src/contentful/blocks/address-input-banner/types.ts +1 -1
  24. package/src/contentful/blocks/anchored-bottom-banner/index.test.tsx +91 -0
  25. package/src/contentful/blocks/anchored-bottom-banner/index.tsx +18 -11
  26. package/src/contentful/blocks/anchored-bottom-banner/types.ts +17 -3
  27. package/src/contentful/blocks/blogs-grid/index.test.tsx +21 -1
  28. package/src/contentful/blocks/blogs-grid/index.tsx +1 -0
  29. package/src/contentful/blocks/blogs-grid/types.ts +3 -0
  30. package/src/contentful/blocks/blogs-grid-base/index.test.tsx +49 -3
  31. package/src/contentful/blocks/blogs-grid-base/index.tsx +11 -2
  32. package/src/contentful/blocks/blogs-grid-base/types.ts +7 -1
  33. package/src/contentful/blocks/cards/blog-card/index.test.tsx +29 -0
  34. package/src/contentful/blocks/cards/blog-card/index.tsx +18 -4
  35. package/src/contentful/blocks/cards/blog-card/types.ts +3 -1
  36. package/src/contentful/blocks/cards/product-card/index.test.tsx +43 -0
  37. package/src/contentful/blocks/cards/product-card/index.tsx +37 -9
  38. package/src/contentful/blocks/cards/product-card/types.ts +3 -0
  39. package/src/contentful/blocks/carousel/helper.test.tsx +13 -0
  40. package/src/contentful/blocks/carousel/helper.tsx +1 -0
  41. package/src/contentful/blocks/carousel/types.ts +2 -0
  42. package/src/contentful/blocks/dynamic-form/conditional-fields.tsx +1 -0
  43. package/src/contentful/blocks/dynamic-form/form-field.tsx +1 -0
  44. package/src/contentful/blocks/dynamic-form/index.test.tsx +48 -0
  45. package/src/contentful/blocks/dynamic-form/index.tsx +19 -7
  46. package/src/contentful/blocks/dynamic-form/render.tsx +13 -7
  47. package/src/contentful/blocks/dynamic-form/types.ts +28 -2
  48. package/src/contentful/blocks/dynamic-tabs/index.test.tsx +52 -2
  49. package/src/contentful/blocks/dynamic-tabs/index.tsx +9 -2
  50. package/src/contentful/blocks/dynamic-tabs/ordered-tab-views.tsx +16 -6
  51. package/src/contentful/blocks/dynamic-tabs/tab-views.tsx +10 -6
  52. package/src/contentful/blocks/dynamic-tabs/types.ts +6 -0
  53. package/src/contentful/blocks/email-input-block/index.test.tsx +51 -3
  54. package/src/contentful/blocks/email-input-block/index.tsx +88 -31
  55. package/src/contentful/blocks/email-input-block/types.ts +14 -1
  56. package/src/contentful/blocks/fiber-form/index.test.tsx +40 -2
  57. package/src/contentful/blocks/fiber-form/index.tsx +32 -13
  58. package/src/contentful/blocks/fiber-form/types.ts +18 -0
  59. package/src/contentful/blocks/iframe-embed/index.test.tsx +44 -7
  60. package/src/contentful/blocks/iframe-embed/index.tsx +3 -2
  61. package/src/contentful/blocks/iframe-embed/types.ts +1 -0
  62. package/src/contentful/blocks/navigation/{Navigation.stories.tsx → navigation.stories.tsx} +1 -1
  63. package/src/contentful/blocks/ordered-list/index.test.tsx +54 -4
  64. package/src/contentful/blocks/ordered-list/index.tsx +14 -2
  65. package/src/contentful/blocks/ordered-list/types.ts +15 -2
  66. package/src/contentful/blocks/primary-hero/index.tsx +5 -5
  67. package/src/contentful/blocks/search-block/index.test.tsx +36 -2
  68. package/src/contentful/blocks/search-block/index.tsx +24 -4
  69. package/src/contentful/blocks/search-block/types.ts +10 -0
  70. package/src/utils/index.ts +1 -1
  71. package/src/utils/replace-tokens.test.ts +40 -0
  72. package/src/utils/replace-tokens.ts +13 -0
@@ -1,9 +1,13 @@
1
1
  import { ReactNode } from "react";
2
2
 
3
+ import type { FieldProps } from "@shared/types/field-props";
4
+
3
5
  export interface DynamicTabT {
4
6
  tabName: string;
5
7
  anchorId: string;
6
8
  tabContent: ReactNode;
9
+ /** Inspector attributes for this tab's name and content. */
10
+ fieldProps?: FieldProps<"tabName" | "tabContent">;
7
11
  }
8
12
 
9
13
  export interface DynamicTabsT {
@@ -22,4 +26,6 @@ export interface DynamicTabsT {
22
26
  textColor?: "navy";
23
27
  navigationType?: boolean;
24
28
  mobileSidebarTabType?: boolean;
29
+ /** Inspector attributes for the block heading fields. */
30
+ fieldProps?: FieldProps<"title" | "subTitle">;
25
31
  }
@@ -15,6 +15,56 @@ describe("EmailInputBlock", () => {
15
15
  jest.clearAllMocks();
16
16
  });
17
17
 
18
+ it("tags content, email input, CTA, validation and caption fields", () => {
19
+ const { container } = render(
20
+ <EmailInputBlock
21
+ {...defaultProps}
22
+ title="Join us"
23
+ subTitle="Stay in touch"
24
+ emailInputDescription="Sign up now"
25
+ successFeedback="Thanks!"
26
+ caption="No spam"
27
+ fieldProps={{
28
+ title: { "data-contentful-field-id": "title" },
29
+ subTitle: { "data-contentful-field-id": "subTitle" },
30
+ emailInputDescription: {
31
+ "data-contentful-field-id": "emailInputDescription",
32
+ },
33
+ email: { "data-contentful-field-id": "email" },
34
+ cta: { "data-contentful-field-id": "cta" },
35
+ errorMessage: { "data-contentful-field-id": "errorMessage" },
36
+ successFeedback: {
37
+ "data-contentful-field-id": "successFeedback",
38
+ },
39
+ caption: { "data-contentful-field-id": "caption" },
40
+ }}
41
+ />
42
+ );
43
+ [
44
+ "title",
45
+ "subTitle",
46
+ "emailInputDescription",
47
+ "email",
48
+ "cta",
49
+ "caption",
50
+ ].forEach(field => {
51
+ expect(
52
+ container.querySelector(`[data-contentful-field-id="${field}"]`)
53
+ ).toBeInTheDocument();
54
+ });
55
+ fireEvent.click(screen.getByText("Sign me up"));
56
+ expect(
57
+ container.querySelector('[data-contentful-field-id="errorMessage"]')
58
+ ).toBeInTheDocument();
59
+ fireEvent.change(screen.getByPlaceholderText("Enter your email here"), {
60
+ target: { value: "user@example.com" },
61
+ });
62
+ fireEvent.click(screen.getByText("Sign me up"));
63
+ expect(
64
+ container.querySelector('[data-contentful-field-id="successFeedback"]')
65
+ ).toBeInTheDocument();
66
+ });
67
+
18
68
  describe("Rendering", () => {
19
69
  it("renders with default props", () => {
20
70
  const { container } = render(<EmailInputBlock {...defaultProps} />);
@@ -176,9 +226,7 @@ describe("EmailInputBlock", () => {
176
226
  describe("Themes", () => {
177
227
  it("applies green theme background by default", () => {
178
228
  const { container } = render(<EmailInputBlock {...defaultProps} />);
179
- expect(
180
- container.querySelector(".bg-bg-fill-brand")
181
- ).toBeInTheDocument();
229
+ expect(container.querySelector(".bg-bg-fill-brand")).toBeInTheDocument();
182
230
  });
183
231
 
184
232
  it("uses min-height instead of fixed height on the container", () => {
@@ -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
- const clipPathId = `logo-cream-clip-${React.useId()}`;
9
- return (
10
- <svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" fill="none" viewBox="0 0 100 99" preserveAspectRatio="xMidYMid meet">
11
- <g fill={fill} clipPath={`url(#${clipPathId})`}>
12
- <path 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" opacity=".45" />
13
- <path 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" opacity=".45" />
14
- <path 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" opacity=".45" />
15
- <path 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" opacity=".45" />
16
- </g>
17
- <defs>
18
- <clipPath id={clipPathId}>
19
- <path fill="#fff" d="M0 0h100v99H0z" />
20
- </clipPath>
21
- </defs>
22
- </svg>
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 = backgroundColorClasses[normalizedBg as keyof typeof backgroundColorClasses] || backgroundColorClasses.white;
65
- const captionTextColorClass = normalizedBg === "white" ? "text-text" : "text-white";
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
- ((themeColor as string) || "").trim().toLowerCase() || "green";
69
- const containerBgClass = themeColorClasses[normalizedTheme as keyof typeof themeColorClasses] || themeColorClasses.green;
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 = padding === "none" ? "!py-0 md:!py-0" : "!py-8 md:!py-20";
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 id={anchorId} className={`w-full px-4 ${paddingClass} md:px-10 ${outerBgClass}`}>
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 className="absolute right-[-50px] top-[-30px] hidden h-[420px] w-[420px] pointer-events-none lg:block" aria-hidden="true">
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 className={`mb-3 text-[2.25rem] leading-[2.5rem] font-black md:text-heading2 md:font-black ${textColor}`}>{title}</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 className={`subheading3 mb-3 font-black ${textColor}`}>
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 className={`!mb-4 md:!mb-16 text-xl leading-7 font-medium md:text-subheading1 md:!leading-8 md:!font-bold opacity-95 ${textColor}`}>
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 className={`mt-2 text-label4 font-semibold ${errorTextColor}`}>
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 className={`mt-2 text-label4 font-semibold ${textColor}`}>
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 className={`mx-auto max-w-[1200px] px-6 md:px-8 ${captionTextColorClass}`}>{caption}</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 className="w-full sm:w-[216px] sm:flex-none">
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 className="mt-16 text-neutral-600">
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 className="body3 my-[32px] text-neutral-600 lg:mb-[64px] lg:mt-[32px]">
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 &amp; data rates may
@@ -512,12 +522,18 @@ export const FiberForm: React.FC<FiberFormProps> = ({
512
522
  </Text>
513
523
  )}
514
524
  {bottomTextDisclaimer && !hideLegalCopy && (
515
- <Text className="mt-2 text-center text-text-secondary">
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 className="mt-16 flex list-disc justify-center gap-8 text-text-secondary">
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 className="mt-8 flex items-center justify-center lg:mt-[64px]">
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, fireEvent } from "@testing-library/react";
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 iframeUrl="https://example.com/embed" anchorId="embed-section" />
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 iframeUrl="https://example.com/embed" backgroundColor="green" />
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(<IframeEmbed iframeUrl="https://example.com/embed" />);
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(window, new MessageEvent("message", { data: { iframeHeight: "abc" } }));
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(window, new MessageEvent("message", { data: { iframeHeight: 99999 } }));
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(window, new MessageEvent("message", { data: { iframeHeight: -100 } }));
136
+ fireEvent(
137
+ window,
138
+ new MessageEvent("message", { data: { iframeHeight: -100 } })
139
+ );
103
140
  expect(iframe.style.height).toBe("");
104
141
  });
105
142
  });