@aurodesignsystem-dev/auro-library 0.0.0-pr187.0 → 0.0.0-pr192.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.4.0](https://github.com/AlaskaAirlines/auro-library/compare/v5.3.3...v5.4.0) (2025-08-14)
4
+
5
+
6
+ ### Features
7
+
8
+ * add a11yTransporter util ([817bb3a](https://github.com/AlaskaAirlines/auro-library/commit/817bb3acb9d20e99bc25a89f17db3a8f2ac91ab5))
9
+
10
+ ## [5.3.3](https://github.com/AlaskaAirlines/auro-library/compare/v5.3.2...v5.3.3) (2025-08-08)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * move test related dependencies to devDependencies ([3d877f8](https://github.com/AlaskaAirlines/auro-library/commit/3d877f86c8a98b9ecdb44d83329fe391a8379483))
16
+
3
17
  ## [5.3.2](https://github.com/AlaskaAirlines/auro-library/compare/v5.3.1...v5.3.2) (2025-07-16)
4
18
 
5
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aurodesignsystem-dev/auro-library",
3
- "version": "0.0.0-pr187.0",
3
+ "version": "0.0.0-pr192.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",
@@ -14,7 +14,16 @@
14
14
  "bin": {
15
15
  "generateDocs": "./bin/generateDocs.mjs"
16
16
  },
17
+ "dependencies": {
18
+ "@floating-ui/dom": "^1.6.11",
19
+ "handlebars": "^4.7.8",
20
+ "markdown-magic": "^2.6.1"
21
+ },
17
22
  "devDependencies": {
23
+ "sinon": "^20.0.0",
24
+ "npm-run-all": "^4.1.5",
25
+ "@open-wc/testing": "^4.0.0",
26
+ "@aurodesignsystem/auro-cli": "^2.5.0",
18
27
  "@aurodesignsystem/eslint-config": "1.3.4",
19
28
  "@commitlint/cli": "^18.5.0",
20
29
  "@commitlint/config-conventional": "^18.5.0",
@@ -79,14 +88,5 @@
79
88
  },
80
89
  "bugs": {
81
90
  "url": "https://github.com/AlaskaAirlines/auro-library/issues"
82
- },
83
- "dependencies": {
84
- "@aurodesignsystem/auro-cli": "^2.5.0",
85
- "@floating-ui/dom": "^1.6.11",
86
- "@open-wc/testing": "^4.0.0",
87
- "handlebars": "^4.7.8",
88
- "markdown-magic": "^2.6.1",
89
- "npm-run-all": "^4.1.5",
90
- "sinon": "^20.0.0"
91
91
  }
92
92
  }
@@ -10,17 +10,15 @@ export class FocusTrap {
10
10
  * Initializes event listeners and prepares the container for focus management.
11
11
  *
12
12
  * @param {HTMLElement} container The DOM element to trap focus within.
13
- * @param {boolean} [controlTabOrder=false] If true enables manual control of the tab order by the FocusTrap.
14
13
  * @throws {Error} If the provided container is not a valid HTMLElement.
15
14
  */
16
- constructor(container, controlTabOrder = false) {
15
+ constructor(container) {
17
16
  if (!container || !(container instanceof HTMLElement)) {
18
17
  throw new Error("FocusTrap requires a valid HTMLElement.");
19
18
  }
20
19
 
21
20
  this.container = container;
22
- this.tabDirection = 'forward'; // or 'backward';
23
- this.controlTabOrder = controlTabOrder;
21
+ this.tabDirection = 'forward'; // or 'backward'
24
22
 
25
23
  this._init();
26
24
  }
@@ -44,106 +42,46 @@ export class FocusTrap {
44
42
  }
45
43
 
46
44
  /**
47
- * Gets an array of currently active (focused) elements in the document and shadow DOM.
48
- * @returns {Array<HTMLElement>} An array of focusable elements within the container.
49
- * @private
50
- */
51
- _getActiveElements() {
52
- // Get the active element(s) in the document and shadow root
53
- // This will include the active element in the shadow DOM if it exists
54
- // Active element may be inside the shadow DOM depending on delegatesFocus, so we need to check both
55
- let {activeElement} = document;
56
- const actives = [activeElement];
57
- while (activeElement?.shadowRoot?.activeElement) {
58
- actives.push(activeElement.shadowRoot.activeElement);
59
- activeElement = activeElement.shadowRoot.activeElement;
60
- }
61
- return actives;
62
- }
63
-
64
- /**
65
- * Gets the next focus index based on the current index and focusable elements.
66
- * @param {number} currentIndex The current index of the focused element.
67
- * @param {Array<HTMLElement>} focusables The array of focusable elements.
68
- * @returns {number|null} The next focus index or null if not determined.
69
- */
70
- _getNextFocusIndex(currentIndex, focusables, actives) {
71
-
72
- // Determine if we need to wrap
73
- const atFirst = actives.includes(focusables[0]) || actives.includes(this.container);
74
- const atLast = actives.includes(focusables[focusables.length - 1]);
75
-
76
- if (this.controlTabOrder) {
77
- // Calculate the new index based on the current index and tab direction
78
- let newFocusIndex = currentIndex + (this.tabDirection === 'forward' ? 1 : -1);
79
-
80
- // Wrap-around logic
81
- if (newFocusIndex < 0) newFocusIndex = focusables.length - 1;
82
- if (newFocusIndex >= focusables.length) newFocusIndex = 0;
83
-
84
- return newFocusIndex;
85
- }
86
-
87
- // Only wrap if at the ends
88
- if (this.tabDirection === 'backward' && atFirst) {
89
- return focusables.length - 1;
90
- }
91
-
92
- if (this.tabDirection === 'forward' && atLast) {
93
- return 0;
94
- }
95
-
96
- // No wrap, so don't change focus, return early
97
- return null;
98
- }
99
-
100
- /**
101
- * Handles the Tab key press event to manage focus within the container.
102
- * @param {KeyboardEvent} e The keyboard event triggered by the user.
103
- * @returns {void}
104
- */
105
- _handleTabKey(e) {
106
-
107
- // Update the focusable elements
108
- const focusables = this._getFocusableElements();
109
-
110
- if (!focusables.length) {
111
- console.warn('FocusTrap: No focusable elements found in the container.');
112
- return;
113
- }
114
-
115
- // Set the tab direction based on the key pressed
116
- this.tabDirection = e.shiftKey ? 'backward' : 'forward';
117
-
118
- // Get the active elements that are currently focused
119
- const actives = this._getActiveElements();
120
-
121
- // If we're at either end of the focusable elements, wrap around to the other end
122
- let focusIndex = focusables.findIndex((el) => actives.includes(el));
123
-
124
- // Fallback if we have no focused element
125
- if (focusIndex === -1) focusIndex = 0;
126
-
127
- // Get the next focus index based on the current focus index, tab direction, and controlTabOrder setting
128
- // Is null if no new focus index is determined
129
- let newFocusIndex = this._getNextFocusIndex(focusIndex, focusables, actives);
130
-
131
- // If we have a new focus index, set focus to that element
132
- if (newFocusIndex !== null) {
133
- e.preventDefault();
134
- focusables[newFocusIndex].focus();
135
- }
136
- }
137
-
138
- /**
139
- * Catches the keydown event
45
+ * Handles keydown events to manage tab navigation within the container.
46
+ * Ensures that focus wraps around when reaching the first or last focusable element.
47
+ *
140
48
  * @param {KeyboardEvent} e The keyboard event triggered by user interaction.
141
49
  * @private
142
50
  */
143
51
  _onKeydown = (e) => {
144
-
145
- // Handle tab
146
- if (e.key === 'Tab') this._handleTabKey(e);
52
+
53
+ if (e.key === 'Tab') {
54
+
55
+ // Set the tab direction based on the key pressed
56
+ this.tabDirection = e.shiftKey ? 'backward' : 'forward';
57
+
58
+ // Get the active element(s) in the document and shadow root
59
+ // This will include the active element in the shadow DOM if it exists
60
+ // Active element may be inside the shadow DOM depending on delegatesFocus, so we need to check both
61
+ let activeElement = document.activeElement;
62
+ const actives = [activeElement];
63
+ while (activeElement?.shadowRoot?.activeElement) {
64
+ actives.push(activeElement.shadowRoot.activeElement);
65
+ activeElement = activeElement.shadowRoot.activeElement;
66
+ }
67
+
68
+ // Update the focusable elements
69
+ const focusables = this._getFocusableElements();
70
+
71
+ // If we're at either end of the focusable elements, wrap around to the other end
72
+ const focusIndex =
73
+ (actives.includes(focusables[0]) || actives.includes(this.container)) && this.tabDirection === 'backward'
74
+ ? focusables.length - 1
75
+ : actives.includes(focusables[focusables.length - 1]) && this.tabDirection === 'forward'
76
+ ? 0
77
+ : null;
78
+
79
+ if (focusIndex !== null) {
80
+ focusables[focusIndex].focus();
81
+ e.preventDefault(); // Prevent default tab behavior
82
+ e.stopPropagation(); // Stop the event from bubbling up
83
+ }
84
+ }
147
85
  };
148
86
 
149
87
  /**
@@ -157,7 +95,7 @@ export class FocusTrap {
157
95
  // Use the imported utility function to get focusable elements
158
96
  const elements = getFocusableElements(this.container);
159
97
 
160
- // Return the elements found
98
+ // Filter out any elements with the 'focus-bookend' class
161
99
  return elements;
162
100
  }
163
101
 
@@ -21,7 +21,7 @@ export const FOCUSABLE_COMPONENTS = [
21
21
  'auro-combobox',
22
22
  'auro-input',
23
23
  'auro-counter',
24
- // 'auro-menu', // Auro menu is not focusable by default, it uses a different interaction model
24
+ 'auro-menu',
25
25
  'auro-select',
26
26
  'auro-datepicker',
27
27
  'auro-hyperlink',
@@ -54,7 +54,6 @@ export function isFocusableComponent(element) {
54
54
  /**
55
55
  * Retrieves all focusable elements within the container in DOM order, including those in shadow DOM and slots.
56
56
  * Returns a unique, ordered array of elements that can receive focus.
57
- * Also sorts elements with tabindex first, preserving their order.
58
57
  *
59
58
  * @param {HTMLElement} container The container to search within
60
59
  * @returns {Array<HTMLElement>} An array of focusable elements within the container.
@@ -126,32 +125,5 @@ export function getFocusableElements(container) {
126
125
  }
127
126
  }
128
127
 
129
- // Move tab-indexed elements to the front while preserving their order
130
- // This ensures that elements with tabindex are prioritized in the focus order
131
-
132
- // First extract elements with tabindex
133
- const elementsWithTabindex = uniqueElements.filter(el => el.hasAttribute('tabindex') && (parseInt(el.getAttribute('tabindex')) ?? -1) > 0);
134
-
135
- // Sort these elements by their tabindex value
136
- elementsWithTabindex.sort((a, b) => {
137
- return parseInt(a.getAttribute('tabindex'), 10) - parseInt(b.getAttribute('tabindex'), 10);
138
- });
139
-
140
- // Elements without tabindex (preserving their original order)
141
- const elementsWithoutTabindex = uniqueElements.filter(el =>
142
-
143
- // Elements without tabindex
144
- !el.hasAttribute('tabindex') ||
145
-
146
- // Preserve tab order of elements with tabindex of 0
147
- (parseInt(el.getAttribute('tabindex')) ?? -1) === 0
148
- );
149
-
150
- // Combine both arrays with tabindex elements first
151
- const tabIndexedUniqueElements = [
152
- ...elementsWithTabindex,
153
- ...elementsWithoutTabindex
154
- ];
155
-
156
- return tabIndexedUniqueElements;
128
+ return uniqueElements;
157
129
  }
@@ -10,7 +10,7 @@ describe('isFocusableComponent', () => {
10
10
  it('returns true for enabled custom focusable components', async () => {
11
11
  for (const tag of [
12
12
  'auro-checkbox', 'auro-radio', 'auro-dropdown', 'auro-button', 'auro-combobox',
13
- 'auro-input', 'auro-counter', 'auro-select', 'auro-datepicker',
13
+ 'auro-input', 'auro-counter', 'auro-menu', 'auro-select', 'auro-datepicker',
14
14
  'auro-hyperlink', 'auro-accordion'
15
15
  ]) {
16
16
  const el = document.createElement(tag);
@@ -26,7 +26,7 @@ describe('isFocusableComponent', () => {
26
26
  it('returns false for custom components with disabled attribute', async () => {
27
27
  for (const tag of [
28
28
  'auro-checkbox', 'auro-radio', 'auro-dropdown', 'auro-button', 'auro-combobox',
29
- 'auro-input', 'auro-counter', 'auro-select', 'auro-datepicker',
29
+ 'auro-input', 'auro-counter', 'auro-menu', 'auro-select', 'auro-datepicker',
30
30
  'auro-hyperlink', 'auro-accordion'
31
31
  ]) {
32
32
  const el = document.createElement(tag);
@@ -0,0 +1,87 @@
1
+ ## Functions
2
+
3
+ <dl>
4
+ <dt><a href="#transportAriaAttributes">transportAriaAttributes</a></dt>
5
+ <dd><p>Transfers all ARIA attributes from the host element to the target element.
6
+ This function allows optional removal of the original attributes and the ability to ignore specific attributes.</p>
7
+ </dd>
8
+ <dt><a href="#transportRoleAttribute">transportRoleAttribute</a></dt>
9
+ <dd><p>Transfers the &#39;role&#39; attribute from the host element to the target element.
10
+ This function can optionally remove the original attribute from the host after transport.</p>
11
+ </dd>
12
+ <dt><a href="#transportTabIndexAttribute">transportTabIndexAttribute</a></dt>
13
+ <dd><p>Transfers the &#39;tabindex&#39; attribute from the host element to the target element.
14
+ This function can optionally remove the original attribute from the host after transport.</p>
15
+ </dd>
16
+ <dt><a href="#transportAllA11yAttributes">transportAllA11yAttributes</a></dt>
17
+ <dd><p>Transfers all accessibility-related attributes (ARIA, role, tabindex) from the host element to the target element.
18
+ This function allows optional removal of the original attributes and the ability to ignore specific attributes.</p>
19
+ </dd>
20
+ </dl>
21
+
22
+ <a name="transportAriaAttributes"></a>
23
+
24
+ ## transportAriaAttributes
25
+ Transfers all ARIA attributes from the host element to the target element.
26
+ This function allows optional removal of the original attributes and the ability to ignore specific attributes.
27
+
28
+ **Kind**: global constant
29
+ **Returns**: <code>function</code> - Function to detach the specific matcher and target pairing.
30
+
31
+ | Param | Type | Default | Description |
32
+ | --- | --- | --- | --- |
33
+ | params | <code>Object</code> | | The parameters object. |
34
+ | params.host | <code>HTMLElement</code> | | The host element to observe. |
35
+ | params.target | <code>HTMLElement</code> | | The target element to receive attributes. |
36
+ | [params.removeOriginal] | <code>boolean</code> | <code>true</code> | Whether to remove original attributes. |
37
+ | [params.ignore] | <code>Array.&lt;String&gt;</code> | | The list of attributes not to transport. |
38
+
39
+ <a name="transportRoleAttribute"></a>
40
+
41
+ ## transportRoleAttribute
42
+ Transfers the 'role' attribute from the host element to the target element.
43
+ This function can optionally remove the original attribute from the host after transport.
44
+
45
+ **Kind**: global constant
46
+ **Returns**: <code>function</code> - Function to detach the specific matcher and target pairing.} param.
47
+
48
+ | Param | Type | Default | Description |
49
+ | --- | --- | --- | --- |
50
+ | params | <code>Object</code> | | The parameters object. |
51
+ | params.host | <code>HTMLElement</code> | | The host element to observe. |
52
+ | params.target | <code>HTMLElement</code> | | The target element to receive attributes. |
53
+ | [params.removeOriginal] | <code>boolean</code> | <code>true</code> | Whether to remove original attributes. |
54
+
55
+ <a name="transportTabIndexAttribute"></a>
56
+
57
+ ## transportTabIndexAttribute
58
+ Transfers the 'tabindex' attribute from the host element to the target element.
59
+ This function can optionally remove the original attribute from the host after transport.
60
+
61
+ **Kind**: global constant
62
+ **Returns**: <code>function</code> - Function to detach the specific matcher and target pairing.} param.
63
+
64
+ | Param | Type | Default | Description |
65
+ | --- | --- | --- | --- |
66
+ | params | <code>Object</code> | | The parameters object. |
67
+ | params.host | <code>HTMLElement</code> | | The host element to observe. |
68
+ | params.target | <code>HTMLElement</code> | | The target element to receive attributes. |
69
+ | [params.removeOriginal] | <code>boolean</code> | <code>true</code> | Whether to remove original attributes. |
70
+
71
+ <a name="transportAllA11yAttributes"></a>
72
+
73
+ ## transportAllA11yAttributes
74
+ Transfers all accessibility-related attributes (ARIA, role, tabindex) from the host element to the target element.
75
+ This function allows optional removal of the original attributes and the ability to ignore specific attributes.
76
+
77
+ **Kind**: global constant
78
+ **Returns**: <code>function</code> - Function to detach the specific matcher and target pairing.} param.
79
+
80
+ | Param | Type | Default | Description |
81
+ | --- | --- | --- | --- |
82
+ | params | <code>Object</code> | | The parameters object. |
83
+ | params.host | <code>HTMLElement</code> | | The host element to observe. |
84
+ | params.target | <code>HTMLElement</code> | | The target element to receive attributes. |
85
+ | [params.removeOriginal] | <code>boolean</code> | <code>true</code> | Whether to remove original attributes. |
86
+ | [params.ignore] | <code>Array.&lt;String&gt;</code> | | The list of attributes not to transport. |
87
+
@@ -0,0 +1,105 @@
1
+ import { transportAttributes } from "./transportAttributes.js";
2
+
3
+ const _matchers = {
4
+ 'aria-': attr => attr.startsWith('aria-'),
5
+ 'role': attr => attr.match(/^role$/),
6
+ 'tabindex': attr => attr.match(/^tabindex$/),
7
+ };
8
+
9
+ /**
10
+ * Transfers all ARIA attributes from the host element to the target element.
11
+ * This function allows optional removal of the original attributes and the ability to ignore specific attributes.
12
+ *
13
+ * @param {Object} params - The parameters object.
14
+ * @param {HTMLElement} params.host - The host element to observe.
15
+ * @param {HTMLElement} params.target - The target element to receive attributes.
16
+ * @param {boolean} [params.removeOriginal=true] - Whether to remove original attributes.
17
+ * @param {Array<String>} [params.ignore] - The list of attributes not to transport.
18
+ *
19
+ * @returns {Function} Function to detach the specific matcher and target pairing.
20
+ */
21
+
22
+ export const transportAriaAttributes = ({ host, target, removeOriginal = true, ignore = undefined }) => {
23
+ return transportAttributes({
24
+ host,
25
+ target,
26
+ match: attr => {
27
+ if (ignore && ignore.includes(attr)) {
28
+ return false;
29
+ }
30
+ return _matchers['aria-'](attr);
31
+ },
32
+ removeOriginal
33
+ });
34
+ };
35
+
36
+ /**
37
+ * Transfers the 'role' attribute from the host element to the target element.
38
+ * This function can optionally remove the original attribute from the host after transport.
39
+ *
40
+ * @param {Object} params - The parameters object.
41
+ * @param {HTMLElement} params.host - The host element to observe.
42
+ * @param {HTMLElement} params.target - The target element to receive attributes.
43
+ * @param {boolean} [params.removeOriginal=true] - Whether to remove original attributes.
44
+ *
45
+ * @returns {Function} Function to detach the specific matcher and target pairing.} param.
46
+ */
47
+ export const transportRoleAttribute = ({ host, target, removeOriginal = true }) => {
48
+ return transportAttributes({
49
+ host,
50
+ target,
51
+ match: _matchers['role'],
52
+ removeOriginal
53
+ });
54
+ };
55
+
56
+ /**
57
+ * Transfers the 'tabindex' attribute from the host element to the target element.
58
+ * This function can optionally remove the original attribute from the host after transport.
59
+ *
60
+ * @param {Object} params - The parameters object.
61
+ * @param {HTMLElement} params.host - The host element to observe.
62
+ * @param {HTMLElement} params.target - The target element to receive attributes.
63
+ * @param {boolean} [params.removeOriginal=true] - Whether to remove original attributes.
64
+ *
65
+ * @returns {Function} Function to detach the specific matcher and target pairing.} param.
66
+ */
67
+ export const transportTabIndexAttribute = ({ host, target, removeOriginal = true }) => {
68
+ return transportAttributes({
69
+ host,
70
+ target,
71
+ match: _matchers['tabindex'],
72
+ removeOriginal
73
+ });
74
+ };
75
+
76
+ /**
77
+ * Transfers all accessibility-related attributes (ARIA, role, tabindex) from the host element to the target element.
78
+ * This function allows optional removal of the original attributes and the ability to ignore specific attributes.
79
+ *
80
+ * @param {Object} params - The parameters object.
81
+ * @param {HTMLElement} params.host - The host element to observe.
82
+ * @param {HTMLElement} params.target - The target element to receive attributes.
83
+ * @param {boolean} [params.removeOriginal=true] - Whether to remove original attributes.
84
+ * @param {Array<String>} [params.ignore] - The list of attributes not to transport.
85
+ *
86
+ * @returns {Function} Function to detach the specific matcher and target pairing.} param.
87
+ */
88
+ export const transportAllA11yAttributes = ({ host, target, removeOriginal = true, ignore = undefined }) => {
89
+ return transportAttributes({
90
+ host,
91
+ target,
92
+ match: attr => {
93
+ if (ignore && ignore.includes(attr)) {
94
+ return false;
95
+ }
96
+ for (const key in _matchers) {
97
+ if (_matchers[key](attr)) {
98
+ return true;
99
+ }
100
+ }
101
+ return false;
102
+ },
103
+ removeOriginal
104
+ });
105
+ }
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Private module-level WeakMap to hold MutationObservers for each host element.
3
+ */
4
+ const _observers = new WeakMap();
5
+
6
+ /**
7
+ * Private module-level WeakMap to hold attribute matchers and targets for each host element.
8
+ * Structure: {
9
+ * host: {
10
+ * matchers: Set<Function>,
11
+ * targets: Map<HTMLElement, Map<Function, {removeOriginal: boolean, currentAttributes: Map<string, string>}>>
12
+ * }
13
+ * }
14
+ */
15
+ const _transportConfig = new WeakMap();
16
+
17
+ /**
18
+ * Transfers all matching attributes from a host element to a target element and observes for future changes.
19
+ *
20
+ * @param {Object} params - The parameters for the function.
21
+ * @param {HTMLElement} params.host - The host element from which attributes will be transported.
22
+ * @param {HTMLElement} params.target - The target element to which attributes will be transported.
23
+ * @param {boolean} [params.removeOriginal=true] - Whether to remove the attributes from the host element.
24
+ * @param {boolean} [params.observe=true] - Whether to attach a MutationObserver to the host element.
25
+ * @returns {Function} A function to detach the observer when no longer needed.
26
+ * @throws {TypeError} If the host or target parameters are not instances of HTMLElement.
27
+ */
28
+ export const transportAttributes = ({ host, target, match, removeOriginal = true }) => {
29
+ // Guard Clause: Ensure host is valid HTMLElement instance
30
+ if (typeof host !== 'object' || !(host instanceof HTMLElement)) {
31
+ throw new TypeError('a11yUtilities.js | transportAttributes | The "host" parameter must be an instance of HTMLElement.');
32
+ }
33
+
34
+ // Guard Clause: Ensure target is valid HTMLElement instance
35
+ if (typeof target !== 'object' || !(target instanceof HTMLElement)) {
36
+ throw new TypeError('a11yUtilities.js | transportAttributes | The "target" parameter must be an instance of HTMLElement.');
37
+ }
38
+
39
+ // Guard Clause: Ensure match is a function
40
+ if (typeof match !== 'function') {
41
+ throw new TypeError('a11yUtilities.js | transportAttributes | The "match" parameter must be a function.');
42
+ }
43
+
44
+ // Guard Clause: Ensure removeOriginal is a boolean
45
+ if (typeof removeOriginal !== 'boolean') {
46
+ throw new TypeError('a11yUtilities.js | transportAttributes | The "removeOriginal" parameter must be a boolean.');
47
+ }
48
+
49
+ // Register this transport and get cleanup function
50
+ return _registerTransport({
51
+ host,
52
+ target,
53
+ matcher: match,
54
+ removeOriginal
55
+ });
56
+ };
57
+
58
+ /**
59
+ * Registers a matcher and target for a host element and attaches an observer if needed.
60
+ *
61
+ * @param {Object} params - The parameters object.
62
+ * @param {HTMLElement} params.host - The host element to observe.
63
+ * @param {HTMLElement} params.target - The target element to receive attributes.
64
+ * @param {Function} params.matcher - Function that determines which attributes to transport.
65
+ * @param {boolean} [params.removeOriginal=true] - Whether to remove original attributes.
66
+ * @returns {Function} Function to detach the specific matcher and target pairing.
67
+ * @private
68
+ */
69
+ const _registerTransport = ({ host, target, matcher, removeOriginal = true }) => {
70
+ // Initialize config for this host if it doesn't exist
71
+ if (!_transportConfig.has(host)) {
72
+ _transportConfig.set(host, {
73
+ matchers: new Set(),
74
+ targets: new Map()
75
+ });
76
+ }
77
+
78
+ const config = _transportConfig.get(host);
79
+
80
+ // Add the matcher to the set of matchers for this host
81
+ config.matchers.add(matcher);
82
+
83
+ // Initialize target entry if it doesn't exist
84
+ if (!config.targets.has(target)) {
85
+ config.targets.set(target, new Map());
86
+ }
87
+
88
+ // Store the matcher with its removeOriginal setting for this target
89
+ config.targets.get(target).set(matcher, {
90
+ removeOriginal,
91
+ currentAttributes: new Map()
92
+ });
93
+
94
+ // Perform initial attribute transport
95
+ _transportAttributes({ host, target, matcher, removeOriginal });
96
+
97
+ // Attach observer
98
+ _attachObserver(host);
99
+
100
+ // Return cleanup function and utility functions
101
+ return {
102
+ cleanup: () => _cleanupTransport(host, target, matcher),
103
+ getObservedAttributes: () => _getObservedAttributes(host, target, matcher),
104
+ getObservedAttribute: (attr) => _getObservedAttribute(host, target, matcher, attr),
105
+ }
106
+ };
107
+
108
+ /**
109
+ * Cleans up resources associated with a specific matcher and target for a host element.
110
+ *
111
+ * @param {HTMLElement} host - The host element.
112
+ * @param {HTMLElement} target - The target element.
113
+ * @param {Function} matcher - The matcher function.
114
+ * @private
115
+ */
116
+ const _cleanupTransport = (host, target, matcher) => {
117
+ const config = _transportConfig.get(host);
118
+ if (!config) return;
119
+
120
+ // Remove this matcher from this target
121
+ const targetMatchers = config.targets.get(target);
122
+ if (targetMatchers) {
123
+ targetMatchers.delete(matcher);
124
+
125
+ // If this target has no more matchers, remove it
126
+ if (targetMatchers.size === 0) {
127
+ config.targets.delete(target);
128
+ }
129
+ }
130
+
131
+ // Check if this matcher is still used by any target
132
+ let matcherStillUsed = false;
133
+ for (const matcherMap of config.targets.values()) {
134
+ if (matcherMap.has(matcher)) {
135
+ matcherStillUsed = true;
136
+ break;
137
+ }
138
+ }
139
+
140
+ // If not used anymore, remove from matchers set
141
+ if (!matcherStillUsed) {
142
+ config.matchers.delete(matcher);
143
+ }
144
+
145
+ // If no more targets or matchers, detach observer
146
+ if (config.targets.size === 0 || config.matchers.size === 0) {
147
+ _detachObserver(host);
148
+ }
149
+ };
150
+
151
+ /**
152
+ * Generic function to transport attributes from a host element to a target element.
153
+ *
154
+ * @param {Object} params - The parameters object.
155
+ * @param {HTMLElement} params.host - The host element from which to transport attributes.
156
+ * @param {HTMLElement} params.target - The target element to which attributes will be transported.
157
+ * @param {Function} params.matcher - Function that takes an attribute name and returns true if it should be transported.
158
+ * @param {boolean} [params.removeOriginal=true] - Whether to remove original attributes from host.
159
+ * @returns {void}
160
+ * @private
161
+ */
162
+ const _transportAttributes = ({ host, target, matcher, removeOriginal = true }) => {
163
+ // Get a list of all matching attributes on the host element and their values
164
+ const matchingAttributes = host.getAttributeNames()
165
+ .filter(attr => matcher(attr))
166
+ .reduce((acc, attr) => {
167
+ acc[attr] = host.getAttribute(attr);
168
+ return acc;
169
+ }, {});
170
+
171
+ // Move matching attributes to the target element, removing them from the host if removeOriginal is true
172
+ Object.entries(matchingAttributes).forEach(([key, value]) => {
173
+ _setObservedAttribute(host, target, matcher, key, value);
174
+ target.setAttribute(key, value);
175
+ if (removeOriginal) {
176
+ host.removeAttribute(key);
177
+ }
178
+ });
179
+ };
180
+
181
+ /**
182
+ * Attaches a MutationObserver to the host element to monitor attribute changes.
183
+ *
184
+ * @param {HTMLElement} host - The element to observe for attribute changes.
185
+ * @returns {MutationObserver} The observer instance.
186
+ * @private
187
+ */
188
+ const _attachObserver = (host) => {
189
+ // If an observer for this host already exists, return it
190
+ if (_observers.has(host)) {
191
+ return _observers.get(host);
192
+ }
193
+
194
+ // Create a new MutationObserver
195
+ const observer = new MutationObserver((mutations) => {
196
+ const config = _transportConfig.get(host);
197
+ if (!config) return;
198
+
199
+ // For each mutation affecting attributes
200
+ mutations
201
+ .filter(mutation => mutation.type === 'attributes')
202
+ .forEach(mutation => {
203
+ const attributeName = mutation.attributeName;
204
+
205
+ // Find matchers that care about this attribute
206
+ for (const matcher of config.matchers) {
207
+ if (matcher(attributeName)) {
208
+ // For each target that uses this matcher
209
+ for (const [target, matcherConfigs] of config.targets.entries()) {
210
+ if (matcherConfigs.has(matcher)) {
211
+ const { removeOriginal } = matcherConfigs.get(matcher);
212
+ _transportAttributes({
213
+ host,
214
+ target,
215
+ matcher,
216
+ removeOriginal
217
+ });
218
+ }
219
+ }
220
+ }
221
+ }
222
+ });
223
+ });
224
+
225
+ // Start observing attribute changes
226
+ observer.observe(host, { attributes: true });
227
+
228
+ // Store the observer
229
+ _observers.set(host, observer);
230
+
231
+ return observer;
232
+ };
233
+
234
+ /**
235
+ * Detaches and cleans up the MutationObserver for a given host element.
236
+ *
237
+ * @param {HTMLElement} host - The element whose observer should be detached.
238
+ * @private
239
+ */
240
+ const _detachObserver = (host) => {
241
+ if (_observers.has(host)) {
242
+ const observer = _observers.get(host);
243
+ observer.disconnect();
244
+ _observers.delete(host);
245
+ }
246
+
247
+ // Clean up the transport config as well
248
+ if (_transportConfig.has(host)) {
249
+ _transportConfig.delete(host);
250
+ }
251
+ };
252
+
253
+ /**
254
+ * Gets the matcher configuration for a specific host, target, and matcher.
255
+ * @param {HTMLElement} host - The host element.
256
+ * @param {HTMLElement} target - The target element.
257
+ * @param {Function} matcher - The matcher function.
258
+ * @returns {Object|undefined} The matcher configuration if found.
259
+ * @private
260
+ */
261
+ const _getMatcherConfig = (host, target, matcher) => {
262
+ const config = _transportConfig.get(host);
263
+ if (!config) return undefined;
264
+
265
+ const targetMatchers = config.targets.get(target);
266
+ if (!targetMatchers) return undefined;
267
+
268
+ return targetMatchers.get(matcher);
269
+ };
270
+
271
+ /**
272
+ * Sets an observed attribute value.
273
+ * @param {HTMLElement} host - The host element.
274
+ * @param {HTMLElement} target - The target element.
275
+ * @param {Function} matcher - The matcher function.
276
+ * @param {string} key - The attribute name.
277
+ * @param {string} value - The attribute value.
278
+ * @private
279
+ */
280
+ const _setObservedAttribute = (host, target, matcher, key, value) => {
281
+ const matcherConfig = _getMatcherConfig(host, target, matcher);
282
+ if (matcherConfig) {
283
+ matcherConfig.currentAttributes.set(key, value);
284
+ }
285
+ };
286
+
287
+ const _getObservedAttribute = (host, target, matcher, attr) => {
288
+ const matcherConfig = _getMatcherConfig(host, target, matcher);
289
+ if (matcherConfig) return matcherConfig.currentAttributes.get(attr);
290
+ return undefined;
291
+ };
292
+
293
+ const _getObservedAttributes = (host, target, matcher) => {
294
+ const matcherConfig = _getMatcherConfig(host, target, matcher);
295
+ if (matcherConfig) return Array.from(matcherConfig.currentAttributes.entries());
296
+ return [];
297
+ };
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable line-comment-position, no-inline-comments */
2
2
 
3
- import { autoUpdate, computePosition, offset, autoPlacement, flip } from '@floating-ui/dom';
3
+ import { autoUpdate, computePosition, offset, autoPlacement, flip, shift } from '@floating-ui/dom';
4
4
 
5
5
 
6
6
  const MAX_CONFIGURATION_COUNT = 10;
@@ -166,6 +166,7 @@ export default class AuroFloatingUI {
166
166
  // Define the middlware for the floater configuration
167
167
  const middleware = [
168
168
  offset(this.element.floaterConfig?.offset || 0),
169
+ ...this.element.floaterConfig?.shift ? [shift()] : [], // Add shift middleware if shift is enabled.
169
170
  ...this.element.floaterConfig?.flip ? [flip()] : [], // Add flip middleware if flip is enabled.
170
171
  ...this.element.floaterConfig?.autoPlacement ? [autoPlacement()] : [], // Add autoPlacement middleware if autoPlacement is enabled.
171
172
  ];