@aurodesignsystem/auro-dialog 4.2.1 → 4.2.2
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.
- package/CHANGELOG.md +2 -3
- package/README.md +70 -82
- package/demo/accessibility.md +7 -2
- package/demo/accessibility.min.js +338 -48
- package/demo/api.md +2 -2
- package/demo/customize.min.js +338 -48
- package/demo/design.min.js +338 -48
- package/demo/index.md +1 -0
- package/demo/index.min.js +338 -48
- package/demo/readme.min.js +338 -48
- package/dist/{auro-dialog-T9I85UZL.js → auro-dialog-FFeXiEB1.js} +4 -4
- package/dist/index.d.ts +16 -8
- package/dist/index.js +1 -1
- package/dist/registered.js +1 -1
- package/package.json +2 -2
package/demo/design.min.js
CHANGED
|
@@ -2288,6 +2288,48 @@ const computePosition = (reference, floating, options) => {
|
|
|
2288
2288
|
});
|
|
2289
2289
|
};
|
|
2290
2290
|
|
|
2291
|
+
/**
|
|
2292
|
+
* Generates a v4 UUID with a three-tier fallback for insecure contexts.
|
|
2293
|
+
*
|
|
2294
|
+
* Tier 1 (secure contexts): crypto.randomUUID() — native, fully random.
|
|
2295
|
+
* Tier 2 (insecure contexts): crypto.getRandomValues() — available on all
|
|
2296
|
+
* modern browsers regardless of secure-context status.
|
|
2297
|
+
* Tier 3 (last resort, no crypto API): new Date().getTime() — lower entropy,
|
|
2298
|
+
* but sufficient for element IDs where collision risk is negligible.
|
|
2299
|
+
*
|
|
2300
|
+
* See: https://w3c.github.io/webcrypto/#dom-crypto-randomuuid (secure-context restriction)
|
|
2301
|
+
*/
|
|
2302
|
+
function generateUUID() {
|
|
2303
|
+
let uuid;
|
|
2304
|
+
|
|
2305
|
+
if (
|
|
2306
|
+
typeof crypto !== "undefined" &&
|
|
2307
|
+
typeof crypto.randomUUID === "function"
|
|
2308
|
+
) {
|
|
2309
|
+
uuid = crypto.randomUUID();
|
|
2310
|
+
} else if (
|
|
2311
|
+
typeof crypto !== "undefined" &&
|
|
2312
|
+
typeof crypto.getRandomValues === "function"
|
|
2313
|
+
) {
|
|
2314
|
+
const bytes = new Uint8Array(16);
|
|
2315
|
+
crypto.getRandomValues(bytes);
|
|
2316
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
2317
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
2318
|
+
const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0"));
|
|
2319
|
+
uuid = `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
|
|
2320
|
+
} else {
|
|
2321
|
+
let d = new Date().getTime();
|
|
2322
|
+
uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
2323
|
+
const r = (d % 16) | 0;
|
|
2324
|
+
d = (d - r) / 16 || new Date().getTime();
|
|
2325
|
+
return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
|
|
2326
|
+
});
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
// CSS selectors require IDs starting with a letter; replace a leading digit with 'a'.
|
|
2330
|
+
return /^[0-9]/.test(uuid) ? `a${uuid.slice(1)}` : uuid;
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2291
2333
|
/* eslint-disable line-comment-position, no-inline-comments */
|
|
2292
2334
|
|
|
2293
2335
|
|
|
@@ -2575,30 +2617,61 @@ class AuroFloatingUI {
|
|
|
2575
2617
|
|
|
2576
2618
|
/**
|
|
2577
2619
|
* @private
|
|
2578
|
-
*
|
|
2579
|
-
*
|
|
2620
|
+
* Mirrors `aria-modal` onto the `<dialog>` inside the bib, tracking the page
|
|
2621
|
+
* scroll lock: an overlay that holds the page hides it from assistive tech.
|
|
2622
|
+
*
|
|
2623
|
+
* The removal is called only from inside lockScroll()'s ownership guard: run
|
|
2624
|
+
* outside it, it fired on every autoUpdate tick of every NON-owning instance,
|
|
2625
|
+
* and a repositioning dropdown stripped `aria-modal` off an open modal dialog
|
|
2626
|
+
* it had nothing to do with. The write is unguarded by design — see the call
|
|
2627
|
+
* site — but is skipped when the attribute already reads correctly, so the
|
|
2628
|
+
* repeat costs no mutation record.
|
|
2629
|
+
* @param {Boolean} modal - True to mark the dialog modal; false to unmark it.
|
|
2580
2630
|
*/
|
|
2581
|
-
|
|
2631
|
+
mirrorAriaModal(modal) {
|
|
2582
2632
|
const element = this.element;
|
|
2633
|
+
const dialog = element?.bib
|
|
2634
|
+
? (element.bib?.shadowRoot || element.bib || element).querySelector(
|
|
2635
|
+
"dialog",
|
|
2636
|
+
)
|
|
2637
|
+
: undefined;
|
|
2583
2638
|
|
|
2584
|
-
if (!
|
|
2639
|
+
if (!dialog) {
|
|
2585
2640
|
return;
|
|
2586
2641
|
}
|
|
2587
2642
|
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
element.bib ||
|
|
2591
|
-
element
|
|
2592
|
-
).querySelector("dialog");
|
|
2593
|
-
if (dialog) {
|
|
2594
|
-
if (lock) {
|
|
2643
|
+
if (modal) {
|
|
2644
|
+
if (dialog.getAttribute("aria-modal") !== "true") {
|
|
2595
2645
|
dialog.setAttribute("aria-modal", "true");
|
|
2596
|
-
} else {
|
|
2597
|
-
dialog.removeAttribute("aria-modal");
|
|
2598
2646
|
}
|
|
2647
|
+
} else {
|
|
2648
|
+
dialog.removeAttribute("aria-modal");
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
/**
|
|
2653
|
+
* @private
|
|
2654
|
+
* Controls whether to lock the scrolling for the document's body.
|
|
2655
|
+
* @param {Boolean} lock - If true, locks the body's scrolling functionlity; otherwise, unlock.
|
|
2656
|
+
*/
|
|
2657
|
+
lockScroll(lock = true) {
|
|
2658
|
+
const element = this.element;
|
|
2659
|
+
|
|
2660
|
+
// Locking needs a bib to lock around; unlocking must not, or a bib that has
|
|
2661
|
+
// already been detached would leave the page frozen with no way back.
|
|
2662
|
+
if (lock && !element?.bib) {
|
|
2663
|
+
return;
|
|
2599
2664
|
}
|
|
2600
2665
|
|
|
2601
2666
|
if (lock) {
|
|
2667
|
+
// Re-asserted on every lock call rather than only on the unlocked ->
|
|
2668
|
+
// locked transition. If the bib's inner <dialog> is replaced while this
|
|
2669
|
+
// instance still holds the lock, the replacement would otherwise go
|
|
2670
|
+
// unmarked until a full unlock/lock cycle. Safe to repeat: it targets
|
|
2671
|
+
// this instance's own element, so it cannot reach another overlay's
|
|
2672
|
+
// dialog, and mirrorAriaModal() skips the write when already correct.
|
|
2673
|
+
this.mirrorAriaModal(true);
|
|
2674
|
+
|
|
2602
2675
|
if (!this._scrollLocked) {
|
|
2603
2676
|
this._scrollLocked = true;
|
|
2604
2677
|
this._savedScrollY = window.scrollY;
|
|
@@ -2656,6 +2729,8 @@ class AuroFloatingUI {
|
|
|
2656
2729
|
}
|
|
2657
2730
|
} else {
|
|
2658
2731
|
if (this._scrollLocked) {
|
|
2732
|
+
this.mirrorAriaModal(false);
|
|
2733
|
+
|
|
2659
2734
|
if (this._viewportHandler && window.visualViewport) {
|
|
2660
2735
|
window.visualViewport.removeEventListener(
|
|
2661
2736
|
"resize",
|
|
@@ -2768,15 +2843,47 @@ class AuroFloatingUI {
|
|
|
2768
2843
|
} else if (this.configureTrial < MAX_CONFIGURATION_COUNT) {
|
|
2769
2844
|
this.configureTrial += 1;
|
|
2770
2845
|
|
|
2771
|
-
|
|
2846
|
+
// The id is retained so disconnect() can cancel it. A retry that fires
|
|
2847
|
+
// after teardown re-enters this method with isPopoverVisible still true
|
|
2848
|
+
// — nothing clears it on the teardown path — and takes the page scroll
|
|
2849
|
+
// lock back, with no owner left to release it.
|
|
2850
|
+
//
|
|
2851
|
+
// This is not the rare timing race it reads as: the lookup above wants
|
|
2852
|
+
// a `.container` inside the BIB's own shadow root, which only a bib
|
|
2853
|
+
// that is itself a custom element has. A consumer whose bib is a plain
|
|
2854
|
+
// element (auro-dialog's `#bib` is a div, with its `.container` a
|
|
2855
|
+
// sibling in the host's shadow root) can never satisfy it, so it
|
|
2856
|
+
// retries the full MAX_CONFIGURATION_COUNT every time. Correcting the
|
|
2857
|
+
// lookup would change bib sizing for those consumers and is tracked
|
|
2858
|
+
// separately; cancelling the timer is what keeps it from freezing the
|
|
2859
|
+
// page.
|
|
2860
|
+
this._configureRetryId = setTimeout(() => {
|
|
2861
|
+
this._configureRetryId = undefined;
|
|
2772
2862
|
this.configureBibStrategy(value);
|
|
2773
2863
|
}, 0);
|
|
2774
2864
|
}
|
|
2775
2865
|
|
|
2776
2866
|
if (element.isPopoverVisible) {
|
|
2777
|
-
|
|
2867
|
+
// Positioning strategy is not scroll ownership. Both "fullscreen" and
|
|
2868
|
+
// "dialog" lay the bib over the page and trap focus inside it, so both
|
|
2869
|
+
// must freeze the page behind them (AB#1647843). Gating this on
|
|
2870
|
+
// "fullscreen" alone left dismissible desktop dialogs and drawers
|
|
2871
|
+
// scrollable, and actively released the lock on every reposition
|
|
2872
|
+
// (AB#1625424, AB#1625435). The nested "cover" strategy takes the else
|
|
2873
|
+
// branch and stays scrollable by design — it sits inside its parent
|
|
2874
|
+
// rather than over the page.
|
|
2875
|
+
this.lockScroll(true);
|
|
2778
2876
|
}
|
|
2779
2877
|
} else {
|
|
2878
|
+
// The mirror of the lock above: a strategy that does not own the page must
|
|
2879
|
+
// hand the lock back, not merely decline to take it. autoUpdate re-runs
|
|
2880
|
+
// this method on every resize tick, and getPositioningStrategy() flips
|
|
2881
|
+
// "fullscreen" -> "floating" across the breakpoint for dropdown behavior —
|
|
2882
|
+
// so an overlay that locked while narrow can land here while still open.
|
|
2883
|
+
// Without this, the page stays frozen behind a small floating bib until
|
|
2884
|
+
// hideBib() happens to run.
|
|
2885
|
+
this.lockScroll(false);
|
|
2886
|
+
|
|
2780
2887
|
element.bib.style.position = "";
|
|
2781
2888
|
element.bib.removeAttribute("isfullscreen");
|
|
2782
2889
|
element.isBibFullscreen = false;
|
|
@@ -2886,6 +2993,46 @@ class AuroFloatingUI {
|
|
|
2886
2993
|
this.hideBib("focusloss");
|
|
2887
2994
|
}
|
|
2888
2995
|
|
|
2996
|
+
/**
|
|
2997
|
+
* @private
|
|
2998
|
+
* Installs the handlers that dismiss an open overlay.
|
|
2999
|
+
*
|
|
3000
|
+
* Shared by showBib(), which opens the overlay, and configure(), which has to
|
|
3001
|
+
* put these back after a rewire tore them down under an overlay that is still
|
|
3002
|
+
* open. A non-modal overlay dismisses on outside click, focus loss and
|
|
3003
|
+
* Escape; a modal one instead swallows Escape so the native dialog's
|
|
3004
|
+
* CloseWatcher never sees it (AB#1613688).
|
|
3005
|
+
*/
|
|
3006
|
+
setupDismissHandlers() {
|
|
3007
|
+
const element = this.element;
|
|
3008
|
+
if (!element) {
|
|
3009
|
+
return;
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
if (!element.modal) {
|
|
3013
|
+
this.setupHideHandlers();
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
if (this.keyDownHandler) {
|
|
3018
|
+
document.removeEventListener("keydown", this.keyDownHandler);
|
|
3019
|
+
}
|
|
3020
|
+
this.keyDownHandler = (evt) => {
|
|
3021
|
+
if (evt.key === "Escape" && element.isPopoverVisible) {
|
|
3022
|
+
// Intercept at keydown so CloseWatcher never sees the keystroke.
|
|
3023
|
+
// Canceling `cancel` alone is insufficient — a second Esc with no
|
|
3024
|
+
// intervening user activation triggers the anti-trap and fires `close`
|
|
3025
|
+
// directly, bypassing any `cancel` preventDefault (AB#1613688).
|
|
3026
|
+
// stopImmediatePropagation is intentional: it prevents other document
|
|
3027
|
+
// keydown handlers (e.g. consumer code, auro-dialog) from also acting
|
|
3028
|
+
// on Escape while the modal owns the key.
|
|
3029
|
+
evt.preventDefault();
|
|
3030
|
+
evt.stopImmediatePropagation();
|
|
3031
|
+
}
|
|
3032
|
+
};
|
|
3033
|
+
document.addEventListener("keydown", this.keyDownHandler);
|
|
3034
|
+
}
|
|
3035
|
+
|
|
2889
3036
|
setupHideHandlers() {
|
|
2890
3037
|
const element = this.element;
|
|
2891
3038
|
if (!element) {
|
|
@@ -3058,27 +3205,7 @@ class AuroFloatingUI {
|
|
|
3058
3205
|
|
|
3059
3206
|
// prevent double showing: isPopovervisible gets first and showBib gets called later
|
|
3060
3207
|
if (!this.showing) {
|
|
3061
|
-
|
|
3062
|
-
this.setupHideHandlers();
|
|
3063
|
-
} else {
|
|
3064
|
-
if (this.keyDownHandler) {
|
|
3065
|
-
document.removeEventListener("keydown", this.keyDownHandler);
|
|
3066
|
-
}
|
|
3067
|
-
this.keyDownHandler = (evt) => {
|
|
3068
|
-
if (evt.key === "Escape" && element.isPopoverVisible) {
|
|
3069
|
-
// Intercept at keydown so CloseWatcher never sees the keystroke.
|
|
3070
|
-
// Canceling `cancel` alone is insufficient — a second Esc with no
|
|
3071
|
-
// intervening user activation triggers the anti-trap and fires `close`
|
|
3072
|
-
// directly, bypassing any `cancel` preventDefault (AB#1613688).
|
|
3073
|
-
// stopImmediatePropagation is intentional: it prevents other document
|
|
3074
|
-
// keydown handlers (e.g. consumer code, auro-dialog) from also acting
|
|
3075
|
-
// on Escape while the modal owns the key.
|
|
3076
|
-
evt.preventDefault();
|
|
3077
|
-
evt.stopImmediatePropagation();
|
|
3078
|
-
}
|
|
3079
|
-
};
|
|
3080
|
-
document.addEventListener("keydown", this.keyDownHandler);
|
|
3081
|
-
}
|
|
3208
|
+
this.setupDismissHandlers();
|
|
3082
3209
|
this.showing = true;
|
|
3083
3210
|
element.isPopoverVisible = true;
|
|
3084
3211
|
this.position();
|
|
@@ -3306,7 +3433,7 @@ class AuroFloatingUI {
|
|
|
3306
3433
|
|
|
3307
3434
|
this.id = element.getAttribute("id");
|
|
3308
3435
|
if (!this.id) {
|
|
3309
|
-
this.id =
|
|
3436
|
+
this.id = generateUUID();
|
|
3310
3437
|
element.setAttribute("id", this.id);
|
|
3311
3438
|
}
|
|
3312
3439
|
|
|
@@ -3318,7 +3445,35 @@ class AuroFloatingUI {
|
|
|
3318
3445
|
this.enableKeyboardHandling = enableKeyboardHandling;
|
|
3319
3446
|
|
|
3320
3447
|
this.eventPrefix = eventPrefix;
|
|
3321
|
-
|
|
3448
|
+
|
|
3449
|
+
// A retained scroll lock's saved body styles were captured for whichever
|
|
3450
|
+
// element held it, so only a same-element rewire may keep it. An actual
|
|
3451
|
+
// element swap has to take the full teardown, or this instance is left
|
|
3452
|
+
// holding a lock that no longer describes the page it points at — with no
|
|
3453
|
+
// owner able to release it (AB#1647843).
|
|
3454
|
+
const rewiringSameElement = this.element === elem;
|
|
3455
|
+
|
|
3456
|
+
if (!rewiringSameElement) {
|
|
3457
|
+
// Tear down against the outgoing element, before this.element is
|
|
3458
|
+
// repointed. Everything disconnect() releases is element-scoped:
|
|
3459
|
+
// aria-modal on that element's inner dialog, the bib transform saved for
|
|
3460
|
+
// it, its autoUpdate loop and its trigger listeners. Repointing first
|
|
3461
|
+
// aimed all of it at the incoming element — the outgoing dialog kept
|
|
3462
|
+
// aria-modal="true" for good, its bib stayed transformed, and the
|
|
3463
|
+
// incoming bib had the outgoing element's saved transform written onto
|
|
3464
|
+
// it. The trigger gate below read the incoming element's trigger, which
|
|
3465
|
+
// is not assigned until later in this method, so on a swap teardown was
|
|
3466
|
+
// usually skipped outright and the page stayed locked with no owner.
|
|
3467
|
+
if (this.element?.trigger) {
|
|
3468
|
+
this.disconnect({ teardown: true });
|
|
3469
|
+
}
|
|
3470
|
+
|
|
3471
|
+
// disconnect() does not clear this.showing, and the rewire block at the
|
|
3472
|
+
// end of this method only runs for a same-element rewire. Left true, the
|
|
3473
|
+
// incoming element's first showBib() is silently dropped by its
|
|
3474
|
+
// `!this.showing` guard and the overlay never opens.
|
|
3475
|
+
this.showing = false;
|
|
3476
|
+
|
|
3322
3477
|
this.element = elem;
|
|
3323
3478
|
}
|
|
3324
3479
|
|
|
@@ -3331,8 +3486,10 @@ class AuroFloatingUI {
|
|
|
3331
3486
|
this.behavior = element.behavior;
|
|
3332
3487
|
}
|
|
3333
3488
|
|
|
3334
|
-
if (element.trigger) {
|
|
3335
|
-
|
|
3489
|
+
if (rewiringSameElement && element.trigger) {
|
|
3490
|
+
// Rewiring, not tearing down — an overlay that is open right now keeps
|
|
3491
|
+
// its page scroll lock across a trigger change.
|
|
3492
|
+
this.disconnect({ teardown: false });
|
|
3336
3493
|
}
|
|
3337
3494
|
element.trigger =
|
|
3338
3495
|
element.triggerElement ||
|
|
@@ -3361,11 +3518,61 @@ class AuroFloatingUI {
|
|
|
3361
3518
|
element.trigger.addEventListener("focus", this.handleEvent);
|
|
3362
3519
|
element.trigger.addEventListener("blur", this.handleEvent);
|
|
3363
3520
|
}
|
|
3521
|
+
|
|
3522
|
+
// disconnect() above stripped the dismissal handlers and killed the
|
|
3523
|
+
// autoUpdate loop; the trigger listeners re-registered here replace
|
|
3524
|
+
// neither. An overlay that is open right now would otherwise be left on
|
|
3525
|
+
// screen with outside-click, focus-loss and Escape all dead and its
|
|
3526
|
+
// position frozen — and, since the lock is deliberately retained, holding
|
|
3527
|
+
// the page with only its original trigger able to give it back.
|
|
3528
|
+
if (rewiringSameElement && this.showing) {
|
|
3529
|
+
this.setupDismissHandlers();
|
|
3530
|
+
|
|
3531
|
+
if (element.bib && (element.trigger || element.parentNode)) {
|
|
3532
|
+
element.cleanup = autoUpdate(
|
|
3533
|
+
element.trigger || element.parentNode,
|
|
3534
|
+
element.bib,
|
|
3535
|
+
() => {
|
|
3536
|
+
this.position();
|
|
3537
|
+
},
|
|
3538
|
+
);
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3364
3541
|
}
|
|
3365
3542
|
|
|
3366
|
-
|
|
3543
|
+
/**
|
|
3544
|
+
* Tears the floater's connections down.
|
|
3545
|
+
*
|
|
3546
|
+
* Two callers with different intent share this: a real teardown, which must
|
|
3547
|
+
* release the page scroll lock, and configure(), which re-runs it purely to
|
|
3548
|
+
* rewire a changed trigger and must leave an open overlay's lock alone.
|
|
3549
|
+
* @param {Object} [options] - Teardown options.
|
|
3550
|
+
* @param {Boolean} [options.teardown] - False when only rewiring; keeps the
|
|
3551
|
+
* page scroll lock held for an overlay that is still open.
|
|
3552
|
+
* @returns {void}
|
|
3553
|
+
*/
|
|
3554
|
+
disconnect({ teardown = true } = {}) {
|
|
3367
3555
|
this.cleanupHideHandlers();
|
|
3368
3556
|
|
|
3557
|
+
// Cancel any pending configureBibStrategy() retry. It would otherwise fire
|
|
3558
|
+
// a macrotask after the unlock below, re-lock the page, and leave it frozen
|
|
3559
|
+
// with the floater already gone. Cancelled on a rewire too: configure()
|
|
3560
|
+
// re-runs position() itself, so a stale retry has nothing to contribute.
|
|
3561
|
+
if (this._configureRetryId !== undefined) {
|
|
3562
|
+
clearTimeout(this._configureRetryId);
|
|
3563
|
+
this._configureRetryId = undefined;
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
// Tearing down while the bib is still open would otherwise strand
|
|
3567
|
+
// body{position:fixed} and leave the page permanently unscrollable —
|
|
3568
|
+
// hideBib() is the only other unlock, and it never runs in this path.
|
|
3569
|
+
// Skipped when merely rewiring: configure() routes through here on every
|
|
3570
|
+
// triggerElement change, and releasing the lock there would unlock the page
|
|
3571
|
+
// behind an overlay that is still open (AB#1625424, AB#1625435).
|
|
3572
|
+
if (teardown) {
|
|
3573
|
+
this.lockScroll(false);
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3369
3576
|
const element = this.element;
|
|
3370
3577
|
if (!element) {
|
|
3371
3578
|
return;
|
|
@@ -3520,6 +3727,14 @@ class ComponentBase extends i$2 {
|
|
|
3520
3727
|
this.isBibFullscreen = false;
|
|
3521
3728
|
this.floater = new AuroFloatingUI(this, 'dialog');
|
|
3522
3729
|
|
|
3730
|
+
/**
|
|
3731
|
+
* @private
|
|
3732
|
+
* True once _teardown() has released the floater and nothing has rewired
|
|
3733
|
+
* it. Read by connectedCallback() to rewire a remounted dialog, and by
|
|
3734
|
+
* _teardown() to stay idempotent.
|
|
3735
|
+
*/
|
|
3736
|
+
this._floaterTornDown = false;
|
|
3737
|
+
|
|
3523
3738
|
const versioning = new AuroDependencyVersioning();
|
|
3524
3739
|
|
|
3525
3740
|
/**
|
|
@@ -3580,10 +3795,12 @@ class ComponentBase extends i$2 {
|
|
|
3580
3795
|
|
|
3581
3796
|
/**
|
|
3582
3797
|
* DEPRECATED - use `close-button-appearance="inverse" instead.
|
|
3798
|
+
* @deprecated Use `close-button-appearance="inverse"` instead.
|
|
3583
3799
|
*/
|
|
3584
3800
|
onDark: {
|
|
3585
3801
|
type: Boolean,
|
|
3586
|
-
reflect: true
|
|
3802
|
+
reflect: true,
|
|
3803
|
+
attribute: 'ondark'
|
|
3587
3804
|
},
|
|
3588
3805
|
|
|
3589
3806
|
/**
|
|
@@ -3601,7 +3818,8 @@ class ComponentBase extends i$2 {
|
|
|
3601
3818
|
*/
|
|
3602
3819
|
isBibFullscreen: {
|
|
3603
3820
|
type: Boolean,
|
|
3604
|
-
reflect: true
|
|
3821
|
+
reflect: true,
|
|
3822
|
+
attribute: 'isbibfullscreen'
|
|
3605
3823
|
},
|
|
3606
3824
|
|
|
3607
3825
|
/**
|
|
@@ -3670,7 +3888,7 @@ class ComponentBase extends i$2 {
|
|
|
3670
3888
|
slotWrapper.classList.remove("dialog-footer");
|
|
3671
3889
|
}
|
|
3672
3890
|
|
|
3673
|
-
this.
|
|
3891
|
+
this._configureFloater();
|
|
3674
3892
|
|
|
3675
3893
|
// Forward FloatingUI toggle event to backward-compatible 'toggle' event
|
|
3676
3894
|
this.addEventListener('auroDialog-toggled', (event) => {
|
|
@@ -3679,9 +3897,15 @@ class ComponentBase extends i$2 {
|
|
|
3679
3897
|
}
|
|
3680
3898
|
});
|
|
3681
3899
|
|
|
3682
|
-
//
|
|
3683
|
-
//
|
|
3684
|
-
//
|
|
3900
|
+
// Defense-in-depth for the native ESC/cancel event. In practice neither
|
|
3901
|
+
// branch fires today: auro-library's floatingUI binds its own document-level
|
|
3902
|
+
// keydown handler, which closes a non-modal dialog via hideBib("keydown")
|
|
3903
|
+
// and, for a modal, swallows the keystroke with preventDefault() +
|
|
3904
|
+
// stopImmediatePropagation() before the platform CloseWatcher ever sees it
|
|
3905
|
+
// (AB#1613688). This listener is kept rather than deleted because it is the
|
|
3906
|
+
// component-side guarantee that a modal never self-closes on ESC: if the
|
|
3907
|
+
// library stops swallowing, the platform `cancel` would otherwise close a
|
|
3908
|
+
// modal dialog, which is exactly the behavior `modal` exists to prevent.
|
|
3685
3909
|
this.dialog.addEventListener('cancel', (e) => {
|
|
3686
3910
|
e.preventDefault();
|
|
3687
3911
|
if (!this.modal) {
|
|
@@ -3728,12 +3952,78 @@ class ComponentBase extends i$2 {
|
|
|
3728
3952
|
}
|
|
3729
3953
|
|
|
3730
3954
|
if (changedProperties.has('triggerElement')) {
|
|
3731
|
-
this.
|
|
3955
|
+
this._configureFloater();
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
/**
|
|
3960
|
+
* @private
|
|
3961
|
+
* Wires the floater to this element and records that it is live, so a later
|
|
3962
|
+
* reconnect can tell a torn-down floater from one that never lost its wiring.
|
|
3963
|
+
* @returns {void}
|
|
3964
|
+
*/
|
|
3965
|
+
_configureFloater() {
|
|
3966
|
+
this._floaterTornDown = false;
|
|
3967
|
+
this.floater.configure(this, this.floaterConfig.prefix);
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
connectedCallback() {
|
|
3971
|
+
super.connectedCallback();
|
|
3972
|
+
|
|
3973
|
+
// Lit does not re-run firstUpdated() on reconnect, so without this a host
|
|
3974
|
+
// that removes and re-inserts the dialog across separate tasks — Vue
|
|
3975
|
+
// <keep-alive>, caching tab/accordion hosts, virtualized lists, or anything
|
|
3976
|
+
// that parks the node in a DocumentFragment in one task and inserts it in
|
|
3977
|
+
// the next — gets back a dead element: _teardown() dropped the trigger
|
|
3978
|
+
// listeners, ran element.cleanup(), tore down autoUpdate and released the
|
|
3979
|
+
// page scroll lock, and nothing rewired any of it.
|
|
3980
|
+
//
|
|
3981
|
+
// Only re-configure when a teardown actually ran. On the normal first
|
|
3982
|
+
// connect the flag is false and firstUpdated() owns the single configure
|
|
3983
|
+
// call; on a same-task DOM move the deferred teardown never runs, so the
|
|
3984
|
+
// flag stays false here too and the move is still a no-op.
|
|
3985
|
+
if (this.hasUpdated && this._floaterTornDown) {
|
|
3986
|
+
this._configureFloater();
|
|
3732
3987
|
}
|
|
3733
3988
|
}
|
|
3734
3989
|
|
|
3735
3990
|
disconnectedCallback() {
|
|
3736
3991
|
super.disconnectedCallback();
|
|
3992
|
+
|
|
3993
|
+
// A same-document DOM move (appendChild of an already-parented node) fires
|
|
3994
|
+
// disconnectedCallback and reconnects within the same task. Defer the
|
|
3995
|
+
// teardown one microtask and bail if the element came back, so a reparent
|
|
3996
|
+
// does not dismantle a dialog that is still open: floater.disconnect()
|
|
3997
|
+
// releases the page scroll lock and tears down autoUpdate, which would
|
|
3998
|
+
// leave the dialog visible over a scrollable page with nothing left to
|
|
3999
|
+
// re-lock it — the AB#1625424 symptom returning by a different route.
|
|
4000
|
+
//
|
|
4001
|
+
// A host that removes and re-inserts across tasks (not a move) is treated
|
|
4002
|
+
// as a real unmount, which is the correct reading of that sequence.
|
|
4003
|
+
queueMicrotask(() => {
|
|
4004
|
+
if (this.isConnected) {
|
|
4005
|
+
return;
|
|
4006
|
+
}
|
|
4007
|
+
this._teardown();
|
|
4008
|
+
});
|
|
4009
|
+
}
|
|
4010
|
+
|
|
4011
|
+
/**
|
|
4012
|
+
* @private
|
|
4013
|
+
* Releases everything the dialog owns outside its own shadow root.
|
|
4014
|
+
* Split out of disconnectedCallback so a DOM move can skip it.
|
|
4015
|
+
*
|
|
4016
|
+
* Idempotent: a repeated disconnect in the same task must not invoke
|
|
4017
|
+
* floater.disconnect() twice, and connectedCallback() reads the same flag to
|
|
4018
|
+
* decide whether the floater needs rewiring.
|
|
4019
|
+
* @returns {void}
|
|
4020
|
+
*/
|
|
4021
|
+
_teardown() {
|
|
4022
|
+
if (this._floaterTornDown) {
|
|
4023
|
+
return;
|
|
4024
|
+
}
|
|
4025
|
+
this._floaterTornDown = true;
|
|
4026
|
+
|
|
3737
4027
|
if (this._hidePopoverTimerId) {
|
|
3738
4028
|
clearTimeout(this._hidePopoverTimerId);
|
|
3739
4029
|
this._hidePopoverTimerId = undefined;
|
package/demo/index.md
CHANGED
|
@@ -93,6 +93,7 @@
|
|
|
93
93
|
<!-- AURO-GENERATED-CONTENT:END -->
|
|
94
94
|
<auro-header level="2" id="modalExample">Modal</auro-header>
|
|
95
95
|
<p>The <code>modal</code> attribute locks the user into interacting with the dialog. The close icon is not rendered and clicking outside the dialog will not dismiss it, so a <code>modal</code> dialog must include an explicit action button in the footer to dismiss it.</p>
|
|
96
|
+
<p>Freezing the page behind the dialog is not what <code>modal</code> controls — every dialog does that while it is open, and restores the scroll position on close. What <code>modal</code> changes is how the dialog can be dismissed. See the Accessibility page for details.</p>
|
|
96
97
|
<div class="exampleWrapper">
|
|
97
98
|
<!-- AURO-GENERATED-CONTENT:START (FILE:src=./../apiExamples/modal.html) -->
|
|
98
99
|
<!-- The below content is automatically added from ./../apiExamples/modal.html -->
|