@windstream/react-shared-components 0.2.60 → 0.2.62
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 +48 -2
- package/dist/contentful/index.esm.js +5 -5
- 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 +1 -1
- package/dist/index.d.ts +3 -3
- 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/package.json +1 -1
- package/src/components/brand-button/index.tsx +1 -1
- package/src/components/video-link/index.tsx +28 -8
- package/src/components/video-link/youtube-embed.test.tsx +11 -7
- package/src/components/video-link/youtube-embed.tsx +58 -58
- package/src/contentful/blocks/anchored-bottom-banner/index.test.tsx +248 -0
- package/src/contentful/blocks/anchored-bottom-banner/index.tsx +197 -29
- package/src/contentful/blocks/anchored-bottom-banner/types.ts +46 -0
- package/src/contentful/blocks/cards/product-card/index.test.tsx +14 -1
- package/src/contentful/blocks/cards/product-card/index.tsx +193 -135
- package/src/contentful/blocks/cards/product-card/types.ts +1 -1
- package/src/contentful/blocks/carousel/Carousel.stories.tsx +59 -0
- package/src/contentful/blocks/carousel/helper.test.tsx +23 -0
- package/src/contentful/blocks/carousel/helper.tsx +18 -5
- package/src/contentful/blocks/carousel/types.ts +3 -0
- package/src/contentful/blocks/dynamic-form/conditional-fields.test.tsx +3 -0
- package/src/contentful/blocks/dynamic-form/conditional-fields.tsx +30 -4
- package/src/contentful/blocks/dynamic-form/context.ts +2 -0
- package/src/contentful/blocks/dynamic-form/field-map.test.tsx +21 -0
- package/src/contentful/blocks/dynamic-form/field-map.tsx +15 -0
- package/src/contentful/blocks/dynamic-form/form-field.tsx +2 -0
- package/src/contentful/blocks/dynamic-form/index.tsx +48 -16
- package/src/contentful/blocks/dynamic-form/render.tsx +1 -0
- package/src/contentful/blocks/dynamic-form/types.ts +26 -0
- package/src/contentful/blocks/dynamic-form/utils.test.ts +86 -0
- package/src/contentful/blocks/dynamic-form/utils.ts +57 -0
- package/src/contentful/index.ts +5 -1
|
@@ -36,80 +36,76 @@ export const YoutubeEmbed: React.FC<Props> = props => {
|
|
|
36
36
|
} = props;
|
|
37
37
|
|
|
38
38
|
const embedId = useMemo(() => getYoutubeEmbedId(link), [link]);
|
|
39
|
-
const
|
|
40
|
-
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
39
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
41
40
|
const progressInterval = useRef<ReturnType<typeof setInterval>>();
|
|
42
41
|
|
|
43
42
|
useFullscreenToggle(iframeRef, onFullscreenToggle);
|
|
44
43
|
|
|
45
44
|
useEffect(() => {
|
|
46
|
-
if (!embedId || !
|
|
45
|
+
if (!embedId || !iframeRef.current) return undefined;
|
|
47
46
|
let player: any;
|
|
48
47
|
let cancelled = false;
|
|
49
48
|
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
iframe.className = "absolute left-0 top-0 h-full w-full";
|
|
77
|
-
iframeRef.current = iframe;
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
onStateChange: (event: any) => {
|
|
81
|
-
const State = YT.PlayerState;
|
|
82
|
-
if (event.data === State.PLAYING) {
|
|
83
|
-
onPlay?.();
|
|
84
|
-
clearInterval(progressInterval.current);
|
|
85
|
-
progressInterval.current = setInterval(() => {
|
|
86
|
-
const duration = player?.getDuration?.() ?? 0;
|
|
87
|
-
const position = player?.getCurrentTime?.() ?? 0;
|
|
88
|
-
onTimeUpdate?.(position, duration);
|
|
89
|
-
}, 1000);
|
|
90
|
-
} else if (event.data === State.PAUSED) {
|
|
91
|
-
onPause?.();
|
|
92
|
-
clearInterval(progressInterval.current);
|
|
93
|
-
} else if (event.data === State.ENDED) {
|
|
94
|
-
onEnded?.();
|
|
95
|
-
clearInterval(progressInterval.current);
|
|
96
|
-
}
|
|
49
|
+
// Attach the JS API to the real <iframe> React already rendered (kept in
|
|
50
|
+
// the markup so crawlers see a video element) instead of letting the API
|
|
51
|
+
// inject its own iframe client-side into an empty div.
|
|
52
|
+
loadYoutubeIframeApi()
|
|
53
|
+
.then(YT => {
|
|
54
|
+
if (cancelled || !iframeRef.current) return;
|
|
55
|
+
player = new YT.Player(iframeRef.current, {
|
|
56
|
+
events: {
|
|
57
|
+
onStateChange: (event: any) => {
|
|
58
|
+
const State = YT.PlayerState;
|
|
59
|
+
if (event.data === State.PLAYING) {
|
|
60
|
+
onPlay?.();
|
|
61
|
+
clearInterval(progressInterval.current);
|
|
62
|
+
progressInterval.current = setInterval(() => {
|
|
63
|
+
const duration = player?.getDuration?.() ?? 0;
|
|
64
|
+
const position = player?.getCurrentTime?.() ?? 0;
|
|
65
|
+
onTimeUpdate?.(position, duration);
|
|
66
|
+
}, 1000);
|
|
67
|
+
} else if (event.data === State.PAUSED) {
|
|
68
|
+
onPause?.();
|
|
69
|
+
clearInterval(progressInterval.current);
|
|
70
|
+
} else if (event.data === State.ENDED) {
|
|
71
|
+
onEnded?.();
|
|
72
|
+
clearInterval(progressInterval.current);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
97
75
|
},
|
|
98
|
-
}
|
|
76
|
+
});
|
|
77
|
+
})
|
|
78
|
+
.catch(() => {
|
|
79
|
+
// ignore API load failures; the iframe still plays on its own
|
|
99
80
|
});
|
|
100
|
-
}).catch(() => {
|
|
101
|
-
// ignore YouTube API load failures; component will remain empty
|
|
102
|
-
});
|
|
103
81
|
|
|
104
82
|
return () => {
|
|
105
83
|
cancelled = true;
|
|
106
84
|
clearInterval(progressInterval.current);
|
|
107
|
-
|
|
108
|
-
|
|
85
|
+
try {
|
|
86
|
+
player?.destroy?.();
|
|
87
|
+
} catch {
|
|
88
|
+
// iframe may already be detached by React on unmount
|
|
89
|
+
}
|
|
109
90
|
};
|
|
110
91
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
111
92
|
}, [embedId]);
|
|
112
93
|
|
|
94
|
+
if (!embedId) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// enablejsapi=1 lets YT.Player attach for analytics; autoplay/mute are driven
|
|
99
|
+
// via the URL so a fresh mount (on play) starts playback.
|
|
100
|
+
const params = new URLSearchParams({
|
|
101
|
+
enablejsapi: "1",
|
|
102
|
+
autoplay: autoplay ? "1" : "0",
|
|
103
|
+
mute: autoplay ? "1" : "0",
|
|
104
|
+
playsinline: "1",
|
|
105
|
+
rel: "0",
|
|
106
|
+
});
|
|
107
|
+
const src = `https://www.youtube.com/embed/${embedId}?${params.toString()}`;
|
|
108
|
+
|
|
113
109
|
return (
|
|
114
110
|
<div
|
|
115
111
|
className={cx(
|
|
@@ -117,10 +113,14 @@ export const YoutubeEmbed: React.FC<Props> = props => {
|
|
|
117
113
|
containerClassName
|
|
118
114
|
)}
|
|
119
115
|
>
|
|
120
|
-
<
|
|
121
|
-
ref={
|
|
116
|
+
<iframe
|
|
117
|
+
ref={iframeRef}
|
|
122
118
|
className="absolute left-0 top-0 h-full w-full"
|
|
119
|
+
src={src}
|
|
123
120
|
title="Embedded youtube"
|
|
121
|
+
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
|
122
|
+
allowFullScreen={true}
|
|
123
|
+
referrerPolicy="strict-origin-when-cross-origin"
|
|
124
124
|
/>
|
|
125
125
|
</div>
|
|
126
126
|
);
|
|
@@ -285,3 +285,251 @@ describe("AnchoredBottomBanner", () => {
|
|
|
285
285
|
consoleSpy.mockRestore();
|
|
286
286
|
});
|
|
287
287
|
});
|
|
288
|
+
|
|
289
|
+
describe("AnchoredBottomBanner scheduledMessages", () => {
|
|
290
|
+
// 2026-05-19T12:00:00Z is a Tuesday.
|
|
291
|
+
beforeEach(() => {
|
|
292
|
+
jest.useFakeTimers();
|
|
293
|
+
jest.setSystemTime(new Date("2026-05-19T12:00:00Z"));
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
afterEach(() => {
|
|
297
|
+
jest.useRealTimers();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("shows the active message's CTA, icon and countdown", () => {
|
|
301
|
+
render(
|
|
302
|
+
<AnchoredBottomBanner
|
|
303
|
+
ctaButtonLabel="Default label"
|
|
304
|
+
scheduledMessages={[
|
|
305
|
+
{
|
|
306
|
+
scheduledStart: "2026-05-18",
|
|
307
|
+
scheduleEnd: "2026-05-20",
|
|
308
|
+
daysToShow: ["ALL"],
|
|
309
|
+
countdownStartDateTime: "2026-05-19T11:00:00Z",
|
|
310
|
+
countdownEndDateTime: "2026-05-19T13:00:00Z",
|
|
311
|
+
ctaButtonLabel: "Flash sale",
|
|
312
|
+
iconName: "bolt",
|
|
313
|
+
},
|
|
314
|
+
]}
|
|
315
|
+
/>
|
|
316
|
+
);
|
|
317
|
+
expect(screen.getByText("Flash sale")).toBeInTheDocument();
|
|
318
|
+
expect(screen.queryByText("Default label")).not.toBeInTheDocument();
|
|
319
|
+
expect(screen.getByText("bolt")).toBeInTheDocument();
|
|
320
|
+
expect(screen.getByText(/01H : 00M : 00/)).toBeInTheDocument();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("treats scheduleEnd as inclusive of the whole end day", () => {
|
|
324
|
+
render(
|
|
325
|
+
<AnchoredBottomBanner
|
|
326
|
+
scheduledMessages={[
|
|
327
|
+
{
|
|
328
|
+
scheduledStart: "2026-05-17",
|
|
329
|
+
scheduleEnd: "2026-05-19",
|
|
330
|
+
countdownEndDateTime: "2026-05-19T13:00:00Z",
|
|
331
|
+
ctaButtonLabel: "Last day",
|
|
332
|
+
},
|
|
333
|
+
]}
|
|
334
|
+
/>
|
|
335
|
+
);
|
|
336
|
+
expect(screen.getByText("Last day")).toBeInTheDocument();
|
|
337
|
+
expect(screen.getByText(/01H : 00M : 00/)).toBeInTheDocument();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("falls back to default when today is outside the display window", () => {
|
|
341
|
+
render(
|
|
342
|
+
<AnchoredBottomBanner
|
|
343
|
+
ctaButtonLabel="Default label"
|
|
344
|
+
scheduledMessages={[
|
|
345
|
+
{
|
|
346
|
+
scheduledStart: "2026-05-25",
|
|
347
|
+
scheduleEnd: "2026-05-26",
|
|
348
|
+
countdownEndDateTime: "2026-05-25T13:00:00Z",
|
|
349
|
+
ctaButtonLabel: "Future message",
|
|
350
|
+
},
|
|
351
|
+
]}
|
|
352
|
+
/>
|
|
353
|
+
);
|
|
354
|
+
expect(screen.getByText("Default label")).toBeInTheDocument();
|
|
355
|
+
expect(screen.queryByText("Future message")).not.toBeInTheDocument();
|
|
356
|
+
expect(screen.queryByText(/H :/)).not.toBeInTheDocument();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("hides the message when daysToShow excludes today's weekday", () => {
|
|
360
|
+
render(
|
|
361
|
+
<AnchoredBottomBanner
|
|
362
|
+
ctaButtonLabel="Default label"
|
|
363
|
+
scheduledMessages={[
|
|
364
|
+
{
|
|
365
|
+
scheduledStart: "2026-05-18",
|
|
366
|
+
scheduleEnd: "2026-05-20",
|
|
367
|
+
daysToShow: ["MONDAY"],
|
|
368
|
+
countdownEndDateTime: "2026-05-19T13:00:00Z",
|
|
369
|
+
ctaButtonLabel: "Monday only",
|
|
370
|
+
},
|
|
371
|
+
]}
|
|
372
|
+
/>
|
|
373
|
+
);
|
|
374
|
+
expect(screen.getByText("Default label")).toBeInTheDocument();
|
|
375
|
+
expect(screen.queryByText("Monday only")).not.toBeInTheDocument();
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("shows the message when daysToShow includes today's weekday (abbrev)", () => {
|
|
379
|
+
render(
|
|
380
|
+
<AnchoredBottomBanner
|
|
381
|
+
scheduledMessages={[
|
|
382
|
+
{
|
|
383
|
+
scheduledStart: "2026-05-18",
|
|
384
|
+
scheduleEnd: "2026-05-20",
|
|
385
|
+
daysToShow: ["TUE"],
|
|
386
|
+
countdownEndDateTime: "2026-05-19T13:00:00Z",
|
|
387
|
+
ctaButtonLabel: "Tuesday deal",
|
|
388
|
+
},
|
|
389
|
+
]}
|
|
390
|
+
/>
|
|
391
|
+
);
|
|
392
|
+
expect(screen.getByText("Tuesday deal")).toBeInTheDocument();
|
|
393
|
+
expect(screen.getByText(/01H : 00M : 00/)).toBeInTheDocument();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("uses the active message's countdown, not the parent's", () => {
|
|
397
|
+
render(
|
|
398
|
+
<AnchoredBottomBanner
|
|
399
|
+
ctaButtonLabel="Default label"
|
|
400
|
+
enableCountdownTimer={true}
|
|
401
|
+
countdownEndDateTime="2026-05-19T12:00:30Z"
|
|
402
|
+
scheduledMessages={[
|
|
403
|
+
{
|
|
404
|
+
scheduledStart: "2026-05-18",
|
|
405
|
+
scheduleEnd: "2026-05-20",
|
|
406
|
+
countdownEndDateTime: "2026-05-19T12:00:10Z",
|
|
407
|
+
ctaButtonLabel: "Active message",
|
|
408
|
+
},
|
|
409
|
+
]}
|
|
410
|
+
/>
|
|
411
|
+
);
|
|
412
|
+
expect(screen.getByText("Active message")).toBeInTheDocument();
|
|
413
|
+
expect(screen.queryByText("Default label")).not.toBeInTheDocument();
|
|
414
|
+
expect(screen.getByText(/00H : 00M : 10/)).toBeInTheDocument();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("selects the first message whose window contains today", () => {
|
|
418
|
+
render(
|
|
419
|
+
<AnchoredBottomBanner
|
|
420
|
+
scheduledMessages={[
|
|
421
|
+
{
|
|
422
|
+
scheduledStart: "2026-05-01",
|
|
423
|
+
scheduleEnd: "2026-05-05",
|
|
424
|
+
ctaButtonLabel: "Past message",
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
scheduledStart: "2026-05-18",
|
|
428
|
+
scheduleEnd: "2026-05-20",
|
|
429
|
+
countdownEndDateTime: "2026-05-19T13:00:00Z",
|
|
430
|
+
ctaButtonLabel: "Current message",
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
scheduledStart: "2026-05-25",
|
|
434
|
+
scheduleEnd: "2026-05-26",
|
|
435
|
+
ctaButtonLabel: "Future message",
|
|
436
|
+
},
|
|
437
|
+
]}
|
|
438
|
+
/>
|
|
439
|
+
);
|
|
440
|
+
expect(screen.getByText("Current message")).toBeInTheDocument();
|
|
441
|
+
expect(screen.queryByText("Past message")).not.toBeInTheDocument();
|
|
442
|
+
expect(screen.queryByText("Future message")).not.toBeInTheDocument();
|
|
443
|
+
});
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
describe("AnchoredBottomBanner scheduling timezone (America/New_York)", () => {
|
|
447
|
+
// 2026-05-19T02:00:00Z is Mon 2026-05-18 22:00 EDT — a different ET calendar
|
|
448
|
+
// day AND weekday than UTC, so it exercises the EST/EDT scheduling rules.
|
|
449
|
+
beforeEach(() => {
|
|
450
|
+
jest.useFakeTimers();
|
|
451
|
+
jest.setSystemTime(new Date("2026-05-19T02:00:00Z"));
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
afterEach(() => {
|
|
455
|
+
jest.useRealTimers();
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
it("uses the ET calendar day for the display window, not UTC", () => {
|
|
459
|
+
render(
|
|
460
|
+
<AnchoredBottomBanner
|
|
461
|
+
ctaButtonLabel="Default label"
|
|
462
|
+
scheduledMessages={[
|
|
463
|
+
{
|
|
464
|
+
scheduledStart: "2026-05-18",
|
|
465
|
+
scheduleEnd: "2026-05-18",
|
|
466
|
+
countdownEndDateTime: "2026-05-25T00:00:00Z",
|
|
467
|
+
ctaButtonLabel: "ET Monday message",
|
|
468
|
+
},
|
|
469
|
+
]}
|
|
470
|
+
/>
|
|
471
|
+
);
|
|
472
|
+
// In ET it is still 2026-05-18, so the 05-18 window is active. Under the old
|
|
473
|
+
// UTC logic it would be 05-19 and this window would already be expired.
|
|
474
|
+
expect(screen.getByText("ET Monday message")).toBeInTheDocument();
|
|
475
|
+
expect(screen.queryByText("Default label")).not.toBeInTheDocument();
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("does not activate a window before its ET start day", () => {
|
|
479
|
+
render(
|
|
480
|
+
<AnchoredBottomBanner
|
|
481
|
+
ctaButtonLabel="Default label"
|
|
482
|
+
scheduledMessages={[
|
|
483
|
+
{
|
|
484
|
+
scheduledStart: "2026-05-19",
|
|
485
|
+
scheduleEnd: "2026-05-19",
|
|
486
|
+
countdownEndDateTime: "2026-05-25T00:00:00Z",
|
|
487
|
+
ctaButtonLabel: "ET Tuesday message",
|
|
488
|
+
},
|
|
489
|
+
]}
|
|
490
|
+
/>
|
|
491
|
+
);
|
|
492
|
+
// ET is still 05-18 (22:00), so the 05-19 window has not started yet.
|
|
493
|
+
expect(screen.getByText("Default label")).toBeInTheDocument();
|
|
494
|
+
expect(screen.queryByText("ET Tuesday message")).not.toBeInTheDocument();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("matches daysToShow against the ET weekday, not the UTC weekday", () => {
|
|
498
|
+
render(
|
|
499
|
+
<AnchoredBottomBanner
|
|
500
|
+
ctaButtonLabel="Default label"
|
|
501
|
+
scheduledMessages={[
|
|
502
|
+
{
|
|
503
|
+
scheduledStart: "2026-05-18",
|
|
504
|
+
scheduleEnd: "2026-05-20",
|
|
505
|
+
daysToShow: ["MONDAY"],
|
|
506
|
+
countdownEndDateTime: "2026-05-25T00:00:00Z",
|
|
507
|
+
ctaButtonLabel: "Monday only",
|
|
508
|
+
},
|
|
509
|
+
]}
|
|
510
|
+
/>
|
|
511
|
+
);
|
|
512
|
+
// ET weekday is Monday (the UTC weekday would be Tuesday).
|
|
513
|
+
expect(screen.getByText("Monday only")).toBeInTheDocument();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it("does not match a UTC weekday that differs from the ET weekday", () => {
|
|
517
|
+
render(
|
|
518
|
+
<AnchoredBottomBanner
|
|
519
|
+
ctaButtonLabel="Default label"
|
|
520
|
+
scheduledMessages={[
|
|
521
|
+
{
|
|
522
|
+
scheduledStart: "2026-05-18",
|
|
523
|
+
scheduleEnd: "2026-05-20",
|
|
524
|
+
daysToShow: ["TUESDAY"],
|
|
525
|
+
countdownEndDateTime: "2026-05-25T00:00:00Z",
|
|
526
|
+
ctaButtonLabel: "Tuesday only",
|
|
527
|
+
},
|
|
528
|
+
]}
|
|
529
|
+
/>
|
|
530
|
+
);
|
|
531
|
+
// UTC says Tuesday, but ET says Monday -> the message must not show.
|
|
532
|
+
expect(screen.getByText("Default label")).toBeInTheDocument();
|
|
533
|
+
expect(screen.queryByText("Tuesday only")).not.toBeInTheDocument();
|
|
534
|
+
});
|
|
535
|
+
});
|