@cocreate/aria 1.1.0 → 1.2.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.
@@ -22,13 +22,13 @@ jobs:
22
22
  runs-on: ubuntu-latest
23
23
  steps:
24
24
  - name: Checkout
25
- uses: actions/checkout@v3
25
+ uses: actions/checkout@v4
26
26
  - name: Setup Node.js
27
- uses: actions/setup-node@v3
27
+ uses: actions/setup-node@v4
28
28
  with:
29
- node-version: 14
29
+ node-version: 22 # Required for the latest semantic-release plugins
30
30
  - name: Semantic Release
31
- uses: cycjimmy/semantic-release-action@v3
31
+ uses: cycjimmy/semantic-release-action@v4 # Update to v4 for better Node 20+ support
32
32
  id: semantic
33
33
  with:
34
34
  extra_plugins: |
@@ -36,9 +36,8 @@ jobs:
36
36
  @semantic-release/git
37
37
  @semantic-release/github
38
38
  env:
39
- GITHUB_TOKEN: "${{ secrets.GITHUB }}"
39
+ GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" # Use the built-in token if possible
40
40
  NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
41
41
  outputs:
42
42
  new_release_published: "${{ steps.semantic.outputs.new_release_published }}"
43
43
  new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
44
-
package/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ # [1.2.0](https://github.com/CoCreate-app/CoCreate-aria/compare/v1.1.1...v1.2.0) (2026-02-09)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * root factory variable Module ([6f35875](https://github.com/CoCreate-app/CoCreate-aria/commit/6f358757c9917ec52c397a35bc6f5e97976de6ed))
7
+
8
+
9
+ ### Features
10
+
11
+ * aria-attribute-group for tab navigation grouping to deactivate state of other tabs in the group ([12822c0](https://github.com/CoCreate-app/CoCreate-aria/commit/12822c04fbe41672833852018242a58c179b9229))
12
+
13
+ ## [1.1.1](https://github.com/CoCreate-app/CoCreate-aria/compare/v1.1.0...v1.1.1) (2025-12-26)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * update worklow ([32b1cff](https://github.com/CoCreate-app/CoCreate-aria/commit/32b1cffdce4f66ab078407cae412a5583afb92a4))
19
+
1
20
  # [1.1.0](https://github.com/CoCreate-app/CoCreate-aria/compare/v1.0.0...v1.1.0) (2025-11-16)
2
21
 
3
22
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # CoCreate-aria
2
2
 
3
- Chain multiple component executions to generate your desired logic, when one action is complete next one will start. The sequence goes until all aria have been completed. Vanilla javascript, easily configured using HTML5 attributes and/or JavaScript API. Take it for a spin in our [playground!](https://cocreate.app/docs/aria)
3
+ Orchestrate dynamic UI behavior and ARIA state with event-driven action chains, configured via HTML5 attributes or the JavaScript API. Take it for a spin in our [playground!](https://cocreate.app/docs/aria)
4
4
 
5
5
  ![minified](https://img.badgesize.io/https://cdn.cocreate.app/aria/latest/CoCreate-aria.min.js?style=flat-square&label=minified&color=orange)
6
6
  ![gzip](https://img.badgesize.io/https://cdn.cocreate.app/aria/latest/CoCreate-aria.min.js?compression=gzip&style=flat-square&label=gzip&color=yellow)
package/package.json CHANGED
@@ -1,17 +1,19 @@
1
1
  {
2
2
  "name": "@cocreate/aria",
3
- "version": "1.1.0",
4
- "description": "Chain multiple component executions to generate your desired logic, when one action is complete next one will start. The sequence goes until all aria have been completed. Vanilla javascript, easily configured using HTML5 attributes and/or JavaScript API.",
3
+ "version": "1.2.0",
4
+ "description": "Orchestrate dynamic UI behavior and ARIA state with event-driven action chains, configured via HTML5 attributes or the JavaScript API.",
5
5
  "keywords": [
6
6
  "aria",
7
- "chain-functions",
7
+ "workflow",
8
+ "orchestration",
9
+ "automation",
10
+ "chain-actions",
11
+ "event-driven",
12
+ "attributes",
13
+ "dom",
8
14
  "low-code",
9
- "realtime",
10
- "realtime-framework",
11
- "collaboration",
12
- "shared-editing",
13
- "html5-framework",
14
- "javascript-framework"
15
+ "html5",
16
+ "javascript"
15
17
  ],
16
18
  "publishConfig": {
17
19
  "access": "public"
@@ -47,8 +49,6 @@
47
49
  "webpack-log": "^3.0.1"
48
50
  },
49
51
  "dependencies": {
50
- "@cocreate/observer": "^1.18.1",
51
- "@cocreate/uuid": "^1.12.1",
52
- "@cocreate/utils": "^1.39.0"
52
+ "@cocreate/observer": "^1.18.4"
53
53
  }
54
54
  }
package/src/index.js CHANGED
@@ -1,231 +1,350 @@
1
+ import Observer from '@cocreate/observer';
2
+
3
+ /**
4
+ * Main initialization entry point for ARIA attributes.
5
+ * Orchestrates the setup of interactive controls and navigation states.
6
+ * @param {Element|Element[]|NodeList} [elements] - Optional specific elements to initialize.
7
+ * If omitted, the script scans the entire document for [aria-controls].
8
+ */
1
9
  function init(elements) {
2
- if (!elements) {
3
- elements = document.querySelectorAll("[aria-controls]");
4
- // initDocument();
5
- }
6
- initElement(elements);
10
+ // 1. Initialize aria-controls (Toggles, Popups, Tabs)
11
+ let controlsElements = elements;
12
+ if (!controlsElements) {
13
+ controlsElements = document.querySelectorAll("[aria-controls]");
14
+ }
15
+ initElement(controlsElements);
16
+
17
+ // 2. Initialize aria-current (Navigation links)
18
+ // Performed after controls to ensure global scan happens last
19
+ setAriaCurrent();
7
20
  }
8
21
 
9
- let popupListener = null;
22
+ /**
23
+ * Initializes interactive elements by binding click events and handling
24
+ * accessibility state changes (aria-expanded, aria-hidden, aria-selected).
25
+ * @param {Element|Element[]|NodeList} elements - Elements to be initialized as ARIA controls.
26
+ */
27
+ const initialized = new Set();
10
28
 
11
- function addPopupListener() {
12
- if (!popupListener) {
13
- popupListener = function (event) {
14
-
15
- const hasPopUps = document.querySelectorAll(
16
- '[aria-controls][aria-haspopup][aria-expanded="true"]'
17
- );
18
-
19
- let skipControlledId = null;
20
- for (let hasPopUp of hasPopUps) {
21
- const controlledId = hasPopUp.getAttribute("aria-controls");
22
- if (skipControlledId === controlledId) {
23
- continue; // Skip this controlledId if it was already processed
24
- }
25
- if (hasPopUp.contains(event.target)) {
26
- skipControlledId = controlledId;
27
- continue; // Ignore clicks inside the popup
28
- }
29
-
30
- const controlledElement = document.getElementById(controlledId);
31
- let closeOn = controlledElement.getAttribute("aria-close-on");
32
- let closeOnEl = controlledElement;
33
- if (!closeOn) {
34
- closeOnEl = event.target.closest(
35
- `#${controlledId} [aria-close-on]`
36
- );
37
- if (closeOnEl) {
38
- closeOn = closeOnEl.getAttribute("aria-close-on");
39
- }
40
- }
41
-
42
- let closeOnConditions = closeOn ? closeOn.split(",").map(c => c.trim()) : [];
43
-
44
- let shouldClose = false;
45
- for (let condition of closeOnConditions) {
46
- if (condition === "outside" && !closeOnEl.contains(event.target)) {
47
- shouldClose = true;
48
- } else if (condition === "inside" && closeOnEl.contains(event.target)) {
49
- shouldClose = true;
50
- } else if (condition === "anywhere") {
51
- shouldClose = true;
52
- }
53
-
54
- // If any condition matches, break the loop
55
- if (shouldClose) break;
56
- }
57
-
58
- if (!shouldClose) continue;
59
-
60
- controlledElement.classList.remove("show");
61
- controlledElement.setAttribute("aria-hidden", "true");
62
- updateAllControls(controlledId, "false");
63
- }
64
- // Remove listener if no popups remain open
65
- if (
66
- !document.querySelector(
67
- '[aria-controls][aria-haspopup][aria-expanded="true"]'
68
- )
69
- ) {
70
- document.removeEventListener("click", popupListener, true);
71
- popupListener = null;
72
- }
73
- };
74
- document.addEventListener("click", popupListener, true);
75
- }
29
+ function initElement(elements) {
30
+ if (
31
+ !Array.isArray(elements) &&
32
+ !(elements instanceof NodeList) &&
33
+ !(elements instanceof HTMLCollection)
34
+ ) {
35
+ elements = [elements];
36
+ }
37
+
38
+ if (elements.length === 0) return;
39
+
40
+ for (let control of elements) {
41
+ if (!control.hasAttribute('aria-controls')) continue;
42
+ if (initialized.has(control)) continue;
43
+ initialized.add(control);
44
+
45
+ initEscapeKey(control);
46
+
47
+ control.addEventListener("click", function (event) {
48
+ // Only prevent default if it's not a standard link leading to another page
49
+ const href = this.getAttribute("href");
50
+ if (!href || href.startsWith("#") || this.hasAttribute("aria-haspopup")) {
51
+ event.preventDefault();
52
+ }
53
+
54
+ const controlledId = this.getAttribute("aria-controls");
55
+ const controlledElement = document.getElementById(controlledId);
56
+
57
+ if (!controlledElement) {
58
+ console.warn(`ARIA Controls: No element found with ID "${controlledId}" controlled by`, this);
59
+ return;
60
+ }
61
+
62
+ const closeOn = controlledElement.getAttribute("aria-close-on");
63
+ const role = this.getAttribute("role");
64
+ const hasAriaOpen = this.hasAttribute("aria-open");
65
+ const hasAriaClose = this.hasAttribute("aria-close");
66
+ const expanded = this.getAttribute("aria-expanded");
67
+ const controlsClass = this.getAttribute("aria-controls-class") || "show";
68
+ const group = this.getAttribute("aria-controls-group");
69
+
70
+ // Prevent interaction if state is already in target position
71
+ if (hasAriaOpen && expanded === "true") return;
72
+ if (hasAriaClose && expanded !== "true") return;
73
+
74
+ if (role === "tab") {
75
+ // Tab Pattern: Mutual exclusivity within a tablist
76
+ const tablist = this.closest("[role='tablist']");
77
+ const tabs = tablist.querySelectorAll('[role="tab"]');
78
+ for (let tab of tabs) {
79
+ const tabControlledId = tab.getAttribute("aria-controls");
80
+ const tabControlledEl = document.getElementById(tabControlledId);
81
+ if (this === tab) {
82
+ tab.setAttribute("aria-selected", "true");
83
+ tabControlledEl.setAttribute("aria-hidden", "false");
84
+ if (controlsClass) tabControlledEl.classList.add(controlsClass);
85
+ } else {
86
+ tab.setAttribute("aria-selected", "false");
87
+ tabControlledEl.setAttribute("aria-hidden", "true");
88
+ if (controlsClass) tabControlledEl.classList.remove(controlsClass);
89
+ }
90
+ }
91
+ } else {
92
+ // Toggle Pattern: Standard expand/collapse
93
+ if (expanded === "true") {
94
+ controlledElement.setAttribute("aria-hidden", "true");
95
+ if (controlsClass) controlledElement.classList.remove(controlsClass);
96
+ updateAllControls(controlledId, "false");
97
+ removePopupListener();
98
+ } else {
99
+ controlledElement.setAttribute("aria-hidden", "false");
100
+ if (controlsClass) controlledElement.classList.add(controlsClass);
101
+
102
+ // Handle grouped controls (Accordions)
103
+ if (group) {
104
+ const groupedControls = document.querySelectorAll(`[aria-controls-group="${group}"][aria-expanded="true"]`);
105
+ for (let groupedControl of groupedControls) {
106
+ const groupedId = groupedControl.getAttribute("aria-controls");
107
+ if (!groupedId || groupedId === controlledId) continue;
108
+ const groupedElement = document.getElementById(groupedId);
109
+ if (!groupedElement) continue;
110
+ const gClass = groupedControl.getAttribute("aria-controls-class") || "show";
111
+ groupedElement.setAttribute("aria-hidden", "true");
112
+ if (gClass) groupedElement.classList.remove(gClass);
113
+ updateAllControls(groupedId, "false");
114
+ }
115
+ }
116
+ updateAllControls(controlledId, "true");
117
+ if (closeOn !== "btn" && closeOn !== "button") {
118
+ addPopupListener();
119
+ }
120
+ }
121
+ }
122
+ });
123
+ }
76
124
  }
77
125
 
78
- function removePopupListener() {
79
- if (popupListener) {
80
- document.removeEventListener("click", popupListener, true);
81
- popupListener = null;
82
- }
126
+ /**
127
+ * Synchronizes aria-expanded state across all elements controlling the same ID.
128
+ * @param {string} controlledId - The ID of the target element.
129
+ * @param {string} state - The target state ('true' or 'false').
130
+ */
131
+ function updateAllControls(controlledId, state) {
132
+ const allControls = document.querySelectorAll(`[aria-controls="${controlledId}"]`);
133
+ allControls.forEach((ctrl) => ctrl.setAttribute("aria-expanded", state));
83
134
  }
84
135
 
85
- const initialized = new Set();
86
- function initElement(elements) {
87
- if (
88
- !Array.isArray(elements) &&
89
- !(elements instanceof NodeList) &&
90
- !(elements instanceof HTMLCollection)
91
- ) {
92
- elements = [elements];
93
- }
94
-
95
- if (elements.length === 0) {
96
- return;
97
- }
98
-
99
- for (let control of elements) {
100
- if (initialized.has(control)) continue;
101
- initialized.add(control);
102
- initEscapeKey(control);
103
- control.addEventListener("click", function (event) {
104
- event.preventDefault(); // Prevent default link behavior for <a> tags
105
-
106
- const controlledId = this.getAttribute("aria-controls");
107
- const controlledElement = document.getElementById(controlledId);
108
-
109
- if (!controlledElement) {
110
- console.warn(
111
- `ARIA Controls: No element found with ID "${controlledId}" controlled by`,
112
- this
113
- );
114
- return;
115
- }
116
-
117
- const closeOn = controlledElement.getAttribute("aria-close-on");
118
- const role = this.getAttribute("role");
119
- const hasAriaOpen = this.hasAttribute("aria-open");
120
- const hasAriaClose = this.hasAttribute("aria-close");
121
- const expanded = this.getAttribute("aria-expanded");
122
- const controlsClass = this.getAttribute("aria-controls-class") || "show";
123
-
124
- // Apply aria-open and aria-close logic globally, before any role-specific logic
125
- if (hasAriaOpen && expanded === "true") {
126
- // Do nothing if already open and aria-open is set
127
- return;
128
- }
129
- if (hasAriaClose && expanded !== "true") {
130
- // Do nothing if already closed and aria-close is set
131
- return;
132
- }
133
-
134
- if (role === "tab") {
135
- const tablist = this.closest("[role='tablist']");
136
- const tabs = tablist.querySelectorAll('[role="tab"]');
137
- for (let tab of tabs) {
138
- const tabControlledId = tab.getAttribute("aria-controls");
139
- const tabControlledEl =
140
- document.getElementById(tabControlledId);
141
- if (this === tab) {
142
- tab.setAttribute("aria-selected", "true");
143
- tabControlledEl.setAttribute("aria-hidden", "false");
144
- if (controlsClass) {
145
- tabControlledEl.classList.add(controlsClass);
146
- }
147
- } else {
148
- tab.setAttribute("aria-selected", "false");
149
- tabControlledEl.setAttribute("aria-hidden", "true");
150
- if (controlsClass) {
151
- tabControlledEl.classList.remove(controlsClass);
152
- }
153
- }
154
- }
155
- } else {
156
- // Default toggle logic
157
- if (expanded === "true") {
158
- controlledElement.setAttribute("aria-hidden", "true");
159
- if (controlsClass) {
160
- controlledElement.classList.remove(controlsClass);
161
- }
162
- updateAllControls(controlledId, "false");
163
- removePopupListener();
164
- } else {
165
- controlledElement.setAttribute("aria-hidden", "false");
166
- if (controlsClass) {
167
- controlledElement.classList.add(controlsClass);
168
- }
169
- updateAllControls(controlledId, "true");
170
- if (closeOn !== "btn" && closeOn !== "button") {
171
- addPopupListener();
172
- }
173
- }
174
- }
175
- });
176
- }
136
+ /**
137
+ * Binds a global click listener to handle "click-outside" behavior for popups.
138
+ * Conditions are defined via 'aria-close-on' attribute (outside, inside, anywhere).
139
+ */
140
+ let popupListener = null;
141
+
142
+ function addPopupListener() {
143
+ if (!popupListener) {
144
+ popupListener = function (event) {
145
+ const hasPopUps = document.querySelectorAll(
146
+ '[aria-controls][aria-haspopup][aria-expanded="true"]'
147
+ );
148
+
149
+ let skipControlledId = null;
150
+ for (let hasPopUp of hasPopUps) {
151
+ const controlledId = hasPopUp.getAttribute("aria-controls");
152
+ if (skipControlledId === controlledId) continue;
153
+ if (hasPopUp.contains(event.target)) {
154
+ skipControlledId = controlledId;
155
+ continue;
156
+ }
157
+
158
+ const controlledElement = document.getElementById(controlledId);
159
+ if (!controlledElement) continue;
160
+
161
+ // Check if the click occurred on an item explicitly marked to exclude closing
162
+ const excludeElement = event.target.closest('[aria-close="false"]');
163
+ if (excludeElement && controlledElement.contains(excludeElement)) {
164
+ skipControlledId = controlledId;
165
+ continue;
166
+ }
167
+
168
+ let closeOn = controlledElement.getAttribute("aria-close-on");
169
+ let closeOnEl = controlledElement;
170
+ if (!closeOn) {
171
+ closeOnEl = event.target.closest(`#${controlledId} [aria-close-on]`);
172
+ if (closeOnEl) closeOn = closeOnEl.getAttribute("aria-close-on");
173
+ }
174
+
175
+ let closeOnConditions = closeOn ? closeOn.split(",").map(c => c.trim()) : [];
176
+ let shouldClose = false;
177
+
178
+ for (let condition of closeOnConditions) {
179
+ if (condition === "outside" && !closeOnEl.contains(event.target)) {
180
+ shouldClose = true;
181
+ } else if (condition === "inside" && closeOnEl.contains(event.target)) {
182
+ shouldClose = true;
183
+ } else if (condition === "anywhere") {
184
+ shouldClose = true;
185
+ }
186
+ if (shouldClose) break;
187
+ }
188
+
189
+ if (!shouldClose) continue;
190
+
191
+ controlledElement.classList.remove("show");
192
+ controlledElement.setAttribute("aria-hidden", "true");
193
+ updateAllControls(controlledId, "false");
194
+ }
195
+
196
+ if (!document.querySelector('[aria-controls][aria-haspopup][aria-expanded="true"]')) {
197
+ document.removeEventListener("click", popupListener, true);
198
+ popupListener = null;
199
+ }
200
+ };
201
+ document.addEventListener("click", popupListener, true);
202
+ }
177
203
  }
178
204
 
179
- function updateAllControls(controlledId, state) {
180
- const allControls = document.querySelectorAll(
181
- `[aria-controls="${controlledId}"]`
182
- );
183
- allControls.forEach((ctrl) => ctrl.setAttribute("aria-expanded", state));
205
+ /**
206
+ * Removes the global popup click listener.
207
+ */
208
+ function removePopupListener() {
209
+ if (popupListener) {
210
+ document.removeEventListener("click", popupListener, true);
211
+ popupListener = null;
212
+ }
184
213
  }
185
214
 
215
+ /**
216
+ * Binds the Escape key to close the controlled element.
217
+ * @param {Element} control - The trigger element that controls the target.
218
+ */
186
219
  function initEscapeKey(control) {
187
- const controlledId = control.getAttribute("aria-controls");
188
- const controlledElement = document.getElementById(controlledId);
189
- if (controlledElement) {
190
- control.addEventListener("keydown", handleEscapeKey);
191
- controlledElement.addEventListener("keydown", handleEscapeKey);
192
- }
220
+ const controlledId = control.getAttribute("aria-controls");
221
+ const controlledElement = document.getElementById(controlledId);
222
+ if (controlledElement) {
223
+ control.addEventListener("keydown", handleEscapeKey);
224
+ controlledElement.addEventListener("keydown", handleEscapeKey);
225
+ }
193
226
  }
194
227
 
228
+ /**
229
+ * Global handler for the Escape key to close open interactive elements.
230
+ * @param {KeyboardEvent} event - The keyboard event object.
231
+ */
195
232
  function handleEscapeKey(event) {
196
- if (event.key === "Escape") {
197
- // Use currentTarget to reference the element the listener is attached to
198
- const toggleButton = event.currentTarget.matches("[aria-controls]")
199
- ? event.currentTarget
200
- : document.querySelector(
201
- `[aria-controls="${event.currentTarget.id}"]`
202
- );
203
-
204
- if (toggleButton) {
205
- toggleButton.click();
206
- }
207
- }
233
+ if (event.key === "Escape") {
234
+ const toggleButton = event.currentTarget.matches("[aria-controls]")
235
+ ? event.currentTarget
236
+ : document.querySelector(`[aria-controls="${event.currentTarget.id}"]`);
237
+
238
+ if (toggleButton) {
239
+ toggleButton.click();
240
+ }
241
+ }
208
242
  }
209
243
 
210
- // Attach Escape key handler globally
244
+ /**
245
+ * Manages navigation state attributes by scanning links and matching them against
246
+ * the current URL and hash. Assigns aria-current="page" or "location".
247
+ * @param {Element|Element[]|NodeList} [elements] - Links or containers to scan.
248
+ * Defaults to all a[href] if not provided.
249
+ */
250
+ function setAriaCurrent(elements) {
251
+ if (!elements) {
252
+ elements = document.querySelectorAll("a[href]");
253
+ }
254
+
255
+ if (
256
+ !Array.isArray(elements) &&
257
+ !(elements instanceof NodeList) &&
258
+ !(elements instanceof HTMLCollection)
259
+ ) {
260
+ elements = [elements];
261
+ }
262
+
263
+ const currentUrl = new URL(window.location.href);
264
+ const currentPage = `${currentUrl.origin}${normalizePath(currentUrl.pathname)}`;
265
+ const currentHash = currentUrl.hash;
266
+
267
+ for (const item of elements) {
268
+ const links = (item.nodeName === 'A' && item.hasAttribute('href'))
269
+ ? [item]
270
+ : (item.querySelectorAll ? item.querySelectorAll("a[href]") : []);
271
+
272
+ for (const link of links) {
273
+ const href = link.getAttribute("href") || "";
274
+
275
+ if (!href || href === "#") {
276
+ link.removeAttribute("aria-current");
277
+ continue;
278
+ }
279
+
280
+ let linkUrl;
281
+ try {
282
+ linkUrl = new URL(href, window.location.href);
283
+ } catch {
284
+ continue;
285
+ }
286
+
287
+ const linkPage = `${linkUrl.origin}${normalizePath(linkUrl.pathname)}`;
288
+ const isSamePage = linkPage === currentPage;
289
+ const linkHasHash = Boolean(linkUrl.hash);
290
+
291
+ let ariaValue = null;
292
+
293
+ if (isSamePage) {
294
+ // Match exact location (hash) or general page
295
+ if (linkHasHash && linkUrl.hash === currentHash) {
296
+ ariaValue = "location";
297
+ } else if (!linkHasHash) {
298
+ ariaValue = "page";
299
+ }
300
+ }
301
+
302
+ if (ariaValue) {
303
+ link.setAttribute("aria-current", ariaValue);
304
+ } else {
305
+ link.removeAttribute("aria-current");
306
+ }
307
+ }
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Sanitizes and normalizes URL paths to prevent trailing slash mismatches.
313
+ * @param {string} path - The raw URL path.
314
+ * @returns {string} The normalized path string.
315
+ */
316
+ const normalizePath = (path) => path.replace(/\/$/, "") || "/";
317
+
318
+ /**
319
+ * Event Listeners and Observers
320
+ * Handles browser navigation events and dynamic DOM mutations.
321
+ */
322
+ window.addEventListener("hashchange", () => setAriaCurrent());
323
+ window.addEventListener("popstate", () => setAriaCurrent());
211
324
  document.addEventListener("keydown", handleEscapeKey);
212
325
 
213
- CoCreate.observer.init({
214
- name: "aria",
215
- types: ["addedNodes"],
216
- selector: "[aria-controls]",
217
- callback: function (mutation) {
218
- initElement(mutation.target);
219
- }
326
+ // Observer: Interactive controls
327
+ Observer.init({
328
+ name: "aria-controls",
329
+ types: ["addedNodes"],
330
+ selector: "[aria-controls]",
331
+ callback: function (mutation) {
332
+ initElement(mutation.target);
333
+ }
220
334
  });
221
335
 
222
- // CoCreate.observer.init({
223
- // name: "aria-attributes",
224
- // types: ["attributes"],
225
- // attributeFilters: ["aria-selected"],
226
- // callback: function (mutation) {
227
- // initElement(mutation.target);
228
- // }
229
- // });
336
+ // Observer: Navigation links
337
+ Observer.init({
338
+ name: "aria-current",
339
+ types: ["addedNodes", "attributes"],
340
+ selector: "a[href]",
341
+ attributeFilters: ["href"],
342
+ callback: function (mutation) {
343
+ setAriaCurrent(mutation.target);
344
+ }
345
+ });
230
346
 
347
+ // Initial execution
231
348
  init();
349
+
350
+ export { init, initElement, setAriaCurrent };