@aurodesignsystem/auro-library 5.11.3 → 5.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Semantic Release Automated Changelog
|
|
2
2
|
|
|
3
|
+
## [5.12.1](https://github.com/AlaskaAirlines/auro-library/compare/v5.12.0...v5.12.1) (2026-04-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* guard element access and add null-element safety test ([efd5f8d](https://github.com/AlaskaAirlines/auro-library/commit/efd5f8d978c18341d49a6908600dca0a5f94bfda))
|
|
9
|
+
* normalize null-element guards in getPositioningStrategy and setupHideHandlers ([d02617c](https://github.com/AlaskaAirlines/auro-library/commit/d02617c05998030f7c0ed15a8d207b3176191c58))
|
|
10
|
+
* tighten floatingUI null guard behavior ([e194e61](https://github.com/AlaskaAirlines/auro-library/commit/e194e61439ac9c49170c5145b75917fb301b9495))
|
|
11
|
+
|
|
12
|
+
# [5.12.0](https://github.com/AlaskaAirlines/auro-library/compare/v5.11.3...v5.12.0) (2026-04-01)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* add keyboard handling toggle to AuroFloatingUI ([100bbe9](https://github.com/AlaskaAirlines/auro-library/commit/100bbe940ff2f1589d8b738ce83631bd5c7b8568))
|
|
18
|
+
|
|
3
19
|
## [5.11.3](https://github.com/AlaskaAirlines/auro-library/compare/v5.11.2...v5.11.3) (2026-03-31)
|
|
4
20
|
|
|
5
21
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aurodesignsystem/auro-library",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.12.1",
|
|
4
4
|
"description": "This repository holds shared scripts, utilities, and workflows utilized across repositories along the Auro Design System.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { expect } from "@open-wc/testing";
|
|
2
|
+
import { html, render } from "lit";
|
|
3
|
+
import sinon from "sinon";
|
|
4
|
+
import AuroFloatingUI from "../../floatingUI.mjs";
|
|
5
|
+
|
|
6
|
+
async function fixture(template) {
|
|
7
|
+
const wrapper = document.createElement("div");
|
|
8
|
+
render(template, wrapper);
|
|
9
|
+
document.body.appendChild(wrapper);
|
|
10
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
11
|
+
return wrapper.firstElementChild;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Builds a minimal element stub that satisfies the properties AuroFloatingUI
|
|
16
|
+
* reads during configure() and keyboard event handling.
|
|
17
|
+
*/
|
|
18
|
+
function makeElement(trigger, bib) {
|
|
19
|
+
return {
|
|
20
|
+
trigger,
|
|
21
|
+
bib,
|
|
22
|
+
bibSizer: null,
|
|
23
|
+
triggerChevron: null,
|
|
24
|
+
shadowRoot: {
|
|
25
|
+
querySelector: () => null,
|
|
26
|
+
append: () => {},
|
|
27
|
+
},
|
|
28
|
+
behavior: "dropdown",
|
|
29
|
+
disabled: false,
|
|
30
|
+
isPopoverVisible: false,
|
|
31
|
+
showing: false,
|
|
32
|
+
noToggle: false,
|
|
33
|
+
modal: false,
|
|
34
|
+
disableEventShow: false,
|
|
35
|
+
hoverToggle: false,
|
|
36
|
+
focusShow: false,
|
|
37
|
+
floaterConfig: null,
|
|
38
|
+
cleanup: null,
|
|
39
|
+
contains: () => false,
|
|
40
|
+
dispatchEvent: () => {},
|
|
41
|
+
getAttribute: () => null,
|
|
42
|
+
setAttribute: () => {},
|
|
43
|
+
querySelectorAll: () => [],
|
|
44
|
+
style: {},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("AuroFloatingUI keyboard gate (enableKeyboardHandling)", () => {
|
|
49
|
+
let triggerEl;
|
|
50
|
+
let bibEl;
|
|
51
|
+
let elem;
|
|
52
|
+
let floatingUi;
|
|
53
|
+
|
|
54
|
+
beforeEach(async () => {
|
|
55
|
+
triggerEl = await fixture(html`<button id="trigger">Toggle</button>`);
|
|
56
|
+
bibEl = await fixture(html`<div id="bib"></div>`);
|
|
57
|
+
|
|
58
|
+
elem = makeElement(triggerEl, bibEl);
|
|
59
|
+
floatingUi = new AuroFloatingUI(elem, "dropdown");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterEach(async () => {
|
|
63
|
+
// Let any setTimeout(0) handlers (e.g. click listener setup in setupHideHandlers)
|
|
64
|
+
// fire before cleanup so cleanupHideHandlers can remove them.
|
|
65
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
66
|
+
document.expandedAuroFloater = null;
|
|
67
|
+
document.expandedAuroFormkitDropdown = null;
|
|
68
|
+
floatingUi?.disconnect();
|
|
69
|
+
floatingUi = null;
|
|
70
|
+
triggerEl?.parentNode?.remove();
|
|
71
|
+
bibEl?.parentNode?.remove();
|
|
72
|
+
sinon.restore();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("default behavior (enableKeyboardHandling = true)", () => {
|
|
76
|
+
beforeEach(() => {
|
|
77
|
+
floatingUi.configure(elem, "auro-dropdown", true);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("Enter on the trigger calls handleClick()", () => {
|
|
81
|
+
const spy = sinon.spy(floatingUi, "handleClick");
|
|
82
|
+
triggerEl.dispatchEvent(
|
|
83
|
+
new KeyboardEvent("keydown", {
|
|
84
|
+
key: "Enter",
|
|
85
|
+
bubbles: true,
|
|
86
|
+
composed: true,
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
expect(spy.calledOnce).to.be.true;
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("Space on the trigger calls handleClick()", () => {
|
|
93
|
+
const spy = sinon.spy(floatingUi, "handleClick");
|
|
94
|
+
triggerEl.dispatchEvent(
|
|
95
|
+
new KeyboardEvent("keydown", {
|
|
96
|
+
key: " ",
|
|
97
|
+
bubbles: true,
|
|
98
|
+
composed: true,
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
expect(spy.calledOnce).to.be.true;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("Escape dismisses when bib is visible", () => {
|
|
105
|
+
elem.isPopoverVisible = true;
|
|
106
|
+
floatingUi.showing = true;
|
|
107
|
+
document.expandedAuroFloater = floatingUi;
|
|
108
|
+
const spy = sinon.spy(floatingUi, "hideBib");
|
|
109
|
+
|
|
110
|
+
// setupHideHandlers attaches the document keydown listener
|
|
111
|
+
floatingUi.setupHideHandlers();
|
|
112
|
+
document.dispatchEvent(
|
|
113
|
+
new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(spy.calledOnce).to.be.true;
|
|
117
|
+
document.expandedAuroFloater = null;
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("keyboard disabled (enableKeyboardHandling = false)", () => {
|
|
122
|
+
beforeEach(() => {
|
|
123
|
+
floatingUi.configure(elem, "auro-dropdown", false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("Enter on the trigger does NOT call handleClick()", () => {
|
|
127
|
+
const spy = sinon.spy(floatingUi, "handleClick");
|
|
128
|
+
triggerEl.dispatchEvent(
|
|
129
|
+
new KeyboardEvent("keydown", {
|
|
130
|
+
key: "Enter",
|
|
131
|
+
bubbles: true,
|
|
132
|
+
composed: true,
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
expect(spy.called).to.be.false;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("Space on the trigger does NOT call handleClick()", () => {
|
|
139
|
+
const spy = sinon.spy(floatingUi, "handleClick");
|
|
140
|
+
triggerEl.dispatchEvent(
|
|
141
|
+
new KeyboardEvent("keydown", {
|
|
142
|
+
key: " ",
|
|
143
|
+
bubbles: true,
|
|
144
|
+
composed: true,
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
expect(spy.called).to.be.false;
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("Escape does NOT dismiss the bib", () => {
|
|
151
|
+
elem.isPopoverVisible = true;
|
|
152
|
+
floatingUi.showing = true;
|
|
153
|
+
document.expandedAuroFloater = floatingUi;
|
|
154
|
+
const spy = sinon.spy(floatingUi, "hideBib");
|
|
155
|
+
|
|
156
|
+
// setupHideHandlers should skip attaching the document keydown listener
|
|
157
|
+
floatingUi.setupHideHandlers();
|
|
158
|
+
document.dispatchEvent(
|
|
159
|
+
new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
expect(spy.called).to.be.false;
|
|
163
|
+
document.expandedAuroFloater = null;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("click on the trigger still calls handleClick()", () => {
|
|
167
|
+
const spy = sinon.spy(floatingUi, "handleClick");
|
|
168
|
+
triggerEl.dispatchEvent(
|
|
169
|
+
new MouseEvent("click", { bubbles: true, composed: true }),
|
|
170
|
+
);
|
|
171
|
+
expect(spy.calledOnce).to.be.true;
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("default argument", () => {
|
|
176
|
+
it("omitting enableKeyboardHandling defaults to true (Enter still works)", () => {
|
|
177
|
+
floatingUi.configure(elem, "auro-dropdown");
|
|
178
|
+
const spy = sinon.spy(floatingUi, "handleClick");
|
|
179
|
+
triggerEl.dispatchEvent(
|
|
180
|
+
new KeyboardEvent("keydown", {
|
|
181
|
+
key: "Enter",
|
|
182
|
+
bubbles: true,
|
|
183
|
+
composed: true,
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
expect(spy.calledOnce).to.be.true;
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
@@ -71,6 +71,11 @@ export default class AuroFloatingUI {
|
|
|
71
71
|
this.clickHandler = null;
|
|
72
72
|
this.keyDownHandler = null;
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* @private
|
|
76
|
+
*/
|
|
77
|
+
this.enableKeyboardHandling = true;
|
|
78
|
+
|
|
74
79
|
/**
|
|
75
80
|
* @private
|
|
76
81
|
*/
|
|
@@ -103,11 +108,19 @@ export default class AuroFloatingUI {
|
|
|
103
108
|
* This ensures that the bib content has the same dimensions as the sizer element.
|
|
104
109
|
*/
|
|
105
110
|
mirrorSize() {
|
|
111
|
+
const element = this.element;
|
|
112
|
+
if (!element) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
106
116
|
// mirror the boxsize from bibSizer
|
|
107
|
-
if (
|
|
108
|
-
const sizerStyle = window.getComputedStyle(
|
|
109
|
-
const bibContent =
|
|
110
|
-
|
|
117
|
+
if (element.bibSizer && element.matchWidth && element.bib?.shadowRoot) {
|
|
118
|
+
const sizerStyle = window.getComputedStyle(element.bibSizer);
|
|
119
|
+
const bibContent = element.bib.shadowRoot.querySelector(".container");
|
|
120
|
+
if (!bibContent) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
111
124
|
if (sizerStyle.width !== "0px") {
|
|
112
125
|
bibContent.style.width = sizerStyle.width;
|
|
113
126
|
}
|
|
@@ -129,9 +142,14 @@ export default class AuroFloatingUI {
|
|
|
129
142
|
* @returns {String} The positioning strategy, one of 'fullscreen', 'floating', 'cover'.
|
|
130
143
|
*/
|
|
131
144
|
getPositioningStrategy() {
|
|
145
|
+
const element = this.element;
|
|
146
|
+
if (!element) {
|
|
147
|
+
return "floating";
|
|
148
|
+
}
|
|
149
|
+
|
|
132
150
|
const breakpoint =
|
|
133
|
-
|
|
134
|
-
|
|
151
|
+
element.bib?.mobileFullscreenBreakpoint ||
|
|
152
|
+
element.floaterConfig?.fullscreenBreakpoint;
|
|
135
153
|
switch (this.behavior) {
|
|
136
154
|
case "tooltip":
|
|
137
155
|
return "floating";
|
|
@@ -142,9 +160,9 @@ export default class AuroFloatingUI {
|
|
|
142
160
|
`(max-width: ${breakpoint})`,
|
|
143
161
|
).matches;
|
|
144
162
|
|
|
145
|
-
|
|
163
|
+
element.expanded = smallerThanBreakpoint;
|
|
146
164
|
}
|
|
147
|
-
if (
|
|
165
|
+
if (element.nested) {
|
|
148
166
|
return "cover";
|
|
149
167
|
}
|
|
150
168
|
return "fullscreen";
|
|
@@ -174,42 +192,65 @@ export default class AuroFloatingUI {
|
|
|
174
192
|
* and applies the calculated position to the bib's style.
|
|
175
193
|
*/
|
|
176
194
|
position() {
|
|
195
|
+
const element = this.element;
|
|
196
|
+
if (!element) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
177
200
|
const strategy = this.getPositioningStrategy();
|
|
178
201
|
this.configureBibStrategy(strategy);
|
|
179
202
|
|
|
180
203
|
if (strategy === "floating") {
|
|
204
|
+
if (!element.trigger || !element.bib) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
181
208
|
this.mirrorSize();
|
|
182
209
|
// Define the middlware for the floater configuration
|
|
183
210
|
const middleware = [
|
|
184
|
-
offset(
|
|
185
|
-
...(
|
|
186
|
-
...(
|
|
187
|
-
...(
|
|
211
|
+
offset(element.floaterConfig?.offset || 0),
|
|
212
|
+
...(element.floaterConfig?.shift ? [shift()] : []), // Add shift middleware if shift is enabled.
|
|
213
|
+
...(element.floaterConfig?.flip ? [flip()] : []), // Add flip middleware if flip is enabled.
|
|
214
|
+
...(element.floaterConfig?.autoPlacement ? [autoPlacement()] : []), // Add autoPlacement middleware if autoPlacement is enabled.
|
|
188
215
|
];
|
|
189
216
|
|
|
190
217
|
// Compute the position of the bib
|
|
191
|
-
computePosition(
|
|
192
|
-
strategy:
|
|
193
|
-
placement:
|
|
218
|
+
computePosition(element.trigger, element.bib, {
|
|
219
|
+
strategy: element.floaterConfig?.strategy || "fixed",
|
|
220
|
+
placement: element.floaterConfig?.placement,
|
|
194
221
|
middleware: middleware || [],
|
|
195
222
|
}).then(({ x, y }) => {
|
|
196
223
|
// eslint-disable-line id-length
|
|
197
|
-
|
|
224
|
+
const currentElement = this.element;
|
|
225
|
+
if (!currentElement?.bib) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
Object.assign(currentElement.bib.style, {
|
|
198
230
|
left: `${x}px`,
|
|
199
231
|
top: `${y}px`,
|
|
200
232
|
});
|
|
201
233
|
});
|
|
202
234
|
} else if (strategy === "cover") {
|
|
235
|
+
if (!element.parentNode || !element.bib) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
203
239
|
// Compute the position of the bib
|
|
204
|
-
computePosition(
|
|
240
|
+
computePosition(element.parentNode, element.bib, {
|
|
205
241
|
placement: "bottom-start",
|
|
206
242
|
}).then(({ x, y }) => {
|
|
207
243
|
// eslint-disable-line id-length
|
|
208
|
-
|
|
244
|
+
const currentElement = this.element;
|
|
245
|
+
if (!currentElement?.bib || !currentElement.parentNode) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
Object.assign(currentElement.bib.style, {
|
|
209
250
|
left: `${x}px`,
|
|
210
|
-
top: `${y -
|
|
211
|
-
width: `${
|
|
212
|
-
height: `${
|
|
251
|
+
top: `${y - currentElement.parentNode.offsetHeight}px`,
|
|
252
|
+
width: `${currentElement.parentNode.offsetWidth}px`,
|
|
253
|
+
height: `${currentElement.parentNode.offsetHeight}px`,
|
|
213
254
|
});
|
|
214
255
|
});
|
|
215
256
|
}
|
|
@@ -221,11 +262,17 @@ export default class AuroFloatingUI {
|
|
|
221
262
|
* @param {Boolean} lock - If true, locks the body's scrolling functionlity; otherwise, unlock.
|
|
222
263
|
*/
|
|
223
264
|
lockScroll(lock = true) {
|
|
265
|
+
const element = this.element;
|
|
266
|
+
|
|
224
267
|
if (lock) {
|
|
268
|
+
if (!element?.bib) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
225
272
|
document.body.style.overflow = "hidden"; // hide body's scrollbar
|
|
226
273
|
|
|
227
274
|
// Move `bib` by the amount the viewport is shifted to stay aligned in fullscreen.
|
|
228
|
-
|
|
275
|
+
element.bib.style.transform = `translateY(${window?.visualViewport?.offsetTop}px)`;
|
|
229
276
|
} else {
|
|
230
277
|
document.body.style.overflow = "";
|
|
231
278
|
}
|
|
@@ -241,20 +288,24 @@ export default class AuroFloatingUI {
|
|
|
241
288
|
* @param {string} strategy - The positioning strategy ('fullscreen' or 'floating').
|
|
242
289
|
*/
|
|
243
290
|
configureBibStrategy(value) {
|
|
291
|
+
const element = this.element;
|
|
292
|
+
if (!element?.bib) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
244
296
|
if (value === "fullscreen") {
|
|
245
|
-
|
|
297
|
+
element.isBibFullscreen = true;
|
|
246
298
|
// reset the prev position
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
299
|
+
element.bib.setAttribute("isfullscreen", "");
|
|
300
|
+
element.bib.style.position = "fixed";
|
|
301
|
+
element.bib.style.top = "0px";
|
|
302
|
+
element.bib.style.left = "0px";
|
|
303
|
+
element.bib.style.width = "";
|
|
304
|
+
element.bib.style.height = "";
|
|
305
|
+
element.style.contain = "";
|
|
254
306
|
|
|
255
307
|
// reset the size that was mirroring `size` css-part
|
|
256
|
-
const bibContent =
|
|
257
|
-
this.element.bib.shadowRoot.querySelector(".container");
|
|
308
|
+
const bibContent = element.bib.shadowRoot?.querySelector(".container");
|
|
258
309
|
if (bibContent) {
|
|
259
310
|
bibContent.style.width = "";
|
|
260
311
|
bibContent.style.height = "";
|
|
@@ -269,14 +320,14 @@ export default class AuroFloatingUI {
|
|
|
269
320
|
}, 0);
|
|
270
321
|
}
|
|
271
322
|
|
|
272
|
-
if (
|
|
323
|
+
if (element.isPopoverVisible) {
|
|
273
324
|
this.lockScroll(true);
|
|
274
325
|
}
|
|
275
326
|
} else {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
327
|
+
element.bib.style.position = "";
|
|
328
|
+
element.bib.removeAttribute("isfullscreen");
|
|
329
|
+
element.isBibFullscreen = false;
|
|
330
|
+
element.style.contain = "layout";
|
|
280
331
|
}
|
|
281
332
|
|
|
282
333
|
const isChanged = this.strategy && this.strategy !== value;
|
|
@@ -294,16 +345,21 @@ export default class AuroFloatingUI {
|
|
|
294
345
|
},
|
|
295
346
|
);
|
|
296
347
|
|
|
297
|
-
|
|
348
|
+
element.dispatchEvent(event);
|
|
298
349
|
}
|
|
299
350
|
}
|
|
300
351
|
|
|
301
352
|
updateState() {
|
|
302
|
-
const
|
|
353
|
+
const element = this.element;
|
|
354
|
+
if (!element) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const isVisible = element.isPopoverVisible;
|
|
303
359
|
if (!isVisible) {
|
|
304
360
|
this.cleanupHideHandlers();
|
|
305
361
|
try {
|
|
306
|
-
|
|
362
|
+
element.cleanup?.();
|
|
307
363
|
} catch (error) {
|
|
308
364
|
// Do nothing
|
|
309
365
|
}
|
|
@@ -319,28 +375,30 @@ export default class AuroFloatingUI {
|
|
|
319
375
|
* If not, and if the bib isn't in fullscreen mode with focus lost, it hides the bib.
|
|
320
376
|
*/
|
|
321
377
|
handleFocusLoss() {
|
|
378
|
+
const element = this.element;
|
|
379
|
+
if (!element?.bib) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
322
383
|
// if mouse is being pressed, skip and let click event to handle the action
|
|
323
384
|
if (AuroFloatingUI.isMousePressed) {
|
|
324
385
|
return;
|
|
325
386
|
}
|
|
326
387
|
|
|
327
388
|
if (
|
|
328
|
-
|
|
329
|
-
|
|
389
|
+
element.noHideOnThisFocusLoss ||
|
|
390
|
+
element.hasAttribute("noHideOnThisFocusLoss")
|
|
330
391
|
) {
|
|
331
392
|
return;
|
|
332
393
|
}
|
|
333
394
|
|
|
334
395
|
// if focus is still inside of trigger or bib, do not close
|
|
335
|
-
if (
|
|
336
|
-
this.element.matches(":focus") ||
|
|
337
|
-
this.element.matches(":focus-within")
|
|
338
|
-
) {
|
|
396
|
+
if (element.matches(":focus") || element.matches(":focus-within")) {
|
|
339
397
|
return;
|
|
340
398
|
}
|
|
341
399
|
|
|
342
400
|
// if fullscreen bib is in fullscreen mode, do not close
|
|
343
|
-
if (
|
|
401
|
+
if (element.bib.hasAttribute("isfullscreen")) {
|
|
344
402
|
return;
|
|
345
403
|
}
|
|
346
404
|
|
|
@@ -348,23 +406,33 @@ export default class AuroFloatingUI {
|
|
|
348
406
|
}
|
|
349
407
|
|
|
350
408
|
setupHideHandlers() {
|
|
409
|
+
const element = this.element;
|
|
410
|
+
if (!element) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
351
414
|
// Define handlers & store references
|
|
352
415
|
this.focusHandler = () => this.handleFocusLoss();
|
|
353
416
|
|
|
354
417
|
this.clickHandler = (evt) => {
|
|
418
|
+
const element = this.element;
|
|
419
|
+
if (!element?.bib) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
355
423
|
// When the bib is fullscreen (modal dialog), don't close on outside
|
|
356
424
|
// clicks. VoiceOver's synthetic click events inside a top-layer modal
|
|
357
425
|
// <dialog> may not include the bib in composedPath(), causing false
|
|
358
426
|
// positives. This mirrors the fullscreen guard in handleFocusLoss().
|
|
359
|
-
if (
|
|
427
|
+
if (element.bib.hasAttribute("isfullscreen")) {
|
|
360
428
|
return;
|
|
361
429
|
}
|
|
362
430
|
|
|
363
431
|
if (
|
|
364
|
-
(!evt.composedPath().includes(
|
|
365
|
-
!evt.composedPath().includes(
|
|
366
|
-
(
|
|
367
|
-
evt.composedPath().includes(
|
|
432
|
+
(!evt.composedPath().includes(element.trigger) &&
|
|
433
|
+
!evt.composedPath().includes(element.bib)) ||
|
|
434
|
+
(element.bib.backdrop &&
|
|
435
|
+
evt.composedPath().includes(element.bib.backdrop))
|
|
368
436
|
) {
|
|
369
437
|
const existedVisibleFloatingUI =
|
|
370
438
|
document.expandedAuroFormkitDropdown || document.expandedAuroFloater;
|
|
@@ -385,7 +453,12 @@ export default class AuroFloatingUI {
|
|
|
385
453
|
|
|
386
454
|
// ESC key handler
|
|
387
455
|
this.keyDownHandler = (evt) => {
|
|
388
|
-
|
|
456
|
+
const element = this.element;
|
|
457
|
+
if (!element) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (evt.key === "Escape" && element.isPopoverVisible) {
|
|
389
462
|
const existedVisibleFloatingUI =
|
|
390
463
|
document.expandedAuroFormkitDropdown || document.expandedAuroFloater;
|
|
391
464
|
if (
|
|
@@ -405,7 +478,9 @@ export default class AuroFloatingUI {
|
|
|
405
478
|
document.addEventListener("focusin", this.focusHandler);
|
|
406
479
|
}
|
|
407
480
|
|
|
408
|
-
|
|
481
|
+
if (this.enableKeyboardHandling) {
|
|
482
|
+
document.addEventListener("keydown", this.keyDownHandler);
|
|
483
|
+
}
|
|
409
484
|
|
|
410
485
|
// send this task to the end of queue to prevent conflicting
|
|
411
486
|
// it conflicts if showBib gets call from a button that's not this.element.trigger
|
|
@@ -440,6 +515,10 @@ export default class AuroFloatingUI {
|
|
|
440
515
|
}
|
|
441
516
|
|
|
442
517
|
updateCurrentExpandedDropdown() {
|
|
518
|
+
if (!this.element) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
443
522
|
// Close any other dropdown that is already open
|
|
444
523
|
const existedVisibleFloatingUI =
|
|
445
524
|
document.expandedAuroFormkitDropdown || document.expandedAuroFloater;
|
|
@@ -456,25 +535,34 @@ export default class AuroFloatingUI {
|
|
|
456
535
|
}
|
|
457
536
|
|
|
458
537
|
showBib() {
|
|
459
|
-
|
|
538
|
+
const element = this.element;
|
|
539
|
+
if (!element) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (!element.bib || (!element.trigger && !element.parentNode)) {
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (!element.disabled && !this.showing) {
|
|
460
548
|
this.updateCurrentExpandedDropdown();
|
|
461
|
-
|
|
549
|
+
element.triggerChevron?.setAttribute("data-expanded", true);
|
|
462
550
|
|
|
463
551
|
// prevent double showing: isPopovervisible gets first and showBib gets called later
|
|
464
552
|
if (!this.showing) {
|
|
465
|
-
if (!
|
|
553
|
+
if (!element.modal) {
|
|
466
554
|
this.setupHideHandlers();
|
|
467
555
|
}
|
|
468
556
|
this.showing = true;
|
|
469
|
-
|
|
557
|
+
element.isPopoverVisible = true;
|
|
470
558
|
this.position();
|
|
471
559
|
this.dispatchEventDropdownToggle();
|
|
472
560
|
}
|
|
473
561
|
|
|
474
562
|
// Setup auto update to handle resize and scroll
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
563
|
+
element.cleanup = autoUpdate(
|
|
564
|
+
element.trigger || element.parentNode,
|
|
565
|
+
element.bib,
|
|
478
566
|
() => {
|
|
479
567
|
this.position();
|
|
480
568
|
},
|
|
@@ -487,22 +575,27 @@ export default class AuroFloatingUI {
|
|
|
487
575
|
* @param {String} eventType - The event type that triggered the hiding action.
|
|
488
576
|
*/
|
|
489
577
|
hideBib(eventType = "unknown") {
|
|
490
|
-
|
|
578
|
+
const element = this.element;
|
|
579
|
+
if (!element) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (element.disabled) {
|
|
491
584
|
return;
|
|
492
585
|
}
|
|
493
586
|
|
|
494
587
|
// noToggle dropdowns should not close when the trigger is clicked (the
|
|
495
588
|
// "toggle" behavior), but they CAN still close via other interactions like
|
|
496
589
|
// Escape key or focus loss.
|
|
497
|
-
if (
|
|
590
|
+
if (element.noToggle && eventType === "click") {
|
|
498
591
|
return;
|
|
499
592
|
}
|
|
500
593
|
|
|
501
594
|
this.lockScroll(false);
|
|
502
|
-
|
|
595
|
+
element.triggerChevron?.removeAttribute("data-expanded");
|
|
503
596
|
|
|
504
|
-
if (
|
|
505
|
-
|
|
597
|
+
if (element.isPopoverVisible) {
|
|
598
|
+
element.isPopoverVisible = false;
|
|
506
599
|
}
|
|
507
600
|
if (this.showing) {
|
|
508
601
|
this.cleanupHideHandlers();
|
|
@@ -522,6 +615,11 @@ export default class AuroFloatingUI {
|
|
|
522
615
|
* @param {String} eventType - The event type that triggered the toggle action.
|
|
523
616
|
*/
|
|
524
617
|
dispatchEventDropdownToggle(eventType) {
|
|
618
|
+
const element = this.element;
|
|
619
|
+
if (!element) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
525
623
|
const event = new CustomEvent(
|
|
526
624
|
this.eventPrefix ? `${this.eventPrefix}-toggled` : "toggled",
|
|
527
625
|
{
|
|
@@ -533,11 +631,16 @@ export default class AuroFloatingUI {
|
|
|
533
631
|
},
|
|
534
632
|
);
|
|
535
633
|
|
|
536
|
-
|
|
634
|
+
element.dispatchEvent(event);
|
|
537
635
|
}
|
|
538
636
|
|
|
539
637
|
handleClick() {
|
|
540
|
-
|
|
638
|
+
const element = this.element;
|
|
639
|
+
if (!element) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (element.isPopoverVisible) {
|
|
541
644
|
this.hideBib("click");
|
|
542
645
|
} else {
|
|
543
646
|
this.showBib();
|
|
@@ -548,64 +651,67 @@ export default class AuroFloatingUI {
|
|
|
548
651
|
{
|
|
549
652
|
composed: true,
|
|
550
653
|
detail: {
|
|
551
|
-
expanded:
|
|
654
|
+
expanded: element.isPopoverVisible,
|
|
552
655
|
},
|
|
553
656
|
},
|
|
554
657
|
);
|
|
555
658
|
|
|
556
|
-
|
|
659
|
+
element.dispatchEvent(event);
|
|
557
660
|
}
|
|
558
661
|
|
|
559
662
|
handleEvent(event) {
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
case "mouseenter":
|
|
577
|
-
if (this.element.hoverToggle) {
|
|
578
|
-
this.showBib();
|
|
579
|
-
}
|
|
580
|
-
break;
|
|
581
|
-
case "mouseleave":
|
|
582
|
-
if (this.element.hoverToggle) {
|
|
583
|
-
this.hideBib("mouseleave");
|
|
584
|
-
}
|
|
585
|
-
break;
|
|
586
|
-
case "focus":
|
|
587
|
-
if (this.element.focusShow) {
|
|
588
|
-
/*
|
|
589
|
-
This needs to better handle clicking that gives focus -
|
|
590
|
-
currently it shows and then immediately hides the bib
|
|
591
|
-
*/
|
|
592
|
-
this.showBib();
|
|
593
|
-
}
|
|
594
|
-
break;
|
|
595
|
-
case "blur":
|
|
596
|
-
// send this task 100ms later queue to
|
|
597
|
-
// wait a frame in case focus moves within the floating element/bib
|
|
598
|
-
setTimeout(() => this.handleFocusLoss(), 0);
|
|
599
|
-
break;
|
|
600
|
-
case "click":
|
|
601
|
-
if (document.activeElement === document.body) {
|
|
602
|
-
event.currentTarget.focus();
|
|
603
|
-
}
|
|
663
|
+
const element = this.element;
|
|
664
|
+
if (!element || element.disableEventShow) {
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
switch (event.type) {
|
|
669
|
+
case "keydown": {
|
|
670
|
+
// Support both Enter and Space keys for accessibility
|
|
671
|
+
// Space is included as it's expected behavior for interactive elements
|
|
672
|
+
|
|
673
|
+
const origin = event.composedPath()[0];
|
|
674
|
+
if (
|
|
675
|
+
event.key === "Enter" ||
|
|
676
|
+
(event.key === " " && (!origin || origin.tagName !== "INPUT"))
|
|
677
|
+
) {
|
|
678
|
+
event.preventDefault();
|
|
604
679
|
this.handleClick();
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
// Do nothing
|
|
680
|
+
}
|
|
681
|
+
break;
|
|
608
682
|
}
|
|
683
|
+
case "mouseenter":
|
|
684
|
+
if (element.hoverToggle) {
|
|
685
|
+
this.showBib();
|
|
686
|
+
}
|
|
687
|
+
break;
|
|
688
|
+
case "mouseleave":
|
|
689
|
+
if (element.hoverToggle) {
|
|
690
|
+
this.hideBib("mouseleave");
|
|
691
|
+
}
|
|
692
|
+
break;
|
|
693
|
+
case "focus":
|
|
694
|
+
if (element.focusShow) {
|
|
695
|
+
/*
|
|
696
|
+
This needs to better handle clicking that gives focus -
|
|
697
|
+
currently it shows and then immediately hides the bib
|
|
698
|
+
*/
|
|
699
|
+
this.showBib();
|
|
700
|
+
}
|
|
701
|
+
break;
|
|
702
|
+
case "blur":
|
|
703
|
+
// send this task 100ms later queue to
|
|
704
|
+
// wait a frame in case focus moves within the floating element/bib
|
|
705
|
+
setTimeout(() => this.handleFocusLoss(), 0);
|
|
706
|
+
break;
|
|
707
|
+
case "click":
|
|
708
|
+
if (document.activeElement === document.body) {
|
|
709
|
+
event.currentTarget.focus();
|
|
710
|
+
}
|
|
711
|
+
this.handleClick();
|
|
712
|
+
break;
|
|
713
|
+
default:
|
|
714
|
+
// Do nothing
|
|
609
715
|
}
|
|
610
716
|
}
|
|
611
717
|
|
|
@@ -616,6 +722,11 @@ export default class AuroFloatingUI {
|
|
|
616
722
|
* This prevents the component itself from being focusable when the trigger element already handles focus.
|
|
617
723
|
*/
|
|
618
724
|
handleTriggerTabIndex() {
|
|
725
|
+
const element = this.element;
|
|
726
|
+
if (!element) {
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
|
|
619
730
|
const focusableElementSelectors = [
|
|
620
731
|
"a",
|
|
621
732
|
"button",
|
|
@@ -628,7 +739,7 @@ export default class AuroFloatingUI {
|
|
|
628
739
|
"auro-hyperlink",
|
|
629
740
|
];
|
|
630
741
|
|
|
631
|
-
const triggerNode =
|
|
742
|
+
const triggerNode = element.querySelectorAll('[slot="trigger"]')[0];
|
|
632
743
|
if (!triggerNode) {
|
|
633
744
|
return;
|
|
634
745
|
}
|
|
@@ -637,13 +748,13 @@ export default class AuroFloatingUI {
|
|
|
637
748
|
focusableElementSelectors.forEach((selector) => {
|
|
638
749
|
// Check if the trigger node element is focusable
|
|
639
750
|
if (triggerNodeTagName === selector) {
|
|
640
|
-
|
|
751
|
+
element.tabIndex = -1;
|
|
641
752
|
return;
|
|
642
753
|
}
|
|
643
754
|
|
|
644
755
|
// Check if any child is focusable
|
|
645
756
|
if (triggerNode.querySelector(selector)) {
|
|
646
|
-
|
|
757
|
+
element.tabIndex = -1;
|
|
647
758
|
}
|
|
648
759
|
});
|
|
649
760
|
}
|
|
@@ -653,82 +764,92 @@ export default class AuroFloatingUI {
|
|
|
653
764
|
* @param {*} eventPrefix
|
|
654
765
|
*/
|
|
655
766
|
regenerateBibId() {
|
|
656
|
-
|
|
767
|
+
const element = this.element;
|
|
768
|
+
if (!element) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
this.id = element.getAttribute("id");
|
|
657
773
|
if (!this.id) {
|
|
658
774
|
this.id = window.crypto.randomUUID();
|
|
659
|
-
|
|
775
|
+
element.setAttribute("id", this.id);
|
|
660
776
|
}
|
|
661
777
|
|
|
662
|
-
|
|
778
|
+
element.bib?.setAttribute("id", `${this.id}-floater-bib`);
|
|
663
779
|
}
|
|
664
780
|
|
|
665
|
-
configure(elem, eventPrefix) {
|
|
781
|
+
configure(elem, eventPrefix, enableKeyboardHandling = true) {
|
|
666
782
|
AuroFloatingUI.setupMousePressChecker();
|
|
783
|
+
this.enableKeyboardHandling = enableKeyboardHandling;
|
|
667
784
|
|
|
668
785
|
this.eventPrefix = eventPrefix;
|
|
669
786
|
if (this.element !== elem) {
|
|
670
787
|
this.element = elem;
|
|
671
788
|
}
|
|
672
789
|
|
|
673
|
-
|
|
674
|
-
|
|
790
|
+
const element = this.element;
|
|
791
|
+
if (!element) {
|
|
792
|
+
return;
|
|
675
793
|
}
|
|
676
794
|
|
|
677
|
-
if (this.element.
|
|
795
|
+
if (this.behavior !== element.behavior) {
|
|
796
|
+
this.behavior = element.behavior;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (element.trigger) {
|
|
678
800
|
this.disconnect();
|
|
679
801
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
this.element.shadowRoot.querySelector("#showStateIcon");
|
|
802
|
+
element.trigger =
|
|
803
|
+
element.triggerElement ||
|
|
804
|
+
element.shadowRoot?.querySelector("#trigger") ||
|
|
805
|
+
element.trigger;
|
|
806
|
+
element.bib = element.shadowRoot?.querySelector("#bib") || element.bib;
|
|
807
|
+
element.bibSizer = element.shadowRoot?.querySelector("#bibSizer");
|
|
808
|
+
element.triggerChevron =
|
|
809
|
+
element.shadowRoot?.querySelector("#showStateIcon");
|
|
689
810
|
|
|
690
|
-
if (
|
|
691
|
-
|
|
811
|
+
if (element.floaterConfig) {
|
|
812
|
+
element.hoverToggle = element.floaterConfig.hoverToggle;
|
|
692
813
|
}
|
|
693
814
|
|
|
694
815
|
this.regenerateBibId();
|
|
695
816
|
this.handleTriggerTabIndex();
|
|
696
817
|
|
|
697
818
|
this.handleEvent = this.handleEvent.bind(this);
|
|
698
|
-
if (
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
819
|
+
if (element.trigger) {
|
|
820
|
+
if (this.enableKeyboardHandling) {
|
|
821
|
+
element.trigger.addEventListener("keydown", this.handleEvent);
|
|
822
|
+
}
|
|
823
|
+
element.trigger.addEventListener("click", this.handleEvent);
|
|
824
|
+
element.trigger.addEventListener("mouseenter", this.handleEvent);
|
|
825
|
+
element.trigger.addEventListener("mouseleave", this.handleEvent);
|
|
826
|
+
element.trigger.addEventListener("focus", this.handleEvent);
|
|
827
|
+
element.trigger.addEventListener("blur", this.handleEvent);
|
|
705
828
|
}
|
|
706
829
|
}
|
|
707
830
|
|
|
708
831
|
disconnect() {
|
|
709
832
|
this.cleanupHideHandlers();
|
|
710
|
-
if (this.element) {
|
|
711
|
-
this.element.cleanup?.();
|
|
712
833
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
834
|
+
const element = this.element;
|
|
835
|
+
if (!element) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
716
838
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
}
|
|
839
|
+
element.cleanup?.();
|
|
840
|
+
|
|
841
|
+
if (element.bib && element.shadowRoot) {
|
|
842
|
+
element.shadowRoot.append(element.bib);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Remove event & keyboard listeners
|
|
846
|
+
if (element.trigger) {
|
|
847
|
+
element.trigger.removeEventListener("keydown", this.handleEvent);
|
|
848
|
+
element.trigger.removeEventListener("click", this.handleEvent);
|
|
849
|
+
element.trigger.removeEventListener("mouseenter", this.handleEvent);
|
|
850
|
+
element.trigger.removeEventListener("mouseleave", this.handleEvent);
|
|
851
|
+
element.trigger.removeEventListener("focus", this.handleEvent);
|
|
852
|
+
element.trigger.removeEventListener("blur", this.handleEvent);
|
|
732
853
|
}
|
|
733
854
|
}
|
|
734
855
|
}
|
|
@@ -99,4 +99,34 @@ describe("AuroFloatingUI", () => {
|
|
|
99
99
|
expect(checkedSelectors).to.deep.equal([":focus", ":focus-within"]);
|
|
100
100
|
expect(hideBibSpy.calledOnceWithExactly("keydown")).to.be.true;
|
|
101
101
|
});
|
|
102
|
+
|
|
103
|
+
it("no-ops safely when element is not set", () => {
|
|
104
|
+
floatingUI.element = null;
|
|
105
|
+
|
|
106
|
+
expect(() => floatingUI.showBib()).to.not.throw();
|
|
107
|
+
expect(() => floatingUI.hideBib()).to.not.throw();
|
|
108
|
+
expect(() => floatingUI.handleClick()).to.not.throw();
|
|
109
|
+
expect(() => floatingUI.handleEvent(new Event("click"))).to.not.throw();
|
|
110
|
+
expect(() => floatingUI.handleFocusLoss()).to.not.throw();
|
|
111
|
+
expect(() => floatingUI.updateState()).to.not.throw();
|
|
112
|
+
expect(() => floatingUI.configureBibStrategy("floating")).to.not.throw();
|
|
113
|
+
expect(() => floatingUI.position()).to.not.throw();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("does not enter a visible state when required DOM nodes are missing", () => {
|
|
117
|
+
host.bib = null;
|
|
118
|
+
host.isPopoverVisible = false;
|
|
119
|
+
|
|
120
|
+
floatingUI.showBib();
|
|
121
|
+
|
|
122
|
+
expect(floatingUI.showing).to.equal(false);
|
|
123
|
+
expect(host.isPopoverVisible).to.equal(false);
|
|
124
|
+
expect(document.expandedAuroFloater).to.not.equal(floatingUI);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("returns an explicit positioning strategy when element is not set", () => {
|
|
128
|
+
floatingUI.element = null;
|
|
129
|
+
|
|
130
|
+
expect(floatingUI.getPositioningStrategy()).to.equal("floating");
|
|
131
|
+
});
|
|
102
132
|
});
|