@aurodesignsystem/auro-library 5.11.2 → 5.12.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,19 @@
1
1
  # Semantic Release Automated Changelog
2
2
 
3
+ # [5.12.0](https://github.com/AlaskaAirlines/auro-library/compare/v5.11.3...v5.12.0) (2026-04-01)
4
+
5
+
6
+ ### Features
7
+
8
+ * add keyboard handling toggle to AuroFloatingUI ([100bbe9](https://github.com/AlaskaAirlines/auro-library/commit/100bbe940ff2f1589d8b738ce83631bd5c7b8568))
9
+
10
+ ## [5.11.3](https://github.com/AlaskaAirlines/auro-library/compare/v5.11.2...v5.11.3) (2026-03-31)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * correct focus local check for shadow DOM ([e713cfc](https://github.com/AlaskaAirlines/auro-library/commit/e713cfc07f1be4809c97e05af97b89050478e988))
16
+
3
17
  ## [5.11.2](https://github.com/AlaskaAirlines/auro-library/compare/v5.11.1...v5.11.2) (2026-03-17)
4
18
 
5
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aurodesignsystem/auro-library",
3
- "version": "5.11.2",
3
+ "version": "5.12.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",
@@ -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
  */
@@ -331,11 +336,10 @@ export default class AuroFloatingUI {
331
336
  return;
332
337
  }
333
338
 
334
- const { activeElement } = document;
335
339
  // if focus is still inside of trigger or bib, do not close
336
340
  if (
337
- this.element.contains(activeElement) ||
338
- this.element.bib?.contains(activeElement)
341
+ this.element.matches(":focus") ||
342
+ this.element.matches(":focus-within")
339
343
  ) {
340
344
  return;
341
345
  }
@@ -406,7 +410,9 @@ export default class AuroFloatingUI {
406
410
  document.addEventListener("focusin", this.focusHandler);
407
411
  }
408
412
 
409
- document.addEventListener("keydown", this.keyDownHandler);
413
+ if (this.enableKeyboardHandling) {
414
+ document.addEventListener("keydown", this.keyDownHandler);
415
+ }
410
416
 
411
417
  // send this task to the end of queue to prevent conflicting
412
418
  // it conflicts if showBib gets call from a button that's not this.element.trigger
@@ -663,8 +669,9 @@ export default class AuroFloatingUI {
663
669
  this.element.bib.setAttribute("id", `${this.id}-floater-bib`);
664
670
  }
665
671
 
666
- configure(elem, eventPrefix) {
672
+ configure(elem, eventPrefix, enableKeyboardHandling = true) {
667
673
  AuroFloatingUI.setupMousePressChecker();
674
+ this.enableKeyboardHandling = enableKeyboardHandling;
668
675
 
669
676
  this.eventPrefix = eventPrefix;
670
677
  if (this.element !== elem) {
@@ -697,7 +704,9 @@ export default class AuroFloatingUI {
697
704
 
698
705
  this.handleEvent = this.handleEvent.bind(this);
699
706
  if (this.element.trigger) {
700
- this.element.trigger.addEventListener("keydown", this.handleEvent);
707
+ if (this.enableKeyboardHandling) {
708
+ this.element.trigger.addEventListener("keydown", this.handleEvent);
709
+ }
701
710
  this.element.trigger.addEventListener("click", this.handleEvent);
702
711
  this.element.trigger.addEventListener("mouseenter", this.handleEvent);
703
712
  this.element.trigger.addEventListener("mouseleave", this.handleEvent);
@@ -0,0 +1,102 @@
1
+ import { expect } from "@open-wc/testing";
2
+ import sinon from "sinon";
3
+ import AuroFloatingUI from "./floatingUI.mjs";
4
+
5
+ describe("AuroFloatingUI", () => {
6
+ let host;
7
+ let bib;
8
+ let floatingUI;
9
+ let hideBibSpy;
10
+
11
+ beforeEach(() => {
12
+ host = document.createElement("div");
13
+ bib = document.createElement("div");
14
+ host.bib = bib;
15
+ host.triggerChevron = document.createElement("span");
16
+
17
+ document.body.append(host, bib);
18
+
19
+ AuroFloatingUI.isMousePressed = false;
20
+ floatingUI = new AuroFloatingUI(host, "dropdown");
21
+ hideBibSpy = sinon.spy(floatingUI, "hideBib");
22
+ });
23
+
24
+ afterEach(() => {
25
+ sinon.restore();
26
+ AuroFloatingUI.isMousePressed = false;
27
+ host?.remove();
28
+ bib?.remove();
29
+ });
30
+
31
+ it("does not hide when the host matches focus-within", () => {
32
+ const checkedSelectors = [];
33
+
34
+ sinon.stub(host, "matches").callsFake((selector) => {
35
+ checkedSelectors.push(selector);
36
+
37
+ if (selector === ":focus") {
38
+ return false;
39
+ }
40
+
41
+ if (selector === ":focus-within") {
42
+ return true;
43
+ }
44
+
45
+ return false;
46
+ });
47
+
48
+ floatingUI.handleFocusLoss();
49
+
50
+ expect(checkedSelectors).to.deep.equal([":focus", ":focus-within"]);
51
+ expect(hideBibSpy.called).to.be.false;
52
+ });
53
+
54
+ it("does not hide when the host matches focus", () => {
55
+ const checkedSelectors = [];
56
+
57
+ sinon.stub(host, "matches").callsFake((selector) => {
58
+ checkedSelectors.push(selector);
59
+
60
+ if (selector === ":focus") {
61
+ return true;
62
+ }
63
+
64
+ return false;
65
+ });
66
+
67
+ floatingUI.handleFocusLoss();
68
+
69
+ expect(checkedSelectors).to.deep.equal([":focus"]);
70
+ expect(hideBibSpy.called).to.be.false;
71
+ });
72
+
73
+ it("does not hide when the bib is fullscreen", () => {
74
+ const checkedSelectors = [];
75
+
76
+ bib.setAttribute("isfullscreen", "");
77
+
78
+ sinon.stub(host, "matches").callsFake((selector) => {
79
+ checkedSelectors.push(selector);
80
+ return false;
81
+ });
82
+
83
+ floatingUI.handleFocusLoss();
84
+
85
+ expect(checkedSelectors).to.deep.equal([":focus", ":focus-within"]);
86
+ expect(hideBibSpy.called).to.be.false;
87
+ });
88
+
89
+ it("hides with a keydown event when the host no longer has focus", () => {
90
+ const checkedSelectors = [];
91
+
92
+ sinon.stub(host, "matches").callsFake((selector) => {
93
+ checkedSelectors.push(selector);
94
+ return false;
95
+ });
96
+
97
+ floatingUI.handleFocusLoss();
98
+
99
+ expect(checkedSelectors).to.deep.equal([":focus", ":focus-within"]);
100
+ expect(hideBibSpy.calledOnceWithExactly("keydown")).to.be.true;
101
+ });
102
+ });