@windstream/react-shared-components 0.2.74 → 0.2.75
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 +2 -2
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/contentful/blocks/cards/product-card/index.test.tsx +31 -0
- package/src/contentful/blocks/cards/product-card/index.tsx +9 -8
- package/src/contentful/blocks/cards/product-card/types.ts +1 -0
- package/src/contentful/blocks/carousel/Carousel.stories.tsx +303 -3
- package/src/contentful/blocks/carousel/helper.test.tsx +265 -3
- package/src/contentful/blocks/carousel/helper.tsx +165 -98
- package/src/contentful/blocks/ordered-list/index.test.tsx +163 -0
- package/src/contentful/blocks/primary-hero/index.tsx +1 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
1
2
|
import { Carousel } from "./index";
|
|
2
3
|
import type { carouselFieldsInterface } from "./types";
|
|
3
4
|
|
|
@@ -31,7 +32,8 @@ const sampleProduct = (
|
|
|
31
32
|
productCardDescription = "Reliable internet for your home.",
|
|
32
33
|
giftRewards: { list: { items: Array<{ title: string; icon: string }> } } = {
|
|
33
34
|
list: { items: [] },
|
|
34
|
-
}
|
|
35
|
+
},
|
|
36
|
+
productCategory?: string
|
|
35
37
|
) => ({
|
|
36
38
|
__typename: "ComponentProductCard" as const,
|
|
37
39
|
sys: { id },
|
|
@@ -41,6 +43,7 @@ const sampleProduct = (
|
|
|
41
43
|
techType: "Fiber",
|
|
42
44
|
highlighted,
|
|
43
45
|
productCardDescription,
|
|
46
|
+
productCategory,
|
|
44
47
|
benefitsTitle: "Plan benefits",
|
|
45
48
|
benefitsExpanded: false,
|
|
46
49
|
cta: { buttonLabel: "Select plan" },
|
|
@@ -83,8 +86,20 @@ const twoColumnFields: carouselFieldsInterface = {
|
|
|
83
86
|
},
|
|
84
87
|
}
|
|
85
88
|
),
|
|
86
|
-
sampleProduct(
|
|
87
|
-
|
|
89
|
+
sampleProduct(
|
|
90
|
+
"plan-3",
|
|
91
|
+
"1 Gig",
|
|
92
|
+
"69",
|
|
93
|
+
false,
|
|
94
|
+
"Good for most day-to-day internet uses including streaming video."
|
|
95
|
+
),
|
|
96
|
+
sampleProduct(
|
|
97
|
+
"plan-4",
|
|
98
|
+
"2 Gig",
|
|
99
|
+
"89",
|
|
100
|
+
false,
|
|
101
|
+
"Worry-free internet for the whole home."
|
|
102
|
+
),
|
|
88
103
|
],
|
|
89
104
|
},
|
|
90
105
|
};
|
|
@@ -106,3 +121,288 @@ export const TwoColumnProductCards: Story = {
|
|
|
106
121
|
},
|
|
107
122
|
};
|
|
108
123
|
|
|
124
|
+
const mastercardGift = (amount: string) => ({
|
|
125
|
+
list: {
|
|
126
|
+
items: [
|
|
127
|
+
{
|
|
128
|
+
title: `|$${amount}| prepaid Mastercard®`,
|
|
129
|
+
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",
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const tabbedFields: carouselFieldsInterface = {
|
|
136
|
+
__typename: "ComponentCarousel",
|
|
137
|
+
sys: { id: "carousel-tabbed-gift" },
|
|
138
|
+
title: "fiber internet plans that fit your way of working",
|
|
139
|
+
twoColumn: false,
|
|
140
|
+
backgroundColor: "white",
|
|
141
|
+
items: {
|
|
142
|
+
items: [
|
|
143
|
+
// "Business Ready" tab — cards carry a gift badge, so the gift slot is
|
|
144
|
+
// reserved and the CTAs stay aligned.
|
|
145
|
+
sampleProduct(
|
|
146
|
+
"br-1",
|
|
147
|
+
"fiber 1 gig",
|
|
148
|
+
"99",
|
|
149
|
+
true,
|
|
150
|
+
"Fast, secure connectivity solution for growing teams.",
|
|
151
|
+
mastercardGift("100"),
|
|
152
|
+
"Business Ready"
|
|
153
|
+
),
|
|
154
|
+
sampleProduct(
|
|
155
|
+
"br-2",
|
|
156
|
+
"fiber 2 gig",
|
|
157
|
+
"129",
|
|
158
|
+
false,
|
|
159
|
+
"Ultra-fast connectivity solution for high-traffic businesses.",
|
|
160
|
+
mastercardGift("200"),
|
|
161
|
+
"Business Ready"
|
|
162
|
+
),
|
|
163
|
+
// "Internet Only" tab — no gift badges, so the gift slot must collapse and
|
|
164
|
+
// the CTA should sit flush with the description (the fix under test).
|
|
165
|
+
sampleProduct(
|
|
166
|
+
"io-1",
|
|
167
|
+
"fiber 300 mbps",
|
|
168
|
+
"49",
|
|
169
|
+
true,
|
|
170
|
+
"Ideal for guest Wi-Fi, multiple POS devices, video uploads and streaming.",
|
|
171
|
+
{ list: { items: [] } },
|
|
172
|
+
"Internet Only"
|
|
173
|
+
),
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const TabbedTemplate = () => {
|
|
179
|
+
const [activeTab, setActiveTab] = useState("Business Ready");
|
|
180
|
+
return (
|
|
181
|
+
<div style={{ width: "100%", maxWidth: 960 }}>
|
|
182
|
+
<Carousel
|
|
183
|
+
fields={tabbedFields}
|
|
184
|
+
hasProductCards={true}
|
|
185
|
+
hasTestimonialCards={false}
|
|
186
|
+
disclaimerText={
|
|
187
|
+
"Price includes a $5.00/mo credit with AutoPay, option to disable at the next step."
|
|
188
|
+
}
|
|
189
|
+
backgroundColor="bg-white"
|
|
190
|
+
showSwitch={true}
|
|
191
|
+
activeTab={activeTab}
|
|
192
|
+
setActiveTab={setActiveTab}
|
|
193
|
+
tabs={["Business Ready", "Internet Only"]}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Reproduces the reported gap: switching to the "Internet Only" tab (whose cards
|
|
201
|
+
* have no gift badge) must collapse the gift slot so the Select plan CTA sits
|
|
202
|
+
* flush with the description, while the "Business Ready" tab (gift badges
|
|
203
|
+
* present) keeps the slot reserved so CTAs stay aligned.
|
|
204
|
+
*/
|
|
205
|
+
export const TabbedGiftAlignment: Story = {
|
|
206
|
+
parameters: { layout: "fullscreen" },
|
|
207
|
+
render: () => <TabbedTemplate />,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const mixedGiftFields: carouselFieldsInterface = {
|
|
211
|
+
__typename: "ComponentCarousel",
|
|
212
|
+
sys: { id: "carousel-mixed-gift" },
|
|
213
|
+
title: "Mixed gift / no-gift cards",
|
|
214
|
+
subTitle: "The no-gift card reserves the gift height so both CTAs align.",
|
|
215
|
+
twoColumn: false,
|
|
216
|
+
backgroundColor: "white",
|
|
217
|
+
items: {
|
|
218
|
+
items: [
|
|
219
|
+
sampleProduct(
|
|
220
|
+
"mixed-1",
|
|
221
|
+
"fiber 1 gig",
|
|
222
|
+
"99",
|
|
223
|
+
true,
|
|
224
|
+
"Fast, secure connectivity solution for growing teams.",
|
|
225
|
+
mastercardGift("100")
|
|
226
|
+
),
|
|
227
|
+
sampleProduct(
|
|
228
|
+
"mixed-2",
|
|
229
|
+
"fiber 2 gig",
|
|
230
|
+
"129",
|
|
231
|
+
false,
|
|
232
|
+
"Ultra-fast connectivity solution for high-traffic businesses."
|
|
233
|
+
),
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* One card has a single gift badge, the other has none. The no-gift card must
|
|
240
|
+
* reserve the gift-slot height so both Select plan CTAs sit at the same height,
|
|
241
|
+
* while the gift card's CTA sits directly below its badge (no over-reservation).
|
|
242
|
+
*/
|
|
243
|
+
export const MixedGiftAlignment: Story = {
|
|
244
|
+
parameters: { layout: "fullscreen" },
|
|
245
|
+
args: {
|
|
246
|
+
fields: mixedGiftFields,
|
|
247
|
+
hasProductCards: true,
|
|
248
|
+
hasTestimonialCards: false,
|
|
249
|
+
disclaimerText: null,
|
|
250
|
+
backgroundColor: "bg-white",
|
|
251
|
+
activeTab: "",
|
|
252
|
+
setActiveTab: () => {},
|
|
253
|
+
tabs: [],
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const tabbedMixedFields: carouselFieldsInterface = {
|
|
258
|
+
__typename: "ComponentCarousel",
|
|
259
|
+
sys: { id: "carousel-tabbed-mixed-gift" },
|
|
260
|
+
title: "fiber internet plans that fit your way of working",
|
|
261
|
+
twoColumn: false,
|
|
262
|
+
backgroundColor: "white",
|
|
263
|
+
items: {
|
|
264
|
+
items: [
|
|
265
|
+
// "Business Ready" tab — one card has a gift badge, one does not. The
|
|
266
|
+
// no-gift card must reserve the gift height so both CTAs align.
|
|
267
|
+
sampleProduct(
|
|
268
|
+
"tm-br-1",
|
|
269
|
+
"fiber 1 gig",
|
|
270
|
+
"99",
|
|
271
|
+
true,
|
|
272
|
+
"Fast, secure connectivity solution for growing teams.",
|
|
273
|
+
mastercardGift("100"),
|
|
274
|
+
"Business Ready"
|
|
275
|
+
),
|
|
276
|
+
sampleProduct(
|
|
277
|
+
"tm-br-2",
|
|
278
|
+
"fiber 2 gig",
|
|
279
|
+
"129",
|
|
280
|
+
false,
|
|
281
|
+
"Ultra-fast connectivity solution for high-traffic businesses.",
|
|
282
|
+
{ list: { items: [] } },
|
|
283
|
+
"Business Ready"
|
|
284
|
+
),
|
|
285
|
+
// "Internet Only" tab — neither card has a gift badge, so both gift slots
|
|
286
|
+
// collapse and both CTAs sit flush with the description (recalculated for
|
|
287
|
+
// this tab on switch).
|
|
288
|
+
sampleProduct(
|
|
289
|
+
"tm-io-1",
|
|
290
|
+
"fiber 300 mbps",
|
|
291
|
+
"49",
|
|
292
|
+
false,
|
|
293
|
+
"Ideal for guest Wi-Fi, multiple POS devices, video uploads and streaming.",
|
|
294
|
+
{ list: { items: [] } },
|
|
295
|
+
"Internet Only"
|
|
296
|
+
),
|
|
297
|
+
sampleProduct(
|
|
298
|
+
"tm-io-2",
|
|
299
|
+
"fiber 500 mbps",
|
|
300
|
+
"59",
|
|
301
|
+
true,
|
|
302
|
+
"Great for busy storefronts with several connected devices.",
|
|
303
|
+
{ list: { items: [] } },
|
|
304
|
+
"Internet Only"
|
|
305
|
+
),
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const TabbedMixedTemplate = () => {
|
|
311
|
+
const [activeTab, setActiveTab] = useState("Business Ready");
|
|
312
|
+
return (
|
|
313
|
+
<div style={{ width: "100%", maxWidth: 960 }}>
|
|
314
|
+
<Carousel
|
|
315
|
+
fields={tabbedMixedFields}
|
|
316
|
+
hasProductCards={true}
|
|
317
|
+
hasTestimonialCards={false}
|
|
318
|
+
disclaimerText={
|
|
319
|
+
"Price includes a $5.00/mo credit with AutoPay, option to disable at the next step."
|
|
320
|
+
}
|
|
321
|
+
backgroundColor="bg-white"
|
|
322
|
+
showSwitch={true}
|
|
323
|
+
activeTab={activeTab}
|
|
324
|
+
setActiveTab={setActiveTab}
|
|
325
|
+
tabs={["Business Ready", "Internet Only"]}
|
|
326
|
+
/>
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Tab-switch coverage: the "Business Ready" tab mixes one gift card with one
|
|
333
|
+
* no-gift card (the no-gift card reserves the gift height so both CTAs align),
|
|
334
|
+
* while the "Internet Only" tab has two no-gift cards (both gift slots collapse
|
|
335
|
+
* and the CTAs sit flush with the description). Switching tabs recalculates the
|
|
336
|
+
* gift-slot height for only the visible cards.
|
|
337
|
+
*/
|
|
338
|
+
export const TabbedMixedGiftAlignment: Story = {
|
|
339
|
+
parameters: { layout: "fullscreen" },
|
|
340
|
+
render: () => <TabbedMixedTemplate />,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const twoColumnRowGiftFields: carouselFieldsInterface = {
|
|
344
|
+
__typename: "ComponentCarousel",
|
|
345
|
+
sys: { id: "carousel-two-column-row-gift" },
|
|
346
|
+
title: "Two-column rows: per-row gift height",
|
|
347
|
+
subTitle:
|
|
348
|
+
"On mobile (resize below 768px) the giftless top row collapses while the gift row below reserves its own height.",
|
|
349
|
+
twoColumn: true,
|
|
350
|
+
backgroundColor: "white",
|
|
351
|
+
items: {
|
|
352
|
+
items: [
|
|
353
|
+
// Row 1 — both giftless: the gift slot must collapse so these CTAs sit
|
|
354
|
+
// flush with the description (they must NOT borrow row 2's gift height).
|
|
355
|
+
sampleProduct(
|
|
356
|
+
"row-1a",
|
|
357
|
+
"fiber 100 mbps",
|
|
358
|
+
"19",
|
|
359
|
+
false,
|
|
360
|
+
"Reliable support for essentials like email, browsing, and video chat."
|
|
361
|
+
),
|
|
362
|
+
sampleProduct(
|
|
363
|
+
"row-1b",
|
|
364
|
+
"fiber 300 mbps",
|
|
365
|
+
"34",
|
|
366
|
+
false,
|
|
367
|
+
"Good for most day-to-day internet uses including streaming video."
|
|
368
|
+
),
|
|
369
|
+
// Row 2 — both have a gift badge: this row reserves its own gift height.
|
|
370
|
+
sampleProduct(
|
|
371
|
+
"row-2a",
|
|
372
|
+
"fiber 1 gig",
|
|
373
|
+
"39",
|
|
374
|
+
false,
|
|
375
|
+
"Boosted speed and capacity for working from a home office.",
|
|
376
|
+
mastercardGift("100")
|
|
377
|
+
),
|
|
378
|
+
sampleProduct(
|
|
379
|
+
"row-2b",
|
|
380
|
+
"fiber 2 gig",
|
|
381
|
+
"59",
|
|
382
|
+
false,
|
|
383
|
+
"Ultra-fast speeds for large smart homes.",
|
|
384
|
+
mastercardGift("200")
|
|
385
|
+
),
|
|
386
|
+
],
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Two-column mobile regression guard: gift-slot height is equalized per row
|
|
392
|
+
* (the cards next to each other), never across the whole tab. Resize below
|
|
393
|
+
* 768px — row 1 (both giftless) collapses so its CTAs sit flush with the
|
|
394
|
+
* description, while row 2 (both with a gift) reserves only its own height.
|
|
395
|
+
*/
|
|
396
|
+
export const TwoColumnRowGiftAlignment: Story = {
|
|
397
|
+
parameters: { layout: "fullscreen" },
|
|
398
|
+
args: {
|
|
399
|
+
fields: twoColumnRowGiftFields,
|
|
400
|
+
hasProductCards: true,
|
|
401
|
+
hasTestimonialCards: false,
|
|
402
|
+
disclaimerText: null,
|
|
403
|
+
backgroundColor: "bg-white",
|
|
404
|
+
activeTab: "",
|
|
405
|
+
setActiveTab: () => {},
|
|
406
|
+
tabs: [],
|
|
407
|
+
},
|
|
408
|
+
};
|
|
@@ -32,22 +32,34 @@ jest.mock("@shared/contentful/blocks/cards/product-card", () => ({
|
|
|
32
32
|
onToggleExpand,
|
|
33
33
|
isExpanded,
|
|
34
34
|
enableBusinessSettings,
|
|
35
|
+
giftBadge,
|
|
36
|
+
hasAnyGift,
|
|
35
37
|
}: any) => (
|
|
36
|
-
<
|
|
38
|
+
<article
|
|
37
39
|
data-testid="product-card"
|
|
38
40
|
data-plan={planName}
|
|
39
41
|
data-speed={speed}
|
|
40
42
|
data-expanded={isExpanded}
|
|
41
43
|
data-business={enableBusinessSettings ? "true" : "false"}
|
|
42
44
|
>
|
|
43
|
-
{planName}
|
|
45
|
+
<div data-pc-header={true}>{planName}</div>
|
|
46
|
+
<div data-pc-description={true}>{speed}</div>
|
|
47
|
+
{giftBadge?.length ? (
|
|
48
|
+
<div data-pc-gift={true} data-testid="gift-slot" />
|
|
49
|
+
) : hasAnyGift ? (
|
|
50
|
+
<div
|
|
51
|
+
data-pc-gift={true}
|
|
52
|
+
data-pc-gift-empty={true}
|
|
53
|
+
data-testid="gift-slot"
|
|
54
|
+
/>
|
|
55
|
+
) : null}
|
|
44
56
|
<button onClick={onCtaClick} data-testid="product-cta">
|
|
45
57
|
CTA
|
|
46
58
|
</button>
|
|
47
59
|
<button onClick={onToggleExpand} data-testid="product-toggle">
|
|
48
60
|
Toggle
|
|
49
61
|
</button>
|
|
50
|
-
</
|
|
62
|
+
</article>
|
|
51
63
|
),
|
|
52
64
|
}));
|
|
53
65
|
|
|
@@ -591,6 +603,256 @@ describe("ProductCardCarousel", () => {
|
|
|
591
603
|
removeSpy.mockRestore();
|
|
592
604
|
});
|
|
593
605
|
});
|
|
606
|
+
|
|
607
|
+
describe("swipe gestures (desktop carousel)", () => {
|
|
608
|
+
const makeCarousel = () => {
|
|
609
|
+
const items = Array.from({ length: 4 }, (_, i) => ({
|
|
610
|
+
...sampleProduct,
|
|
611
|
+
sys: { id: `prod-${i}` },
|
|
612
|
+
speed: `${100 * (i + 1)} Mbps|Fiber`,
|
|
613
|
+
}));
|
|
614
|
+
const utils = render(
|
|
615
|
+
<ProductCardCarousel fields={createProductFields(items)} />
|
|
616
|
+
);
|
|
617
|
+
// computeMaxIndex relies on offsetWidth (mocked to 900); a resize makes
|
|
618
|
+
// sure maxIndex is computed so nextSlide/prevSlide can move.
|
|
619
|
+
fireEvent(window, new Event("resize"));
|
|
620
|
+
const windowEl = utils.container.querySelector(
|
|
621
|
+
".select-none"
|
|
622
|
+
) as HTMLElement;
|
|
623
|
+
const track = utils.container.querySelector(
|
|
624
|
+
'[style*="translateX"]'
|
|
625
|
+
) as HTMLElement;
|
|
626
|
+
return { ...utils, windowEl, track };
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
it("advances to the next slide when swiped left past the threshold", () => {
|
|
630
|
+
const { windowEl, track } = makeCarousel();
|
|
631
|
+
expect(screen.queryByTestId("icon-arrow_back")).toBeNull();
|
|
632
|
+
|
|
633
|
+
fireEvent.touchStart(windowEl, { touches: [{ clientX: 500 }] });
|
|
634
|
+
fireEvent.touchMove(windowEl, { touches: [{ clientX: 300 }] });
|
|
635
|
+
// The track follows the finger while dragging (diff = -200).
|
|
636
|
+
expect(track.getAttribute("style")).toContain("-200px");
|
|
637
|
+
|
|
638
|
+
fireEvent.touchEnd(windowEl);
|
|
639
|
+
// nextSlide advanced the index, so the Previous arrow now shows.
|
|
640
|
+
expect(screen.queryByTestId("icon-arrow_back")).not.toBeNull();
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
it("returns to the previous slide when swiped right past the threshold", () => {
|
|
644
|
+
const { windowEl } = makeCarousel();
|
|
645
|
+
// Move forward first so there is a previous slide to return to.
|
|
646
|
+
fireEvent.touchStart(windowEl, { touches: [{ clientX: 500 }] });
|
|
647
|
+
fireEvent.touchMove(windowEl, { touches: [{ clientX: 300 }] });
|
|
648
|
+
fireEvent.touchEnd(windowEl);
|
|
649
|
+
expect(screen.queryByTestId("icon-arrow_back")).not.toBeNull();
|
|
650
|
+
|
|
651
|
+
fireEvent.touchStart(windowEl, { touches: [{ clientX: 300 }] });
|
|
652
|
+
fireEvent.touchMove(windowEl, { touches: [{ clientX: 600 }] });
|
|
653
|
+
fireEvent.touchEnd(windowEl);
|
|
654
|
+
// prevSlide returned to the first slide, hiding the Previous arrow.
|
|
655
|
+
expect(screen.queryByTestId("icon-arrow_back")).toBeNull();
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
it("snaps back without changing slides when the swipe is below the threshold", () => {
|
|
659
|
+
const { windowEl, track } = makeCarousel();
|
|
660
|
+
fireEvent.touchStart(windowEl, { touches: [{ clientX: 500 }] });
|
|
661
|
+
fireEvent.touchMove(windowEl, { touches: [{ clientX: 450 }] }); // diff -50
|
|
662
|
+
fireEvent.touchEnd(windowEl);
|
|
663
|
+
expect(screen.queryByTestId("icon-arrow_back")).toBeNull();
|
|
664
|
+
// Offset is reset to 0 after release.
|
|
665
|
+
expect(track.getAttribute("style")).toContain("+ 0px");
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it("applies drag resistance at the first-slide boundary", () => {
|
|
669
|
+
const { windowEl, track } = makeCarousel();
|
|
670
|
+
// Dragging right while already at the first slide is damped (diff * 0.3).
|
|
671
|
+
fireEvent.touchStart(windowEl, { touches: [{ clientX: 100 }] });
|
|
672
|
+
fireEvent.touchMove(windowEl, { touches: [{ clientX: 300 }] }); // diff 200
|
|
673
|
+
expect(track.getAttribute("style")).toContain("60px");
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it("ignores a touch move that is not preceded by a touch start", () => {
|
|
677
|
+
const { windowEl, track } = makeCarousel();
|
|
678
|
+
fireEvent.touchMove(windowEl, { touches: [{ clientX: 300 }] });
|
|
679
|
+
// No swipe offset is applied.
|
|
680
|
+
expect(track.getAttribute("style")).toContain("+ 0px");
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
describe("gift-slot per-row equalization", () => {
|
|
685
|
+
const mockRect = (el: Element, rect: { top?: number; height?: number }) => {
|
|
686
|
+
(el as HTMLElement).getBoundingClientRect = () =>
|
|
687
|
+
({
|
|
688
|
+
top: rect.top ?? 0,
|
|
689
|
+
left: 0,
|
|
690
|
+
right: 0,
|
|
691
|
+
bottom: (rect.top ?? 0) + (rect.height ?? 0),
|
|
692
|
+
width: 0,
|
|
693
|
+
height: rect.height ?? 0,
|
|
694
|
+
x: 0,
|
|
695
|
+
y: 0,
|
|
696
|
+
toJSON: () => ({}),
|
|
697
|
+
}) as DOMRect;
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const makeGrid = (
|
|
701
|
+
config: { gift: boolean; top: number; height: number }[]
|
|
702
|
+
) => {
|
|
703
|
+
const items = config.map((c, i) => ({
|
|
704
|
+
...sampleProduct,
|
|
705
|
+
sys: { id: `g-${i}` },
|
|
706
|
+
giftRewards: { list: { items: c.gift ? [{ title: "$100" }] : [] } },
|
|
707
|
+
}));
|
|
708
|
+
const { container } = render(
|
|
709
|
+
<ProductCardCarousel fields={createProductFields(items)} />
|
|
710
|
+
);
|
|
711
|
+
const grid = container.querySelector(".grid.grid-cols-2") as HTMLElement;
|
|
712
|
+
const articles = Array.from(grid.querySelectorAll("article"));
|
|
713
|
+
const slots = articles.map(
|
|
714
|
+
a => a.querySelector("[data-pc-gift]") as HTMLElement
|
|
715
|
+
);
|
|
716
|
+
articles.forEach((a, i) => mockRect(a, { top: config[i].top }));
|
|
717
|
+
slots.forEach((s, i) => {
|
|
718
|
+
mockRect(s, { height: config[i].height });
|
|
719
|
+
Object.defineProperty(s, "offsetParent", {
|
|
720
|
+
configurable: true,
|
|
721
|
+
get: () => document.body,
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
return { slots };
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
it("collapses a giftless row and never borrows another row's gift height", () => {
|
|
728
|
+
// Row 1 (both giftless) above Row 2 (both with a gift).
|
|
729
|
+
const { slots } = makeGrid([
|
|
730
|
+
{ gift: false, top: 0, height: 20 },
|
|
731
|
+
{ gift: false, top: 0, height: 20 },
|
|
732
|
+
{ gift: true, top: 500, height: 76 },
|
|
733
|
+
{ gift: true, top: 500, height: 76 },
|
|
734
|
+
]);
|
|
735
|
+
|
|
736
|
+
fireEvent(window, new Event("resize"));
|
|
737
|
+
|
|
738
|
+
// Row 1 collapses instead of reserving the gift height.
|
|
739
|
+
expect(slots[0].style.display).toBe("none");
|
|
740
|
+
expect(slots[1].style.display).toBe("none");
|
|
741
|
+
expect(slots[0].style.minHeight).toBe("");
|
|
742
|
+
// Row 2 reserves only its own gift height.
|
|
743
|
+
expect(slots[2].style.minHeight).toBe("76px");
|
|
744
|
+
expect(slots[3].style.minHeight).toBe("76px");
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it("reserves the tallest gift height within a mixed row so CTAs align", () => {
|
|
748
|
+
// Row 1 = gift + giftless; Row 2 = gift + gift.
|
|
749
|
+
const { slots } = makeGrid([
|
|
750
|
+
{ gift: true, top: 0, height: 76 },
|
|
751
|
+
{ gift: false, top: 0, height: 20 },
|
|
752
|
+
{ gift: true, top: 500, height: 76 },
|
|
753
|
+
{ gift: true, top: 500, height: 76 },
|
|
754
|
+
]);
|
|
755
|
+
|
|
756
|
+
fireEvent(window, new Event("resize"));
|
|
757
|
+
|
|
758
|
+
// The giftless neighbour reserves the taller gift height (CTAs align).
|
|
759
|
+
expect(slots[0].style.minHeight).toBe("76px");
|
|
760
|
+
expect(slots[1].style.minHeight).toBe("76px");
|
|
761
|
+
expect(slots[1].style.display).not.toBe("none");
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
describe("two-column mobile section equalization", () => {
|
|
766
|
+
let originalMatchMedia: typeof window.matchMedia;
|
|
767
|
+
|
|
768
|
+
const setMatchMedia = (matches: boolean) => {
|
|
769
|
+
window.matchMedia = ((query: string) => ({
|
|
770
|
+
matches,
|
|
771
|
+
media: query,
|
|
772
|
+
onchange: null,
|
|
773
|
+
addListener: jest.fn(),
|
|
774
|
+
removeListener: jest.fn(),
|
|
775
|
+
addEventListener: jest.fn(),
|
|
776
|
+
removeEventListener: jest.fn(),
|
|
777
|
+
dispatchEvent: jest.fn(),
|
|
778
|
+
})) as any;
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
const mockHeight = (el: Element, height: number) => {
|
|
782
|
+
(el as HTMLElement).getBoundingClientRect = () =>
|
|
783
|
+
({
|
|
784
|
+
top: 0,
|
|
785
|
+
left: 0,
|
|
786
|
+
right: 0,
|
|
787
|
+
bottom: height,
|
|
788
|
+
width: 0,
|
|
789
|
+
height,
|
|
790
|
+
x: 0,
|
|
791
|
+
y: 0,
|
|
792
|
+
toJSON: () => ({}),
|
|
793
|
+
}) as DOMRect;
|
|
794
|
+
};
|
|
795
|
+
|
|
796
|
+
beforeEach(() => {
|
|
797
|
+
originalMatchMedia = window.matchMedia;
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
afterEach(() => {
|
|
801
|
+
window.matchMedia = originalMatchMedia;
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it("matches header and description heights within each row (pair)", () => {
|
|
805
|
+
setMatchMedia(true);
|
|
806
|
+
const items = Array.from({ length: 4 }, (_, i) => ({
|
|
807
|
+
...sampleProduct,
|
|
808
|
+
sys: { id: `prod-${i}` },
|
|
809
|
+
}));
|
|
810
|
+
const { container } = render(
|
|
811
|
+
<ProductCardCarousel fields={createProductFields(items)} />
|
|
812
|
+
);
|
|
813
|
+
const grid = container.querySelector(".grid.grid-cols-2") as HTMLElement;
|
|
814
|
+
const headers = Array.from(
|
|
815
|
+
grid.querySelectorAll<HTMLElement>("[data-pc-header]")
|
|
816
|
+
);
|
|
817
|
+
const descriptions = Array.from(
|
|
818
|
+
grid.querySelectorAll<HTMLElement>("[data-pc-description]")
|
|
819
|
+
);
|
|
820
|
+
// Row 1 header heights 30/50 -> 50; Row 2 header heights 40/20 -> 40.
|
|
821
|
+
[30, 50, 40, 20].forEach((h, i) => mockHeight(headers[i], h));
|
|
822
|
+
// Row 1 descriptions 25/25 -> 25; Row 2 descriptions 10/60 -> 60.
|
|
823
|
+
[25, 25, 10, 60].forEach((h, i) => mockHeight(descriptions[i], h));
|
|
824
|
+
|
|
825
|
+
fireEvent(window, new Event("resize"));
|
|
826
|
+
|
|
827
|
+
expect(headers[0].style.minHeight).toBe("50px");
|
|
828
|
+
expect(headers[1].style.minHeight).toBe("50px");
|
|
829
|
+
expect(headers[2].style.minHeight).toBe("40px");
|
|
830
|
+
expect(headers[3].style.minHeight).toBe("40px");
|
|
831
|
+
|
|
832
|
+
expect(descriptions[0].style.minHeight).toBe("25px");
|
|
833
|
+
expect(descriptions[1].style.minHeight).toBe("25px");
|
|
834
|
+
expect(descriptions[2].style.minHeight).toBe("60px");
|
|
835
|
+
expect(descriptions[3].style.minHeight).toBe("60px");
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it("clears section min-heights above the mobile breakpoint", () => {
|
|
839
|
+
setMatchMedia(false);
|
|
840
|
+
const items = Array.from({ length: 4 }, (_, i) => ({
|
|
841
|
+
...sampleProduct,
|
|
842
|
+
sys: { id: `prod-${i}` },
|
|
843
|
+
}));
|
|
844
|
+
const { container } = render(
|
|
845
|
+
<ProductCardCarousel fields={createProductFields(items)} />
|
|
846
|
+
);
|
|
847
|
+
const grid = container.querySelector(".grid.grid-cols-2") as HTMLElement;
|
|
848
|
+
const header = grid.querySelector<HTMLElement>("[data-pc-header]")!;
|
|
849
|
+
header.style.setProperty("min-height", "123px");
|
|
850
|
+
|
|
851
|
+
fireEvent(window, new Event("resize"));
|
|
852
|
+
|
|
853
|
+
expect(header.style.minHeight).toBe("");
|
|
854
|
+
});
|
|
855
|
+
});
|
|
594
856
|
});
|
|
595
857
|
|
|
596
858
|
describe("TestimonialCarousel", () => {
|