@windstream/react-shared-components 0.2.11 → 0.2.12

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/core.d.ts CHANGED
@@ -170,7 +170,7 @@ interface ListItemProps extends LiHTMLAttributes<HTMLLIElement> {
170
170
 
171
171
  declare const ListItem: React$1.ForwardRefExoticComponent<ListItemProps & React$1.RefAttributes<HTMLLIElement>>;
172
172
 
173
- declare const List: React__default.ForwardRefExoticComponent<ListProps & React__default.RefAttributes<HTMLUListElement | HTMLOListElement>>;
173
+ declare const List: React__default.ForwardRefExoticComponent<ListProps & React__default.RefAttributes<HTMLOListElement | HTMLUListElement>>;
174
174
 
175
175
  declare const iconNameList: readonly ["info", "download", "featured_seasonal_and_gifts", "visibility", "paid", "check_box", "check_box_outline_blank", "radio_button_unchecked", "radio_button_checked", "counter_1", "counter_2", "counter_3", "error", "search", "cancel", "router", "broadcast_on_home", "check", "check_circle", "lock", "shield_lock", "phone_in_talk", "call", "chevron_left", "chevron_right", "keyboard_arrow_down", "keyboard_arrow_up", "arrow_back", "arrow_forward", "help", "home_pin", "location_on", "timer", "schedule", "add", "remove", "bolt", "rocket", "cloud_sync", "close", "groups", "speed", "mail", "credit_card", "language", "forum", "contract", "local_atm", "headset_mic", "sensors", "wifi", "settings", "home", "devices", "build", "thumb_up", "desktop_windows", "block", "all_inclusive", "verified_user", "extension", "stadia_controller", "visibility_off", "close_fullscreen", "progress_activity", "travel_explore", "share", "business_center", "upload", "videocam", "expand_circle_right", "add_circle", "do_not_disturb_on", "expand_circle_up", "expand_circle_down", "replay", "fiber_manual_record", "menu", "dangerous", "star", "play_arrow", "calendar_clock", "shopping_cart"];
176
176
  declare const OpticalSizes: readonly ["20dp", "24dp", "40dp", "48dp"];
@@ -351,7 +351,7 @@ type ButtonAsAnchor = ButtonCustomProps & Omit<AnchorHTMLAttributes<HTMLAnchorEl
351
351
  };
352
352
  type ButtonProps = ButtonAsButton | ButtonAsAnchor;
353
353
 
354
- declare const BrandButton: React__default.ForwardRefExoticComponent<ButtonProps$2 & React__default.RefAttributes<HTMLAnchorElement | HTMLButtonElement>>;
354
+ declare const BrandButton: React__default.ForwardRefExoticComponent<ButtonProps$2 & React__default.RefAttributes<HTMLButtonElement | HTMLAnchorElement>>;
355
355
 
356
356
  declare const Checklist: React__default.FC<ChecklistProps>;
357
357
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windstream/react-shared-components",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "type": "module",
5
5
  "description": "Shared React components for Kinetic applications",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,105 @@
