testimonials 0.5.2 → 0.6.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a660a9eadbc3bfa58f9fe6bfc2d9f4feebe867fe536fe0755a156c9b931aca49
4
- data.tar.gz: 73c439422ee9c88ee56836338dc8be3b9e41063cec3009bad76025673ca675d6
3
+ metadata.gz: 789b6567b59d690a280075cd7cf5f0ba49ca6db9463e383fcdc8d655caf5e742
4
+ data.tar.gz: c846b3cf72bcae677ab487276c9436d0a22125887ebe41462c474179a263dcc1
5
5
  SHA512:
6
- metadata.gz: 1198770a514947ebc0a23a78a19f29a690c906c03c12e2135b8aa7a4538d4a6f3b406ae0822123ff4b3763e8502111298f1d9b37791dba9254305fc3634fb426
7
- data.tar.gz: 6b5585808618f90d8826fdd4f49a142442ac230417e1a2d0272b755b8b497c4fb31f05fef262c02650715b68af94c6f1b36b1ee7d07830a51a82e4f114a65b58
6
+ metadata.gz: 7f155c2fe3927e8c31fd747ef63f641e20315407ae9e12f5f26e67786522c13daa3184364d5792ec87d69590bfc3abe95029ffade71a8be71d5a8c19eb2bcf00
7
+ data.tar.gz: 1b43087d51e2dd5c1c94228bd39335a5f4f7d5605e9e5c96d3ef586bf8951d9e56deabc4e3c45f149613599813a0dfa7bc1f80735e44a4208d7fe73701f1a7db
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ - **The full-screen mobile dialog now survives the on-screen keyboard.** On
6
+ phones the dialog is a fixed, full-height (`100dvh`) element, but iOS Safari
7
+ raises the keyboard without resizing it — so the composer and Submit/Cancel
8
+ row slid behind the keyboard. While the dialog is open the widget now tracks
9
+ `window.visualViewport` and pins the overlay (and the dialog filling it) to
10
+ the visible viewport, so the action row sits just above the keyboard. Gated on
11
+ the `max-width:480px` mobile media query, so desktop is untouched; browsers
12
+ without `visualViewport` no-op and keep the previous behavior. Shared change
13
+ across the gem family.
14
+ - The "🎥 Video attached | Remove" row under a video preview is replaced with a
15
+ discreet round ✕ button in the top-right corner of the preview (the standard
16
+ "remove attachment" affordance) — clearly a delete control, but no longer a
17
+ full-width, easy-to-mis-tap target. Applies to both a just-recorded/uploaded
18
+ clip and an existing video being edited; remove behavior is unchanged.
19
+
3
20
  ## 0.5.2
4
21
 
5
22
  - A just-recorded or just-uploaded video now shows a playable preview on the
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Testimonials
4
- VERSION = '0.5.2'
4
+ VERSION = '0.6.0'
5
5
  end
@@ -32,6 +32,7 @@
32
32
  var savedOverflow = null; // documentElement overflow to restore when the modal closes
33
33
  var state = null; // one open session: { kind, auto, stage, rating, videoBlob, ... }
34
34
  var media = { stream: null, recorder: null, chunks: [], timer: null };
35
+ var viewportHandler = null; // visualViewport listener while the dialog is open
35
36
 
