@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.
Files changed (44) hide show
  1. package/dist/contentful/index.d.ts +20 -4
  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 +4 -4
  5. package/dist/contentful/index.js.map +1 -1
  6. package/dist/core.d.ts +1 -1
  7. package/dist/index.esm.js +1 -1
  8. package/dist/index.esm.js.map +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/index.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/components/checklist/index.test.tsx +31 -1
  13. package/src/components/checklist/index.tsx +1 -0
  14. package/src/components/checklist/types.ts +2 -0
  15. package/src/contentful/blocks/accordion/index.test.tsx +53 -0
  16. package/src/contentful/blocks/accordion/index.tsx +7 -2
  17. package/src/contentful/blocks/accordion/types.ts +4 -0
  18. package/src/contentful/blocks/callout/{callout.stories.tsx → Callout.stories.tsx} +1 -1
  19. package/src/contentful/blocks/callout/field-props.test.tsx +184 -0
  20. package/src/contentful/blocks/callout/index.tsx +26 -4
  21. package/src/contentful/blocks/callout/types.ts +17 -1
  22. package/src/contentful/blocks/cards/blog-card/index.tsx +6 -2
  23. package/src/contentful/blocks/cards/blog-card/types.ts +4 -0
  24. package/src/contentful/blocks/cards/floating-image-card/index.tsx +4 -2
  25. package/src/contentful/blocks/cards/floating-image-card/types.ts +4 -0
  26. package/src/contentful/blocks/cards/full-image-card/index.tsx +24 -5
  27. package/src/contentful/blocks/cards/full-image-card/types.ts +4 -0
  28. package/src/contentful/blocks/cards/product-card/index.test.tsx +29 -4
  29. package/src/contentful/blocks/cards/product-card/index.tsx +46 -44
  30. package/src/contentful/blocks/cards/product-card/types.ts +1 -1
  31. package/src/contentful/blocks/cards/simple-card/index.tsx +21 -3
  32. package/src/contentful/blocks/cards/simple-card/types.ts +4 -0
  33. package/src/contentful/blocks/carousel/helper.test.tsx +24 -0
  34. package/src/contentful/blocks/carousel/helper.tsx +1 -1
  35. package/src/contentful/blocks/comparison-table/index.test.tsx +75 -0
  36. package/src/contentful/blocks/comparison-table/index.tsx +15 -3
  37. package/src/contentful/blocks/comparison-table/types.ts +8 -2
  38. package/src/contentful/blocks/cta-callout/index.test.tsx +93 -1
  39. package/src/contentful/blocks/cta-callout/index.tsx +9 -2
  40. package/src/contentful/blocks/cta-callout/types.ts +3 -0
  41. package/src/contentful/blocks/modal/index.test.tsx +75 -2
  42. package/src/contentful/blocks/modal/index.tsx +11 -2
  43. package/src/contentful/blocks/modal/types.ts +4 -0
  44. 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 {...defaultProps} giftBadge={giftBadge} hasAnyGift={true} />
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
- <div className="flex flex-col items-start md:items-end">
141
- <div className="flex items-start" {...fieldProps?.price}>
142
- <Text
143
- as="span"
144
- className={cx(
145
- priceUnitClass,
146
- "mr-1 mt-[2px]",
147
- isDark ? "text-text-inverse" : "text-text"
148
- )}
149
- >
150
- $
151
- </Text>
152
- <Text
153
- as="span"
154
- className={cx(
155
- priceIntegerClass,
156
- "leading-none",
157
- isDark ? "text-text-inverse" : "text-text"
158
- )}
159
- >
160
- {price.split(".")[0]}
161
- </Text>
162
- <Text
163
- as="span"
164
- className={cx(
165
- priceUnitClass,
166
- isDark ? "text-text-inverse" : "text-text"
167
- )}
168
- >
169
- {`${price.split(".")[1]}${priceUnit}`}
170
- </Text>
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
- {priceSuffixBottom ? (
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: string;
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={card.imageAltText ?? card.imageDescription ?? card.imageAlt ?? card.title ?? "card-image"}
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={card.imageAltText ?? card.imageDescription ?? card.imageAlt ?? card.title ?? "card-icon"}
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 className={cx(!isCtaOnly && "mt-2", ctaAlignmentClass)}>
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 = `${item?.price || "0"}.${priceCents}`;
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 as="h2" className="heading2 md:heading1 md:text-center">
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 className="comparison-table-container mt-5 p-5 pt-0 md:mt-10 md:px-10 md:py-9">
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 as="div" className="micro mt-8 text-center">
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?: React.ReactNode;
4
- table: React.ReactNode;
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.buttonLabel || props.children}
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 w-full`}
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 className="flex justify-center pt-5 lg:pt-14">
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 =