@windstream/react-shared-components 0.2.80 → 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 +8 -0
- 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/index.d.ts +3 -3
- 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/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/index.d.ts
CHANGED
|
@@ -67,11 +67,11 @@ type InputFieldProps = Omit<InputHTMLAttributes<HTMLInputElement>, "size"> & {
|
|
|
67
67
|
|
|
68
68
|
declare const InputField: ForwardRefRenderFunction<HTMLInputElement, InputFieldProps>;
|
|
69
69
|
declare const Input: React$1.ForwardRefExoticComponent<Omit<React$1.InputHTMLAttributes<HTMLInputElement>, "size"> & {
|
|
70
|
-
state?: "
|
|
71
|
-
size?: "
|
|
70
|
+
state?: "default" | "focus" | "active" | "hover" | "filled" | "error" | undefined;
|
|
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
|
|
@@ -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
|
|
@@ -84,9 +84,16 @@ jest.mock("@shared/components/material-icon", () => ({
|
|
|
84
84
|
}));
|
|
85
85
|
|
|
86
86
|
jest.mock("@shared/components/next-image", () => ({
|
|
87
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
88
87
|
NextImage: ({ src, alt, onClick, ...rest }: any) => (
|
|
89
|
-
<img
|
|
88
|
+
<img
|
|
89
|
+
data-testid="next-image"
|
|
90
|
+
src={src}
|
|
91
|
+
alt={alt}
|
|
92
|
+
onClick={onClick}
|
|
93
|
+
{...Object.fromEntries(
|
|
94
|
+
Object.entries(rest).filter(([k]) => k.startsWith("data-"))
|
|
95
|
+
)}
|
|
96
|
+
/>
|
|
90
97
|
),
|
|
91
98
|
}));
|
|
92
99
|
|
|
@@ -149,6 +156,37 @@ const mobileMenuProps: NavigationProps = {
|
|
|
149
156
|
describe("Navigation", () => {
|
|
150
157
|
beforeEach(() => jest.clearAllMocks());
|
|
151
158
|
|
|
159
|
+
describe("fieldProps", () => {
|
|
160
|
+
const tag = (field: string) => ({ "data-contentful-field-id": field });
|
|
161
|
+
|
|
162
|
+
it("spreads logo fieldProps onto the desktop and mobile logo links", () => {
|
|
163
|
+
const { container } = render(
|
|
164
|
+
<Navigation {...defaultProps} fieldProps={{ logo: tag("logo") }} />
|
|
165
|
+
);
|
|
166
|
+
const tagged = container.querySelectorAll(
|
|
167
|
+
'a[data-contentful-field-id="logo"]'
|
|
168
|
+
);
|
|
169
|
+
expect(tagged).toHaveLength(2);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("spreads searchBarIcon fieldProps onto the desktop search icon", () => {
|
|
173
|
+
const { container } = render(
|
|
174
|
+
<Navigation
|
|
175
|
+
{...defaultProps}
|
|
176
|
+
fieldProps={{ searchBarIcon: tag("searchBarIcon") }}
|
|
177
|
+
/>
|
|
178
|
+
);
|
|
179
|
+
expect(
|
|
180
|
+
container.querySelector('img[data-contentful-field-id="searchBarIcon"]')
|
|
181
|
+
).not.toBeNull();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("renders no tagging attributes when fieldProps is omitted", () => {
|
|
185
|
+
const { container } = render(<Navigation {...defaultProps} />);
|
|
186
|
+
expect(container.querySelector("[data-contentful-field-id]")).toBeNull();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
152
190
|
describe("enableHamburgerNavigationOnDesktop (desktop hamburger A/B variant)", () => {
|
|
153
191
|
it("keeps current behavior when the flag is off", () => {
|
|
154
192
|
const { container } = render(<Navigation {...defaultProps} />);
|
|
@@ -46,6 +46,7 @@ export const Navigation: React.FC<NavigationProps> = props => {
|
|
|
46
46
|
callNowCtaIcon,
|
|
47
47
|
navigationBackgroundColor,
|
|
48
48
|
navigationLinkColor,
|
|
49
|
+
fieldProps,
|
|
49
50
|
} = props;
|
|
50
51
|
|
|
51
52
|
const logoUrl =
|
|
@@ -94,6 +95,7 @@ export const Navigation: React.FC<NavigationProps> = props => {
|
|
|
94
95
|
width={24}
|
|
95
96
|
height={24}
|
|
96
97
|
className="mr-2 self-center rounded-full"
|
|
98
|
+
{...fieldProps?.callNowCtaIcon}
|
|
97
99
|
/>
|
|
98
100
|
) : null}
|
|
99
101
|
<Text as="span" className="text-secondary-navy500 footnote">
|
|
@@ -159,7 +161,18 @@ export const Navigation: React.FC<NavigationProps> = props => {
|
|
|
159
161
|
<CallButton
|
|
160
162
|
className="border-none"
|
|
161
163
|
href={invocaPhoneNumberLink}
|
|
162
|
-
prefix={
|
|
164
|
+
prefix={
|
|
165
|
+
callNowCtaText ? (
|
|
166
|
+
<span
|
|
167
|
+
className="text-sm font-normal text-text"
|
|
168
|
+
{...fieldProps?.callNowCtaText}
|
|
169
|
+
>
|
|
170
|
+
{callNowCtaText}
|
|
171
|
+
</span>
|
|
172
|
+
) : (
|
|
173
|
+
callNowCtaText
|
|
174
|
+
)
|
|
175
|
+
}
|
|
163
176
|
onClick={onCallClickDesktop}
|
|
164
177
|
>
|
|
165
178
|
<Text className="body3 text-text">
|
|
@@ -210,7 +223,7 @@ export const Navigation: React.FC<NavigationProps> = props => {
|
|
|
210
223
|
>
|
|
211
224
|
<div>
|
|
212
225
|
{logoUrl ? (
|
|
213
|
-
<Link href="/" className="flex">
|
|
226
|
+
<Link href="/" className="flex" {...fieldProps?.logo}>
|
|
214
227
|
<NextImage
|
|
215
228
|
src={logoUrl}
|
|
216
229
|
alt="Kinetic business logo"
|
|
@@ -265,7 +278,7 @@ export const Navigation: React.FC<NavigationProps> = props => {
|
|
|
265
278
|
<div className="mx-auto flex h-14 max-w-120 items-center justify-between">
|
|
266
279
|
<div className="flex h-full">
|
|
267
280
|
{logoUrl ? (
|
|
268
|
-
<Link href="/" className="flex">
|
|
281
|
+
<Link href="/" className="flex" {...fieldProps?.logo}>
|
|
269
282
|
<NextImage
|
|
270
283
|
src={logoUrl}
|
|
271
284
|
alt="Kinetic business logo"
|
|
@@ -302,6 +315,7 @@ export const Navigation: React.FC<NavigationProps> = props => {
|
|
|
302
315
|
: searchBarIcon?.url || ""
|
|
303
316
|
}
|
|
304
317
|
onSearch={onSearch}
|
|
318
|
+
iconFieldProps={fieldProps?.searchBarIcon}
|
|
305
319
|
/>
|
|
306
320
|
) : null}
|
|
307
321
|
{supportNavigationLinks?.map((links, index) => {
|
|
@@ -448,6 +462,7 @@ const MobileMenu = (props: NavigationProps) => {
|
|
|
448
462
|
: props.searchBarIcon?.url || ""
|
|
449
463
|
}
|
|
450
464
|
onSearch={props.onSearch || (() => {})}
|
|
465
|
+
iconFieldProps={props.fieldProps?.searchBarIcon}
|
|
451
466
|
/>
|
|
452
467
|
) : null}
|
|
453
468
|
<div
|
|
@@ -510,8 +525,10 @@ const MobileSearchInput = (props: {
|
|
|
510
525
|
onSearch: (query: string) => void;
|
|
511
526
|
isMenuOpen: boolean;
|
|
512
527
|
searchBarIconURL: string;
|
|
528
|
+
iconFieldProps?: Record<string, unknown>;
|
|
513
529
|
}) => {
|
|
514
|
-
const { closeMenu, onSearch, isMenuOpen, searchBarIconURL } =
|
|
530
|
+
const { closeMenu, onSearch, isMenuOpen, searchBarIconURL, iconFieldProps } =
|
|
531
|
+
props;
|
|
515
532
|
const [searchValue, setSearchValue] = React.useState("");
|
|
516
533
|
const searchInputRef = React.useRef<HTMLInputElement>(null);
|
|
517
534
|
|
|
@@ -541,6 +558,7 @@ const MobileSearchInput = (props: {
|
|
|
541
558
|
alt="Search icon"
|
|
542
559
|
role="button"
|
|
543
560
|
onClick={redirectToSearchResults}
|
|
561
|
+
{...iconFieldProps}
|
|
544
562
|
/>
|
|
545
563
|
<div className="flex-grow">
|
|
546
564
|
<Input
|
|
@@ -561,8 +579,9 @@ const MobileSearchInput = (props: {
|
|
|
561
579
|
const DesktopSearchInput = (props: {
|
|
562
580
|
searchBarIconURL: string;
|
|
563
581
|
onSearch: (query: string) => void;
|
|
582
|
+
iconFieldProps?: Record<string, unknown>;
|
|
564
583
|
}) => {
|
|
565
|
-
const { searchBarIconURL, onSearch } = props;
|
|
584
|
+
const { searchBarIconURL, onSearch, iconFieldProps } = props;
|
|
566
585
|
const [searchValue, setSearchValue] = React.useState("");
|
|
567
586
|
const searchInputRef = React.useRef<HTMLInputElement>(null);
|
|
568
587
|
|
|
@@ -585,6 +604,7 @@ const DesktopSearchInput = (props: {
|
|
|
585
604
|
alt="Search icon"
|
|
586
605
|
role="button"
|
|
587
606
|
onClick={redirectToSearchResults}
|
|
607
|
+
{...iconFieldProps}
|
|
588
608
|
/>
|
|
589
609
|
{/* Input */}
|
|
590
610
|
<Input
|
|
@@ -28,6 +28,7 @@ jest.mock("@shared/contentful/blocks/button", () => ({
|
|
|
28
28
|
className={buttonClassName || linkClassName}
|
|
29
29
|
onClick={onClick}
|
|
30
30
|
aria-expanded={rest["aria-expanded"]}
|
|
31
|
+
data-field-props={JSON.stringify(rest.fieldProps ?? null)}
|
|
31
32
|
>
|
|
32
33
|
{children || rest.buttonLabel}
|
|
33
34
|
</button>
|
|
@@ -35,6 +36,19 @@ jest.mock("@shared/contentful/blocks/button", () => ({
|
|
|
35
36
|
}));
|
|
36
37
|
|
|
37
38
|
describe("MobileLinkGroups", () => {
|
|
39
|
+
it("passes the group's title fieldProps to the toggle button", () => {
|
|
40
|
+
const link = {
|
|
41
|
+
title: "Shop",
|
|
42
|
+
items: { items: [] },
|
|
43
|
+
fieldProps: { title: { "data-contentful-field-id": "title" } },
|
|
44
|
+
};
|
|
45
|
+
render(<MobileLinkGroups link={link as any} />);
|
|
46
|
+
expect(screen.getByTestId("button")).toHaveAttribute(
|
|
47
|
+
"data-field-props",
|
|
48
|
+
JSON.stringify({ "data-contentful-field-id": "title" })
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
38
52
|
it("renders nothing when link is undefined", () => {
|
|
39
53
|
const { container } = render(<MobileLinkGroups link={undefined} />);
|
|
40
54
|
expect(container.innerHTML).toBe("");
|