studio-engine 0.62.4 → 0.62.5
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 629e819d3b8b6a9c92140a69829e75a3d6d6c3c8e9ff4faeede098ffe7394a1e
|
|
4
|
+
data.tar.gz: 562d9d5caffa68f7c282cc91cb91369fe132a8e90c63257b97f43bf4d328669b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3bc2df617a2f90f9c46efde0d73f7f39f7b2f063147971303d004b080c80af4710cb4cce605cbb7a608e87c37bcdd58dceaa4a2d0088c791b9024625c3da1fb3
|
|
7
|
+
data.tar.gz: 5a69fabcce40e7e93c5b8d0a3b88431a74d4047e100854393be1fb5ead4b4bacb0229b7182953e148655fe64a1faa60eacbb6aca426531fb20e34bff30116686
|
|
@@ -301,7 +301,7 @@
|
|
|
301
301
|
_backdropEl = null;
|
|
302
302
|
// Only restore if the element is still in the document — a Turbo visit can
|
|
303
303
|
// replace the page under an open modal, and focusing a detached node
|
|
304
|
-
// silently moves focus to
|
|
304
|
+
// silently moves focus to the document body, which is worse than leaving it alone.
|
|
305
305
|
if (target && target.focus && document.contains(target)) target.focus();
|
|
306
306
|
},
|
|
307
307
|
|
|
@@ -349,13 +349,25 @@
|
|
|
349
349
|
var entry = this.current();
|
|
350
350
|
if (!entry) return 'Dialog';
|
|
351
351
|
var p = entry.props || {};
|
|
352
|
-
|
|
352
|
+
// TRIMMED, not merely truthy. `title: ' '` is a truthy string, so it used to
|
|
353
|
+
// pass straight through — and the accessible-name computation then trims it to
|
|
354
|
+
// empty and announces a bare "dialog", the exact outcome this fallback chain
|
|
355
|
+
// exists to prevent. An empty string already fell through (it is falsy); a
|
|
356
|
+
// whitespace-only one did not, which is the more likely typo of the two.
|
|
357
|
+
var named = function(v) {
|
|
358
|
+
return (typeof v === 'string' && v.trim() !== '') ? v : null;
|
|
359
|
+
};
|
|
360
|
+
return named(p.ariaLabel) || named(p.title) || String(entry.id).replace(/[-_]/g, ' ');
|
|
353
361
|
},
|
|
354
362
|
|
|
355
363
|
open: function(id, props, opts) {
|
|
356
364
|
props = props || {};
|
|
357
365
|
opts = opts || {};
|
|
358
366
|
var self = this;
|
|
367
|
+
// Set by any branch that re-mounts the INNER content template without
|
|
368
|
+
// re-mounting the OUTER one — see the push branch below. The replace
|
|
369
|
+
// branch returns early and calls refocus() from its own phase 2.
|
|
370
|
+
var remounted = false;
|
|
359
371
|
|
|
360
372
|
if (opts.replace && this.stack.length > 0) {
|
|
361
373
|
var current = this.current();
|
|
@@ -404,10 +416,25 @@
|
|
|
404
416
|
}
|
|
405
417
|
// Already mid-close — just hot-swap so we don't double up timers.
|
|
406
418
|
this.stack[this.stack.length - 1] = { id: id, props: props };
|
|
419
|
+
remounted = true;
|
|
407
420
|
} else {
|
|
421
|
+
// A push onto a NON-EMPTY stack is the third re-mount seam, and the one
|
|
422
|
+
// the trap missed longest. current() stays truthy, so the OUTER template
|
|
423
|
+
// never re-mounts and x-init never re-runs captureFocus — while the INNER
|
|
424
|
+
// content template DOES re-mount and unmounts whatever was focused.
|
|
425
|
+
// Focus lands on the document body, which is not a descendant of the backdrop, so the
|
|
426
|
+
// @keydown.tab handler bound there stops firing and native tabbing resumes.
|
|
427
|
+
// MEASURED on /lab/birthday_gate: open a card, Tab once, push a second
|
|
428
|
+
// card, then Shift+Tab three times and you walk onto three BACKGROUND-PAGE
|
|
429
|
+
// buttons behind a still-open dialog.
|
|
430
|
+
//
|
|
431
|
+
// Only the FIRST push is safe: current() goes falsy -> truthy, the outer
|
|
432
|
+
// template mounts, and x-init captures for us.
|
|
433
|
+
remounted = this.stack.length > 0;
|
|
408
434
|
this.stack.push({ id: id, props: props });
|
|
409
435
|
}
|
|
410
436
|
this._sync();
|
|
437
|
+
if (remounted) this.refocus();
|
|
411
438
|
},
|
|
412
439
|
// Sugar — `swap('foo', props)` ≡ `open('foo', props, { replace: true })`.
|
|
413
440
|
// Pass opts through so callers can request a back-direction slide
|
|
@@ -491,7 +518,14 @@
|
|
|
491
518
|
// (open → swap → close) unmounts one dialog and mounts the next, and
|
|
492
519
|
// restoring to the background page in between would yank focus out of
|
|
493
520
|
// a flow the user is still inside.
|
|
521
|
+
//
|
|
522
|
+
// BUT NOT RESTORING IS NOT THE SAME AS DOING NOTHING. Closing down to an
|
|
523
|
+
// underlying modal re-mounts the inner template, so focus falls to the document body
|
|
524
|
+
// with the dialog still open — the trap is off while the user is still
|
|
525
|
+
// inside it. releaseFocus() was correctly gated on an empty stack; the
|
|
526
|
+
// missing half was the else. MEASURED on both hosts.
|
|
494
527
|
if (self.stack.length === 0) self.releaseFocus();
|
|
528
|
+
else self.refocus();
|
|
495
529
|
}, modalAnim('exit', entry.props && entry.props.exitAnim).ms);
|
|
496
530
|
},
|
|
497
531
|
closeAll: function() {
|
|
@@ -517,6 +551,15 @@
|
|
|
517
551
|
return modal.props && modal.props.dismissible === false;
|
|
518
552
|
});
|
|
519
553
|
this._sync();
|
|
554
|
+
// Same re-mount, same release: a surviving non-dismissible card is still on
|
|
555
|
+
// screen, and the cards dropped from under it took focus with them. MEASURED:
|
|
556
|
+
// activeElement the document body behind a still-open non-dismissible dialog.
|
|
557
|
+
//
|
|
558
|
+
// Nothing is done when the stack empties. That is deliberate and different
|
|
559
|
+
// from close(): this runs on the Turbo/bfcache teardown path, where the page
|
|
560
|
+
// is going away and closeAll() already declines to restore into a document
|
|
561
|
+
// about to be replaced.
|
|
562
|
+
if (this.stack.length > 0) this.refocus();
|
|
520
563
|
},
|
|
521
564
|
// isOpen(id) — is a card with this id ON THE STACK, in any
|
|
522
565
|
// lifecycle state? A card mid-close still counts: close() flips
|
|
@@ -162,7 +162,15 @@
|
|
|
162
162
|
var entry = this.current();
|
|
163
163
|
if (!entry) return 'Dialog';
|
|
164
164
|
var p = entry.props || {};
|
|
165
|
-
|
|
165
|
+
// TRIMMED, not merely truthy. `title: ' '` is a truthy string, so it used to
|
|
166
|
+
// pass straight through — and the accessible-name computation then trims it to
|
|
167
|
+
// empty and announces a bare "dialog", the exact outcome this fallback chain
|
|
168
|
+
// exists to prevent. An empty string already fell through (it is falsy); a
|
|
169
|
+
// whitespace-only one did not, which is the more likely typo of the two.
|
|
170
|
+
var named = function(v) {
|
|
171
|
+
return (typeof v === 'string' && v.trim() !== '') ? v : null;
|
|
172
|
+
};
|
|
173
|
+
return named(p.ariaLabel) || named(p.title) || String(entry.id).replace(/[-_]/g, ' ');
|
|
166
174
|
},
|
|
167
175
|
|
|
168
176
|
// open(id, props, opts) — opts.replace swaps the top entry in place
|
|
@@ -172,17 +180,26 @@
|
|
|
172
180
|
opts = opts || {};
|
|
173
181
|
var entry = { id: id, props: props };
|
|
174
182
|
|
|
175
|
-
|
|
183
|
+
// Did this call re-mount the INNER content template without re-mounting the
|
|
184
|
+
// OUTER one? That is the whole question the focus trap turns on.
|
|
185
|
+
var remounted = false;
|
|
176
186
|
if (opts.replace && this.stack.length > 0) {
|
|
177
187
|
this.stack.splice(this.stack.length - 1, 1, entry);
|
|
178
|
-
|
|
188
|
+
remounted = true;
|
|
179
189
|
} else {
|
|
190
|
+
// THE COMMENT THIS REPLACES SAID "only on a REPLACE", on the premise that a
|
|
191
|
+
// fresh push takes current() falsy -> truthy so the outer template mounts and
|
|
192
|
+
// x-init captures for us. That is TRUE only when the stack was EMPTY, and
|
|
193
|
+
// silently false for a stacked push: current() was already truthy, so the
|
|
194
|
+
// outer template does not re-mount, x-init never re-runs, and the inner
|
|
195
|
+
// template unmounts whatever was focused. Focus lands on the document body — not a
|
|
196
|
+
// descendant of the backdrop — so the @keydown.tab handler bound there stops
|
|
197
|
+
// firing and native tabbing walks straight onto the page behind.
|
|
198
|
+
remounted = this.stack.length > 0;
|
|
180
199
|
this.stack.push(entry);
|
|
181
200
|
}
|
|
182
201
|
this._sync();
|
|
183
|
-
|
|
184
|
-
// outer template mounts and x-init calls captureFocus for us.
|
|
185
|
-
if (replaced) this.refocus();
|
|
202
|
+
if (remounted) this.refocus();
|
|
186
203
|
},
|
|
187
204
|
|
|
188
205
|
swap: function(id, props, opts) {
|
|
@@ -205,7 +222,13 @@
|
|
|
205
222
|
// Only when the LAST modal leaves — a stacked flow unmounts one dialog
|
|
206
223
|
// and mounts the next, and restoring in between yanks focus out of a
|
|
207
224
|
// flow the user is still inside.
|
|
225
|
+
//
|
|
226
|
+
// NOT RESTORING IS NOT THE SAME AS DOING NOTHING. Closing down TO an
|
|
227
|
+
// underlying modal re-mounts the inner template, so focus falls to the document body
|
|
228
|
+
// with that dialog still open — the trap is off while the user is still
|
|
229
|
+
// inside it. The gate was right; the else was missing.
|
|
208
230
|
if (self.stack.length === 0) self.releaseFocus();
|
|
231
|
+
else self.refocus();
|
|
209
232
|
}, CLOSE_ANIM_MS);
|
|
210
233
|
},
|
|
211
234
|
|
|
@@ -224,6 +247,12 @@
|
|
|
224
247
|
return entry.props && entry.props.dismissible === false;
|
|
225
248
|
});
|
|
226
249
|
this._sync();
|
|
250
|
+
// Same re-mount, same release: a surviving non-dismissible card is still on
|
|
251
|
+
// screen and the cards dropped from under it took focus with them. Nothing is
|
|
252
|
+
// done when the stack empties — deliberately unlike close(): this is the
|
|
253
|
+
// Turbo/bfcache teardown path, where closeAll() already declines to restore
|
|
254
|
+
// into a document about to be replaced.
|
|
255
|
+
if (this.stack.length > 0) this.refocus();
|
|
227
256
|
},
|
|
228
257
|
|
|
229
258
|
// isOpen(id) — on the stack in ANY lifecycle state, including a card
|
|
@@ -78,6 +78,7 @@
|
|
|
78
78
|
_remaining: #{redirect_secs},
|
|
79
79
|
_total: #{redirect_secs},
|
|
80
80
|
_redirectTimer: null,
|
|
81
|
+
_confettiTimer: null,
|
|
81
82
|
startCountdown(url) {
|
|
82
83
|
if (!url) return;
|
|
83
84
|
var self = this;
|
|
@@ -92,9 +93,16 @@
|
|
|
92
93
|
}, 1000);
|
|
93
94
|
},
|
|
94
95
|
fireConfetti() {
|
|
95
|
-
|
|
96
|
+
var self = this;
|
|
97
|
+
if (self._confettiTimer) clearTimeout(self._confettiTimer);
|
|
98
|
+
self._confettiTimer = setTimeout(function() {
|
|
99
|
+
self._confettiTimer = null;
|
|
96
100
|
try { if (window.fireSuccessConfetti) window.fireSuccessConfetti(); } catch (_) {}
|
|
97
101
|
}, 100);
|
|
102
|
+
},
|
|
103
|
+
destroy() {
|
|
104
|
+
if (this._redirectTimer) { clearInterval(this._redirectTimer); this._redirectTimer = null; }
|
|
105
|
+
if (this._confettiTimer) { clearTimeout(this._confettiTimer); this._confettiTimer = null; }
|
|
98
106
|
}
|
|
99
107
|
}".gsub(/\s+/, ' ').html_safe
|
|
100
108
|
|
data/lib/studio/version.rb
CHANGED