stimeo-ui 0.9.0 → 0.11.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 +4 -4
- data/CHANGELOG.md +149 -0
- data/dist/cable/index.js +226 -43
- data/dist/controllers/alert_dialog_controller.js +17 -7
- data/dist/controllers/announcer_controller.js +1 -1
- data/dist/controllers/bulk_select_controller.js +2 -2
- data/dist/controllers/collapsible_controller.js +1 -1
- data/dist/controllers/command_palette_controller.js +15 -5
- data/dist/controllers/confirm_controller.js +15 -5
- data/dist/controllers/countdown_controller.js +1 -1
- data/dist/controllers/data_grid_controller.js +1 -1
- data/dist/controllers/dialog_controller.js +15 -5
- data/dist/controllers/direct_upload_controller.js +1 -5
- data/dist/controllers/drawer_controller.js +17 -7
- data/dist/controllers/focus_controller.js +45 -7
- data/dist/controllers/form_field_controller.js +1 -1
- data/dist/controllers/menubar_controller.js +2 -2
- data/dist/controllers/multi_select_controller.js +1 -1
- data/dist/controllers/overflow_menu_controller.js +2 -2
- data/dist/controllers/password_reveal_controller.js +102 -3
- data/dist/controllers/password_strength_controller.js +287 -42
- data/dist/controllers/pointer_drag_controller.js +64 -18
- data/dist/controllers/portal_controller.js +32 -11
- data/dist/controllers/preview_guard_controller.js +180 -20
- data/dist/controllers/resizable_controller.js +1 -1
- data/dist/controllers/roving_controller.js +127 -14
- data/dist/controllers/scroll_restore_controller.js +127 -27
- data/dist/controllers/scroll_visibility_controller.js +167 -14
- data/dist/controllers/sidebar_controller.js +15 -5
- data/dist/controllers/step_indicator_controller.js +0 -5
- data/dist/controllers/stick_to_bottom_controller.js +149 -28
- data/dist/controllers/theme_controller.js +151 -40
- data/dist/controllers/toast_controller.js +2 -2
- data/dist/controllers/transition_controller.js +79 -15
- data/dist/index.js +1188 -318
- data/dist/positioning/index.js +84 -25
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -380,15 +380,25 @@ var FocusTrap = class {
|
|
|
380
380
|
* content cannot be focused or reached by assistive technology, honoring the
|
|
381
381
|
* `aria-modal="true"` contract. An element that was *already* `inert` is left
|
|
382
382
|
* untracked so `#releaseBackground` does not wrongly clear it.
|
|
383
|
+
*
|
|
384
|
+
* The walk climbs from the container to `body` and inerts each ancestor's other
|
|
385
|
+
* children. Scanning only `body`'s children would skip the branch the container
|
|
386
|
+
* sits in — everything beside it inside that branch is background too, and a
|
|
387
|
+
* nested container is the ordinary case.
|
|
383
388
|
*/
|
|
384
389
|
#isolateBackground() {
|
|
385
390
|
const container = this.#getContainer();
|
|
386
391
|
this.#inertedSiblings = [];
|
|
387
|
-
for (
|
|
388
|
-
|
|
389
|
-
if (
|
|
390
|
-
sibling.
|
|
391
|
-
|
|
392
|
+
for (let node = container; node !== document.body; ) {
|
|
393
|
+
const parent = node.parentElement;
|
|
394
|
+
if (!parent) break;
|
|
395
|
+
for (const sibling of Array.from(parent.children)) {
|
|
396
|
+
if (!(sibling instanceof HTMLElement)) continue;
|
|
397
|
+
if (sibling === node || sibling.inert) continue;
|
|
398
|
+
sibling.inert = true;
|
|
399
|
+
this.#inertedSiblings.push(sibling);
|
|
400
|
+
}
|
|
401
|
+
node = parent;
|
|
392
402
|
}
|
|
393
403
|
}
|
|
394
404
|
/** Reverts the `inert` flags applied by `#isolateBackground`. */
|
|
@@ -315,15 +315,25 @@ var FocusTrap = class {
|
|
|
315
315
|
* content cannot be focused or reached by assistive technology, honoring the
|
|
316
316
|
* `aria-modal="true"` contract. An element that was *already* `inert` is left
|
|
317
317
|
* untracked so `#releaseBackground` does not wrongly clear it.
|
|
318
|
+
*
|
|
319
|
+
* The walk climbs from the container to `body` and inerts each ancestor's other
|
|
320
|
+
* children. Scanning only `body`'s children would skip the branch the container
|
|
321
|
+
* sits in — everything beside it inside that branch is background too, and a
|
|
322
|
+
* nested container is the ordinary case.
|
|
318
323
|
*/
|
|
319
324
|
#isolateBackground() {
|
|
320
325
|
const container = this.#getContainer();
|
|
321
326
|
this.#inertedSiblings = [];
|
|
322
|
-
for (
|
|
323
|
-
|
|
324
|
-
if (
|
|
325
|
-
sibling.
|
|
326
|
-
|
|
327
|
+
for (let node = container; node !== document.body; ) {
|
|
328
|
+
const parent = node.parentElement;
|
|
329
|
+
if (!parent) break;
|
|
330
|
+
for (const sibling of Array.from(parent.children)) {
|
|
331
|
+
if (!(sibling instanceof HTMLElement)) continue;
|
|
332
|
+
if (sibling === node || sibling.inert) continue;
|
|
333
|
+
sibling.inert = true;
|
|
334
|
+
this.#inertedSiblings.push(sibling);
|
|
335
|
+
}
|
|
336
|
+
node = parent;
|
|
327
337
|
}
|
|
328
338
|
}
|
|
329
339
|
/** Reverts the `inert` flags applied by `#isolateBackground`. */
|
|
@@ -121,7 +121,7 @@ var CountdownController = class extends Controller {
|
|
|
121
121
|
#pausedAmount = 0;
|
|
122
122
|
/**
|
|
123
123
|
* The amount the slots are currently showing, floored to the second they render.
|
|
124
|
-
* It lags {@link currentAmount} by up to one tick, and it — not the live reading —
|
|
124
|
+
* It lags {@link #currentAmount} by up to one tick, and it — not the live reading —
|
|
125
125
|
* is what a pause has to preserve: storing the fraction behind the display instead
|
|
126
126
|
* makes the first tick after a resume step by two units.
|
|
127
127
|
*/
|
|
@@ -298,7 +298,7 @@ var DataGridController = class extends Controller {
|
|
|
298
298
|
const row = cell.closest("[role='row']");
|
|
299
299
|
if (row && this.rowTargets.includes(row)) this.#toggleRow(row);
|
|
300
300
|
}
|
|
301
|
-
/**
|
|
301
|
+
/** Cycles a header's sort on keyboard activation and emits `sort`. */
|
|
302
302
|
#cycleSort(header) {
|
|
303
303
|
const direction = nextSortDirection(header.getAttribute("aria-sort") ?? "none");
|
|
304
304
|
for (const other of this.columnHeaderTargets) {
|
|
@@ -315,15 +315,25 @@ var FocusTrap = class {
|
|
|
315
315
|
* content cannot be focused or reached by assistive technology, honoring the
|
|
316
316
|
* `aria-modal="true"` contract. An element that was *already* `inert` is left
|
|
317
317
|
* untracked so `#releaseBackground` does not wrongly clear it.
|
|
318
|
+
*
|
|
319
|
+
* The walk climbs from the container to `body` and inerts each ancestor's other
|
|
320
|
+
* children. Scanning only `body`'s children would skip the branch the container
|
|
321
|
+
* sits in — everything beside it inside that branch is background too, and a
|
|
322
|
+
* nested container is the ordinary case.
|
|
318
323
|
*/
|
|
319
324
|
#isolateBackground() {
|
|
320
325
|
const container = this.#getContainer();
|
|
321
326
|
this.#inertedSiblings = [];
|
|
322
|
-
for (
|
|
323
|
-
|
|
324
|
-
if (
|
|
325
|
-
sibling.
|
|
326
|
-
|
|
327
|
+
for (let node = container; node !== document.body; ) {
|
|
328
|
+
const parent = node.parentElement;
|
|
329
|
+
if (!parent) break;
|
|
330
|
+
for (const sibling of Array.from(parent.children)) {
|
|
331
|
+
if (!(sibling instanceof HTMLElement)) continue;
|
|
332
|
+
if (sibling === node || sibling.inert) continue;
|
|
333
|
+
sibling.inert = true;
|
|
334
|
+
this.#inertedSiblings.push(sibling);
|
|
335
|
+
}
|
|
336
|
+
node = parent;
|
|
327
337
|
}
|
|
328
338
|
}
|
|
329
339
|
/** Reverts the `inert` flags applied by `#isolateBackground`. */
|
|
@@ -310,10 +310,6 @@ var DirectUploadController = class extends Controller {
|
|
|
310
310
|
this.#rows.delete(id);
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
|
-
/**
|
|
314
|
-
* Returns the widget to its pre-upload state just before Turbo caches the
|
|
315
|
-
* page, so the snapshot never replays rows for uploads that cannot resume.
|
|
316
|
-
*/
|
|
317
313
|
/**
|
|
318
314
|
* Rewinds for the snapshot and reports what that discarded. An upload in flight
|
|
319
315
|
* cannot survive the navigation, so a consumer mirroring the rows would keep a
|
|
@@ -352,7 +348,7 @@ var DirectUploadController = class extends Controller {
|
|
|
352
348
|
#detail(event) {
|
|
353
349
|
return event.detail ?? {};
|
|
354
350
|
}
|
|
355
|
-
/** The file name
|
|
351
|
+
/** The event's file name, or `""` when it carries none. */
|
|
356
352
|
#name(detail) {
|
|
357
353
|
return detail.file?.name ?? "";
|
|
358
354
|
}
|
|
@@ -315,15 +315,25 @@ var FocusTrap = class {
|
|
|
315
315
|
* content cannot be focused or reached by assistive technology, honoring the
|
|
316
316
|
* `aria-modal="true"` contract. An element that was *already* `inert` is left
|
|
317
317
|
* untracked so `#releaseBackground` does not wrongly clear it.
|
|
318
|
+
*
|
|
319
|
+
* The walk climbs from the container to `body` and inerts each ancestor's other
|
|
320
|
+
* children. Scanning only `body`'s children would skip the branch the container
|
|
321
|
+
* sits in — everything beside it inside that branch is background too, and a
|
|
322
|
+
* nested container is the ordinary case.
|
|
318
323
|
*/
|
|
319
324
|
#isolateBackground() {
|
|
320
325
|
const container = this.#getContainer();
|
|
321
326
|
this.#inertedSiblings = [];
|
|
322
|
-
for (
|
|
323
|
-
|
|
324
|
-
if (
|
|
325
|
-
sibling.
|
|
326
|
-
|
|
327
|
+
for (let node = container; node !== document.body; ) {
|
|
328
|
+
const parent = node.parentElement;
|
|
329
|
+
if (!parent) break;
|
|
330
|
+
for (const sibling of Array.from(parent.children)) {
|
|
331
|
+
if (!(sibling instanceof HTMLElement)) continue;
|
|
332
|
+
if (sibling === node || sibling.inert) continue;
|
|
333
|
+
sibling.inert = true;
|
|
334
|
+
this.#inertedSiblings.push(sibling);
|
|
335
|
+
}
|
|
336
|
+
node = parent;
|
|
327
337
|
}
|
|
328
338
|
}
|
|
329
339
|
/** Reverts the `inert` flags applied by `#isolateBackground`. */
|
|
@@ -576,8 +586,8 @@ var DrawerController = class extends Controller {
|
|
|
576
586
|
* rather than being re-derived from the declarative `open` Value (which would
|
|
577
587
|
* close a user-opened drawer). The `open` Value only seeds a genuinely fresh
|
|
578
588
|
* render. We normalize to a clean closed baseline first so {@link open} runs its
|
|
579
|
-
* full reveal + trap activation — the {@link FocusTrap} is
|
|
580
|
-
*
|
|
589
|
+
* full reveal + trap activation — the {@link FocusTrap} is inactive after a
|
|
590
|
+
* disconnect and must be re-activated.
|
|
581
591
|
*/
|
|
582
592
|
connect() {
|
|
583
593
|
this.#connected = true;
|
|
@@ -315,15 +315,25 @@ var FocusTrap = class {
|
|
|
315
315
|
* content cannot be focused or reached by assistive technology, honoring the
|
|
316
316
|
* `aria-modal="true"` contract. An element that was *already* `inert` is left
|
|
317
317
|
* untracked so `#releaseBackground` does not wrongly clear it.
|
|
318
|
+
*
|
|
319
|
+
* The walk climbs from the container to `body` and inerts each ancestor's other
|
|
320
|
+
* children. Scanning only `body`'s children would skip the branch the container
|
|
321
|
+
* sits in — everything beside it inside that branch is background too, and a
|
|
322
|
+
* nested container is the ordinary case.
|
|
318
323
|
*/
|
|
319
324
|
#isolateBackground() {
|
|
320
325
|
const container = this.#getContainer();
|
|
321
326
|
this.#inertedSiblings = [];
|
|
322
|
-
for (
|
|
323
|
-
|
|
324
|
-
if (
|
|
325
|
-
sibling.
|
|
326
|
-
|
|
327
|
+
for (let node = container; node !== document.body; ) {
|
|
328
|
+
const parent = node.parentElement;
|
|
329
|
+
if (!parent) break;
|
|
330
|
+
for (const sibling of Array.from(parent.children)) {
|
|
331
|
+
if (!(sibling instanceof HTMLElement)) continue;
|
|
332
|
+
if (sibling === node || sibling.inert) continue;
|
|
333
|
+
sibling.inert = true;
|
|
334
|
+
this.#inertedSiblings.push(sibling);
|
|
335
|
+
}
|
|
336
|
+
node = parent;
|
|
327
337
|
}
|
|
328
338
|
}
|
|
329
339
|
/** Reverts the `inert` flags applied by `#isolateBackground`. */
|
|
@@ -375,13 +385,39 @@ var FocusController = class extends Controller {
|
|
|
375
385
|
initialFocus: () => this.hasInitialTarget ? this.initialTarget : null,
|
|
376
386
|
onEscape: () => this.deactivate()
|
|
377
387
|
});
|
|
388
|
+
/**
|
|
389
|
+
* Whether this scope is currently trapping.
|
|
390
|
+
*
|
|
391
|
+
* Held here rather than read back from the trap: the trap releases itself
|
|
392
|
+
* before Turbo caches the page, so its own flag stops answering for the state
|
|
393
|
+
* this controller publishes on the element and in its events.
|
|
394
|
+
*/
|
|
395
|
+
#active = false;
|
|
396
|
+
/**
|
|
397
|
+
* Returns the element to its untrapped form before Turbo copies the page. The
|
|
398
|
+
* trap performs its own release on the same event, so what is left is the hook
|
|
399
|
+
* this controller owns — carried into the snapshot it would describe a scope
|
|
400
|
+
* that is no longer trapping.
|
|
401
|
+
*
|
|
402
|
+
* Silent, like `disconnect()`: the page is about to be frozen, and a release
|
|
403
|
+
* nobody asked for is not a close to report.
|
|
404
|
+
*/
|
|
405
|
+
#beforeCache = new BeforeCacheReset(() => {
|
|
406
|
+
this.#active = false;
|
|
407
|
+
this.element.removeAttribute("data-focus-trapped");
|
|
408
|
+
});
|
|
378
409
|
/** Stimulus drives activation from the `trap` value (also fires on connect). */
|
|
379
410
|
trapValueChanged() {
|
|
380
411
|
if (this.trapValue) this.#activate();
|
|
381
412
|
else this.#deactivate();
|
|
382
413
|
}
|
|
414
|
+
connect() {
|
|
415
|
+
this.#beforeCache.activate();
|
|
416
|
+
}
|
|
383
417
|
disconnect() {
|
|
384
418
|
this.#trap.deactivate({ restoreFocus: false });
|
|
419
|
+
this.#beforeCache.deactivate();
|
|
420
|
+
this.#active = false;
|
|
385
421
|
this.element.removeAttribute("data-focus-trapped");
|
|
386
422
|
}
|
|
387
423
|
/** Turns the trap on. Acts synchronously and keeps the `trap` value in sync. */
|
|
@@ -395,13 +431,15 @@ var FocusController = class extends Controller {
|
|
|
395
431
|
this.#deactivate();
|
|
396
432
|
}
|
|
397
433
|
#activate() {
|
|
398
|
-
if (this.#
|
|
434
|
+
if (this.#active) return;
|
|
435
|
+
this.#active = true;
|
|
399
436
|
this.#trap.activate();
|
|
400
437
|
this.element.setAttribute("data-focus-trapped", "true");
|
|
401
438
|
this.dispatch("activate", { detail: {} });
|
|
402
439
|
}
|
|
403
440
|
#deactivate() {
|
|
404
|
-
if (!this.#
|
|
441
|
+
if (!this.#active) return;
|
|
442
|
+
this.#active = false;
|
|
405
443
|
this.#trap.deactivate({ restoreFocus: this.restoreValue });
|
|
406
444
|
this.element.removeAttribute("data-focus-trapped");
|
|
407
445
|
this.dispatch("deactivate", { detail: {} });
|
|
@@ -146,7 +146,7 @@ var FormFieldController = class _FormFieldController extends Controller {
|
|
|
146
146
|
static #INVALID_ATTR = "data-stimeo--form-field-invalid";
|
|
147
147
|
/** Collapses one target/morph batch into one silent ARIA reconciliation. */
|
|
148
148
|
#reconcile = new MicrotaskCoalescer(() => this.#reconcileDom());
|
|
149
|
-
/**
|
|
149
|
+
/** Returns borrowed control ARIA before Turbo snapshots the page. */
|
|
150
150
|
#beforeCache = new BeforeCacheReset(() => this.#rewindForCache());
|
|
151
151
|
#ariaDescribedBy = new AttributeLease("aria-describedby");
|
|
152
152
|
#ariaErrorMessage = new AttributeLease("aria-errormessage");
|
|
@@ -629,8 +629,8 @@ var MenubarController = class extends Controller {
|
|
|
629
629
|
* being hidden itself, a menu can stay visible after its owning top was removed,
|
|
630
630
|
* an open pair can appear from a morph with no layer registered for it, and the
|
|
631
631
|
* Tab stop can end up on a now-inert top, on a runtime-added one, or on none at
|
|
632
|
-
* all.
|
|
633
|
-
* this layer is on
|
|
632
|
+
* all. The only state carried between calls is the tracked focus record and which side
|
|
633
|
+
* of the Escape stack this layer is on (which the stack itself does not expose), so the outcome is the
|
|
634
634
|
* same whichever mutation arrived and calling it more often than needed is free.
|
|
635
635
|
*/
|
|
636
636
|
#reconcile() {
|
|
@@ -1048,7 +1048,7 @@ var MultiSelectController = class extends Controller {
|
|
|
1048
1048
|
get #values() {
|
|
1049
1049
|
return this.#selectedOptions.map((option) => this.#optionValue(option));
|
|
1050
1050
|
}
|
|
1051
|
-
/** Normalized cardinality cap: zero
|
|
1051
|
+
/** Normalized cardinality cap: zero and below are unlimited; a positive value floors, never below 1. */
|
|
1052
1052
|
get #selectionLimit() {
|
|
1053
1053
|
if (!Number.isFinite(this.maxValue) || this.maxValue <= 0) return 0;
|
|
1054
1054
|
return Math.max(1, Math.floor(this.maxValue));
|
|
@@ -382,8 +382,8 @@ var OverflowMenuController = class extends Controller {
|
|
|
382
382
|
*
|
|
383
383
|
* With no known child at all (a fresh instance connecting to markup that already
|
|
384
384
|
* holds banked items), a fully-banked snapshot's inert boundary preserves whether an
|
|
385
|
-
* unindexed run was prepended or appended before connect.
|
|
386
|
-
*
|
|
385
|
+
* unindexed run was prepended or appended before connect. Markup that holds banked items but no boundary —
|
|
386
|
+
* server-rendered or hand-authored — uses the saved index as the
|
|
387
387
|
* offset, the inverse of the move that banked it.
|
|
388
388
|
*/
|
|
389
389
|
#merge(bar, banked, boundaryAt) {
|
|
@@ -2,6 +2,35 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/password_reveal_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/before_cache_reset.ts
|
|
6
|
+
var BeforeCacheReset = class _BeforeCacheReset {
|
|
7
|
+
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
8
|
+
static #subscribers = /* @__PURE__ */ new Set();
|
|
9
|
+
/** The shared listener; installed while at least one instance is subscribed. */
|
|
10
|
+
static #onBeforeCache = () => {
|
|
11
|
+
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
12
|
+
};
|
|
13
|
+
#rewind;
|
|
14
|
+
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
15
|
+
constructor(rewind) {
|
|
16
|
+
this.#rewind = rewind;
|
|
17
|
+
}
|
|
18
|
+
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
19
|
+
activate() {
|
|
20
|
+
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
21
|
+
_BeforeCacheReset.#subscribers.add(this);
|
|
22
|
+
if (first) {
|
|
23
|
+
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
27
|
+
deactivate() {
|
|
28
|
+
_BeforeCacheReset.#subscribers.delete(this);
|
|
29
|
+
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
30
|
+
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
5
34
|
// src/utils/safe_timeout.ts
|
|
6
35
|
var TimerRegistry = class {
|
|
7
36
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -56,6 +85,7 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
56
85
|
};
|
|
57
86
|
|
|
58
87
|
// src/controllers/password_reveal_controller.ts
|
|
88
|
+
var MAX_DELAY = 2 ** 31 - 1;
|
|
59
89
|
var PasswordRevealController = class extends Controller {
|
|
60
90
|
static targets = ["input", "toggle"];
|
|
61
91
|
static values = {
|
|
@@ -65,11 +95,54 @@ var PasswordRevealController = class extends Controller {
|
|
|
65
95
|
static events = ["toggle"];
|
|
66
96
|
/** Auto re-mask timer; torn down on disconnect. */
|
|
67
97
|
#timers = new SafeTimeout();
|
|
98
|
+
/**
|
|
99
|
+
* Whether this controller is between `connect()` and `disconnect()`.
|
|
100
|
+
*
|
|
101
|
+
* Target callbacks outlive the controller: Stimulus stops the target observer
|
|
102
|
+
* after `disconnect()`, so a field leaving after teardown still reaches
|
|
103
|
+
* {@link PasswordRevealController.inputTargetDisconnected}. Arming from there
|
|
104
|
+
* would put a timer back that nothing will clear. The element staying in the
|
|
105
|
+
* document does not answer this — unloading the controller leaves it there.
|
|
106
|
+
*/
|
|
107
|
+
#connected = false;
|
|
108
|
+
/** Masks the field before Turbo copies the page into its snapshot. */
|
|
109
|
+
#beforeCache = new BeforeCacheReset(() => this.#rewindToMasked());
|
|
68
110
|
connect() {
|
|
69
|
-
this.#
|
|
111
|
+
this.#connected = true;
|
|
112
|
+
const visible = this.#isVisible;
|
|
113
|
+
this.#reflect(visible);
|
|
114
|
+
this.#beforeCache.activate();
|
|
115
|
+
this.#arm(visible);
|
|
70
116
|
}
|
|
71
117
|
disconnect() {
|
|
118
|
+
this.#connected = false;
|
|
72
119
|
this.#timers.clearAll();
|
|
120
|
+
this.#beforeCache.deactivate();
|
|
121
|
+
}
|
|
122
|
+
/** Re-derives the hooks and the re-mask for a field swapped in after connect. */
|
|
123
|
+
inputTargetConnected() {
|
|
124
|
+
const visible = this.#connected && this.#isVisible;
|
|
125
|
+
this.#reflect(visible);
|
|
126
|
+
this.#arm(visible);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Re-derives from whatever field is left rather than assuming none is. A swap
|
|
130
|
+
* delivers this callback next to the arrival in either order, so a revealed
|
|
131
|
+
* replacement that answered "masked" here would be described as hidden while
|
|
132
|
+
* showing the password, and would carry no re-mask.
|
|
133
|
+
*
|
|
134
|
+
* Teardown is the one case with nothing to derive: target callbacks run after
|
|
135
|
+
* `disconnect()`, so the field is still revealed and re-arming from it would
|
|
136
|
+
* outlive the controller. A detached root is the signal to stand down.
|
|
137
|
+
*/
|
|
138
|
+
inputTargetDisconnected() {
|
|
139
|
+
const visible = this.#connected && this.element.isConnected && this.#isVisible;
|
|
140
|
+
this.#reflect(visible);
|
|
141
|
+
this.#arm(visible);
|
|
142
|
+
}
|
|
143
|
+
/** Re-derives the pressed state for a button swapped in after connect. */
|
|
144
|
+
toggleTargetConnected() {
|
|
145
|
+
this.#reflect(this.#isVisible);
|
|
73
146
|
}
|
|
74
147
|
/** Toggles the input between masked and revealed. Bound via `data-action`. */
|
|
75
148
|
toggle() {
|
|
@@ -98,11 +171,37 @@ var PasswordRevealController = class extends Controller {
|
|
|
98
171
|
}
|
|
99
172
|
this.#reflect(visible);
|
|
100
173
|
this.dispatch("toggle", { detail: { visible } });
|
|
174
|
+
this.#arm(visible);
|
|
175
|
+
}
|
|
176
|
+
/** Schedules the auto re-mask for a revealed field, replacing any pending one. */
|
|
177
|
+
#arm(visible) {
|
|
101
178
|
this.#timers.clearAll();
|
|
102
|
-
|
|
103
|
-
|
|
179
|
+
const delay = this.#autoHideDelay;
|
|
180
|
+
if (visible && delay > 0) {
|
|
181
|
+
this.#timers.set(() => this.#setVisible(false), delay);
|
|
104
182
|
}
|
|
105
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* The auto re-mask delay, held to what `setTimeout` can carry. Past that limit
|
|
186
|
+
* a delay folds to zero, turning "keep it showing" into "hide it at once" —
|
|
187
|
+
* the opposite of what the declaration asked for. A value that is no delay at
|
|
188
|
+
* all stays out of the positive range {@link PasswordRevealController.#arm}
|
|
189
|
+
* requires, so it schedules nothing.
|
|
190
|
+
*/
|
|
191
|
+
get #autoHideDelay() {
|
|
192
|
+
return Math.min(this.autoHideValue, MAX_DELAY);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Returns the field to masked before Turbo copies the page. Silent and
|
|
196
|
+
* focus-free: the page is about to be frozen, so there is no one to tell and
|
|
197
|
+
* nowhere for focus to go.
|
|
198
|
+
*/
|
|
199
|
+
#rewindToMasked() {
|
|
200
|
+
this.#timers.clearAll();
|
|
201
|
+
if (!this.hasInputTarget) return;
|
|
202
|
+
this.inputTarget.type = "password";
|
|
203
|
+
this.#reflect(false);
|
|
204
|
+
}
|
|
106
205
|
/** Reflects the visible state onto `aria-pressed` and `data-state`. */
|
|
107
206
|
#reflect(visible) {
|
|
108
207
|
if (this.hasToggleTarget) {
|