@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.
- package/dist/contentful/index.d.ts +12 -1
- package/dist/contentful/index.esm.js +3 -3
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +3 -3
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +3 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.esm.js +6 -6
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/next/index.esm.js +2 -2
- package/dist/next/index.esm.js.map +1 -1
- package/dist/next/index.js +2 -2
- package/dist/next/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -1
- package/src/components/next-image/index.tsx +10 -1
- package/src/components/video-link/contentful-video-embed.tsx +17 -1
- package/src/components/video-link/index.test.tsx +196 -24
- package/src/components/video-link/index.tsx +84 -15
- package/src/components/video-link/types.ts +15 -1
- package/src/components/video-link/vimeo-embed.tsx +51 -2
- package/src/components/video-link/youtube-embed.test.tsx +81 -54
- package/src/components/video-link/youtube-embed.tsx +97 -16
- package/src/components/video-link/youtube-iframe-api-loader.ts +45 -0
- package/src/contentful/blocks/callout/index.test.tsx +2 -2
- package/src/contentful/blocks/callout/index.tsx +1 -0
- package/src/contentful/blocks/cards/blog-card/index.tsx +9 -2
- package/src/contentful/blocks/cards/blog-card/types.ts +4 -0
- package/src/contentful/blocks/cards/floating-image-card/index.tsx +20 -6
- package/src/contentful/blocks/cards/floating-image-card/types.ts +4 -0
- package/src/contentful/blocks/cards/full-image-card/index.tsx +2 -1
- package/src/contentful/blocks/cards/full-image-card/types.ts +4 -0
- package/src/contentful/blocks/cards/simple-card/index.tsx +2 -2
- package/src/contentful/blocks/cards/simple-card/types.ts +4 -0
- package/src/contentful/blocks/find-kinetic/index.tsx +20 -7
- package/src/contentful/blocks/find-kinetic/types.ts +1 -0
- package/src/contentful/blocks/image-promo-bar/index.test.tsx +2 -2
- package/src/contentful/blocks/image-promo-bar/index.tsx +4 -1
- package/src/contentful/blocks/image-promo-bar/types.ts +6 -1
- package/src/contentful/blocks/primary-hero/index.tsx +6 -6
- package/src/hooks/contentful/use-contentful-rich-text.test.tsx +6 -6
- package/src/hooks/use-fullscreen-toggle.ts +23 -0
- package/src/hooks/use-video-analytics.ts +126 -0
- 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
|
|
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
|
|
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 {
|
|
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-
|
|
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-[
|
|
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-[
|
|
364
|
+
hasDesktopVideo && "h-[584px] w-[584px]"
|
|
365
365
|
)}
|
|
366
366
|
>
|
|
367
|
-
<Badge className="absolute -left-
|
|
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={
|
|
374
|
-
height={
|
|
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
|
|
246
|
+
expect(a).toHaveAttribute("rel", "noopener");
|
|
247
247
|
});
|
|
248
248
|
|
|
249
|
-
it("renders internal link
|
|
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"
|
|
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"
|
|
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
|
|
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"
|
|
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
|
+
}
|