@tp3/chat-widget 0.1.9 → 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,6 +64,7 @@ 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);
69
70
  (0, import_react.useEffect)(() => {
@@ -87,6 +88,21 @@ function ChatWidget({
87
88
  if (open) window.scrollTo(0, top);
88
89
  };
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]);
90
106
  (0, import_react.useEffect)(() => {
91
107
  if (messages.length > 1) {
92
108
  messagesEnd.current?.scrollIntoView({ behavior: "smooth" });
@@ -282,6 +298,7 @@ function ChatWidget({
282
298
  open && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
283
299
  "div",
284
300
  {
301
+ ref: chatWindowRef,
285
302
  className: closing ? "chat-window-out" : "chat-window",
286
303
  style: {
287
304
  position: "fixed",
@@ -292,6 +309,7 @@ function ChatWidget({
292
309
  maxWidth: "calc(100vw - 48px)",
293
310
  height: chatHeight,
294
311
  maxHeight: "calc(100dvh - 48px)",
312
+ overscrollBehavior: "contain",
295
313
  background: "var(--chat-primary-fg, #fff)",
296
314
  display: "flex",
297
315
  flexDirection: "column",
@@ -356,6 +374,7 @@ function ChatWidget({
356
374
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
357
375
  "div",
358
376
  {
377
+ "data-chat-messages": true,
359
378
  style: {
360
379
  flex: 1,
361
380
  overflowY: "auto",
@@ -40,6 +40,7 @@ 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);
45
46
  useEffect(() => {
@@ -63,6 +64,21 @@ function ChatWidget({
63
64
  if (open) window.scrollTo(0, top);
64
65
  };
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]);
66
82
  useEffect(() => {
67
83
  if (messages.length > 1) {
68
84
  messagesEnd.current?.scrollIntoView({ behavior: "smooth" });
@@ -258,6 +274,7 @@ function ChatWidget({
258
274
  open && /* @__PURE__ */ jsxs(
259
275
  "div",
260
276
  {
277
+ ref: chatWindowRef,
261
278
  className: closing ? "chat-window-out" : "chat-window",
262
279
  style: {
263
280
  position: "fixed",
@@ -268,6 +285,7 @@ function ChatWidget({
268
285
  maxWidth: "calc(100vw - 48px)",
269
286
  height: chatHeight,
270
287
  maxHeight: "calc(100dvh - 48px)",
288
+ overscrollBehavior: "contain",
271
289
  background: "var(--chat-primary-fg, #fff)",
272
290
  display: "flex",
273
291
  flexDirection: "column",
@@ -332,6 +350,7 @@ function ChatWidget({
332
350
  /* @__PURE__ */ jsxs(
333
351
  "div",
334
352
  {
353
+ "data-chat-messages": true,
335
354
  style: {
336
355
  flex: 1,
337
356
  overflowY: "auto",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tp3/chat-widget",
3
- "version": "0.1.9",
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,10 +64,13 @@ 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
 
70
- // Lock body scroll when chat is open (no overflow:hidden to avoid keyboard issues)
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
71
74
  useEffect(() => {
72
75
  if (open && !closing) {
73
76
  const scrollY = window.scrollY;
@@ -90,6 +93,28 @@ export default function ChatWidget({
90
93
  };
91
94
  }, [open, closing]);
92
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
+
93
118
  useEffect(() => {
94
119
  // Don't scroll on first render — wait for the open animation
95
120
  if (messages.length > 1) {
@@ -305,6 +330,7 @@ export default function ChatWidget({
305
330
 
306
331
  {open && (
307
332
  <div
333
+ ref={chatWindowRef}
308
334
  className={closing ? "chat-window-out" : "chat-window"}
309
335
  style={{
310
336
  position: "fixed",
@@ -315,6 +341,7 @@ export default function ChatWidget({
315
341
  maxWidth: "calc(100vw - 48px)",
316
342
  height: chatHeight,
317
343
  maxHeight: "calc(100dvh - 48px)",
344
+ overscrollBehavior: "contain",
318
345
  background: "var(--chat-primary-fg, #fff)",
319
346
  display: "flex",
320
347
  flexDirection: "column",
@@ -374,6 +401,7 @@ export default function ChatWidget({
374
401
 
375
402
  {/* Messages */}
376
403
  <div
404
+ data-chat-messages
377
405
  style={{
378
406
  flex: 1,
379
407
  overflowY: "auto",