@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
@@ -35,5 +35,7 @@ export interface BlogCardProps {
35
35
  asGrid?: boolean;
36
36
  index?: number;
37
37
  /** Per-field DOM attributes supplied by the consumer or parent Callout item. */
38
- fieldProps?: FieldProps<"title" | "description">;
38
+ fieldProps?: FieldProps<
39
+ "title" | "description" | "image" | "category" | "date" | "href"
40
+ >;
39
41
  }
@@ -126,6 +126,49 @@ describe("ProductCard", () => {
126
126
  });
127
127
  });
128
128
 
129
+ describe("Strikethrough price", () => {
130
+ it("does not render a strikethrough when not provided", () => {
131
+ const { container } = render(<ProductCard {...defaultProps} />);
132
+ expect(container.querySelector("s")).toBeNull();
133
+ expect(screen.queryByText(/Regular price/)).toBeNull();
134
+ });
135
+
136
+ it("does not render a strikethrough for an empty/whitespace value", () => {
137
+ const { container } = render(
138
+ <ProductCard {...defaultProps} strikethroughPrice=" " />
139
+ );
140
+ expect(container.querySelector("s")).toBeNull();
141
+ });
142
+
143
+ it("renders the strikethrough price with a leading $ when truthy", () => {
144
+ const { container } = render(
145
+ <ProductCard {...defaultProps} strikethroughPrice="64.99" />
146
+ );
147
+ const strike = container.querySelector("s");
148
+ expect(strike).toHaveTextContent("$64.99");
149
+ expect(screen.getByText(/Regular price/)).toHaveClass("sr-only");
150
+ });
151
+
152
+ it("does not double the $ when the value already has one", () => {
153
+ const { container } = render(
154
+ <ProductCard {...defaultProps} strikethroughPrice="$64.99" />
155
+ );
156
+ expect(container.querySelector("s")?.textContent).toBe("$64.99");
157
+ });
158
+
159
+ it("keeps the price unit rendered alongside the strikethrough", () => {
160
+ render(<ProductCard {...defaultProps} strikethroughPrice="64.99" />);
161
+ expect(screen.getByText("99/mo")).toBeInTheDocument();
162
+ });
163
+
164
+ it("does not render a strikethrough when there is no price", () => {
165
+ const { container } = render(
166
+ <ProductCard {...defaultProps} price="" strikethroughPrice="64.99" />
167
+ );
168
+ expect(container.querySelector("s")).toBeNull();
169
+ });
170
+ });
171
+
129
172
  describe("Business theme", () => {
130
173
  it("uses the semantic brand token for the plan name and lowercases it by default", () => {
131
174
  render(<ProductCard {...defaultProps} />);
@@ -30,11 +30,22 @@ const renderPipeBold = (value: React.ReactNode): React.ReactNode => {
30
30
  );
31
31
  };
32
32
 
33
+ /**
34
+ * Normalizes a strikethrough (regular) price to a single leading `$`, e.g.
35
+ * `"44.99"` and `"$44.99"` both render as `"$44.99"`. Returns `""` when empty.
36
+ */
37
+ const formatStrikethroughPrice = (value?: string | null): string => {
38
+ if (typeof value !== "string") return "";
39
+ const amount = value.trim().replace(/^\$\s*/, "");
40
+ return amount ? `$${amount}` : "";
41
+ };
42
+
33
43
  export const ProductCard: React.FC<ProductCardProps> = ({
34
44
  planName,
35
45
  planSubtext,
36
46
  speed,
37
47
  price,
48
+ strikethroughPrice,
38
49
  priceUnit = "/mo",
39
50
  description,
40
51
  bestValue = false,
@@ -73,6 +84,9 @@ export const ProductCard: React.FC<ProductCardProps> = ({
73
84
 
74
85
  const isDark = theme === "dark";
75
86
 
87
+ const formattedStrikethroughPrice =
88
+ formatStrikethroughPrice(strikethroughPrice);
89
+
76
90
  // Two-column mobile cards use smaller type below `md` and step up to the
77
91
  // standard sizes from `md` up (matches production residential). One-column
78
92
  // stacks keep the standard sizes at every breakpoint.
@@ -160,15 +174,29 @@ export const ProductCard: React.FC<ProductCardProps> = ({
160
174
  >
161
175
  {price.split(".")[0]}
162
176
  </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>
177
+ <div className="flex flex-col gap-1">
178
+ <Text
179
+ as="span"
180
+ className={cx(
181
+ priceUnitClass,
182
+ isDark ? "text-text-inverse" : "text-text"
183
+ )}
184
+ >
185
+ {`${price.split(".")[1]}${priceUnit}`}
186
+ </Text>
187
+ {formattedStrikethroughPrice ? (
188
+ <Text
189
+ as="span"
190
+ className={cx(
191
+ "micro whitespace-nowrap",
192
+ isDark ? "text-text-inverse" : "text-text-info"
193
+ )}
194
+ >
195
+ <span className="sr-only">Regular price </span>
196
+ <s>{formattedStrikethroughPrice}</s>
197
+ </Text>
198
+ ) : null}
199
+ </div>
172
200
  </div>
173
201
  {priceSuffixBottom ? (
174
202
  <Text
@@ -11,6 +11,9 @@ export interface ProductCardProps {
11
11
  techType?: string;
12
12
  ismaxSpeed?: boolean;
13
13
  price?: string;
14
+ /** Regular (pre-promo) price rendered struck through under the price unit.
15
+ * Rendered only when truthy; a leading `$` is added if missing. */
16
+ strikethroughPrice?: string;
14
17
  /** The price unit rendered after the cents, e.g. "/mo" or "/mo*". Defaults
15
18
  * to "/mo". Lets Contentful control a trailing disclaimer star. */
16
19
  priceUnit?: string;
@@ -35,12 +35,14 @@ jest.mock("@shared/contentful/blocks/cards/product-card", () => ({
35
35
  giftBadge,
36
36
  hasAnyGift,
37
37
  price,
38
+ strikethroughPrice,
38
39
  }: any) => (
39
40
  <article
40
41
  data-testid="product-card"
41
42
  data-plan={planName}
42
43
  data-speed={speed}
43
44
  data-price={price}
45
+ data-strikethrough={strikethroughPrice}
44
46
  data-expanded={isExpanded}
45
47
  data-business={enableBusinessSettings ? "true" : "false"}
46
48
  >
@@ -157,6 +159,17 @@ describe("ProductCardCarousel", () => {
157
159
  expect(screen.getAllByTestId("product-card")).toHaveLength(1);
158
160
  });
159
161
 
162
+ it("passes strikethroughPrice through to the ProductCard", () => {
163
+ const fields = createProductFields([
164
+ { ...sampleProduct, strikethroughPrice: "64.99" },
165
+ { ...sampleProduct, sys: { id: "prod-2" } },
166
+ ]);
167
+ render(<ProductCardCarousel fields={fields} />);
168
+ const [withStrike, withoutStrike] = screen.getAllByTestId("product-card");
169
+ expect(withStrike).toHaveAttribute("data-strikethrough", "64.99");
170
+ expect(withoutStrike).not.toHaveAttribute("data-strikethrough");
171
+ });
172
+
160
173
  it("renders non-carousel layout for a single item", () => {
161
174
  const fields = createProductFields([sampleProduct]);
162
175
  const { container } = render(<ProductCardCarousel fields={fields} />);
@@ -431,6 +431,7 @@ function ProductCardPanel({
431
431
  techType={item?.techType || ""}
432
432
  ismaxSpeed={item?.ismaxSpeed || ""}
433
433
  price={formattedPrice}
434
+ strikethroughPrice={item?.strikethroughPrice}
434
435
  priceUnit={priceUnit}
435
436
  description={item?.productCardDescription || ""}
436
437
  giftBadge={rawBadge}
@@ -60,6 +60,8 @@ export interface ProductCardFields {
60
60
  };
61
61
  speed?: string;
62
62
  price?: string;
63
+ /** Regular (pre-promo) price shown struck through on the card when truthy. */
64
+ strikethroughPrice?: string;
63
65
  priceSuffix?: string;
64
66
  priceSuffixBottom?: string;
65
67
  techType?: string;
@@ -83,6 +83,7 @@ const ConditionalFormFields: React.FC<ConditionalFormFieldsProps> = props => {
83
83
  fieldId === constants.TARGET_EMAIL &&
84
84
  "rounded-b-xl border-x border-t border-border-secondary pb-5"
85
85
  )}
86
+ {...props.fieldProps?.field}
86
87
  >
87
88
  <div className="mb-4">
88
89
  {labelCreator(displayName, requiredField)}
@@ -170,6 +170,7 @@ export const FormFieldElement: React.FC<FormFieldElementProps> = props => {
170
170
  : "",
171
171
  fieldClassName?.split("|")[1] || "w-full"
172
172
  )}
173
+ {...props.fieldProps?.field}
173
174
  >
174
175
  {groupingField && !isGrouped
175
176
  ? labelCreator(
@@ -23,6 +23,54 @@ const fields: DynamicFormItem[] = [
23
23
  ];
24
24
 
25
25
  describe("DynamicForm", () => {
26
+ it("tags block fields, individual form fields and the submit region", () => {
27
+ const { container } = render(
28
+ <DynamicForm
29
+ title="Contact us"
30
+ subTitle="Get in touch"
31
+ formHeading="Your details"
32
+ description={<p>Form description</p>}
33
+ disclaimer="Privacy notice"
34
+ fields={[
35
+ {
36
+ ...fields[0],
37
+ fieldProps: {
38
+ field: {
39
+ "data-contentful-entry-id": "form-field",
40
+ "data-contentful-field-id": "firstName",
41
+ },
42
+ },
43
+ },
44
+ ]}
45
+ fieldProps={{
46
+ title: { "data-contentful-field-id": "title" },
47
+ subTitle: { "data-contentful-field-id": "subTitle" },
48
+ formHeading: { "data-contentful-field-id": "formHeading" },
49
+ description: { "data-contentful-field-id": "description" },
50
+ disclaimer: { "data-contentful-field-id": "disclaimer" },
51
+ submit: { "data-contentful-field-id": "submit" },
52
+ }}
53
+ />
54
+ );
55
+ [
56
+ "title",
57
+ "subTitle",
58
+ "formHeading",
59
+ "description",
60
+ "disclaimer",
61
+ "submit",
62
+ ].forEach(field => {
63
+ expect(
64
+ container.querySelector(`[data-contentful-field-id="${field}"]`)
65
+ ).toBeInTheDocument();
66
+ });
67
+ expect(
68
+ container.querySelector(
69
+ '[data-contentful-entry-id="form-field"][data-contentful-field-id="firstName"]'
70
+ )
71
+ ).toBeInTheDocument();
72
+ });
73
+
26
74
  it("renders the title, fields and submit button", () => {
27
75
  render(
28
76
  <DynamicForm title="Contact us" fields={fields} submitLabel="Send" />
@@ -123,6 +123,7 @@ export const DynamicForm: React.FC<DynamicFormProps> = props => {
123
123
  resolveExistingDataSource,
124
124
  renderAddressAutocomplete,
125
125
  renderSubmitButton,
126
+ fieldProps,
126
127
  } = props;
127
128
 
128
129
  const submissionIdRef = useRef<string>(generateSubmissionId());
@@ -501,7 +502,7 @@ export const DynamicForm: React.FC<DynamicFormProps> = props => {
501
502
  className="mx-auto min-h-screen w-full px-4 sm:px-6 lg:px-8"
502
503
  >
503
504
  <div className="max-w-2xl mx-auto py-4 sm:py-6 lg:py-8">
504
- {successMessage}
505
+ <div {...fieldProps?.successMessage}>{successMessage}</div>
505
506
  </div>
506
507
  </div>
507
508
  );
@@ -511,7 +512,7 @@ export const DynamicForm: React.FC<DynamicFormProps> = props => {
511
512
  return (
512
513
  <div className="mx-auto min-h-screen w-full px-4 sm:px-6 lg:px-8">
513
514
  <div className="max-w-2xl mx-auto py-4 sm:py-6 lg:py-8">
514
- {errorNode}
515
+ <div {...fieldProps?.errorMessage}>{errorNode}</div>
515
516
  </div>
516
517
  </div>
517
518
  );
@@ -523,24 +524,35 @@ export const DynamicForm: React.FC<DynamicFormProps> = props => {
523
524
  <div className="pb-8 sm:pb-6 lg:pb-4">
524
525
  <div className="mb-8 lg:mb-16">
525
526
  {title && (
526
- <Text as="h2" className="heading2 text-center">
527
+ <Text
528
+ as="h2"
529
+ className="heading2 text-center"
530
+ {...fieldProps?.title}
531
+ >
527
532
  {title}
528
533
  </Text>
529
534
  )}
530
535
  {subTitle && (
531
- <Text as="h3" className="subheading3 mt-3 text-center">
536
+ <Text
537
+ as="h3"
538
+ className="subheading3 mt-3 text-center"
539
+ {...fieldProps?.subTitle}
540
+ >
532
541
  {subTitle}
533
542
  </Text>
534
543
  )}
535
544
  </div>
536
545
  {description ? (
537
- <div className="text-center">{description}</div>
546
+ <div className="text-center" {...fieldProps?.description}>
547
+ {description}
548
+ </div>
538
549
  ) : null}
539
550
  {formHeading ? (
540
551
  <div className="mb-6 w-full px-2 sm:px-4">
541
552
  <Text
542
553
  as="div"
543
554
  className="body3 mb-2 text-center text-xl sm:mb-4 md:text-left"
555
+ {...fieldProps?.formHeading}
544
556
  >
545
557
  {formHeading}
546
558
  </Text>
@@ -567,7 +579,7 @@ export const DynamicForm: React.FC<DynamicFormProps> = props => {
567
579
  {validationMessage}
568
580
  </Text>
569
581
  )}
570
- <div className="flex justify-center">
582
+ <div className="flex justify-center" {...fieldProps?.submit}>
571
583
  {renderSubmitButton ? (
572
584
  renderSubmitButton({
573
585
  isLoading,
@@ -594,7 +606,7 @@ export const DynamicForm: React.FC<DynamicFormProps> = props => {
594
606
  </FormContext.Provider>
595
607
  </div>
596
608
  {disclaimer && (
597
- <Text as="div" className="footnote m-2">
609
+ <Text as="div" className="footnote m-2" {...fieldProps?.disclaimer}>
598
610
  {disclaimer}
599
611
  </Text>
600
612
  )}
@@ -52,16 +52,22 @@ const renderItem = (
52
52
  const widthClass = item.dividerLength === false ? "w-1/2" : "w-full";
53
53
  return (
54
54
  <div key={key} className="flex w-full justify-center">
55
- <Divider
56
- role="separator"
57
- aria-label="Content divider"
58
- className={widthClass}
59
- style={{ height: `${strokeWidth}px` }}
60
- />
55
+ <div {...item.fieldProps?.field}>
56
+ <Divider
57
+ role="separator"
58
+ aria-label="Content divider"
59
+ className={widthClass}
60
+ style={{ height: `${strokeWidth}px` }}
61
+ />
62
+ </div>
61
63
  </div>
62
64
  );
63
65
  }
64
- return <React.Fragment key={key}>{item.content}</React.Fragment>;
66
+ return (
67
+ <div key={key} {...item.fieldProps?.field}>
68
+ {item.content}
69
+ </div>
70
+ );
65
71
  };
66
72
 
67
73
  export const dynamicRenderingWithGroupings = (
@@ -1,5 +1,7 @@
1
1
  import React from "react";
2
2
 
3
+ import type { FieldProps } from "@shared/types/field-props";
4
+
3
5
  /**
4
6
  * The set of submit actions the DynamicForm understands. Each maps to an
5
7
  * injected async handler provided by the consuming application. The package
@@ -53,6 +55,8 @@ export interface DynamicFormFieldConfig {
53
55
  selectPresetValidationsToApply?: string;
54
56
  /** When true, a Text field renders the app-injected address autocomplete. */
55
57
  enableAddressAutocomplete?: boolean;
58
+ /** Inspector attributes applied to the field's label/control group. */
59
+ fieldProps?: FieldProps<"field">;
56
60
  }
57
61
 
58
62
  export interface ConditionalFieldConfig {
@@ -67,6 +71,8 @@ export interface ConditionalFieldConfig {
67
71
  * Injected by the consuming app so the shared package stays route-agnostic.
68
72
  */
69
73
  grouped?: boolean;
74
+ /** Inspector attributes applied to the conditional field group. */
75
+ fieldProps?: FieldProps<"field">;
70
76
  }
71
77
 
72
78
  export interface DynamicFormDividerConfig {
@@ -81,8 +87,17 @@ export interface DynamicFormDividerConfig {
81
87
  export type DynamicFormItem =
82
88
  | ({ itemType: "formField" } & DynamicFormFieldConfig)
83
89
  | ({ itemType: "conditional" } & ConditionalFieldConfig)
84
- | ({ itemType: "divider"; fieldId?: string } & DynamicFormDividerConfig)
85
- | { itemType: "custom"; fieldId?: string; content: React.ReactNode };
90
+ | ({
91
+ itemType: "divider";
92
+ fieldId?: string;
93
+ fieldProps?: FieldProps<"field">;
94
+ } & DynamicFormDividerConfig)
95
+ | {
96
+ itemType: "custom";
97
+ fieldId?: string;
98
+ content: React.ReactNode;
99
+ fieldProps?: FieldProps<"field">;
100
+ };
86
101
 
87
102
  export interface DynamicFormPayload {
88
103
  submissionId: string;
@@ -250,4 +265,15 @@ export interface DynamicFormProps {
250
265
  submitLabel: string;
251
266
  onSubmit: (event: any) => void;
252
267
  }) => React.ReactNode;
268
+ /** Inspector attributes for block-level copy and CTA fields. */
269
+ fieldProps?: FieldProps<
270
+ | "title"
271
+ | "subTitle"
272
+ | "formHeading"
273
+ | "description"
274
+ | "disclaimer"
275
+ | "successMessage"
276
+ | "errorMessage"
277
+ | "submit"
278
+ >;
253
279
  }
@@ -49,8 +49,10 @@ jest.mock("@shared/components/material-icon", () => ({
49
49
  }));
50
50
 
51
51
  jest.mock("@shared/components/text", () => ({
52
- Text: ({ children, className }: any) => (
53
- <span className={className}>{children}</span>
52
+ Text: ({ children, className, ...props }: any) => (
53
+ <span className={className} {...props}>
54
+ {children}
55
+ </span>
54
56
  ),
55
57
  }));
56
58
 
@@ -71,6 +73,54 @@ jest.mock("@shared/utils", () => ({
71
73
  }));
72
74
 
73
75
  describe("DynamicTabs", () => {
76
+ it("tags block headings and each tab name and content", () => {
77
+ const { container } = render(
78
+ <DynamicTabs
79
+ title="Plans"
80
+ subTitle="Choose a plan"
81
+ fieldProps={{
82
+ title: { "data-contentful-field-id": "title" },
83
+ subTitle: { "data-contentful-field-id": "subTitle" },
84
+ }}
85
+ tabs={{
86
+ items: [
87
+ {
88
+ tabName: "Internet",
89
+ anchorId: "internet",
90
+ tabContent: <div>Internet content</div>,
91
+ fieldProps: {
92
+ tabName: {
93
+ "data-contentful-entry-id": "tab-1",
94
+ "data-contentful-field-id": "tabName",
95
+ },
96
+ tabContent: {
97
+ "data-contentful-entry-id": "tab-1",
98
+ "data-contentful-field-id": "tabContent",
99
+ },
100
+ },
101
+ },
102
+ ],
103
+ }}
104
+ />
105
+ );
106
+ expect(
107
+ container.querySelector('[data-contentful-field-id="title"]')
108
+ ).toBeInTheDocument();
109
+ expect(
110
+ container.querySelector('[data-contentful-field-id="subTitle"]')
111
+ ).toBeInTheDocument();
112
+ expect(
113
+ container.querySelector(
114
+ '[data-contentful-entry-id="tab-1"][data-contentful-field-id="tabName"]'
115
+ )
116
+ ).toBeInTheDocument();
117
+ expect(
118
+ container.querySelector(
119
+ '[data-contentful-entry-id="tab-1"][data-contentful-field-id="tabContent"]'
120
+ )
121
+ ).toBeInTheDocument();
122
+ });
123
+
74
124
  const defaultProps = {
75
125
  tabs: {
76
126
  items: [
@@ -23,6 +23,7 @@ export function DynamicTabs(props: DynamicTabsT) {
23
23
  mobileSidebarTabType = true,
24
24
  dynamic = false,
25
25
  isOrderedList = false,
26
+ fieldProps,
26
27
  } = props;
27
28
 
28
29
  const items = tabs?.items ?? [];
@@ -89,12 +90,18 @@ export function DynamicTabs(props: DynamicTabsT) {
89
90
  {title || subTitle ? (
90
91
  <div className="mb-8 mt-5 flex flex-col gap-5 md:gap-6">
91
92
  {title ? (
92
- <Text className="heading2 px-5 md:heading1 md:text-center">
93
+ <Text
94
+ className="heading2 px-5 md:heading1 md:text-center"
95
+ {...fieldProps?.title}
96
+ >
93
97
  {title}
94
98
  </Text>
95
99
  ) : null}
96
100
  {subTitle ? (
97
- <Text className="subheading3 px-5 md:subheading1 md:text-center">
101
+ <Text
102
+ className="subheading3 px-5 md:subheading1 md:text-center"
103
+ {...fieldProps?.subTitle}
104
+ >
98
105
  {subTitle}
99
106
  </Text>
100
107
  ) : null}
@@ -64,6 +64,7 @@ export function OrderedTabView({
64
64
  {items.map((tab, index) => (
65
65
  <Button
66
66
  showButtonAs="unstyled"
67
+ fieldProps={tab.fieldProps?.tabName}
67
68
  key={index}
68
69
  onClick={() => onTabClick(index, tab.anchorId || "")}
69
70
  buttonClassName={cx(
@@ -73,7 +74,7 @@ export function OrderedTabView({
73
74
  : "body1"
74
75
  )}
75
76
  >
76
- {tab.tabName}
77
+ <span {...tab.fieldProps?.tabName}>{tab.tabName}</span>
77
78
  </Button>
78
79
  ))}
79
80
  </div>
@@ -96,7 +97,10 @@ export function OrderedTabView({
96
97
  )}
97
98
  id={tab.anchorId || `tab-${index}`}
98
99
  >
99
- <div className="px-5 py-4 md:px-10">
100
+ <div
101
+ className="px-5 py-4 md:px-10"
102
+ {...tab.fieldProps?.tabContent}
103
+ >
100
104
  {renderOrderedChildren(tab.tabContent, subitemColCount)}
101
105
  </div>
102
106
  </motion.div>
@@ -129,7 +133,9 @@ export function OrderedAccordionView({
129
133
  index === currentTabIndex ? "bg-bg-surface-hover" : ""
130
134
  )}
131
135
  >
132
- <span className="heading6">{tab.tabName}</span>
136
+ <span className="heading6" {...tab.fieldProps?.tabName}>
137
+ {tab.tabName}
138
+ </span>
133
139
  <MaterialIcon
134
140
  name={
135
141
  index === currentTabIndex
@@ -155,7 +161,10 @@ export function OrderedAccordionView({
155
161
  )}
156
162
  id={tab.anchorId || `tab-${index}`}
157
163
  >
158
- <div className="px-5 py-4 md:px-10">
164
+ <div
165
+ className="px-5 py-4 md:px-10"
166
+ {...tab.fieldProps?.tabContent}
167
+ >
159
168
  {renderOrderedChildren(tab.tabContent, subitemColCount)}
160
169
  </div>
161
170
  </motion.div>
@@ -192,6 +201,7 @@ export function OrderedSidebarView({
192
201
  return (
193
202
  <button
194
203
  key={index}
204
+ {...tab.fieldProps?.tabName}
195
205
  onClick={() => onTabClick(index, tab.anchorId || "")}
196
206
  className={cx(
197
207
  "label1 whitespace-nowrap rounded-lg px-4 py-3 text-left",
@@ -200,7 +210,7 @@ export function OrderedSidebarView({
200
210
  : "text-text"
201
211
  )}
202
212
  >
203
- {tab.tabName}
213
+ <span {...tab.fieldProps?.tabName}>{tab.tabName}</span>
204
214
  </button>
205
215
  );
206
216
  })}
@@ -212,7 +222,7 @@ export function OrderedSidebarView({
212
222
 
213
223
  <div className="w-full">
214
224
  {items.map((tab, index) => (
215
- <div key={index}>
225
+ <div key={index} {...tab.fieldProps?.tabContent}>
216
226
  <AnimatePresence>
217
227
  <motion.div
218
228
  initial={{ opacity: 0, y: 20 }}
@@ -32,14 +32,14 @@ export function TabView({ items, currentTabIndex, onTabClick }: TabViewProps) {
32
32
  : "body1"
33
33
  )}
34
34
  >
35
- {item.tabName}
35
+ <span {...item.fieldProps?.tabName}>{item.tabName}</span>
36
36
  </Button>
37
37
  ))}
38
38
  </div>
39
39
  </div>
40
40
 
41
41
  {items.map((item, index) => (
42
- <div key={index}>
42
+ <div key={index} {...item.fieldProps?.tabContent}>
43
43
  <AnimatePresence>
44
44
  <motion.div
45
45
  initial={{ height: 0, opacity: 0 }}
@@ -76,7 +76,9 @@ export function AccordionView({
76
76
  index === currentTabIndex ? "bg-bg-surface-active" : ""
77
77
  )}
78
78
  >
79
- <span className={"heading6"}>{item.tabName}</span>
79
+ <span className="heading6" {...item.fieldProps?.tabName}>
80
+ {item.tabName}
81
+ </span>
80
82
  <MaterialIcon
81
83
  name={
82
84
  index === currentTabIndex
@@ -88,7 +90,9 @@ export function AccordionView({
88
90
  />
89
91
  </Button>
90
92
  <Collapse open={index === currentTabIndex}>
91
- <div className={cx("px-4 py-4")}>{item.tabContent}</div>
93
+ <div className={cx("px-4 py-4")} {...item.fieldProps?.tabContent}>
94
+ {item.tabContent}
95
+ </div>
92
96
  </Collapse>
93
97
  </div>
94
98
  ))}
@@ -117,7 +121,7 @@ export function SidebarView({
117
121
  : "body1"
118
122
  )}
119
123
  >
120
- {item.tabName}
124
+ <span {...item.fieldProps?.tabName}>{item.tabName}</span>
121
125
  </Button>
122
126
  ))}
123
127
  </div>
@@ -125,7 +129,7 @@ export function SidebarView({
125
129
 
126
130
  <div className="grow">
127
131
  {items.map((item, index) => (
128
- <div key={index}>
132
+ <div key={index} {...item.fieldProps?.tabContent}>
129
133
  <AnimatePresence>
130
134
  <motion.div
131
135
  initial={{ height: 0, opacity: 0 }}