Skip to content

Commit ffed3f0

Browse files
committed
feat: Add <HtmlPanel> component
1 parent 4c95ecc commit ffed3f0

File tree

7 files changed

+162
-5
lines changed

7 files changed

+162
-5
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,13 +274,28 @@ Accepted props:
274274
loading
275275
- `chatboxRef` (resp. `inboxRef`, `popupRef`) - Pass a ref (created with `useRef`) and it'll be set to the vanilla JS [Chatbox](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/Chatbox/) (resp. [Inbox](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/Inbox/), [Popup](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/Popup/)) instance. See [above](#using-refs) for an example.
276276
- All [Talk.ChatboxOptions](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/Session/#ChatboxOptions)
277+
- `children?: ReactNode` - Optional. You can provide an `<HtmlPanel>` component as a child to use [HTML Panels](https://talkjs.com/docs/Features/Customizations/HTML_Panels/).
277278

278279
Accepted events (props that start with "on"):
279280

280281
- All events accepted by [`Talk.Chatbox`](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/Chatbox/#Chatbox__methods) (resp. [Inbox](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/Inbox/#Inbox__methods), [Popup](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/Popup/#Popup__methods))
281282

282283
Note: For `<Chatbox>` and `<Popup>`, you must provide exactly one of `conversationId` and `syncConversation`. For `<Inbox>`, leaving both unset selects the latest conversation this user participates in (if any). See [Inbox.select](https://talkjs.com/docs/Reference/JavaScript_Chat_SDK/Inbox/#Inbox__select) for more information.
283284

285+
### `<HtmlPanel>`
286+
287+
Accepted props:
288+
289+
- `url: string` - The URL you want to load inside the HTML panel. The URL can be absolute or relative. We recommend using same origin pages to have better control of the page. Learn more about HTML Panels and same origin pages [here](https://talkjs.com/docs/Features/Customizations/HTML_Panels/)
290+
291+
- `height?: number` - Optional. The panel height in pixels. Defaults to `100px`.
292+
293+
- `show?: boolean` - Optional. Sets the visibility of the panel. Defaults to `true`.
294+
295+
- `conversationId?: string` - Optional. If given, the panel will only show up for the conversation that has an `id` matching the one given.
296+
297+
- `children: React.ReactNode` - The content that gets rendered inside the `<body>` of the panel.
298+
284299

285300
## Contributing
286301

example/App.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import "./App.css";
33
import { Session, Chatbox } from "../lib/main";
44
import Talk from "talkjs";
55
import { ChangeEvent, useCallback, useMemo, useRef, useState } from "react";
6+
import { HtmlPanel } from "../lib/HtmlPanel";
67

78
const convIds = ["talk-react-94872948u429843", "talk-react-194872948u429843"];
89
const users = [
@@ -104,6 +105,9 @@ function App() {
104105
setDn(JSON.parse(event.target!.value));
105106
}, []);
106107

108+
const [panelHeight, setPanelHeight] = useState(100);
109+
const [panelVisible, setPanelVisible] = useState(true);
110+
107111
if (typeof import.meta.env.VITE_APP_ID !== "string") {
108112
return (
109113
<div style={{ maxWidth: "50em" }}>
@@ -150,8 +154,23 @@ function App() {
150154
loadingComponent={<span>LOADING....</span>}
151155
{...(blur ? { onBlur } : {})}
152156
style={{ width: 500, height: 600 }}
153-
/>
157+
>
158+
<HtmlPanel
159+
url="/example/panel.html"
160+
height={panelHeight}
161+
show={panelVisible}
162+
>
163+
I am an HTML panel.
164+
<button
165+
onClick={() => setPanelHeight(panelHeight > 100 ? 100 : 150)}
166+
>
167+
Toggle panel height
168+
</button>
169+
<button onClick={() => setPanelVisible(false)}>Hide panel</button>
170+
</HtmlPanel>
171+
</Chatbox>
154172
</Session>
173+
155174
<button onClick={otherMe}>switch user (new session)</button>
156175
<br />
157176
<button onClick={switchConv}>

example/panel.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Document</title>
7+
<style>
8+
body {
9+
background-color: lightblue;
10+
}
11+
button {
12+
display: block;
13+
width: 10rem;
14+
margin: 0.6rem auto;
15+
}
16+
</style>
17+
</head>
18+
<body></body>
19+
</html>

lib/HtmlPanel.tsx

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { useContext, useEffect, useState } from "react";
2+
import { createPortal } from "react-dom";
3+
import Talk from "talkjs";
4+
import { BoxContext } from "./MountedBox";
5+
6+
type HtmlPanelProps = {
7+
/**
8+
* The URL you want to load inside the HTML panel. The URL can be absolute or
9+
* relative. We recommend using same origin pages to have better control of
10+
* the page. Learn more about HTML Panels and same origin pages {@link https://talkjs.com/docs/Features/Customizations/HTML_Panels/ | here}.
11+
*/
12+
url: string;
13+
14+
/** The panel height in pixels. Defaults to `100px`. */
15+
height?: number;
16+
17+
/** Sets the visibility of the panel. Defaults to `true`. */
18+
show?: boolean;
19+
20+
/** If given, the panel will only show up for the conversation that has an `id` matching the one given. */
21+
conversationId?: string;
22+
23+
/** The content that gets rendered inside the `<body>` of the panel. */
24+
children: React.ReactNode;
25+
};
26+
27+
type State =
28+
| { type: "none" }
29+
| { type: "loading" }
30+
| { type: "loaded"; panel: Talk.HtmlPanel };
31+
32+
export function HtmlPanel({
33+
url,
34+
height = 100,
35+
show = true,
36+
conversationId,
37+
children,
38+
}: HtmlPanelProps) {
39+
const [state, setState] = useState<State>({ type: "none" });
40+
const box = useContext(BoxContext);
41+
42+
useEffect(() => {
43+
async function run() {
44+
if (state.type !== "none" || !box) return;
45+
46+
setState({ type: "loading" });
47+
const panel = await box.createHtmlPanel({
48+
url,
49+
conversation: conversationId,
50+
height,
51+
show,
52+
});
53+
await panel.windowLoadedPromise;
54+
setState({ type: "loaded", panel });
55+
}
56+
57+
run();
58+
59+
return () => {
60+
if (state.type === "loaded") {
61+
state.panel.destroy();
62+
setState({ type: "none" });
63+
}
64+
};
65+
// We intentionally exclude `height` and `show` from the dependency array so
66+
// that we update them later via methods instead of by re-creating the
67+
// entire panel from scratch each time.
68+
//
69+
// eslint-disable-next-line react-hooks/exhaustive-deps
70+
}, [state, url, box, conversationId]);
71+
72+
useEffect(() => {
73+
if (state.type === "loaded") {
74+
state.panel.setHeight(height);
75+
}
76+
}, [state, height]);
77+
78+
useEffect(() => {
79+
if (state.type === "loaded") {
80+
if (show) {
81+
state.panel.show();
82+
} else {
83+
state.panel.hide();
84+
}
85+
}
86+
}, [state, show]);
87+
88+
return (
89+
<>
90+
{state.type === "loaded" &&
91+
createPortal(children, state.panel.window.document.body)}
92+
</>
93+
);
94+
}

lib/MountedBox.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CSSProperties, ReactNode, useRef } from "react";
1+
import React, { CSSProperties, ReactNode, useRef } from "react";
22
import type Talk from "talkjs";
33
import { EventListeners } from "./EventListeners";
44
import { useMountBox } from "./hooks";
@@ -11,31 +11,37 @@ interface Props {
1111
className?: string;
1212

1313
handlers: Record<`on${string}`, Func>;
14+
children?: React.ReactNode;
1415
}
1516

1617
/**
1718
* Mounts the given `UIBox` and attaches event handlers to it. Renders a
1819
* `loadingComponent` fallback until the mount is complete.
1920
*/
2021
export function MountedBox(props: Props & { session: Talk.Session }) {
21-
const { box, loadingComponent, className, handlers } = props;
22+
const { box, loadingComponent, className, children, handlers } = props;
2223

2324
const ref = useRef<HTMLDivElement>(null);
2425
const mounted = useMountBox(box, ref.current);
2526

2627
const style = mounted ? props.style : { ...props.style, display: "none" };
2728

2829
return (
29-
<>
30+
<BoxContext.Provider value={box}>
3031
{!mounted && (
3132
<div style={props.style} className={className}>
3233
{loadingComponent}
3334
</div>
3435
)}
3536

3637
<div ref={ref} style={style} className={className} />
38+
{children}
3739

3840
<EventListeners target={box} handlers={handlers} />
39-
</>
41+
</BoxContext.Provider>
4042
);
4143
}
44+
45+
export const BoxContext = React.createContext<Talk.UIBox | undefined>(
46+
undefined,
47+
);

lib/main.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export { Chatbox } from "./ui/Chatbox";
44
export { Inbox } from "./ui/Inbox";
55
export { Popup } from "./ui/Popup";
66
export { useSession } from "./SessionContext";
7+
export { HtmlPanel } from "./HtmlPanel";

lib/ui/Chatbox.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type ChatboxProps = UIBoxProps<Talk.Chatbox> &
1313
loadingComponent?: ReactNode;
1414
style?: CSSProperties;
1515
className?: string;
16+
children?: React.ReactNode;
1617
};
1718

1819
export function Chatbox(props: ChatboxProps) {
@@ -39,6 +40,7 @@ function ActiveChatbox(props: ChatboxProps & { session: Talk.Session }) {
3940
style,
4041
className,
4142
loadingComponent,
43+
children,
4244
...optionsAndEvents
4345
} = props;
4446

@@ -60,6 +62,7 @@ function ActiveChatbox(props: ChatboxProps & { session: Talk.Session }) {
6062
style={style}
6163
loadingComponent={loadingComponent}
6264
handlers={events}
65+
children={children}
6366
/>
6467
);
6568
}

0 commit comments

Comments
 (0)