@aurodesignsystem/auro-library 5.12.0 → 5.12.2

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