@aurodesignsystem/auro-library 3.0.2 → 3.0.4

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,19 @@
1
1
  # Semantic Release Automated Changelog
2
2
 
3
+ ## [3.0.4](https://github.com/AlaskaAirlines/auro-library/compare/v3.0.3...v3.0.4) (2024-12-27)
4
+
5
+
6
+ ### Performance Improvements
7
+
8
+ * update `floatingUI` to match with `auro-formkit/dropdown` ([d003072](https://github.com/AlaskaAirlines/auro-library/commit/d00307245d16ad3a4d5aa1b2d60bb374caf3d454))
9
+
10
+ ## [3.0.3](https://github.com/AlaskaAirlines/auro-library/compare/v3.0.2...v3.0.3) (2024-12-23)
11
+
12
+
13
+ ### Performance Improvements
14
+
15
+ * update node to version 22 ([6005e32](https://github.com/AlaskaAirlines/auro-library/commit/6005e32156c3c4e6d9b8205270092b4c77a1bf1a))
16
+
3
17
  ## [3.0.2](https://github.com/AlaskaAirlines/auro-library/compare/v3.0.1...v3.0.2) (2024-11-13)
4
18
 
5
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aurodesignsystem/auro-library",
3
- "version": "3.0.2",
3
+ "version": "3.0.4",
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",
@@ -9,7 +9,7 @@
9
9
  "main": "index.js",
10
10
  "license": "Apache-2.0",
11
11
  "engines": {
12
- "node": "^18 || ^20"
12
+ "node": "^20.x || ^22.x"
13
13
  },
14
14
  "bin": {
15
15
  "generateDocs": "./bin/generateDocs.mjs"
@@ -1,141 +1,389 @@
1
1
  /* eslint-disable line-comment-position, no-inline-comments */
2
2
 
3
- import {computePosition, offset, autoPlacement, flip} from '@floating-ui/dom';
3
+ import { autoUpdate, computePosition, offset, autoPlacement, flip } from '@floating-ui/dom';
4
4
 
5
5
  export default class AuroFloatingUI {
6
- bibUpdate(element) {
7
- this.position(element, element.trigger, element.bib);
6
+ constructor() {
7
+ // Store event listener references for cleanup
8
+ this.focusHandler = null;
9
+ this.clickHandler = null;
10
+ this.keyDownHandler = null;
11
+
12
+ /**
13
+ * @private
14
+ */
15
+ this.eventPrefix = undefined;
8
16
  }
9
17
 
10
- position(element, referenceEl, floatingEl) {
11
- const middleware = [offset(element.floaterConfig.offset || 0)];
12
-
13
- if (element.floaterConfig.flip) {
14
- middleware.push(flip());
18
+ /**
19
+ * @private
20
+ * Adjusts the size of the bib content based on the bibSizer dimensions.
21
+ *
22
+ * This method retrieves the computed styles of the bibSizer element and applies them to the bib content.
23
+ * If the fullscreen parameter is true, it resets the dimensions to their default values. Otherwise, it
24
+ * mirrors the width and height from the bibSizer, ensuring that they are not set to zero.
25
+ *
26
+ * @param {boolean} fullscreen - A flag indicating whether to reset the dimensions for fullscreen mode.
27
+ * If true, the bib content dimensions are cleared; if false, they are set
28
+ * based on the bibSizer's computed styles.
29
+ */
30
+ mirrorSize(fullscreen) {
31
+ // mirror the boxsize from bibSizer
32
+ if (this.element.bibSizer) {
33
+ const sizerStyle = window.getComputedStyle(this.element.bibSizer);
34
+ const bibContent = this.element.bib.shadowRoot.querySelector(".container");
35
+ if (fullscreen) {
36
+ bibContent.style.width = '';
37
+ bibContent.style.height = '';
38
+ bibContent.style.maxWidth = '';
39
+ bibContent.style.maxHeight = '';
40
+ } else {
41
+ if (sizerStyle.width !== '0px') {
42
+ bibContent.style.width = sizerStyle.width;
43
+ }
44
+ if (sizerStyle.height !== '0px') {
45
+ bibContent.style.height = sizerStyle.height;
46
+ }
47
+ bibContent.style.maxWidth = sizerStyle.maxWidth;
48
+ bibContent.style.maxHeight = sizerStyle.maxHeight;
49
+ }
15
50
  }
51
+ }
16
52
 
17
- if (element.floaterConfig.autoPlacement) {
18
- middleware.push(autoPlacement());
53
+ /**
54
+ * @private
55
+ * Determines the positioning strategy based on the current viewport size and mobile breakpoint.
56
+ *
57
+ * This method checks if the current viewport width is less than or equal to the specified mobile fullscreen breakpoint
58
+ * defined in the bib element. If it is, the strategy is set to 'fullscreen'; otherwise, it defaults to 'floating'.
59
+ *
60
+ * @returns {String} The positioning strategy, either 'fullscreen' or 'floating'.
61
+ */
62
+ getPositioningStrategy() {
63
+ let strategy = 'floating';
64
+ if (this.element.bib.mobileFullscreenBreakpoint) {
65
+ const isMobile = window.matchMedia(`(max-width: ${this.element.bib.mobileFullscreenBreakpoint})`).matches;
66
+ if (isMobile) {
67
+ strategy = 'fullscreen';
68
+ }
19
69
  }
70
+ return strategy;
71
+ }
20
72
 
21
- computePosition(referenceEl, floatingEl, {
22
- placement: element.floaterConfig.placement || 'bottom',
23
- middleware: middleware || []
24
- }).then(({x, y}) => { // eslint-disable-line id-length
25
- Object.assign(floatingEl.style, {
26
- left: `${x}px`,
27
- top: `${y}px`,
73
+ /**
74
+ * @private
75
+ * Positions the bib element based on the current configuration and positioning strategy.
76
+ *
77
+ * This method determines the appropriate positioning strategy (fullscreen or not) and configures the bib accordingly.
78
+ * It also sets up middleware for the floater configuration, computes the position of the bib relative to the trigger element,
79
+ * and applies the calculated position to the bib's style.
80
+ */
81
+ position() {
82
+ const strategy = this.getPositioningStrategy();
83
+ if (strategy === 'fullscreen') {
84
+ this.configureBibFullscreen(true);
85
+ this.mirrorSize(true);
86
+ } else {
87
+ this.configureBibFullscreen(false);
88
+ this.mirrorSize(false);
89
+
90
+ // Define the middlware for the floater configuration
91
+ const middleware = [
92
+ offset(this.element.floaterConfig.offset || 0),
93
+ ...(this.element.floaterConfig.flip ? [flip()] : []), // Add flip middleware if flip is enabled
94
+ ...(this.element.floaterConfig.autoPlacement ? [autoPlacement()] : []), // Add autoPlacement middleware if autoPlacement is enabled
95
+ ];
96
+
97
+ // Compute the position of the bib
98
+ computePosition(this.element.trigger, this.element.bib, {
99
+ placement: this.element.floaterConfig.placement || 'bottom',
100
+ middleware: middleware || []
101
+ }).then(({x, y}) => { // eslint-disable-line id-length
102
+ Object.assign(this.element.bib.style, {
103
+ left: `${x}px`,
104
+ top: `${y}px`,
105
+ });
28
106
  });
29
- });
107
+ }
30
108
  }
31
109
 
32
- showBib(element) {
33
- if (!element.disabled && !element.isPopoverVisible) {
34
- // First, close any other dropdown that is already open
35
- if (document.expandedAuroDropdown) {
36
- // document.expandedAuroDropdown.hideBib();
37
- this.hideBib(document.expandedAuroDropdown);
110
+ /**
111
+ * @private
112
+ * Configures the bib element for fullscreen mode based on the mobile status.
113
+ *
114
+ * This method sets the 'isFullscreen' attribute on the bib element to "true" if the `isMobile` parameter is true,
115
+ * and resets its position to the top-left corner of the viewport. If `isMobile` is false, it removes the
116
+ * 'isFullscreen' attribute, indicating that the bib is not in fullscreen mode.
117
+ *
118
+ * @param {boolean} isMobile - A flag indicating whether the current device is mobile.
119
+ */
120
+ configureBibFullscreen(isMobile) {
121
+ if (isMobile) {
122
+ this.element.bib.setAttribute('isFullscreen', "true");
123
+ // reset the prev position
124
+ this.element.bib.style.top = "0px";
125
+ this.element.bib.style.left = "0px";
126
+ } else {
127
+ this.element.bib.removeAttribute('isFullscreen');
128
+ }
129
+ }
130
+
131
+ updateState() {
132
+ const isVisible = this.element.isPopoverVisible;
133
+ this.element.trigger.setAttribute('aria-expanded', isVisible);
134
+
135
+ if (isVisible) {
136
+ this.element.bib.setAttribute('data-show', true);
137
+ } else {
138
+ this.element.bib.removeAttribute('data-show');
139
+ }
140
+
141
+ if (!isVisible) {
142
+ this.cleanupHideHandlers();
143
+ try {
144
+ this.element.cleanup?.();
145
+ } catch (error) {
146
+ // Do nothing
38
147
  }
148
+ }
149
+ }
39
150
 
40
- document.expandedAuroDropdown = this;
41
-
42
- // Then, show this dropdown
43
- element.bib.style.display = 'block';
44
- this.bibUpdate(element);
45
- element.isPopoverVisible = true; // does Floating UI already surface this?
46
- element.trigger.setAttribute('aria-expanded', true);
47
-
48
- // wrap this so we can clean it up when the bib is hidden
49
- // document.querySelector('body').addEventListener('click', (evt) => {
50
- // if (!evt.composedPath().includes(this)) {
51
- // this.hideBib();
52
- // }
53
- // });
54
- if (!element.noHideOnThisFocusLoss && !element.hasAttribute('noHideOnThisFocusLoss')) {
55
- document.activeElement.addEventListener('focusout', () => {
56
- if (document.activeElement !== document.querySelector('body') && !element.contains(document.activeElement)) {
57
- this.hideBib(element);
58
- }
59
- });
151
+ handleFocusLoss() {
152
+ if (this.element.noHideOnThisFocusLoss ||
153
+ this.element.hasAttribute('noHideOnThisFocusLoss')) {
154
+ return;
155
+ }
60
156
 
61
- document.querySelector('body').addEventListener('click', (evt) => {
62
- if (!evt.composedPath().includes(element)) {
63
- this.hideBib(element);
64
- }
65
- });
157
+ const {activeElement} = document;
158
+ if (activeElement === document.querySelector('body') ||
159
+ this.element.contains(activeElement) ||
160
+ this.element.bibContent?.contains(activeElement)) {
161
+ return;
162
+ }
163
+
164
+ this.hideBib();
165
+ }
166
+
167
+ setupHideHandlers() {
168
+ // Define handlers & store references
169
+ this.focusHandler = () => this.handleFocusLoss();
170
+
171
+ this.clickHandler = (evt) => {
172
+ if (!evt.composedPath().includes(this.element.trigger) &&
173
+ !evt.composedPath().includes(this.element.bibContent)) {
174
+ this.hideBib();
66
175
  }
176
+ };
177
+
178
+ // ESC key handler
179
+ this.keyDownHandler = (evt) => {
180
+ if (evt.key === 'Escape' && this.element.isPopoverVisible) {
181
+ this.hideBib();
182
+ }
183
+ };
184
+
185
+ // Add event listeners using the stored references
186
+ document.addEventListener('focusin', this.focusHandler);
187
+ window.addEventListener('click', this.clickHandler);
188
+ document.addEventListener('keydown', this.keyDownHandler);
189
+ }
190
+
191
+ cleanupHideHandlers() {
192
+ // Remove event listeners if they exist
193
+ if (this.focusHandler) {
194
+ document.removeEventListener('focusin', this.focusHandler);
195
+ this.focusHandler = null;
67
196
  }
197
+
198
+ if (this.clickHandler) {
199
+ window.removeEventListener('click', this.clickHandler);
200
+ this.clickHandler = null;
201
+ }
202
+
203
+ if (this.keyDownHandler) {
204
+ document.removeEventListener('keydown', this.keyDownHandler);
205
+ this.keyDownHandler = null;
206
+ }
207
+ }
208
+
209
+ handleUpdate(changedProperties) {
210
+ if (changedProperties.has('isPopoverVisible')) {
211
+ this.updateState();
212
+ }
213
+ }
214
+
215
+ updateCurrentExpandedDropdown() {
216
+ // Close any other dropdown that is already open
217
+ if (document.expandedAuroDropdown) {
218
+ this.hideBib(document.expandedAuroDropdown);
219
+ }
220
+
221
+ document.expandedAuroDropdown = this;
68
222
  }
69
223
 
70
- hideBib(element) {
71
- if (element.isPopoverVisible && !element.disabled && !element.noToggle) { // do we really want noToggle here?
72
- element.bib.style.display = ''; // should this be unset or none?
73
- element.isPopoverVisible = false;
74
- element.trigger.setAttribute('aria-expanded', false);
224
+ showBib() {
225
+ if (!this.element.disabled && !this.element.isPopoverVisible) {
226
+ this.updateCurrentExpandedDropdown();
227
+ this.element.isPopoverVisible = true;
228
+ this.element.triggerChevron?.setAttribute('data-expanded', true);
229
+ this.dispatchEventDropdownToggle();
230
+ this.position();
231
+
232
+ // Clean up any existing handlers before setting up new ones
233
+ this.cleanupHideHandlers();
234
+ this.setupHideHandlers();
235
+
236
+ // Setup auto update to handle resize and scroll
237
+ this.element.cleanup = autoUpdate(this.element.trigger, this.element.bib, () => {
238
+ this.position();
239
+ });
75
240
  }
76
241
  }
77
242
 
78
- configure(element) {
79
- element.trigger = element.shadowRoot.querySelector('#trigger');
80
- element.bib = element.shadowRoot.querySelector('#bib');
243
+ hideBib() {
244
+ if (this.element.isPopoverVisible && !this.element.disabled && !this.element.noToggle) {
245
+ this.element.isPopoverVisible = false;
246
+ this.element.triggerChevron?.removeAttribute('data-expanded');
247
+ this.dispatchEventDropdownToggle();
248
+ }
249
+ }
250
+
251
+ /**
252
+ * @private
253
+ * @returns {void} Dispatches event with an object showing the state of the dropdown.
254
+ */
255
+ dispatchEventDropdownToggle() {
256
+ const event = new CustomEvent(this.eventPrefix ? `${this.eventPrefix}-toggled` : 'toggled', {
257
+ detail: {
258
+ expanded: this.isPopoverVisible,
259
+ },
260
+ composed: true
261
+ });
81
262
 
82
- element.trigger.addEventListener('click', (event) => this.handleEvent(event, element));
83
- element.trigger.addEventListener('mouseenter', (event) => this.handleEvent(event, element));
84
- element.trigger.addEventListener('mouseleave', (event) => this.handleEvent(event, element));
85
- element.trigger.addEventListener('focus', (event) => this.handleEvent(event, element));
86
- element.trigger.addEventListener('blur', (event) => this.handleEvent(event, element));
263
+ this.element.dispatchEvent(event);
87
264
  }
88
265
 
89
- handleClick(element) {
90
- if (this.isPopoverVisible) {
91
- this.hideBib(element);
266
+ handleClick() {
267
+ if (this.element.isPopoverVisible) {
268
+ this.hideBib();
92
269
  } else {
93
- this.showBib(element);
270
+ this.showBib();
94
271
  }
95
272
 
96
- // should this be left in dropdown?
97
- const event = new CustomEvent('auroDropdown-triggerClick', {
273
+ const event = new CustomEvent(this.eventPrefix ? `${this.eventPrefix}-triggerClick` : "triggerClick", {
98
274
  composed: true,
99
275
  details: {
100
- expanded: this.isPopoverVisible
276
+ expanded: this.element.isPopoverVisible
101
277
  }
102
278
  });
103
279
 
104
- element.dispatchEvent(event);
280
+ this.element.dispatchEvent(event);
105
281
  }
106
282
 
107
- handleEvent(event, element) {
108
- if (!element.disableEventShow) {
283
+ handleEvent(event) {
284
+ if (!this.element.disableEventShow) {
109
285
  switch (event.type) {
286
+ case 'keydown':
287
+ // Support both Enter and Space keys for accessibility
288
+ // Space is included as it's expected behavior for interactive elements
289
+ if (event.key === 'Enter' || event.key === ' ') {
290
+ event.preventDefault(); // Prevent page scroll on space
291
+ this.handleClick();
292
+ }
293
+ break;
110
294
  case 'mouseenter':
111
- if (element.hoverToggle) {
112
- this.showBib(element);
295
+ if (this.element.hoverToggle) {
296
+ this.showBib();
113
297
  }
114
298
  break;
115
299
  case 'mouseleave':
116
- if (element.hoverToggle) {
117
- this.hideBib(element);
300
+ if (this.element.hoverToggle) {
301
+ this.hideBib();
118
302
  }
119
303
  break;
120
304
  case 'focus':
121
- if (element.focusShow) {
122
- // this needs to better handle clicking that gives focus - currently it shows and then immediately hides the bib
123
- this.showBib(element);
305
+ if (this.element.focusShow) {
306
+ /*
307
+ This needs to better handle clicking that gives focus -
308
+ currently it shows and then immediately hides the bib
309
+ */
310
+ this.showBib();
124
311
  }
125
312
  break;
126
313
  case 'blur':
127
- // this likely needs to be improved to handle focus within the bib for datepicker
128
- if (!element.noHideOnThisFocusLoss && !element.hasAttribute('noHideOnThisFocusLoss')) { // why do we have to do both here?
129
- this.hideBib(element);
130
- }
314
+ this.handleFocusLoss();
131
315
  break;
132
316
  case 'click':
133
- this.handleClick(element);
317
+ this.handleClick();
134
318
  break;
135
319
  default:
136
- // do nothing
137
- // add cases for show and toggle by keyboard space and enter key - maybe this is handled already?
320
+ // Do nothing
321
+ }
322
+ }
323
+ }
324
+
325
+ handleTriggerTabIndex() {
326
+ const focusableElementSelectors = [
327
+ 'a',
328
+ 'button',
329
+ 'input:not([type="hidden"])',
330
+ 'select',
331
+ 'textarea',
332
+ '[tabindex]:not([tabindex="-1"])',
333
+ 'auro-button',
334
+ 'auro-input',
335
+ 'auro-hyperlink'
336
+ ];
337
+
338
+ const triggerNode = this.element.querySelectorAll('[slot="trigger"]')[0];
339
+ const triggerNodeTagName = triggerNode.tagName.toLowerCase();
340
+
341
+ focusableElementSelectors.forEach((selector) => {
342
+ // Check if the trigger node element is focusable
343
+ if (triggerNodeTagName === selector) {
344
+ this.element.tabIndex = -1;
345
+ return;
346
+ }
347
+
348
+ // Check if any child is focusable
349
+ if (triggerNode.querySelector(selector)) {
350
+ this.element.tabIndex = -1;
138
351
  }
352
+ });
353
+ }
354
+
355
+ configure(elem, eventPrefix) {
356
+ this.eventPrefix = eventPrefix;
357
+ this.element = elem;
358
+ this.element.trigger = this.element.shadowRoot.querySelector('#trigger');
359
+ this.element.bib = this.element.shadowRoot.querySelector('#bib');
360
+ this.element.bibSizer = this.element.shadowRoot.querySelector('#bibSizer');
361
+ this.element.triggerChevron = this.element.shadowRoot.querySelector('#showStateIcon');
362
+
363
+ document.body.append(this.element.bib);
364
+
365
+ this.handleTriggerTabIndex();
366
+
367
+ this.element.trigger.addEventListener('keydown', (event) => this.handleEvent(event));
368
+ this.element.trigger.addEventListener('click', (event) => this.handleEvent(event));
369
+ this.element.trigger.addEventListener('mouseenter', (event) => this.handleEvent(event));
370
+ this.element.trigger.addEventListener('mouseleave', (event) => this.handleEvent(event));
371
+ this.element.trigger.addEventListener('focus', (event) => this.handleEvent(event));
372
+ this.element.trigger.addEventListener('blur', (event) => this.handleEvent(event));
373
+ }
374
+
375
+ disconnect() {
376
+ this.cleanupHideHandlers();
377
+ this.element.cleanup?.();
378
+
379
+ // Remove event & keyboard listeners
380
+ if (this.element?.trigger) {
381
+ this.element.trigger.removeEventListener('keydown', (event) => this.handleEvent(event));
382
+ this.element.trigger.removeEventListener('click', (event) => this.handleEvent(event));
383
+ this.element.trigger.removeEventListener('mouseenter', (event) => this.handleEvent(event));
384
+ this.element.trigger.removeEventListener('mouseleave', (event) => this.handleEvent(event));
385
+ this.element.trigger.removeEventListener('focus', (event) => this.handleEvent(event));
386
+ this.element.trigger.removeEventListener('blur', (event) => this.handleEvent(event));
139
387
  }
140
388
  }
141
389
  }