Skip to content

Commit b6dafc5

Browse files
authored
Merge pull request #1335 from easyops-cn/steve/aside
Steve/aside
2 parents f2f344a + fceccdc commit b6dafc5

File tree

12 files changed

+267
-25
lines changed

12 files changed

+267
-25
lines changed

bricks/basic/src/bootstrap.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,5 @@ import "./event-agent/index.js";
4747
import "./message-listener/index.js";
4848
import "./broadcast-channel/index.js";
4949
import "./home-redirect/index.js";
50+
import "./iframe/index.js";
51+
import "./data-providers/set-timeout.js";
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { describe, test, expect } from "@jest/globals";
2+
import { providerSetTimeout } from "./set-timeout.js";
3+
4+
describe("providerSetTimeout", () => {
5+
beforeAll(() => {
6+
jest.useFakeTimers();
7+
});
8+
9+
afterAll(() => {
10+
jest.useRealTimers();
11+
});
12+
13+
test("should work", async () => {
14+
const promise = providerSetTimeout(100, "done");
15+
jest.advanceTimersByTime(100);
16+
expect(promise).resolves.toBe("done");
17+
});
18+
});
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { createProviderClass } from "@next-core/utils/general";
2+
3+
/**
4+
* @param delay Delay in milliseconds
5+
* @param returnValue
6+
*/
7+
export function providerSetTimeout(
8+
delay?: number,
9+
returnValue?: unknown
10+
): Promise<unknown> {
11+
return new Promise((resolve) => {
12+
setTimeout(() => {
13+
resolve(returnValue);
14+
}, delay);
15+
});
16+
}
17+
18+
customElements.define(
19+
"basic.set-timeout",
20+
createProviderClass(providerSetTimeout)
21+
);

bricks/basic/src/data-providers/show-dialog/show-dialog.spec.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { describe, test, expect } from "@jest/globals";
33
import { waitFor, act, render, fireEvent } from "@testing-library/react";
44
import { showDialog, DialogComponent } from "./show-dialog.js";
55

