@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.
@@ -2222,6 +2222,48 @@ const computePosition = (reference, floating, options) => {
2222
2222
  });
2223
2223
  };
2224
2224
 
2225
+ /**
2226
+ * Generates a v4 UUID with a three-tier fallback for insecure contexts.
2227
+ *
2228
+ * Tier 1 (secure contexts): crypto.randomUUID() — native, fully random.
2229
+ * Tier 2 (insecure contexts): crypto.getRandomValues() — available on all
2230
+ * modern browsers regardless of secure-context status.
2231
+ * Tier 3 (last resort, no crypto API): new Date().getTime() — lower entropy,
2232
+ * but sufficient for element IDs where collision risk is negligible.
2233
+ *
2234
+ * See: https://w3c.github.io/webcrypto/#dom-crypto-randomuuid (secure-context restriction)
2235
+ */
2236
+ function generateUUID() {
2237
+ let uuid;
2238
+
2239
+ if (
2240
+ typeof crypto !== "undefined" &&
2241
+ typeof crypto.randomUUID === "function"
2242
+ ) {
2243
+ uuid = crypto.randomUUID();
2244
+ } else if (
2245
+ typeof crypto !== "undefined" &&
2246
+ typeof crypto.getRandomValues === "function"
2247
+ ) {
2248
+ const bytes = new Uint8Array(16);
2249
+ crypto.getRandomValues(bytes);
2250
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
2251
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
2252
+ const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0"));
2253
+ 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("")}`;
2254
+ } else {
2255
+ let d = new Date().getTime();
2256
+ uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
2257
+ const r = (d % 16) | 0;
2258
+ d = (d - r) / 16 || new Date().getTime();
2259
+ return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
2260
+ });
2261
+ }
2262
+
2263
+ // CSS selectors require IDs starting with a letter; replace a leading digit with 'a'.
2264
+ return /^[0-9]/.test(uuid) ? `a${uuid.slice(1)}` : uuid;
2265
+ }
2266
+
2225
2267
  /* eslint-disable line-comment-position, no-inline-comments */
2226
2268
 
2227
2269
 
@@ -2509,30 +2551,61 @@ class AuroFloatingUI {
2509
2551
 
2510
2552
  /**
2511
2553
  * @private
2512
- * Controls whether to lock the scrolling for the document's body.
2513
- * @param {Boolean} lock - If true, locks the body's scrolling functionlity; otherwise, unlock.
2554
+ * Mirrors `aria-modal` onto the `<dialog>` inside the bib, tracking the page
2555
+ * scroll lock: an overlay that holds the page hides it from assistive tech.
2556
+ *
2557
+ * The removal is called only from inside lockScroll()'s ownership guard: run
2558
+ * outside it, it fired on every autoUpdate tick of every NON-owning instance,
2559
+ * and a repositioning dropdown stripped `aria-modal` off an open modal dialog
2560
+ * it had nothing to do with. The write is unguarded by design — see the call
2561
+ * site — but is skipped when the attribute already reads correctly, so the
2562
+ * repeat costs no mutation record.
2563
+ * @param {Boolean} modal - True to mark the dialog modal; false to unmark it.
2514
2564
  */
2515
- lockScroll(lock = true) {
2565
+ mirrorAriaModal(modal) {
2516
2566
  const element = this.element;
2567
+ const dialog = element?.bib
2568
+ ? (element.bib?.shadowRoot || element.bib || element).querySelector(
2569
+ "dialog",
2570
+ )
2571
+ : undefined;
2517
2572
 
2518
- if (!element?.bib) {
2573
+ if (!dialog) {
2519
2574
  return;
2520
2575
  }
2521
2576
 
2522
- const dialog = (
2523
- element.bib?.shadowRoot ||
2524
- element.bib ||
2525
- element
2526
- ).querySelector("dialog");
2527
- if (dialog) {
2528
- if (lock) {
2577
+ if (modal) {
2578
+ if (dialog.getAttribute("aria-modal") !== "true") {
2529
2579
  dialog.setAttribute("aria-modal", "true");
2530
- } else {
2531
- dialog.removeAttribute("aria-modal");
2532
2580
  }
2581
+ } else {
2582
+ dialog.removeAttribute("aria-modal");
2583
+ }
2584
+ }
2585
+
2586
+ /**
2587
+ * @private
2588
+ * Controls whether to lock the scrolling for the document's body.
2589
+ * @param {Boolean} lock - If true, locks the body's scrolling functionlity; otherwise, unlock.
2590
+ */
2591
+ lockScroll(lock = true) {
2592
+ const element = this.element;
2593
+
2594
+ // Locking needs a bib to lock around; unlocking must not, or a bib that has
2595
+ // already been detached would leave the page frozen with no way back.
2596
+ if (lock && !element?.bib) {
2597
+ return;
2533
2598
  }
2534
2599
 
2535
2600
  if (lock) {
2601
+ // Re-asserted on every lock call rather than only on the unlocked ->
2602
+ // locked transition. If the bib's inner <dialog> is replaced while this
2603
+ // instance still holds the lock, the replacement would otherwise go
2604
+ // unmarked until a full unlock/lock cycle. Safe to repeat: it targets
2605
+ // this instance's own element, so it cannot reach another overlay's
2606
+ // dialog, and mirrorAriaModal() skips the write when already correct.
2607
+ this.mirrorAriaModal(true);
2608
+
2536
2609
  if (!this._scrollLocked) {
2537
2610
  this._scrollLocked = true;
2538
2611
  this._savedScrollY = window.scrollY;
@@ -2590,6 +2663,8 @@ class AuroFloatingUI {
2590
2663
  }
2591
2664
  } else {
2592
2665
  if (this._scrollLocked) {
2666
+ this.mirrorAriaModal(false);
2667
+
2593
2668
  if (this._viewportHandler && window.visualViewport) {
2594
2669
  window.visualViewport.removeEventListener(
2595
2670
  "resize",
@@ -2702,15 +2777,47 @@ class AuroFloatingUI {
2702
2777
  } else if (this.configureTrial < MAX_CONFIGURATION_COUNT) {
2703
2778
  this.configureTrial += 1;
2704
2779
 
2705
- setTimeout(() => {
2780
+ // The id is retained so disconnect() can cancel it. A retry that fires
2781
+ // after teardown re-enters this method with isPopoverVisible still true
2782
+ // — nothing clears it on the teardown path — and takes the page scroll
2783
+ // lock back, with no owner left to release it.
2784
+ //
2785
+ // This is not the rare timing race it reads as: the lookup above wants
2786
+ // a `.container` inside the BIB's own shadow root, which only a bib
2787
+ // that is itself a custom element has. A consumer whose bib is a plain
2788
+ // element (auro-dialog's `#bib` is a div, with its `.container` a
2789
+ // sibling in the host's shadow root) can never satisfy it, so it
2790
+ // retries the full MAX_CONFIGURATION_COUNT every time. Correcting the
2791
+ // lookup would change bib sizing for those consumers and is tracked
2792
+ // separately; cancelling the timer is what keeps it from freezing the
2793
+ // page.
2794
+ this._configureRetryId = setTimeout(() => {
2795
+ this._configureRetryId = undefined;
2706
2796
  this.configureBibStrategy(value);
2707
2797
  }, 0);
2708
2798
  }
