@windstream/react-shared-components 0.2.33 → 0.2.34

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.
Files changed (25) hide show
  1. package/dist/contentful/index.d.ts +1 -5
  2. package/dist/contentful/index.esm.js +2 -2
  3. package/dist/contentful/index.esm.js.map +1 -1
  4. package/dist/contentful/index.js +3 -3
  5. package/dist/contentful/index.js.map +1 -1
  6. package/dist/styles.css +1 -1
  7. package/package.json +1 -1
  8. package/src/components/video-link/contentful-video-embed.test.tsx +76 -0
  9. package/src/components/video-link/contentful-video-embed.tsx +42 -0
  10. package/src/components/video-link/index.test.tsx +227 -0
  11. package/src/components/video-link/index.tsx +142 -0
  12. package/src/components/video-link/types.ts +28 -0
  13. package/src/contentful/blocks/blogs-grid-base/index.tsx +1 -1
  14. package/src/contentful/blocks/button/index.test.tsx +52 -0
  15. package/src/contentful/blocks/button/index.tsx +8 -3
  16. package/src/contentful/blocks/image-promo-bar/index.test.tsx +15 -198
  17. package/src/contentful/blocks/image-promo-bar/index.tsx +8 -92
  18. package/src/contentful/blocks/image-promo-bar/types.ts +1 -17
  19. package/src/contentful/blocks/primary-hero/index.tsx +18 -18
  20. /package/src/{contentful/blocks/image-promo-bar → components/video-link}/helper.test.tsx +0 -0
  21. /package/src/{contentful/blocks/image-promo-bar → components/video-link}/helper.tsx +0 -0
  22. /package/src/{contentful/blocks/image-promo-bar → components/video-link}/vimeo-embed.test.tsx +0 -0
  23. /package/src/{contentful/blocks/image-promo-bar → components/video-link}/vimeo-embed.tsx +0 -0
  24. /package/src/{contentful/blocks/image-promo-bar → components/video-link}/youtube-embed.test.tsx +0 -0
  25. /package/src/{contentful/blocks/image-promo-bar → components/video-link}/youtube-embed.tsx +0 -0
@@ -2,7 +2,7 @@ import React from "react";
2
2
  import { ImagePromoBar } from "./index";
3
3
  import { ImagePromoBarProps } from "./types";
4
4
 
5
- import { fireEvent, render, screen } from "@testing-library/react";
5
+ import { render, screen } from "@testing-library/react";
6
6
 
