@windstream/react-shared-components 0.2.79 → 0.2.81

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 (30) hide show
  1. package/dist/contentful/index.d.ts +17 -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 +2 -2
  5. package/dist/contentful/index.js.map +1 -1
  6. package/dist/core.d.ts +1 -1
  7. package/dist/index.d.ts +1 -1
  8. package/package.json +1 -1
  9. package/src/contentful/blocks/address-input-banner/index.test.tsx +13 -0
  10. package/src/contentful/blocks/address-input-banner/index.tsx +5 -1
  11. package/src/contentful/blocks/address-input-banner/types.ts +3 -0
  12. package/src/contentful/blocks/button/index.test.tsx +45 -2
  13. package/src/contentful/blocks/button/index.tsx +13 -1
  14. package/src/contentful/blocks/button/types.ts +2 -0
  15. package/src/contentful/blocks/cards/product-card/index.tsx +11 -3
  16. package/src/contentful/blocks/cards/product-card/types.ts +11 -0
  17. package/src/contentful/blocks/carousel/helper.tsx +1 -0
  18. package/src/contentful/blocks/carousel/index.tsx +8 -1
  19. package/src/contentful/blocks/carousel/types.ts +15 -0
  20. package/src/contentful/blocks/footer/index.test.tsx +65 -2
  21. package/src/contentful/blocks/footer/index.tsx +14 -4
  22. package/src/contentful/blocks/footer/types.ts +4 -0
  23. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.test.tsx +26 -2
  24. package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.tsx +8 -2
  25. package/src/contentful/blocks/navigation/index.test.tsx +40 -2
  26. package/src/contentful/blocks/navigation/index.tsx +25 -5
  27. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.test.tsx +14 -0
  28. package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.tsx +4 -1
  29. package/src/contentful/blocks/navigation/types.ts +5 -0
  30. package/src/types/micro-components.ts +6 -0
package/dist/core.d.ts CHANGED
@@ -71,7 +71,7 @@ declare const Input: React$1.ForwardRefExoticComponent<Omit<React$1.InputHTMLAtt
71
71
  size?: "slim" | "medium" | "large" | undefined;
72
72
  label?: string | undefined;
73
73
  errorText?: string | undefined;
74
- prefixIconName?: "search" | "location_on" | undefined;
74
+ prefixIconName?: "location_on" | "search" | undefined;
75
75
  prefixIconSize?: 20 | 24 | 40 | 48 | undefined;
76
76
  prefixIconFill?: boolean | undefined;
77
77
  suffixIconFill?: boolean | undefined;
package/dist/index.d.ts CHANGED
@@ -71,7 +71,7 @@ declare const Input: React$1.ForwardRefExoticComponent<Omit<React$1.InputHTMLAtt
71
71
  size?: "slim" | "medium" | "large" | undefined;
72
72
  label?: string | undefined;
73
73
  errorText?: string | undefined;
74
- prefixIconName?: "search" | "location_on" | undefined;
74
+ prefixIconName?: "location_on" | "search" | undefined;
75
75
  prefixIconSize?: 20 | 24 | 40 | 48 | undefined;
76
76
  prefixIconFill?: boolean | undefined;
77
77
  suffixIconFill?: boolean | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windstream/react-shared-components",
3
- "version": "0.2.79",
3
+ "version": "0.2.81",
4
4
  "type": "module",
5
5
  "description": "Shared React components for Kinetic applications",
6
6
  "main": "dist/index.js",
@@ -14,6 +14,19 @@ describe("AddressInputBanner", () => {
14
14
  expect(screen.getByText("Check availability")).toBeInTheDocument();
15
15
  });
16
16
 