36
37
  function ready(fn) {
37
38
  if (document.readyState === "loading") {
@@ -154,13 +155,69 @@
154
155
  document.body.appendChild(overlay);
155
156
  lockScroll();
156
157
  overlay.addEventListener("keydown", trapFocus);
158
+ startViewportTracking();
157
159
 
158
160
  if (auto) postEvent(kind, "shown");
159
161
  return true;
160
162
  }
161
163
 
164
+ // --- mobile keyboard --------------------------------------------------------
165
+ // On phones the dialog is full-screen at height:100dvh, but iOS Safari shows
166
+ // the on-screen keyboard WITHOUT resizing that fixed element — so the action
167
+ // row gets hidden behind the keyboard. While the dialog is open we track
168
+ // window.visualViewport and pin the full-screen overlay (and the dialog that
169
+ // fills it) to the currently-visible area, so Submit/Cancel sit just above the
170
+ // keyboard. The dialog itself is a static-positioned flex child, so `top` on
171
+ // it does nothing — we move the fixed overlay for position and size the dialog
172
+ // to match. Desktop is untouched: everything is gated on the mobile media
173
+ // query, and browsers without visualViewport no-op (today's behavior).
174
+
175
+ function applyViewport() {
176
+ if (!overlay) return;
177
+ var dialog = overlay.querySelector("#tml-dialog");
178
+ var vv = window.visualViewport;
179
+ if (vv && window.matchMedia("(max-width:480px)").matches) {
180
+ overlay.style.top = vv.offsetTop + "px";
181
+ overlay.style.height = vv.height + "px";
182
+ overlay.style.bottom = "auto"; // height + top win; drop the inset:0 bottom
183
+ if (dialog) dialog.style.height = vv.height + "px";
184
+ } else {
185
+ // Not the mobile full-screen layout: clear every override so the
186
+ // stylesheet's centered desktop rules apply cleanly.
187
+ overlay.style.top = "";
188
+ overlay.style.height = "";
189
+ overlay.style.bottom = "";
190
+ if (dialog) dialog.style.height = "";
191
+ }
192
+ }
193
+
194
+ function startViewportTracking() {
195
+ if (!window.visualViewport) return;
196
+ viewportHandler = function () { applyViewport(); };
197
+ window.visualViewport.addEventListener("resize", viewportHandler);
198
+ window.visualViewport.addEventListener("scroll", viewportHandler);
199
+ applyViewport(); // pin once on open
200
+ }
201
+
202
+ function stopViewportTracking() {
203
+ if (viewportHandler && window.visualViewport) {
204
+ window.visualViewport.removeEventListener("resize", viewportHandler);
205
+ window.visualViewport.removeEventListener("scroll", viewportHandler);
206
+ }
207
+ viewportHandler = null;
208
+ // Clear inline styles so a later desktop open starts from the stylesheet.
209
+ if (overlay) {
210
+ overlay.style.top = "";
211
+ overlay.style.height = "";
212
+ overlay.style.bottom = "";
213
+ var dialog = overlay.querySelector("#tml-dialog");
214
+ if (dialog) dialog.style.height = "";
215
+ }
216
+ }
217
+
162
218
  function close() {
163
219
  if (!overlay) return;
220
+ stopViewportTracking();
164
221
  stopMedia();
165
222
  if (state && state.playbackUrl) URL.revokeObjectURL(state.playbackUrl);
166
223
  if (state && state.posterUrl) URL.revokeObjectURL(state.posterUrl);
@@ -194,7 +251,12 @@
194
251
  dialog.textContent = "";
195
252
  dialog.appendChild(node);
196
253
  var focusable = dialog.querySelector("textarea, input, button");
197
- if (focusable && overlay) focusable.focus();
254
+ if (focusable && overlay) {
255
+ focusable.focus();
256
+ // With the viewport pinned above the keyboard the shrink handles most of
257
+ // it; nudge the focused field fully into view for the rest.
258
+ if (focusable.scrollIntoView) focusable.scrollIntoView({ block: "nearest" });
259
+ }
198
260
  }
199
261
 
200
262
  function trapFocus(event) {
@@ -429,8 +491,7 @@
429
491
  state.posterUrl = URL.createObjectURL(state.posterBlob);
430
492
  recorded.poster = state.posterUrl;
431
493
  }
432
- wrap.appendChild(recorded);
433
- wrap.appendChild(attachedVideoChip(form, wrap, recorded, function () {
494
+ wrap.appendChild(videoPreview(form, wrap, recorded, function () {
434
495
  state.videoBlob = null;
435
496
  }));
436
497
  return wrap;
@@ -448,8 +509,7 @@
448
509
  // fix for Safari, which won't paint a fragmented-MP4 frame on its own.
449
510
  if (state.existingPosterUrl) playback.poster = state.existingPosterUrl;
450
511
  playback.src = state.existingVideoUrl;
451
- wrap.appendChild(playback);
452
- wrap.appendChild(attachedVideoChip(form, wrap, playback, function () {
512
+ wrap.appendChild(videoPreview(form, wrap, playback, function () {
453
513
  state.removeExistingVideo = true;
454
514
  }));
455
515
  return wrap;
@@ -475,18 +535,21 @@
475
535
  return wrap;
476
536
  }
477
537
 
478
- function attachedVideoChip(form, wrap, playback, onRemove) {
479
- var chip = el("div", "tml-chip");
480
- chip.appendChild(el("span", "tml-chip-label", "🎥 " + config.labels.videoAttached));
481
- var remove = el("button", "tml-chip-remove", config.labels.remove);
538
+ // A discreet "remove attachment" affordance: the video preview wrapped so a
539
+ // small round ✕ can sit in its top-right corner (overlaid on the clip, clear
540
+ // of the centered play button), instead of a full-width, easy-to-mis-tap row.
541
+ function videoPreview(form, wrap, playback, onRemove) {
542
+ var box = el("div", "tml-video-wrap");
543
+ box.appendChild(playback);
544
+ var remove = el("button", "tml-video-remove", "✕");
482
545
  remove.type = "button";
546
+ remove.setAttribute("aria-label", config.labels.remove);
483
547
  remove.addEventListener("click", function () {
484
548
  onRemove();
485
- if (playback) playback.remove();
486
549
  wrap.replaceWith(videoControl(form));
487
550
  });
488
- chip.appendChild(remove);
489
- return chip;
551
+ box.appendChild(remove);
552
+ return box;
490
553
  }
491
554
 
492
555
  // Recording lives in its own stage: camera check -> 3-2-1 -> recording ->
@@ -1117,11 +1180,14 @@
1117
1180
  ".tml-record:hover{background:rgba(37,99,235,.12)}",
1118
1181
  ".tml-record-label{color:#2563eb;font-weight:600}",
1119
1182
  ".tml-record-hint{font-size:12px;color:#6b7280}",
1120
- ".tml-chip{display:flex;align-items:center;justify-content:space-between;gap:8px;",
1121
- "padding:10px 12px;border-radius:10px;background:rgba(37,99,235,.07)}",
1122
- ".tml-chip-label{font-weight:600}",
1123
- ".tml-chip-remove{border:0;background:none;color:#2563eb;cursor:pointer;font:inherit;padding:2px 6px}",
1124
1183
  ".tml-video{display:block;width:100%;border-radius:10px;background:#000;aspect-ratio:4/3;margin:0 0 12px}",
1184
+ // A recorded/uploaded/existing clip with a discreet corner remove control.
1185
+ ".tml-video-wrap{position:relative;margin:0 0 12px}",
1186
+ ".tml-video-wrap .tml-video{margin:0}",
1187
+ ".tml-video-remove{position:absolute;top:8px;inset-inline-end:8px;width:28px;height:28px;padding:0;",
1188
+ "display:flex;align-items:center;justify-content:center;border:0;border-radius:50%;",
1189
+ "background:rgba(0,0,0,.6);color:#fff;font-size:15px;line-height:1;cursor:pointer}",
1190
+ ".tml-video-remove:hover{background:rgba(0,0,0,.8)}",
1125
1191
  ".tml-preview{position:relative;margin-bottom:4px}",
1126
1192
  ".tml-preview .tml-video{margin-bottom:0}",
1127
1193
  ".tml-timer{position:absolute;top:8px;inset-inline-end:8px;background:rgba(0,0,0,.55);color:#fff;",
@@ -1151,8 +1217,7 @@
1151
1217
  ".tml-nps-score{border-color:#2a313a}",
1152
1218
  ".tml-record{border-color:#3b82f6;background:rgba(59,130,246,.1)}",
1153
1219
  ".tml-record:hover{background:rgba(59,130,246,.18)}",
1154
- ".tml-record-label,.tml-chip-remove{color:#3b82f6}",
1155
- ".tml-chip{background:rgba(59,130,246,.12)}",
1220
+ ".tml-record-label{color:#3b82f6}",
1156
1221
  ".tml-hint,.tml-nps-legend,.tml-record-hint{color:#9aa2ab}",
1157
1222
  "}",
1158
1223
  // The dialog is the scrollable region; keep its scroll from chaining
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: testimonials
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov