@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/demo/index.min.js CHANGED
@@ -2266,6 +2266,48 @@ const computePosition = (reference, floating, options) => {
2266
2266
  });
2267
2267
  };
2268
2268
 
2269
+ /**
2270
+ * Generates a v4 UUID with a three-tier fallback for insecure contexts.
2271
+ *
2272
+ * Tier 1 (secure contexts): crypto.randomUUID() — native, fully random.
2273
+ * Tier 2 (insecure contexts): crypto.getRandomValues() — available on all
2274
+ * modern browsers regardless of secure-context status.
2275
+ * Tier 3 (last resort, no crypto API): new Date().getTime() — lower entropy,
2276
+ * but sufficient for element IDs where collision risk is negligible.
2277
+ *
2278
+ * See: https://w3c.github.io/webcrypto/#dom-crypto-randomuuid (secure-context restriction)
2279
+ */
2280
+ function generateUUID() {
2281
+ let uuid;
2282
+
2283
+ if (
2284
+ typeof crypto !== "undefined" &&
2285
+ typeof crypto.randomUUID === "function"
2286
+ ) {
2287
+ uuid = crypto.randomUUID();
2288
+ } else if (
2289
+ typeof crypto !== "undefined" &&
2290
+ typeof crypto.getRandomValues === "function"
2291
+ ) {
2292
+ const bytes = new Uint8Array(16);
2293
+ crypto.getRandomValues(bytes);
2294
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
2295
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
2296
+ const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0"));
2297
+ 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("")}`;
2298
+ } else {
2299
+ let d = new Date().getTime();
2300
+ uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
2301
+ const r = (d % 16) | 0;
2302
+ d = (d - r) / 16 || new Date().getTime();
2303
+ return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
2304
+ });
2305
+ }
2306
+
2307
+ // CSS selectors require IDs starting with a letter; replace a leading digit with 'a'.
2308
+ return /^[0-9]/.test(uuid) ? `a${uuid.slice(1)}` : uuid;
2309
+ }
2310
+
2269
2311
  /* eslint-disable line-comment-position, no-inline-comments */
2270
2312
 
2271
2313
 
@@ -2553,30 +2595,61 @@ class AuroFloatingUI {
2553
2595
 
2554
2596
  /**
2555
2597
  * @private
2556
- * Controls whether to lock the scrolling for the document's body.
2557
- * @param {Boolean} lock - If true, locks the body's scrolling functionlity; otherwise, unlock.
2598
+ * Mirrors `aria-modal` onto the `<dialog>` inside the bib, tracking the page
2599
+ * scroll lock: an overlay that holds the page hides it from assistive tech.
2600
+ *
2601
+ * The removal is called only from inside lockScroll()'s ownership guard: run
2602
+ * outside it, it fired on every autoUpdate tick of every NON-owning instance,
2603
+ * and a repositioning dropdown stripped `aria-modal` off an open modal dialog
2604
+ * it had nothing to do with. The write is unguarded by design — see the call
2605
+ * site — but is skipped when the attribute already reads correctly, so the
2606
+ * repeat costs no mutation record.
2607
+ * @param {Boolean} modal - True to mark the dialog modal; false to unmark it.
2558
2608
  */
2559
- lockScroll(lock = true) {
2609
+ mirrorAriaModal(modal) {
2560
2610
  const element = this.element;
2611
+ const dialog = element?.bib
2612
+ ? (element.bib?.shadowRoot || element.bib || element).querySelector(
2613
+ "dialog",
2614
+ )
2615
+ : undefined;
2561
2616
 
2562
- if (!element?.bib) {
2617
+ if (!dialog) {
2563
2618
  return;
2564
2619
  }
2565
2620
 
2566
- const dialog = (
2567
- element.bib?.shadowRoot ||
2568
- element.bib ||
2569
- element
2570
- ).querySelector("dialog");
2571
- if (dialog) {
2572
- if (lock) {
2621
+ if (modal) {
2622
+ if (dialog.getAttribute("aria-modal") !== "true") {
2573
2623
  dialog.setAttribute("aria-modal", "true");
2574
- } else {
2575
- dialog.removeAttribute("aria-modal");
2576
2624
  }
2625
+ } else {
2626
+ dialog.removeAttribute("aria-modal");
2627
+ }
2628
+ }
2629
+
2630
+ /**
2631
+ * @private
2632
+ * Controls whether to lock the scrolling for the document's body.
2633
+ * @param {Boolean} lock - If true, locks the body's scrolling functionlity; otherwise, unlock.
2634
+ */
2635
+ lockScroll(lock = true) {
2636
+ const element = this.element;
2637
+
2638
+ // Locking needs a bib to lock around; unlocking must not, or a bib that has
2639
+ // already been detached would leave the page frozen with no way back.
2640
+ if (lock && !element?.bib) {
2641
+ return;
2577
2642
  }
2578
2643
 
2579
2644
  if (lock) {
2645
+ // Re-asserted on every lock call rather than only on the unlocked ->
2646
+ // locked transition. If the bib's inner <dialog> is replaced while this
2647
+ // instance still holds the lock, the replacement would otherwise go
2648
+ // unmarked until a full unlock/lock cycle. Safe to repeat: it targets
2649
+ // this instance's own element, so it cannot reach another overlay's
2650
+ // dialog, and mirrorAriaModal() skips the write when already correct.
2651
+ this.mirrorAriaModal(true);
2652
+
2580
2653
  if (!this._scrollLocked) {
2581
2654
  this._scrollLocked = true;
2582
2655
  this._savedScrollY = window.scrollY;
@@ -2634,6 +2707,8 @@ class AuroFloatingUI {
2634
2707
  }
2635
2708
  } else {
2636
2709
  if (this._scrollLocked) {
2710
+ this.mirrorAriaModal(false);
2711
+
2637
2712
  if (this._viewportHandler && window.visualViewport) {
2638
2713
  window.visualViewport.removeEventListener(
2639
2714
  "resize",
@@ -2746,15 +2821,47 @@ class AuroFloatingUI {
2746
2821
  } else if (this.configureTrial < MAX_CONFIGURATION_COUNT) {
2747
2822
  this.configureTrial += 1;
2748
2823
 
2749
- setTimeout(() => {
2824
+ // The id is retained so disconnect() can cancel it. A retry that fires
2825
+ // after teardown re-enters this method with isPopoverVisible still true
2826
+ // — nothing clears it on the teardown path — and takes the page scroll
2827
+ // lock back, with no owner left to release it.
2828
+ //
2829
+ // This is not the rare timing race it reads as: the lookup above wants
2830
+ // a `.container` inside the BIB's own shadow root, which only a bib
2831
+ // that is itself a custom element has. A consumer whose bib is a plain
2832
+ // element (auro-dialog's `#bib` is a div, with its `.container` a
2833
+ // sibling in the host's shadow root) can never satisfy it, so it
2834
+ // retries the full MAX_CONFIGURATION_COUNT every time. Correcting the
2835
+ // lookup would change bib sizing for those consumers and is tracked
2836
+ // separately; cancelling the timer is what keeps it from freezing the
2837
+ // page.
2838
+ this._configureRetryId = setTimeout(() => {
2839
+ this._configureRetryId = undefined;
2750
2840
  this.configureBibStrategy(value);
2751
2841
  }, 0);
2752
2842
  }