17
+ it("spreads title fieldProps onto the title element", () => {
18
+ render(
19
+ <AddressInputBanner
20
+ {...defaultProps}
21
+ fieldProps={{ title: { "data-contentful-field-id": "title" } }}
22
+ />
23
+ );
24
+ expect(screen.getByText("Check availability")).toHaveAttribute(
25
+ "data-contentful-field-id",
26
+ "title"
27
+ );
28
+ });
29
+
17
30
  it("returns null when isVisible is false", () => {
18
31
  const { container } = render(
19
32
  <AddressInputBanner {...defaultProps} isVisible={false} />
@@ -19,6 +19,7 @@ export const AddressInputBanner: FC<AddressInputBannerProps> = props => {
19
19
  navHeight,
20
20
  isVisible,
21
21
  renderCheckPlans,
22
+ fieldProps,
22
23
  } = props;
23
24
 
24
25
  const style = variantStyles[variant?.toLowerCase()] || variantStyles.yellow;
@@ -43,7 +44,10 @@ export const AddressInputBanner: FC<AddressInputBannerProps> = props => {
43
44
  "flex flex-col items-center justify-center gap-3 p-[10px] lg:flex-row lg:gap-8 lg:px-6 lg:py-[10px]"
44
45
  )}
45
46
  >
46
- <Text className="label3 w-full text-center md:label1 lg:w-auto lg:text-left">
47
+ <Text
48
+ className="label3 w-full text-center md:label1 lg:w-auto lg:text-left"
49
+ {...fieldProps?.title}
50
+ >
47
51
  {title}
48
52
  </Text>
49
53
  {renderedCheckPlans ? <div>{renderedCheckPlans}</div> : null}
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
 
3
3
  import { ButtonProps } from "@shared/contentful/blocks/button/types";
4
+ import type { FieldProps } from "@shared/types/field-props";
4
5
  import { CheckPlansProps } from "@shared/types/micro-components";
5
6
 
6
7
  export interface AddressInputBannerProps {
@@ -11,4 +12,6 @@ export interface AddressInputBannerProps {
11
12
  navHeight?: number;
12
13
  isVisible?: boolean;
13
14
  renderCheckPlans?: (overrides?: CheckPlansProps) => React.ReactNode;
15
+ /** Extra DOM attribute props, spread onto the respective rendered elements. */
16
+ fieldProps?: FieldProps<"title">;
14
17
  }
@@ -5,6 +5,10 @@ import { beforeEach, describe, expect, it, jest } from "@jest/globals";
5
5
  import "@testing-library/jest-dom/jest-globals";
6
6
  import { fireEvent, render, screen } from "@testing-library/react";
7
7
 
8
+ // Keeps mocked DOM elements free of non-DOM props while still surfacing data-* attributes.
9
+ const dataAttrs = (rest: Record<string, unknown>) =>
10
+ Object.fromEntries(Object.entries(rest).filter(([k]) => k.startsWith("data-")));
11
+
8
12
  jest.mock("@shared/components/brand-button", () => ({
9
13
  BrandButton: ({
10
14
  as,
@@ -37,6 +41,7 @@ jest.mock("@shared/components/brand-button", () => ({
37
41
  data-icon-fill={iconFill}
38
42
  className={buttonClassName}
39
43
  onClick={onClick}
44
+ {...dataAttrs(rest)}
40
45
  >
41
46
  {label} {text}
42
47
  </Tag>
@@ -45,9 +50,13 @@ jest.mock("@shared/components/brand-button", () => ({
45
50
  }));
46
51
 
47
52
  jest.mock("@shared/components/button", () => ({
48
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
49
53
  Button: ({ onClick, className, children, ...rest }: any) => (
50
- <button data-testid="core-button" className={className} onClick={onClick}>
54
+ <button
55
+ data-testid="core-button"
56
+ className={className}
57
+ onClick={onClick}
58
+ {...dataAttrs(rest)}
59
+ >
51
60
  {children}
52
61
  </button>
53
62
  ),
@@ -63,6 +72,7 @@ jest.mock("@shared/components/link", () => ({
63
72
  className,
64
73
  title,
65
74
  onClick,
75
+ ...rest
66
76
  }: any) => (
67
77
  <a
68
78
  data-testid="link"
@@ -71,6 +81,7 @@ jest.mock("@shared/components/link", () => ({
71
81
  className={className}
72
82
  title={title}
73
83
  onClick={onClick}
84
+ {...dataAttrs(rest)}
74
85
  >
75
86
  {children}
76
87
  </a>
@@ -97,6 +108,38 @@ describe("Button", () => {
97
108
  jest.clearAllMocks();
98
109
  });
99
110
 
111
+ describe("fieldProps", () => {
112
+ const fieldProps = {
113
+ "data-contentful-entry-id": "entry-1",
114
+ "data-contentful-field-id": "title",
115
+ };
116
+
117
+ it.each([
118
+ ["solid", "brand-button"],
119
+ ["text", "link"],
120
+ ["unstyled", "core-button"],
121
+ ] as const)("spreads fieldProps onto the %s variant", (variant, testId) => {
122
+ render(
123
+ <Button
124
+ buttonLabel="Go"
125
+ href="/page"
126
+ showButtonAs={variant}
127
+ fieldProps={fieldProps}
128
+ />
129
+ );
130
+ const el = screen.getByTestId(testId);
131
+ expect(el).toHaveAttribute("data-contentful-entry-id", "entry-1");
132
+ expect(el).toHaveAttribute("data-contentful-field-id", "title");
133
+ });
134
+
135
+ it("renders no tagging attributes when fieldProps is omitted", () => {
136
+ render(<Button buttonLabel="Go" href="/page" showButtonAs="text" />);
137
+ expect(screen.getByTestId("link")).not.toHaveAttribute(
138
+ "data-contentful-entry-id"
139
+ );
140
+ });
141
+ });
142
+
100
143
  describe("showButtonAs='solid' (default)", () => {
101
144
  it("renders BrandButton as anchor when href is provided", () => {
102
145
  render(<Button buttonLabel="Click" href="/page" />);
@@ -50,6 +50,7 @@ export const Button: React.FC<ButtonProps> = props => {
50
50
  preserveQueryParameters,
51
51
  disableAnimation,
52
52
  animationType = DEFAULT_BUTTON_ANIMATION_TYPE,
53
+ fieldProps,
53
54
  ...rest
54
55
  } = props;
55
56
 
@@ -143,7 +144,12 @@ export const Button: React.FC<ButtonProps> = props => {
143
144
  ? buildPreservedQueryHref(href, currentSearch)
144
145
  : href;
145
146
  let CTA = (
146
- <CoreButton onClick={buttonClick} className={buttonClassName} {...rest} />
147
+ <CoreButton
148
+ onClick={buttonClick}
149
+ className={buttonClassName}
150
+ {...rest}
151
+ {...fieldProps}
152
+ />
147
153
  );
148
154
 
149
155
  switch (showButtonAs) {
@@ -166,6 +172,7 @@ export const Button: React.FC<ButtonProps> = props => {
166
172
  iconFill={iconFill}
167
173
  disableAnimation={disableAnimation}
168
174
  animationType={animationType}
175
+ {...fieldProps}
169
176
  />
170
177
  ) : (
171
178
  <BrandButton
@@ -183,6 +190,7 @@ export const Button: React.FC<ButtonProps> = props => {
183
190
  buttonClassName={brandButtonClassName}
184
191
  disableAnimation={disableAnimation}
185
192
  animationType={animationType}
193
+ {...fieldProps}
186
194
  />
187
195
  );
188
196
  break;
@@ -199,6 +207,7 @@ export const Button: React.FC<ButtonProps> = props => {
199
207
  linkClassName
200
208
  )}
201
209
  onClick={linkClick}
210
+ {...fieldProps}
202
211
  >
203
212
  {matIconName ? (
204
213
  <>
@@ -242,6 +251,7 @@ export const Button: React.FC<ButtonProps> = props => {
242
251
  iconFill={iconFill}
243
252
  disableAnimation={disableAnimation}
244
253
  animationType={animationType}
254
+ {...fieldProps}
245
255
  />
246
256
  ) : (
247
257
  <BrandButton
@@ -258,6 +268,7 @@ export const Button: React.FC<ButtonProps> = props => {
258
268
  iconFill={iconFill}
259
269
  disableAnimation={disableAnimation}
260
270
  animationType={animationType}
271
+ {...fieldProps}
261
272
  />
262
273
  );
263
274
  break;
@@ -287,6 +298,7 @@ export const Button: React.FC<ButtonProps> = props => {
287
298
  buttonClassName
288
299
  )}
289
300
  aria-label={buttonLabel}
301
+ {...fieldProps}
290
302
  >
291
303
  {isLeftIcon && matIconName ? (
292
304
  <MaterialIcon
@@ -45,4 +45,6 @@ export type ButtonProps = {
45
45
  iconSize?: Size;
46
46
  disableAnimation?: boolean;
47
47
  animationType?: AnimationType | AnimationType[];
48
+ /** Extra DOM attribute props, spread onto the rendered link/button element. */
49
+ fieldProps?: Record<string, unknown>;
48
50
  };
@@ -56,6 +56,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
56
56
  hasAnyGift = false,
57
57
  twoColumn = false,
58
58
  enableBusinessSettings = false,
59
+ fieldProps,
59
60
  }) => {
60
61
  const [internalExpanded, setInternalExpanded] = useState(false);
61
62
  // Use parent state if provided, otherwise use internal state
@@ -86,7 +87,10 @@ export const ProductCard: React.FC<ProductCardProps> = ({
86
87
  {/* Best Value Banner. Non-best cards render an invisible banner of the
87
88
  same height when a sibling has one, so content stays aligned. */}
88
89
  {bestValue ? (
89
- <div className="label3 rounded-t-[20px] bg-bg-fill-brand-accent px-6 py-2 text-center text-text">
90
+ <div
91
+ {...fieldProps?.bestValueText}
92
+ className="label3 rounded-t-[20px] bg-bg-fill-brand-accent px-6 py-2 text-center text-text"
93
+ >
90
94
  {bestValueText}
91
95
  </div>
92
96
  ) : hasAnyBadge ? (
@@ -116,6 +120,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
116
120
  <div className="flex w-full flex-col md:w-auto">
117
121
  <Text
118
122
  as="h3"
123
+ {...fieldProps?.planName}
119
124
  className={cx(
120
125
  planNameClass,
121
126
  "whitespace-normal",
@@ -127,13 +132,13 @@ export const ProductCard: React.FC<ProductCardProps> = ({
127
132
  {planSubtext ? (
128
133
  <>
129
134
  <br />
130
- {planSubtext}
135
+ <span {...fieldProps?.planSubtext}>{planSubtext}</span>
131
136
  </>
132
137
  ) : null}
133
138
  </Text>
134
139
  </div>
135
140
  <div className="flex flex-col items-start md:items-end">
136
- <div className="flex items-start">
141
+ <div className="flex items-start" {...fieldProps?.price}>
137
142
  <Text
138
143
  as="span"
139
144
  className={cx(
@@ -167,6 +172,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
167
172
  {priceSuffixBottom ? (
168
173
  <Text
169
174
  as="span"
175
+ {...fieldProps?.priceSuffixBottom}
170
176
  className={cx(
171
177
  "micro text-nowrap",
172
178
  isDark ? "text-text-inverse" : "text-text"
@@ -181,6 +187,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
181
187
  {/* Description */}
182
188
  <section data-pc-description={true}>
183
189
  <Text
190
+ {...fieldProps?.description}
184
191
  className={cx(
185
192
  twoColumn ? "footnote md:body2" : "body2",
186
193
  "md:h-[56px]",
@@ -268,6 +275,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
268
275
  >
269
276
  <Text
270
277
  as="h4"
278
+ {...fieldProps?.featuresTitle}
271
279
  className={cx(
272
280
  "label3",
273
281
  isDark ? "text-text-inverse" : "text-text"
@@ -2,6 +2,7 @@ import React, { ReactNode } from "react";
2
2
 
3
3
  import { ButtonProps } from "@shared/contentful/blocks/button/types";
4
4
  import { CheckPlansProps } from "@shared/types/micro-components";
5
+ import type { FieldProps } from "@shared/types/field-props";
5
6
 
6
7
  export interface ProductCardProps {
7
8
  planName: string;
@@ -37,4 +38,14 @@ export interface ProductCardProps {
37
38
  * sizes at every breakpoint. */
38
39
  twoColumn?: boolean;
39
40
  enableBusinessSettings?: boolean;
41
+ /** Extra DOM attribute props, spread onto the respective rendered elements. */
42
+ fieldProps?: FieldProps<
43
+ | "planName"
44
+ | "planSubtext"
45
+ | "price"
46
+ | "priceSuffixBottom"
47
+ | "description"
48
+ | "bestValueText"
49
+ | "featuresTitle"
50
+ >;
40
51
  }
@@ -438,6 +438,7 @@ function ProductCardPanel({
438
438
  theme={item?.highlighted ? "dark" : "light"}
439
439
  bestValue={bestValue}
440
440
  bestValueText={item?.topBadgeText || "best value"}
441
+ fieldProps={item?.fieldProps}
441
442
  featuresTitle={item?.benefitsTitle}
442
443
  features={features}
443
444
  isExpanded={isExpanded}
@@ -25,6 +25,7 @@ export const Carousel: React.FC<CarouselProps> = ({
25
25
  productCardsRef,
26
26
  onProductCardCtaClick,
27
27
  enableBusinessSettings = false,
28
+ fieldProps,
28
29
  }) => {
29
30
  const baseClass = hasProductCards
30
31
  ? "px-3 py-9 md:py-20"
@@ -38,11 +39,16 @@ export const Carousel: React.FC<CarouselProps> = ({
38
39
  <Text
39
40
  as="h2"
40
41
  className="heading2 text-text md:heading1 md:text-center"
42
+ {...fieldProps?.title}
41
43
  >
42
44
  {fields?.title}
43
45
  </Text>
44
46
  {fields?.subTitle && (
45
- <Text as="h3" className="body1 px-4 pt-4 md:text-center">
47
+ <Text
48
+ as="h3"
49
+ className="body1 px-4 pt-4 md:text-center"
50
+ {...fieldProps?.subTitle}
51
+ >
46
52
  {fields?.subTitle}
47
53
  </Text>
48
54
  )}
@@ -85,6 +91,7 @@ export const Carousel: React.FC<CarouselProps> = ({
85
91
  <Text
86
92
  as="div"
87
93
  className="body3 mt-8 px-4 text-center text-text-secondary"
94
+ {...fieldProps?.disclaimerText}
88
95
  >
89
96
  {disclaimerText}
90
97
  </Text>
@@ -1,6 +1,7 @@
1
1
  import React, { ReactNode } from "react";
2
2
 
3
3
  import { CheckPlansProps } from "@shared/types/micro-components";
4
+ import type { FieldProps } from "@shared/types/field-props";
4
5
 
5
6
  export interface carouselFieldsInterface {
6
7
  __typename: "ComponentCarousel";
@@ -85,6 +86,18 @@ export interface ProductCardFields {
85
86
  url: string;
86
87
  [key: string]: any;
87
88
  };
89
+ /** Pre-computed per-field Inspector Mode / Live Preview DOM attributes for
90
+ * this item, supplied by the consumer and passed through to the rendered
91
+ * ProductCard unchanged. The library itself stays CMS-agnostic. */
92
+ fieldProps?: FieldProps<
93
+ | "planName"
94
+ | "planSubtext"
95
+ | "price"
96
+ | "priceSuffixBottom"
97
+ | "description"
98
+ | "bestValueText"
99
+ | "featuresTitle"
100
+ >;
88
101
  }
89
102
 
90
103
  export interface CardFields {
@@ -148,6 +161,8 @@ export type CarouselProps = {
148
161
  event: React.MouseEvent<HTMLElement>
149
162
  ) => void;
150
163
  enableBusinessSettings?: boolean;
164
+ /** Extra DOM attribute props, spread onto the respective rendered elements. */
165
+ fieldProps?: FieldProps<"title" | "subTitle" | "disclaimerText">;
151
166
  };
152
167
 
153
168
  export interface TabSwitchProps {
@@ -12,8 +12,15 @@ jest.mock("@shared/components/divider", () => ({
12
12
  }));
13
13
 
14
14
  jest.mock("@shared/components/text", () => ({
15
- Text: ({ as: Tag = "span", children, className }: any) => (
16
- <Tag className={className}>{children}</Tag>
15
+ Text: ({ as: Tag = "span", children, className, ...rest }: any) => (
16
+ <Tag
17
+ className={className}
18
+ {...Object.fromEntries(
19
+ Object.entries(rest).filter(([k]) => k.startsWith("data-"))
20
+ )}
21
+ >
22
+ {children}
23
+ </Tag>
17
24
  ),
18
25
  }));
19
26
 
@@ -74,6 +81,62 @@ const defaultProps: FooterProps = {
74
81
  };
75
82
 
76
83
  describe("Footer", () => {
84
+ describe("fieldProps", () => {
85
+ const tag = (field: string) => ({
86
+ "data-contentful-field-id": field,
87
+ });
88
+
89
+ it("spreads body, terms and copyrights fieldProps onto their elements", () => {
90
+ const { container } = render(
91
+ <Footer
92
+ {...defaultProps}
93
+ fieldProps={{
94
+ body: tag("body"),
95
+ terms: tag("termsText"),
96
+ copyrights: tag("copyrightText"),
97
+ }}
98
+ />
99
+ );
100
+ expect(
101
+ screen.getByLabelText("Promotions and disclaimers")
102
+ ).toHaveAttribute("data-contentful-field-id", "body");
103
+ expect(container.querySelector(".terms-text")).toHaveAttribute(
104
+ "data-contentful-field-id",
105
+ "termsText"
106
+ );
107
+ expect(container.querySelector(".copy-rights")).toHaveAttribute(
108
+ "data-contentful-field-id",
109
+ "copyrightText"
110
+ );
111
+ });
112
+
113
+ it("spreads a link group's title fieldProps onto the group title", () => {
114
+ render(
115
+ <Footer
116
+ {...defaultProps}
117
+ links={[
118
+ { ...mockLinks[0], fieldProps: { title: tag("title") } },
119
+ mockLinks[1],
120
+ ]}
121
+ />
122
+ );
123
+ expect(screen.getByText("Products")).toHaveAttribute(
124
+ "data-contentful-field-id",
125
+ "title"
126
+ );
127
+ expect(screen.getByText("Support")).not.toHaveAttribute(
128
+ "data-contentful-field-id"
129
+ );
130
+ });
131
+
132
+ it("renders no tagging attributes when fieldProps is omitted", () => {
133
+ const { container } = render(<Footer {...defaultProps} />);
134
+ expect(
135
+ container.querySelector("[data-contentful-field-id]")
136
+ ).toBeNull();
137
+ });
138
+ });
139
+
77
140
  describe("Rendering", () => {
78
141
  it("renders with all props", () => {
79
142
  render(<Footer {...defaultProps} />);
@@ -13,6 +13,7 @@ export const Footer: React.FC<FooterProps> = ({
13
13
  terms,
14
14
  maxWidth = true,
15
15
  onClick,
16
+ fieldProps,
16
17
  }) => {
17
18
  function footerClick(
18
19
  event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>
@@ -23,7 +24,11 @@ export const Footer: React.FC<FooterProps> = ({
23
24
  return (
24
25
  <div className="component-container bg-bg-inverse text-white">
25
26
  <div className={` ${maxWidth ? "mx-auto max-w-120" : ""} px-5 py-8`}>
26
- <aside className="break-words" aria-label="Promotions and disclaimers">
27
+ <aside
28
+ className="break-words"
29
+ aria-label="Promotions and disclaimers"
30
+ {...fieldProps?.body}
31
+ >
27
32
  {body}
28
33
  </aside>
29
34
  <Divider className="my-8 border-border-inverse lg:my-16" />
@@ -36,7 +41,7 @@ export const Footer: React.FC<FooterProps> = ({
36
41
  className="site-links-group"
37
42
  key={`link-group-${index}`}
38
43
  >
39
- <Text as="div" className="label3">
44
+ <Text as="div" className="label3" {...link?.fieldProps?.title}>
40
45
  {link?.title}
41
46
  </Text>
42
47
  <ul className="flex flex-col">
@@ -61,10 +66,15 @@ export const Footer: React.FC<FooterProps> = ({
61
66
  <div className="py-8">
62
67
  <Divider className="my-8 hidden border-border-inverse md:my-8 md:block" />
63
68
 
64
- <p className="terms-text footnote text-white">
69
+ <p className="terms-text footnote text-white" {...fieldProps?.terms}>
65
70
  &copy; {new Date().getFullYear()} {terms}
66
71
  </p>
67
- <div className="copy-rights whitespace-pre-line">{copyrights}</div>
72
+ <div
73
+ className="copy-rights whitespace-pre-line"
74
+ {...fieldProps?.copyrights}
75
+ >
76
+ {copyrights}
77
+ </div>
68
78
  <nav aria-label="Footer legal">
69
79
  <ul className="bottom-links list-none">
70
80
  {bottomLinks?.map((link: any, index: number) => (
@@ -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 type FooterProps = {
4
6
  body: React.ReactNode;
5
7
  links?: Array<any>;
@@ -10,4 +12,6 @@ export type FooterProps = {
10
12
  onClick?: (
11
13
  event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>
12
14
  ) => void;
15
+ /** Extra DOM attribute props, spread onto the respective rendered elements. */
16
+ fieldProps?: FieldProps<"body" | "terms" | "copyrights">;
13
17
  };
@@ -12,8 +12,15 @@ jest.mock("@shared/components/material-icon", () => ({
12
12
  }));
13
13
 
14
14
  jest.mock("@shared/components/text", () => ({
15
- Text: ({ as: Tag = "span", children, className }: any) => (
16
- <Tag className={className}>{children}</Tag>
15
+ Text: ({ as: Tag = "span", children, className, ...rest }: any) => (
16
+ <Tag
17
+ className={className}
18
+ {...Object.fromEntries(
19
+ Object.entries(rest).filter(([k]) => k.startsWith("data-"))
20
+ )}
21
+ >
22
+ {children}
23
+ </Tag>
17
24
  ),
18
25
  }));
19
26
 
@@ -50,6 +57,23 @@ jest.mock("@shared/utils", () => ({
50
57
  }));
51
58
 
52
59
  describe("DesktopLinkGroups", () => {
60
+ it("spreads the group's title fieldProps onto the title element", () => {
61
+ render(
62
+ <DesktopLinkGroups
63
+ anchorName="test"
64
+ link={{
65
+ title: "Shop",
66
+ items: { items: [] },
67
+ fieldProps: { title: { "data-contentful-field-id": "title" } },
68
+ }}
69
+ />
70
+ );
71
+ expect(screen.getByText("Shop")).toHaveAttribute(
72
+ "data-contentful-field-id",
73
+ "title"
74
+ );
75
+ });
76
+
53
77
  it("renders nothing when link is undefined", () => {
54
78
  const { container } = render(
55
79
  <DesktopLinkGroups anchorName="test" link={undefined} />
@@ -5,12 +5,14 @@ import { Text } from "@shared/components/text";
5
5
  import { Button } from "@shared/contentful/blocks/button";
6
6
  import { ButtonProps as ContentfulButtonProps } from "@shared/contentful/blocks/button/types";
7
7
  import { useOutsideClick } from "@shared/hooks/use-outside-click";
8
+ import type { FieldProps } from "@shared/types/field-props";
8
9
  import { cx } from "@shared/utils";
9
10
 
10
11
  type ComponentButtonGroup = {
11
12
  anchorId?: string | null;
12
13
  title?: string | null;
13
14
  items?: { items?: ContentfulButtonProps[] | null } | null;
15
+ fieldProps?: FieldProps<"title">;
14
16
  };
15
17
 
16
18
  type Link = ContentfulButtonProps | ComponentButtonGroup;
@@ -61,7 +63,7 @@ export const DesktopLinkGroups: React.FC<LinkGroupsProps> = ({
61
63
  }
62
64
 
63
65
  // Group
64
- const { anchorId, title, items } = link;
66
+ const { anchorId, title, items, fieldProps } = link;
65
67
  const subMenu = Array.isArray(items?.items) ? items!.items! : [];
66
68
  const fullAnchorName = `--link-anchor-${anchorName}`;
67
69
 
@@ -81,7 +83,11 @@ export const DesktopLinkGroups: React.FC<LinkGroupsProps> = ({
81
83
  key={anchorId}
82
84
  showButtonAs="unstyled"
83
85
  >
84
- <Text as="span" className="group-hover:underline">
86
+ <Text
87
+ as="span"
88
+ className="group-hover:underline"
89
+ {...fieldProps?.title}
90
+ >
85
91
  {title ?? null}
86
92
  </Text>
87
93
  <MaterialIcon