livechat 0.3.2 → 0.3.4
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 +14 -0
- data/README.md +14 -0
- data/lib/generators/livechat/install/templates/initializer.rb +8 -0
- data/lib/livechat/version.rb +1 -1
- data/lib/livechat/widget.js +52 -0
- 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: aa83a99e1d1ebe0259347b286862ec7cf7cfffbfe47ea4a624baff36b3ca066e
|
|
4
|
+
data.tar.gz: 0d017808858c6dfbce423e283b061476a74b3075b42bb70c3f10c54fb74b5622
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97dc8a43bfb5ab7fecd0dabdfaf5f126e3346b97e73f56aa96129331046e74355bbc201a7fc0ab010c1ef0e48a73d15ec7aaadac0dd185c4310480271486984c
|
|
7
|
+
data.tar.gz: e1469a54e206b39b539d6dacc767568caad7a44da3540d5ff3b746fb0affb74e83224f3ca9f0ea87163f2796656d66b274aaa58332e1cd40ddfda9b441b1028d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.4
|
|
4
|
+
|
|
5
|
+
- Docs: show how to wire `current_user` with Rails 8's built-in authentication
|
|
6
|
+
(`bin/rails generate authentication`), alongside the existing Devise/Warden
|
|
7
|
+
example — in the README and the generated initializer.
|
|
8
|
+
|
|
9
|
+
## 0.3.3
|
|
10
|
+
|
|
11
|
+
- Accessibility: on phones (where the panel is a full-screen modal) focus is
|
|
12
|
+
trapped inside it and page scroll is locked behind it; closing restores
|
|
13
|
+
focus to whatever opened the chat. Errors and the email-saved note are now
|
|
14
|
+
announced to screen readers. The desktop popover deliberately stays
|
|
15
|
+
non-modal — you can read the page while chatting.
|
|
16
|
+
|
|
3
17
|
## 0.3.2
|
|
4
18
|
|
|
5
19
|
- The chat panel is full-screen on phones (≤480px) — a conversation is an
|
data/README.md
CHANGED
|
@@ -91,6 +91,20 @@ Livechat.configure do |config|
|
|
|
91
91
|
end
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
`current_user` (and any admin gate) receives the raw request, so it works
|
|
95
|
+
with whatever auth you have:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
# Devise / Warden:
|
|
99
|
+
config.current_user = ->(request) { request.env["warden"]&.user }
|
|
100
|
+
|
|
101
|
+
# Rails 8 built-in auth (bin/rails generate authentication):
|
|
102
|
+
config.current_user = lambda do |request|
|
|
103
|
+
token = request.cookies["session_token"]
|
|
104
|
+
Session.find_signed(token)&.user if token
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
94
108
|
### Brand color
|
|
95
109
|
|
|
96
110
|
```ruby
|
|
@@ -16,7 +16,15 @@ Livechat.configure do |config|
|
|
|
16
16
|
# Resolve the current user (optional). Return an object responding to #id,
|
|
17
17
|
# or nil. Signed-in visitors keep one conversation across devices; guests
|
|
18
18
|
# are tracked with a cookie. The same user is the agent when replying.
|
|
19
|
+
#
|
|
20
|
+
# Devise / Warden:
|
|
19
21
|
# config.current_user = ->(request) { request.env["warden"]&.user }
|
|
22
|
+
#
|
|
23
|
+
# Rails 8 built-in auth (bin/rails generate authentication):
|
|
24
|
+
# config.current_user = lambda do |request|
|
|
25
|
+
# token = request.cookies["session_token"]
|
|
26
|
+
# Session.find_signed(token)&.user if token
|
|
27
|
+
# end
|
|
20
28
|
|
|
21
29
|
# How visitors appear in the inbox.
|
|
22
30
|
# config.visitor_label = ->(user) { user.name.presence || user.email }
|
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() {
|