@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 +14 -0
- package/package.json +10 -10
- package/scripts/runtime/FocusTrap/FocusTrap.mjs +39 -101
- package/scripts/runtime/Focusables/Focusables.mjs +2 -30
- package/scripts/runtime/Focusables/test/Focusables.test.js +2 -2
- package/scripts/runtime/a11yTransporter/README.md +87 -0
- package/scripts/runtime/a11yTransporter/a11yTransporter.js +105 -0
- package/scripts/runtime/a11yTransporter/transportAttributes.js +297 -0
- package/scripts/runtime/floatingUI.mjs +2 -1
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-
|
|
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
|
|
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
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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 'role' 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 'tabindex' 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.<String></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.<String></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
|
];
|