@windstream/react-shared-components 0.2.49 → 0.2.51

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 (46) hide show
  1. package/dist/contentful/index.d.ts +12 -1
  2. package/dist/contentful/index.esm.js +3 -3
  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/core.d.ts +3 -1
  7. package/dist/index.d.ts +4 -2
  8. package/dist/index.esm.js +6 -6
  9. package/dist/index.esm.js.map +1 -1
  10. package/dist/index.js +6 -6
  11. package/dist/index.js.map +1 -1
  12. package/dist/next/index.esm.js +2 -2
  13. package/dist/next/index.esm.js.map +1 -1
  14. package/dist/next/index.js +2 -2
  15. package/dist/next/index.js.map +1 -1
  16. package/dist/styles.css +1 -1
  17. package/package.json +2 -1
  18. package/src/components/next-image/index.tsx +10 -1
  19. package/src/components/video-link/contentful-video-embed.tsx +17 -1
  20. package/src/components/video-link/index.test.tsx +196 -24
  21. package/src/components/video-link/index.tsx +84 -15
  22. package/src/components/video-link/types.ts +15 -1
  23. package/src/components/video-link/vimeo-embed.tsx +51 -2
  24. package/src/components/video-link/youtube-embed.test.tsx +81 -54
  25. package/src/components/video-link/youtube-embed.tsx +97 -16
  26. package/src/components/video-link/youtube-iframe-api-loader.ts +45 -0
  27. package/src/contentful/blocks/callout/index.test.tsx +2 -2
  28. package/src/contentful/blocks/callout/index.tsx +1 -0
  29. package/src/contentful/blocks/cards/blog-card/index.tsx +9 -2
  30. package/src/contentful/blocks/cards/blog-card/types.ts +4 -0
  31. package/src/contentful/blocks/cards/floating-image-card/index.tsx +20 -6
  32. package/src/contentful/blocks/cards/floating-image-card/types.ts +4 -0
  33. package/src/contentful/blocks/cards/full-image-card/index.tsx +2 -1
  34. package/src/contentful/blocks/cards/full-image-card/types.ts +4 -0
  35. package/src/contentful/blocks/cards/simple-card/index.tsx +2 -2
  36. package/src/contentful/blocks/cards/simple-card/types.ts +4 -0
  37. package/src/contentful/blocks/find-kinetic/index.tsx +20 -7
  38. package/src/contentful/blocks/find-kinetic/types.ts +1 -0
  39. package/src/contentful/blocks/image-promo-bar/index.test.tsx +2 -2
  40. package/src/contentful/blocks/image-promo-bar/index.tsx +4 -1
  41. package/src/contentful/blocks/image-promo-bar/types.ts +6 -1
  42. package/src/contentful/blocks/primary-hero/index.tsx +6 -6
  43. package/src/hooks/contentful/use-contentful-rich-text.test.tsx +6 -6
  44. package/src/hooks/use-fullscreen-toggle.ts +23 -0
  45. package/src/hooks/use-video-analytics.ts +126 -0
  46. package/src/types/vimeo-player.d.ts +11 -0
