@aurodesignsystem/auro-library 3.0.13 → 4.0.0

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,22 @@
1
1
  # Semantic Release Automated Changelog
2
2
 
3
+ # [4.0.0](https://github.com/AlaskaAirlines/auro-library/compare/v3.0.13...v4.0.0) (2025-03-24)
4
+
5
+
6
+ ### Features
7
+
8
+ * add drawer behavior to floatingUI ([55e6d30](https://github.com/AlaskaAirlines/auro-library/commit/55e6d30df013193174af6b865e023bb14eaa6dd5))
9
+
10
+
11
+ ### Performance Improvements
12
+
13
+ * update randomize id logic to use crypto ([0ac1a85](https://github.com/AlaskaAirlines/auro-library/commit/0ac1a85a6ecdc90b68d2bc79238a9410bd00613d))
14
+
15
+
16
+ ### BREAKING CHANGES
17
+
18
+ * `data-show` attribute on bib won't be set by floatingUI
19
+
3
20
  ## [3.0.13](https://github.com/AlaskaAirlines/auro-library/compare/v3.0.12...v3.0.13) (2025-03-19)
4
21
 
5
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aurodesignsystem/auro-library",
3
- "version": "3.0.13",
3
+ "version": "4.0.0",
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",
@@ -2,17 +2,43 @@
2
2
 
3
3
  import { autoUpdate, computePosition, offset, autoPlacement, flip } from '@floating-ui/dom';
4
4
 
5
+
6
+ const MAX_CONFIGURATION_COUNT = 10;
7
+
5
8
  export default class AuroFloatingUI {
6
- constructor() {
9
+ constructor(element, behavior) {
10
+ this.element = element;
11
+ this.behavior = behavior;
12
+
7
13
  // Store event listener references for cleanup
8
14
  this.focusHandler = null;
9
15
  this.clickHandler = null;
10
16
  this.keyDownHandler = null;
11
-
17
+
18
+ /**
19
+ * @private
20
+ */
21
+ this.configureTrial = 0;
22
+
12
23
  /**
13
24
  * @private
14
25
  */
15
26
  this.eventPrefix = undefined;
27
+
28
+ /**
29
+ * @private
30
+ */
31
+ this.id = undefined;
32
+
33
+ /**
34
+ * @private
35
+ */
36
+ this.showing = false;
37
+
38
+ /**
39
+ * @private
40
+ */
41
+ this.strategy = undefined;
16
42
  }
17
43
 
18
44
  /**
@@ -40,29 +66,48 @@ export default class AuroFloatingUI {
40
66
  * @private
41
67
  * Determines the positioning strategy based on the current viewport size and mobile breakpoint.
42
68
  *
43
- * This method checks if the current viewport width is less than or equal to the specified mobile fullscreen breakpoint
69
+ * This method checks if the current viewport width is less than or equal to the specified mobile fullscreen breakpoint
44
70
  * defined in the bib element. If it is, the strategy is set to 'fullscreen'; otherwise, it defaults to 'floating'.
45
71
  *
46
- * @returns {String} The positioning strategy, either 'fullscreen' or 'floating'.
72
+ * @returns {String} The positioning strategy, one of 'fullscreen', 'floating', 'cover'.
47
73
  */
48
74
  getPositioningStrategy() {
49
- let strategy = 'floating';
50
- if (this.element.bib.mobileFullscreenBreakpoint) {
51
- const smallerThanBreakpoint = window.matchMedia(`(max-width: ${this.element.bib.mobileFullscreenBreakpoint})`).matches;
52
- if (smallerThanBreakpoint) {
53
- strategy = 'fullscreen';
54
- }
75
+ const breakpoint = this.element.bib.mobileFullscreenBreakpoint || this.element.floaterConfig?.fullscreenBreakpoint;
76
+ switch (this.behavior) {
77
+ case "tooltip":
78
+ return "floating";
79
+ case "dialog":
80
+ case "drawer":
81
+ if (breakpoint) {
82
+ const smallerThanBreakpoint = window.matchMedia(`(max-width: ${breakpoint})`).matches;
83
+
84
+ this.element.expanded = smallerThanBreakpoint;
85
+ }
86
+ if (this.element.nested) {
87
+ return "cover";
88
+ }
89
+ return 'fullscreen';
90
+ case "dropdown":
91
+ case undefined:
92
+ case null:
93
+ if (breakpoint) {
94
+ const smallerThanBreakpoint = window.matchMedia(`(max-width: ${breakpoint})`).matches;
95
+ if (smallerThanBreakpoint) {
96
+ return 'fullscreen';
97
+ }
98
+ }
99
+ return "floating";
100
+ default:
101
+ return this.behavior;
55
102
  }
56
-
57
- return strategy;
58
103
  }
59
104
 
60
105
  /**
61
106
  * @private
62
107
  * Positions the bib element based on the current configuration and positioning strategy.
63
108
  *
64
- * This method determines the appropriate positioning strategy (fullscreen or not) and configures the bib accordingly.
65
- * It also sets up middleware for the floater configuration, computes the position of the bib relative to the trigger element,
109
+ * This method determines the appropriate positioning strategy (fullscreen or not) and configures the bib accordingly.
110
+ * It also sets up middleware for the floater configuration, computes the position of the bib relative to the trigger element,
66
111
  * and applies the calculated position to the bib's style.
67
112
  */
68
113
  position() {
@@ -73,21 +118,33 @@ export default class AuroFloatingUI {
73
118
  this.mirrorSize();
74
119
  // Define the middlware for the floater configuration
75
120
  const middleware = [
76
- offset(this.element.floaterConfig.offset || 0),
77
- ...(this.element.floaterConfig.flip ? [flip()] : []), // Add flip middleware if flip is enabled
78
- ...(this.element.floaterConfig.autoPlacement ? [autoPlacement()] : []), // Add autoPlacement middleware if autoPlacement is enabled
121
+ offset(this.element.floaterConfig?.offset || 0),
122
+ ...this.element.floaterConfig?.flip ? [flip()] : [], // Add flip middleware if flip is enabled.
123
+ ...this.element.floaterConfig?.autoPlacement ? [autoPlacement()] : [], // Add autoPlacement middleware if autoPlacement is enabled.
79
124
  ];
80
125
 
81
126
  // Compute the position of the bib
82
127
  computePosition(this.element.trigger, this.element.bib, {
83
- placement: this.element.floaterConfig.placement || 'bottom',
128
+ placement: this.element.floaterConfig?.placement,
84
129
  middleware: middleware || []
85
- }).then(({x, y}) => { // eslint-disable-line id-length
130
+ }).then(({ x, y }) => { // eslint-disable-line id-length
86
131
  Object.assign(this.element.bib.style, {
87
132
  left: `${x}px`,
88
133
  top: `${y}px`,
89
134
  });
90
135
  });
136
+ } else if (strategy === 'cover') {
137
+ // Compute the position of the bib
138
+ computePosition(this.element.parentNode, this.element.bib, {
139
+ placement: 'bottom-start'
140
+ }).then(({ x, y }) => { // eslint-disable-line id-length
141
+ Object.assign(this.element.bib.style, {
142
+ left: `${x}px`,
143
+ top: `${y - this.element.parentNode.offsetHeight}px`,
144
+ width: `${this.element.parentNode.offsetWidth}px`,
145
+ height: `${this.element.parentNode.offsetHeight}px`
146
+ });
147
+ });
91
148
  }
92
149
  }
93
150
 
@@ -116,34 +173,48 @@ export default class AuroFloatingUI {
116
173
  *
117
174
  * @param {string} strategy - The positioning strategy ('fullscreen' or 'floating').
118
175
  */
119
- configureBibStrategy(strategy) {
120
- const prevStrategy = this.element.isBibFullscreen ? 'fullscreen' : 'floating';
121
- if (strategy === 'fullscreen') {
176
+ configureBibStrategy(value) {
177
+ if (value === 'fullscreen') {
122
178
  this.element.isBibFullscreen = true;
123
179
  // reset the prev position
180
+ this.element.bib.setAttribute('isfullscreen', "");
181
+ this.element.bib.style.position = 'fixed';
124
182
  this.element.bib.style.top = "0px";
125
183
  this.element.bib.style.left = "0px";
184
+ this.element.bib.style.width = '';
185
+ this.element.bib.style.height = '';
126
186
 
127
187
  // reset the size that was mirroring `size` css-part
128
188
  const bibContent = this.element.bib.shadowRoot.querySelector(".container");
129
- bibContent.style.width = '';
130
- bibContent.style.height = '';
131
- bibContent.style.maxWidth = '';
132
- bibContent.style.maxHeight = `${window.visualViewport.height}px`;
189
+ if (bibContent) {
190
+ bibContent.style.width = '';
191
+ bibContent.style.height = '';
192
+ bibContent.style.maxWidth = '';
193
+ bibContent.style.maxHeight = `${window.visualViewport.height}px`;
194
+ this.configureTrial = 0;
195
+ } else if (this.configureTrial < MAX_CONFIGURATION_COUNT) {
196
+ this.configureTrial += 1;
197
+
198
+ setTimeout(() => {
199
+ this.configureBibStrategy(value);
200
+ });
201
+ }
133
202
 
134
203
  if (this.element.isPopoverVisible) {
135
204
  this.lockScroll(true);
136
205
  }
137
206
  } else {
207
+ this.element.bib.style.position = '';
208
+ this.element.bib.removeAttribute('isfullscreen');
138
209
  this.element.isBibFullscreen = false;
139
-
140
- this.lockScroll(false);
141
210
  }
142
211
 
143
- if (prevStrategy !== strategy) {
212
+ const isChanged = this.strategy && this.strategy !== value;
213
+ this.strategy = value;
214
+ if (isChanged) {
144
215
  const event = new CustomEvent(this.eventPrefix ? `${this.eventPrefix}-strategy-change` : 'strategy-change', {
145
216
  detail: {
146
- strategy,
217
+ value,
147
218
  },
148
219
  composed: true
149
220
  });
@@ -154,18 +225,6 @@ export default class AuroFloatingUI {
154
225
 
155
226
  updateState() {
156
227
  const isVisible = this.element.isPopoverVisible;
157
-
158
- // Refactor this to apply attribute to correct focusable element
159
- // Reference Issue: https://github.com/AlaskaAirlines/auro-library/issues/105
160
- //
161
- // this.element.trigger.setAttribute('aria-expanded', isVisible);
162
-
163
- if (isVisible) {
164
- this.element.bib.setAttribute('data-show', true);
165
- } else {
166
- this.element.bib.removeAttribute('data-show');
167
- }
168
-
169
228
  if (!isVisible) {
170
229
  this.cleanupHideHandlers();
171
230
  try {
@@ -177,15 +236,15 @@ export default class AuroFloatingUI {
177
236
  }
178
237
 
179
238
  handleFocusLoss() {
180
- if (this.element.noHideOnThisFocusLoss ||
181
- this.element.hasAttribute('noHideOnThisFocusLoss')) {
239
+ if (this.element.noHideOnThisFocusLoss ||
240
+ this.element.hasAttribute('noHideOnThisFocusLoss')) {
182
241
  return;
183
242
  }
184
243
 
185
- const {activeElement} = document;
244
+ const { activeElement } = document;
186
245
  if (activeElement === document.querySelector('body') ||
187
- this.element.contains(activeElement) ||
188
- this.element.bibContent?.contains(activeElement)) {
246
+ this.element.contains(activeElement) ||
247
+ this.element.bib?.contains(activeElement)) {
189
248
  return;
190
249
  }
191
250
 
@@ -197,23 +256,46 @@ export default class AuroFloatingUI {
197
256
  this.focusHandler = () => this.handleFocusLoss();
198
257
 
199
258
  this.clickHandler = (evt) => {
200
- if (!evt.composedPath().includes(this.element.trigger) &&
201
- !evt.composedPath().includes(this.element.bibContent)) {
202
- this.hideBib();
259
+ if ((!evt.composedPath().includes(this.element.trigger) &&
260
+ !evt.composedPath().includes(this.element.bib)) ||
261
+ (this.element.bib.backdrop && evt.composedPath().includes(this.element.bib.backdrop))) {
262
+ const existedVisibleFloatingUI = document.expandedAuroFormkitDropdown || document.expandedAuroFloater;
263
+
264
+ if (existedVisibleFloatingUI && existedVisibleFloatingUI.element.isPopoverVisible) {
265
+ // if something else is open, close that
266
+ existedVisibleFloatingUI.hideBib();
267
+ document.expandedAuroFormkitDropdown = null;
268
+ document.expandedAuroFloater = this;
269
+ } else {
270
+ this.hideBib();
271
+ }
203
272
  }
204
273
  };
205
274
 
206
275
  // ESC key handler
207
276
  this.keyDownHandler = (evt) => {
208
277
  if (evt.key === 'Escape' && this.element.isPopoverVisible) {
278
+ const existedVisibleFloatingUI = document.expandedAuroFormkitDropdown || document.expandedAuroFloater;
279
+ if (existedVisibleFloatingUI && existedVisibleFloatingUI !== this && existedVisibleFloatingUI.element.isPopoverVisible) {
280
+ // if something else is open, let it handle itself
281
+ return;
282
+ }
209
283
  this.hideBib();
210
284
  }
211
285
  };
212
286
 
213
- // Add event listeners using the stored references
214
- document.addEventListener('focusin', this.focusHandler);
215
- window.addEventListener('click', this.clickHandler);
287
+ if (this.behavior !== 'drawer' && this.behavior !== 'dialog') {
288
+ // Add event listeners using the stored references
289
+ document.addEventListener('focusin', this.focusHandler);
290
+ }
291
+
216
292
  document.addEventListener('keydown', this.keyDownHandler);
293
+
294
+ // send this task to the end of queue to prevent conflicting
295
+ // it conflicts if showBib gets call from a button that's not this.element.trigger
296
+ setTimeout(() => {
297
+ window.addEventListener('click', this.clickHandler);
298
+ });
217
299
  }
218
300
 
219
301
  cleanupHideHandlers() {
@@ -242,40 +324,54 @@ export default class AuroFloatingUI {
242
324
 
243
325
  updateCurrentExpandedDropdown() {
244
326
  // Close any other dropdown that is already open
245
- if (document.expandedAuroFormkitDropdown) {
246
- document.expandedAuroFormkitDropdown.hide;
327
+ const existedVisibleFloatingUI = document.expandedAuroFormkitDropdown || document.expandedAuroFloater;
328
+ if (existedVisibleFloatingUI && existedVisibleFloatingUI !== this &&
329
+ existedVisibleFloatingUI.isPopoverVisible &&
330
+ document.expandedAuroFloater.eventPrefix === this.eventPrefix) {
331
+ document.expandedAuroFloater.hideBib();
247
332
  }
248
333
 
249
- document.expandedAuroFormkitDropdown = this;
334
+ document.expandedAuroFloater = this;
250
335
  }
251
336
 
252
337
  showBib() {
253
- if (!this.element.disabled && !this.element.isPopoverVisible) {
338
+ if (!this.element.disabled && !this.showing) {
254
339
  this.updateCurrentExpandedDropdown();
255
- this.element.isPopoverVisible = true;
256
340
  this.element.triggerChevron?.setAttribute('data-expanded', true);
257
-
258
- this.dispatchEventDropdownToggle();
259
- this.position();
260
-
261
- // Clean up any existing handlers before setting up new ones
262
- this.cleanupHideHandlers();
263
- this.setupHideHandlers();
341
+
342
+ // prevent double showing: isPopovervisible gets first and showBib gets called later
343
+ if (!this.showing) {
344
+ if (!this.element.modal) {
345
+ this.setupHideHandlers();
346
+ }
347
+ this.showing = true;
348
+ this.element.isPopoverVisible = true;
349
+ this.position();
350
+ this.dispatchEventDropdownToggle();
351
+ }
264
352
 
265
353
  // Setup auto update to handle resize and scroll
266
- this.element.cleanup = autoUpdate(this.element.trigger, this.element.bib, () => {
354
+ this.element.cleanup = autoUpdate(this.element.trigger || this.element.parentNode, this.element.bib, () => {
267
355
  this.position();
268
356
  });
269
357
  }
270
358
  }
271
359
 
272
360
  hideBib() {
273
- if (this.element.isPopoverVisible && !this.element.disabled && !this.element.noToggle) {
274
- this.element.isPopoverVisible = false;
361
+ if (!this.element.disabled && !this.element.noToggle) {
275
362
  this.lockScroll(false);
276
363
  this.element.triggerChevron?.removeAttribute('data-expanded');
277
- this.dispatchEventDropdownToggle();
364
+
365
+ if (this.element.isPopoverVisible) {
366
+ this.element.isPopoverVisible = false;
367
+ }
368
+ if (this.showing) {
369
+ this.cleanupHideHandlers();
370
+ this.showing = false;
371
+ this.dispatchEventDropdownToggle();
372
+ }
278
373
  }
374
+ document.expandedAuroFloater = null;
279
375
  }
280
376
 
281
377
  /**
@@ -285,7 +381,7 @@ export default class AuroFloatingUI {
285
381
  dispatchEventDropdownToggle() {
286
382
  const event = new CustomEvent(this.eventPrefix ? `${this.eventPrefix}-toggled` : 'toggled', {
287
383
  detail: {
288
- expanded: this.element.isPopoverVisible,
384
+ expanded: this.showing,
289
385
  },
290
386
  composed: true
291
387
  });
@@ -333,9 +429,10 @@ export default class AuroFloatingUI {
333
429
  break;
334
430
  case 'focus':
335
431
  if (this.element.focusShow) {
432
+
336
433
  /*
337
- This needs to better handle clicking that gives focus -
338
- currently it shows and then immediately hides the bib
434
+ This needs to better handle clicking that gives focus -
435
+ currently it shows and then immediately hides the bib
339
436
  */
340
437
  this.showBib();
341
438
  }
@@ -350,7 +447,7 @@ export default class AuroFloatingUI {
350
447
  this.handleClick();
351
448
  break;
352
449
  default:
353
- // Do nothing
450
+ // Do nothing
354
451
  }
355
452
  }
356
453
  }
@@ -394,31 +491,63 @@ export default class AuroFloatingUI {
394
491
  });
395
492
  }
396
493
 
494
+ /**
495
+ *
496
+ * @param {*} eventPrefix
497
+ */
498
+ regenerateBibId() {
499
+ this.id = this.element.getAttribute('id');
500
+ if (!this.id) {
501
+ this.id = window.crypto.randomUUID();
502
+ this.element.setAttribute('id', this.id);
503
+ }
504
+
505
+ this.element.bib.setAttribute("id", `${this.id}-floater-bib`);
506
+ }
507
+
397
508
  configure(elem, eventPrefix) {
398
509
  this.eventPrefix = eventPrefix;
399
- this.element = elem;
400
- this.element.trigger = this.element.shadowRoot.querySelector('#trigger');
401
- this.element.bib = this.element.shadowRoot.querySelector('#bib');
510
+ if (this.element !== elem) {
511
+ this.element = elem;
512
+ }
513
+
514
+ if (this.behavior !== this.element.behavior) {
515
+ this.behavior = this.element.behavior;
516
+ }
517
+
518
+ if (this.element.trigger) {
519
+ this.disconnect();
520
+ }
521
+ this.element.trigger = this.element.triggerElement || this.element.shadowRoot.querySelector('#trigger') || this.element.trigger;
522
+ this.element.bib = this.element.shadowRoot.querySelector('#bib') || this.element.bib;
402
523
  this.element.bibSizer = this.element.shadowRoot.querySelector('#bibSizer');
403
524
  this.element.triggerChevron = this.element.shadowRoot.querySelector('#showStateIcon');
404
525
 
526
+
527
+ if (this.element.floaterConfig) {
528
+ this.element.hoverToggle = this.element.floaterConfig.hoverToggle;
529
+ }
530
+
405
531
  document.body.append(this.element.bib);
406
532
 
533
+ this.regenerateBibId();
407
534
  this.handleTriggerTabIndex();
408
535
 
409
536
  this.handleEvent = this.handleEvent.bind(this);
410
- this.element.trigger.addEventListener('keydown', this.handleEvent);
411
- this.element.trigger.addEventListener('click', this.handleEvent);
412
- this.element.trigger.addEventListener('mouseenter', this.handleEvent);
413
- this.element.trigger.addEventListener('mouseleave', this.handleEvent);
414
- this.element.trigger.addEventListener('focus', this.handleEvent);
415
- this.element.trigger.addEventListener('blur', this.handleEvent);
537
+ if (this.element.trigger) {
538
+ this.element.trigger.addEventListener('keydown', this.handleEvent);
539
+ this.element.trigger.addEventListener('click', this.handleEvent);
540
+ this.element.trigger.addEventListener('mouseenter', this.handleEvent);
541
+ this.element.trigger.addEventListener('mouseleave', this.handleEvent);
542
+ this.element.trigger.addEventListener('focus', this.handleEvent);
543
+ this.element.trigger.addEventListener('blur', this.handleEvent);
544
+ }
416
545
  }
417
546
 
418
547
  disconnect() {
419
548
  this.cleanupHideHandlers();
420
549
  this.element.cleanup?.();
421
-
550
+
422
551
  // Remove event & keyboard listeners
423
552
  if (this.element?.trigger) {
424
553
  this.element.trigger.removeEventListener('keydown', this.handleEvent);