livechat 0.3.1 → 0.3.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/lib/livechat/version.rb +1 -1
- data/lib/livechat/widget.js +66 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3263710888d695851123f5afeffc0dd73985901e0ad559f5d04562a59c411537
|
|
4
|
+
data.tar.gz: 9db6f6b5df671b05b594dd174180cc54c2988695aac907ab3f7f3e38c32df732
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5ae2806f1b7f1c8d08b14bc61043c8fe23159873c260a75c3ed6a5b263bc2ada95630af19a323a64c3de774d107e17aef8094454535460bba76cb10c91d5846
|
|
7
|
+
data.tar.gz: cda4c08755760e0a1a1996be4aa5165e8d7ddf10ca86cd0362905b0d02c5ee1dd8e73f3a2f8dcec3cbb9b3b68a23418d3898f9bca48146365799544ca029d332
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.3
|
|
4
|
+
|
|
5
|
+
- Accessibility: on phones (where the panel is a full-screen modal) focus is
|
|
6
|
+
trapped inside it and page scroll is locked behind it; closing restores
|
|
7
|
+
focus to whatever opened the chat. Errors and the email-saved note are now
|
|
8
|
+
announced to screen readers. The desktop popover deliberately stays
|
|
9
|
+
non-modal — you can read the page while chatting.
|
|
10
|
+
|
|
11
|
+
## 0.3.2
|
|
12
|
+
|
|
13
|
+
- The chat panel is full-screen on phones (≤480px) — a conversation is an
|
|
14
|
+
app screen, not a floating card. Inputs are 16px on mobile so iOS Safari
|
|
15
|
+
never zoom-jumps on focus; the composer respects the home-indicator safe
|
|
16
|
+
area; thread scrolling no longer rubber-bands the page behind it.
|
|
17
|
+
|
|
3
18
|
## 0.3.1
|
|
4
19
|
|
|
5
20
|
- The Agents column shows initials avatars (deterministic colors, full name
|
data/lib/livechat/version.rb
CHANGED
data/lib/livechat/widget.js
CHANGED
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
var emailRowEl = null;
|
|
34
34
|
var errorEl = null;
|
|
35
35
|
var isOpen = false;
|
|
36
|
+
var lastFocused = null;
|
|
37
|
+
// On phones the panel is a full-screen modal (focus trapped, page scroll
|
|
38
|
+
// locked); on desktop it stays a non-modal popover you can chat alongside.
|
|
39
|
+
var mobileModal = window.matchMedia ? window.matchMedia("(max-width: 480px)") : null;
|
|
40
|
+
var savedOverflow = null;
|
|
36
41
|
// The id of the newest message actually RENDERED into the list — never
|
|
37
42
|
// advanced by background polls, so reopening the panel refetches exactly
|
|
38
43
|
// the messages it hasn't shown yet.
|
|
@@ -112,6 +117,9 @@
|
|
|
112
117
|
isOpen = false;
|
|
113
118
|
lastRenderedId = 0;
|
|
114
119
|
lastAuthorKey = null;
|
|
120
|
+
// A Turbo body swap removes the panel without closePanel() running; the
|
|
121
|
+
// documentElement (and any scroll lock on it) survives the swap.
|
|
122
|
+
unlockScroll();
|
|
115
123
|
|
|
116
124
|
root = document.createElement("div");
|
|
117
125
|
root.id = "lvc-root";
|
|
@@ -173,6 +181,7 @@
|
|
|
173
181
|
panel.id = "lvc-panel";
|
|
174
182
|
panel.setAttribute("role", "dialog");
|
|
175
183
|
panel.setAttribute("aria-label", config.appName);
|
|
184
|
+
panel.addEventListener("keydown", trapFocus);
|
|
176
185
|
|
|
177
186
|
var header = document.createElement("div");
|
|
178
187
|
header.id = "lvc-header";
|
|
@@ -206,6 +215,9 @@
|
|
|
206
215
|
|
|
207
216
|
errorEl = document.createElement("div");
|
|
208
217
|
errorEl.id = "lvc-error";
|
|
218
|
+
// The alert region exists before any text lands in it, so screen
|
|
219
|
+
// readers announce the message instead of a silent visual update.
|
|
220
|
+
errorEl.setAttribute("role", "alert");
|
|
209
221
|
errorEl.hidden = true;
|
|
210
222
|
panel.appendChild(errorEl);
|
|
211
223
|
|
|
@@ -252,6 +264,7 @@
|
|
|
252
264
|
function buildEmailRow() {
|
|
253
265
|
var row = document.createElement("form");
|
|
254
266
|
row.id = "lvc-email";
|
|
267
|
+
row.setAttribute("aria-live", "polite"); // "we'll also reply by email" is a status
|
|
255
268
|
row.hidden = true;
|
|
256
269
|
|
|
257
270
|
var label = document.createElement("span");
|
|
@@ -297,6 +310,7 @@
|
|
|
297
310
|
mount();
|
|
298
311
|
if (!isOpen) {
|
|
299
312
|
isOpen = true;
|
|
313
|
+
lastFocused = document.activeElement;
|
|
300
314
|
sessionSet("livechat_open", "1");
|
|
301
315
|
|
|
302
316
|
// The panel is built once and then hidden/shown — closing must never
|
|
@@ -307,6 +321,8 @@
|
|
|
307
321
|
root.appendChild(panel);
|
|
308
322
|
}
|
|
309
323
|
panel.hidden = false;
|
|
324
|
+
panel.setAttribute("aria-modal", isMobileModal() ? "true" : "false");
|
|
325
|
+
if (isMobileModal()) lockScroll();
|
|
310
326
|
setUnread(0);
|
|
311
327
|
poll();
|
|
312
328
|
schedulePoll();
|
|
@@ -325,9 +341,45 @@
|
|
|
325
341
|
sessionSet("livechat_open", "");
|
|
326
342
|
var panel = document.getElementById("lvc-panel");
|
|
327
343
|
if (panel) panel.hidden = true;
|
|
344
|
+
unlockScroll();
|
|
345
|
+
if (lastFocused && document.contains(lastFocused)) lastFocused.focus();
|
|
328
346
|
schedulePoll();
|
|
329
347
|
}
|
|
330
348
|
|
|
349
|
+
function isMobileModal() {
|
|
350
|
+
return !!(mobileModal && mobileModal.matches);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Tab cycles inside the panel only while it is the full-screen mobile
|
|
354
|
+
// modal; the desktop popover deliberately lets focus reach the page.
|
|
355
|
+
function trapFocus(event) {
|
|
356
|
+
if (event.key !== "Tab" || !isMobileModal()) return;
|
|
357
|
+
var panel = document.getElementById("lvc-panel");
|
|
358
|
+
var focusable = panel.querySelectorAll("button, [href], input, textarea, select");
|
|
359
|
+
if (!focusable.length) return;
|
|
360
|
+
var first = focusable[0];
|
|
361
|
+
var last = focusable[focusable.length - 1];
|
|
362
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
363
|
+
event.preventDefault();
|
|
364
|
+
last.focus();
|
|
365
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
366
|
+
event.preventDefault();
|
|
367
|
+
first.focus();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function lockScroll() {
|
|
372
|
+
if (savedOverflow !== null) return;
|
|
373
|
+
savedOverflow = document.documentElement.style.overflow;
|
|
374
|
+
document.documentElement.style.overflow = "hidden";
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function unlockScroll() {
|
|
378
|
+
if (savedOverflow === null) return;
|
|
379
|
+
document.documentElement.style.overflow = savedOverflow;
|
|
380
|
+
savedOverflow = null;
|
|
381
|
+
}
|
|
382
|
+
|
|
331
383
|
// --- polling ----------------------------------------------------------------
|
|
332
384
|
|
|
333
385
|
function schedulePoll() {
|
|
@@ -548,8 +600,6 @@
|
|
|
548
600
|
"#lvc-panel[hidden]{display:none}" +
|
|
549
601
|
"#lvc-root[dir=rtl] #lvc-panel{right:auto;left:20px}" +
|
|
550
602
|
"#lvc-root.lvc-no-launcher #lvc-panel{bottom:20px;max-height:calc(100dvh - 40px)}" +
|
|
551
|
-
"@media(max-width:480px){#lvc-panel{right:12px;bottom:80px}" +
|
|
552
|
-
"#lvc-root.lvc-no-launcher #lvc-panel{right:12px;bottom:12px;max-height:calc(100dvh - 24px)}}" +
|
|
553
603
|
"#lvc-header{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;" +
|
|
554
604
|
"padding:14px 16px;background:var(--lvc-accent);color:var(--lvc-accent-text)}" +
|
|
555
605
|
"#lvc-header strong{display:block;font-size:15px}" +
|
|
@@ -557,8 +607,8 @@
|
|
|
557
607
|
"#lvc-close{border:none;background:none;color:var(--lvc-accent-text);font-size:22px;" +
|
|
558
608
|
"line-height:1;cursor:pointer;padding:2px 6px;border-radius:6px}" +
|
|
559
609
|
"#lvc-close:hover{background:rgba(255,255,255,.15)}" +
|
|
560
|
-
"#lvc-list{flex:1;overflow-y:auto;padding:14px;
|
|
561
|
-
"flex-direction:column;gap:4px}" +
|
|
610
|
+
"#lvc-list{flex:1;overflow-y:auto;overscroll-behavior:contain;padding:14px;" +
|
|
611
|
+
"background:var(--lvc-bg);display:flex;flex-direction:column;gap:4px}" +
|
|
562
612
|
// Class rules carry the #lvc-root prefix so they outrank the
|
|
563
613
|
// id-level `#lvc-root *` reset above.
|
|
564
614
|
"#lvc-root .lvc-greeting{color:var(--lvc-muted);font-size:13px;margin-bottom:8px}" +
|
|
@@ -594,6 +644,18 @@
|
|
|
594
644
|
"display:flex;align-items:center;justify-content:center;cursor:pointer}" +
|
|
595
645
|
"#lvc-form button:disabled{opacity:.6;cursor:default}" +
|
|
596
646
|
"#lvc-root[dir=rtl] #lvc-form button svg{transform:scaleX(-1)}" +
|
|
647
|
+
// Phones get the whole screen — a chat is an app screen, not a popup.
|
|
648
|
+
// Selectors repeat the id twice so they outrank the rtl/no-launcher
|
|
649
|
+
// desktop rules regardless of source order.
|
|
650
|
+
"@media(max-width:480px){" +
|
|
651
|
+
"#lvc-root #lvc-panel,#lvc-root[dir=rtl] #lvc-panel,#lvc-root.lvc-no-launcher #lvc-panel" +
|
|
652
|
+
"{left:0;right:0;top:0;bottom:0;width:100%;max-width:100%;height:100dvh;max-height:100dvh;" +
|
|
653
|
+
"border-radius:0;border:none}" +
|
|
654
|
+
// 16px stops iOS Safari from zoom-jumping into focused fields.
|
|
655
|
+
"#lvc-root #lvc-form textarea,#lvc-root #lvc-email input{font-size:16px}" +
|
|
656
|
+
// Keep the composer above the iPhone home indicator.
|
|
657
|
+
"#lvc-root #lvc-form{padding-bottom:calc(10px + env(safe-area-inset-bottom))}" +
|
|
658
|
+
"}" +
|
|
597
659
|
// Lives in host DOM (on data-livechat-open elements), so no #lvc-root prefix.
|
|
598
660
|
".lvc-opener-badge{display:inline-flex;min-width:18px;height:18px;padding:0 5px;" +
|
|
599
661
|
"margin-inline-start:6px;border-radius:999px;background:#dc2626;color:#fff;" +
|