@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
|
@@ -46,10 +46,14 @@ function ProductCardPanel({
|
|
|
46
46
|
// When any card is flagged best-value, every card reserves the banner height
|
|
47
47
|
// so plan name/price stay aligned across a two-column row.
|
|
48
48
|
const hasAnyBadge = items.some((item: any) => item?.highlighted);
|
|
49
|
+
const hasAnyGift = items.some(
|
|
50
|
+
(item: any) => (item?.giftRewards?.list?.items?.length ?? 0) > 0
|
|
51
|
+
);
|
|
49
52
|
const showArrows = fields?.showArrows !== false && isCarousel;
|
|
50
53
|
const cardsRef = useRef<(HTMLDivElement | null)[]>([]);
|
|
51
54
|
const windowRef = useRef<HTMLDivElement | null>(null);
|
|
52
55
|
const mobileGridRef = useRef<HTMLDivElement | null>(null);
|
|
56
|
+
const panelRef = useRef<HTMLDivElement | null>(null);
|
|
53
57
|
const touchStartX = useRef<number>(0);
|
|
54
58
|
|
|
55
59
|
// Determine how many cards fit so we never scroll past the last card
|
|
@@ -176,16 +180,13 @@ function ProductCardPanel({
|
|
|
176
180
|
}, [isCarousel, isVisible, desktopExpanded, items.length]);
|
|
177
181
|
|
|
178
182
|
// Two-column mobile: match each card's sections to the tallest card in the
|
|
179
|
-
// same row (pair) so section spacing
|
|
183
|
+
// same row (pair) so section spacing aligns per row. The gift slot is handled
|
|
184
|
+
// separately (panel-wide) so it sizes to the real gift content. Desktop keeps
|
|
180
185
|
// its fixed section heights and is left untouched.
|
|
181
186
|
useEffect(() => {
|
|
182
187
|
if (!twoColumn || !isVisible) return;
|
|
183
188
|
|
|
184
|
-
const selectors = [
|
|
185
|
-
"[data-pc-header]",
|
|
186
|
-
"[data-pc-description]",
|
|
187
|
-
"[data-pc-gift]",
|
|
188
|
-
];
|
|
189
|
+
const selectors = ["[data-pc-header]", "[data-pc-description]"];
|
|
189
190
|
|
|
190
191
|
const clear = () => {
|
|
191
192
|
const grid = mobileGridRef.current;
|
|
@@ -193,35 +194,20 @@ function ProductCardPanel({
|
|
|
193
194
|
selectors.forEach(sel =>
|
|
194
195
|
grid.querySelectorAll<HTMLElement>(sel).forEach(el => {
|
|
195
196
|
el.style.removeProperty("min-height");
|
|
196
|
-
el.style.removeProperty("display");
|
|
197
197
|
})
|
|
198
198
|
);
|
|
199
199
|
};
|
|
200
200
|
|
|
201
201
|
// `important` is required because Tailwind utilities are generated with
|
|
202
202
|
// !important in this project (tailwind.config `important: true`).
|
|
203
|
-
const equalizePairs = (sel: string
|
|
203
|
+
const equalizePairs = (sel: string) => {
|
|
204
204
|
const grid = mobileGridRef.current;
|
|
205
205
|
if (!grid) return;
|
|
206
206
|
const els = Array.from(grid.querySelectorAll<HTMLElement>(sel));
|
|
207
207
|
// Reset before measuring so a previous pass doesn't inflate the result.
|
|
208
|
-
els.forEach(el =>
|
|
209
|
-
el.style.removeProperty("min-height");
|
|
210
|
-
el.style.removeProperty("display");
|
|
211
|
-
});
|
|
208
|
+
els.forEach(el => el.style.removeProperty("min-height"));
|
|
212
209
|
for (let i = 0; i < els.length; i += 2) {
|
|
213
210
|
const pair = els.slice(i, i + 2);
|
|
214
|
-
// Collapse a row (or lone trailing card) whose gift slots are all empty
|
|
215
|
-
// so no space is reserved when the row has no gift.
|
|
216
|
-
if (
|
|
217
|
-
collapseEmpty &&
|
|
218
|
-
pair.every(el => el.hasAttribute("data-pc-gift-empty"))
|
|
219
|
-
) {
|
|
220
|
-
pair.forEach(el =>
|
|
221
|
-
el.style.setProperty("display", "none", "important")
|
|
222
|
-
);
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
211
|
if (pair.length < 2) break;
|
|
226
212
|
const max = Math.max(
|
|
227
213
|
...pair.map(el => el.getBoundingClientRect().height)
|
|
@@ -240,7 +226,6 @@ function ProductCardPanel({
|
|
|
240
226
|
}
|
|
241
227
|
equalizePairs("[data-pc-header]");
|
|
242
228
|
equalizePairs("[data-pc-description]");
|
|
243
|
-
equalizePairs("[data-pc-gift]", true);
|
|
244
229
|
};
|
|
245
230
|
|
|
246
231
|
const rafId = requestAnimationFrame(equalize);
|
|
@@ -257,6 +242,83 @@ function ProductCardPanel({
|
|
|
257
242
|
};
|
|
258
243
|
}, [twoColumn, isVisible, desktopExpanded, items.length]);
|
|
259
244
|
|
|
245
|
+
// Size the gift-reward slot per visual row so the Select plan CTA sits
|
|
246
|
+
// directly below the gift badge, while staying aligned only with the cards
|
|
247
|
+
// that sit next to it (same row). Cards are grouped by their on-screen row,
|
|
248
|
+
// so a giftless row never borrows the gift height of another row in the same
|
|
249
|
+
// tab: desktop equalizes the single row of cards; two-column mobile equalizes
|
|
250
|
+
// each pair; a row whose slots are all empty collapses so its CTA sits flush
|
|
251
|
+
// with the description. When the tab has no gift at all, `hasAnyGift` omits
|
|
252
|
+
// the slot entirely.
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
if (!isVisible) return;
|
|
255
|
+
|
|
256
|
+
const root = panelRef.current;
|
|
257
|
+
|
|
258
|
+
const reset = () => {
|
|
259
|
+
if (!root) return;
|
|
260
|
+
root.querySelectorAll<HTMLElement>("[data-pc-gift]").forEach(el => {
|
|
261
|
+
el.style.removeProperty("min-height");
|
|
262
|
+
el.style.removeProperty("display");
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const equalizeGiftSlots = () => {
|
|
267
|
+
if (!root) return;
|
|
268
|
+
reset();
|
|
269
|
+
const els = Array.from(
|
|
270
|
+
root.querySelectorAll<HTMLElement>("[data-pc-gift]")
|
|
271
|
+
).filter(el => el.offsetParent !== null);
|
|
272
|
+
if (els.length === 0) return;
|
|
273
|
+
|
|
274
|
+
// Group slots by the top of their owning card so each visual row (the
|
|
275
|
+
// cards that sit next to each other) is equalized independently.
|
|
276
|
+
const tolerance = 4;
|
|
277
|
+
const rows: { top: number; els: HTMLElement[] }[] = [];
|
|
278
|
+
els.forEach(el => {
|
|
279
|
+
const card = (el.closest("article") as HTMLElement) || el;
|
|
280
|
+
const top = card.getBoundingClientRect().top;
|
|
281
|
+
const row = rows.find(r => Math.abs(r.top - top) <= tolerance);
|
|
282
|
+
if (row) {
|
|
283
|
+
row.els.push(el);
|
|
284
|
+
} else {
|
|
285
|
+
rows.push({ top, els: [el] });
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
rows.forEach(({ els: rowEls }) => {
|
|
290
|
+
// Collapse a row whose slots are all empty so no space is reserved.
|
|
291
|
+
if (rowEls.every(el => el.hasAttribute("data-pc-gift-empty"))) {
|
|
292
|
+
rowEls.forEach(el =>
|
|
293
|
+
el.style.setProperty("display", "none", "important")
|
|
294
|
+
);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const max = Math.max(
|
|
298
|
+
...rowEls.map(el => el.getBoundingClientRect().height)
|
|
299
|
+
);
|
|
300
|
+
if (max <= 0) return;
|
|
301
|
+
rowEls.forEach(el =>
|
|
302
|
+
el.style.setProperty("min-height", `${max}px`, "important")
|
|
303
|
+
);
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
equalizeGiftSlots();
|
|
308
|
+
const rafId = requestAnimationFrame(equalizeGiftSlots);
|
|
309
|
+
const t1 = setTimeout(equalizeGiftSlots, 150);
|
|
310
|
+
const t2 = setTimeout(equalizeGiftSlots, 500);
|
|
311
|
+
window.addEventListener("resize", equalizeGiftSlots);
|
|
312
|
+
|
|
313
|
+
return () => {
|
|
314
|
+
cancelAnimationFrame(rafId);
|
|
315
|
+
clearTimeout(t1);
|
|
316
|
+
clearTimeout(t2);
|
|
317
|
+
window.removeEventListener("resize", equalizeGiftSlots);
|
|
318
|
+
reset();
|
|
319
|
+
};
|
|
320
|
+
}, [isVisible, desktopExpanded, items.length]);
|
|
321
|
+
|
|
260
322
|
if (!items.length && !fields?.title) {
|
|
261
323
|
return null;
|
|
262
324
|
}
|
|
@@ -338,6 +400,7 @@ function ProductCardPanel({
|
|
|
338
400
|
cta={item.cta}
|
|
339
401
|
priceSuffixBottom={item?.priceSuffixBottom}
|
|
340
402
|
hasAnyBadge={hasAnyBadge}
|
|
403
|
+
hasAnyGift={hasAnyGift}
|
|
341
404
|
twoColumn={twoColumn}
|
|
342
405
|
enableBusinessSettings={enableBusinessSettings}
|
|
343
406
|
/>
|
|
@@ -345,87 +408,91 @@ function ProductCardPanel({
|
|
|
345
408
|
);
|
|
346
409
|
};
|
|
347
410
|
|
|
348
|
-
return
|
|
349
|
-
<div
|
|
350
|
-
|
|
351
|
-
className={cx(
|
|
352
|
-
"w-full max-w-[1280px] gap-4",
|
|
353
|
-
twoColumn
|
|
354
|
-
? "grid grid-cols-2 md:flex md:flex-wrap md:justify-center"
|
|
355
|
-
: "flex flex-wrap justify-center"
|
|
356
|
-
)}
|
|
357
|
-
>
|
|
358
|
-
{items.map((item, index) => renderCard(item, index, twoColumn))}
|
|
359
|
-
</div>
|
|
360
|
-
) : (
|
|
361
|
-
<>
|
|
362
|
-
{/* Mobile/Tablet View: Vertical Stack (or two-column grid) */}
|
|
363
|
-
<div
|
|
364
|
-
ref={mobileGridRef}
|
|
365
|
-
className={cx(
|
|
366
|
-
"md:hidden",
|
|
367
|
-
twoColumn ? "grid grid-cols-2 gap-4" : "flex flex-col gap-6"
|
|
368
|
-
)}
|
|
369
|
-
>
|
|
370
|
-
{items.map((item, index) => renderCard(item, index, true))}
|
|
371
|
-
</div>
|
|
372
|
-
|
|
373
|
-
{/* Desktop View: Horizontal Carousel */}
|
|
374
|
-
<div className="relative hidden w-full md:block">
|
|
375
|
-
{/* Navigation Arrows */}
|
|
376
|
-
{showArrows && (
|
|
377
|
-
<div className="pointer-events-none absolute -left-16 -right-16 top-[50%] z-30 flex -translate-y-1/2 justify-between px-4 md:px-10">
|
|
378
|
-
{currentIndex > 0 ? (
|
|
379
|
-
<Button
|
|
380
|
-
onClick={prevSlide}
|
|
381
|
-
className="pointer-events-auto flex h-12 w-12 items-center justify-center rounded-full border border-gray-100 bg-white p-2 text-text shadow-cardDrop transition-all hover:bg-gray-50"
|
|
382
|
-
aria-label="Previous"
|
|
383
|
-
>
|
|
384
|
-
<MaterialIcon name="arrow_back" size={24} />
|
|
385
|
-
</Button>
|
|
386
|
-
) : (
|
|
387
|
-
<span className="h-12 w-12" aria-hidden="true" />
|
|
388
|
-
)}
|
|
389
|
-
|
|
390
|
-
{currentIndex < maxIndex ? (
|
|
391
|
-
<Button
|
|
392
|
-
onClick={nextSlide}
|
|
393
|
-
className="pointer-events-auto flex h-12 w-12 items-center justify-center rounded-full border border-gray-100 bg-white p-2 text-text shadow-cardDrop transition-all hover:bg-gray-50"
|
|
394
|
-
aria-label="Next"
|
|
395
|
-
>
|
|
396
|
-
<MaterialIcon name="arrow_forward" size={24} />
|
|
397
|
-
</Button>
|
|
398
|
-
) : (
|
|
399
|
-
<span className="h-12 w-12" aria-hidden="true" />
|
|
400
|
-
)}
|
|
401
|
-
</div>
|
|
402
|
-
)}
|
|
403
|
-
|
|
404
|
-
{/* Carousel Window */}
|
|
411
|
+
return (
|
|
412
|
+
<div ref={panelRef} style={{ display: "contents" }}>
|
|
413
|
+
{!isCarousel ? (
|
|
405
414
|
<div
|
|
406
|
-
ref={
|
|
407
|
-
className=
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
415
|
+
ref={mobileGridRef}
|
|
416
|
+
className={cx(
|
|
417
|
+
"w-full max-w-[1280px] gap-4",
|
|
418
|
+
twoColumn
|
|
419
|
+
? "grid grid-cols-2 md:flex md:flex-wrap md:justify-center"
|
|
420
|
+
: "flex flex-wrap justify-center"
|
|
421
|
+
)}
|
|
412
422
|
>
|
|
423
|
+
{items.map((item, index) => renderCard(item, index, twoColumn))}
|
|
424
|
+
</div>
|
|
425
|
+
) : (
|
|
426
|
+
<>
|
|
427
|
+
{/* Mobile/Tablet View: Vertical Stack (or two-column grid) */}
|
|
413
428
|
<div
|
|
429
|
+
ref={mobileGridRef}
|
|
414
430
|
className={cx(
|
|
415
|
-
"
|
|
416
|
-
|
|
417
|
-
? "transition-none"
|
|
418
|
-
: "transition-transform duration-500 ease-in-out"
|
|
431
|
+
"md:hidden",
|
|
432
|
+
twoColumn ? "grid grid-cols-2 gap-4" : "flex flex-col gap-6"
|
|
419
433
|
)}
|
|
420
|
-
style={{
|
|
421
|
-
transform: `translateX(calc(-${currentIndex} * (392px + 16px) + ${swipeOffset}px))`,
|
|
422
|
-
}}
|
|
423
434
|
>
|
|
424
|
-
{items.map((item, index) => renderCard(item, index,
|
|
435
|
+
{items.map((item, index) => renderCard(item, index, true))}
|
|
425
436
|
</div>
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
437
|
+
|
|
438
|
+
{/* Desktop View: Horizontal Carousel */}
|
|
439
|
+
<div className="relative hidden w-full md:block">
|
|
440
|
+
{/* Navigation Arrows */}
|
|
441
|
+
{showArrows && (
|
|
442
|
+
<div className="pointer-events-none absolute -left-16 -right-16 top-[50%] z-30 flex -translate-y-1/2 justify-between px-4 md:px-10">
|
|
443
|
+
{currentIndex > 0 ? (
|
|
444
|
+
<Button
|
|
445
|
+
onClick={prevSlide}
|
|
446
|
+
className="pointer-events-auto flex h-12 w-12 items-center justify-center rounded-full border border-gray-100 bg-white p-2 text-text shadow-cardDrop transition-all hover:bg-gray-50"
|
|
447
|
+
aria-label="Previous"
|
|
448
|
+
>
|
|
449
|
+
<MaterialIcon name="arrow_back" size={24} />
|
|
450
|
+
</Button>
|
|
451
|
+
) : (
|
|
452
|
+
<span className="h-12 w-12" aria-hidden="true" />
|
|
453
|
+
)}
|
|
454
|
+
|
|
455
|
+
{currentIndex < maxIndex ? (
|
|
456
|
+
<Button
|
|
457
|
+
onClick={nextSlide}
|
|
458
|
+
className="pointer-events-auto flex h-12 w-12 items-center justify-center rounded-full border border-gray-100 bg-white p-2 text-text shadow-cardDrop transition-all hover:bg-gray-50"
|
|
459
|
+
aria-label="Next"
|
|
460
|
+
>
|
|
461
|
+
<MaterialIcon name="arrow_forward" size={24} />
|
|
462
|
+
</Button>
|
|
463
|
+
) : (
|
|
464
|
+
<span className="h-12 w-12" aria-hidden="true" />
|
|
465
|
+
)}
|
|
466
|
+
</div>
|
|
467
|
+
)}
|
|
468
|
+
|
|
469
|
+
{/* Carousel Window */}
|
|
470
|
+
<div
|
|
471
|
+
ref={windowRef}
|
|
472
|
+
className="mx-auto max-w-[1280px] select-none overflow-hidden"
|
|
473
|
+
onTouchStart={handleTouchStart}
|
|
474
|
+
onTouchMove={handleTouchMove}
|
|
475
|
+
onTouchEnd={handleTouchEnd}
|
|
476
|
+
onTouchCancel={handleTouchEnd}
|
|
477
|
+
>
|
|
478
|
+
<div
|
|
479
|
+
className={cx(
|
|
480
|
+
"ml-8 flex items-stretch gap-4 pb-4",
|
|
481
|
+
isSwiping
|
|
482
|
+
? "transition-none"
|
|
483
|
+
: "transition-transform duration-500 ease-in-out"
|
|
484
|
+
)}
|
|
485
|
+
style={{
|
|
486
|
+
transform: `translateX(calc(-${currentIndex} * (392px + 16px) + ${swipeOffset}px))`,
|
|
487
|
+
}}
|
|
488
|
+
>
|
|
489
|
+
{items.map((item, index) => renderCard(item, index, false))}
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
</>
|
|
494
|
+
)}
|
|
495
|
+
</div>
|
|
429
496
|
);
|
|
430
497
|
}
|
|
431
498
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { OrderedList } from "./index";
|
|
5
|
+
|
|
6
|
+
import { render, screen, within } from "@testing-library/react";
|
|
7
|
+
|
|
8
|
+
jest.mock("@shared/components/link", () => ({
|
|
9
|
+
Link: ({ children, href, title, className }: any) => (
|
|
10
|
+
<a href={href} title={title} className={className} data-testid="ol-link">
|
|
11
|
+
{children}
|
|
12
|
+
</a>
|
|
13
|
+
),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
jest.mock("@shared/components/text", () => ({
|
|
17
|
+
Text: ({ children, as: Tag = "span", className }: any) => (
|
|
18
|
+
<Tag className={className}>{children}</Tag>
|
|
19
|
+
),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
jest.mock("@shared/contentful/blocks/dynamic-tabs", () => ({
|
|
23
|
+
DynamicTabs: ({
|
|
24
|
+
tabs,
|
|
25
|
+
dynamic,
|
|
26
|
+
isOrderedList,
|
|
27
|
+
navigationType,
|
|
28
|
+
mobileSidebarTabType,
|
|
29
|
+
}: any) => (
|
|
30
|
+
<div
|
|
31
|
+
data-testid="dynamic-tabs"
|
|
32
|
+
data-dynamic={String(dynamic)}
|
|
33
|
+
data-ordered={String(isOrderedList)}
|
|
34
|
+
data-nav={String(navigationType)}
|
|
35
|
+
data-sidebar={String(mobileSidebarTabType)}
|
|
36
|
+
>
|
|
37
|
+
{tabs.items.map((tab: any) => (
|
|
38
|
+
<div
|
|
39
|
+
key={tab.anchorId}
|
|
40
|
+
data-testid="dt-tab"
|
|
41
|
+
data-tabname={tab.tabName}
|
|
42
|
+
data-anchor={tab.anchorId}
|
|
43
|
+
>
|
|
44
|
+
{tab.tabContent}
|
|
45
|
+
</div>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
describe("OrderedList", () => {
|
|
52
|
+
const tabs = [
|
|
53
|
+
{
|
|
54
|
+
items: {
|
|
55
|
+
tabName: "States",
|
|
56
|
+
tabContent: [
|
|
57
|
+
{ itemName: "new york", itemSlug: "/new-york" },
|
|
58
|
+
{ itemName: "los angeles" },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
items: {
|
|
64
|
+
tabName: "Cities",
|
|
65
|
+
tabContent: [{ itemName: "san FRANCISCO", itemSlug: "/sf" }],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
const renderComponent = (overrides: Partial<any> = {}) =>
|
|
71
|
+
render(
|
|
72
|
+
<OrderedList
|
|
73
|
+
title="Browse locations"
|
|
74
|
+
description="Find availability near you"
|
|
75
|
+
tabs={tabs}
|
|
76
|
+
{...overrides}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
it("renders the title and description", () => {
|
|
81
|
+
renderComponent();
|
|
82
|
+
expect(screen.getByText("Browse locations")).toBeInTheDocument();
|
|
83
|
+
expect(screen.getByText("Find availability near you")).toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("renders the section with the provided anchor id and testid", () => {
|
|
87
|
+
const { container } = renderComponent({ anchorId: "locations" });
|
|
88
|
+
const section = container.querySelector(
|
|
89
|
+
'[data-testid="non-fiber-city-list"]'
|
|
90
|
+
);
|
|
91
|
+
expect(section).toBeInTheDocument();
|
|
92
|
+
expect(section).toHaveAttribute("id", "locations");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("defaults the background to white", () => {
|
|
96
|
+
const { container } = renderComponent();
|
|
97
|
+
expect(container.querySelector(".bg-secondary-white")).toBeInTheDocument();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("applies the cream background when requested", () => {
|
|
101
|
+
const { container } = renderComponent({ backgroundColor: "cream" });
|
|
102
|
+
expect(container.querySelector(".bg-secondary-cream")).toBeInTheDocument();
|
|
103
|
+
expect(
|
|
104
|
+
container.querySelector(".bg-secondary-white")
|
|
105
|
+
).not.toBeInTheDocument();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("forwards the expected flags to DynamicTabs", () => {
|
|
109
|
+
renderComponent();
|
|
110
|
+
const dynamicTabs = screen.getByTestId("dynamic-tabs");
|
|
111
|
+
expect(dynamicTabs).toHaveAttribute("data-dynamic", "true");
|
|
112
|
+
expect(dynamicTabs).toHaveAttribute("data-ordered", "true");
|
|
113
|
+
expect(dynamicTabs).toHaveAttribute("data-nav", "false");
|
|
114
|
+
expect(dynamicTabs).toHaveAttribute("data-sidebar", "false");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("maps each tab with its name and a generated anchor id", () => {
|
|
118
|
+
renderComponent();
|
|
119
|
+
const dtTabs = screen.getAllByTestId("dt-tab");
|
|
120
|
+
expect(dtTabs).toHaveLength(2);
|
|
121
|
+
expect(dtTabs[0]).toHaveAttribute("data-tabname", "States");
|
|
122
|
+
expect(dtTabs[0]).toHaveAttribute("data-anchor", "tab-0");
|
|
123
|
+
expect(dtTabs[1]).toHaveAttribute("data-tabname", "Cities");
|
|
124
|
+
expect(dtTabs[1]).toHaveAttribute("data-anchor", "tab-1");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("title-cases item names regardless of input casing", () => {
|
|
128
|
+
renderComponent();
|
|
129
|
+
// "new york" -> "New York", "los angeles" -> "Los Angeles",
|
|
130
|
+
// "san FRANCISCO" -> "San Francisco"
|
|
131
|
+
expect(screen.getByText("New York")).toBeInTheDocument();
|
|
132
|
+
expect(screen.getByText("Los Angeles")).toBeInTheDocument();
|
|
133
|
+
expect(screen.getByText("San Francisco")).toBeInTheDocument();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("renders a linked item with an uppercased title and href", () => {
|
|
137
|
+
renderComponent();
|
|
138
|
+
const link = screen.getByText("New York").closest("a");
|
|
139
|
+
expect(link).not.toBeNull();
|
|
140
|
+
expect(link).toHaveAttribute("href", "/new-york");
|
|
141
|
+
expect(link).toHaveAttribute("title", "NEW YORK");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("renders an item without a slug as plain text (no link)", () => {
|
|
145
|
+
renderComponent();
|
|
146
|
+
const losAngeles = screen.getByText("Los Angeles");
|
|
147
|
+
expect(losAngeles.closest("a")).toBeNull();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("keeps each tab's items within that tab", () => {
|
|
151
|
+
renderComponent();
|
|
152
|
+
const dtTabs = screen.getAllByTestId("dt-tab");
|
|
153
|
+
expect(within(dtTabs[0]).getByText("New York")).toBeInTheDocument();
|
|
154
|
+
expect(within(dtTabs[0]).getByText("Los Angeles")).toBeInTheDocument();
|
|
155
|
+
expect(within(dtTabs[1]).getByText("San Francisco")).toBeInTheDocument();
|
|
156
|
+
expect(within(dtTabs[1]).queryByText("New York")).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("does not render DynamicTabs when there are no tabs", () => {
|
|
160
|
+
renderComponent({ tabs: [] });
|
|
161
|
+
expect(screen.queryByTestId("dynamic-tabs")).toBeNull();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -184,6 +184,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
|
|
|
184
184
|
const checklistEl = (
|
|
185
185
|
<Checklist
|
|
186
186
|
iconSize={24}
|
|
187
|
+
iconPosition="top"
|
|
187
188
|
listItemClassName={`body2 text-left ${textColorClass}`}
|
|
188
189
|
items={checklist.map(item =>
|
|
189
190
|
item.iconUrl
|