From 5c0f3c47fb467ae1a03c4da87e414f83af1a6731 Mon Sep 17 00:00:00 2001 From: weareoutman Date: Wed, 4 Sep 2024 15:52:37 +0800 Subject: [PATCH] feat(): new brick: eo-iframe --- bricks/basic/src/bootstrap.ts | 1 + bricks/basic/src/iframe/index.spec.tsx | 43 ++++++++ bricks/basic/src/iframe/index.tsx | 120 ++++++++++++++++++++++ bricks/basic/src/iframe/styles.shadow.css | 16 +++ shared/common-bricks/common-bricks.json | 3 +- 5 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 bricks/basic/src/iframe/index.spec.tsx create mode 100644 bricks/basic/src/iframe/index.tsx create mode 100644 bricks/basic/src/iframe/styles.shadow.css diff --git a/bricks/basic/src/bootstrap.ts b/bricks/basic/src/bootstrap.ts index 8effc47d1..07d48f863 100644 --- a/bricks/basic/src/bootstrap.ts +++ b/bricks/basic/src/bootstrap.ts @@ -47,3 +47,4 @@ import "./event-agent/index.js"; import "./message-listener/index.js"; import "./broadcast-channel/index.js"; import "./home-redirect/index.js"; +import "./iframe/index.js"; diff --git a/bricks/basic/src/iframe/index.spec.tsx b/bricks/basic/src/iframe/index.spec.tsx new file mode 100644 index 000000000..02fe8dccf --- /dev/null +++ b/bricks/basic/src/iframe/index.spec.tsx @@ -0,0 +1,43 @@ +import { describe, test, expect, jest } from "@jest/globals"; +import { act } from "react-dom/test-utils"; +import { fireEvent } from "@testing-library/dom"; +import "./"; +import type { Iframe } from "./index.js"; + +jest.mock("@next-core/theme", () => ({})); + +describe("eo-iframe", () => { + test("basic usage", async () => { + const element = document.createElement("eo-iframe") as Iframe; + element.src = "http://localhost/iframe"; + + const onLoad = jest.fn(); + element.addEventListener("load", onLoad); + + act(() => { + document.body.appendChild(element); + }); + expect(element.shadowRoot?.childNodes.length).toBeGreaterThan(1); + + const iframe = element.shadowRoot?.querySelector( + "iframe" + ) as HTMLIFrameElement; + fireEvent.load(iframe); + expect(onLoad).toBeCalledTimes(1); + + const mockPostMessage = jest.fn(); + Object.defineProperty(iframe, "contentWindow", { + get() { + return { + postMessage: mockPostMessage, + } as any; + }, + }); + element.postMessage("hello", location.origin); + expect(mockPostMessage).toBeCalledWith("hello", location.origin); + + act(() => { + document.body.removeChild(element); + }); + }); +}); diff --git a/bricks/basic/src/iframe/index.tsx b/bricks/basic/src/iframe/index.tsx new file mode 100644 index 000000000..89297114b --- /dev/null +++ b/bricks/basic/src/iframe/index.tsx @@ -0,0 +1,120 @@ +import React, { + createRef, + forwardRef, + useEffect, + useImperativeHandle, + useRef, + type CSSProperties, + type Ref, +} from "react"; +import { createDecorators, type EventEmitter } from "@next-core/element"; +import { ReactNextElement } from "@next-core/react-element"; +import styleText from "./styles.shadow.css"; + +const { defineElement, property, event, method } = createDecorators(); + +export interface IframeProps { + src?: string; + iframeStyle?: CSSProperties; +} + +type PostMessageParameters = + | [message: unknown, targetOrigin: string, transfer?: Transferable[]] + | [message: unknown, options?: WindowPostMessageOptions]; + +interface IframeRef { + postMessage: (...args: PostMessageParameters) => void; +} + +const IframeComponent = forwardRef( + LegacyIframeComponent +); + +/** + * 构件 `eo-iframe` + */ +export +@defineElement("eo-iframe", { + styleTexts: [styleText], +}) +class Iframe extends ReactNextElement implements IframeProps { + /** + * @required + */ + @property() accessor src: string | undefined; + + /** + * Default style: + * + * ```css + * iframe { + * margin: 0; + * border: 0; + * padding: 0; + * width: 100%; + * height: 100%; + * vertical-align: top; + * } + * ``` + */ + @property({ attribute: false }) + accessor iframeStyle: CSSProperties | undefined; + + @event({ type: "load" }) + accessor #loadEvent!: EventEmitter; + + #handleLoad = () => { + this.#loadEvent.emit(); + }; + + #iframeRef = createRef(); + + @method() + postMessage(...args: PostMessageParameters) { + this.#iframeRef.current?.postMessage(...args); + } + + render() { + return ( + + ); + } +} + +export interface IframeComponentProps extends IframeProps { + onLoad: () => void; +} + +export function LegacyIframeComponent( + { src, iframeStyle, onLoad }: IframeComponentProps, + ref: Ref +) { + const iframeRef = useRef(null); + + useImperativeHandle( + ref, + () => ({ + postMessage(...args: PostMessageParameters) { + iframeRef.current?.contentWindow?.postMessage( + ...(args as Parameters) + ); + }, + }), + [] + ); + + useEffect(() => { + const iframe = iframeRef.current; + iframe?.addEventListener("load", onLoad); + return () => { + iframe?.removeEventListener("load", onLoad); + }; + }, [onLoad]); + + return