7
7
  jest.mock("../button", () => ({
8
8
  __esModule: true,
@@ -25,24 +25,11 @@ jest.mock("../button", () => ({
25
25
  ),
26
26
  }));
27
27
 
28
- jest.mock("./helper", () => ({
29
- PlayButton: ({ isHovered }: any) => (
30
- <div data-testid="play-button" data-hovered={isHovered} />
31
- ),
32
- }));
33
-
34
- jest.mock("./vimeo-embed", () => ({
35
- VimeoEmbed: ({ link, autoplay }: any) => (
36
- <div data-testid="vimeo-embed" data-link={link} data-autoplay={autoplay} />
37
- ),
38
- }));
39
-
40
- jest.mock("./youtube-embed", () => ({
41
- YoutubeEmbed: ({ link, autoplay }: any) => (
28
+ jest.mock("@shared/components/video-link", () => ({
29
+ VideoLink: ({ videoLink }: any) => (
42
30
  <div
43
- data-testid="youtube-embed"
44
- data-link={link}
45
- data-autoplay={autoplay}
31
+ data-testid="video-link"
32
+ data-link={videoLink?.link || videoLink?.videoAssetUrl || ""}
46
33
  />
47
34
  ),
48
35
  }));
@@ -304,109 +291,7 @@ describe("ImagePromoBar", () => {
304
291
  });
305
292
 
306
293
  describe("Video", () => {
307
- it("renders video section with play button when videoLink has link and image", () => {
308
- render(
309
- <ImagePromoBar
310
- {...defaultProps}
311
- videoLink={{
312
- link: "https://youtube.com/watch?v=123",
313
- image: "thumb.jpg",
314
- }}
315
- />
316
- );
317
- expect(screen.getByTestId("play-button")).toBeInTheDocument();
318
- expect(screen.getByAltText("Video preview")).toBeInTheDocument();
319
- });
320
-
321
- it("shows youtube embed on click for youtube link", () => {
322
- render(
323
- <ImagePromoBar
324
- {...defaultProps}
325
- videoLink={{
326
- link: "https://youtube.com/watch?v=123",
327
- image: "thumb.jpg",
328
- videoPopup: false,
329
- }}
330
- />
331
- );
332
- fireEvent.click(
333
- screen.getByTestId("play-button").closest(".video-section")!
334
- );
335
- expect(screen.getByTestId("youtube-embed")).toBeInTheDocument();
336
- });
337
-
338
- it("shows vimeo embed on click for vimeo link", () => {
339
- render(
340
- <ImagePromoBar
341
- {...defaultProps}
342
- videoLink={{
343
- link: "https://vimeo.com/123",
344
- image: "thumb.jpg",
345
- videoPopup: false,
346
- }}
347
- />
348
- );
349
- fireEvent.click(
350
- screen.getByTestId("play-button").closest(".video-section")!
351
- );
352
- expect(screen.getByTestId("vimeo-embed")).toBeInTheDocument();
353
- });
354
-
355
- it("shows popup overlay when videoPopup is true and playing", () => {
356
- const { container } = render(
357
- <ImagePromoBar
358
- {...defaultProps}
359
- videoLink={{
360
- link: "https://youtube.com/watch?v=123",
361
- image: "thumb.jpg",
362
- videoPopup: true,
363
- }}
364
- />
365
- );
366
- fireEvent.click(
367
- screen.getByTestId("play-button").closest(".video-section")!
368
- );
369
- expect(container.querySelector(".fixed")).toBeInTheDocument();
370
- });
371
-
372
- it("closes popup on overlay click", () => {
373
- const { container } = render(
374
- <ImagePromoBar
375
- {...defaultProps}
376
- videoLink={{
377
- link: "https://youtube.com/watch?v=123",
378
- image: "thumb.jpg",
379
- videoPopup: true,
380
- }}
381
- />
382
- );
383
- fireEvent.click(
384
- screen.getByTestId("play-button").closest(".video-section")!
385
- );
386
- fireEvent.click(container.querySelector(".fixed")!);
387
- expect(container.querySelector(".fixed")).not.toBeInTheDocument();
388
- });
389
-
390
- it("does not close popup when clicking inside video container", () => {
391
- const { container } = render(
392
- <ImagePromoBar
393
- {...defaultProps}
394
- videoLink={{
395
- link: "https://youtube.com/watch?v=123",
396
- image: "thumb.jpg",
397
- videoPopup: true,
398
- }}
399
- />
400
- );
401
- fireEvent.click(
402
- screen.getByTestId("play-button").closest(".video-section")!
403
- );
404
- const inner = container.querySelector(".max-w-6xl");
405
- fireEvent.click(inner!);
406
- expect(container.querySelector(".fixed")).toBeInTheDocument();
407
- });
408
-
409
- it("handles hover state on video section", () => {
294
+ it("renders VideoLink when videoLink is provided", () => {
410
295
  render(
411
296
  <ImagePromoBar
412
297
  {...defaultProps}
@@ -416,93 +301,25 @@ describe("ImagePromoBar", () => {
416
301
  }}
417
302
  />
418
303
  );
419
- const videoSection = screen
420
- .getByTestId("play-button")
421
- .closest(".video-section")!;
422
- fireEvent.mouseEnter(videoSection);
423
- expect(screen.getByTestId("play-button")).toHaveAttribute(
424
- "data-hovered",
425
- "true"
426
- );
427
- fireEvent.mouseLeave(videoSection);
428
- expect(screen.getByTestId("play-button")).toHaveAttribute(
429
- "data-hovered",
430
- "false"
431
- );
304
+ expect(screen.getByTestId("video-link")).toBeInTheDocument();
432
305
  });
433
306
 
434
- it("does not render video section when videoLink.link is empty", () => {
435
- const { container } = render(
436
- <ImagePromoBar
437
- {...defaultProps}
438
- videoLink={{ link: "", image: "thumb.jpg" }}
439
- />
440
- );
441
- expect(container.querySelector(".video-section")).not.toBeInTheDocument();
442
- });
443
-
444
- it("does not render video preview image when videoLink.image is missing", () => {
307
+ it("passes the videoLink prop through to VideoLink", () => {
445
308
  render(
446
309
  <ImagePromoBar
447
310
  {...defaultProps}
448
- videoLink={{ link: "https://youtube.com/watch?v=123" }}
311
+ videoLink={{ link: "https://vimeo.com/123", image: "thumb.jpg" }}
449
312
  />
450
313
  );
451
- expect(screen.queryByAltText("Video preview")).not.toBeInTheDocument();
452
- });
453
-
454
- it("handles videoLink being undefined", () => {
455
- const { container } = render(
456
- <ImagePromoBar {...defaultProps} videoLink={undefined as any} />
314
+ expect(screen.getByTestId("video-link")).toHaveAttribute(
315
+ "data-link",
316
+ "https://vimeo.com/123"
457
317
  );
458
- expect(container.querySelector(".video-section")).not.toBeInTheDocument();
459
318
  });
460
319
 
461
- it("handles videoLink with no videoPopup property (defaults to false)", () => {
462
- const { container } = render(
463
- <ImagePromoBar
464
- {...defaultProps}
465
- videoLink={{
466
- link: "https://youtube.com/watch?v=123",
467
- image: "thumb.jpg",
468
- }}
469
- />
470
- );
471
- fireEvent.click(container.querySelector(".video-section")!);
472
- // Should show inline embed, not popup
473
- expect(container.querySelector(".fixed")).not.toBeInTheDocument();
474
- expect(screen.getByTestId("youtube-embed")).toBeInTheDocument();
475
- });
476
-
477
- it("does not render inline embed when videoPopup is true", () => {
478
- const { container } = render(
479
- <ImagePromoBar
480
- {...defaultProps}
481
- videoLink={{
482
- link: "https://youtube.com/watch?v=123",
483
- image: "thumb.jpg",
484
- videoPopup: true,
485
- }}
486
- />
487
- );
488
- fireEvent.click(container.querySelector(".video-section")!);
489
- // Popup renders, inline section hidden via the !videoPopup && isPlaying condition
490
- expect(container.querySelector(".fixed")).toBeInTheDocument();
491
- });
492
-
493
- it("does not render contentVideo embed when videoLink has no image", () => {
494
- const { container } = render(
495
- <ImagePromoBar
496
- {...defaultProps}
497
- videoLink={{
498
- link: "https://youtube.com/watch?v=123",
499
- videoPopup: false,
500
- }}
501
- />
502
- );
503
- fireEvent.click(container.querySelector(".video-section")!);
504
- // contentVideo is null when videoImage is falsy
505
- expect(screen.queryByTestId("youtube-embed")).not.toBeInTheDocument();
320
+ it("renders VideoLink even when videoLink has no link (VideoLink owns the gating)", () => {
321
+ render(<ImagePromoBar {...defaultProps} videoLink={{}} />);
322
+ expect(screen.getByTestId("video-link")).toBeInTheDocument();
506
323
  });
507
324
  });
508
325
  });
@@ -1,15 +1,13 @@
1
- import React, { useState } from "react";
1
+ import React from "react";
2
2
  import { Button } from "../button";
3
- import { PlayButton } from "./helper";
4
3
  import { ImagePromoBarProps } from "./types";
5
- import { VimeoEmbed } from "./vimeo-embed";
6
- import { YoutubeEmbed } from "./youtube-embed";
7
4
 
8
5
  import { Checklist } from "@shared/components/checklist";
9
6
  import { Image } from "@shared/components/image";
10
7
  import { Link } from "@shared/components/link";
11
8
  import { NextImage } from "@shared/components/next-image";
12
9
  import { Text } from "@shared/components/text";
10
+ import { VideoLink } from "@shared/components/video-link";
13
11
  import { cx } from "@shared/utils";
14
12
 
15
13
  export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
@@ -38,32 +36,6 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
38
36
  renderCheckPlans,
39
37
  enableAnimation = false,
40
38
  }) => {
41
- const [activeVideo, setActiveVideo] = useState("");
42
- const [isHovered, setIsHovered] = useState(false);
43
-
44
- const handlePlayClick = () => {
45
- setActiveVideo(videoLink?.link ?? "");
46
- };
47
-
48
- const handleCloseVideo = (e: React.MouseEvent) => {
49
- e.stopPropagation();
50
- setActiveVideo("");
51
- };
52
-
53
- const videoHref = videoLink?.link ?? "";
54
- const videoImage = videoLink?.image;
55
- const videoPopup = videoLink?.videoPopup ?? false;
56
- const isPlaying = Boolean(activeVideo && activeVideo === videoHref);
57
-
58
- const embedSelector = () => {
59
- if (videoHref.includes("vimeo")) {
60
- return <VimeoEmbed link={videoHref} autoplay={isPlaying} />;
61
- } else {
62
- return <YoutubeEmbed link={videoHref} autoplay={isPlaying} />;
63
- }
64
- };
65
- const contentVideo = videoLink && videoImage ? embedSelector() : null;
66
-
67
39
  return (
68
40
  <div className="component-container shared-component-ipb pb-16 pt-16">
69
41
  <div
@@ -219,72 +191,16 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
219
191
  </div>
220
192
  )}
221
193
  {/* Video Link Section */}
222
- {videoLink?.link && (
223
- <div
224
- className={cx(
225
- "video-section relative w-full cursor-pointer rounded-image transition-all duration-300 lg:w-[486px]",
226
- !isPlaying && "hover:ring-2 hover:ring-border-focus"
227
- )}
228
- onClick={handlePlayClick}
229
- onMouseEnter={() => setIsHovered(true)}
230
- onMouseLeave={() => setIsHovered(false)}
231
- >
232
- {/* Preview Image & Play Button */}
233
- <div
234
- className={cx(
235
- isPlaying && !videoPopup && "hidden",
236
- isPlaying && !videoPopup ? "opacity-0" : "opacity-100",
237
- "relative w-full transition-opacity duration-300"
238
- )}
239
- >
240
- {videoLink.image && (
241
- <NextImage
242
- src={videoLink.image}
243
- alt="Video preview"
244
- width={imageWidth || 660}
245
- height={imageHeight || 660}
246
- sizes="(max-width: 430px) 100vw, (max-width: 834px) 100vw, 50vw"
247
- className="h-auto w-full rounded-image"
248
- />
249
- )}
250
- <div className="absolute inset-0 flex items-center justify-center">
251
- <PlayButton isHovered={isHovered} />
252
- </div>
253
- </div>
254
-
255
- {/* Inline Player (if not popup) */}
256
- {!videoPopup && isPlaying && (
257
- <div
258
- className={cx(
259
- "aspect-[16/9] w-full overflow-hidden rounded-image transition-opacity duration-300",
260
- isPlaying ? "opacity-100" : "opacity-0",
261
- !useOriginalImageAspectRatio &&
262
- "xl:aspect-square xl:max-h-[486px] xl:w-[486px]"
263
- )}
264
- >
265
- {contentVideo}
266
- </div>
267
- )}
268
- </div>
269
- )}
194
+ <VideoLink
195
+ videoLink={videoLink}
196
+ imageWidth={imageWidth}
197
+ imageHeight={imageHeight}
198
+ useOriginalImageAspectRatio={useOriginalImageAspectRatio}
199
+ />
270
200
  </aside>
271
201
  </div>
272
202
  </div>
273
203
 
274
- {/* Video Popup Overlay */}
275
- {videoPopup && isPlaying && (
276
- <div
277
- className="fixed inset-0 top-20 z-[100] flex items-center justify-center bg-black/80 p-4 transition-all duration-300"
278
- onClick={handleCloseVideo}
279
- >
280
- <div
281
- className="max-w-6xl aspect-video w-full overflow-hidden rounded-3xl bg-black shadow-2xl md:w-4/6"
282
- onClick={e => e.stopPropagation()}
283
- >
284
- {contentVideo}
285
- </div>
286
- </div>
287
- )}
288
204
  {disclaimer && (
289
205
  <div className="mt-10 flex justify-center xl:mt-18">
290
206
  <div>{disclaimer}</div>
@@ -1,19 +1,8 @@
1
1
  import React from "react";
2
2
 
3
+ import { VideoLinkProps } from "@shared/components/video-link";
3
4
  import { CheckPlansProps } from "@shared/types/micro-components";
4
5
 
5
- export type VideoLinkProps = {
6
- image?: string;
7
- videoPopup?: boolean;
8
- link?: string;
9
- };
10
-
11
- export type VideoEmbedProps = {
12
- containerClassName?: string;
13
- autoplay?: boolean;
14
- debug?: boolean;
15
- } & VideoLinkProps;
16
-
17
6
  export type ImagePromoBarProps = {
18
7
  brow: string;
19
8
  enableHeading: boolean;
@@ -41,8 +30,3 @@ export type ImagePromoBarProps = {
41
30
  onClick?: (event: React.MouseEvent, source: "primary" | "secondary") => void;
42
31
  renderCheckPlans?: (overrides?: CheckPlansProps) => React.ReactNode;
43
32
  };
44
-
45
- export type PlayButtonProps = {
46
- isHovered?: boolean;
47
- containerClassName?: string;
48
- };
@@ -55,7 +55,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
55
55
  const bottomLinkLabel = bottomLink?.buttonLabel ?? bottomLink?.label;
56
56
  const textColorClass = textColor === "light" ? " text-white" : " text-text";
57
57
  return (
58
- <div className="component-container relative overflow-hidden p-5 xl:flex xl:min-h-[724px] xl:items-center xl:p-0 xl:py-4">
58
+ <div className="component-container relative overflow-hidden p-4 sm:px-6 sm:py-12 xl:flex xl:min-h-[724px] xl:items-center xl:px-10">
59
59
  {hasDesktopVideo ? (
60
60
  <video
61
61
  autoPlay={true}
@@ -72,13 +72,13 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
72
72
  ) : null}
73
73
  <div
74
74
  className={cx(
75
- "primary-hero-container relative z-10 mx-auto flex w-full flex-col sm:items-center sm:text-center xl:h-full xl:max-w-120 xl:flex-row xl:items-center xl:justify-between xl:gap-4 xl:text-left"
75
+ "primary-hero-container relative z-10 mx-auto flex w-full max-w-[626px] flex-col sm:items-center sm:text-center xl:h-full xl:max-w-120 xl:flex-row xl:items-center xl:justify-between xl:gap-4 xl:text-left"
76
76
  )}
77
77
  >
78
78
  {/* Left column: navy background, headline, body, address, button, link */}
79
79
  <div
80
80
  className={cx(
81
- `relative flex flex-col ${textColorClass} sm:w-[485px]`
81
+ `relative flex flex-col ${textColorClass} w-full xl:w-[485px]`
82
82
  )}
83
83
  >
84
84
  <div className="flex flex-col gap-5 lg:gap-6">
@@ -169,7 +169,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
169
169
  <div className="flex flex-col gap-3">
170
170
  {/* desktop CTA */}
171
171
  {showCtasSideBySide ? (
172
- <div className="flex flex-col justify-center gap-3 sm:flex-row sm:gap-4">
172
+ <div className="flex flex-col justify-center gap-3 sm:flex-row sm:gap-4 xl:justify-start">
173
173
  <div className={cx(hideOnMobileCta1 && "hidden md:block")}>
174
174
  <Button
175
175
  {...primaryCta1}
@@ -221,7 +221,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
221
221
  )}
222
222
  </>
223
223
  )}
224
- {secondaryCtaPrefix && secondaryCta ? (
224
+ {secondaryCtaPrefix || secondaryCta ? (
225
225
  <div className="hidden md:flex">
226
226
  {secondaryCtaPrefix && (
227
227
  <Text as="span" className="body2 mr-1">
@@ -253,7 +253,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
253
253
  </div>
254
254
 
255
255
  {/* mobile hero image and bottom link */}
256
- <div className="xl:hidden">
256
+ <div className="w-full xl:hidden">
257
257
  <div className={"relative my-8"}>
258
258
  {hasMobileVideo ? (
259
259
  <video
@@ -263,7 +263,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
263
263
  playsInline={true}
264
264
  preload="auto"
265
265
  aria-label={heroVideoMobile?.title || title || "Hero video"}
266
- className="aspect-[1.71:1] sm:aspect-[1.41:1] w-full rounded-[40px] object-cover object-center sm:h-[420px]"
266
+ className="h-[350px] w-full rounded-[40px] object-cover object-center sm:h-[420px] md:h-[520px]"
267
267
  data-testid="hero-video-mobile"
268
268
  >
269
269
  <source src={mobileVideoUrl} type="video/mp4" />
@@ -285,7 +285,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
285
285
  <NextImage
286
286
  src={carouselImages[0]}
287
287
  alt={"Hero"}
288
- className="aspect-[1.71:1] sm:aspect-[1.41:1] w-full rounded-[40px] object-cover object-center sm:h-[420px]"
288
+ className="h-[205px] w-full rounded-[40px] object-cover object-center sm:h-[420px]"
289
289
  width={1024}
290
290
  height={600}
291
291
  sizes="100vw"
@@ -297,16 +297,6 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
297
297
  )}
298
298
  </div>
299
299
 
300
- {bottomLink && (bottomLinkLabel || bottomLink?.href) && (
301
- <div className="md:hidden">
302
- <Button
303
- {...bottomLink}
304
- onModalButtonClick={onModalButtonClick}
305
- linkClassName={`body3 ${textColorClass}`}
306
- />
307
- </div>
308
- )}
309
-
310
300
  {secondaryCtaPrefix || secondaryCta ? (
311
301
  <div
312
302
  className={`flex flex-wrap items-center ${textColorClass} md:hidden`}
@@ -325,6 +315,16 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
325
315
  )}
326
316
  </div>
327
317
  ) : null}
318
+
319
+ {bottomLink && (bottomLinkLabel || bottomLink?.href) && (
320
+ <div className="md:hidden">
321
+ <Button
322
+ {...bottomLink}
323
+ onModalButtonClick={onModalButtonClick}
324
+ linkClassName={`body3 ${textColorClass}`}
325
+ />
326
+ </div>
327
+ )}
328
328
  </div>
329
329
 
330
330
  {/* Right column: bright blue background, diagonal top edge, hero image */}