@@ -6,6 +6,7 @@ export type FindKineticProps = {
6
6
  localPathPrefix?: string;
7
7
  background?: "blue" | "green" | "navy" | "purple" | "white" | "yellow";
8
8
  image?: string;
9
+ imageAltText?: string;
9
10
  list?: { name: string; code: string }[];
10
11
  maxWidth?: boolean;
11
12
  color?: "dark" | "light";
@@ -177,14 +177,14 @@ describe("ImagePromoBar", () => {
177
177
  expect(container.querySelector(".max-w-120")).not.toBeInTheDocument();
178
178
  });
179
179
 
180
- it("applies lg:flex-row-reverse when mediaPosition is true", () => {
180
+ it("applies xl:flex-row-reverse when mediaPosition is true", () => {
181
181
  const { container } = render(
182
182
  <ImagePromoBar {...defaultProps} mediaPosition={true} />
183
183
  );
184
184
  expect(container.innerHTML).toContain("xl:flex-row-reverse");
185
185
  });
186
186
 
187
- it("applies lg:flex-row when mediaPosition is false", () => {
187
+ it("applies xl:flex-row when mediaPosition is false", () => {
188
188
  const { container } = render(
189
189
  <ImagePromoBar {...defaultProps} mediaPosition={false} />
190
190
  );
@@ -19,6 +19,7 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
19
19
  disclaimer,
20
20
  description,
21
21
  image,
22
+ imageAltText,
22
23
  imageLinks,
23
24
  mediaPosition = true,
24
25
  checklist,
@@ -33,6 +34,7 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
33
34
  isAboveTheFold = false,
34
35
  onModalButtonClick,
35
36
  onClick,
37
+ onVideoEvent,
36
38
  renderCheckPlans,
37
39
  enableAnimation = false,
38
40
  }) => {
@@ -174,7 +176,7 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
174
176
  >
175
177
  <NextImage
176
178
  src={image}
177
- alt="section-image"
179
+ alt={imageAltText ?? "section-image"}
178
180
  priority={isAboveTheFold}
179
181
  fill={!useOriginalImageAspectRatio}
180
182
  width={
@@ -199,6 +201,7 @@ export const ImagePromoBar: React.FC<ImagePromoBarProps> = ({
199
201
  imageHeight={imageHeight}
200
202
  useOriginalImageAspectRatio={useOriginalImageAspectRatio}
201
203
  isAboveTheFold={isAboveTheFold}
204
+ onVideoEvent={onVideoEvent}
202
205
  />
203
206
  </aside>
204
207
  </div>
@@ -1,6 +1,9 @@
1
1
  import React from "react";
2
2
 
3
- import { VideoLinkProps } from "@shared/components/video-link";
3
+ import {
4
+ VideoAnalyticsHandler,
5
+ VideoLinkProps,
6
+ } from "@shared/components/video-link";
4
7
  import { CheckPlansProps } from "@shared/types/micro-components";
5
8
 
6
9
  export type ImagePromoBarProps = {
@@ -9,6 +12,7 @@ export type ImagePromoBarProps = {
9
12
  title: string;
10
13
  subTitle: string;
11
14
  image: string;
15
+ imageAltText?: string;
12
16
  imageWidth: number;
13
17
  imageHeight: number;
14
18
  useOriginalImageAspectRatio?: boolean;
@@ -26,6 +30,7 @@ export type ImagePromoBarProps = {
26
30
  maxWidth?: boolean;
27
31
  color: "light" | "dark";
28
32
  enableAnimation?: boolean;
33
+ onVideoEvent?: VideoAnalyticsHandler;
29
34
  onModalButtonClick?: (id?: string) => void;
30
35
  onClick?: (event: React.MouseEvent, source: "primary" | "secondary") => void;
31
36
  renderCheckPlans?: (overrides?: CheckPlansProps) => React.ReactNode;
@@ -75,7 +75,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
75
75
  />
76
76
  ) : null;
77
77
  return (
78
- <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">
78
+ <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-4">
79
79
  {hasDesktopVideo ? (
80
80
  <video
81
81
  autoPlay={true}
@@ -98,7 +98,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
98
98
  {/* Left column: navy background, headline, body, address, button, link */}
99
99
  <div
100
100
  className={cx(
101
- `relative flex flex-col ${textColorClass} w-full xl:w-[485px]`
101
+ `relative flex flex-col ${textColorClass} w-full xl:w-[486px]`
102
102
  )}
103
103
  >
104
104
  <div className="flex flex-col gap-5 lg:gap-6">
@@ -361,17 +361,17 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
361
361
  <div
362
362
  className={cx(
363
363
  "relative hidden xl:block",
364
- hasDesktopVideo && "h-[600px] w-[600px]"
364
+ hasDesktopVideo && "h-[584px] w-[584px]"
365
365
  )}
366
366
  >
367
- <Badge className="absolute -left-10 top-18 z-10 aspect-square w-52 object-cover object-center" />
367
+ <Badge className="absolute -left-[114px] top-[62px] z-10 aspect-square w-[200px] object-cover object-center" />
368
368
  {!hasDesktopVideo && heroImageUrl ? (
369
369
  <NextImage
370
370
  src={heroImageUrl}
371
371
  alt={"Hero"}
372
372
  className="aspect-square rounded-[40px] object-cover object-center"
373
- width={600}
374
- height={600}
373
+ width={584}
374
+ height={584}
375
375
  quality={90}
376
376
  loading="eager"
377
377
  />
@@ -243,14 +243,14 @@ describe("renderContentfulRichText", () => {
243
243
  const a = container.querySelector("a")!;
244
244
  expect(a).toHaveAttribute("href", "https://example.com");
245
245
  expect(a).toHaveAttribute("target", "_blank");
246
- expect(a).toHaveAttribute("rel", "noopener noreferrer");
246
+ expect(a).toHaveAttribute("rel", "noopener");
247
247
  });
248
248
 
249
- it("renders internal link with target=_self for relative URLs", () => {
249
+ it("renders internal link without target for relative URLs", () => {
250
250
  const doc = makeDoc(makeHyperlink("/about", "About"));
251
251
  const { container } = render(<>{renderContentfulRichText(doc)}</>);
252
252
  const a = container.querySelector("a")!;
253
- expect(a).toHaveAttribute("target", "_self");
253
+ expect(a).not.toHaveAttribute("target");
254
254
  expect(a).not.toHaveAttribute("rel");
255
255
  });
256
256
 
@@ -265,7 +265,7 @@ describe("renderContentfulRichText", () => {
265
265
  const doc = makeDoc(makeHyperlink("https://example.com", "External"));
266
266
  const { container } = render(<>{renderContentfulRichText(doc, false)}</>);
267
267
  const a = container.querySelector("a")!;
268
- expect(a).toHaveAttribute("target", "_self");
268
+ expect(a).not.toHaveAttribute("target");
269
269
  });
270
270
 
271
271
  it("applies custom linkClassName", () => {
@@ -536,13 +536,13 @@ describe("renderContentfulRichText", () => {
536
536
  expect(a).toHaveAttribute("target", "_blank");
537
537
  });
538
538
 
539
- it("renders internal link with _self when target is undefined", () => {
539
+ it("renders internal link without target when target is undefined", () => {
540
540
  const doc = makeDoc(makeHyperlink("/page", "Page"));
541
541
  const { container } = render(
542
542
  <>{renderContentfulRichText(doc, undefined)}</>
543
543
  );
544
544
  const a = container.querySelector("a")!;
545
- expect(a).toHaveAttribute("target", "_self");
545
+ expect(a).not.toHaveAttribute("target");
546
546
  });
547
547
 
548
548
  it("handles hyperlink with missing uri data", () => {
@@ -0,0 +1,23 @@
1
+ import { useEffect, type RefObject } from "react";
2
+
3
+ export function useFullscreenToggle(
4
+ ref: RefObject<Element>,
5
+ onToggle?: (isFullscreen: boolean) => void
6
+ ) {
7
+ useEffect(() => {
8
+ if (!onToggle) return;
9
+
10
+ const handleChange = () => {
11
+ const fullscreenElement =
12
+ document.fullscreenElement || (document as any).webkitFullscreenElement;
13
+ onToggle(fullscreenElement === ref.current);
14
+ };
15
+
16
+ document.addEventListener("fullscreenchange", handleChange);
17
+ document.addEventListener("webkitfullscreenchange", handleChange);
18
+ return () => {
19
+ document.removeEventListener("fullscreenchange", handleChange);
20
+ document.removeEventListener("webkitfullscreenchange", handleChange);
21
+ };
22
+ }, [onToggle, ref]);
23
+ }
@@ -0,0 +1,126 @@
1
+ import { useRef } from "react";
2
+
3
+ export type VideoProvider = "youtube" | "vimeo" | "contentful";
4
+
5
+ export type VideoAnalyticsEventName =
6
+ | "played"
7
+ | "paused"
8
+ | "progress"
9
+ | "completed"
10
+ | "fullscreen_toggled"
11
+ | "closed";
12
+
13
+ export type VideoAnalyticsPayload = {
14
+ videoId: string;
15
+ videoTitle?: string;
16
+ provider: VideoProvider;
17
+ position: number;
18
+ duration: number;
19
+ percentComplete: number;
20
+ isFullscreen?: boolean;
21
+ };
22
+
23
+ export type VideoAnalyticsHandler = (
24
+ event: VideoAnalyticsEventName,
25
+ payload: VideoAnalyticsPayload
26
+ ) => void;
27
+
28
+ export type VideoAnalyticsMeta = {
29
+ videoId: string;
30
+ videoTitle?: string;
31
+ provider: VideoProvider;
32
+ };
33
+
34
+ const PROGRESS_MILESTONES = [25, 50, 75];
35
+
36
+ // Centralizes progress-milestone/completion dedup so each provider embed
37
+ // only has to report raw play/pause/timeupdate/ended/fullscreen callbacks -
38
+ // none of them know about Segment or the unified event schema.
39
+ export function useVideoAnalytics(onVideoEvent?: VideoAnalyticsHandler) {
40
+ const firedMilestones = useRef<Set<number>>(new Set());
41
+ const hasCompleted = useRef(false);
42
+ const lastPosition = useRef(0);
43
+ const lastDuration = useRef(0);
44
+
45
+ const emit = (
46
+ meta: VideoAnalyticsMeta,
47
+ event: VideoAnalyticsEventName,
48
+ extra?: Partial<VideoAnalyticsPayload>
49
+ ) => {
50
+ if (!onVideoEvent) return;
51
+ onVideoEvent(event, {
52
+ videoId: meta.videoId,
53
+ videoTitle: meta.videoTitle,
54
+ provider: meta.provider,
55
+ position: lastPosition.current,
56
+ duration: lastDuration.current,
57
+ percentComplete:
58
+ lastDuration.current > 0
59
+ ? Math.min(
60
+ 100,
61
+ Math.round((lastPosition.current / lastDuration.current) * 100)
62
+ )
63
+ : 0,
64
+ ...extra,
65
+ });
66
+ };
67
+
68
+ const reset = () => {
69
+ firedMilestones.current = new Set();
70
+ hasCompleted.current = false;
71
+ lastPosition.current = 0;
72
+ lastDuration.current = 0;
73
+ };
74
+
75
+ const handlePlay = (meta: VideoAnalyticsMeta) => emit(meta, "played");
76
+
77
+ const handlePause = (meta: VideoAnalyticsMeta) => emit(meta, "paused");
78
+
79
+ const handleFullscreenToggle = (
80
+ meta: VideoAnalyticsMeta,
81
+ isFullscreen: boolean
82
+ ) => emit(meta, "fullscreen_toggled", { isFullscreen });
83
+
84
+ const handleClosed = (meta: VideoAnalyticsMeta) => emit(meta, "closed");
85
+
86
+ const handleTimeUpdate = (
87
+ meta: VideoAnalyticsMeta,
88
+ position: number,
89
+ duration: number
90
+ ) => {
91
+ const safePosition = Number.isFinite(position) ? position : 0;
92
+ const safeDuration = Number.isFinite(duration) ? duration : 0;
93
+ lastPosition.current = safePosition;
94
+ lastDuration.current = safeDuration;
95
+ if (safeDuration <= 0) return;
96
+ const percent = Math.min(100, (safePosition / safeDuration) * 100);
97
+ PROGRESS_MILESTONES.forEach(milestone => {
98
+ if (percent >= milestone && !firedMilestones.current.has(milestone)) {
99
+ firedMilestones.current.add(milestone);
100
+ emit(meta, "progress");
101
+ }
102
+ });
103
+
104
+ if (percent >= 99.5 && !hasCompleted.current) {
105
+ hasCompleted.current = true;
106
+ emit(meta, "completed");
107
+ }
108
+ };
109
+
110
+ const handleEnded = (meta: VideoAnalyticsMeta) => {
111
+ if (hasCompleted.current) return;
112
+ hasCompleted.current = true;
113
+ if (lastDuration.current > 0) lastPosition.current = lastDuration.current;
114
+ emit(meta, "completed");
115
+ };
116
+
117
+ return {
118
+ handlePlay,
119
+ handlePause,
120
+ handleFullscreenToggle,
121
+ handleClosed,
122
+ handleTimeUpdate,
123
+ handleEnded,
124
+ reset,
125
+ };
126
+ }
@@ -0,0 +1,11 @@
1
+ declare module "@vimeo/player" {
2
+ export default class Player {
3
+ constructor(
4
+ element: HTMLIFrameElement | HTMLElement | string,
5
+ options?: any
6
+ );
7
+ on(event: string, callback: (data: any) => void): void;
8
+ off(event: string, callback?: (data: any) => void): void;
9
+ destroy(): Promise<void>;
10
+ }
11
+ }