@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 +9 -0
- package/package.json +1 -1
- package/scripts/runtime/floatingUI.mjs +274 -163
- package/scripts/runtime/floatingUI.test.js +30 -0
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.
|
|
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 (
|
|
113
|
-
const sizerStyle = window.getComputedStyle(
|
|
114
|
-
const bibContent =
|
|
115
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
163
|
+
element.expanded = smallerThanBreakpoint;
|
|
151
164
|
}
|
|
152
|
-
if (
|
|
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(
|
|
190
|
-
...(
|
|
191
|
-
...(
|
|
192
|
-
...(
|
|
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(
|
|
197
|
-
strategy:
|
|
198
|
-
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
|
-
|
|
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(
|
|
240
|
+
computePosition(element.parentNode, element.bib, {
|
|
210
241
|
placement: "bottom-start",
|
|
211
242
|
}).then(({ x, y }) => {
|
|
212
243
|
// eslint-disable-line id-length
|
|
213
|
-
|
|
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 -
|
|
216
|
-
width: `${
|
|
217
|
-
height: `${
|
|
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
|
-
|
|
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
|
-
|
|
297
|
+
element.isBibFullscreen = true;
|
|
251
298
|
// reset the prev position
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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 (
|
|
323
|
+
if (element.isPopoverVisible) {
|
|
278
324
|
this.lockScroll(true);
|
|
279
325
|
}
|
|
280
326
|
} else {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
348
|
+
element.dispatchEvent(event);
|
|
303
349
|
}
|
|
304
350
|
}
|
|
305
351
|
|
|
306
352
|
updateState() {
|
|
307
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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 (
|
|
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 (
|
|
427
|
+
if (element.bib.hasAttribute("isfullscreen")) {
|
|
365
428
|
return;
|
|
366
429
|
}
|
|
367
430
|
|
|
368
431
|
if (
|
|
369
|
-
(!evt.composedPath().includes(
|
|
370
|
-
!evt.composedPath().includes(
|
|
371
|
-
(
|
|
372
|
-
evt.composedPath().includes(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
553
|
+
if (!element.modal) {
|
|
473
554
|
this.setupHideHandlers();
|
|
474
555
|
}
|
|
475
556
|
this.showing = true;
|
|
476
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
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 (
|
|
590
|
+
if (element.noToggle && eventType === "click") {
|
|
505
591
|
return;
|
|
506
592
|
}
|
|
507
593
|
|
|
508
594
|
this.lockScroll(false);
|
|
509
|
-
|
|
595
|
+
element.triggerChevron?.removeAttribute("data-expanded");
|
|
510
596
|
|
|
511
|
-
if (
|
|
512
|
-
|
|
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
|
-
|
|
634
|
+
element.dispatchEvent(event);
|
|
544
635
|
}
|
|
545
636
|
|
|
546
637
|
handleClick() {
|
|
547
|
-
|
|
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:
|
|
654
|
+
expanded: element.isPopoverVisible,
|
|
559
655
|
},
|
|
560
656
|
},
|
|
561
657
|
);
|
|
562
658
|
|
|
563
|
-
|
|
659
|
+
element.dispatchEvent(event);
|
|
564
660
|
}
|
|
565
661
|
|
|
566
662
|
handleEvent(event) {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
613
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
775
|
+
element.setAttribute("id", this.id);
|
|
667
776
|
}
|
|
668
777
|
|
|
669
|
-
|
|
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
|
-
|
|
682
|
-
|
|
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 (
|
|
799
|
+
if (element.trigger) {
|
|
686
800
|
this.disconnect();
|
|
687
801
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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 (
|
|
699
|
-
|
|
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 (
|
|
819
|
+
if (element.trigger) {
|
|
707
820
|
if (this.enableKeyboardHandling) {
|
|
708
|
-
|
|
821
|
+
element.trigger.addEventListener("keydown", this.handleEvent);
|
|
709
822
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
834
|
+
const element = this.element;
|
|
835
|
+
if (!element) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
726
838
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
});
|