@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 +16 -0
- package/package.json +1 -1
- package/scripts/runtime/floatingUI.mjs +302 -163
- package/scripts/runtime/floatingUI.test.js +30 -0
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.
|
|
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 (
|
|
113
|
-
const sizerStyle = window.getComputedStyle(
|
|
114
|
-
const bibContent =
|
|
115
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
164
|
+
element.expanded = smallerThanBreakpoint;
|
|
151
165
|
}
|
|
152
|
-
if (
|
|
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(
|
|
190
|
-
...(
|
|
191
|
-
...(
|
|
192
|
-
...(
|
|
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(
|
|
197
|
-
strategy:
|
|
198
|
-
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
|
-
|
|
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(
|
|
241
|
+
computePosition(element.parentNode, element.bib, {
|
|
210
242
|
placement: "bottom-start",
|
|
211
243
|
}).then(({ x, y }) => {
|
|
212
244
|
// eslint-disable-line id-length
|
|
213
|
-
|
|
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 -
|
|
216
|
-
width: `${
|
|
217
|
-
height: `${
|
|
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
|
-
|
|
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
|
-
|
|
298
|
+
element.isBibFullscreen = true;
|
|
251
299
|
// reset the prev position
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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 (
|
|
324
|
+
if (element.isPopoverVisible) {
|
|
278
325
|
this.lockScroll(true);
|
|
279
326
|
}
|
|
280
327
|
} else {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
349
|
+
element.dispatchEvent(event);
|
|
303
350
|
}
|
|
304
351
|
}
|
|
305
352
|
|
|
306
353
|
updateState() {
|
|
307
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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 (
|
|
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 (
|
|
428
|
+
if (element.bib.hasAttribute("isfullscreen")) {
|
|
365
429
|
return;
|
|
366
430
|
}
|
|
367
431
|
|
|
368
432
|
if (
|
|
369
|
-
(!evt.composedPath().includes(
|
|
370
|
-
!evt.composedPath().includes(
|
|
371
|
-
(
|
|
372
|
-
evt.composedPath().includes(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
581
|
+
if (!element.modal) {
|
|
473
582
|
this.setupHideHandlers();
|
|
474
583
|
}
|
|
475
584
|
this.showing = true;
|
|
476
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
-
|
|
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 (
|
|
618
|
+
if (element.noToggle && eventType === "click") {
|
|
505
619
|
return;
|
|
506
620
|
}
|
|
507
621
|
|
|
508
622
|
this.lockScroll(false);
|
|
509
|
-
|
|
623
|
+
element.triggerChevron?.removeAttribute("data-expanded");
|
|
510
624
|
|
|
511
|
-
if (
|
|
512
|
-
|
|
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
|
-
|
|
662
|
+
element.dispatchEvent(event);
|
|
544
663
|
}
|
|
545
664
|
|
|
546
665
|
handleClick() {
|
|
547
|
-
|
|
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:
|
|
682
|
+
expanded: element.isPopoverVisible,
|
|
559
683
|
},
|
|
560
684
|
},
|
|
561
685
|
);
|
|
562
686
|
|
|
563
|
-
|
|
687
|
+
element.dispatchEvent(event);
|
|
564
688
|
}
|
|
565
689
|
|
|
566
690
|
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
|
-
}
|
|
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
|
-
|
|
613
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
803
|
+
element.setAttribute("id", this.id);
|
|
667
804
|
}
|
|
668
805
|
|
|
669
|
-
|
|
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
|
-
|
|
682
|
-
|
|
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 (
|
|
827
|
+
if (element.trigger) {
|
|
686
828
|
this.disconnect();
|
|
687
829
|
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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 (
|
|
699
|
-
|
|
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 (
|
|
847
|
+
if (element.trigger) {
|
|
707
848
|
if (this.enableKeyboardHandling) {
|
|
708
|
-
|
|
849
|
+
element.trigger.addEventListener("keydown", this.handleEvent);
|
|
709
850
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
724
|
-
|
|
725
|
-
|
|
862
|
+
const element = this.element;
|
|
863
|
+
if (!element) {
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
726
866
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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
|
});
|