@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.
- package/dist/contentful/index.d.ts +17 -1
- package/dist/contentful/index.esm.js +2 -2
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +2 -2
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/contentful/blocks/address-input-banner/index.test.tsx +13 -0
- package/src/contentful/blocks/address-input-banner/index.tsx +5 -1
- package/src/contentful/blocks/address-input-banner/types.ts +3 -0
- package/src/contentful/blocks/button/index.test.tsx +45 -2
- package/src/contentful/blocks/button/index.tsx +13 -1
- package/src/contentful/blocks/button/types.ts +2 -0
- package/src/contentful/blocks/cards/product-card/index.tsx +11 -3
- package/src/contentful/blocks/cards/product-card/types.ts +11 -0
- package/src/contentful/blocks/carousel/helper.tsx +1 -0
- package/src/contentful/blocks/carousel/index.tsx +8 -1
- package/src/contentful/blocks/carousel/types.ts +15 -0
- package/src/contentful/blocks/footer/index.test.tsx +65 -2
- package/src/contentful/blocks/footer/index.tsx +14 -4
- package/src/contentful/blocks/footer/types.ts +4 -0
- package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.test.tsx +26 -2
- package/src/contentful/blocks/navigation/desktop-link-groups.tsx/index.tsx +8 -2
- package/src/contentful/blocks/navigation/index.test.tsx +40 -2
- package/src/contentful/blocks/navigation/index.tsx +25 -5
- package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.test.tsx +14 -0
- package/src/contentful/blocks/navigation/mobile-link-groups.tsx/index.tsx +4 -1
- package/src/contentful/blocks/navigation/types.ts +5 -0
- 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?: "
|
|
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?: "
|
|
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
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
© {new Date().getFullYear()} {terms}
|
|
66
71
|
</p>
|
|
67
|
-
<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
|
|
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
|
|
86
|
+
<Text
|
|
87
|
+
as="span"
|
|
88
|
+
className="group-hover:underline"
|
|
89
|
+
{...fieldProps?.title}
|
|
90
|
+
>
|
|
85
91
|
{title ?? null}
|
|
86
92
|
</Text>
|
|
87
93
|
<MaterialIcon
|