@windstream/react-shared-components 0.2.60 → 0.2.61

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 (31) hide show
  1. package/dist/contentful/index.d.ts +27 -1
  2. package/dist/contentful/index.esm.js +2 -2
  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/dist/styles.css +1 -1
  12. package/package.json +1 -1
  13. package/src/components/brand-button/index.tsx +1 -1
  14. package/src/contentful/blocks/cards/product-card/index.test.tsx +14 -1
  15. package/src/contentful/blocks/cards/product-card/index.tsx +189 -136
  16. package/src/contentful/blocks/cards/product-card/types.ts +1 -1
  17. package/src/contentful/blocks/carousel/Carousel.stories.tsx +59 -0
  18. package/src/contentful/blocks/carousel/helper.test.tsx +23 -0
  19. package/src/contentful/blocks/carousel/helper.tsx +18 -5
  20. package/src/contentful/blocks/carousel/types.ts +3 -0
  21. package/src/contentful/blocks/dynamic-form/conditional-fields.test.tsx +3 -0
  22. package/src/contentful/blocks/dynamic-form/conditional-fields.tsx +30 -4
  23. package/src/contentful/blocks/dynamic-form/context.ts +2 -0
  24. package/src/contentful/blocks/dynamic-form/field-map.test.tsx +21 -0
  25. package/src/contentful/blocks/dynamic-form/field-map.tsx +15 -0
  26. package/src/contentful/blocks/dynamic-form/form-field.tsx +2 -0
  27. package/src/contentful/blocks/dynamic-form/index.tsx +48 -16
  28. package/src/contentful/blocks/dynamic-form/render.tsx +1 -0
  29. package/src/contentful/blocks/dynamic-form/types.ts +26 -0
  30. package/src/contentful/blocks/dynamic-form/utils.test.ts +86 -0
  31. package/src/contentful/blocks/dynamic-form/utils.ts +57 -0
