@aurodesignsystem/auro-library 5.12.0 → 5.12.1

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 CHANGED
@@ -1,5 +1,14 @@
1
1
  # Semantic Release Automated Changelog
2
2
 
3
+ ## [5.12.1](https://github.com/AlaskaAirlines/auro-library/compare/v5.12.0...v5.12.1) (2026-04-07)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * guard element access and add null-element safety test ([efd5f8d](https://github.com/AlaskaAirlines/auro-library/commit/efd5f8d978c18341d49a6908600dca0a5f94bfda))
9
+ * normalize null-element guards in getPositioningStrategy and setupHideHandlers ([d02617c](https://github.com/AlaskaAirlines/auro-library/commit/d02617c05998030f7c0ed15a8d207b3176191c58))
10
+ * tighten floatingUI null guard behavior ([e194e61](https://github.com/AlaskaAirlines/auro-library/commit/e194e61439ac9c49170c5145b75917fb301b9495))
11
+
3
12
  # [5.12.0](https://github.com/AlaskaAirlines/auro-library/compare/v5.11.3...v5.12.0) (2026-04-01)
4
13
 
5
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aurodesignsystem/auro-library",
3
- "version": "5.12.0",
3
+ "version": "5.12.1",
4
4
  "description": "This repository holds shared scripts, utilities, and workflows utilized across repositories along the Auro Design System.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -108,11 +108,19 @@ export default class AuroFloatingUI {
108
108
  * This ensures that the bib content has the same dimensions as the sizer element.
109
109
  */
110
110
  mirrorSize() {
111
+ const element = this.element;
112
+ if (!element) {
113
+ return;
114
+ }
115
+
111
116
  // mirror the boxsize from bibSizer
112
- if (this.element.bibSizer && this.element.matchWidth) {
113
- const sizerStyle = window.getComputedStyle(this.element.bibSizer);
114
- const bibContent =
115
- this.element.bib.shadowRoot.querySelector(".container");
117
+ if (element.bibSizer && element.matchWidth && element.bib?.shadowRoot) {
118
+ const sizerStyle = window.getComputedStyle(element.bibSizer);
119
+ const bibContent = element.bib.shadowRoot.querySelector(".container");
120
+ if (!bibContent) {
121
+ return;
122
+ }
123
+
116
124
  if (sizerStyle.width !== "0px") {
117
125
  bibContent.style.width = sizerStyle.width;
118
126
  }
@@ -134,9 +142,14 @@ export default class AuroFloatingUI {
134
142
  * @returns {String} The positioning strategy, one of 'fullscreen', 'floating', 'cover'.
135
143
  */
136
144
  getPositioningStrategy() {
145
+ const element = this.element;
146
+ if (!element) {
147
+ return "floating";
148
+ }
149
+
137
150
  const breakpoint =
138
- this.element.bib.mobileFullscreenBreakpoint ||
139
- this.element.floaterConfig?.fullscreenBreakpoint;
151
+ element.bib?.mobileFullscreenBreakpoint ||
152
+ element.floaterConfig?.fullscreenBreakpoint;
140
153
  switch (this.behavior) {
141
154
  case "tooltip":
142
155
  return "floating";
@@ -147,9 +160,9 @@ export default class AuroFloatingUI {
147
160
  `(max-width: ${breakpoint})`,
148
161
  ).matches;
149
162
 
150
- this.element.expanded = smallerThanBreakpoint;
163
+ element.expanded = smallerThanBreakpoint;
151
164
  }
152
- if (this.element.nested) {
165
+ if (element.nested) {
153
166
  return "cover";
154
167
  }
155
168
  return "fullscreen";
@@ -179,42 +192,65 @@ export default class AuroFloatingUI {
179
192
  * and applies the calculated position to the bib's style.
180
193
  */
181
194
  position() {
195
+ const element = this.element;
196
+ if (!element) {
197
+ return;
198
+ }
199
+
182
200
  const strategy = this.getPositioningStrategy();
183
201
  this.configureBibStrategy(strategy);
184
202
 
185
203
  if (strategy === "floating") {
204
+ if (!element.trigger || !element.bib) {
205
+ return;
206
+ }
207
+
186
208
  this.mirrorSize();
187
209
  // Define the middlware for the floater configuration
188
210
  const middleware = [
189
- offset(this.element.floaterConfig?.offset || 0),
190
- ...(this.element.floaterConfig?.shift ? [shift()] : []), // Add shift middleware if shift is enabled.
191
- ...(this.element.floaterConfig?.flip ? [flip()] : []), // Add flip middleware if flip is enabled.
192
- ...(this.element.floaterConfig?.autoPlacement ? [autoPlacement()] : []), // Add autoPlacement middleware if autoPlacement is enabled.
211
+ offset(element.floaterConfig?.offset || 0),
212
+ ...(element.floaterConfig?.shift ? [shift()] : []), // Add shift middleware if shift is enabled.
213
+ ...(element.floaterConfig?.flip ? [flip()] : []), // Add flip middleware if flip is enabled.
214
+ ...(element.floaterConfig?.autoPlacement ? [autoPlacement()] : []), // Add autoPlacement middleware if autoPlacement is enabled.
193
215
  ];
194
216
 
195
217
  // Compute the position of the bib
196
- computePosition(this.element.trigger, this.element.bib, {
197
- strategy: this.element.floaterConfig?.strategy || "fixed",
198
- placement: this.element.floaterConfig?.placement,
218
+ computePosition(element.trigger, element.bib, {
219
+ strategy: element.floaterConfig?.strategy || "fixed",
220
+ placement: element.floaterConfig?.placement,
199
221
  middleware: middleware || [],
200
222
  }).then(({ x, y }) => {
201
223
  // eslint-disable-line id-length
202
- Object.assign(this.element.bib.style, {
224
+ const currentElement = this.element;
225
+ if (!currentElement?.bib) {
226
+ return;
227
+ }
228
+
229
+ Object.assign(currentElement.bib.style, {
203
230
  left: `${x}px`,
204
231
  top: `${y}px`,
205
232
  });
206
233
  });
207
234
  } else if (strategy === "cover") {
235
+ if (!element.parentNode || !element.bib) {
236
+ return;
237
+ }
238
+
208
239
  // Compute the position of the bib
209
- computePosition(this.element.parentNode, this.element.bib, {
240
+ computePosition(element.parentNode, element.bib, {
210
241
  placement: "bottom-start",
211
242
  }).then(({ x, y }) => {
212
243
  // eslint-disable-line id-length
213
- Object.assign(this.element.bib.style, {
244
+ const currentElement = this.element;
245
+ if (!currentElement?.bib || !currentElement.parentNode) {
246
+ return;
247
+ }
248
+
249
+ Object.assign(currentElement.bib.style, {
214
250
  left: `${x}px`,
215
- top: `${y - this.element.parentNode.offsetHeight}px`,
216
- width: `${this.element.parentNode.offsetWidth}px`,
217
- height: `${this.element.parentNode.offsetHeight}px`,
251
+ top: `${y - currentElement.parentNode.offsetHeight}px`,
252
+ width: `${currentElement.parentNode.offsetWidth}px`,
253
+ height: `${currentElement.parentNode.offsetHeight}px`,
218
254
  });
219
255
  });
220
256
  }
@@ -226,11 +262,17 @@ export default class AuroFloatingUI {
226
262
  * @param {Boolean} lock - If true, locks the body's scrolling functionlity; otherwise, unlock.
227
263
  */
228
264
  lockScroll(lock = true) {
265
+ const element = this.element;
266
+
229
267
  if (lock) {
268
+ if (!element?.bib) {
269
+ return;
270
+ }
271
+
230
272
  document.body.style.overflow = "hidden"; // hide body's scrollbar
231
273
 
232
274
  // Move `bib` by the amount the viewport is shifted to stay aligned in fullscreen.
233
- this.element.bib.style.transform = `translateY(${window?.visualViewport?.offsetTop}px)`;
275
+ element.bib.style.transform = `translateY(${window?.visualViewport?.offsetTop}px)`;
234
276
  } else {
235
277
  document.body.style.overflow = "";
236
278
  }
@@ -246,20 +288,24 @@ export default class AuroFloatingUI {
246
288
  * @param {string} strategy - The positioning strategy ('fullscreen' or 'floating').
247
289
  */
248
290
  configureBibStrategy(value) {
291
+ const element = this.element;
292
+ if (!element?.bib) {
293
+ return;
294
+ }
295
+
249
296
  if (value === "fullscreen") {
250
- this.element.isBibFullscreen = true;
297
+ element.isBibFullscreen = true;
251
298
  // reset the prev position
252
- this.element.bib.setAttribute("isfullscreen", "");
253
- this.element.bib.style.position = "fixed";
254
- this.element.bib.style.top = "0px";
255
- this.element.bib.style.left = "0px";
256
- this.element.bib.style.width = "";
257
- this.element.bib.style.height = "";
258
- this.element.style.contain = "";
299
+ element.bib.setAttribute("isfullscreen", "");
300
+ element.bib.style.position = "fixed";
301
+ element.bib.style.top = "0px";
302
+ element.bib.style.left = "0px";
303
+ element.bib.style.width = "";
304
+ element.bib.style.height = "";
305
+ element.style.contain = "";
259
306
 
260
307
  // reset the size that was mirroring `size` css-part
261
- const bibContent =
262
- this.element.bib.shadowRoot.querySelector(".container");
308
+ const bibContent = element.bib.shadowRoot?.querySelector(".container");
263
309
  if (bibContent) {
264
310
  bibContent.style.width = "";
265
311
  bibContent.style.height = "";
@@ -274,14 +320,14 @@ export default class AuroFloatingUI {
274
320
  }, 0);
275
321
  }
276
322
 
277
- if (this.element.isPopoverVisible) {
323
+ if (element.isPopoverVisible) {
278
324
  this.lockScroll(true);
279
325
  }
280
326
  } else {
281
- this.element.bib.style.position = "";
282
- this.element.bib.removeAttribute("isfullscreen");
283
- this.element.isBibFullscreen = false;
284
- this.element.style.contain = "layout";
327
+ element.bib.style.position = "";
328
+ element.bib.removeAttribute("isfullscreen");
329
+ element.isBibFullscreen = false;
330
+ element.style.contain = "layout";
285
331
  }
286
332
 
287
333
  const isChanged = this.strategy && this.strategy !== value;
@@ -299,16 +345,21 @@ export default class AuroFloatingUI {
299
345
  },
300
346
  );
301
347
 
302
- this.element.dispatchEvent(event);
348
+ element.dispatchEvent(event);
303
349
  }
304
350
  }
305
351
 
306
352
  updateState() {
307
- const isVisible = this.element.isPopoverVisible;
353
+ const element = this.element;
354
+ if (!element) {
355
+ return;
356
+ }
357
+
358
+ const isVisible = element.isPopoverVisible;
308
359
  if (!isVisible) {
309
360
  this.cleanupHideHandlers();
310
361
  try {
311
- this.element.cleanup?.();
362
+ element.cleanup?.();
312
363
  } catch (error) {
313
364
  // Do nothing
314
365
  }
@@ -324,28 +375,30 @@ export default class AuroFloatingUI {
324
375
  * If not, and if the bib isn't in fullscreen mode with focus lost, it hides the bib.
325
376
  */
326
377
  handleFocusLoss() {
378
+ const element = this.element;
379
+ if (!element?.bib) {
380
+ return;
381
+ }
382
+
327
383
  // if mouse is being pressed, skip and let click event to handle the action
328
384
  if (AuroFloatingUI.isMousePressed) {
329
385
  return;
330
386
  }
331
387
 
332
388
  if (
333
- this.element.noHideOnThisFocusLoss ||
334
- this.element.hasAttribute("noHideOnThisFocusLoss")
389
+ element.noHideOnThisFocusLoss ||
390
+ element.hasAttribute("noHideOnThisFocusLoss")
335
391
  ) {
336
392
  return;
337
393
  }
338
394
 
339
395
  // if focus is still inside of trigger or bib, do not close
340
- if (
341
- this.element.matches(":focus") ||
342
- this.element.matches(":focus-within")
343
- ) {
396
+ if (element.matches(":focus") || element.matches(":focus-within")) {
344
397
  return;
345
398
  }
346
399
 
347
400
  // if fullscreen bib is in fullscreen mode, do not close
348
- if (this.element.bib.hasAttribute("isfullscreen")) {
401
+ if (element.bib.hasAttribute("isfullscreen")) {
349
402
  return;
350
403
  }
351
404
 
@@ -353,23 +406,33 @@ export default class AuroFloatingUI {
353
406
  }
354
407
 
355
408
  setupHideHandlers() {
409
+ const element = this.element;
410
+ if (!element) {
411
+ return;
412
+ }
413
+
356
414
  // Define handlers & store references
357
415
  this.focusHandler = () => this.handleFocusLoss();
358
416
 
359
417
  this.clickHandler = (evt) => {
418
+ const element = this.element;
419
+ if (!element?.bib) {
420
+ return;
421
+ }
422
+
360
423
  // When the bib is fullscreen (modal dialog), don't close on outside
361
424
  // clicks. VoiceOver's synthetic click events inside a top-layer modal
362
425
  // <dialog> may not include the bib in composedPath(), causing false
363
426
  // positives. This mirrors the fullscreen guard in handleFocusLoss().
364
- if (this.element.bib && this.element.bib.hasAttribute("isfullscreen")) {
427
+ if (element.bib.hasAttribute("isfullscreen")) {
365
428
  return;
366
429
  }
367
430
 
368
431
  if (
369
- (!evt.composedPath().includes(this.element.trigger) &&
370
- !evt.composedPath().includes(this.element.bib)) ||
371
- (this.element.bib.backdrop &&
372
- evt.composedPath().includes(this.element.bib.backdrop))
432
+ (!evt.composedPath().includes(element.trigger) &&
433
+ !evt.composedPath().includes(element.bib)) ||
434
+ (element.bib.backdrop &&
435
+ evt.composedPath().includes(element.bib.backdrop))
373
436
  ) {
374
437
  const existedVisibleFloatingUI =
375
438
  document.expandedAuroFormkitDropdown || document.expandedAuroFloater;
@@ -390,7 +453,12 @@ export default class AuroFloatingUI {
390
453
 
391
454
  // ESC key handler
392
455
  this.keyDownHandler = (evt) => {
393
- if (evt.key === "Escape" && this.element.isPopoverVisible) {
456
+ const element = this.element;
457
+ if (!element) {
458
+ return;
459
+ }
460
+
461
+ if (evt.key === "Escape" && element.isPopoverVisible) {
394
462
  const existedVisibleFloatingUI =
395
463
  document.expandedAuroFormkitDropdown || document.expandedAuroFloater;
396
464
  if (
@@ -447,6 +515,10 @@ export default class AuroFloatingUI {
447
515
  }
448
516
 
449
517
  updateCurrentExpandedDropdown() {
518
+ if (!this.element) {
519
+ return;
520
+ }
521
+
450
522
  // Close any other dropdown that is already open
451
523
  const existedVisibleFloatingUI =
452
524
  document.expandedAuroFormkitDropdown || document.expandedAuroFloater;
@@ -463,25 +535,34 @@ export default class AuroFloatingUI {
463
535
  }
464
536
 
465
537
  showBib() {
466
- if (!this.element.disabled && !this.showing) {
538
+ const element = this.element;
539
+ if (!element) {
540
+ return;
541
+ }
542
+
543
+ if (!element.bib || (!element.trigger && !element.parentNode)) {
544
+ return;
545
+ }
546
+
547
+ if (!element.disabled && !this.showing) {
467
548
  this.updateCurrentExpandedDropdown();
468
- this.element.triggerChevron?.setAttribute("data-expanded", true);
549
+ element.triggerChevron?.setAttribute("data-expanded", true);
469
550
 
470
551
  // prevent double showing: isPopovervisible gets first and showBib gets called later
471
552
  if (!this.showing) {
472
- if (!this.element.modal) {
553
+ if (!element.modal) {
473
554
  this.setupHideHandlers();
474
555
  }
475
556
  this.showing = true;
476
- this.element.isPopoverVisible = true;
557
+ element.isPopoverVisible = true;
477
558
  this.position();
478
559
  this.dispatchEventDropdownToggle();
479
560
  }
480
561
 
481
562
  // Setup auto update to handle resize and scroll
482
- this.element.cleanup = autoUpdate(
483
- this.element.trigger || this.element.parentNode,
484
- this.element.bib,
563
+ element.cleanup = autoUpdate(
564
+ element.trigger || element.parentNode,
565
+ element.bib,
485
566
  () => {
486
567
  this.position();
487
568
  },
@@ -494,22 +575,27 @@ export default class AuroFloatingUI {
494
575
  * @param {String} eventType - The event type that triggered the hiding action.
495
576
  */
496
577
  hideBib(eventType = "unknown") {
497
- if (this.element.disabled) {
578
+ const element = this.element;
579
+ if (!element) {
580
+ return;
581
+ }
582
+
583
+ if (element.disabled) {
498
584
  return;
499
585
  }
500
586
 
501
587
  // noToggle dropdowns should not close when the trigger is clicked (the
502
588
  // "toggle" behavior), but they CAN still close via other interactions like
503
589
  // Escape key or focus loss.
504
- if (this.element.noToggle && eventType === "click") {
590
+ if (element.noToggle && eventType === "click") {
505
591
  return;
506
592
  }
507
593
 
508
594
  this.lockScroll(false);
509
- this.element.triggerChevron?.removeAttribute("data-expanded");
595
+ element.triggerChevron?.removeAttribute("data-expanded");
510
596
 
511
- if (this.element.isPopoverVisible) {
512
- this.element.isPopoverVisible = false;
597
+ if (element.isPopoverVisible) {
598
+ element.isPopoverVisible = false;
513
599
  }
514
600
  if (this.showing) {
515
601
  this.cleanupHideHandlers();
@@ -529,6 +615,11 @@ export default class AuroFloatingUI {
529
615
  * @param {String} eventType - The event type that triggered the toggle action.
530
616
  */
531
617
  dispatchEventDropdownToggle(eventType) {
618
+ const element = this.element;
619
+ if (!element) {
620
+ return;
621
+ }
622
+
532
623
  const event = new CustomEvent(
533
624
  this.eventPrefix ? `${this.eventPrefix}-toggled` : "toggled",
534
625
  {
@@ -540,11 +631,16 @@ export default class AuroFloatingUI {
540
631
  },
541
632
  );
542
633
 
543
- this.element.dispatchEvent(event);
634
+ element.dispatchEvent(event);
544
635
  }
545
636
 
546
637
  handleClick() {
547
- if (this.element.isPopoverVisible) {
638
+ const element = this.element;
639
+ if (!element) {
640
+ return;
641
+ }
642
+
643
+ if (element.isPopoverVisible) {
548
644
  this.hideBib("click");
549
645
  } else {
550
646
  this.showBib();
@@ -555,64 +651,67 @@ export default class AuroFloatingUI {
555
651
  {
556
652
  composed: true,
557
653
  detail: {
558
- expanded: this.element.isPopoverVisible,
654
+ expanded: element.isPopoverVisible,
559
655
  },
560
656
  },
561
657
  );
562
658
 
563
- this.element.dispatchEvent(event);
659
+ element.dispatchEvent(event);
564
660
  }
565
661
 
566
662
  handleEvent(event) {
567
- if (!this.element.disableEventShow) {
568
- switch (event.type) {
569
- case "keydown": {
570
- // Support both Enter and Space keys for accessibility
571
- // Space is included as it's expected behavior for interactive elements
572
-
573
- const origin = event.composedPath()[0];
574
- if (
575
- event.key === "Enter" ||
576
- (event.key === " " && (!origin || origin.tagName !== "INPUT"))
577
- ) {
578
- event.preventDefault();
579
- this.handleClick();
580
- }
581
- break;
582
- }
583
- case "mouseenter":
584
- if (this.element.hoverToggle) {
585
- this.showBib();
586
- }
587
- break;
588
- case "mouseleave":
589
- if (this.element.hoverToggle) {
590
- this.hideBib("mouseleave");
591
- }
592
- break;
593
- case "focus":
594
- if (this.element.focusShow) {
595
- /*
596
- This needs to better handle clicking that gives focus -
597
- currently it shows and then immediately hides the bib
598
- */
599
- this.showBib();
600
- }
601
- break;
602
- case "blur":
603
- // send this task 100ms later queue to
604
- // wait a frame in case focus moves within the floating element/bib
605
- setTimeout(() => this.handleFocusLoss(), 0);
606
- break;
607
- case "click":
608
- if (document.activeElement === document.body) {
609
- event.currentTarget.focus();
610
- }
663
+ const element = this.element;
664
+ if (!element || element.disableEventShow) {
665
+ return;
666
+ }
667
+
668
+ switch (event.type) {
669
+ case "keydown": {
670
+ // Support both Enter and Space keys for accessibility
671
+ // Space is included as it's expected behavior for interactive elements
672
+
673
+ const origin = event.composedPath()[0];
674
+ if (
675
+ event.key === "Enter" ||
676
+ (event.key === " " && (!origin || origin.tagName !== "INPUT"))
677
+ ) {
678
+ event.preventDefault();
611
679
  this.handleClick();
612
- break;
613
- default:
614
- // Do nothing
680
+ }
681
+ break;
615
682
  }
683
+ case "mouseenter":
684
+ if (element.hoverToggle) {
685
+ this.showBib();
686
+ }
687
+ break;
688
+ case "mouseleave":
689
+ if (element.hoverToggle) {
690
+ this.hideBib("mouseleave");
691
+ }
692
+ break;
693
+ case "focus":
694
+ if (element.focusShow) {
695
+ /*
696
+ This needs to better handle clicking that gives focus -
697
+ currently it shows and then immediately hides the bib
698
+ */
699
+ this.showBib();
700
+ }
701
+ break;
702
+ case "blur":
703
+ // send this task 100ms later queue to
704
+ // wait a frame in case focus moves within the floating element/bib
705
+ setTimeout(() => this.handleFocusLoss(), 0);
706
+ break;
707
+ case "click":
708
+ if (document.activeElement === document.body) {
709
+ event.currentTarget.focus();
710
+ }
711
+ this.handleClick();
712
+ break;
713
+ default:
714
+ // Do nothing
616
715
  }
617
716
  }
618
717
 
@@ -623,6 +722,11 @@ export default class AuroFloatingUI {
623
722
  * This prevents the component itself from being focusable when the trigger element already handles focus.
624
723
  */
625
724
  handleTriggerTabIndex() {
725
+ const element = this.element;
726
+ if (!element) {
727
+ return;
728
+ }
729
+
626
730
  const focusableElementSelectors = [
627
731
  "a",
628
732
  "button",
@@ -635,7 +739,7 @@ export default class AuroFloatingUI {
635
739
  "auro-hyperlink",
636
740
  ];
637
741
 
638
- const triggerNode = this.element.querySelectorAll('[slot="trigger"]')[0];
742
+ const triggerNode = element.querySelectorAll('[slot="trigger"]')[0];
639
743
  if (!triggerNode) {
640
744
  return;
641
745
  }
@@ -644,13 +748,13 @@ export default class AuroFloatingUI {
644
748
  focusableElementSelectors.forEach((selector) => {
645
749
  // Check if the trigger node element is focusable
646
750
  if (triggerNodeTagName === selector) {
647
- this.element.tabIndex = -1;
751
+ element.tabIndex = -1;
648
752
  return;
649
753
  }
650
754
 
651
755
  // Check if any child is focusable
652
756
  if (triggerNode.querySelector(selector)) {
653
- this.element.tabIndex = -1;
757
+ element.tabIndex = -1;
654
758
  }
655
759
  });
656
760
  }
@@ -660,13 +764,18 @@ export default class AuroFloatingUI {
660
764
  * @param {*} eventPrefix
661
765
  */
662
766
  regenerateBibId() {
663
- this.id = this.element.getAttribute("id");
767
+ const element = this.element;
768
+ if (!element) {
769
+ return;
770
+ }
771
+
772
+ this.id = element.getAttribute("id");
664
773
  if (!this.id) {
665
774
  this.id = window.crypto.randomUUID();
666
- this.element.setAttribute("id", this.id);
775
+ element.setAttribute("id", this.id);
667
776
  }
668
777
 
669
- this.element.bib.setAttribute("id", `${this.id}-floater-bib`);
778
+ element.bib?.setAttribute("id", `${this.id}-floater-bib`);
670
779
  }
671
780
 
672
781
  configure(elem, eventPrefix, enableKeyboardHandling = true) {
@@ -678,67 +787,69 @@ export default class AuroFloatingUI {
678
787
  this.element = elem;
679
788
  }
680
789
 
681
- if (this.behavior !== this.element.behavior) {
682
- this.behavior = this.element.behavior;
790
+ const element = this.element;
791
+ if (!element) {
792
+ return;
793
+ }
794
+
795
+ if (this.behavior !== element.behavior) {
796
+ this.behavior = element.behavior;
683
797
  }
684
798
 
685
- if (this.element.trigger) {
799
+ if (element.trigger) {
686
800
  this.disconnect();
687
801
  }
688
- this.element.trigger =
689
- this.element.triggerElement ||
690
- this.element.shadowRoot.querySelector("#trigger") ||
691
- this.element.trigger;
692
- this.element.bib =
693
- this.element.shadowRoot.querySelector("#bib") || this.element.bib;
694
- this.element.bibSizer = this.element.shadowRoot.querySelector("#bibSizer");
695
- this.element.triggerChevron =
696
- this.element.shadowRoot.querySelector("#showStateIcon");
802
+ element.trigger =
803
+ element.triggerElement ||
804
+ element.shadowRoot?.querySelector("#trigger") ||
805
+ element.trigger;
806
+ element.bib = element.shadowRoot?.querySelector("#bib") || element.bib;
807
+ element.bibSizer = element.shadowRoot?.querySelector("#bibSizer");
808
+ element.triggerChevron =
809
+ element.shadowRoot?.querySelector("#showStateIcon");
697
810
 
698
- if (this.element.floaterConfig) {
699
- this.element.hoverToggle = this.element.floaterConfig.hoverToggle;
811
+ if (element.floaterConfig) {
812
+ element.hoverToggle = element.floaterConfig.hoverToggle;
700
813
  }
701
814
 
702
815
  this.regenerateBibId();
703
816
  this.handleTriggerTabIndex();
704
817
 
705
818
  this.handleEvent = this.handleEvent.bind(this);
706
- if (this.element.trigger) {
819
+ if (element.trigger) {
707
820
  if (this.enableKeyboardHandling) {
708
- this.element.trigger.addEventListener("keydown", this.handleEvent);
821
+ element.trigger.addEventListener("keydown", this.handleEvent);
709
822
  }
710
- this.element.trigger.addEventListener("click", this.handleEvent);
711
- this.element.trigger.addEventListener("mouseenter", this.handleEvent);
712
- this.element.trigger.addEventListener("mouseleave", this.handleEvent);
713
- this.element.trigger.addEventListener("focus", this.handleEvent);
714
- this.element.trigger.addEventListener("blur", this.handleEvent);
823
+ element.trigger.addEventListener("click", this.handleEvent);
824
+ element.trigger.addEventListener("mouseenter", this.handleEvent);
825
+ element.trigger.addEventListener("mouseleave", this.handleEvent);
826
+ element.trigger.addEventListener("focus", this.handleEvent);
827
+ element.trigger.addEventListener("blur", this.handleEvent);
715
828
  }
716
829
  }
717
830
 
718
831
  disconnect() {
719
832
  this.cleanupHideHandlers();
720
- if (this.element) {
721
- this.element.cleanup?.();
722
833
 
723
- if (this.element.bib) {
724
- this.element.shadowRoot.append(this.element.bib);
725
- }
834
+ const element = this.element;
835
+ if (!element) {
836
+ return;
837
+ }
726
838
 
727
- // Remove event & keyboard listeners
728
- if (this.element?.trigger) {
729
- this.element.trigger.removeEventListener("keydown", this.handleEvent);
730
- this.element.trigger.removeEventListener("click", this.handleEvent);
731
- this.element.trigger.removeEventListener(
732
- "mouseenter",
733
- this.handleEvent,
734
- );
735
- this.element.trigger.removeEventListener(
736
- "mouseleave",
737
- this.handleEvent,
738
- );
739
- this.element.trigger.removeEventListener("focus", this.handleEvent);
740
- this.element.trigger.removeEventListener("blur", this.handleEvent);
741
- }
839
+ element.cleanup?.();
840
+
841
+ if (element.bib && element.shadowRoot) {
842
+ element.shadowRoot.append(element.bib);
843
+ }
844
+
845
+ // Remove event & keyboard listeners
846
+ if (element.trigger) {
847
+ element.trigger.removeEventListener("keydown", this.handleEvent);
848
+ element.trigger.removeEventListener("click", this.handleEvent);
849
+ element.trigger.removeEventListener("mouseenter", this.handleEvent);
850
+ element.trigger.removeEventListener("mouseleave", this.handleEvent);
851
+ element.trigger.removeEventListener("focus", this.handleEvent);
852
+ element.trigger.removeEventListener("blur", this.handleEvent);
742
853
  }
743
854
  }
744
855
  }
@@ -99,4 +99,34 @@ describe("AuroFloatingUI", () => {
99
99
  expect(checkedSelectors).to.deep.equal([":focus", ":focus-within"]);
100
100
  expect(hideBibSpy.calledOnceWithExactly("keydown")).to.be.true;
101
101
  });
102
+
103
+ it("no-ops safely when element is not set", () => {
104
+ floatingUI.element = null;
105
+
106
+ expect(() => floatingUI.showBib()).to.not.throw();
107
+ expect(() => floatingUI.hideBib()).to.not.throw();
108
+ expect(() => floatingUI.handleClick()).to.not.throw();
109
+ expect(() => floatingUI.handleEvent(new Event("click"))).to.not.throw();
110
+ expect(() => floatingUI.handleFocusLoss()).to.not.throw();
111
+ expect(() => floatingUI.updateState()).to.not.throw();
112
+ expect(() => floatingUI.configureBibStrategy("floating")).to.not.throw();
113
+ expect(() => floatingUI.position()).to.not.throw();
114
+ });
115
+
116
+ it("does not enter a visible state when required DOM nodes are missing", () => {
117
+ host.bib = null;
118
+ host.isPopoverVisible = false;
119
+
120
+ floatingUI.showBib();
121
+
122
+ expect(floatingUI.showing).to.equal(false);
123
+ expect(host.isPopoverVisible).to.equal(false);
124
+ expect(document.expandedAuroFloater).to.not.equal(floatingUI);
125
+ });
126
+
127
+ it("returns an explicit positioning strategy when element is not set", () => {
128
+ floatingUI.element = null;
129
+
130
+ expect(floatingUI.getPositioningStrategy()).to.equal("floating");
131
+ });
102
132
  });