@aurodesignsystem-dev/auro-library 0.0.0-pr187.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/.husky/commit-msg +4 -0
- package/.husky/pre-commit +4 -0
- package/.tool-versions +1 -0
- package/CHANGELOG.md +664 -0
- package/LICENSE +201 -0
- package/README.md +235 -0
- package/bin/generateDocs.mjs +210 -0
- package/bin/generateDocs_index.mjs +210 -0
- package/package.json +92 -0
- package/scripts/build/generateDocs.mjs +24 -0
- package/scripts/build/generateReadme.mjs +60 -0
- package/scripts/build/generateWcaComponent.mjs +43 -0
- package/scripts/build/postCss.mjs +66 -0
- package/scripts/build/postinstall.mjs +31 -0
- package/scripts/build/pre-commit.mjs +17 -0
- package/scripts/build/prepWcaCompatibleCode.mjs +19 -0
- package/scripts/build/processors/defaultDocsProcessor.mjs +83 -0
- package/scripts/build/processors/defaultDotGithubSync.mjs +83 -0
- package/scripts/build/staticStyles-template.js +2 -0
- package/scripts/build/syncGithubFiles.mjs +25 -0
- package/scripts/build/versionWriter.js +26 -0
- package/scripts/runtime/FocusTrap/FocusTrap.mjs +194 -0
- package/scripts/runtime/FocusTrap/index.mjs +1 -0
- package/scripts/runtime/FocusTrap/test/FocusTrap.test.js +168 -0
- package/scripts/runtime/Focusables/Focusables.mjs +157 -0
- package/scripts/runtime/Focusables/index.mjs +1 -0
- package/scripts/runtime/Focusables/test/Focusables.test.js +165 -0
- package/scripts/runtime/dateUtilities/baseDateUtilities.mjs +58 -0
- package/scripts/runtime/dateUtilities/dateConstraints.mjs +11 -0
- package/scripts/runtime/dateUtilities/dateFormatter.mjs +104 -0
- package/scripts/runtime/dateUtilities/dateUtilities.mjs +218 -0
- package/scripts/runtime/dateUtilities/index.mjs +26 -0
- package/scripts/runtime/dependencyTagVersioning.mjs +42 -0
- package/scripts/runtime/floatingUI.mjs +646 -0
- package/scripts/test-plugin/iterateWithA11Check.mjs +82 -0
- package/scripts/utils/auroFileHandler.mjs +70 -0
- package/scripts/utils/auroLibraryUtils.mjs +206 -0
- package/scripts/utils/auroTemplateFiller.mjs +178 -0
- package/scripts/utils/logger.mjs +73 -0
- package/scripts/utils/runtimeUtils.mjs +70 -0
- package/scripts/utils/sharedFileProcessorUtils.mjs +270 -0
- package/shellScripts/README.md +58 -0
- package/shellScripts/automation.sh +104 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
/* eslint-disable line-comment-position, no-inline-comments */
|
|
2
|
+
|
|
3
|
+
import { autoUpdate, computePosition, offset, autoPlacement, flip } from '@floating-ui/dom';
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
const MAX_CONFIGURATION_COUNT = 10;
|
|
7
|
+
|
|
8
|
+
export default class AuroFloatingUI {
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @private
|
|
12
|
+
*/
|
|
13
|
+
static isMousePressed = false;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @private
|
|
17
|
+
*/
|
|
18
|
+
static isMousePressHandlerInitialized = false;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @private
|
|
22
|
+
*/
|
|
23
|
+
static setupMousePressChecker() {
|
|
24
|
+
if (!AuroFloatingUI.isMousePressHandlerInitialized && window && window.addEventListener) {
|
|
25
|
+
AuroFloatingUI.isMousePressHandlerInitialized = true;
|
|
26
|
+
|
|
27
|
+
// Track timeout for isMousePressed reset to avoid race conditions
|
|
28
|
+
if (!AuroFloatingUI._mousePressedTimeout) {
|
|
29
|
+
AuroFloatingUI._mousePressedTimeout = null;
|
|
30
|
+
}
|
|
31
|
+
const mouseEventGlobalHandler = (event) => {
|
|
32
|
+
const isPressed = event.type === 'mousedown';
|
|
33
|
+
if (isPressed) {
|
|
34
|
+
// Clear any pending timeout to prevent race condition
|
|
35
|
+
if (AuroFloatingUI._mousePressedTimeout !== null) {
|
|
36
|
+
clearTimeout(AuroFloatingUI._mousePressedTimeout);
|
|
37
|
+
AuroFloatingUI._mousePressedTimeout = null;
|
|
38
|
+
}
|
|
39
|
+
if (!AuroFloatingUI.isMousePressed) {
|
|
40
|
+
AuroFloatingUI.isMousePressed = true;
|
|
41
|
+
}
|
|
42
|
+
} else if (AuroFloatingUI.isMousePressed && !isPressed) {
|
|
43
|
+
// Schedule reset and track timeout ID
|
|
44
|
+
AuroFloatingUI._mousePressedTimeout = setTimeout(() => {
|
|
45
|
+
AuroFloatingUI.isMousePressed = false;
|
|
46
|
+
AuroFloatingUI._mousePressedTimeout = null;
|
|
47
|
+
}, 0);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
window.addEventListener('mousedown', mouseEventGlobalHandler);
|
|
52
|
+
window.addEventListener('mouseup', mouseEventGlobalHandler);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
constructor(element, behavior) {
|
|
57
|
+
this.element = element;
|
|
58
|
+
this.behavior = behavior;
|
|
59
|
+
|
|
60
|
+
// Store event listener references for cleanup
|
|
61
|
+
this.focusHandler = null;
|
|
62
|
+
this.clickHandler = null;
|
|
63
|
+
this.keyDownHandler = null;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
this.configureTrial = 0;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @private
|
|
72
|
+
*/
|
|
73
|
+
this.eventPrefix = undefined;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @private
|
|
77
|
+
*/
|
|
78
|
+
this.id = undefined;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @private
|
|
82
|
+
*/
|
|
83
|
+
this.showing = false;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
88
|
+
this.strategy = undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Mirrors the size of the bibSizer element to the bib content.
|
|
93
|
+
* Copies the width, height, max-width, and max-height styles from the bibSizer element to the bib content container.
|
|
94
|
+
* This ensures that the bib content has the same dimensions as the sizer element.
|
|
95
|
+
*/
|
|
96
|
+
mirrorSize() {
|
|
97
|
+
// mirror the boxsize from bibSizer
|
|
98
|
+
if (this.element.bibSizer && this.element.matchWidth) {
|
|
99
|
+
const sizerStyle = window.getComputedStyle(this.element.bibSizer);
|
|
100
|
+
const bibContent = this.element.bib.shadowRoot.querySelector(".container");
|
|
101
|
+
if (sizerStyle.width !== '0px') {
|
|
102
|
+
bibContent.style.width = sizerStyle.width;
|
|
103
|
+
}
|
|
104
|
+
if (sizerStyle.height !== '0px') {
|
|
105
|
+
bibContent.style.height = sizerStyle.height;
|
|
106
|
+
}
|
|
107
|
+
bibContent.style.maxWidth = sizerStyle.maxWidth;
|
|
108
|
+
bibContent.style.maxHeight = sizerStyle.maxHeight;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @private
|
|
114
|
+
* Determines the positioning strategy based on the current viewport size and mobile breakpoint.
|
|
115
|
+
*
|
|
116
|
+
* This method checks if the current viewport width is less than or equal to the specified mobile fullscreen breakpoint
|
|
117
|
+
* defined in the bib element. If it is, the strategy is set to 'fullscreen'; otherwise, it defaults to 'floating'.
|
|
118
|
+
*
|
|
119
|
+
* @returns {String} The positioning strategy, one of 'fullscreen', 'floating', 'cover'.
|
|
120
|
+
*/
|
|
121
|
+
getPositioningStrategy() {
|
|
122
|
+
const breakpoint = this.element.bib.mobileFullscreenBreakpoint || this.element.floaterConfig?.fullscreenBreakpoint;
|
|
123
|
+
switch (this.behavior) {
|
|
124
|
+
case "tooltip":
|
|
125
|
+
return "floating";
|
|
126
|
+
case "dialog":
|
|
127
|
+
case "drawer":
|
|
128
|
+
if (breakpoint) {
|
|
129
|
+
const smallerThanBreakpoint = window.matchMedia(`(max-width: ${breakpoint})`).matches;
|
|
130
|
+
|
|
131
|
+
this.element.expanded = smallerThanBreakpoint;
|
|
132
|
+
}
|
|
133
|
+
if (this.element.nested) {
|
|
134
|
+
return "cover";
|
|
135
|
+
}
|
|
136
|
+
return 'fullscreen';
|
|
137
|
+
case "dropdown":
|
|
138
|
+
case undefined:
|
|
139
|
+
case null:
|
|
140
|
+
if (breakpoint) {
|
|
141
|
+
const smallerThanBreakpoint = window.matchMedia(`(max-width: ${breakpoint})`).matches;
|
|
142
|
+
if (smallerThanBreakpoint) {
|
|
143
|
+
return 'fullscreen';
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return "floating";
|
|
147
|
+
default:
|
|
148
|
+
return this.behavior;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* @private
|
|
154
|
+
* Positions the bib element based on the current configuration and positioning strategy.
|
|
155
|
+
*
|
|
156
|
+
* This method determines the appropriate positioning strategy (fullscreen or not) and configures the bib accordingly.
|
|
157
|
+
* It also sets up middleware for the floater configuration, computes the position of the bib relative to the trigger element,
|
|
158
|
+
* and applies the calculated position to the bib's style.
|
|
159
|
+
*/
|
|
160
|
+
position() {
|
|
161
|
+
const strategy = this.getPositioningStrategy();
|
|
162
|
+
this.configureBibStrategy(strategy);
|
|
163
|
+
|
|
164
|
+
if (strategy === 'floating') {
|
|
165
|
+
this.mirrorSize();
|
|
166
|
+
// Define the middlware for the floater configuration
|
|
167
|
+
const middleware = [
|
|
168
|
+
offset(this.element.floaterConfig?.offset || 0),
|
|
169
|
+
...this.element.floaterConfig?.flip ? [flip()] : [], // Add flip middleware if flip is enabled.
|
|
170
|
+
...this.element.floaterConfig?.autoPlacement ? [autoPlacement()] : [], // Add autoPlacement middleware if autoPlacement is enabled.
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
// Compute the position of the bib
|
|
174
|
+
computePosition(this.element.trigger, this.element.bib, {
|
|
175
|
+
strategy: this.element.floaterConfig?.strategy || 'fixed',
|
|
176
|
+
placement: this.element.floaterConfig?.placement,
|
|
177
|
+
middleware: middleware || []
|
|
178
|
+
}).then(({ x, y }) => { // eslint-disable-line id-length
|
|
179
|
+
Object.assign(this.element.bib.style, {
|
|
180
|
+
left: `${x}px`,
|
|
181
|
+
top: `${y}px`,
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
} else if (strategy === 'cover') {
|
|
185
|
+
// Compute the position of the bib
|
|
186
|
+
computePosition(this.element.parentNode, this.element.bib, {
|
|
187
|
+
placement: 'bottom-start'
|
|
188
|
+
}).then(({ x, y }) => { // eslint-disable-line id-length
|
|
189
|
+
Object.assign(this.element.bib.style, {
|
|
190
|
+
left: `${x}px`,
|
|
191
|
+
top: `${y - this.element.parentNode.offsetHeight}px`,
|
|
192
|
+
width: `${this.element.parentNode.offsetWidth}px`,
|
|
193
|
+
height: `${this.element.parentNode.offsetHeight}px`
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @private
|
|
201
|
+
* Controls whether to lock the scrolling for the document's body.
|
|
202
|
+
* @param {Boolean} lock - If true, locks the body's scrolling functionlity; otherwise, unlock.
|
|
203
|
+
*/
|
|
204
|
+
lockScroll(lock = true) {
|
|
205
|
+
if (lock) {
|
|
206
|
+
document.body.style.overflow = 'hidden'; // hide body's scrollbar
|
|
207
|
+
|
|
208
|
+
// Move `bib` by the amount the viewport is shifted to stay aligned in fullscreen.
|
|
209
|
+
this.element.bib.style.transform = `translateY(${visualViewport.offsetTop}px)`;
|
|
210
|
+
} else {
|
|
211
|
+
document.body.style.overflow = '';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* @private
|
|
217
|
+
* Configures the bib element's display strategy.
|
|
218
|
+
*
|
|
219
|
+
* Sets the bib to fullscreen or floating mode based on the provided strategy.
|
|
220
|
+
* Dispatches a 'strategy-change' event if the strategy changes.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} strategy - The positioning strategy ('fullscreen' or 'floating').
|
|
223
|
+
*/
|
|
224
|
+
configureBibStrategy(value) {
|
|
225
|
+
if (value === 'fullscreen') {
|
|
226
|
+
this.element.isBibFullscreen = true;
|
|
227
|
+
// reset the prev position
|
|
228
|
+
this.element.bib.setAttribute('isfullscreen', "");
|
|
229
|
+
this.element.bib.style.position = 'fixed';
|
|
230
|
+
this.element.bib.style.top = "0px";
|
|
231
|
+
this.element.bib.style.left = "0px";
|
|
232
|
+
this.element.bib.style.width = '';
|
|
233
|
+
this.element.bib.style.height = '';
|
|
234
|
+
this.element.style.contain = '';
|
|
235
|
+
|
|
236
|
+
// reset the size that was mirroring `size` css-part
|
|
237
|
+
const bibContent = this.element.bib.shadowRoot.querySelector(".container");
|
|
238
|
+
if (bibContent) {
|
|
239
|
+
bibContent.style.width = '';
|
|
240
|
+
bibContent.style.height = '';
|
|
241
|
+
bibContent.style.maxWidth = '';
|
|
242
|
+
bibContent.style.maxHeight = `${window.visualViewport.height}px`;
|
|
243
|
+
this.configureTrial = 0;
|
|
244
|
+
} else if (this.configureTrial < MAX_CONFIGURATION_COUNT) {
|
|
245
|
+
this.configureTrial += 1;
|
|
246
|
+
|
|
247
|
+
setTimeout(() => {
|
|
248
|
+
this.configureBibStrategy(value);
|
|
249
|
+
}, 0);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (this.element.isPopoverVisible) {
|
|
253
|
+
this.lockScroll(true);
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
this.element.bib.style.position = '';
|
|
257
|
+
this.element.bib.removeAttribute('isfullscreen');
|
|
258
|
+
this.element.isBibFullscreen = false;
|
|
259
|
+
this.element.style.contain = 'layout';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const isChanged = this.strategy && this.strategy !== value;
|
|
263
|
+
this.strategy = value;
|
|
264
|
+
if (isChanged) {
|
|
265
|
+
const event = new CustomEvent(this.eventPrefix ? `${this.eventPrefix}-strategy-change` : 'strategy-change', {
|
|
266
|
+
detail: {
|
|
267
|
+
value,
|
|
268
|
+
},
|
|
269
|
+
composed: true
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
this.element.dispatchEvent(event);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
updateState() {
|
|
277
|
+
const isVisible = this.element.isPopoverVisible;
|
|
278
|
+
if (!isVisible) {
|
|
279
|
+
this.cleanupHideHandlers();
|
|
280
|
+
try {
|
|
281
|
+
this.element.cleanup?.();
|
|
282
|
+
} catch (error) {
|
|
283
|
+
// Do nothing
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* @private
|
|
290
|
+
* getting called on 'blur' in trigger or `focusin` in document
|
|
291
|
+
*
|
|
292
|
+
* Hides the bib if focus moves outside of the trigger or bib, unless a 'noHideOnThisFocusLoss' flag is set.
|
|
293
|
+
* This method checks if the currently active element is still within the trigger or bib.
|
|
294
|
+
* If not, and if the bib isn't in fullscreen mode with focus lost, it hides the bib.
|
|
295
|
+
*/
|
|
296
|
+
handleFocusLoss() {
|
|
297
|
+
// if mouse is being pressed, skip and let click event to handle the action
|
|
298
|
+
if (AuroFloatingUI.isMousePressed) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (this.element.noHideOnThisFocusLoss ||
|
|
303
|
+
this.element.hasAttribute('noHideOnThisFocusLoss')) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const { activeElement } = document;
|
|
308
|
+
// if focus is still inside of trigger or bib, do not close
|
|
309
|
+
if (this.element.contains(activeElement) || this.element.bib?.contains(activeElement)) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// if fullscreen bib is in fullscreen mode, do not close
|
|
314
|
+
if (this.element.bib.hasAttribute('isfullscreen')) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
this.hideBib("keydown");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
setupHideHandlers() {
|
|
322
|
+
// Define handlers & store references
|
|
323
|
+
this.focusHandler = () => this.handleFocusLoss();
|
|
324
|
+
|
|
325
|
+
this.clickHandler = (evt) => {
|
|
326
|
+
if ((!evt.composedPath().includes(this.element.trigger) &&
|
|
327
|
+
!evt.composedPath().includes(this.element.bib)) ||
|
|
328
|
+
(this.element.bib.backdrop && evt.composedPath().includes(this.element.bib.backdrop))) {
|
|
329
|
+
const existedVisibleFloatingUI = document.expandedAuroFormkitDropdown || document.expandedAuroFloater;
|
|
330
|
+
|
|
331
|
+
if (existedVisibleFloatingUI && existedVisibleFloatingUI.element.isPopoverVisible) {
|
|
332
|
+
// if something else is open, close that
|
|
333
|
+
existedVisibleFloatingUI.hideBib();
|
|
334
|
+
document.expandedAuroFormkitDropdown = null;
|
|
335
|
+
document.expandedAuroFloater = this;
|
|
336
|
+
} else {
|
|
337
|
+
this.hideBib("click");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// ESC key handler
|
|
343
|
+
this.keyDownHandler = (evt) => {
|
|
344
|
+
if (evt.key === 'Escape' && this.element.isPopoverVisible) {
|
|
345
|
+
const existedVisibleFloatingUI = document.expandedAuroFormkitDropdown || document.expandedAuroFloater;
|
|
346
|
+
if (existedVisibleFloatingUI && existedVisibleFloatingUI !== this && existedVisibleFloatingUI.element.isPopoverVisible) {
|
|
347
|
+
// if something else is open, let it handle itself
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
this.hideBib("keydown");
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
if (this.behavior !== 'drawer' && this.behavior !== 'dialog') {
|
|
355
|
+
// Add event listeners using the stored references
|
|
356
|
+
document.addEventListener('focusin', this.focusHandler);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
document.addEventListener('keydown', this.keyDownHandler);
|
|
360
|
+
|
|
361
|
+
// send this task to the end of queue to prevent conflicting
|
|
362
|
+
// it conflicts if showBib gets call from a button that's not this.element.trigger
|
|
363
|
+
setTimeout(() => {
|
|
364
|
+
window.addEventListener('click', this.clickHandler);
|
|
365
|
+
}, 0);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
cleanupHideHandlers() {
|
|
369
|
+
// Remove event listeners if they exist
|
|
370
|
+
|
|
371
|
+
if (this.focusHandler) {
|
|
372
|
+
document.removeEventListener('focusin', this.focusHandler);
|
|
373
|
+
this.focusHandler = null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (this.clickHandler) {
|
|
377
|
+
window.removeEventListener('click', this.clickHandler);
|
|
378
|
+
this.clickHandler = null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (this.keyDownHandler) {
|
|
382
|
+
document.removeEventListener('keydown', this.keyDownHandler);
|
|
383
|
+
this.keyDownHandler = null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
handleUpdate(changedProperties) {
|
|
388
|
+
if (changedProperties.has('isPopoverVisible')) {
|
|
389
|
+
this.updateState();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
updateCurrentExpandedDropdown() {
|
|
394
|
+
// Close any other dropdown that is already open
|
|
395
|
+
const existedVisibleFloatingUI = document.expandedAuroFormkitDropdown || document.expandedAuroFloater;
|
|
396
|
+
if (existedVisibleFloatingUI && existedVisibleFloatingUI !== this &&
|
|
397
|
+
existedVisibleFloatingUI.element.isPopoverVisible &&
|
|
398
|
+
document.expandedAuroFloater.eventPrefix === this.eventPrefix) {
|
|
399
|
+
document.expandedAuroFloater.hideBib();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
document.expandedAuroFloater = this;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
showBib() {
|
|
406
|
+
if (!this.element.disabled && !this.showing) {
|
|
407
|
+
this.updateCurrentExpandedDropdown();
|
|
408
|
+
this.element.triggerChevron?.setAttribute('data-expanded', true);
|
|
409
|
+
|
|
410
|
+
// prevent double showing: isPopovervisible gets first and showBib gets called later
|
|
411
|
+
if (!this.showing) {
|
|
412
|
+
if (!this.element.modal) {
|
|
413
|
+
this.setupHideHandlers();
|
|
414
|
+
}
|
|
415
|
+
this.showing = true;
|
|
416
|
+
this.element.isPopoverVisible = true;
|
|
417
|
+
this.position();
|
|
418
|
+
this.dispatchEventDropdownToggle();
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Setup auto update to handle resize and scroll
|
|
422
|
+
this.element.cleanup = autoUpdate(this.element.trigger || this.element.parentNode, this.element.bib, () => {
|
|
423
|
+
this.position();
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Hides the floating UI element.
|
|
430
|
+
* @param {String} eventType - The event type that triggered the hiding action.
|
|
431
|
+
*/
|
|
432
|
+
hideBib(eventType = "unknown") {
|
|
433
|
+
if (!this.element.disabled && !this.element.noToggle) {
|
|
434
|
+
this.lockScroll(false);
|
|
435
|
+
this.element.triggerChevron?.removeAttribute('data-expanded');
|
|
436
|
+
|
|
437
|
+
if (this.element.isPopoverVisible) {
|
|
438
|
+
this.element.isPopoverVisible = false;
|
|
439
|
+
}
|
|
440
|
+
if (this.showing) {
|
|
441
|
+
this.cleanupHideHandlers();
|
|
442
|
+
this.showing = false;
|
|
443
|
+
this.dispatchEventDropdownToggle(eventType);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
document.expandedAuroFloater = null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* @private
|
|
451
|
+
* @returns {void} Dispatches event with an object showing the state of the dropdown.
|
|
452
|
+
* @param {String} eventType - The event type that triggered the toggle action.
|
|
453
|
+
*/
|
|
454
|
+
dispatchEventDropdownToggle(eventType) {
|
|
455
|
+
const event = new CustomEvent(this.eventPrefix ? `${this.eventPrefix}-toggled` : 'toggled', {
|
|
456
|
+
detail: {
|
|
457
|
+
expanded: this.showing,
|
|
458
|
+
eventType: eventType || "unknown",
|
|
459
|
+
},
|
|
460
|
+
composed: true
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
this.element.dispatchEvent(event);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
handleClick() {
|
|
467
|
+
if (this.element.isPopoverVisible) {
|
|
468
|
+
this.hideBib("click");
|
|
469
|
+
} else {
|
|
470
|
+
this.showBib();
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const event = new CustomEvent(this.eventPrefix ? `${this.eventPrefix}-triggerClick` : "triggerClick", {
|
|
474
|
+
composed: true,
|
|
475
|
+
detail: {
|
|
476
|
+
expanded: this.element.isPopoverVisible
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
this.element.dispatchEvent(event);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
handleEvent(event) {
|
|
484
|
+
if (!this.element.disableEventShow) {
|
|
485
|
+
switch (event.type) {
|
|
486
|
+
case 'keydown':
|
|
487
|
+
// Support both Enter and Space keys for accessibility
|
|
488
|
+
// Space is included as it's expected behavior for interactive elements
|
|
489
|
+
|
|
490
|
+
const origin = event.composedPath()[0];
|
|
491
|
+
if (event.key === 'Enter' || event.key === ' ' && (!origin || origin.tagName !== "INPUT")) {
|
|
492
|
+
|
|
493
|
+
event.preventDefault();
|
|
494
|
+
this.handleClick();
|
|
495
|
+
}
|
|
496
|
+
break;
|
|
497
|
+
case 'mouseenter':
|
|
498
|
+
if (this.element.hoverToggle) {
|
|
499
|
+
this.showBib();
|
|
500
|
+
}
|
|
501
|
+
break;
|
|
502
|
+
case 'mouseleave':
|
|
503
|
+
if (this.element.hoverToggle) {
|
|
504
|
+
this.hideBib("mouseleave");
|
|
505
|
+
}
|
|
506
|
+
break;
|
|
507
|
+
case 'focus':
|
|
508
|
+
if (this.element.focusShow) {
|
|
509
|
+
|
|
510
|
+
/*
|
|
511
|
+
This needs to better handle clicking that gives focus -
|
|
512
|
+
currently it shows and then immediately hides the bib
|
|
513
|
+
*/
|
|
514
|
+
this.showBib();
|
|
515
|
+
}
|
|
516
|
+
break;
|
|
517
|
+
case 'blur':
|
|
518
|
+
// send this task 100ms later queue to
|
|
519
|
+
// wait a frame in case focus moves within the floating element/bib
|
|
520
|
+
setTimeout(() => this.handleFocusLoss(), 0);
|
|
521
|
+
break;
|
|
522
|
+
case 'click':
|
|
523
|
+
if (document.activeElement === document.body) {
|
|
524
|
+
event.currentTarget.focus();
|
|
525
|
+
}
|
|
526
|
+
this.handleClick();
|
|
527
|
+
break;
|
|
528
|
+
default:
|
|
529
|
+
// Do nothing
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Manages the tabIndex of the trigger element based on its focusability.
|
|
536
|
+
*
|
|
537
|
+
* If the trigger element or any of its children are inherently focusable, the tabIndex of the component is set to -1.
|
|
538
|
+
* This prevents the component itself from being focusable when the trigger element already handles focus.
|
|
539
|
+
*/
|
|
540
|
+
handleTriggerTabIndex() {
|
|
541
|
+
const focusableElementSelectors = [
|
|
542
|
+
'a',
|
|
543
|
+
'button',
|
|
544
|
+
'input:not([type="hidden"])',
|
|
545
|
+
'select',
|
|
546
|
+
'textarea',
|
|
547
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
548
|
+
'auro-button',
|
|
549
|
+
'auro-input',
|
|
550
|
+
'auro-hyperlink'
|
|
551
|
+
];
|
|
552
|
+
|
|
553
|
+
const triggerNode = this.element.querySelectorAll('[slot="trigger"]')[0];
|
|
554
|
+
if (!triggerNode) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const triggerNodeTagName = triggerNode.tagName.toLowerCase();
|
|
558
|
+
|
|
559
|
+
focusableElementSelectors.forEach((selector) => {
|
|
560
|
+
// Check if the trigger node element is focusable
|
|
561
|
+
if (triggerNodeTagName === selector) {
|
|
562
|
+
this.element.tabIndex = -1;
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Check if any child is focusable
|
|
567
|
+
if (triggerNode.querySelector(selector)) {
|
|
568
|
+
this.element.tabIndex = -1;
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
*
|
|
575
|
+
* @param {*} eventPrefix
|
|
576
|
+
*/
|
|
577
|
+
regenerateBibId() {
|
|
578
|
+
this.id = this.element.getAttribute('id');
|
|
579
|
+
if (!this.id) {
|
|
580
|
+
this.id = window.crypto.randomUUID();
|
|
581
|
+
this.element.setAttribute('id', this.id);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
this.element.bib.setAttribute("id", `${this.id}-floater-bib`);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
configure(elem, eventPrefix) {
|
|
588
|
+
AuroFloatingUI.setupMousePressChecker();
|
|
589
|
+
|
|
590
|
+
this.eventPrefix = eventPrefix;
|
|
591
|
+
if (this.element !== elem) {
|
|
592
|
+
this.element = elem;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (this.behavior !== this.element.behavior) {
|
|
596
|
+
this.behavior = this.element.behavior;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (this.element.trigger) {
|
|
600
|
+
this.disconnect();
|
|
601
|
+
}
|
|
602
|
+
this.element.trigger = this.element.triggerElement || this.element.shadowRoot.querySelector('#trigger') || this.element.trigger;
|
|
603
|
+
this.element.bib = this.element.shadowRoot.querySelector('#bib') || this.element.bib;
|
|
604
|
+
this.element.bibSizer = this.element.shadowRoot.querySelector('#bibSizer');
|
|
605
|
+
this.element.triggerChevron = this.element.shadowRoot.querySelector('#showStateIcon');
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
if (this.element.floaterConfig) {
|
|
609
|
+
this.element.hoverToggle = this.element.floaterConfig.hoverToggle;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
this.regenerateBibId();
|
|
613
|
+
this.handleTriggerTabIndex();
|
|
614
|
+
|
|
615
|
+
this.handleEvent = this.handleEvent.bind(this);
|
|
616
|
+
if (this.element.trigger) {
|
|
617
|
+
this.element.trigger.addEventListener('keydown', this.handleEvent);
|
|
618
|
+
this.element.trigger.addEventListener('click', this.handleEvent);
|
|
619
|
+
this.element.trigger.addEventListener('mouseenter', this.handleEvent);
|
|
620
|
+
this.element.trigger.addEventListener('mouseleave', this.handleEvent);
|
|
621
|
+
this.element.trigger.addEventListener('focus', this.handleEvent);
|
|
622
|
+
this.element.trigger.addEventListener('blur', this.handleEvent);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
disconnect() {
|
|
627
|
+
this.cleanupHideHandlers();
|
|
628
|
+
if (this.element) {
|
|
629
|
+
this.element.cleanup?.();
|
|
630
|
+
|
|
631
|
+
if (this.element.bib) {
|
|
632
|
+
this.element.shadowRoot.append(this.element.bib);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Remove event & keyboard listeners
|
|
636
|
+
if (this.element?.trigger) {
|
|
637
|
+
this.element.trigger.removeEventListener('keydown', this.handleEvent);
|
|
638
|
+
this.element.trigger.removeEventListener('click', this.handleEvent);
|
|
639
|
+
this.element.trigger.removeEventListener('mouseenter', this.handleEvent);
|
|
640
|
+
this.element.trigger.removeEventListener('mouseleave', this.handleEvent);
|
|
641
|
+
this.element.trigger.removeEventListener('focus', this.handleEvent);
|
|
642
|
+
this.element.trigger.removeEventListener('blur', this.handleEvent);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|