@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.
@@ -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",
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tp3/chat-widget",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "main": "dist/ChatWidget.js",
5
5
  "module": "dist/ChatWidget.mjs",
6
6
  "types": "dist/ChatWidget.d.ts",
@@ -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",