2709
2799
 
2710
2800
  if (element.isPopoverVisible) {
2711
- this.lockScroll(value === "fullscreen");
2801
+ // Positioning strategy is not scroll ownership. Both "fullscreen" and
2802
+ // "dialog" lay the bib over the page and trap focus inside it, so both
2803
+ // must freeze the page behind them (AB#1647843). Gating this on
2804
+ // "fullscreen" alone left dismissible desktop dialogs and drawers
2805
+ // scrollable, and actively released the lock on every reposition
2806
+ // (AB#1625424, AB#1625435). The nested "cover" strategy takes the else
2807
+ // branch and stays scrollable by design — it sits inside its parent
2808
+ // rather than over the page.
2809
+ this.lockScroll(true);
2712
2810
  }
2713
2811
  } else {
2812
+ // The mirror of the lock above: a strategy that does not own the page must
2813
+ // hand the lock back, not merely decline to take it. autoUpdate re-runs
2814
+ // this method on every resize tick, and getPositioningStrategy() flips
2815
+ // "fullscreen" -> "floating" across the breakpoint for dropdown behavior —
2816
+ // so an overlay that locked while narrow can land here while still open.
2817
+ // Without this, the page stays frozen behind a small floating bib until
2818
+ // hideBib() happens to run.
2819
+ this.lockScroll(false);
2820
+
2714
2821
  element.bib.style.position = "";
2715
2822
  element.bib.removeAttribute("isfullscreen");
2716
2823
  element.isBibFullscreen = false;
@@ -2820,6 +2927,46 @@ class AuroFloatingUI {
2820
2927
  this.hideBib("focusloss");
2821
2928
  }
2822
2929
 
2930
+ /**
2931
+ * @private
2932
+ * Installs the handlers that dismiss an open overlay.
2933
+ *
2934
+ * Shared by showBib(), which opens the overlay, and configure(), which has to
2935
+ * put these back after a rewire tore them down under an overlay that is still
2936
+ * open. A non-modal overlay dismisses on outside click, focus loss and
2937
+ * Escape; a modal one instead swallows Escape so the native dialog's
2938
+ * CloseWatcher never sees it (AB#1613688).
2939
+ */
2940
+ setupDismissHandlers() {
2941
+ const element = this.element;
2942
+ if (!element) {
2943
+ return;
2944
+ }
2945
+
2946
+ if (!element.modal) {
2947
+ this.setupHideHandlers();
2948
+ return;
2949
+ }
2950
+
2951
+ if (this.keyDownHandler) {
2952
+ document.removeEventListener("keydown", this.keyDownHandler);
2953
+ }
2954
+ this.keyDownHandler = (evt) => {
2955
+ if (evt.key === "Escape" && element.isPopoverVisible) {
2956
+ // Intercept at keydown so CloseWatcher never sees the keystroke.
2957
+ // Canceling `cancel` alone is insufficient — a second Esc with no
2958
+ // intervening user activation triggers the anti-trap and fires `close`
2959
+ // directly, bypassing any `cancel` preventDefault (AB#1613688).
2960
+ // stopImmediatePropagation is intentional: it prevents other document
2961
+ // keydown handlers (e.g. consumer code, auro-dialog) from also acting
2962
+ // on Escape while the modal owns the key.
2963
+ evt.preventDefault();
2964
+ evt.stopImmediatePropagation();
2965
+ }
2966
+ };
2967
+ document.addEventListener("keydown", this.keyDownHandler);
2968
+ }
2969
+
2823
2970
  setupHideHandlers() {
2824
2971
  const element = this.element;
2825
2972
  if (!element) {
@@ -2992,27 +3139,7 @@ class AuroFloatingUI {
2992
3139
 
2993
3140
  // prevent double showing: isPopovervisible gets first and showBib gets called later
2994
3141
  if (!this.showing) {
2995
- if (!element.modal) {
2996
- this.setupHideHandlers();
2997
- } else {
2998
- if (this.keyDownHandler) {
2999
- document.removeEventListener("keydown", this.keyDownHandler);
3000
- }
3001
- this.keyDownHandler = (evt) => {
3002
- if (evt.key === "Escape" && element.isPopoverVisible) {
3003
- // Intercept at keydown so CloseWatcher never sees the keystroke.
3004
- // Canceling `cancel` alone is insufficient — a second Esc with no
3005
- // intervening user activation triggers the anti-trap and fires `close`
3006
- // directly, bypassing any `cancel` preventDefault (AB#1613688).
3007
- // stopImmediatePropagation is intentional: it prevents other document
3008
- // keydown handlers (e.g. consumer code, auro-dialog) from also acting
3009
- // on Escape while the modal owns the key.
3010
- evt.preventDefault();
3011
- evt.stopImmediatePropagation();
3012
- }
3013
- };
3014
- document.addEventListener("keydown", this.keyDownHandler);
3015
- }
3142
+ this.setupDismissHandlers();
3016
3143
  this.showing = true;
3017
3144
  element.isPopoverVisible = true;
3018
3145
  this.position();
@@ -3240,7 +3367,7 @@ class AuroFloatingUI {
3240
3367
 
3241
3368
  this.id = element.getAttribute("id");
3242
3369
  if (!this.id) {
3243
- this.id = window.crypto.randomUUID();
3370
+ this.id = generateUUID();
3244
3371
  element.setAttribute("id", this.id);
3245
3372
  }
3246
3373
 
@@ -3252,7 +3379,35 @@ class AuroFloatingUI {
3252
3379
  this.enableKeyboardHandling = enableKeyboardHandling;
3253
3380
 
3254
3381
  this.eventPrefix = eventPrefix;
3255
- if (this.element !== elem) {
3382
+
3383
+ // A retained scroll lock's saved body styles were captured for whichever
3384
+ // element held it, so only a same-element rewire may keep it. An actual
3385
+ // element swap has to take the full teardown, or this instance is left
3386
+ // holding a lock that no longer describes the page it points at — with no
3387
+ // owner able to release it (AB#1647843).
3388
+ const rewiringSameElement = this.element === elem;
3389
+
3390
+ if (!rewiringSameElement) {
3391
+ // Tear down against the outgoing element, before this.element is
3392
+ // repointed. Everything disconnect() releases is element-scoped:
3393
+ // aria-modal on that element's inner dialog, the bib transform saved for
3394
+ // it, its autoUpdate loop and its trigger listeners. Repointing first
3395
+ // aimed all of it at the incoming element — the outgoing dialog kept
3396
+ // aria-modal="true" for good, its bib stayed transformed, and the
3397
+ // incoming bib had the outgoing element's saved transform written onto
3398
+ // it. The trigger gate below read the incoming element's trigger, which
3399
+ // is not assigned until later in this method, so on a swap teardown was
3400
+ // usually skipped outright and the page stayed locked with no owner.
3401
+ if (this.element?.trigger) {
3402
+ this.disconnect({ teardown: true });
3403
+ }
3404
+
3405
+ // disconnect() does not clear this.showing, and the rewire block at the
3406
+ // end of this method only runs for a same-element rewire. Left true, the
3407
+ // incoming element's first showBib() is silently dropped by its
3408
+ // `!this.showing` guard and the overlay never opens.
3409
+ this.showing = false;
3410
+
3256
3411
  this.element = elem;
3257
3412
  }
3258
3413
 
@@ -3265,8 +3420,10 @@ class AuroFloatingUI {
3265
3420
  this.behavior = element.behavior;
3266
3421
  }
3267
3422
 
3268
- if (element.trigger) {
3269
- this.disconnect();
3423
+ if (rewiringSameElement && element.trigger) {
3424
+ // Rewiring, not tearing down — an overlay that is open right now keeps
3425
+ // its page scroll lock across a trigger change.
3426
+ this.disconnect({ teardown: false });
3270
3427
  }
3271
3428
  element.trigger =
3272
3429
  element.triggerElement ||
@@ -3295,11 +3452,61 @@ class AuroFloatingUI {
3295
3452
  element.trigger.addEventListener("focus", this.handleEvent);
3296
3453
  element.trigger.addEventListener("blur", this.handleEvent);
3297
3454
  }
3455
+
3456
+ // disconnect() above stripped the dismissal handlers and killed the
3457
+ // autoUpdate loop; the trigger listeners re-registered here replace
3458
+ // neither. An overlay that is open right now would otherwise be left on
3459
+ // screen with outside-click, focus-loss and Escape all dead and its
3460
+ // position frozen — and, since the lock is deliberately retained, holding
3461
+ // the page with only its original trigger able to give it back.
3462
+ if (rewiringSameElement && this.showing) {
3463
+ this.setupDismissHandlers();
3464
+
3465
+ if (element.bib && (element.trigger || element.parentNode)) {
3466
+ element.cleanup = autoUpdate(
3467
+ element.trigger || element.parentNode,
3468
+ element.bib,
3469
+ () => {
3470
+ this.position();
3471
+ },
3472
+ );
3473
+ }
3474
+ }
3298
3475
  }
3299
3476
 
3300
- disconnect() {
3477
+ /**
3478
+ * Tears the floater's connections down.
3479
+ *
3480
+ * Two callers with different intent share this: a real teardown, which must
3481
+ * release the page scroll lock, and configure(), which re-runs it purely to
3482
+ * rewire a changed trigger and must leave an open overlay's lock alone.
3483
+ * @param {Object} [options] - Teardown options.
3484
+ * @param {Boolean} [options.teardown] - False when only rewiring; keeps the
3485
+ * page scroll lock held for an overlay that is still open.
3486
+ * @returns {void}
3487
+ */
3488
+ disconnect({ teardown = true } = {}) {
3301
3489
  this.cleanupHideHandlers();
3302
3490
 
3491
+ // Cancel any pending configureBibStrategy() retry. It would otherwise fire
3492
+ // a macrotask after the unlock below, re-lock the page, and leave it frozen
3493
+ // with the floater already gone. Cancelled on a rewire too: configure()
3494
+ // re-runs position() itself, so a stale retry has nothing to contribute.
3495
+ if (this._configureRetryId !== undefined) {
3496
+ clearTimeout(this._configureRetryId);
3497
+ this._configureRetryId = undefined;
3498
+ }
3499
+
3500
+ // Tearing down while the bib is still open would otherwise strand
3501
+ // body{position:fixed} and leave the page permanently unscrollable —
3502
+ // hideBib() is the only other unlock, and it never runs in this path.
3503
+ // Skipped when merely rewiring: configure() routes through here on every
3504
+ // triggerElement change, and releasing the lock there would unlock the page
3505
+ // behind an overlay that is still open (AB#1625424, AB#1625435).
3506
+ if (teardown) {
3507
+ this.lockScroll(false);
3508
+ }
3509
+
3303
3510
  const element = this.element;
3304
3511
  if (!element) {
3305
3512
  return;
@@ -3454,6 +3661,14 @@ class ComponentBase extends i$2 {
3454
3661
  this.isBibFullscreen = false;
3455
3662
  this.floater = new AuroFloatingUI(this, 'dialog');
3456
3663
 
3664
+ /**
3665
+ * @private
3666
+ * True once _teardown() has released the floater and nothing has rewired
3667
+ * it. Read by connectedCallback() to rewire a remounted dialog, and by
3668
+ * _teardown() to stay idempotent.
3669
+ */
3670
+ this._floaterTornDown = false;
3671
+
3457
3672
  const versioning = new AuroDependencyVersioning();
3458
3673
 
3459
3674
  /**
@@ -3514,10 +3729,12 @@ class ComponentBase extends i$2 {
3514
3729
 
3515
3730
  /**
3516
3731
  * DEPRECATED - use `close-button-appearance="inverse" instead.
3732
+ * @deprecated Use `close-button-appearance="inverse"` instead.
3517
3733
  */
3518
3734
  onDark: {
3519
3735
  type: Boolean,
3520
- reflect: true
3736
+ reflect: true,
3737
+ attribute: 'ondark'
3521
3738
  },
3522
3739
 
3523
3740
  /**
@@ -3535,7 +3752,8 @@ class ComponentBase extends i$2 {
3535
3752
  */
3536
3753
  isBibFullscreen: {
3537
3754
  type: Boolean,
3538
- reflect: true
3755
+ reflect: true,
3756
+ attribute: 'isbibfullscreen'
3539
3757
  },
3540
3758
 
3541
3759
  /**
@@ -3604,7 +3822,7 @@ class ComponentBase extends i$2 {
3604
3822
  slotWrapper.classList.remove("dialog-footer");
3605
3823
  }
3606
3824
 
3607
- this.floater.configure(this, this.floaterConfig.prefix);
3825
+ this._configureFloater();
3608
3826
 
3609
3827
  // Forward FloatingUI toggle event to backward-compatible 'toggle' event
3610
3828
  this.addEventListener('auroDialog-toggled', (event) => {
@@ -3613,9 +3831,15 @@ class ComponentBase extends i$2 {
3613
3831
  }
3614
3832
  });
3615
3833
 
3616
- // Always intercept the native ESC/cancel event so we can decide
3617
- // whether to honour it based on `modal`. Re-dispatch via FloatingUI
3618
- // for non-modal so the hide lifecycle runs correctly.
3834
+ // Defense-in-depth for the native ESC/cancel event. In practice neither
3835
+ // branch fires today: auro-library's floatingUI binds its own document-level
3836
+ // keydown handler, which closes a non-modal dialog via hideBib("keydown")
3837
+ // and, for a modal, swallows the keystroke with preventDefault() +
3838
+ // stopImmediatePropagation() before the platform CloseWatcher ever sees it
3839
+ // (AB#1613688). This listener is kept rather than deleted because it is the
3840
+ // component-side guarantee that a modal never self-closes on ESC: if the
3841
+ // library stops swallowing, the platform `cancel` would otherwise close a
3842
+ // modal dialog, which is exactly the behavior `modal` exists to prevent.
3619
3843
  this.dialog.addEventListener('cancel', (e) => {
3620
3844
  e.preventDefault();
3621
3845
  if (!this.modal) {
@@ -3662,12 +3886,78 @@ class ComponentBase extends i$2 {
3662
3886
  }
3663
3887
 
3664
3888
  if (changedProperties.has('triggerElement')) {
3665
- this.floater.configure(this, this.floaterConfig.prefix);
3889
+ this._configureFloater();
3890
+ }
3891
+ }
3892
+
3893
+ /**
3894
+ * @private
3895
+ * Wires the floater to this element and records that it is live, so a later
3896
+ * reconnect can tell a torn-down floater from one that never lost its wiring.
3897
+ * @returns {void}
3898
+ */
3899
+ _configureFloater() {
3900
+ this._floaterTornDown = false;
3901
+ this.floater.configure(this, this.floaterConfig.prefix);
3902
+ }
3903
+
3904
+ connectedCallback() {
3905
+ super.connectedCallback();
3906
+
3907
+ // Lit does not re-run firstUpdated() on reconnect, so without this a host
3908
+ // that removes and re-inserts the dialog across separate tasks — Vue
3909
+ // <keep-alive>, caching tab/accordion hosts, virtualized lists, or anything
3910
+ // that parks the node in a DocumentFragment in one task and inserts it in
3911
+ // the next — gets back a dead element: _teardown() dropped the trigger
3912
+ // listeners, ran element.cleanup(), tore down autoUpdate and released the
3913
+ // page scroll lock, and nothing rewired any of it.
3914
+ //
3915
+ // Only re-configure when a teardown actually ran. On the normal first
3916
+ // connect the flag is false and firstUpdated() owns the single configure
3917
+ // call; on a same-task DOM move the deferred teardown never runs, so the
3918
+ // flag stays false here too and the move is still a no-op.
3919
+ if (this.hasUpdated && this._floaterTornDown) {
3920
+ this._configureFloater();
3666
3921
  }
3667
3922
  }
3668
3923
 
3669
3924
  disconnectedCallback() {
3670
3925
  super.disconnectedCallback();
3926
+
3927
+ // A same-document DOM move (appendChild of an already-parented node) fires
3928
+ // disconnectedCallback and reconnects within the same task. Defer the
3929
+ // teardown one microtask and bail if the element came back, so a reparent
3930
+ // does not dismantle a dialog that is still open: floater.disconnect()
3931
+ // releases the page scroll lock and tears down autoUpdate, which would
3932
+ // leave the dialog visible over a scrollable page with nothing left to
3933
+ // re-lock it — the AB#1625424 symptom returning by a different route.
3934
+ //
3935
+ // A host that removes and re-inserts across tasks (not a move) is treated
3936
+ // as a real unmount, which is the correct reading of that sequence.
3937
+ queueMicrotask(() => {
3938
+ if (this.isConnected) {
3939
+ return;
3940
+ }
3941
+ this._teardown();
3942
+ });
3943
+ }
3944
+
3945
+ /**
3946
+ * @private
3947
+ * Releases everything the dialog owns outside its own shadow root.
3948
+ * Split out of disconnectedCallback so a DOM move can skip it.
3949
+ *
3950
+ * Idempotent: a repeated disconnect in the same task must not invoke
3951
+ * floater.disconnect() twice, and connectedCallback() reads the same flag to
3952
+ * decide whether the floater needs rewiring.
3953
+ * @returns {void}
3954
+ */
3955
+ _teardown() {
3956
+ if (this._floaterTornDown) {
3957
+ return;
3958
+ }
3959
+ this._floaterTornDown = true;
3960
+
3671
3961
  if (this._hidePopoverTimerId) {
3672
3962
  clearTimeout(this._hidePopoverTimerId);
3673
3963
  this._hidePopoverTimerId = undefined;