@windstream/react-shared-components 0.2.62 → 0.2.64
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 -1
- package/dist/contentful/index.esm.js +4 -4
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +4 -4
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +4 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +2 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.esm.js +1 -1
- package/dist/utils/index.esm.js.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/select-plan-button/index.tsx +6 -2
- package/src/components/select-plan-button/types.ts +1 -0
- package/src/components/video-link/vimeo-embed.tsx +19 -10
- package/src/contentful/blocks/cards/product-card/index.tsx +50 -24
- package/src/contentful/blocks/cards/product-card/types.ts +7 -0
- package/src/contentful/blocks/carousel/Carousel.stories.tsx +34 -8
- package/src/contentful/blocks/carousel/helper.test.tsx +127 -10
- package/src/contentful/blocks/carousel/helper.tsx +114 -21
- package/src/contentful/blocks/image-promo-bar/index.tsx +2 -2
- package/src/hooks/contentful/use-contentful-rich-text.test.tsx +12 -0
- package/src/hooks/contentful/use-contentful-rich-text.tsx +17 -2
package/package.json
CHANGED
|
@@ -15,6 +15,7 @@ export const SelectPlanButton: React.FC<SelectPlanButtonProps> = ({
|
|
|
15
15
|
cta,
|
|
16
16
|
techType,
|
|
17
17
|
isMax,
|
|
18
|
+
twoColumn = false,
|
|
18
19
|
}) => {
|
|
19
20
|
const [open, setOpen] = useState(false);
|
|
20
21
|
const className = `btn-medium rounded-button pl-15 pr-15 inline-flex gap-2 items-center justify-center outline-none focus:ring-2 focus:ring-offset-2 cursor-pointer transition-colors duration-200 align-top ${isSelected ? " bg-bg-surface text-text focus:ring-bg-fill-surface-inverse" : " bg-bg-fill-brand text-text-brand-on-bg-fill enabled:hover:bg-bg-fill-brand-hover focus:ring-bg-fill-brand"} `;
|
|
@@ -39,15 +40,18 @@ export const SelectPlanButton: React.FC<SelectPlanButtonProps> = ({
|
|
|
39
40
|
<>
|
|
40
41
|
<Button
|
|
41
42
|
onClick={handleSelect}
|
|
42
|
-
className={cx(className,
|
|
43
|
+
className={cx(className, "w-full justify-between px-3 md:px-5")}
|
|
43
44
|
data-track-element-name="speed_plan_select_button"
|
|
44
45
|
data-track-click-text={`Select plan speed ${speed}`}
|
|
45
46
|
data-track-element-clicked="speed_plan_card"
|
|
46
47
|
>
|
|
47
|
-
<span
|
|
48
|
+
<span className={twoColumn ? "label3 md:label1" : "label1"}>
|
|
49
|
+
Select plan
|
|
50
|
+
</span>
|
|
48
51
|
<MaterialIcon
|
|
49
52
|
className={cx(
|
|
50
53
|
iconClassName,
|
|
54
|
+
"!text-[24px] md:!text-[40px]",
|
|
51
55
|
isSelected ? "text-icon" : "text-icon-inverse"
|
|
52
56
|
)}
|
|
53
57
|
name="expand_circle_right"
|
|
@@ -5,18 +5,27 @@ import Player from "@vimeo/player";
|
|
|
5
5
|
import { cx } from "@shared/utils/cx";
|
|
6
6
|
|
|
7
7
|
const extractVimeoIdandHash = (href: string) => {
|
|
8
|
-
// Strip query parameters and fragments
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
cleanHref.match(/vimeo\.com\/(?:.*\/)?(\d{6,})/) ||
|
|
12
|
-
cleanHref.match(/player\.vimeo\.com\/video\/(\d{6,})/);
|
|
8
|
+
// Strip query parameters and fragments without a backtracking-prone regex
|
|
9
|
+
let cleanHref = href.split(/[?#]/)[0];
|
|
10
|
+
while (cleanHref.endsWith("/")) cleanHref = cleanHref.slice(0, -1);
|
|
13
11
|
|
|
14
|
-
const parts = cleanHref.split("/");
|
|
15
|
-
|
|
12
|
+
const parts = cleanHref.split("/").filter(Boolean);
|
|
13
|
+
|
|
14
|
+
// Locate the vimeo domain segment (vimeo.com or player.vimeo.com) manually
|
|
15
|
+
// instead of a backtracking regex like `(?:.*\/)?(\d{6,})`
|
|
16
|
+
const domainIndex = parts.findIndex(
|
|
17
|
+
part => part === "vimeo.com" || part === "player.vimeo.com"
|
|
18
|
+
);
|
|
19
|
+
if (domainIndex === -1) return "";
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
const id = parts.find(
|
|
22
|
+
(part, i) => i > domainIndex && /^\d{6,}$/.test(part)
|
|
23
|
+
);
|
|
24
|
+
if (!id) return "";
|
|
25
|
+
|
|
26
|
+
const hash = parts[parts.length - 1];
|
|
27
|
+
if (id === hash) return id;
|
|
28
|
+
return `${id}/${hash}`;
|
|
20
29
|
};
|
|
21
30
|
|
|
22
31
|
export const getVimeoEmbedId = (link?: string): string => {
|
|
@@ -46,6 +46,8 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
46
46
|
techType,
|
|
47
47
|
ismaxSpeed,
|
|
48
48
|
priceSuffixBottom,
|
|
49
|
+
hasAnyBadge = false,
|
|
50
|
+
twoColumn = false,
|
|
49
51
|
}) => {
|
|
50
52
|
const [internalExpanded, setInternalExpanded] = useState(false);
|
|
51
53
|
// Use parent state if provided, otherwise use internal state
|
|
@@ -62,19 +64,31 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
62
64
|
|
|
63
65
|
const isDark = theme === "dark";
|
|
64
66
|
|
|
67
|
+
// Two-column mobile cards use smaller type below `md` and step up to the
|
|
68
|
+
// standard sizes from `md` up (matches production residential). One-column
|
|
69
|
+
// stacks keep the standard sizes at every breakpoint.
|
|
70
|
+
const planNameClass = twoColumn ? "label3 md:subheading2" : "subheading2";
|
|
71
|
+
const priceIntegerClass = twoColumn
|
|
72
|
+
? "heading2 md:subheading6"
|
|
73
|
+
: "subheading6";
|
|
74
|
+
const priceUnitClass = twoColumn ? "label3 md:label2" : "label2";
|
|
75
|
+
|
|
65
76
|
return (
|
|
66
|
-
<article
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
)}
|
|
71
|
-
>
|
|
72
|
-
{/* Best Value Banner */}
|
|
73
|
-
{bestValue && (
|
|
77
|
+
<article className="relative flex h-full w-full max-w-[392px] flex-col">
|
|
78
|
+
{/* Best Value Banner. Non-best cards render an invisible banner of the
|
|
79
|
+
same height when a sibling has one, so content stays aligned. */}
|
|
80
|
+
{bestValue ? (
|
|
74
81
|
<div className="label3 rounded-t-[20px] bg-bg-fill-brand-accent px-6 py-2 text-center text-text">
|
|
75
82
|
{bestValueText}
|
|
76
83
|
</div>
|
|
77
|
-
)
|
|
84
|
+
) : hasAnyBadge ? (
|
|
85
|
+
<div
|
|
86
|
+
aria-hidden="true"
|
|
87
|
+
className="label3 invisible px-6 py-2 text-center"
|
|
88
|
+
>
|
|
89
|
+
{"\u00A0"}
|
|
90
|
+
</div>
|
|
91
|
+
) : null}
|
|
78
92
|
|
|
79
93
|
{/* Main Card */}
|
|
80
94
|
<div
|
|
@@ -87,12 +101,16 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
87
101
|
)}
|
|
88
102
|
>
|
|
89
103
|
{/* Plan Name & Price */}
|
|
90
|
-
<header
|
|
91
|
-
|
|
104
|
+
<header
|
|
105
|
+
data-pc-header
|
|
106
|
+
className="overflow-none flex flex-wrap items-start justify-between gap-2 md:gap-4"
|
|
107
|
+
>
|
|
108
|
+
<div className="flex w-full flex-col md:w-auto">
|
|
92
109
|
<Text
|
|
93
110
|
as="h3"
|
|
94
111
|
className={cx(
|
|
95
|
-
|
|
112
|
+
planNameClass,
|
|
113
|
+
"whitespace-normal lowercase",
|
|
96
114
|
isDark ? "text-text-accent-on-bg-inverse" : "text-text-brand"
|
|
97
115
|
)}
|
|
98
116
|
>
|
|
@@ -102,18 +120,19 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
102
120
|
as="p"
|
|
103
121
|
className={cx(
|
|
104
122
|
"label3 text-wrap lowercase",
|
|
105
|
-
isDark ? "text-text-inverse" : "text-text"
|
|
123
|
+
isDark ? "text-text-accent-on-bg-inverse" : "text-text-brand"
|
|
106
124
|
)}
|
|
107
125
|
>
|
|
108
126
|
{planSubtext}
|
|
109
127
|
</Text>
|
|
110
128
|
</div>
|
|
111
|
-
<div className="flex flex-col items-end">
|
|
129
|
+
<div className="flex flex-col items-start md:items-end">
|
|
112
130
|
<div className="flex items-start">
|
|
113
131
|
<Text
|
|
114
132
|
as="span"
|
|
115
133
|
className={cx(
|
|
116
|
-
|
|
134
|
+
priceUnitClass,
|
|
135
|
+
"mr-1 mt-[2px]",
|
|
117
136
|
isDark ? "text-text-inverse" : "text-text"
|
|
118
137
|
)}
|
|
119
138
|
>
|
|
@@ -122,7 +141,8 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
122
141
|
<Text
|
|
123
142
|
as="span"
|
|
124
143
|
className={cx(
|
|
125
|
-
|
|
144
|
+
priceIntegerClass,
|
|
145
|
+
"leading-none",
|
|
126
146
|
isDark ? "text-text-inverse" : "text-text"
|
|
127
147
|
)}
|
|
128
148
|
>
|
|
@@ -131,7 +151,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
131
151
|
<Text
|
|
132
152
|
as="span"
|
|
133
153
|
className={cx(
|
|
134
|
-
|
|
154
|
+
priceUnitClass,
|
|
135
155
|
isDark ? "text-text-inverse" : "text-text"
|
|
136
156
|
)}
|
|
137
157
|
>
|
|
@@ -153,10 +173,10 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
153
173
|
</header>
|
|
154
174
|
|
|
155
175
|
{/* Description */}
|
|
156
|
-
<section>
|
|
176
|
+
<section data-pc-description>
|
|
157
177
|
<Text
|
|
158
178
|
className={cx(
|
|
159
|
-
"body2 h-[56px]",
|
|
179
|
+
"footnote md:body2 md:h-[56px]",
|
|
160
180
|
isDark ? "text-text-inverse" : "text-text"
|
|
161
181
|
)}
|
|
162
182
|
>
|
|
@@ -166,7 +186,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
166
186
|
|
|
167
187
|
{/* Gift Badge */}
|
|
168
188
|
{giftBadge?.length ? (
|
|
169
|
-
<div className="min-h-[76px]">
|
|
189
|
+
<div data-pc-gift className="min-h-[76px]">
|
|
170
190
|
<div
|
|
171
191
|
className={cx(
|
|
172
192
|
"flex-col items-center rounded-surface-sm p-2",
|
|
@@ -197,7 +217,12 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
197
217
|
</div>
|
|
198
218
|
</div>
|
|
199
219
|
) : (
|
|
200
|
-
<div
|
|
220
|
+
<div
|
|
221
|
+
data-pc-gift
|
|
222
|
+
data-pc-gift-empty
|
|
223
|
+
className="invisible min-h-[76px] p-2"
|
|
224
|
+
aria-hidden="true"
|
|
225
|
+
>
|
|
201
226
|
<Text
|
|
202
227
|
as="div"
|
|
203
228
|
className={cx(
|
|
@@ -222,6 +247,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
222
247
|
iconSize={40}
|
|
223
248
|
techType={techType}
|
|
224
249
|
isMax={ismaxSpeed}
|
|
250
|
+
twoColumn={twoColumn}
|
|
225
251
|
/>
|
|
226
252
|
{/* Features */}
|
|
227
253
|
{features.length > 0 && (
|
|
@@ -268,12 +294,12 @@ export const ProductCard: React.FC<ProductCardProps> = ({
|
|
|
268
294
|
/>
|
|
269
295
|
</div>
|
|
270
296
|
{innerBadge?.badgeIcon ? (
|
|
271
|
-
<div className="mt-3 flex items-center gap-2 text-
|
|
297
|
+
<div className="mt-3 flex items-center gap-2 text-left">
|
|
272
298
|
<Image
|
|
273
299
|
src={innerBadge.badgeIcon}
|
|
274
300
|
alt="Inner Badge"
|
|
275
|
-
width={
|
|
276
|
-
height={
|
|
301
|
+
width={24}
|
|
302
|
+
height={24}
|
|
277
303
|
/>
|
|
278
304
|
<span
|
|
279
305
|
className={cx(
|
|
@@ -25,4 +25,11 @@ export interface ProductCardProps {
|
|
|
25
25
|
cta?: ButtonProps;
|
|
26
26
|
onCtaClick: () => void;
|
|
27
27
|
renderCheckPlans?: (overrides?: CheckPlansProps) => React.ReactNode;
|
|
28
|
+
/** True when any card in the set has a best-value badge, so non-badge cards
|
|
29
|
+
* reserve the banner height and stay vertically aligned. */
|
|
30
|
+
hasAnyBadge?: boolean;
|
|
31
|
+
/** True when the card renders in the two-column mobile layout, which uses the
|
|
32
|
+
* compact mobile typography below `md`. One-column stacks keep the standard
|
|
33
|
+
* sizes at every breakpoint. */
|
|
34
|
+
twoColumn?: boolean;
|
|
28
35
|
}
|
|
@@ -27,7 +27,11 @@ const sampleProduct = (
|
|
|
27
27
|
id: string,
|
|
28
28
|
planName: string,
|
|
29
29
|
price: string,
|
|
30
|
-
highlighted = false
|
|
30
|
+
highlighted = false,
|
|
31
|
+
productCardDescription = "Reliable internet for your home.",
|
|
32
|
+
giftRewards: { list: { items: Array<{ title: string; icon: string }> } } = {
|
|
33
|
+
list: { items: [] },
|
|
34
|
+
}
|
|
31
35
|
) => ({
|
|
32
36
|
__typename: "ComponentProductCard" as const,
|
|
33
37
|
sys: { id },
|
|
@@ -36,14 +40,14 @@ const sampleProduct = (
|
|
|
36
40
|
priceSuffix: "99/mo",
|
|
37
41
|
techType: "Fiber",
|
|
38
42
|
highlighted,
|
|
39
|
-
productCardDescription
|
|
43
|
+
productCardDescription,
|
|
40
44
|
benefitsTitle: "Plan benefits",
|
|
41
45
|
benefitsExpanded: false,
|
|
42
46
|
cta: { buttonLabel: "Select plan" },
|
|
43
47
|
benefits: {
|
|
44
|
-
items: [{
|
|
48
|
+
items: [{ title: "No data caps" }, { title: "Free installation" }],
|
|
45
49
|
},
|
|
46
|
-
giftRewards
|
|
50
|
+
giftRewards,
|
|
47
51
|
});
|
|
48
52
|
|
|
49
53
|
const twoColumnFields: carouselFieldsInterface = {
|
|
@@ -55,10 +59,32 @@ const twoColumnFields: carouselFieldsInterface = {
|
|
|
55
59
|
backgroundColor: "white",
|
|
56
60
|
items: {
|
|
57
61
|
items: [
|
|
58
|
-
sampleProduct(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
sampleProduct(
|
|
63
|
+
"plan-1",
|
|
64
|
+
"300 Mbps",
|
|
65
|
+
"49",
|
|
66
|
+
false,
|
|
67
|
+
"Reliable support for essentials like email, browsing, and video chat."
|
|
68
|
+
),
|
|
69
|
+
sampleProduct(
|
|
70
|
+
"plan-2",
|
|
71
|
+
"600 Mbps",
|
|
72
|
+
"59",
|
|
73
|
+
true,
|
|
74
|
+
"Ultra-fast speeds for large smart homes.",
|
|
75
|
+
{
|
|
76
|
+
list: {
|
|
77
|
+
items: [
|
|
78
|
+
{
|
|
79
|
+
title: "$100 prepaid Mastercard®",
|
|
80
|
+
icon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Crect width='24' height='24' rx='4' fill='%23ffd200'/%3E%3C/svg%3E",
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
),
|
|
86
|
+
sampleProduct("plan-3", "1 Gig", "69", false, "Good for most day-to-day internet uses including streaming video."),
|
|
87
|
+
sampleProduct("plan-4", "2 Gig", "89", false, "Worry-free internet for the whole home."),
|
|
62
88
|
],
|
|
63
89
|
},
|
|
64
90
|
};
|
|
@@ -152,37 +152,63 @@ describe("ProductCardCarousel", () => {
|
|
|
152
152
|
});
|
|
153
153
|
|
|
154
154
|
it("renders carousel layout for more than 2 items", () => {
|
|
155
|
-
const items = Array.from({ length:
|
|
155
|
+
const items = Array.from({ length: 3 }, (_, i) => ({
|
|
156
156
|
...sampleProduct,
|
|
157
157
|
sys: { id: `prod-${i}` },
|
|
158
158
|
}));
|
|
159
159
|
const fields = createProductFields(items);
|
|
160
160
|
const { container } = render(<ProductCardCarousel fields={fields} />);
|
|
161
|
-
//
|
|
161
|
+
// 3 cards stay a vertical stack on mobile (flex-col for md:hidden)
|
|
162
162
|
expect(container.querySelector(".flex-col")).toBeInTheDocument();
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
it("renders a two-column grid on mobile
|
|
165
|
+
it("renders a two-column grid on mobile when more than 3 cards", () => {
|
|
166
166
|
const items = Array.from({ length: 4 }, (_, i) => ({
|
|
167
167
|
...sampleProduct,
|
|
168
168
|
sys: { id: `prod-${i}` },
|
|
169
169
|
}));
|
|
170
170
|
const fields = createProductFields(items);
|
|
171
|
-
fields.twoColumn = true;
|
|
172
171
|
const { container } = render(<ProductCardCarousel fields={fields} />);
|
|
173
172
|
// Mobile view uses a two-column grid instead of a vertical stack
|
|
174
173
|
expect(container.querySelector(".grid.grid-cols-2")).toBeInTheDocument();
|
|
175
174
|
expect(container.querySelector(".flex-col")).not.toBeInTheDocument();
|
|
176
175
|
});
|
|
177
176
|
|
|
178
|
-
it("
|
|
179
|
-
const
|
|
180
|
-
sampleProduct,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
fields
|
|
177
|
+
it("does not render a two-column grid on mobile for 3 or fewer cards", () => {
|
|
178
|
+
const items = Array.from({ length: 3 }, (_, i) => ({
|
|
179
|
+
...sampleProduct,
|
|
180
|
+
sys: { id: `prod-${i}` },
|
|
181
|
+
}));
|
|
182
|
+
const fields = createProductFields(items);
|
|
183
|
+
const { container } = render(<ProductCardCarousel fields={fields} />);
|
|
184
|
+
expect(
|
|
185
|
+
container.querySelector(".grid.grid-cols-2")
|
|
186
|
+
).not.toBeInTheDocument();
|
|
187
|
+
expect(container.querySelector(".flex-col")).toBeInTheDocument();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("forces a two-column grid on mobile when the CMS twoColumn flag is set, even with 3 or fewer cards", () => {
|
|
191
|
+
const items = Array.from({ length: 3 }, (_, i) => ({
|
|
192
|
+
...sampleProduct,
|
|
193
|
+
sys: { id: `prod-${i}` },
|
|
194
|
+
}));
|
|
195
|
+
const fields = { ...createProductFields(items), twoColumn: true };
|
|
184
196
|
const { container } = render(<ProductCardCarousel fields={fields} />);
|
|
185
197
|
expect(container.querySelector(".grid.grid-cols-2")).toBeInTheDocument();
|
|
198
|
+
expect(container.querySelector(".flex-col")).not.toBeInTheDocument();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("keeps a vertical stack when the CMS twoColumn flag is explicitly false", () => {
|
|
202
|
+
const items = Array.from({ length: 4 }, (_, i) => ({
|
|
203
|
+
...sampleProduct,
|
|
204
|
+
sys: { id: `prod-${i}` },
|
|
205
|
+
}));
|
|
206
|
+
const fields = { ...createProductFields(items), twoColumn: false };
|
|
207
|
+
const { container } = render(<ProductCardCarousel fields={fields} />);
|
|
208
|
+
expect(
|
|
209
|
+
container.querySelector(".grid.grid-cols-2")
|
|
210
|
+
).not.toBeInTheDocument();
|
|
211
|
+
expect(container.querySelector(".flex-col")).toBeInTheDocument();
|
|
186
212
|
});
|
|
187
213
|
|
|
188
214
|
it("parses speed field correctly (planName | planSubtext)", () => {
|
|
@@ -433,6 +459,97 @@ describe("ProductCardCarousel", () => {
|
|
|
433
459
|
const visibleTab = container.querySelector("[style*='display: block']");
|
|
434
460
|
expect(visibleTab).toBeInTheDocument();
|
|
435
461
|
});
|
|
462
|
+
|
|
463
|
+
describe("desktop card height equalization", () => {
|
|
464
|
+
const getDesktopCards = (container: HTMLElement) =>
|
|
465
|
+
Array.from(container.querySelectorAll<HTMLElement>(".flex-shrink-0"));
|
|
466
|
+
|
|
467
|
+
const makeItems = (speeds: string[]) =>
|
|
468
|
+
speeds.map((speed, index) => ({
|
|
469
|
+
...sampleProduct,
|
|
470
|
+
sys: { id: `prod-${index}` },
|
|
471
|
+
speed,
|
|
472
|
+
}));
|
|
473
|
+
|
|
474
|
+
// Drive offsetHeight off each card's plan name so cards can report
|
|
475
|
+
// differing natural heights. jsdom otherwise reports every element as 0.
|
|
476
|
+
const mockCardHeights = (heights: Record<string, number>) => {
|
|
477
|
+
Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
|
|
478
|
+
configurable: true,
|
|
479
|
+
get() {
|
|
480
|
+
if (
|
|
481
|
+
typeof this.className === "string" &&
|
|
482
|
+
this.className.includes("flex-shrink-0")
|
|
483
|
+
) {
|
|
484
|
+
const label = Object.keys(heights).find(key =>
|
|
485
|
+
(this.textContent || "").includes(key)
|
|
486
|
+
);
|
|
487
|
+
return label ? heights[label] : 0;
|
|
488
|
+
}
|
|
489
|
+
return 0;
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
afterEach(() => {
|
|
495
|
+
Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
|
|
496
|
+
configurable: true,
|
|
497
|
+
get: () => 0,
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it("sets every desktop card to the tallest card's height", () => {
|
|
502
|
+
mockCardHeights({ "100 Mbps": 100, "200 Mbps": 200, "300 Mbps": 150 });
|
|
503
|
+
const fields = createProductFields(
|
|
504
|
+
makeItems(["100 Mbps", "200 Mbps", "300 Mbps"])
|
|
505
|
+
);
|
|
506
|
+
const { container } = render(<ProductCardCarousel fields={fields} />);
|
|
507
|
+
|
|
508
|
+
const cards = getDesktopCards(container);
|
|
509
|
+
expect(cards).toHaveLength(3);
|
|
510
|
+
cards.forEach(card => expect(card.style.height).toBe("200px"));
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("recalculates heights when the window is resized", () => {
|
|
514
|
+
const heights: Record<string, number> = {
|
|
515
|
+
"100 Mbps": 100,
|
|
516
|
+
"200 Mbps": 200,
|
|
517
|
+
"300 Mbps": 150,
|
|
518
|
+
};
|
|
519
|
+
mockCardHeights(heights);
|
|
520
|
+
const fields = createProductFields(
|
|
521
|
+
makeItems(["100 Mbps", "200 Mbps", "300 Mbps"])
|
|
522
|
+
);
|
|
523
|
+
const { container } = render(<ProductCardCarousel fields={fields} />);
|
|
524
|
+
|
|
525
|
+
getDesktopCards(container).forEach(card =>
|
|
526
|
+
expect(card.style.height).toBe("200px")
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
// A card grows taller (e.g. late-loading content); resize should
|
|
530
|
+
// re-equalize every card to the new maximum.
|
|
531
|
+
heights["300 Mbps"] = 300;
|
|
532
|
+
fireEvent(window, new Event("resize"));
|
|
533
|
+
|
|
534
|
+
getDesktopCards(container).forEach(card =>
|
|
535
|
+
expect(card.style.height).toBe("300px")
|
|
536
|
+
);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it("removes the resize listener on unmount", () => {
|
|
540
|
+
mockCardHeights({ "100 Mbps": 100, "200 Mbps": 200, "300 Mbps": 150 });
|
|
541
|
+
const removeSpy = jest.spyOn(window, "removeEventListener");
|
|
542
|
+
const fields = createProductFields(
|
|
543
|
+
makeItems(["100 Mbps", "200 Mbps", "300 Mbps"])
|
|
544
|
+
);
|
|
545
|
+
const { unmount } = render(<ProductCardCarousel fields={fields} />);
|
|
546
|
+
|
|
547
|
+
unmount();
|
|
548
|
+
|
|
549
|
+
expect(removeSpy).toHaveBeenCalledWith("resize", expect.any(Function));
|
|
550
|
+
removeSpy.mockRestore();
|
|
551
|
+
});
|
|
552
|
+
});
|
|
436
553
|
});
|
|
437
554
|
|
|
438
555
|
describe("TestimonialCarousel", () => {
|