@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.
@@ -2191,6 +2191,48 @@ const computePosition = (reference, floating, options) => {
2191
2191
  });
2192
2192
  };
2193
2193
 
2194
+ /**
2195
+ * Generates a v4 UUID with a three-tier fallback for insecure contexts.
2196
+ *
2197
+ * Tier 1 (secure contexts): crypto.randomUUID() — native, fully random.
2198
+ * Tier 2 (insecure contexts): crypto.getRandomValues() — available on all
2199
+ * modern browsers regardless of secure-context status.
2200
+ * Tier 3 (last resort, no crypto API): new Date().getTime() — lower entropy,
2201
+ * but sufficient for element IDs where collision risk is negligible.
2202
+ *
2203
+ * See: https://w3c.github.io/webcrypto/#dom-crypto-randomuuid (secure-context restriction)
2204
+ */
2205
+ function generateUUID() {
2206
+ let uuid;
2207
+
2208
+ if (
2209
+ typeof crypto !== "undefined" &&
2210
+ typeof crypto.randomUUID === "function"
2211
+ ) {
2212
+ uuid = crypto.randomUUID();
2213
+ } else if (
2214
+ typeof crypto !== "undefined" &&
2215
+ typeof crypto.getRandomValues === "function"
2216
+ ) {
2217
+ const bytes = new Uint8Array(16);
2218
+ crypto.getRandomValues(bytes);
2219
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
2220
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
2221
+ const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0"));
2222
+ 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("")}`;
2223
+ } else {
2224
+ let d = new Date().getTime();
2225
+ uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
2226
+ const r = (d % 16) | 0;
2227
+ d = (d - r) / 16 || new Date().getTime();
2228
+ return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
2229
+ });
2230
+ }
2231
+
2232
+ // CSS selectors require IDs starting with a letter; replace a leading digit with 'a'.
2233
+ return /^[0-9]/.test(uuid) ? `a${uuid.slice(1)}` : uuid;
2234
+ }
2235
+
2194
2236
  /* eslint-disable line-comment-position, no-inline-comments */
2195
2237
 
2196
2238
 
@@ -2478,30 +2520,61 @@ class AuroFloatingUI {
2478
2520
 
2479
2521
  /**
2480
2522
  * @private
2481
- * Controls whether to lock the scrolling for the document's body.
2482
- * @param {Boolean} lock - If true, locks the body's scrolling functionlity; otherwise, unlock.
2523
+ * Mirrors `aria-modal` onto the `<dialog>` inside the bib, tracking the page
2524
+ * scroll lock: an overlay that holds the page hides it from assistive tech.
2525
+ *
2526
+ * The removal is called only from inside lockScroll()'s ownership guard: run
2527
+ * outside it, it fired on every autoUpdate tick of every NON-owning instance,
2528
+ * and a repositioning dropdown stripped `aria-modal` off an open modal dialog
2529
+ * it had nothing to do with. The write is unguarded by design — see the call
2530
+ * site — but is skipped when the attribute already reads correctly, so the
2531
+ * repeat costs no mutation record.
2532
+ * @param {Boolean} modal - True to mark the dialog modal; false to unmark it.
2483
2533
  */
2484
- lockScroll(lock = true) {
2534
+ mirrorAriaModal(modal) {
2485
2535
  const element = this.element;
2536
+ const dialog = element?.bib
2537
+ ? (element.bib?.shadowRoot || element.bib || element).querySelector(
2538
+ "dialog",
2539
+ )
2540
+ : undefined;
2486
2541
 
2487
- if (!element?.bib) {
2542
+ if (!dialog) {
2488
2543
  return;
2489
2544
  }
2490
2545
 
2491
- const dialog = (
2492
- element.bib?.shadowRoot ||
2493
- element.bib ||
2494
- element
2495
- ).querySelector("dialog");
2496
- if (dialog) {
2497
- if (lock) {
2546
+ if (modal) {
2547
+ if (dialog.getAttribute("aria-modal") !== "true") {
2498
2548
  dialog.setAttribute("aria-modal", "true");
2499
- } else {
2500
- dialog.removeAttribute("aria-modal");
2501
2549
  }
2550
+ } else {
2551
+ dialog.removeAttribute("aria-modal");
2552
+ }
2553
+ }
2554
+
2555
+ /**
2556
+ * @private
2557
+ * Controls whether to lock the scrolling for the document's body.
2558
+ * @param {Boolean} lock - If true, locks the body's scrolling functionlity; otherwise, unlock.
2559
+ */
2560
+ lockScroll(lock = true) {
2561
+ const element = this.element;
2562
+
2563
+ // Locking needs a bib to lock around; unlocking must not, or a bib that has
2564
+ // already been detached would leave the page frozen with no way back.
2565
+ if (lock && !element?.bib) {
2566
+ return;
2502
2567
  }
2503
2568
 
2504
2569
  if (lock) {
2570
+ // Re-asserted on every lock call rather than only on the unlocked ->
2571
+ // locked transition. If the bib's inner <dialog> is replaced while this
2572
+ // instance still holds the lock, the replacement would otherwise go
2573
+ // unmarked until a full unlock/lock cycle. Safe to repeat: it targets
2574
+ // this instance's own element, so it cannot reach another overlay's
2575
+ // dialog, and mirrorAriaModal() skips the write when already correct.
2576
+ this.mirrorAriaModal(true);
2577
+
2505
2578
  if (!this._scrollLocked) {
2506
2579
  this._scrollLocked = true;
2507
2580
  this._savedScrollY = window.scrollY;
@@ -2559,6 +2632,8 @@ class AuroFloatingUI {
2559
2632
  }
2560
2633
  } else {
2561
2634
  if (this._scrollLocked) {
2635
+ this.mirrorAriaModal(false);
2636
+
2562
2637
  if (this._viewportHandler && window.visualViewport) {
2563
2638
  window.visualViewport.removeEventListener(
2564
2639
  "resize",
@@ -2671,15 +2746,47 @@ class AuroFloatingUI {
2671
2746
  } else if (this.configureTrial < MAX_CONFIGURATION_COUNT) {
2672
2747
  this.configureTrial += 1;
2673
2748
 
2674
- setTimeout(() => {
2749
+ // The id is retained so disconnect() can cancel it. A retry that fires
2750
+ // after teardown re-enters this method with isPopoverVisible still true
2751
+ // — nothing clears it on the teardown path — and takes the page scroll
2752
+ // lock back, with no owner left to release it.
2753
+ //
2754
+ // This is not the rare timing race it reads as: the lookup above wants
2755
+ // a `.container` inside the BIB's own shadow root, which only a bib
2756
+ // that is itself a custom element has. A consumer whose bib is a plain
2757
+ // element (auro-dialog's `#bib` is a div, with its `.container` a
2758
+ // sibling in the host's shadow root) can never satisfy it, so it
2759
+ // retries the full MAX_CONFIGURATION_COUNT every time. Correcting the
2760
+ // lookup would change bib sizing for those consumers and is tracked
2761
+ // separately; cancelling the timer is what keeps it from freezing the
2762
+ // page.
2763
+ this._configureRetryId = setTimeout(() => {
2764
+ this._configureRetryId = undefined;
2675
2765
  this.configureBibStrategy(value);
2676
2766
  }, 0);
2677
2767
  }