6+
jest.mock("@next-core/theme", () => ({}));
7+
68
// <sl-dialog> uses those API which is not supported in jsdom.
79
Element.prototype.getAnimations = jest.fn().mockReturnValue([]);
810
Element.prototype.animate = jest.fn().mockReturnValue({

bricks/basic/src/data-providers/show-dialog/show-dialog.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { i18n, initializeI18n } from "@next-core/i18n";
44
import { createRoot } from "react-dom/client";
55
import { wrapBrick } from "@next-core/react-element";
66
import type { AntdIcon, AntdIconProps } from "@next-bricks/icons/antd-icon";
7+
import { get } from "lodash";
8+
import "@next-core/theme";
79
import { K, NS, locales } from "./i18n.js";
810
import { SlDialogElement, WrappedSlDialog } from "./sl-dialog.js";
9-
import { Button, ButtonProps } from "../../button/index.js";
10-
import type { ReactNextElement } from "@next-core/react-element";
11-
import { get } from "lodash";
11+
import type { Button, ButtonProps } from "../../button/index.js";
1212
import styles from "./dialog.module.css";
1313

1414
initializeI18n(NS, locales);
@@ -26,7 +26,7 @@ interface InputEventsMap {
2626
onValueChange: "change";
2727
}
2828
const WrappedInput = wrapBrick<
29-
ReactNextElement,
29+
HTMLElement,
3030
InputProps,
3131
InputEvents,
3232
InputEventsMap
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, test, expect, jest } from "@jest/globals";
2+
import { act } from "react-dom/test-utils";
3+
import { fireEvent } from "@testing-library/dom";
4+
import "./";
5+
import type { Iframe } from "./index.js";
6+
7+
jest.mock("@next-core/theme", () => ({}));
8+
9+
describe("eo-iframe", () => {
10+
test("basic usage", async () => {
11+
const element = document.createElement("eo-iframe") as Iframe;
12+
element.src = "http://localhost/iframe";
13+
14+
const onLoad = jest.fn();
15+
element.addEventListener("load", onLoad);
16+
17+
act(() => {
18+
document.body.appendChild(element);
19+
});
20+
expect(element.shadowRoot?.childNodes.length).toBeGreaterThan(1);
21+
22+
const iframe = element.shadowRoot?.querySelector(
23+
"iframe"
24+
) as HTMLIFrameElement;
25+
fireEvent.load(iframe);
26+
expect(onLoad).toBeCalledTimes(1);
27+
28+
const mockPostMessage = jest.fn();
29+
Object.defineProperty(iframe, "contentWindow", {
30+
get() {
31+
return {
32+
postMessage: mockPostMessage,
33+
} as any;
34+
},
35+
});
36+
element.postMessage("hello", location.origin);
37+
expect(mockPostMessage).toBeCalledWith("hello", location.origin);
38+
39+
act(() => {
40+
document.body.removeChild(element);
41+
});
42+
});
43+
});

bricks/basic/src/iframe/index.tsx

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import React, {
2+
createRef,
3+
forwardRef,
4+
useEffect,
5+
useImperativeHandle,
6+
useRef,
7+
type CSSProperties,
8+
type Ref,
9+
} from "react";
10+
import { createDecorators, type EventEmitter } from "@next-core/element";
11+
import { ReactNextElement } from "@next-core/react-element";
12+
import styleText from "./styles.shadow.css";
13+
14+
const { defineElement, property, event, method } = createDecorators();
15+
16+
export interface IframeProps {
17+
src?: string;
18+
iframeStyle?: CSSProperties;
19+
}
20+
21+
type PostMessageParameters =
22+
| [message: unknown, targetOrigin: string, transfer?: Transferable[]]
23+
| [message: unknown, options?: WindowPostMessageOptions];
24+
25+
interface IframeRef {
26+
postMessage: (...args: PostMessageParameters) => void;
27+
}
28+
29+
const IframeComponent = forwardRef<IframeRef, IframeComponentProps>(
30+
LegacyIframeComponent
31+
);
32+
33+
/**
34+
* 构件 `eo-iframe`
35+
*/
36+
export
37+
@defineElement("eo-iframe", {
38+
styleTexts: [styleText],
39+
})
40+
class Iframe extends ReactNextElement implements IframeProps {
41+
/**
42+
* @required
43+
*/
44+
@property() accessor src: string | undefined;
45+
46+
/**
47+
* Default style:
48+
*
49+
* ```css
50+
* iframe {
51+
* margin: 0;
52+
* border: 0;
53+
* padding: 0;
54+
* width: 100%;
55+
* height: 100%;
56+
* vertical-align: top;
57+
* }
58+
* ```
59+
*/
60+
@property({ attribute: false })
61+
accessor iframeStyle: CSSProperties | undefined;
62+
63+
@event({ type: "load" })
64+
accessor #loadEvent!: EventEmitter<void>;
65+
66+
#handleLoad = () => {
67+
this.#loadEvent.emit();
68+
};
69+
70+
#iframeRef = createRef<IframeRef>();
71+
72+
@method()
73+
postMessage(...args: PostMessageParameters) {
74+
this.#iframeRef.current?.postMessage(...args);
75+
}
76+
77+
render() {
78+
return (
79+
<IframeComponent
80+
src={this.src}
81+
iframeStyle={this.iframeStyle}
82+
onLoad={this.#handleLoad}
83+
ref={this.#iframeRef}
84+
/>
85+
);
86+
}
87+
}
88+
89+
export interface IframeComponentProps extends IframeProps {
90+
onLoad: () => void;
91+
}
92+
93+
export function LegacyIframeComponent(
94+
{ src, iframeStyle, onLoad }: IframeComponentProps,
95+
ref: Ref<IframeRef>
96+
) {
97+
const iframeRef = useRef<HTMLIFrameElement>(null);
98+
99+
useImperativeHandle(
100+
ref,
101+
() => ({
102+
postMessage(...args: PostMessageParameters) {
103+
iframeRef.current?.contentWindow?.postMessage(
104+
...(args as Parameters<Window["postMessage"]>)
105+
);
106+
},
107+
}),
108+
[]
109+
);
110+
111+
useEffect(() => {
112+
const iframe = iframeRef.current;
113+
iframe?.addEventListener("load", onLoad);
114+
return () => {
115+
iframe?.removeEventListener("load", onLoad);
116+
};
117+
}, [onLoad]);
118+
119+
return <iframe src={src} style={iframeStyle} ref={iframeRef} />;
120+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
:host {
2+
display: block;
3+
}
4+
5+
:host([hidden]) {
6+
display: none;
7+
}
8+
9+
iframe {
10+
border: 0;
11+
margin: 0;
12+
padding: 0;
13+
width: 100%;
14+
height: 100%;
15+
vertical-align: top;
16+
}

bricks/basic/src/tooltip/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import React, { CSSProperties, useEffect, useRef, useState } from "react";
22
import { EventEmitter, createDecorators } from "@next-core/element";
33
import { ReactNextElement, wrapBrick } from "@next-core/react-element";
4+
import type {
5+
GeneralIcon,
6+
GeneralIconProps,
7+
} from "@next-bricks/icons/general-icon";
48
import "@next-core/theme";
59
import styleText from "./styles.shadow.css";
610
import {
@@ -10,7 +14,6 @@ import {
1014
ARROW_SIZE,
1115
DISTANCE,
1216
} from "./sl-tooltip.js";
13-
import { GeneralIcon, GeneralIconProps } from "@next-bricks/icons/general-icon";
1417

1518
const { defineElement, property, method, event } = createDecorators();
1619
const WrappedIcon = wrapBrick<GeneralIcon, GeneralIconProps>("eo-icon");

bricks/containers/src/page-view/index.tsx

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -107,28 +107,33 @@ export function EoPageViewComponent({
107107

108108
return (
109109
<>
110-
<div className="header">
111-
<slot name="header" />
112-
</div>
113-
<div className="main">
114-
<div className="sidebar">
115-
<slot name="sidebar" />
116-
</div>
117-
<div className="sub-sidebar">
118-
<slot name="subSidebar" />
110+
<div className="header-and-main">
111+
<div className="header">
112+
<slot name="header" />
119113
</div>
120-
<div className="content">
121-
<slot />
122-
<div
123-
className={classNames("footer", { pinned: footerPinned })}
124-
ref={footerRef}
125-
>
126-
<WrappedNarrowView size={narrow}>
127-
<slot name="footer" />
128-
</WrappedNarrowView>
114+
<div className="main">
115+
<div className="sidebar">
116+
<slot name="sidebar" />
117+
</div>
118+
<div className="sub-sidebar">
119+
<slot name="subSidebar" />
120+
</div>
121+
<div className="content">
122+
<slot />
123+
<div
124+
className={classNames("footer", { pinned: footerPinned })}
125+
ref={footerRef}
126+
>
127+
<WrappedNarrowView size={narrow}>
128+
<slot name="footer" />
129+
</WrappedNarrowView>
130+
</div>
129131
</div>
130132
</div>
131133
</div>
134+
<div className="aside">
135+
<slot name="aside" />
136+
</div>
132137
</>
133138
);
134139
}

0 commit comments

Comments
 (0)