@tp3/chat-widget 0.1.8 → 0.1.10
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/ChatWidget.js +41 -0
- package/dist/ChatWidget.mjs +41 -0
- package/package.json +1 -1
- package/src/ChatWidget.tsx +52 -0
package/dist/ChatWidget.js
CHANGED
|
@@ -64,8 +64,45 @@ function ChatWidget({
|
|
|
64
64
|
const wsRef = (0, import_react.useRef)(null);
|
|
65
65
|
const messagesEnd = (0, import_react.useRef)(null);
|
|
66
66
|
const inputRef = (0, import_react.useRef)(null);
|
|
67
|
+
const chatWindowRef = (0, import_react.useRef)(null);
|
|
67
68
|
const typewriterQueue = (0, import_react.useRef)([]);
|
|
68
69
|
const typewriterTimer = (0, import_react.useRef)(null);
|
|
70
|
+
(0, import_react.useEffect)(() => {
|
|
71
|
+
if (open && !closing) {
|
|
72
|
+
const scrollY = window.scrollY;
|
|
73
|
+
document.body.style.position = "fixed";
|
|
74
|
+
document.body.style.top = `-${scrollY}px`;
|
|
75
|
+
document.body.style.width = "100%";
|
|
76
|
+
} else if (!open) {
|
|
77
|
+
const top = Math.abs(parseInt(document.body.style.top || "0", 10));
|
|
78
|
+
document.body.style.position = "";
|
|
79
|
+
document.body.style.top = "";
|
|
80
|
+
document.body.style.width = "";
|
|
81
|
+
window.scrollTo(0, top);
|
|
82
|
+
}
|
|
83
|
+
return () => {
|
|
84
|
+
const top = Math.abs(parseInt(document.body.style.top || "0", 10));
|
|
85
|
+
document.body.style.position = "";
|
|
86
|
+
document.body.style.top = "";
|
|
87
|
+
document.body.style.width = "";
|
|
88
|
+
if (open) window.scrollTo(0, top);
|
|
89
|
+
};
|
|
90
|
+
}, [open, closing]);
|
|
91
|
+
(0, import_react.useEffect)(() => {
|
|
92
|
+
const el = chatWindowRef.current;
|
|
93
|
+
if (!el || !open) return;
|
|
94
|
+
function blockIfOutsideMessages(e) {
|
|
95
|
+
const target = e.target;
|
|
96
|
+
if (target.closest("[data-chat-messages]")) return;
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
}
|
|
99
|
+
el.addEventListener("wheel", blockIfOutsideMessages, { passive: false });
|
|
100
|
+
el.addEventListener("touchmove", blockIfOutsideMessages, { passive: false });
|
|
101
|
+
return () => {
|
|
102
|
+
el.removeEventListener("wheel", blockIfOutsideMessages);
|
|
103
|
+
el.removeEventListener("touchmove", blockIfOutsideMessages);
|
|
104
|
+
};
|
|
105
|
+
}, [open]);
|
|
69
106
|
(0, import_react.useEffect)(() => {
|
|
70
107
|
if (messages.length > 1) {
|
|
71
108
|
messagesEnd.current?.scrollIntoView({ behavior: "smooth" });
|
|
@@ -261,6 +298,7 @@ function ChatWidget({
|
|
|
261
298
|
open && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
262
299
|
"div",
|
|
263
300
|
{
|
|
301
|
+
ref: chatWindowRef,
|
|
264
302
|
className: closing ? "chat-window-out" : "chat-window",
|
|
265
303
|
style: {
|
|
266
304
|
position: "fixed",
|
|
@@ -271,6 +309,7 @@ function ChatWidget({
|
|
|
271
309
|
maxWidth: "calc(100vw - 48px)",
|
|
272
310
|
height: chatHeight,
|
|
273
311
|
maxHeight: "calc(100dvh - 48px)",
|
|
312
|
+
overscrollBehavior: "contain",
|
|
274
313
|
background: "var(--chat-primary-fg, #fff)",
|
|
275
314
|
display: "flex",
|
|
276
315
|
flexDirection: "column",
|
|
@@ -335,9 +374,11 @@ function ChatWidget({
|
|
|
335
374
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
336
375
|
"div",
|
|
337
376
|
{
|
|
377
|
+
"data-chat-messages": true,
|
|
338
378
|
style: {
|
|
339
379
|
flex: 1,
|
|
340
380
|
overflowY: "auto",
|
|
381
|
+
overscrollBehavior: "contain",
|
|
341
382
|
padding: "12px 16px",
|
|
342
383
|
display: "flex",
|
|
343
384
|
flexDirection: "column",
|
package/dist/ChatWidget.mjs
CHANGED
|
@@ -40,8 +40,45 @@ function ChatWidget({
|
|
|
40
40
|
const wsRef = useRef(null);
|
|
41
41
|
const messagesEnd = useRef(null);
|
|
42
42
|
const inputRef = useRef(null);
|
|
43
|
+
const chatWindowRef = useRef(null);
|
|
43
44
|
const typewriterQueue = useRef([]);
|
|
44
45
|
const typewriterTimer = useRef(null);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (open && !closing) {
|
|
48
|
+
const scrollY = window.scrollY;
|
|
49
|
+
document.body.style.position = "fixed";
|
|
50
|
+
document.body.style.top = `-${scrollY}px`;
|
|
51
|
+
document.body.style.width = "100%";
|
|
52
|
+
} else if (!open) {
|
|
53
|
+
const top = Math.abs(parseInt(document.body.style.top || "0", 10));
|
|
54
|
+
document.body.style.position = "";
|
|
55
|
+
document.body.style.top = "";
|
|
56
|
+
document.body.style.width = "";
|
|
57
|
+
window.scrollTo(0, top);
|
|
58
|
+
}
|
|
59
|
+
return () => {
|
|
60
|
+
const top = Math.abs(parseInt(document.body.style.top || "0", 10));
|
|
61
|
+
document.body.style.position = "";
|
|
62
|
+
document.body.style.top = "";
|
|
63
|
+
document.body.style.width = "";
|
|
64
|
+
if (open) window.scrollTo(0, top);
|
|
65
|
+
};
|
|
66
|
+
}, [open, closing]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const el = chatWindowRef.current;
|
|
69
|
+
if (!el || !open) return;
|
|
70
|
+
function blockIfOutsideMessages(e) {
|
|
71
|
+
const target = e.target;
|
|
72
|
+
if (target.closest("[data-chat-messages]")) return;
|
|
73
|
+
e.preventDefault();
|
|
74
|
+
}
|
|
75
|
+
el.addEventListener("wheel", blockIfOutsideMessages, { passive: false });
|
|
76
|
+
el.addEventListener("touchmove", blockIfOutsideMessages, { passive: false });
|
|
77
|
+
return () => {
|
|
78
|
+
el.removeEventListener("wheel", blockIfOutsideMessages);
|
|
79
|
+
el.removeEventListener("touchmove", blockIfOutsideMessages);
|
|
80
|
+
};
|
|
81
|
+
}, [open]);
|
|
45
82
|
useEffect(() => {
|
|
46
83
|
if (messages.length > 1) {
|
|
47
84
|
messagesEnd.current?.scrollIntoView({ behavior: "smooth" });
|
|
@@ -237,6 +274,7 @@ function ChatWidget({
|
|
|
237
274
|
open && /* @__PURE__ */ jsxs(
|
|
238
275
|
"div",
|
|
239
276
|
{
|
|
277
|
+
ref: chatWindowRef,
|
|
240
278
|
className: closing ? "chat-window-out" : "chat-window",
|
|
241
279
|
style: {
|
|
242
280
|
position: "fixed",
|
|
@@ -247,6 +285,7 @@ function ChatWidget({
|
|
|
247
285
|
maxWidth: "calc(100vw - 48px)",
|
|
248
286
|
height: chatHeight,
|
|
249
287
|
maxHeight: "calc(100dvh - 48px)",
|
|
288
|
+
overscrollBehavior: "contain",
|
|
250
289
|
background: "var(--chat-primary-fg, #fff)",
|
|
251
290
|
display: "flex",
|
|
252
291
|
flexDirection: "column",
|
|
@@ -311,9 +350,11 @@ function ChatWidget({
|
|
|
311
350
|
/* @__PURE__ */ jsxs(
|
|
312
351
|
"div",
|
|
313
352
|
{
|
|
353
|
+
"data-chat-messages": true,
|
|
314
354
|
style: {
|
|
315
355
|
flex: 1,
|
|
316
356
|
overflowY: "auto",
|
|
357
|
+
overscrollBehavior: "contain",
|
|
317
358
|
padding: "12px 16px",
|
|
318
359
|
display: "flex",
|
|
319
360
|
flexDirection: "column",
|
package/package.json
CHANGED
package/src/ChatWidget.tsx
CHANGED
|
@@ -64,9 +64,57 @@ export default function ChatWidget({
|
|
|
64
64
|
const wsRef = useRef<WebSocket | null>(null);
|
|
65
65
|
const messagesEnd = useRef<HTMLDivElement>(null);
|
|
66
66
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
67
|
+
const chatWindowRef = useRef<HTMLDivElement>(null);
|
|
67
68
|
const typewriterQueue = useRef<string[]>([]);
|
|
68
69
|
const typewriterTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
69
70
|
|
|
71
|
+
// Lock body scroll when chat is open. Uses two mechanisms:
|
|
72
|
+
// 1. position:fixed on body (mobile keyboard safe)
|
|
73
|
+
// 2. wheel/touch listeners on the chat window to prevent event leakage
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (open && !closing) {
|
|
76
|
+
const scrollY = window.scrollY;
|
|
77
|
+
document.body.style.position = "fixed";
|
|
78
|
+
document.body.style.top = `-${scrollY}px`;
|
|
79
|
+
document.body.style.width = "100%";
|
|
80
|
+
} else if (!open) {
|
|
81
|
+
const top = Math.abs(parseInt(document.body.style.top || "0", 10));
|
|
82
|
+
document.body.style.position = "";
|
|
83
|
+
document.body.style.top = "";
|
|
84
|
+
document.body.style.width = "";
|
|
85
|
+
window.scrollTo(0, top);
|
|
86
|
+
}
|
|
87
|
+
return () => {
|
|
88
|
+
const top = Math.abs(parseInt(document.body.style.top || "0", 10));
|
|
89
|
+
document.body.style.position = "";
|
|
90
|
+
document.body.style.top = "";
|
|
91
|
+
document.body.style.width = "";
|
|
92
|
+
if (open) window.scrollTo(0, top);
|
|
93
|
+
};
|
|
94
|
+
}, [open, closing]);
|
|
95
|
+
|
|
96
|
+
// Prevent events on the chat window from scrolling the page behind.
|
|
97
|
+
// Only blocks events targeting the window itself, not the scrollable messages area.
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const el = chatWindowRef.current;
|
|
100
|
+
if (!el || !open) return;
|
|
101
|
+
|
|
102
|
+
function blockIfOutsideMessages(e: WheelEvent | TouchEvent) {
|
|
103
|
+
const target = e.target as HTMLElement;
|
|
104
|
+
// Allow scroll inside the messages container
|
|
105
|
+
if (target.closest('[data-chat-messages]')) return;
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
el.addEventListener("wheel", blockIfOutsideMessages, { passive: false });
|
|
110
|
+
el.addEventListener("touchmove", blockIfOutsideMessages, { passive: false });
|
|
111
|
+
|
|
112
|
+
return () => {
|
|
113
|
+
el.removeEventListener("wheel", blockIfOutsideMessages);
|
|
114
|
+
el.removeEventListener("touchmove", blockIfOutsideMessages);
|
|
115
|
+
};
|
|
116
|
+
}, [open]);
|
|
117
|
+
|
|
70
118
|
useEffect(() => {
|
|
71
119
|
// Don't scroll on first render — wait for the open animation
|
|
72
120
|
if (messages.length > 1) {
|
|
@@ -282,6 +330,7 @@ export default function ChatWidget({
|
|
|
282
330
|
|
|
283
331
|
{open && (
|
|
284
332
|
<div
|
|
333
|
+
ref={chatWindowRef}
|
|
285
334
|
className={closing ? "chat-window-out" : "chat-window"}
|
|
286
335
|
style={{
|
|
287
336
|
position: "fixed",
|
|
@@ -292,6 +341,7 @@ export default function ChatWidget({
|
|
|
292
341
|
maxWidth: "calc(100vw - 48px)",
|
|
293
342
|
height: chatHeight,
|
|
294
343
|
maxHeight: "calc(100dvh - 48px)",
|
|
344
|
+
overscrollBehavior: "contain",
|
|
295
345
|
background: "var(--chat-primary-fg, #fff)",
|
|
296
346
|
display: "flex",
|
|
297
347
|
flexDirection: "column",
|
|
@@ -351,9 +401,11 @@ export default function ChatWidget({
|
|
|
351
401
|
|
|
352
402
|
{/* Messages */}
|
|
353
403
|
<div
|
|
404
|
+
data-chat-messages
|
|
354
405
|
style={{
|
|
355
406
|
flex: 1,
|
|
356
407
|
overflowY: "auto",
|
|
408
|
+
overscrollBehavior: "contain",
|
|
357
409
|
padding: "12px 16px",
|
|
358
410
|
display: "flex",
|
|
359
411
|
flexDirection: "column",
|