2678
2768
 
2679
2769
  if (element.isPopoverVisible) {
2680
- this.lockScroll(value === "fullscreen");
2770
+ // Positioning strategy is not scroll ownership. Both "fullscreen" and
2771
+ // "dialog" lay the bib over the page and trap focus inside it, so both
2772
+ // must freeze the page behind them (AB#1647843). Gating this on
2773
+ // "fullscreen" alone left dismissible desktop dialogs and drawers
2774
+ // scrollable, and actively released the lock on every reposition
2775
+ // (AB#1625424, AB#1625435). The nested "cover" strategy takes the else
2776
+ // branch and stays scrollable by design — it sits inside its parent
2777
+ // rather than over the page.
2778
+ this.lockScroll(true);
2681
2779
  }
2682
2780
  } else {
2781
+ // The mirror of the lock above: a strategy that does not own the page must
2782
+ // hand the lock back, not merely decline to take it. autoUpdate re-runs
2783
+ // this method on every resize tick, and getPositioningStrategy() flips
2784
+ // "fullscreen" -> "floating" across the breakpoint for dropdown behavior —
2785
+ // so an overlay that locked while narrow can land here while still open.
2786
+ // Without this, the page stays frozen behind a small floating bib until
2787
+ // hideBib() happens to run.
2788
+ this.lockScroll(false);
2789
+
2683
2790
  element.bib.style.position = "";
2684
2791
  element.bib.removeAttribute("isfullscreen");
2685
2792
  element.isBibFullscreen = false;
@@ -2789,6 +2896,46 @@ class AuroFloatingUI {
2789
2896
  this.hideBib("focusloss");
2790
2897
  }
2791
2898
 
2899
+ /**
2900
+ * @private
2901
+ * Installs the handlers that dismiss an open overlay.
2902
+ *
2903
+ * Shared by showBib(), which opens the overlay, and configure(), which has to
2904
+ * put these back after a rewire tore them down under an overlay that is still
2905
+ * open. A non-modal overlay dismisses on outside click, focus loss and
2906
+ * Escape; a modal one instead swallows Escape so the native dialog's
2907
+ * CloseWatcher never sees it (AB#1613688).
2908
+ */
2909
+ setupDismissHandlers() {
2910
+ const element = this.element;
2911
+ if (!element) {
2912
+ return;
2913
+ }
2914
+
2915
+ if (!element.modal) {
2916
+ this.setupHideHandlers();
2917
+ return;
2918
+ }
2919
+
2920
+ if (this.keyDownHandler) {
2921
+ document.removeEventListener("keydown", this.keyDownHandler);
2922
+ }
2923
+ this.keyDownHandler = (evt) => {
2924
+ if (evt.key === "Escape" && element.isPopoverVisible) {
2925
+ // Intercept at keydown so CloseWatcher never sees the keystroke.
2926
+ // Canceling `cancel` alone is insufficient — a second Esc with no
2927
+ // intervening user activation triggers the anti-trap and fires `close`
2928
+ // directly, bypassing any `cancel` preventDefault (AB#1613688).
2929
+ // stopImmediatePropagation is intentional: it prevents other document
2930
+ // keydown handlers (e.g. consumer code, auro-dialog) from also acting
2931
+ // on Escape while the modal owns the key.
2932
+ evt.preventDefault();
2933
+ evt.stopImmediatePropagation();
2934
+ }
2935
+ };
2936
+ document.addEventListener("keydown", this.keyDownHandler);
2937
+ }
2938
+
2792
2939
  setupHideHandlers() {
2793
2940
  const element = this.element;
2794
2941
  if (!element) {
@@ -2961,27 +3108,7 @@ class AuroFloatingUI {
2961
3108
 
2962
3109
  // prevent double showing: isPopovervisible gets first and showBib gets called later
2963
3110
  if (!this.showing) {
2964
- if (!element.modal) {
2965
- this.setupHideHandlers();
2966
- } else {
2967
- if (this.keyDownHandler) {
2968
- document.removeEventListener("keydown", this.keyDownHandler);
2969
- }
2970
- this.keyDownHandler = (evt) => {
2971
- if (evt.key === "Escape" && element.isPopoverVisible) {
2972
- // Intercept at keydown so CloseWatcher never sees the keystroke.
2973
- // Canceling `cancel` alone is insufficient — a second Esc with no
2974
- // intervening user activation triggers the anti-trap and fires `close`
2975
- // directly, bypassing any `cancel` preventDefault (AB#1613688).
2976
- // stopImmediatePropagation is intentional: it prevents other document
2977
- // keydown handlers (e.g. consumer code, auro-dialog) from also acting
2978
- // on Escape while the modal owns the key.
2979
- evt.preventDefault();
2980
- evt.stopImmediatePropagation();
2981
- }
2982
- };
2983
- document.addEventListener("keydown", this.keyDownHandler);
2984
- }
3111
+ this.setupDismissHandlers();
2985
3112
  this.showing = true;
2986
3113
  element.isPopoverVisible = true;
2987
3114
  this.position();
@@ -3209,7 +3336,7 @@ class AuroFloatingUI {
3209
3336
 
3210
3337
  this.id = element.getAttribute("id");
3211
3338
  if (!this.id) {
3212
- this.id = window.crypto.randomUUID();
3339
+ this.id = generateUUID();
3213
3340
  element.setAttribute("id", this.id);
3214
3341
  }
3215
3342
 
@@ -3221,7 +3348,35 @@ class AuroFloatingUI {
3221
3348
  this.enableKeyboardHandling = enableKeyboardHandling;
3222
3349
 
3223
3350
  this.eventPrefix = eventPrefix;
3224
- if (this.element !== elem) {
3351
+
3352
+ // A retained scroll lock's saved body styles were captured for whichever
3353
+ // element held it, so only a same-element rewire may keep it. An actual
3354
+ // element swap has to take the full teardown, or this instance is left
3355
+ // holding a lock that no longer describes the page it points at — with no
3356
+ // owner able to release it (AB#1647843).
3357
+ const rewiringSameElement = this.element === elem;
3358
+
3359
+ if (!rewiringSameElement) {
3360
+ // Tear down against the outgoing element, before this.element is
3361
+ // repointed. Everything disconnect() releases is element-scoped:
3362
+ // aria-modal on that element's inner dialog, the bib transform saved for
3363
+ // it, its autoUpdate loop and its trigger listeners. Repointing first
3364
+ // aimed all of it at the incoming element — the outgoing dialog kept
3365
+ // aria-modal="true" for good, its bib stayed transformed, and the
3366
+ // incoming bib had the outgoing element's saved transform written onto
3367
+ // it. The trigger gate below read the incoming element's trigger, which
3368
+ // is not assigned until later in this method, so on a swap teardown was
3369
+ // usually skipped outright and the page stayed locked with no owner.
3370
+ if (this.element?.trigger) {
3371
+ this.disconnect({ teardown: true });
3372
+ }
3373
+
3374
+ // disconnect() does not clear this.showing, and the rewire block at the
3375
+ // end of this method only runs for a same-element rewire. Left true, the
3376
+ // incoming element's first showBib() is silently dropped by its
3377
+ // `!this.showing` guard and the overlay never opens.
3378
+ this.showing = false;
3379
+
3225
3380
  this.element = elem;
3226
3381
  }
3227
3382
 
@@ -3234,8 +3389,10 @@ class AuroFloatingUI {
3234
3389
  this.behavior = element.behavior;
3235
3390
  }
3236
3391
 
3237
- if (element.trigger) {
3238
- this.disconnect();
3392
+ if (rewiringSameElement && element.trigger) {
3393
+ // Rewiring, not tearing down — an overlay that is open right now keeps
3394
+ // its page scroll lock across a trigger change.
3395
+ this.disconnect({ teardown: false });
3239
3396
  }
3240
3397
  element.trigger =
3241
3398
  element.triggerElement ||
@@ -3264,11 +3421,61 @@ class AuroFloatingUI {
3264
3421
  element.trigger.addEventListener("focus", this.handleEvent);
3265
3422
  element.trigger.addEventListener("blur", this.handleEvent);
3266
3423
  }
3424
+
3425
+ // disconnect() above stripped the dismissal handlers and killed the
3426
+ // autoUpdate loop; the trigger listeners re-registered here replace
3427
+ // neither. An overlay that is open right now would otherwise be left on
3428
+ // screen with outside-click, focus-loss and Escape all dead and its
3429
+ // position frozen — and, since the lock is deliberately retained, holding
3430
+ // the page with only its original trigger able to give it back.
3431
+ if (rewiringSameElement && this.showing) {
3432
+ this.setupDismissHandlers();
3433
+
3434
+ if (element.bib && (element.trigger || element.parentNode)) {
3435
+ element.cleanup = autoUpdate(
3436
+ element.trigger || element.parentNode,
3437
+ element.bib,
3438
+ () => {
3439
+ this.position();
3440
+ },
3441
+ );
3442
+ }
3443
+ }
3267
3444
  }
3268
3445
 
3269
- disconnect() {
3446
+ /**
3447
+ * Tears the floater's connections down.
3448
+ *
3449
+ * Two callers with different intent share this: a real teardown, which must
3450
+ * release the page scroll lock, and configure(), which re-runs it purely to
3451
+ * rewire a changed trigger and must leave an open overlay's lock alone.
3452
+ * @param {Object} [options] - Teardown options.
3453
+ * @param {Boolean} [options.teardown] - False when only rewiring; keeps the
3454
+ * page scroll lock held for an overlay that is still open.
3455
+ * @returns {void}
3456
+ */
3457
+ disconnect({ teardown = true } = {}) {
3270
3458
  this.cleanupHideHandlers();
3271
3459
 
3460
+ // Cancel any pending configureBibStrategy() retry. It would otherwise fire
3461
+ // a macrotask after the unlock below, re-lock the page, and leave it frozen
3462
+ // with the floater already gone. Cancelled on a rewire too: configure()
3463
+ // re-runs position() itself, so a stale retry has nothing to contribute.
3464
+ if (this._configureRetryId !== undefined) {
3465
+ clearTimeout(this._configureRetryId);
3466
+ this._configureRetryId = undefined;
3467
+ }
3468
+
3469
+ // Tearing down while the bib is still open would otherwise strand
3470
+ // body{position:fixed} and leave the page permanently unscrollable —
3471
+ // hideBib() is the only other unlock, and it never runs in this path.
3472
+ // Skipped when merely rewiring: configure() routes through here on every
3473
+ // triggerElement change, and releasing the lock there would unlock the page
3474
+ // behind an overlay that is still open (AB#1625424, AB#1625435).
3475
+ if (teardown) {
3476
+ this.lockScroll(false);
3477
+ }
3478
+
3272
3479
  const element = this.element;
3273
3480
  if (!element) {
3274
3481
  return;
@@ -3423,6 +3630,14 @@ class ComponentBase extends i$2 {
3423
3630
  this.isBibFullscreen = false;
3424
3631
  this.floater = new AuroFloatingUI(this, 'dialog');
3425
3632
 
3633
+ /**
3634
+ * @private
3635
+ * True once _teardown() has released the floater and nothing has rewired
3636
+ * it. Read by connectedCallback() to rewire a remounted dialog, and by
3637
+ * _teardown() to stay idempotent.
3638
+ */
3639
+ this._floaterTornDown = false;
3640
+
3426
3641
  const versioning = new AuroDependencyVersioning();
3427
3642
 
3428
3643
  /**
@@ -3483,10 +3698,12 @@ class ComponentBase extends i$2 {
3483
3698
 
3484
3699
  /**
3485
3700
  * DEPRECATED - use `close-button-appearance="inverse" instead.
3701
+ * @deprecated Use `close-button-appearance="inverse"` instead.
3486
3702
  */
3487
3703
  onDark: {
3488
3704
  type: Boolean,
3489
- reflect: true
3705
+ reflect: true,
3706
+ attribute: 'ondark'
3490
3707
  },
3491
3708
 
3492
3709
  /**
@@ -3504,7 +3721,8 @@ class ComponentBase extends i$2 {
3504
3721
  */
3505
3722
  isBibFullscreen: {
3506
3723
  type: Boolean,
3507
- reflect: true
3724
+ reflect: true,
3725
+ attribute: 'isbibfullscreen'
3508
3726
  },
3509
3727
 
3510
3728
  /**
@@ -3573,7 +3791,7 @@ class ComponentBase extends i$2 {
3573
3791
  slotWrapper.classList.remove("dialog-footer");
3574
3792
  }
3575
3793
 
3576
- this.floater.configure(this, this.floaterConfig.prefix);
3794
+ this._configureFloater();
3577
3795
 
3578
3796
  // Forward FloatingUI toggle event to backward-compatible 'toggle' event
3579
3797
  this.addEventListener('auroDialog-toggled', (event) => {
@@ -3582,9 +3800,15 @@ class ComponentBase extends i$2 {
3582
3800
  }
3583
3801
  });
3584
3802
 
3585
- // Always intercept the native ESC/cancel event so we can decide
3586
- // whether to honour it based on `modal`. Re-dispatch via FloatingUI
3587
- // for non-modal so the hide lifecycle runs correctly.
3803
+ // Defense-in-depth for the native ESC/cancel event. In practice neither
3804
+ // branch fires today: auro-library's floatingUI binds its own document-level
3805
+ // keydown handler, which closes a non-modal dialog via hideBib("keydown")
3806
+ // and, for a modal, swallows the keystroke with preventDefault() +
3807
+ // stopImmediatePropagation() before the platform CloseWatcher ever sees it
3808
+ // (AB#1613688). This listener is kept rather than deleted because it is the
3809
+ // component-side guarantee that a modal never self-closes on ESC: if the
3810
+ // library stops swallowing, the platform `cancel` would otherwise close a
3811
+ // modal dialog, which is exactly the behavior `modal` exists to prevent.
3588
3812
  this.dialog.addEventListener('cancel', (e) => {
3589
3813
  e.preventDefault();
3590
3814
  if (!this.modal) {
@@ -3631,12 +3855,78 @@ class ComponentBase extends i$2 {
3631
3855
  }
3632
3856
 
3633
3857
  if (changedProperties.has('triggerElement')) {
3634
- this.floater.configure(this, this.floaterConfig.prefix);
3858
+ this._configureFloater();
3859
+ }
3860
+ }
3861
+
3862
+ /**
3863
+ * @private
3864
+ * Wires the floater to this element and records that it is live, so a later
3865
+ * reconnect can tell a torn-down floater from one that never lost its wiring.
3866
+ * @returns {void}
3867
+ */
3868
+ _configureFloater() {
3869
+ this._floaterTornDown = false;
3870
+ this.floater.configure(this, this.floaterConfig.prefix);
3871
+ }
3872
+
3873
+ connectedCallback() {
3874
+ super.connectedCallback();
3875
+
3876
+ // Lit does not re-run firstUpdated() on reconnect, so without this a host
3877
+ // that removes and re-inserts the dialog across separate tasks — Vue
3878
+ // <keep-alive>, caching tab/accordion hosts, virtualized lists, or anything
3879
+ // that parks the node in a DocumentFragment in one task and inserts it in
3880
+ // the next — gets back a dead element: _teardown() dropped the trigger
3881
+ // listeners, ran element.cleanup(), tore down autoUpdate and released the
3882
+ // page scroll lock, and nothing rewired any of it.
3883
+ //
3884
+ // Only re-configure when a teardown actually ran. On the normal first
3885
+ // connect the flag is false and firstUpdated() owns the single configure
3886
+ // call; on a same-task DOM move the deferred teardown never runs, so the
3887
+ // flag stays false here too and the move is still a no-op.
3888
+ if (this.hasUpdated && this._floaterTornDown) {
3889
+ this._configureFloater();
3635
3890
  }
3636
3891
  }
3637
3892
 
3638
3893
  disconnectedCallback() {
3639
3894
  super.disconnectedCallback();
3895
+
3896
+ // A same-document DOM move (appendChild of an already-parented node) fires
3897
+ // disconnectedCallback and reconnects within the same task. Defer the
3898
+ // teardown one microtask and bail if the element came back, so a reparent
3899
+ // does not dismantle a dialog that is still open: floater.disconnect()
3900
+ // releases the page scroll lock and tears down autoUpdate, which would
3901
+ // leave the dialog visible over a scrollable page with nothing left to
3902
+ // re-lock it — the AB#1625424 symptom returning by a different route.
3903
+ //
3904
+ // A host that removes and re-inserts across tasks (not a move) is treated
3905
+ // as a real unmount, which is the correct reading of that sequence.
3906
+ queueMicrotask(() => {
3907
+ if (this.isConnected) {
3908
+ return;
3909
+ }
3910
+ this._teardown();
3911
+ });
3912
+ }
3913
+
3914
+ /**
3915
+ * @private
3916
+ * Releases everything the dialog owns outside its own shadow root.
3917
+ * Split out of disconnectedCallback so a DOM move can skip it.
3918
+ *
3919
+ * Idempotent: a repeated disconnect in the same task must not invoke
3920
+ * floater.disconnect() twice, and connectedCallback() reads the same flag to
3921
+ * decide whether the floater needs rewiring.
3922
+ * @returns {void}
3923
+ */
3924
+ _teardown() {
3925
+ if (this._floaterTornDown) {
3926
+ return;
3927
+ }
3928
+ this._floaterTornDown = true;
3929
+
3640
3930
  if (this._hidePopoverTimerId) {
3641
3931
  clearTimeout(this._hidePopoverTimerId);
3642
3932
  this._hidePopoverTimerId = undefined;