1
+ import React from "react";
2
+ import { IframeEmbed } from "./index";
3
+
4
+ import { render, screen, fireEvent } from "@testing-library/react";
5
+
6
+ describe("IframeEmbed", () => {
7
+ it("renders nothing when iframeUrl is not provided", () => {
8
+ const { container } = render(<IframeEmbed iframeUrl="" />);
9
+ expect(container.querySelector("section")).not.toBeInTheDocument();
10
+ });
11
+
12
+ it("renders the iframe when iframeUrl is provided", () => {
13
+ render(<IframeEmbed iframeUrl="https://example.com/embed" />);
14
+ const iframe = screen.getByTitle("Iframe Embed");
15
+ expect(iframe).toBeInTheDocument();
16
+ expect(iframe).toHaveAttribute("src", "https://example.com/embed");
17
+ });
18
+
19
+ it("applies anchorId to the section", () => {
20
+ const { container } = render(
21
+ <IframeEmbed iframeUrl="https://example.com/embed" anchorId="embed-section" />
22
+ );
23
+ expect(container.querySelector("#embed-section")).toBeInTheDocument();
24
+ });
25
+
26
+ it("applies background color when backgroundColor is provided", () => {
27
+ render(
28
+ <IframeEmbed iframeUrl="https://example.com/embed" backgroundColor="green" />
29
+ );
30
+ const iframe = screen.getByTitle("Iframe Embed");
31
+ expect(iframe.style.background).toBe("rgb(36, 167, 106)");
32
+ });
33
+
34
+ it("applies transparent background when backgroundColor is not provided", () => {
35
+ render(<IframeEmbed iframeUrl="https://example.com/embed" />);
36
+ const iframe = screen.getByTitle("Iframe Embed");
37
+ expect(iframe.style.background).toBe("transparent");
38
+ });
39
+
40
+ it("updates iframe height on postMessage event", () => {
41
+ render(<IframeEmbed iframeUrl="https://example.com/embed" />);
42
+ const iframe = screen.getByTitle("Iframe Embed");
43
+
44
+ fireEvent(
45
+ window,
46
+ new MessageEvent("message", { data: { iframeHeight: 300 } })
47
+ );
48
+
49
+ expect(iframe.style.height).toBe("300px");
50
+ });
51
+
52
+ it("removes message listener on unmount", () => {
53
+ const removeEventListenerSpy = jest.spyOn(window, "removeEventListener");
54
+ const { unmount } = render(<IframeEmbed iframeUrl="https://example.com/embed" />);
55
+
56
+ unmount();
57
+
58
+ expect(removeEventListenerSpy).toHaveBeenCalledWith(
59
+ "message",
60
+ expect.any(Function)
61
+ );
62
+ removeEventListenerSpy.mockRestore();
63
+ });
64
+
65
+ it("does not attach message listener when iframeUrl is empty", () => {
66
+ const addEventListenerSpy = jest.spyOn(window, "addEventListener");
67
+ render(<IframeEmbed iframeUrl="" />);
68
+
69
+ expect(addEventListenerSpy).not.toHaveBeenCalledWith(
70
+ "message",
71
+ expect.any(Function)
72
+ );
73
+ addEventListenerSpy.mockRestore();
74
+ });
75
+
76
+ it("applies initial height when height prop is provided", () => {
77
+ render(<IframeEmbed iframeUrl="https://example.com/embed" height={200} />);
78
+ const iframe = screen.getByTitle("Iframe Embed");
79
+ expect(iframe.style.height).toBe("200px");
80
+ });
81
+
82
+ it("ignores non-numeric iframeHeight values", () => {
83
+ render(<IframeEmbed iframeUrl="https://example.com/embed" />);
84
+ const iframe = screen.getByTitle("Iframe Embed");
85
+
86
+ fireEvent(window, new MessageEvent("message", { data: { iframeHeight: "abc" } }));
87
+ expect(iframe.style.height).toBe("");
88
+ });
89
+
90
+ it("ignores iframeHeight exceeding max cap", () => {
91
+ render(<IframeEmbed iframeUrl="https://example.com/embed" />);
92
+ const iframe = screen.getByTitle("Iframe Embed");
93
+
94
+ fireEvent(window, new MessageEvent("message", { data: { iframeHeight: 99999 } }));
95
+ expect(iframe.style.height).toBe("");
96
+ });
97
+
98
+ it("ignores negative iframeHeight values", () => {
99
+ render(<IframeEmbed iframeUrl="https://example.com/embed" />);
100
+ const iframe = screen.getByTitle("Iframe Embed");
101
+
102
+ fireEvent(window, new MessageEvent("message", { data: { iframeHeight: -100 } }));
103
+ expect(iframe.style.height).toBe("");
104
+ });
105
+ });
@@ -0,0 +1,79 @@
1
+ "use client";
2
+
3
+ import React, { useEffect, useRef } from "react";
4
+ import { IframeEmbedProps, bgColorMap } from "./types";
5
+
6
+ /**
7
+ * IframeEmbed — A generic, responsive iframe component for embedding third-party content
8
+ * such as Brand Featured badges, widgets, or any external HTML page.
9
+ *
10
+ * Features:
11
+ * - **Auto-resize:** Listens for `postMessage` events with an `{ iframeHeight: number }` payload
12
+ * from the embedded page and adjusts the iframe height dynamically.
13
+ * - **Background color:** Supports predefined background colors (`gray90`, `green`, `white`)
14
+ * behind the iframe, defaulting to transparent.
15
+ * - **Lazy loading:** The iframe uses `loading="lazy"` for performance.
16
+ * - **Anchor support:** An optional `anchorId` is applied to the wrapping `<section>` for
17
+ * deep-linking or scroll-to navigation.
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * <IframeEmbed
22
+ * iframeUrl="https://api.brandfeatured.com/badges/badge-news_dark.html?id=123"
23
+ * backgroundColor="green"
24
+ * anchorId="brand-badge"
25
+ * />
26
+ * ```
27
+ */
28
+ export const IframeEmbed: React.FC<IframeEmbedProps> = ({
29
+ iframeUrl,
30
+ anchorId,
31
+ backgroundColor,
32
+ height,
33
+ }) => {
34
+ const iframeRef = useRef<HTMLIFrameElement>(null);
35
+
36
+ useEffect(() => {
37
+ if (!iframeUrl) return;
38
+
39
+ const handleMessage = (event: MessageEvent) => {
40
+ const height = event.data?.iframeHeight;
41
+ if (
42
+ typeof height === "number" &&
43
+ Number.isFinite(height) &&
44
+ height > 0 &&
45
+ height <= 10000 &&
46
+ iframeRef.current
47
+ ) {
48
+ iframeRef.current.style.height = `${height}px`;
49
+ }
50
+ };
51
+
52
+ window.addEventListener("message", handleMessage);
53
+ return () => window.removeEventListener("message", handleMessage);
54
+ }, [iframeUrl]);
55
+
56
+ if (!iframeUrl) return null;
57
+
58
+ return (
59
+ <section id={anchorId} className="w-full">
60
+ <iframe
61
+ ref={iframeRef}
62
+ src={iframeUrl}
63
+ width="100%"
64
+ frameBorder="0"
65
+ scrolling="no"
66
+ style={{
67
+ background: backgroundColor
68
+ ? bgColorMap[backgroundColor]
69
+ : "transparent",
70
+ ...(height ? { height: `${height}px` } : {}),
71
+ }}
72
+ title="Iframe Embed"
73
+ loading="lazy"
74
+ />
75
+ </section>
76
+ );
77
+ };
78
+
79
+ export default IframeEmbed;
@@ -0,0 +1,18 @@
1
+ export const bgColorMap = {
2
+ gray90: "#464646",
3
+ green: "#24A76A",
4
+ white: "#FFFFFF",
5
+ } as const;
6
+
7
+ export type BackgroundColor = keyof typeof bgColorMap;
8
+
9
+ export type IframeEmbedProps = {
10
+ /** The full URL of the third-party iframe to embed (e.g., Brand Featured badge URL). */
11
+ iframeUrl: string;
12
+ /** Optional HTML `id` for the wrapping `<section>`, useful for anchor linking or scroll-to navigation. */
13
+ anchorId?: string;
14
+ /** Optional background color applied behind the iframe. Defaults to `"transparent"` when not provided. */
15
+ backgroundColor?: BackgroundColor;
16
+ /** Optional initial/minimum height (in px) for the iframe before postMessage auto-resize kicks in. */
17
+ height?: number;
18
+ };
@@ -89,6 +89,9 @@ export type { EmailInputBlockProps } from "./blocks/email-input-block/types";
89
89
  export { default as CartRetentionBanner } from "./blocks/cart-retention-banner";
90
90
  export type { TypeCartRetentionBannerFields as CartRetentionBannerProps } from "./blocks/cart-retention-banner/types";
91
91
 
92
+ export { IframeEmbed } from "./blocks/iframe-embed";
93
+ export type { IframeEmbedProps } from "./blocks/iframe-embed/types";
94
+
92
95
  // Contentful Rich Text Hooks & Utils
93
96
  export { toDocument } from "../utils/contentful/to-document";
94
97