@windstream/react-shared-components 0.2.53 → 0.2.55
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 +1 -0
- package/dist/contentful/index.esm.js +1 -1
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +1 -1
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.esm.js +2 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/precisely-address-autocomplete/index.tsx +2 -2
- package/src/contentful/blocks/award-marquee/AwardMarquee.stories.tsx +50 -50
- package/src/contentful/blocks/award-marquee/index.test.tsx +86 -86
- package/src/contentful/blocks/award-marquee/index.tsx +110 -110
- package/src/contentful/blocks/award-marquee/types.ts +19 -19
- package/src/contentful/blocks/fiber-form/index.test.tsx +97 -0
- package/src/contentful/blocks/fiber-form/index.tsx +13 -6
- package/src/contentful/blocks/primary-hero/index.test.tsx +28 -3
- package/src/contentful/blocks/primary-hero/index.tsx +31 -10
- package/src/contentful/blocks/primary-hero/types.ts +1 -0
- package/src/styles/globals.css +21 -0
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
import "@testing-library/jest-dom";
|
|
2
|
-
|
|
3
|
-
import React from "react";
|
|
4
|
-
import { AwardMarquee } from "./index";
|
|
5
|
-
import { AwardMarqueeItem } from "./types";
|
|
6
|
-
|
|
7
|
-
import { render, screen } from "@testing-library/react";
|
|
8
|
-
|
|
9
|
-
const buildItems = (count: number): AwardMarqueeItem[] =>
|
|
10
|
-
Array.from({ length: count }, (_, index) => ({
|
|
11
|
-
iconUrl: `https://example.com/icon-${index}.png`,
|
|
12
|
-
iconAlt: `Award ${index}`,
|
|
13
|
-
title: `Award ${index}`,
|
|
14
|
-
description: `Description ${index}`,
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
describe("AwardMarquee", () => {
|
|
18
|
-
it("renders award items synchronously (SSR, no hydration gate)", () => {
|
|
19
|
-
render(<AwardMarquee items={buildItems(2)} />);
|
|
20
|
-
expect(screen.getByText("Award 0")).toBeInTheDocument();
|
|
21
|
-
expect(screen.getByText("Award 1")).toBeInTheDocument();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("renders the icon with its alt text", () => {
|
|
25
|
-
render(<AwardMarquee items={buildItems(1)} />);
|
|
26
|
-
const icon = screen.getByAltText("Award 0") as HTMLImageElement;
|
|
27
|
-
expect(icon).toBeInTheDocument();
|
|
28
|
-
expect(icon.src).toContain("icon-0.png");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("renders a static (non-animated) list when scroll is disabled", () => {
|
|
32
|
-
const { container } = render(
|
|
33
|
-
<AwardMarquee items={buildItems(5)} scroll={false} />
|
|
34
|
-
);
|
|
35
|
-
expect(container.querySelector(".animate-award-marquee")).toBeNull();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("animates at every breakpoint when scroll is enabled with more than 3 items", () => {
|
|
39
|
-
const { container } = render(
|
|
40
|
-
<AwardMarquee items={buildItems(5)} scroll={true} />
|
|
41
|
-
);
|
|
42
|
-
expect(container.querySelector(".animate-award-marquee")).not.toBeNull();
|
|
43
|
-
// No breakpoint split -> no mobile-only wrapper.
|
|
44
|
-
expect(container.querySelector(".sm\\:hidden")).toBeNull();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("renders both mobile (animated) and desktop (static) variants when scroll is enabled with 3 or fewer items", () => {
|
|
48
|
-
const { container } = render(
|
|
49
|
-
<AwardMarquee items={buildItems(2)} scroll={true} />
|
|
50
|
-
);
|
|
51
|
-
// Mobile variant animates and is hidden from >= sm.
|
|
52
|
-
expect(container.querySelector(".sm\\:hidden")).not.toBeNull();
|
|
53
|
-
expect(container.querySelector(".animate-award-marquee")).not.toBeNull();
|
|
54
|
-
// Desktop variant is a static list shown only at >= sm.
|
|
55
|
-
expect(container.querySelector(".hidden.sm\\:block")).not.toBeNull();
|
|
56
|
-
// 2 items duplicated for the animated loop (4) + 2 in the static list.
|
|
57
|
-
expect(screen.getAllByText("Award 0")).toHaveLength(3);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("applies the anchorId to the wrapper in every render branch", () => {
|
|
61
|
-
const staticView = render(
|
|
62
|
-
<AwardMarquee
|
|
63
|
-
items={buildItems(2)}
|
|
64
|
-
scroll={false}
|
|
65
|
-
anchorId="static-anchor"
|
|
66
|
-
/>
|
|
67
|
-
);
|
|
68
|
-
expect(staticView.container.querySelector("#static-anchor")).not.toBeNull();
|
|
69
|
-
|
|
70
|
-
const animatedView = render(
|
|
71
|
-
<AwardMarquee
|
|
72
|
-
items={buildItems(5)}
|
|
73
|
-
scroll={true}
|
|
74
|
-
anchorId="animated-anchor"
|
|
75
|
-
/>
|
|
76
|
-
);
|
|
77
|
-
expect(
|
|
78
|
-
animatedView.container.querySelector("#animated-anchor")
|
|
79
|
-
).not.toBeNull();
|
|
80
|
-
|
|
81
|
-
const splitView = render(
|
|
82
|
-
<AwardMarquee items={buildItems(2)} scroll={true} anchorId="split-anchor" />
|
|
83
|
-
);
|
|
84
|
-
expect(splitView.container.querySelector("#split-anchor")).not.toBeNull();
|
|
85
|
-
});
|
|
86
|
-
});
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { AwardMarquee } from "./index";
|
|
5
|
+
import { AwardMarqueeItem } from "./types";
|
|
6
|
+
|
|
7
|
+
import { render, screen } from "@testing-library/react";
|
|
8
|
+
|
|
9
|
+
const buildItems = (count: number): AwardMarqueeItem[] =>
|
|
10
|
+
Array.from({ length: count }, (_, index) => ({
|
|
11
|
+
iconUrl: `https://example.com/icon-${index}.png`,
|
|
12
|
+
iconAlt: `Award ${index}`,
|
|
13
|
+
title: `Award ${index}`,
|
|
14
|
+
description: `Description ${index}`,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe("AwardMarquee", () => {
|
|
18
|
+
it("renders award items synchronously (SSR, no hydration gate)", () => {
|
|
19
|
+
render(<AwardMarquee items={buildItems(2)} />);
|
|
20
|
+
expect(screen.getByText("Award 0")).toBeInTheDocument();
|
|
21
|
+
expect(screen.getByText("Award 1")).toBeInTheDocument();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("renders the icon with its alt text", () => {
|
|
25
|
+
render(<AwardMarquee items={buildItems(1)} />);
|
|
26
|
+
const icon = screen.getByAltText("Award 0") as HTMLImageElement;
|
|
27
|
+
expect(icon).toBeInTheDocument();
|
|
28
|
+
expect(icon.src).toContain("icon-0.png");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("renders a static (non-animated) list when scroll is disabled", () => {
|
|
32
|
+
const { container } = render(
|
|
33
|
+
<AwardMarquee items={buildItems(5)} scroll={false} />
|
|
34
|
+
);
|
|
35
|
+
expect(container.querySelector(".animate-award-marquee")).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("animates at every breakpoint when scroll is enabled with more than 3 items", () => {
|
|
39
|
+
const { container } = render(
|
|
40
|
+
<AwardMarquee items={buildItems(5)} scroll={true} />
|
|
41
|
+
);
|
|
42
|
+
expect(container.querySelector(".animate-award-marquee")).not.toBeNull();
|
|
43
|
+
// No breakpoint split -> no mobile-only wrapper.
|
|
44
|
+
expect(container.querySelector(".sm\\:hidden")).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("renders both mobile (animated) and desktop (static) variants when scroll is enabled with 3 or fewer items", () => {
|
|
48
|
+
const { container } = render(
|
|
49
|
+
<AwardMarquee items={buildItems(2)} scroll={true} />
|
|
50
|
+
);
|
|
51
|
+
// Mobile variant animates and is hidden from >= sm.
|
|
52
|
+
expect(container.querySelector(".sm\\:hidden")).not.toBeNull();
|
|
53
|
+
expect(container.querySelector(".animate-award-marquee")).not.toBeNull();
|
|
54
|
+
// Desktop variant is a static list shown only at >= sm.
|
|
55
|
+
expect(container.querySelector(".hidden.sm\\:block")).not.toBeNull();
|
|
56
|
+
// 2 items duplicated for the animated loop (4) + 2 in the static list.
|
|
57
|
+
expect(screen.getAllByText("Award 0")).toHaveLength(3);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("applies the anchorId to the wrapper in every render branch", () => {
|
|
61
|
+
const staticView = render(
|
|
62
|
+
<AwardMarquee
|
|
63
|
+
items={buildItems(2)}
|
|
64
|
+
scroll={false}
|
|
65
|
+
anchorId="static-anchor"
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
expect(staticView.container.querySelector("#static-anchor")).not.toBeNull();
|
|
69
|
+
|
|
70
|
+
const animatedView = render(
|
|
71
|
+
<AwardMarquee
|
|
72
|
+
items={buildItems(5)}
|
|
73
|
+
scroll={true}
|
|
74
|
+
anchorId="animated-anchor"
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
expect(
|
|
78
|
+
animatedView.container.querySelector("#animated-anchor")
|
|
79
|
+
).not.toBeNull();
|
|
80
|
+
|
|
81
|
+
const splitView = render(
|
|
82
|
+
<AwardMarquee items={buildItems(2)} scroll={true} anchorId="split-anchor" />
|
|
83
|
+
);
|
|
84
|
+
expect(splitView.container.querySelector("#split-anchor")).not.toBeNull();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import {
|
|
3
|
-
AwardMarqueeBackground,
|
|
4
|
-
AwardMarqueeItem,
|
|
5
|
-
AwardMarqueeProps,
|
|
6
|
-
AwardMarqueeSpeed,
|
|
7
|
-
} from "./types";
|
|
8
|
-
|
|
9
|
-
import { cx } from "@shared/utils";
|
|
10
|
-
|
|
11
|
-
const DURATION_IN_SECONDS: Record<AwardMarqueeSpeed, number> = {
|
|
12
|
-
slow: 30,
|
|
13
|
-
medium: 20,
|
|
14
|
-
fast: 10,
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const BACKGROUND_CLASS: Record<AwardMarqueeBackground, string> = {
|
|
18
|
-
cream: "bg-[#FFFEEF]",
|
|
19
|
-
gray90: "bg-[#FBFBFC]",
|
|
20
|
-
white: "bg-white",
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const renderAwardItem = (
|
|
24
|
-
{ iconUrl, iconAlt, title, description }: AwardMarqueeItem,
|
|
25
|
-
key: React.Key
|
|
26
|
-
) => (
|
|
27
|
-
<div
|
|
28
|
-
key={key}
|
|
29
|
-
className="ml-4 flex w-auto flex-shrink-0 items-center whitespace-nowrap"
|
|
30
|
-
>
|
|
31
|
-
<img src={iconUrl} alt={iconAlt ?? ""} width={200} />
|
|
32
|
-
<div className="w-[250px] text-center">
|
|
33
|
-
<p className="flex justify-center text-center text-body3 font-bold">
|
|
34
|
-
{title}
|
|
35
|
-
</p>
|
|
36
|
-
<p className="flex flex-wrap whitespace-normal text-center text-footnote">
|
|
37
|
-
{description}
|
|
38
|
-
</p>
|
|
39
|
-
</div>
|
|
40
|
-
</div>
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
export const AwardMarquee: React.FC<AwardMarqueeProps> = ({
|
|
44
|
-
anchorId = "Award-Marquee",
|
|
45
|
-
items = [],
|
|
46
|
-
speed = "medium",
|
|
47
|
-
scroll = false,
|
|
48
|
-
backgroundColor = "white",
|
|
49
|
-
className,
|
|
50
|
-
}) => {
|
|
51
|
-
const scrollAvailable = items.length > 3;
|
|
52
|
-
const shouldDuplicate = items.length < 8;
|
|
53
|
-
const duration = DURATION_IN_SECONDS[speed];
|
|
54
|
-
|
|
55
|
-
const animatedMarquee = () => {
|
|
56
|
-
const marqueeItems = shouldDuplicate ? [...items, ...items] : items;
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<div
|
|
60
|
-
className={cx(
|
|
61
|
-
"flex items-center overflow-x-hidden",
|
|
62
|
-
BACKGROUND_CLASS[backgroundColor]
|
|
63
|
-
)}
|
|
64
|
-
>
|
|
65
|
-
<div
|
|
66
|
-
className="flex animate-award-marquee gap-4 px-2"
|
|
67
|
-
style={
|
|
68
|
-
{
|
|
69
|
-
"--award-marquee-duration": `${duration}s`,
|
|
70
|
-
} as React.CSSProperties
|
|
71
|
-
}
|
|
72
|
-
>
|
|
73
|
-
{marqueeItems.map((item, index) => renderAwardItem(item, index))}
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
);
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const staticMarquee = (
|
|
80
|
-
<div className="flex flex-nowrap items-center justify-around overflow-clip">
|
|
81
|
-
{items.map((item, index) => renderAwardItem(item, index))}
|
|
82
|
-
</div>
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
// No scroll: always static at every breakpoint.
|
|
86
|
-
if (!scroll)
|
|
87
|
-
return (
|
|
88
|
-
<div id={anchorId} className={className}>
|
|
89
|
-
{staticMarquee}
|
|
90
|
-
</div>
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
// Scroll with >3 items: animate at every breakpoint.
|
|
94
|
-
if (scrollAvailable)
|
|
95
|
-
return (
|
|
96
|
-
<div id={anchorId} className={className}>
|
|
97
|
-
{animatedMarquee()}
|
|
98
|
-
</div>
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
// Scroll with <=3 items: animate on mobile, static on >= sm (CSS-driven).
|
|
102
|
-
return (
|
|
103
|
-
<div id={anchorId} className={className}>
|
|
104
|
-
<div className="sm:hidden">{animatedMarquee()}</div>
|
|
105
|
-
<div className="hidden sm:block">{staticMarquee}</div>
|
|
106
|
-
</div>
|
|
107
|
-
);
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
export default AwardMarquee;
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
AwardMarqueeBackground,
|
|
4
|
+
AwardMarqueeItem,
|
|
5
|
+
AwardMarqueeProps,
|
|
6
|
+
AwardMarqueeSpeed,
|
|
7
|
+
} from "./types";
|
|
8
|
+
|
|
9
|
+
import { cx } from "@shared/utils";
|
|
10
|
+
|
|
11
|
+
const DURATION_IN_SECONDS: Record<AwardMarqueeSpeed, number> = {
|
|
12
|
+
slow: 30,
|
|
13
|
+
medium: 20,
|
|
14
|
+
fast: 10,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const BACKGROUND_CLASS: Record<AwardMarqueeBackground, string> = {
|
|
18
|
+
cream: "bg-[#FFFEEF]",
|
|
19
|
+
gray90: "bg-[#FBFBFC]",
|
|
20
|
+
white: "bg-white",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const renderAwardItem = (
|
|
24
|
+
{ iconUrl, iconAlt, title, description }: AwardMarqueeItem,
|
|
25
|
+
key: React.Key
|
|
26
|
+
) => (
|
|
27
|
+
<div
|
|
28
|
+
key={key}
|
|
29
|
+
className="ml-4 flex w-auto flex-shrink-0 items-center whitespace-nowrap"
|
|
30
|
+
>
|
|
31
|
+
<img src={iconUrl} alt={iconAlt ?? ""} width={200} />
|
|
32
|
+
<div className="w-[250px] text-center">
|
|
33
|
+
<p className="flex justify-center text-center text-body3 font-bold">
|
|
34
|
+
{title}
|
|
35
|
+
</p>
|
|
36
|
+
<p className="flex flex-wrap whitespace-normal text-center text-footnote">
|
|
37
|
+
{description}
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
export const AwardMarquee: React.FC<AwardMarqueeProps> = ({
|
|
44
|
+
anchorId = "Award-Marquee",
|
|
45
|
+
items = [],
|
|
46
|
+
speed = "medium",
|
|
47
|
+
scroll = false,
|
|
48
|
+
backgroundColor = "white",
|
|
49
|
+
className,
|
|
50
|
+
}) => {
|
|
51
|
+
const scrollAvailable = items.length > 3;
|
|
52
|
+
const shouldDuplicate = items.length < 8;
|
|
53
|
+
const duration = DURATION_IN_SECONDS[speed];
|
|
54
|
+
|
|
55
|
+
const animatedMarquee = () => {
|
|
56
|
+
const marqueeItems = shouldDuplicate ? [...items, ...items] : items;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
className={cx(
|
|
61
|
+
"flex items-center overflow-x-hidden",
|
|
62
|
+
BACKGROUND_CLASS[backgroundColor]
|
|
63
|
+
)}
|
|
64
|
+
>
|
|
65
|
+
<div
|
|
66
|
+
className="flex animate-award-marquee gap-4 px-2"
|
|
67
|
+
style={
|
|
68
|
+
{
|
|
69
|
+
"--award-marquee-duration": `${duration}s`,
|
|
70
|
+
} as React.CSSProperties
|
|
71
|
+
}
|
|
72
|
+
>
|
|
73
|
+
{marqueeItems.map((item, index) => renderAwardItem(item, index))}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const staticMarquee = (
|
|
80
|
+
<div className="flex flex-nowrap items-center justify-around overflow-clip">
|
|
81
|
+
{items.map((item, index) => renderAwardItem(item, index))}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// No scroll: always static at every breakpoint.
|
|
86
|
+
if (!scroll)
|
|
87
|
+
return (
|
|
88
|
+
<div id={anchorId} className={className}>
|
|
89
|
+
{staticMarquee}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Scroll with >3 items: animate at every breakpoint.
|
|
94
|
+
if (scrollAvailable)
|
|
95
|
+
return (
|
|
96
|
+
<div id={anchorId} className={className}>
|
|
97
|
+
{animatedMarquee()}
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Scroll with <=3 items: animate on mobile, static on >= sm (CSS-driven).
|
|
102
|
+
return (
|
|
103
|
+
<div id={anchorId} className={className}>
|
|
104
|
+
<div className="sm:hidden">{animatedMarquee()}</div>
|
|
105
|
+
<div className="hidden sm:block">{staticMarquee}</div>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export default AwardMarquee;
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
export type AwardMarqueeSpeed = "fast" | "medium" | "slow";
|
|
2
|
-
|
|
3
|
-
export type AwardMarqueeBackground = "cream" | "gray90" | "white";
|
|
4
|
-
|
|
5
|
-
export type AwardMarqueeItem = {
|
|
6
|
-
iconUrl: string;
|
|
7
|
-
iconAlt?: string;
|
|
8
|
-
title?: string;
|
|
9
|
-
description?: string;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type AwardMarqueeProps = {
|
|
13
|
-
anchorId?: string;
|
|
14
|
-
items: AwardMarqueeItem[];
|
|
15
|
-
speed?: AwardMarqueeSpeed;
|
|
16
|
-
scroll?: boolean;
|
|
17
|
-
backgroundColor?: AwardMarqueeBackground;
|
|
18
|
-
className?: string;
|
|
19
|
-
};
|
|
1
|
+
export type AwardMarqueeSpeed = "fast" | "medium" | "slow";
|
|
2
|
+
|
|
3
|
+
export type AwardMarqueeBackground = "cream" | "gray90" | "white";
|
|
4
|
+
|
|
5
|
+
export type AwardMarqueeItem = {
|
|
6
|
+
iconUrl: string;
|
|
7
|
+
iconAlt?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type AwardMarqueeProps = {
|
|
13
|
+
anchorId?: string;
|
|
14
|
+
items: AwardMarqueeItem[];
|
|
15
|
+
speed?: AwardMarqueeSpeed;
|
|
16
|
+
scroll?: boolean;
|
|
17
|
+
backgroundColor?: AwardMarqueeBackground;
|
|
18
|
+
className?: string;
|
|
19
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import * as Yup from "yup";
|
|
3
|
+
import { FiberForm } from ".";
|
|
4
|
+
import type { FiberFormValues } from "./types";
|
|
5
|
+
|
|
6
|
+
jest.mock("@vimeo/player", () => jest.fn(), { virtual: true });
|
|
7
|
+
|
|
8
|
+
const initialValues: FiberFormValues = {
|
|
9
|
+
firstName: "",
|
|
10
|
+
lastName: "",
|
|
11
|
+
address: "",
|
|
12
|
+
phone: "",
|
|
13
|
+
email: "",
|
|
14
|
+
unit: "",
|
|
15
|
+
zip: "",
|
|
16
|
+
isManualAddress: false,
|
|
17
|
+
isMarketingCom: false,
|
|
18
|
+
addressToSend: {},
|
|
19
|
+
notes: "",
|
|
20
|
+
referralCode: "",
|
|
21
|
+
hasUnits: false,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const AddressAutocomplete = () => null;
|
|
25
|
+
|
|
26
|
+
const deferred = () => {
|
|
27
|
+
let resolve!: () => void;
|
|
28
|
+
let reject!: (error: Error) => void;
|
|
29
|
+
const promise = new Promise<void>((resolvePromise, rejectPromise) => {
|
|
30
|
+
resolve = resolvePromise;
|
|
31
|
+
reject = rejectPromise;
|
|
32
|
+
});
|
|
33
|
+
return { promise, resolve, reject };
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const renderForm = (onSubmit: (values: FiberFormValues) => Promise<void>) =>
|
|
37
|
+
render(
|
|
38
|
+
<FiberForm
|
|
39
|
+
AddressAutocomplete={AddressAutocomplete}
|
|
40
|
+
initialValues={initialValues}
|
|
41
|
+
inputList={{}}
|
|
42
|
+
formSchema={Yup.object({})}
|
|
43
|
+
onSubmit={onSubmit}
|
|
44
|
+
buttonText="Submit"
|
|
45
|
+
hideLegalCopy={true}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
describe("FiberForm submit loading state", () => {
|
|
50
|
+
it("shows a spinner and disables submit until submission resolves", async () => {
|
|
51
|
+
const submission = deferred();
|
|
52
|
+
const onSubmit = jest.fn(() => submission.promise);
|
|
53
|
+
renderForm(onSubmit);
|
|
54
|
+
const button = screen.getByRole("button", { name: /submit/i });
|
|
55
|
+
|
|
56
|
+
expect(button).toHaveClass("w-[198px]", "max-w-full");
|
|
57
|
+
|
|
58
|
+
fireEvent.click(button);
|
|
59
|
+
|
|
60
|
+
await waitFor(() => expect(button).toBeDisabled());
|
|
61
|
+
expect(button).toHaveAttribute("aria-busy", "true");
|
|
62
|
+
expect(button).toHaveClass("opacity-75");
|
|
63
|
+
expect(screen.getByRole("status")).toBeInTheDocument();
|
|
64
|
+
|
|
65
|
+
fireEvent.click(button);
|
|
66
|
+
expect(onSubmit).toHaveBeenCalledTimes(1);
|
|
67
|
+
|
|
68
|
+
await act(async () => submission.resolve());
|
|
69
|
+
|
|
70
|
+
await waitFor(() => expect(button).toBeEnabled());
|
|
71
|
+
expect(button).toHaveAttribute("aria-busy", "false");
|
|
72
|
+
expect(button).not.toHaveClass("opacity-75");
|
|
73
|
+
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns submit to normal after a handled rejection", async () => {
|
|
77
|
+
const submission = deferred();
|
|
78
|
+
renderForm(async () => {
|
|
79
|
+
try {
|
|
80
|
+
await submission.promise;
|
|
81
|
+
} catch {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const button = screen.getByRole("button", { name: /submit/i });
|
|
86
|
+
|
|
87
|
+
fireEvent.click(button);
|
|
88
|
+
|
|
89
|
+
await waitFor(() => expect(button).toBeDisabled());
|
|
90
|
+
expect(screen.getByRole("status")).toBeInTheDocument();
|
|
91
|
+
|
|
92
|
+
await act(async () => submission.reject(new Error("Request failed")));
|
|
93
|
+
|
|
94
|
+
await waitFor(() => expect(button).toBeEnabled());
|
|
95
|
+
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -5,6 +5,7 @@ import { Button } from "../../../components/button";
|
|
|
5
5
|
import { Input } from "../../../components/input";
|
|
6
6
|
import { MaterialIcon } from "../../../components/material-icon";
|
|
7
7
|
import { Select, type SelectOption } from "../../../components/select";
|
|
8
|
+
import { Spinner } from "../../../components/spinner";
|
|
8
9
|
import { Text } from "../../../components/text";
|
|
9
10
|
import { Tooltip } from "../../../components/tooltip";
|
|
10
11
|
import { cx } from "../../../utils";
|
|
@@ -160,7 +161,7 @@ export const FiberForm: React.FC<FiberFormProps> = ({
|
|
|
160
161
|
setFieldValue,
|
|
161
162
|
values,
|
|
162
163
|
}) => (
|
|
163
|
-
<Form className={cx("w-full", className)}>
|
|
164
|
+
<Form className={cx("fiber-form w-full", className)}>
|
|
164
165
|
<div className="flex flex-col gap-5 sm:flex-row">
|
|
165
166
|
{inputList.firstName && (
|
|
166
167
|
<div className="flex-1">
|
|
@@ -259,7 +260,7 @@ export const FiberForm: React.FC<FiberFormProps> = ({
|
|
|
259
260
|
>
|
|
260
261
|
<button
|
|
261
262
|
type="button"
|
|
262
|
-
className="flex w-full items-center gap-2 px-4 py-3 text-left hover:bg-bg-surface-hover"
|
|
263
|
+
className="body2 flex w-full items-center gap-2 px-4 py-3 text-left hover:bg-bg-surface-hover"
|
|
263
264
|
onClick={() =>
|
|
264
265
|
selectCandidate(candidate, setFieldValue)
|
|
265
266
|
}
|
|
@@ -487,7 +488,7 @@ export const FiberForm: React.FC<FiberFormProps> = ({
|
|
|
487
488
|
</label>
|
|
488
489
|
<Tooltip
|
|
489
490
|
tooltipMsg="By opting in to receive text messages, you will receive automated text message related to service notifications including installations, service changes and updates, billing information, marketing offers from Kinetic including cart reminders. Consent is not a condition of purchase. Reply STOP at any time to unsubscribe. Message frequency varies. Msg & data rates may apply. To contact our Customer Support via email, chat, or phone, visit our Support page below."
|
|
490
|
-
className="ff-NR mt-0"
|
|
491
|
+
className="fiber-form-tooltip ff-NR mt-0"
|
|
491
492
|
>
|
|
492
493
|
<MaterialIcon
|
|
493
494
|
name="info"
|
|
@@ -531,16 +532,22 @@ export const FiberForm: React.FC<FiberFormProps> = ({
|
|
|
531
532
|
))}
|
|
532
533
|
</ul>
|
|
533
534
|
)}
|
|
534
|
-
<div className="mt-8 flex items-center justify-center
|
|
535
|
+
<div className="mt-8 flex items-center justify-center lg:mt-[64px]">
|
|
535
536
|
<Button
|
|
536
537
|
type="submit"
|
|
537
538
|
disabled={isSubmitting}
|
|
538
539
|
className={cx(
|
|
539
|
-
"
|
|
540
|
+
"flex h-16 w-[198px] max-w-full cursor-pointer items-center justify-center overflow-hidden rounded-button border border-brand bg-bg-fill-brand px-[60px] py-4 text-center font-black leading-7 text-text-brand-on-bg-fill no-underline transition-colors duration-300 ease-in-out enabled:hover:border-[var(--color-bg-fill-brand-hover)] enabled:hover:bg-bg-fill-brand-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-border-brand disabled:cursor-not-allowed",
|
|
541
|
+
isSubmitting && "opacity-75",
|
|
540
542
|
buttonClassName
|
|
541
543
|
)}
|
|
542
544
|
>
|
|
543
|
-
|
|
545
|
+
<span className="relative flex w-full items-center justify-center">
|
|
546
|
+
{isSubmitting && (
|
|
547
|
+
<Spinner size="small" className="!h-5 !w-5" />
|
|
548
|
+
)}
|
|
549
|
+
<span>{buttonText}</span>
|
|
550
|
+
</span>
|
|
544
551
|
</Button>
|
|
545
552
|
</div>
|
|
546
553
|
</Form>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { PrimaryHero } from "./index";
|
|
3
3
|
|
|
4
|
-
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
5
5
|
|
|
6
6
|
jest.mock("@shared/components/checklist", () => ({
|
|
7
7
|
Checklist: ({ items }: any) => (
|
|
@@ -14,8 +14,14 @@ jest.mock("@shared/components/checklist", () => ({
|
|
|
14
14
|
}));
|
|
15
15
|
|
|
16
16
|
jest.mock("@shared/components/next-image", () => ({
|
|
17
|
-
NextImage: ({ src, alt, className }: any) => (
|
|
18
|
-
<img
|
|
17
|
+
NextImage: ({ src, alt, className, onClick }: any) => (
|
|
18
|
+
<img
|
|
19
|
+
data-testid="next-image"
|
|
20
|
+
src={src}
|
|
21
|
+
alt={alt}
|
|
22
|
+
className={className}
|
|
23
|
+
onClick={onClick}
|
|
24
|
+
/>
|
|
19
25
|
),
|
|
20
26
|
}));
|
|
21
27
|
|
|
@@ -347,6 +353,25 @@ describe("PrimaryHero", () => {
|
|
|
347
353
|
expect(badges).toHaveLength(2);
|
|
348
354
|
});
|
|
349
355
|
|
|
356
|
+
it("adds cursor class and triggers onImageClick for badge image", () => {
|
|
357
|
+
const onImageClick = jest.fn();
|
|
358
|
+
|
|
359
|
+
render(
|
|
360
|
+
<PrimaryHero
|
|
361
|
+
{...baseProps}
|
|
362
|
+
badgeImage="https://img.test/badge.png"
|
|
363
|
+
onImageClick={onImageClick}
|
|
364
|
+
/>
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const badges = screen.getAllByAltText("Badge");
|
|
368
|
+
expect(badges).toHaveLength(2);
|
|
369
|
+
badges.forEach((badge) => expect(badge).toHaveClass("cursor-pointer"));
|
|
370
|
+
|
|
371
|
+
fireEvent.click(badges[0]);
|
|
372
|
+
expect(onImageClick).toHaveBeenCalledTimes(1);
|
|
373
|
+
});
|
|
374
|
+
|
|
350
375
|
it("does not render badge image when not provided", () => {
|
|
351
376
|
render(<PrimaryHero {...baseProps} />);
|
|
352
377
|
expect(screen.queryByAltText("Badge")).not.toBeInTheDocument();
|