2753
2843
 
2754
2844
  if (element.isPopoverVisible) {
2755
- this.lockScroll(value === "fullscreen");
2845
+ // Positioning strategy is not scroll ownership. Both "fullscreen" and
2846
+ // "dialog" lay the bib over the page and trap focus inside it, so both
2847
+ // must freeze the page behind them (AB#1647843). Gating this on
2848
+ // "fullscreen" alone left dismissible desktop dialogs and drawers
2849
+ // scrollable, and actively released the lock on every reposition
2850
+ // (AB#1625424, AB#1625435). The nested "cover" strategy takes the else
2851
+ // branch and stays scrollable by design — it sits inside its parent
2852
+ // rather than over the page.
2853
+ this.lockScroll(true);
2756
2854
  }
2757
2855
  } else {
2856
+ // The mirror of the lock above: a strategy that does not own the page must
2857
+ // hand the lock back, not merely decline to take it. autoUpdate re-runs
2858
+ // this method on every resize tick, and getPositioningStrategy() flips
2859
+ // "fullscreen" -> "floating" across the breakpoint for dropdown behavior —
2860
+ // so an overlay that locked while narrow can land here while still open.
2861
+ // Without this, the page stays frozen behind a small floating bib until
2862
+ // hideBib() happens to run.
2863
+ this.lockScroll(false);
2864
+
2758
2865
  element.bib.style.position = "";
2759
2866
  element.bib.removeAttribute("isfullscreen");
2760
2867
  element.isBibFullscreen = false;
@@ -2864,6 +2971,46 @@ class AuroFloatingUI {
2864
2971
  this.hideBib("focusloss");
2865
2972
  }
2866
2973
 
2974
+ /**
2975
+ * @private
2976
+ * Installs the handlers that dismiss an open overlay.
2977
+ *
2978
+ * Shared by showBib(), which opens the overlay, and configure(), which has to
2979
+ * put these back after a rewire tore them down under an overlay that is still
2980
+ * open. A non-modal overlay dismisses on outside click, focus loss and
2981
+ * Escape; a modal one instead swallows Escape so the native dialog's
2982
+ * CloseWatcher never sees it (AB#1613688).
2983
+ */
2984
+ setupDismissHandlers() {
2985
+ const element = this.element;
2986
+ if (!element) {
2987
+ return;
2988
+ }
2989
+
2990
+ if (!element.modal) {
2991
+ this.setupHideHandlers();
2992
+ return;
2993
+ }
2994
+
2995
+ if (this.keyDownHandler) {
2996
+ document.removeEventListener("keydown", this.keyDownHandler);
2997
+ }
2998
+ this.keyDownHandler = (evt) => {
2999
+ if (evt.key === "Escape" && element.isPopoverVisible) {
3000
+ // Intercept at keydown so CloseWatcher never sees the keystroke.
3001
+ // Canceling `cancel` alone is insufficient — a second Esc with no
3002
+ // intervening user activation triggers the anti-trap and fires `close`
3003
+ // directly, bypassing any `cancel` preventDefault (AB#1613688).
3004
+ // stopImmediatePropagation is intentional: it prevents other document
3005
+ // keydown handlers (e.g. consumer code, auro-dialog) from also acting
3006
+ // on Escape while the modal owns the key.
3007
+ evt.preventDefault();
3008
+ evt.stopImmediatePropagation();
3009
+ }
3010
+ };
3011
+ document.addEventListener("keydown", this.keyDownHandler);
3012
+ }
3013
+
2867
3014
  setupHideHandlers() {
2868
3015
  const element = this.element;
2869
3016
  if (!element) {
@@ -3036,27 +3183,7 @@ class AuroFloatingUI {
3036
3183
 
3037
3184
  // prevent double showing: isPopovervisible gets first and showBib gets called later
3038
3185
  if (!this.showing) {
3039
- if (!element.modal) {
3040
- this.setupHideHandlers();
3041
- } else {
3042
- if (this.keyDownHandler) {
3043
- document.removeEventListener("keydown", this.keyDownHandler);
3044
- }
3045
- this.keyDownHandler = (evt) => {
3046
- if (evt.key === "Escape" && element.isPopoverVisible) {
3047
- // Intercept at keydown so CloseWatcher never sees the keystroke.
3048
- // Canceling `cancel` alone is insufficient — a second Esc with no
3049
- // intervening user activation triggers the anti-trap and fires `close`
3050
- // directly, bypassing any `cancel` preventDefault (AB#1613688).
3051
- // stopImmediatePropagation is intentional: it prevents other document
3052
- // keydown handlers (e.g. consumer code, auro-dialog) from also acting
3053
- // on Escape while the modal owns the key.
3054
- evt.preventDefault();
3055
- evt.stopImmediatePropagation();
3056
- }
3057
- };
3058
- document.addEventListener("keydown", this.keyDownHandler);
3059
- }
3186
+ this.setupDismissHandlers();
3060
3187
  this.showing = true;
3061
3188
  element.isPopoverVisible = true;
3062
3189
  this.position();
@@ -3284,7 +3411,7 @@ class AuroFloatingUI {
3284
3411
 
3285
3412
  this.id = element.getAttribute("id");
3286
3413
  if (!this.id) {
3287
- this.id = window.crypto.randomUUID();
3414
+ this.id = generateUUID();
3288
3415
  element.setAttribute("id", this.id);
3289
3416
  }
3290
3417
 
@@ -3296,7 +3423,35 @@ class AuroFloatingUI {
3296
3423
  this.enableKeyboardHandling = enableKeyboardHandling;
3297
3424
 
3298
3425
  this.eventPrefix = eventPrefix;
3299
- if (this.element !== elem) {
3426
+
3427
+ // A retained scroll lock's saved body styles were captured for whichever
3428
+ // element held it, so only a same-element rewire may keep it. An actual
3429
+ // element swap has to take the full teardown, or this instance is left
3430
+ // holding a lock that no longer describes the page it points at — with no
3431
+ // owner able to release it (AB#1647843).
3432
+ const rewiringSameElement = this.element === elem;
3433
+
3434
+ if (!rewiringSameElement) {
3435
+ // Tear down against the outgoing element, before this.element is
3436
+ // repointed. Everything disconnect() releases is element-scoped:
3437
+ // aria-modal on that element's inner dialog, the bib transform saved for
3438
+ // it, its autoUpdate loop and its trigger listeners. Repointing first
3439
+ // aimed all of it at the incoming element — the outgoing dialog kept
3440
+ // aria-modal="true" for good, its bib stayed transformed, and the
3441
+ // incoming bib had the outgoing element's saved transform written onto
3442
+ // it. The trigger gate below read the incoming element's trigger, which
3443
+ // is not assigned until later in this method, so on a swap teardown was
3444
+ // usually skipped outright and the page stayed locked with no owner.
3445
+ if (this.element?.trigger) {
3446
+ this.disconnect({ teardown: true });
3447
+ }
3448
+
3449
+ // disconnect() does not clear this.showing, and the rewire block at the
3450
+ // end of this method only runs for a same-element rewire. Left true, the
3451
+ // incoming element's first showBib() is silently dropped by its
3452
+ // `!this.showing` guard and the overlay never opens.
3453
+ this.showing = false;
3454
+
3300
3455
  this.element = elem;
3301
3456
  }
3302
3457
 
@@ -3309,8 +3464,10 @@ class AuroFloatingUI {
3309
3464
  this.behavior = element.behavior;
3310
3465
  }
3311
3466
 
3312
- if (element.trigger) {
3313
- this.disconnect();
3467
+ if (rewiringSameElement && element.trigger) {
3468
+ // Rewiring, not tearing down — an overlay that is open right now keeps
3469
+ // its page scroll lock across a trigger change.
3470
+ this.disconnect({ teardown: false });
3314
3471
  }
3315
3472
  element.trigger =
3316
3473
  element.triggerElement ||
@@ -3339,11 +3496,61 @@ class AuroFloatingUI {
3339
3496
  element.trigger.addEventListener("focus", this.handleEvent);
3340
3497
  element.trigger.addEventListener("blur", this.handleEvent);
3341
3498
  }
3499
+
3500
+ // disconnect() above stripped the dismissal handlers and killed the
3501
+ // autoUpdate loop; the trigger listeners re-registered here replace
3502
+ // neither. An overlay that is open right now would otherwise be left on
3503
+ // screen with outside-click, focus-loss and Escape all dead and its
3504
+ // position frozen — and, since the lock is deliberately retained, holding
3505
+ // the page with only its original trigger able to give it back.
3506
+ if (rewiringSameElement && this.showing) {
3507
+ this.setupDismissHandlers();
3508
+
3509
+ if (element.bib && (element.trigger || element.parentNode)) {
3510
+ element.cleanup = autoUpdate(
3511
+ element.trigger || element.parentNode,
3512
+ element.bib,
3513
+ () => {
3514
+ this.position();
3515
+ },
3516
+ );
3517
+ }
3518
+ }
3342
3519
  }
3343
3520
 
3344
- disconnect() {
3521
+ /**
3522
+ * Tears the floater's connections down.
3523
+ *
3524
+ * Two callers with different intent share this: a real teardown, which must
3525
+ * release the page scroll lock, and configure(), which re-runs it purely to
3526
+ * rewire a changed trigger and must leave an open overlay's lock alone.
3527
+ * @param {Object} [options] - Teardown options.
3528
+ * @param {Boolean} [options.teardown] - False when only rewiring; keeps the
3529
+ * page scroll lock held for an overlay that is still open.
3530
+ * @returns {void}
3531
+ */
3532
+ disconnect({ teardown = true } = {}) {
3345
3533
  this.cleanupHideHandlers();
3346
3534
 
3535
+ // Cancel any pending configureBibStrategy() retry. It would otherwise fire
3536
+ // a macrotask after the unlock below, re-lock the page, and leave it frozen
3537
+ // with the floater already gone. Cancelled on a rewire too: configure()
3538
+ // re-runs position() itself, so a stale retry has nothing to contribute.
3539
+ if (this._configureRetryId !== undefined) {
3540
+ clearTimeout(this._configureRetryId);
3541
+ this._configureRetryId = undefined;
3542
+ }
3543
+
3544
+ // Tearing down while the bib is still open would otherwise strand
3545
+ // body{position:fixed} and leave the page permanently unscrollable —
3546
+ // hideBib() is the only other unlock, and it never runs in this path.
3547
+ // Skipped when merely rewiring: configure() routes through here on every
3548
+ // triggerElement change, and releasing the lock there would unlock the page
3549
+ // behind an overlay that is still open (AB#1625424, AB#1625435).
3550
+ if (teardown) {
3551
+ this.lockScroll(false);
3552
+ }
3553
+
3347
3554
  const element = this.element;
3348
3555
  if (!element) {
3349
3556
  return;
@@ -3498,6 +3705,14 @@ class ComponentBase extends i$2 {
3498
3705
  this.isBibFullscreen = false;
3499
3706
  this.floater = new AuroFloatingUI(this, 'dialog');
3500
3707
 
3708
+ /**
3709
+ * @private
3710
+ * True once _teardown() has released the floater and nothing has rewired
3711
+ * it. Read by connectedCallback() to rewire a remounted dialog, and by
3712
+ * _teardown() to stay idempotent.
3713
+ */
3714
+ this._floaterTornDown = false;
3715
+
3501
3716
  const versioning = new AuroDependencyVersioning();
3502
3717
 
3503
3718
  /**
@@ -3558,10 +3773,12 @@ class ComponentBase extends i$2 {
3558
3773
 
3559
3774
  /**
3560
3775
  * DEPRECATED - use `close-button-appearance="inverse" instead.
3776
+ * @deprecated Use `close-button-appearance="inverse"` instead.
3561
3777
  */
3562
3778
  onDark: {
3563
3779
  type: Boolean,
3564
- reflect: true
3780
+ reflect: true,
3781
+ attribute: 'ondark'
3565
3782
  },
3566
3783
 
3567
3784
  /**
@@ -3579,7 +3796,8 @@ class ComponentBase extends i$2 {
3579
3796
  */
3580
3797
  isBibFullscreen: {
3581
3798
  type: Boolean,
3582
- reflect: true
3799
+ reflect: true,
3800
+ attribute: 'isbibfullscreen'
3583
3801
  },
3584
3802
 
3585
3803
  /**
@@ -3648,7 +3866,7 @@ class ComponentBase extends i$2 {
3648
3866
  slotWrapper.classList.remove("dialog-footer");
3649
3867
  }
3650
3868
 
3651
- this.floater.configure(this, this.floaterConfig.prefix);
3869
+ this._configureFloater();
3652
3870
 
3653
3871
  // Forward FloatingUI toggle event to backward-compatible 'toggle' event
3654
3872
  this.addEventListener('auroDialog-toggled', (event) => {
@@ -3657,9 +3875,15 @@ class ComponentBase extends i$2 {
3657
3875
  }
3658
3876
  });
3659
3877
 
3660
- // Always intercept the native ESC/cancel event so we can decide
3661
- // whether to honour it based on `modal`. Re-dispatch via FloatingUI
3662
- // for non-modal so the hide lifecycle runs correctly.
3878
+ // Defense-in-depth for the native ESC/cancel event. In practice neither
3879
+ // branch fires today: auro-library's floatingUI binds its own document-level
3880
+ // keydown handler, which closes a non-modal dialog via hideBib("keydown")
3881
+ // and, for a modal, swallows the keystroke with preventDefault() +
3882
+ // stopImmediatePropagation() before the platform CloseWatcher ever sees it
3883
+ // (AB#1613688). This listener is kept rather than deleted because it is the
3884
+ // component-side guarantee that a modal never self-closes on ESC: if the
3885
+ // library stops swallowing, the platform `cancel` would otherwise close a
3886
+ // modal dialog, which is exactly the behavior `modal` exists to prevent.
3663
3887
  this.dialog.addEventListener('cancel', (e) => {
3664
3888
  e.preventDefault();
3665
3889
  if (!this.modal) {
@@ -3706,12 +3930,78 @@ class ComponentBase extends i$2 {
3706
3930
  }
3707
3931
 
3708
3932
  if (changedProperties.has('triggerElement')) {
3709
- this.floater.configure(this, this.floaterConfig.prefix);
3933
+ this._configureFloater();
3934
+ }
3935
+ }
3936
+
3937
+ /**
3938
+ * @private
3939
+ * Wires the floater to this element and records that it is live, so a later
3940
+ * reconnect can tell a torn-down floater from one that never lost its wiring.
3941
+ * @returns {void}
3942
+ */
3943
+ _configureFloater() {
3944
+ this._floaterTornDown = false;
3945
+ this.floater.configure(this, this.floaterConfig.prefix);
3946
+ }
3947
+
3948
+ connectedCallback() {
3949
+ super.connectedCallback();
3950
+
3951
+ // Lit does not re-run firstUpdated() on reconnect, so without this a host
3952
+ // that removes and re-inserts the dialog across separate tasks — Vue
3953
+ // <keep-alive>, caching tab/accordion hosts, virtualized lists, or anything
3954
+ // that parks the node in a DocumentFragment in one task and inserts it in
3955
+ // the next — gets back a dead element: _teardown() dropped the trigger
3956
+ // listeners, ran element.cleanup(), tore down autoUpdate and released the
3957
+ // page scroll lock, and nothing rewired any of it.
3958
+ //
3959
+ // Only re-configure when a teardown actually ran. On the normal first
3960
+ // connect the flag is false and firstUpdated() owns the single configure
3961
+ // call; on a same-task DOM move the deferred teardown never runs, so the
3962
+ // flag stays false here too and the move is still a no-op.
3963
+ if (this.hasUpdated && this._floaterTornDown) {
3964
+ this._configureFloater();
3710
3965
  }
3711
3966
  }
3712
3967
 
3713
3968
  disconnectedCallback() {
3714
3969
  super.disconnectedCallback();
3970
+
3971
+ // A same-document DOM move (appendChild of an already-parented node) fires
3972
+ // disconnectedCallback and reconnects within the same task. Defer the
3973
+ // teardown one microtask and bail if the element came back, so a reparent
3974
+ // does not dismantle a dialog that is still open: floater.disconnect()
3975
+ // releases the page scroll lock and tears down autoUpdate, which would
3976
+ // leave the dialog visible over a scrollable page with nothing left to
3977
+ // re-lock it — the AB#1625424 symptom returning by a different route.
3978
+ //
3979
+ // A host that removes and re-inserts across tasks (not a move) is treated
3980
+ // as a real unmount, which is the correct reading of that sequence.
3981
+ queueMicrotask(() => {
3982
+ if (this.isConnected) {
3983
+ return;
3984
+ }
3985
+ this._teardown();
3986
+ });
3987
+ }
3988
+
3989
+ /**
3990
+ * @private
3991
+ * Releases everything the dialog owns outside its own shadow root.
3992
+ * Split out of disconnectedCallback so a DOM move can skip it.
3993
+ *
3994
+ * Idempotent: a repeated disconnect in the same task must not invoke
3995
+ * floater.disconnect() twice, and connectedCallback() reads the same flag to
3996
+ * decide whether the floater needs rewiring.
3997
+ * @returns {void}
3998
+ */
3999
+ _teardown() {
4000
+ if (this._floaterTornDown) {
4001
+ return;
4002
+ }
4003
+ this._floaterTornDown = true;
4004
+
3715
4005
  if (this._hidePopoverTimerId) {
3716
4006
  clearTimeout(this._hidePopoverTimerId);
3717
4007
  this._hidePopoverTimerId = undefined;