@@ -11,6 +11,20 @@ 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
+ /** Renders text where `|...|`-wrapped segments are bold (pipes are stripped). */
15
+ const renderPipeBold = (value: React.ReactNode): React.ReactNode => {
16
+ if (typeof value !== "string" || !value.includes("|")) return value;
17
+ return value.split("|").map((segment, index) =>
18
+ index % 2 === 1 ? (
19
+ <span key={index} className="font-bold">
20
+ {segment}
21
+ </span>
22
+ ) : (
23
+ <React.Fragment key={index}>{segment}</React.Fragment>
24
+ )
25
+ );
26
+ };
27
+
14
28
  export const ProductCard: React.FC<ProductCardProps> = ({
15
29
  planName,
16
30
  planSubtext,
@@ -31,6 +45,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
31
45
  cta,
32
46
  techType,
33
47
  ismaxSpeed,
48
+ priceSuffixBottom,
34
49
  }) => {
35
50
  const [internalExpanded, setInternalExpanded] = useState(false);
36
51
  // Use parent state if provided, otherwise use internal state
@@ -77,7 +92,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
77
92
  <Text
78
93
  as="h3"
79
94
  className={cx(
80
- "subheading2",
95
+ "subheading2 lowercase",
81
96
  isDark ? "text-text-accent-on-bg-inverse" : "text-text-brand"
82
97
  )}
83
98
  >
@@ -85,46 +100,65 @@ export const ProductCard: React.FC<ProductCardProps> = ({
85
100
  </Text>
86
101
  <Text
87
102
  as="p"
88
- className={`label3 text-wrap ${isDark ? "text-text-accent-on-bg-inverse" : "text-text-brand"}`}
89
- >
90
- {planSubtext}
91
- </Text>
92
- </div>
93
- <div className="flex items-start">
94
- <Text
95
- as="span"
96
- className={cx(
97
- "label2 mr-1 mt-[2px]",
98
- isDark ? "text-text-inverse" : "text-text"
99
- )}
100
- >
101
- $
102
- </Text>
103
- <Text
104
- as="span"
105
103
  className={cx(
106
- "subheading6",
107
- isDark ? "text-text-inverse" : "text-text"
108
- )}
109
- >
110
- {price.split(".")[0]}
111
- </Text>
112
- <Text
113
- as="span"
114
- className={cx(
115
- "label2",
116
- isDark ? "text-text-inverse" : "text-text"
104
+ "label3 text-wrap lowercase",
105
+ isDark ? "text-text-accent-on-bg-inverse" : "text-text-brand"
117
106
  )}
118
107
  >
119
- {price.split(".")[1]}/mo
108
+ {planSubtext}
120
109
  </Text>
121
110
  </div>
111
+ <div className="flex flex-col items-end">
112
+ <div className="flex items-start">
113
+ <Text
114
+ as="span"
115
+ className={cx(
116
+ "label2 mr-1 mt-[2px]",
117
+ isDark ? "text-text-inverse" : "text-text"
118
+ )}
119
+ >
120
+ $
121
+ </Text>
122
+ <Text
123
+ as="span"
124
+ className={cx(
125
+ "subheading6",
126
+ isDark ? "text-text-inverse" : "text-text"
127
+ )}
128
+ >
129
+ {price.split(".")[0]}
130
+ </Text>
131
+ <Text
132
+ as="span"
133
+ className={cx(
134
+ "label2",
135
+ isDark ? "text-text-inverse" : "text-text"
136
+ )}
137
+ >
138
+ {price.split(".")[1]}/mo
139
+ </Text>
140
+ </div>
141
+ {priceSuffixBottom ? (
142
+ <Text
143
+ as="span"
144
+ className={cx(
145
+ "footnote text-nowrap",
146
+ isDark ? "text-text-inverse" : "text-text"
147
+ )}
148
+ >
149
+ {priceSuffixBottom}
150
+ </Text>
151
+ ) : null}
152
+ </div>
122
153
  </header>
123
154
 
124
155
  {/* Description */}
125
156
  <section>
126
157
  <Text
127
- className={cx("body2", isDark ? "text-text-inverse" : "text-text")}
158
+ className={cx(
159
+ "body2 h-[56px]",
160
+ isDark ? "text-text-inverse" : "text-text"
161
+ )}
128
162
  >
129
163
  {description}
130
164
  </Text>
@@ -132,118 +166,137 @@ export const ProductCard: React.FC<ProductCardProps> = ({
132
166
 
133
167
  {/* Gift Badge */}
134
168
  {giftBadge?.length ? (
135
- <div
136
- className={cx(
137
- "flex-col items-center rounded-surface-sm p-2",
138
- isDark ? "bg-bg-surface-inverse" : "bg-bg-surface-info"
139
- )}
140
- >
141
- {giftBadge?.map((badge: any) => {
142
- return (
143
- <div key={badge.title} className="flex items-center gap-2">
144
- <Image
145
- src={badge.icon}
146
- alt={badge.title}
147
- width={24}
148
- height={24}
149
- />
150
- <Text
151
- as="div"
152
- className={cx(
153
- "body3",
154
- isDark ? "text-text-inverse" : "text-text"
155
- )}
156
- >
157
- {badge.title}
158
- </Text>
159
- </div>
160
- );
161
- })}
162
- </div>
163
- ) : null}
164
-
165
- {/* CTA */}
166
- <SelectPlanButton
167
- onSelect={onCtaClick}
168
- speed={speed}
169
- isSelected={isDark}
170
- renderCheckPlans={renderCheckPlans}
171
- cta={cta}
172
- iconSize={40}
173
- techType={techType}
174
- isMax={ismaxSpeed}
175
- />
176
- {/* Features */}
177
- {features.length > 0 && (
178
- <section className="flex flex-col gap-3">
179
- <Button
180
- onClick={handleToggle}
181
- className="group flex w-full items-center gap-2 text-left"
182
- >
183
- <Text
184
- as="h4"
185
- className={cx(
186
- "label3",
187
- isDark ? "text-text-inverse" : "text-text"
188
- )}
189
- >
190
- {featuresTitle}
191
- </Text>
192
- <MaterialIcon
193
- name="keyboard_arrow_down"
194
- fill={1}
195
- size={24}
196
- className={cx(
197
- "transition-transform duration-300",
198
- isExpanded && "rotate-180",
199
- isDark ? "text-text-inverse" : "text-text"
200
- )}
201
- />
202
- </Button>
169
+ <div className="min-h-[76px]">
203
170
  <div
204
171
  className={cx(
205
- "overflow-hidden transition-all duration-200 ease-in-out",
206
- isExpanded ? "opacity-100" : "max-h-0 opacity-0"
172
+ "flex-col items-center rounded-surface-sm p-2",
173
+ isDark ? "bg-bg-surface-inverse" : "bg-bg-surface-info"
207
174
  )}
208
175
  >
209
- <div className="flex flex-col">
210
- <Checklist
211
- items={features}
212
- iconSize={24}
213
- iconPosition="top"
214
- iconClassName="mt-0"
215
- iconColor={isDark ? "yellow" : "green"}
216
- listContainerClassName="mt-0 space-y-0 flex flex-col gap-3"
217
- listItemClassName={`${isDark ? "text-text-inverse" : "text-text"}`}
176
+ {giftBadge?.map((badge: any) => {
177
+ return (
178
+ <div key={badge.title} className="flex items-center gap-2">
179
+ <Image
180
+ src={badge.icon}
181
+ alt={badge.title}
182
+ width={24}
183
+ height={24}
184
+ />
185
+ <Text
186
+ as="div"
187
+ className={cx(
188
+ "body3",
189
+ isDark ? "text-text-inverse" : "text-text"
190
+ )}
191
+ >
192
+ {renderPipeBold(badge.title)}
193
+ </Text>
194
+ </div>
195
+ );
196
+ })}
197
+ </div>
198
+ </div>
199
+ ) : (
200
+ <div className="invisible min-h-[76px] p-2" aria-hidden="true">
201
+ <Text
202
+ as="div"
203
+ className={cx(
204
+ "body3",
205
+ isDark ? "text-text-inverse" : "text-text"
206
+ )}
207
+ >
208
+ &nbsp;
209
+ </Text>
210
+ </div>
211
+ )}
212
+
213
+ {/* CTA + features pinned to the bottom so they align across cards of
214
+ differing content height (e.g. with/without a gift badge). */}
215
+ <div className="flex flex-col gap-5">
216
+ <SelectPlanButton
217
+ onSelect={onCtaClick}
218
+ speed={speed}
219
+ isSelected={isDark}
220
+ renderCheckPlans={renderCheckPlans}
221
+ cta={cta}
222
+ iconSize={40}
223
+ techType={techType}
224
+ isMax={ismaxSpeed}
225
+ />
226
+ {/* Features */}
227
+ {features.length > 0 && (
228
+ <section className="flex flex-col gap-3">
229
+ <Button
230
+ onClick={handleToggle}
231
+ className="group flex w-full items-center gap-2 text-left"
232
+ >
233
+ <Text
234
+ as="h4"
235
+ className={cx(
236
+ "label3",
237
+ isDark ? "text-text-inverse" : "text-text"
238
+ )}
239
+ >
240
+ {featuresTitle}
241
+ </Text>
242
+ <MaterialIcon
243
+ name="keyboard_arrow_down"
244
+ fill={1}
245
+ size={24}
246
+ className={cx(
247
+ "transition-transform duration-300",
248
+ isExpanded && "rotate-180",
249
+ isDark ? "text-text-inverse" : "text-text"
250
+ )}
218
251
  />
219
- </div>
220
- {innerBadge?.badgeIcon ? (
221
- <div className="flex items-center gap-2 text-center">
222
- <Image
223
- src={innerBadge.badgeIcon}
224
- alt="Inner Badge"
225
- width={12}
226
- height={12}
252
+ </Button>
253
+ <div
254
+ className={cx(
255
+ "overflow-hidden transition-all duration-200 ease-in-out",
256
+ isExpanded ? "opacity-100" : "max-h-0 opacity-0"
257
+ )}
258
+ >
259
+ <div className="flex flex-col">
260
+ <Checklist
261
+ items={features}
262
+ iconSize={24}
263
+ iconPosition="top"
264
+ iconClassName="mt-0"
265
+ iconColor={isDark ? "yellow" : "green"}
266
+ listContainerClassName="mt-0 space-y-0 flex flex-col gap-3"
267
+ listItemClassName={`${isDark ? "text-text-inverse" : "text-text"}`}
227
268
  />
228
- <span className="footnote text-text">
229
- {innerBadge.badgeText.includes("|") ? (
230
- <>
231
- <span className="font-bold">
232
- {innerBadge.badgeText.split("|")[0]}
233
- </span>
234
- <span className="font-normal">
235
- {innerBadge.badgeText.split("|")[1]}
236
- </span>
237
- </>
238
- ) : (
239
- <Text className="font-bold">{innerBadge.badgeText}</Text>
240
- )}
241
- </span>
242
269
  </div>
243
- ) : null}
244
- </div>
245
- </section>
246
- )}
270
+ {innerBadge?.badgeIcon ? (
271
+ <div className="mt-3 flex items-center gap-2 text-center">
272
+ <Image
273
+ src={innerBadge.badgeIcon}
274
+ alt="Inner Badge"
275
+ width={12}
276
+ height={12}
277
+ />
278
+ <span className="footnote text-text">
279
+ {innerBadge.badgeText.includes("|") ? (
280
+ <>
281
+ <span className="font-bold">
282
+ {innerBadge.badgeText.split("|")[0]}
283
+ </span>
284
+ <span className="font-normal">
285
+ {innerBadge.badgeText.split("|")[1]}
286
+ </span>
287
+ </>
288
+ ) : (
289
+ <Text className="font-bold">
290
+ {innerBadge.badgeText}
291
+ </Text>
292
+ )}
293
+ </span>
294
+ </div>
295
+ ) : null}
296
+ </div>
297
+ </section>
298
+ )}
299
+ </div>
247
300
  </div>
248
301
  </article>
249
302
  );
@@ -10,6 +10,7 @@ export interface ProductCardProps {
10
10
  techType?: string;
11
11
  ismaxSpeed?: boolean;
12
12
  price: string;
13
+ priceSuffixBottom?: string;
13
14
  description: string;
14
15
  bestValue?: boolean;
15
16
  bestValueText?: string;
@@ -23,6 +24,5 @@ export interface ProductCardProps {
23
24
  ctaText?: string;
24
25
  cta?: ButtonProps;
25
26
  onCtaClick: () => void;
26
- hostType: "residential" | "smb";
27
27
  renderCheckPlans?: (overrides?: CheckPlansProps) => React.ReactNode;
28
28
  }
@@ -1,4 +1,5 @@
1
1
  import { Carousel } from "./index";
2
+ import type { carouselFieldsInterface } from "./types";
2
3
 
3
4
  import { DocsPage } from "@shared/stories/DocsTemplate";
4
5
  import type { Meta, StoryObj } from "@storybook/react";
@@ -21,3 +22,61 @@ const meta: Meta<typeof Carousel> = {
21
22
  export default meta;
22
23
  type Story = StoryObj<typeof meta>;
23
24
  export const Default: Story = {};
25
+
26
+ const sampleProduct = (
27
+ id: string,
28
+ planName: string,
29
+ price: string,
30
+ highlighted = false
31
+ ) => ({
32
+ __typename: "ComponentProductCard" as const,
33
+ sys: { id },
34
+ speed: `${planName}|Fiber internet`,
35
+ price,
36
+ priceSuffix: "99/mo",
37
+ techType: "Fiber",
38
+ highlighted,
39
+ productCardDescription: "Reliable internet for your home.",
40
+ benefitsTitle: "Plan benefits",
41
+ benefitsExpanded: false,
42
+ cta: { buttonLabel: "Select plan" },
43
+ benefits: {
44
+ items: [{ text: "No data caps" }, { text: "Free installation" }],
45
+ },
46
+ giftRewards: { list: { items: [] } },
47
+ });
48
+
49
+ const twoColumnFields: carouselFieldsInterface = {
50
+ __typename: "ComponentCarousel",
51
+ sys: { id: "carousel-two-column" },
52
+ title: "Two-column product cards",
53
+ subTitle: "Renders as a two-column grid on mobile (resize below 768px).",
54
+ twoColumn: true,
55
+ backgroundColor: "white",
56
+ items: {
57
+ items: [
58
+ sampleProduct("plan-1", "300 Mbps", "49"),
59
+ sampleProduct("plan-2", "600 Mbps", "59", true),
60
+ sampleProduct("plan-3", "1 Gig", "69"),
61
+ sampleProduct("plan-4", "2 Gig", "89"),
62
+ ],
63
+ },
64
+ };
65
+
66
+ /**
67
+ * Two-column variant: on mobile (below 768px) product cards render in a
68
+ * two-column grid instead of a vertical stack. Resize the canvas to see it.
69
+ */
70
+ export const TwoColumnProductCards: Story = {
71
+ args: {
72
+ fields: twoColumnFields,
73
+ hasProductCards: true,
74
+ hasTestimonialCards: false,
75
+ disclaimerText: null,
76
+ backgroundColor: "bg-white",
77
+ activeTab: "",
78
+ setActiveTab: () => {},
79
+ tabs: [],
80
+ },
81
+ };
82
+
@@ -162,6 +162,29 @@ describe("ProductCardCarousel", () => {
162
162
  expect(container.querySelector(".flex-col")).toBeInTheDocument();
163
163
  });
164
164
 
165
+ it("renders a two-column grid on mobile in carousel mode when twoColumn is set", () => {
166
+ const items = Array.from({ length: 4 }, (_, i) => ({
167
+ ...sampleProduct,
168
+ sys: { id: `prod-${i}` },
169
+ }));
170
+ const fields = createProductFields(items);
171
+ fields.twoColumn = true;
172
+ const { container } = render(<ProductCardCarousel fields={fields} />);
173
+ // Mobile view uses a two-column grid instead of a vertical stack
174
+ expect(container.querySelector(".grid.grid-cols-2")).toBeInTheDocument();
175
+ expect(container.querySelector(".flex-col")).not.toBeInTheDocument();
176
+ });
177
+
178
+ it("renders a two-column grid on mobile in non-carousel mode when twoColumn is set", () => {
179
+ const fields = createProductFields([
180
+ sampleProduct,
181
+ { ...sampleProduct, sys: { id: "prod-2" } },
182
+ ]);
183
+ fields.twoColumn = true;
184
+ const { container } = render(<ProductCardCarousel fields={fields} />);
185
+ expect(container.querySelector(".grid.grid-cols-2")).toBeInTheDocument();
186
+ });
187
+
165
188
  it("parses speed field correctly (planName | planSubtext)", () => {
166
189
  const fields = createProductFields([sampleProduct]);
167
190
  render(<ProductCardCarousel fields={fields} />);
@@ -33,6 +33,7 @@ function ProductCardPanel({
33
33
  const [swipeOffset, setSwipeOffset] = useState(0);
34
34
  const [isSwiping, setIsSwiping] = useState(false);
35
35
  const items = fields?.items?.items || [];
36
+ const twoColumn = fields?.twoColumn || false;
36
37
  const isCarousel = items.length > 2;
37
38
  const showArrows = fields?.showArrows !== false && isCarousel;
38
39
  const cardsRef = useRef<(HTMLDivElement | null)[]>([]);
@@ -232,20 +233,32 @@ function ProductCardPanel({
232
233
  onCtaClick={handleCtaClick}
233
234
  renderCheckPlans={renderCheckPlans}
234
235
  cta={item.cta}
235
- hostType="smb"
236
+ priceSuffixBottom={item?.priceSuffixBottom}
236
237
  />
237
238
  </div>
238
239
  );
239
240
  };
240
241
 
241
242
  return !isCarousel ? (
242
- <div className="flex w-full max-w-[1280px] flex-wrap justify-center gap-4">
243
- {items.map((item, index) => renderCard(item, index))}
243
+ <div
244
+ className={cx(
245
+ "w-full max-w-[1280px] gap-4",
246
+ twoColumn
247
+ ? "grid grid-cols-2 md:flex md:flex-wrap md:justify-center"
248
+ : "flex flex-wrap justify-center"
249
+ )}
250
+ >
251
+ {items.map((item, index) => renderCard(item, index, twoColumn))}
244
252
  </div>
245
253
  ) : (
246
254
  <>
247
- {/* Mobile/Tablet View: Vertical Stack */}
248
- <div className="flex flex-col gap-6 md:hidden">
255
+ {/* Mobile/Tablet View: Vertical Stack (or two-column grid) */}
256
+ <div
257
+ className={cx(
258
+ "md:hidden",
259
+ twoColumn ? "grid grid-cols-2 gap-4" : "flex flex-col gap-6"
260
+ )}
261
+ >
249
262
  {items.map((item, index) => renderCard(item, index, true))}
250
263
  </div>
251
264
 
@@ -17,6 +17,8 @@ export interface carouselFieldsInterface {
17
17
  initialSlideIndex?: number;
18
18
  infiniteScroll?: boolean;
19
19
  mobileNavigationType?: boolean;
20
+ // When true, product cards render in a two-column grid on mobile instead of a vertical stack.
21
+ twoColumn?: boolean;
20
22
  disclaimerText?: {
21
23
  json: any;
22
24
  };
@@ -58,6 +60,7 @@ export interface ProductCardFields {
58
60
  speed?: string;
59
61
  price?: string;
60
62
  priceSuffix?: string;
63
+ priceSuffixBottom?: string;
61
64
  techType?: string;
62
65
  highlighted?: boolean;
63
66
  productCardDescription?: string;
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import ConditionalFormFields from "./conditional-fields";
3
3
  import { FormContext, FormContextType } from "./context";
4
+ import { dynamicRenderingWithGroupings } from "./render";
4
5
  import { DynamicFormItem } from "./types";
5
6
 
6
7
  import { fireEvent, render, screen } from "@testing-library/react";
@@ -46,6 +47,7 @@ const renderConditional = (
46
47
  formData={{}}
47
48
  setFormData={setFormData}
48
49
  submitErrorFields={[]}
50
+ renderConditionalFields={dynamicRenderingWithGroupings}
49
51
  {...props}
50
52
  />
51
53
  </FormContext.Provider>
@@ -99,6 +101,7 @@ describe("ConditionalFormFields", () => {
99
101
  formData={{}}
100
102
  setFormData={jest.fn()}
101
103
  submitErrorFields={[]}
104
+ renderConditionalFields={dynamicRenderingWithGroupings}
102
105
  />
103
106
  </FormContext.Provider>
104
107
  );
@@ -1,12 +1,13 @@
1
1
  "use client";
2
2
 
3
3
  import React, { useContext, useState } from "react";
4
+ import { constants } from "./constants";
4
5
  import { FormContext } from "./context";
5
6
  import { FormFieldTypeMap, labelCreator } from "./field-map";
6
- import { dynamicRenderingWithGroupings } from "./render";
7
- import { ConditionalFieldConfig } from "./types";
7
+ import { ConditionalFieldConfig, DynamicFormItem } from "./types";
8
8
 
9
9
  import { Text } from "@shared/components/text";
10
+ import { cx } from "@shared/utils";
10
11
 
11
12
  type FieldError = { fieldId?: string; errorMessage: string };
12
13
  type FormValues = Record<string, any>;
@@ -15,6 +16,13 @@ export interface ConditionalFormFieldsProps extends ConditionalFieldConfig {
15
16
  formData: FormValues;
16
17
  setFormData: (updater: (prev: FormValues) => FormValues) => void;
17
18
  submitErrorFields: string[];
19
+ renderConditionalFields: (
20
+ isLoading: boolean,
21
+ fields: DynamicFormItem[],
22
+ formData: FormValues,
23
+ setFormData: (updater: (prev: FormValues) => FormValues) => void,
24
+ submitErrorFields: string[]
25
+ ) => React.ReactNode[];
18
26
  }
19
27
 
20
28
  const ConditionalFormFields: React.FC<ConditionalFormFieldsProps> = props => {
@@ -29,6 +37,8 @@ const ConditionalFormFields: React.FC<ConditionalFormFieldsProps> = props => {
29
37
  formFieldsToShow = [],
30
38
  dataSource,
31
39
  note,
40
+ grouped = false,
41
+ renderConditionalFields,
32
42
  } = props;
33
43
 
34
44
  const [showElements, setShowElements] = useState(false);
@@ -37,6 +47,11 @@ const ConditionalFormFields: React.FC<ConditionalFormFieldsProps> = props => {
37
47
  errorMessage: "",
38
48
  });
39
49
 
50
+ const isExigentGroupedField =
51
+ grouped &&
52
+ (fieldId === constants.TARGET_EMAIL ||
53
+ fieldId === constants.IP_PHONE_NUMBER);
54
+
40
55
  const Component = FormFieldTypeMap["Radio"];
41
56
  if (!Component) return null;
42
57
 
@@ -57,7 +72,18 @@ const ConditionalFormFields: React.FC<ConditionalFormFieldsProps> = props => {
57
72
  };
58
73
 
59
74
  return (
60
- <div className="w-full">
75
+ <div
76
+ className={cx(
77
+ "w-full",
78
+ isExigentGroupedField && "bg-bg-surface-secondary px-5 pt-1",
79
+ isExigentGroupedField &&
80
+ fieldId === constants.IP_PHONE_NUMBER &&
81
+ "rounded-t-xl border-x border-b border-border-secondary",
82
+ isExigentGroupedField &&
83
+ fieldId === constants.TARGET_EMAIL &&
84
+ "rounded-b-xl border-x border-t border-border-secondary pb-5"
85
+ )}
86
+ >
61
87
  <div className="mb-4">
62
88
  {labelCreator(displayName, requiredField)}
63
89
  <Component
@@ -88,7 +114,7 @@ const ConditionalFormFields: React.FC<ConditionalFormFieldsProps> = props => {
88
114
  }`}
89
115
  >
90
116
  {showElements &&
91
- dynamicRenderingWithGroupings(
117
+ renderConditionalFields(
92
118
  false,
93
119
  formFieldsToShow,
94
120
  formData,
@@ -22,6 +22,8 @@ export type FormContextType = {
22
22
  fieldComponentOverrides?: Partial<
23
23
  Record<DynamicFormFieldType, React.FC<FieldRenderProps>>
24
24
  >;
25
+ /** App-injected renderer for address-autocomplete Text fields. */
26
+ renderAddressAutocomplete?: React.FC<FieldRenderProps>;
25
27
  /** Optional error reporter (logger) injected by the app. */
26
28
  onError?: (info: {
27
29
  type: string;