desy-html 17.0.0 → 17.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/index.html +5 -0
- package/package.json +3 -3
- package/src/js/aria/MenuHorizontal.js +4 -6
- package/src/js/aria/MenuNavigation.js +22 -53
- package/src/js/aria/MenuVertical.js +4 -4
- package/src/js/aria/MenubarAction.js +29 -6
- package/src/js/aria/MenubarItemAction.js +1 -9
- package/src/js/aria/Nav.js +4 -4
- package/src/js/aria/PopupMenuAction.js +2 -2
- package/src/js/aria/accordion.js +27 -10
- package/src/js/aria/alert.js +8 -3
- package/src/js/aria/combobox.js +16 -3
- package/src/js/aria/dataGrid.js +25 -22
- package/src/js/aria/linksList.js +25 -3
- package/src/js/aria/listbox.js +3 -3
- package/src/js/aria/tabs.js +20 -6
- package/src/js/aria/tree.js +60 -40
- package/src/js/aria/treeitem.js +22 -13
- package/src/js/aria/utils.js +21 -0
- package/vite.config.js +69 -30
package/docs/index.html
CHANGED
|
@@ -147,6 +147,11 @@ cd desy-html</code></pre>
|
|
|
147
147
|
|
|
148
148
|
<h2>Changelog (English)</h2>
|
|
149
149
|
<p>What's new in the latest version of desy-html</p>
|
|
150
|
+
<h3>v.17.0.1</h3>
|
|
151
|
+
<ul class="text-sm">
|
|
152
|
+
<li>Security fixes from deepsec.</li>
|
|
153
|
+
<li>Fixed a bug that avoid using breakpoint variants in desy-html-starter.</li>
|
|
154
|
+
</ul>
|
|
150
155
|
<h3>v.17.0.0</h3>
|
|
151
156
|
<ul class="text-sm">
|
|
152
157
|
<li>Added Combobox component.</li>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "desy-html",
|
|
3
|
-
"version": "17.0.
|
|
3
|
+
"version": "17.0.1",
|
|
4
4
|
"description": "desy-html contains the code you need to start building a user interface for Gobierno de Aragón government webapps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": {
|
|
@@ -40,13 +40,13 @@
|
|
|
40
40
|
"@floating-ui/dom": "^1.6.13",
|
|
41
41
|
"@tailwindcss/forms": "^0.5.10",
|
|
42
42
|
"@tailwindcss/typography": "^0.5.18",
|
|
43
|
-
"@tailwindcss/vite": "^4.
|
|
43
|
+
"@tailwindcss/vite": "^4.2",
|
|
44
44
|
"autoprefixer": "^10.4.21",
|
|
45
45
|
"cally": "^0.8.0",
|
|
46
46
|
"chokidar": "^3.6.0",
|
|
47
47
|
"hex-rgb": "^5.0.0",
|
|
48
48
|
"js-yaml": "^4.1.0",
|
|
49
|
-
"tailwindcss": "^4.
|
|
49
|
+
"tailwindcss": "^4.2"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"cross-env": "^10.1.0",
|
|
@@ -36,18 +36,16 @@ export function MenuHorizontal(aria) {
|
|
|
36
36
|
aria.MenuHorizontal.prototype.deactivateElement = function (elementDeactivated) {
|
|
37
37
|
elementDeactivated.removeAttribute('aria-current');
|
|
38
38
|
elementDeactivated.classList.remove('c-menu-horizontal__active');
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
getStrong.
|
|
42
|
-
const replaceStrong = elementDeactivated.innerHTML.replace('strong', 'span');
|
|
43
|
-
elementDeactivated.innerHTML = `${replaceStrong}`;
|
|
39
|
+
const getStrong = elementDeactivated.querySelector('strong');
|
|
40
|
+
if (getStrong) {
|
|
41
|
+
getStrong.replaceWith(...getStrong.childNodes);
|
|
44
42
|
}
|
|
45
43
|
};
|
|
46
44
|
|
|
47
45
|
window.activateItemMenuHorizontal = function (menuId, activeItemId) {
|
|
48
46
|
const menu = document.getElementById(menuId);
|
|
49
47
|
if (menu) {
|
|
50
|
-
const activeItem =
|
|
48
|
+
const activeItem = menu.querySelector('#' + CSS.escape(activeItemId));
|
|
51
49
|
if (activeItem) {
|
|
52
50
|
const menuInstance = new aria.MenuHorizontal(menu);
|
|
53
51
|
menuInstance.activateElement(activeItemId);
|
|
@@ -84,6 +84,9 @@ export function MenuNavigation(aria) {
|
|
|
84
84
|
const getAllLiElements = this.rootNode.querySelectorAll('li');
|
|
85
85
|
[...getAllLiElements].forEach((element) => {
|
|
86
86
|
const getElement = element.querySelector('a');
|
|
87
|
+
if (!getElement) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
87
90
|
if (getElement.id === elementActive) {
|
|
88
91
|
this.wrapActiveElement(getElement);
|
|
89
92
|
} else {
|
|
@@ -93,7 +96,7 @@ export function MenuNavigation(aria) {
|
|
|
93
96
|
};
|
|
94
97
|
|
|
95
98
|
aria.MenuNavigation.prototype.activateSubElement = function (menuId, elementActive) {
|
|
96
|
-
const getAllLinks = this.rootNode.querySelectorAll(`#${menuId} ul li li a`);
|
|
99
|
+
const getAllLinks = this.rootNode.querySelectorAll(`#${CSS.escape(menuId)} ul li li a`);
|
|
97
100
|
[...getAllLinks].forEach((link) => {
|
|
98
101
|
if(link.id === elementActive) {
|
|
99
102
|
link.setAttribute('aria-current', 'page');
|
|
@@ -104,10 +107,10 @@ export function MenuNavigation(aria) {
|
|
|
104
107
|
getElementParent.removeAttribute('aria-current');
|
|
105
108
|
const mainSpanParent2 = getElementParent.querySelector('span:not(.sr-only)');
|
|
106
109
|
if (mainSpanParent2) {
|
|
107
|
-
mainSpanParent2
|
|
110
|
+
unwrapStrong(mainSpanParent2);
|
|
108
111
|
}
|
|
109
112
|
link.removeAttribute('aria-current');
|
|
110
|
-
link
|
|
113
|
+
unwrapStrong(link);
|
|
111
114
|
}
|
|
112
115
|
});
|
|
113
116
|
[...getAllLinks].forEach((link) => {
|
|
@@ -129,16 +132,29 @@ export function MenuNavigation(aria) {
|
|
|
129
132
|
elementActive.innerHTML = `<strong class="font-bold">${elementActive.textContent}</strong>`;
|
|
130
133
|
};
|
|
131
134
|
|
|
135
|
+
// Unwrap the <strong> of `node` in place, preserving the rest of its markup.
|
|
136
|
+
// If there is no <strong> (e.g. a never-activated sibling with its own
|
|
137
|
+
// icon/span), the node is left untouched instead of flattened to text.
|
|
138
|
+
function unwrapStrong(node) {
|
|
139
|
+
if (!node) return;
|
|
140
|
+
const strong = node.querySelector('strong');
|
|
141
|
+
if (!strong) return;
|
|
142
|
+
while (strong.firstChild) {
|
|
143
|
+
strong.parentNode.insertBefore(strong.firstChild, strong);
|
|
144
|
+
}
|
|
145
|
+
strong.remove();
|
|
146
|
+
}
|
|
147
|
+
|
|
132
148
|
aria.MenuNavigation.prototype.deactivateElement = function (elementDeactivated) {
|
|
133
149
|
elementDeactivated.removeAttribute('aria-current');
|
|
134
150
|
elementDeactivated.classList.remove('c-menu-navigation__button--primary', 'c-menu-navigation__button--has-selection');
|
|
135
|
-
elementDeactivated
|
|
151
|
+
unwrapStrong(elementDeactivated);
|
|
136
152
|
};
|
|
137
153
|
|
|
138
154
|
window.activateItemMenuNavigation = function (menuId, activeItemId) {
|
|
139
155
|
const menu = document.getElementById(menuId);
|
|
140
156
|
if (menu) {
|
|
141
|
-
const activeItem =
|
|
157
|
+
const activeItem = menu.querySelector('#' + CSS.escape(activeItemId));
|
|
142
158
|
if (activeItem) {
|
|
143
159
|
const menuInstance = new aria.MenuNavigation(menu);
|
|
144
160
|
menuInstance.activateElement(activeItemId);
|
|
@@ -156,7 +172,7 @@ export function MenuNavigation(aria) {
|
|
|
156
172
|
window.activateSubItemMenuNavigation = function (menuId, activeSubItemId) {
|
|
157
173
|
const menu = document.getElementById(menuId);
|
|
158
174
|
if (menu) {
|
|
159
|
-
const activeSubItem =
|
|
175
|
+
const activeSubItem = menu.querySelector('#' + CSS.escape(activeSubItemId));
|
|
160
176
|
if (activeSubItem) {
|
|
161
177
|
const menuInstance = new aria.MenuNavigation(menu);
|
|
162
178
|
menuInstance.activateSubElement(menuId, activeSubItemId);
|
|
@@ -327,51 +343,4 @@ export function MenuNavigation(aria) {
|
|
|
327
343
|
aria.MenuNavigation.prototype.updateKeyControls = function (useArrowKeys) {
|
|
328
344
|
this.useArrowKeys = useArrowKeys;
|
|
329
345
|
};
|
|
330
|
-
|
|
331
|
-
window.addEventListener(
|
|
332
|
-
'load',
|
|
333
|
-
function () {
|
|
334
|
-
var menus = document.querySelectorAll('.disclosure-nav');
|
|
335
|
-
var disclosureMenus = [];
|
|
336
|
-
|
|
337
|
-
for (var i = 0; i < menus.length; i++) {
|
|
338
|
-
disclosureMenus[i] = new DisclosureNav(menus[i]);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// listen to arrow key checkbox
|
|
342
|
-
var arrowKeySwitch = document.getElementById('arrow-behavior-switch');
|
|
343
|
-
if (arrowKeySwitch) {
|
|
344
|
-
arrowKeySwitch.addEventListener('change', function () {
|
|
345
|
-
var checked = arrowKeySwitch.checked;
|
|
346
|
-
for (var i = 0; i < disclosureMenus.length; i++) {
|
|
347
|
-
disclosureMenus[i].updateKeyControls(checked);
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// fake link behavior
|
|
353
|
-
disclosureMenus.forEach((disclosureNav, i) => {
|
|
354
|
-
var links = menus[i].querySelectorAll('[href="#mythical-page-content"]');
|
|
355
|
-
var examplePageHeading = document.getElementById('mythical-page-heading');
|
|
356
|
-
for (var k = 0; k < links.length; k++) {
|
|
357
|
-
// The codepen export script updates the internal link href with a full URL
|
|
358
|
-
// we're just manually fixing that behavior here
|
|
359
|
-
links[k].href = '#mythical-page-content';
|
|
360
|
-
|
|
361
|
-
links[k].addEventListener('click', (event) => {
|
|
362
|
-
// change the heading text to fake a page change
|
|
363
|
-
var pageTitle = event.target.innerText;
|
|
364
|
-
examplePageHeading.innerText = pageTitle;
|
|
365
|
-
|
|
366
|
-
// handle aria-current
|
|
367
|
-
for (var n = 0; n < links.length; n++) {
|
|
368
|
-
links[n].removeAttribute('aria-current');
|
|
369
|
-
}
|
|
370
|
-
event.target.setAttribute('aria-current', 'page');
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
},
|
|
375
|
-
false
|
|
376
|
-
);
|
|
377
346
|
}
|
|
@@ -33,16 +33,16 @@ export function MenuVertical(aria) {
|
|
|
33
33
|
|
|
34
34
|
aria.MenuVertical.prototype.deactivateElement = function (elementDeactivated) {
|
|
35
35
|
elementDeactivated.removeAttribute('aria-current');
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
const getStrong = elementDeactivated.querySelector('strong');
|
|
37
|
+
if (getStrong) {
|
|
38
|
+
getStrong.replaceWith(...getStrong.childNodes);
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
window.activateItemMenuVertical = function (menuId, activeItemId) {
|
|
43
43
|
const menu = document.getElementById(menuId);
|
|
44
44
|
if (menu) {
|
|
45
|
-
const activeItem =
|
|
45
|
+
const activeItem = menu.querySelector('#' + CSS.escape(activeItemId));
|
|
46
46
|
if (activeItem) {
|
|
47
47
|
const menuInstance = new aria.MenuVertical(menu);
|
|
48
48
|
menuInstance.activateElement(activeItemId);
|
|
@@ -24,7 +24,7 @@ export function MenubarAction(aria) {
|
|
|
24
24
|
var msgPrefix = 'Menubar constructor argument domNode ';
|
|
25
25
|
|
|
26
26
|
// Check whether domNode is a DOM element
|
|
27
|
-
if (!domNode instanceof Element) {
|
|
27
|
+
if (!(domNode instanceof Element)) {
|
|
28
28
|
throw new TypeError(msgPrefix + 'is not a DOM Element.');
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -218,14 +218,14 @@ export function MenubarAction(aria) {
|
|
|
218
218
|
if (!this.isItemDisabled(this.menubarItems[index])) {
|
|
219
219
|
this.menubarItems[index].domNode.focus();
|
|
220
220
|
this.menubarItems[index].domNode.tabIndex = 0;
|
|
221
|
-
currentItem.tabIndex = -1;
|
|
221
|
+
currentItem.domNode.tabIndex = -1;
|
|
222
222
|
} else {
|
|
223
223
|
// Find the next enabled item that starts with the same character
|
|
224
224
|
var nextEnabledIndex = this.findNextEnabledItemWithChar(index, char);
|
|
225
225
|
if (nextEnabledIndex !== -1) {
|
|
226
226
|
this.menubarItems[nextEnabledIndex].domNode.focus();
|
|
227
227
|
this.menubarItems[nextEnabledIndex].domNode.tabIndex = 0;
|
|
228
|
-
currentItem.tabIndex = -1;
|
|
228
|
+
currentItem.domNode.tabIndex = -1;
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
}
|
|
@@ -293,7 +293,7 @@ export function MenubarAction(aria) {
|
|
|
293
293
|
};
|
|
294
294
|
|
|
295
295
|
aria.MenubarAction.prototype.activateElements = function (menuId, activateElements) {
|
|
296
|
-
const getAllElements = this.domNode.querySelectorAll(`#${menuId} ul > li `);
|
|
296
|
+
const getAllElements = this.domNode.querySelectorAll(`#${CSS.escape(menuId)} ul > li `);
|
|
297
297
|
const filterElements = [...getAllElements].filter((element) => element.id !== '' && element.getAttribute('role') !== 'separator');
|
|
298
298
|
[...filterElements].forEach((element) => {
|
|
299
299
|
if(isActive(element)) {
|
|
@@ -305,7 +305,7 @@ export function MenubarAction(aria) {
|
|
|
305
305
|
: element.parentElement.parentElement.querySelector('a');
|
|
306
306
|
|
|
307
307
|
getElementParent.classList.remove('c-menubar__button--has-selection');
|
|
308
|
-
getElementParent.
|
|
308
|
+
unwrapStrong(getElementParent.firstElementChild);
|
|
309
309
|
element.setAttribute('aria-checked', 'false');
|
|
310
310
|
element.innerHTML = `${element.textContent}`;
|
|
311
311
|
}
|
|
@@ -316,10 +316,33 @@ export function MenubarAction(aria) {
|
|
|
316
316
|
? element.parentElement.parentElement.parentElement.parentElement.querySelector('a')
|
|
317
317
|
: element.parentElement.parentElement.querySelector('a');
|
|
318
318
|
getElementParent.classList.add('c-menubar__button--has-selection');
|
|
319
|
-
getElementParent.
|
|
319
|
+
wrapStrong(getElementParent.firstElementChild);
|
|
320
320
|
}
|
|
321
321
|
});
|
|
322
322
|
|
|
323
|
+
// Wrap the existing children of `node` in a <strong> in place, preserving
|
|
324
|
+
// any markup the consumer put in the label (icons, SVG, spans).
|
|
325
|
+
function wrapStrong(node) {
|
|
326
|
+
if (!node || node.querySelector('strong')) return;
|
|
327
|
+
const strong = document.createElement('strong');
|
|
328
|
+
strong.className = 'font-bold';
|
|
329
|
+
while (node.firstChild) {
|
|
330
|
+
strong.appendChild(node.firstChild);
|
|
331
|
+
}
|
|
332
|
+
node.appendChild(strong);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Unwrap the <strong> of `node` in place, preserving the rest of its markup.
|
|
336
|
+
function unwrapStrong(node) {
|
|
337
|
+
if (!node) return;
|
|
338
|
+
const strong = node.querySelector('strong');
|
|
339
|
+
if (!strong) return;
|
|
340
|
+
while (strong.firstChild) {
|
|
341
|
+
strong.parentNode.insertBefore(strong.firstChild, strong);
|
|
342
|
+
}
|
|
343
|
+
strong.remove();
|
|
344
|
+
}
|
|
345
|
+
|
|
323
346
|
function isActive(element){
|
|
324
347
|
const { id } = element;
|
|
325
348
|
return typeof activateElements === "string"
|
|
@@ -65,8 +65,7 @@ export function MenubarItemAction(aria) {
|
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
aria.MenubarItemAction.prototype.handleKeydown = function (event) {
|
|
68
|
-
var
|
|
69
|
-
char = event.key,
|
|
68
|
+
var char = event.key,
|
|
70
69
|
flag = false;
|
|
71
70
|
|
|
72
71
|
function isPrintableCharacter (str) {
|
|
@@ -127,13 +126,6 @@ export function MenubarItemAction(aria) {
|
|
|
127
126
|
flag = true;
|
|
128
127
|
break;
|
|
129
128
|
|
|
130
|
-
case this.keyCode.ESC:
|
|
131
|
-
if (this.popupMenu) {
|
|
132
|
-
this.popupMenu.close();
|
|
133
|
-
}
|
|
134
|
-
flag = true;
|
|
135
|
-
break;
|
|
136
|
-
|
|
137
129
|
default:
|
|
138
130
|
if (isPrintableCharacter(char)) {
|
|
139
131
|
this.menubar.setFocusByFirstCharacter(this, char);
|
package/src/js/aria/Nav.js
CHANGED
|
@@ -33,16 +33,16 @@ export function Nav(aria) {
|
|
|
33
33
|
|
|
34
34
|
aria.Nav.prototype.deactivateElement = function (elementDeactivated) {
|
|
35
35
|
elementDeactivated.removeAttribute('aria-current');
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
const getStrong = elementDeactivated.querySelector('strong');
|
|
37
|
+
if (getStrong) {
|
|
38
|
+
getStrong.replaceWith(...getStrong.childNodes);
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
window.activateItemNav = function (menuId, activeItemId) {
|
|
43
43
|
const menu = document.getElementById(menuId);
|
|
44
44
|
if (menu) {
|
|
45
|
-
const activeItem =
|
|
45
|
+
const activeItem = menu.querySelector('#' + CSS.escape(activeItemId));
|
|
46
46
|
if (activeItem) {
|
|
47
47
|
const menuInstance = new aria.Nav(menu);
|
|
48
48
|
menuInstance.activateElement(activeItemId);
|
|
@@ -35,7 +35,7 @@ export function PopupMenuAction(aria) {
|
|
|
35
35
|
msgPrefix = 'PopupMenu constructor argument domNode ';
|
|
36
36
|
|
|
37
37
|
// Check whether domNode is a DOM element
|
|
38
|
-
if (!domNode instanceof Element) {
|
|
38
|
+
if (!(domNode instanceof Element)) {
|
|
39
39
|
throw new TypeError(msgPrefix + 'is not a DOM Element.');
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -71,7 +71,7 @@ export function PopupMenuAction(aria) {
|
|
|
71
71
|
this.domNode.setAttribute('role', 'menu');
|
|
72
72
|
|
|
73
73
|
if (!this.domNode.getAttribute('aria-labelledby') && !this.domNode.getAttribute('aria-label') && !this.domNode.getAttribute('title')) {
|
|
74
|
-
label = this.controller.domNode.
|
|
74
|
+
label = this.controller.domNode.textContent.trim();
|
|
75
75
|
this.domNode.setAttribute('aria-label', label);
|
|
76
76
|
}
|
|
77
77
|
|
package/src/js/aria/accordion.js
CHANGED
|
@@ -159,7 +159,8 @@ export function accordion(aria) {
|
|
|
159
159
|
const getAllElementsHide = target.parentElement.parentElement.querySelectorAll('.c-accordion__hide')
|
|
160
160
|
const getAllPanels = target.parentElement.parentElement.querySelectorAll('.c-accordion__panel')
|
|
161
161
|
const getAllTriggers = target.parentElement.parentElement.querySelectorAll('.c-accordion__trigger')
|
|
162
|
-
|
|
162
|
+
var shouldShow = (show === null) ? target.textContent.includes('Mostrar todo') : (show === true);
|
|
163
|
+
if(shouldShow) {
|
|
163
164
|
getAllElementsShow.forEach(element => {
|
|
164
165
|
element.classList.add('hidden')
|
|
165
166
|
})
|
|
@@ -205,17 +206,33 @@ export function accordion(aria) {
|
|
|
205
206
|
|
|
206
207
|
window.activateItemAccordion = function (menuId, activeItemId) {
|
|
207
208
|
const menu = document.getElementById(menuId);
|
|
208
|
-
if (menu) {
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
if (!menu) {
|
|
210
|
+
console.log('There is no accordion with this id in the document.');
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (Array.isArray(activeItemId)) {
|
|
215
|
+
const activeItems = activeItemId.map(function (id) {
|
|
216
|
+
return document.getElementById(id);
|
|
217
|
+
});
|
|
218
|
+
const allInMenu = activeItems.every(function (item) {
|
|
219
|
+
return item && menu.contains(item);
|
|
220
|
+
});
|
|
221
|
+
if (allInMenu) {
|
|
211
222
|
activateElement(menuId, activeItemId);
|
|
212
|
-
return [menu,
|
|
223
|
+
return [menu, activeItems];
|
|
213
224
|
} else {
|
|
214
225
|
console.log('There is no item with this id in the menu.');
|
|
215
226
|
return null;
|
|
216
227
|
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const activeItem = document.getElementById(activeItemId);
|
|
231
|
+
if (activeItem && menu.contains(activeItem)) {
|
|
232
|
+
activateElement(menuId, activeItemId);
|
|
233
|
+
return [menu, activeItem]
|
|
217
234
|
} else {
|
|
218
|
-
console.log('There is no
|
|
235
|
+
console.log('There is no item with this id in the menu.');
|
|
219
236
|
return null;
|
|
220
237
|
}
|
|
221
238
|
};
|
|
@@ -232,10 +249,10 @@ export function accordion(aria) {
|
|
|
232
249
|
};
|
|
233
250
|
|
|
234
251
|
function activateElement(menu, activeItem) {
|
|
235
|
-
const getAccordion = document.
|
|
252
|
+
const getAccordion = document.getElementById(menu);
|
|
236
253
|
const allowMultiple = getAccordion.hasAttribute('data-allow-multiple');
|
|
237
|
-
const allowToggle = (allowMultiple) ? allowMultiple :
|
|
238
|
-
const selectAllTriggers =
|
|
254
|
+
const allowToggle = (allowMultiple) ? allowMultiple : getAccordion.hasAttribute('data-allow-toggle');
|
|
255
|
+
const selectAllTriggers = getAccordion.querySelectorAll('.c-accordion__trigger');
|
|
239
256
|
[...selectAllTriggers].forEach((trigger) => {
|
|
240
257
|
const getPanel = trigger.parentElement.parentElement.querySelector('.c-accordion__panel');
|
|
241
258
|
const getShowMessage = trigger.querySelector('.c-accordion__show');
|
|
@@ -255,7 +272,7 @@ export function accordion(aria) {
|
|
|
255
272
|
|
|
256
273
|
function isActive(element){
|
|
257
274
|
const { id } = element;
|
|
258
|
-
return
|
|
275
|
+
return Array.isArray(activeItem)
|
|
259
276
|
? activeItem.includes(id)
|
|
260
277
|
: id === activeItem;
|
|
261
278
|
}
|
package/src/js/aria/alert.js
CHANGED
|
@@ -5,8 +5,15 @@ export function alert(aria) {
|
|
|
5
5
|
throw new Error('No element found with id="' + alertId + '".');
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
var templateNode = document.getElementById('template-' + alertId);
|
|
9
|
+
if (templateNode === null) {
|
|
10
|
+
throw new Error('No template found with id="template-' + alertId + '".');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
this.alertNode.innerHTML = templateNode.innerHTML;
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
// Capture the previously focused element before moving focus below.
|
|
16
|
+
this.lastFocus = document.activeElement;
|
|
10
17
|
|
|
11
18
|
if (typeof focusAfterClosed === 'string') {
|
|
12
19
|
this.focusAfterClosed = document.getElementById(focusAfterClosed);
|
|
@@ -32,8 +39,6 @@ export function alert(aria) {
|
|
|
32
39
|
if (this.focusFirst) {
|
|
33
40
|
this.focusFirst.focus();
|
|
34
41
|
}
|
|
35
|
-
|
|
36
|
-
this.lastFocus = document.activeElement;
|
|
37
42
|
}; // end Alert constructor
|
|
38
43
|
|
|
39
44
|
window.openAlert = function (alertId, focusAfterClosed, focusFirst) {
|
package/src/js/aria/combobox.js
CHANGED
|
@@ -147,9 +147,17 @@ export function combobox(aria) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
if (text.length === 0 && !this.multiselectable) {
|
|
150
|
+
var hadSelection = this.selectedValue || this.hiddenInputNode.value;
|
|
150
151
|
for (var i = 0; i < this.allOptions.length; i++) {
|
|
151
152
|
this.allOptions[i].node.setAttribute('aria-selected', 'false');
|
|
152
153
|
}
|
|
154
|
+
this.selectedValue = '';
|
|
155
|
+
this.selectedId = '';
|
|
156
|
+
this.hiddenInputNode.value = '';
|
|
157
|
+
this.hiddenInputNode.setAttribute('value', '');
|
|
158
|
+
if (hadSelection) {
|
|
159
|
+
this.dispatchCustomEvent('combobox:clear', {});
|
|
160
|
+
}
|
|
153
161
|
}
|
|
154
162
|
|
|
155
163
|
this._isDeleting = false;
|
|
@@ -569,10 +577,15 @@ export function combobox(aria) {
|
|
|
569
577
|
|
|
570
578
|
var inputNode = comboboxNode.querySelector('[data-module="c-combobox-input"]');
|
|
571
579
|
if (inputNode && inputNode._comboboxInstance) {
|
|
572
|
-
|
|
573
|
-
|
|
580
|
+
var instance = inputNode._comboboxInstance;
|
|
581
|
+
if (!instance.listboxNode.contains(option)) {
|
|
582
|
+
console.log('The option does not belong to the target combobox.');
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
if (instance.multiselectable) {
|
|
586
|
+
instance.toggleOption(option);
|
|
574
587
|
} else {
|
|
575
|
-
|
|
588
|
+
instance.selectOption(option);
|
|
576
589
|
}
|
|
577
590
|
return [comboboxNode, option];
|
|
578
591
|
}
|
package/src/js/aria/dataGrid.js
CHANGED
|
@@ -524,33 +524,34 @@ export function dataGrid(aria) {
|
|
|
524
524
|
sortType = aria.SortType.ASCENDING;
|
|
525
525
|
}
|
|
526
526
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
var row1Text = row1.children[columnIndex].innerText;
|
|
532
|
-
var row2Text = row2.children[columnIndex].innerText;
|
|
533
|
-
var row1Value = parseInt(row1Text) ? parseInt(row1Text.replace(/[^0-9\.]+/g, '')) : row1Text
|
|
534
|
-
var row2Value = parseInt(row2Text) ? parseInt(row2Text.replace(/[^0-9\.]+/g, '')) : row2Text
|
|
535
|
-
|
|
536
|
-
var orderValue = parseInt(row1Text) ? 'numbers' : 'strings'
|
|
537
|
-
|
|
538
|
-
if (sortType === aria.SortType.ASCENDING && orderValue === 'numbers') {
|
|
539
|
-
return row1Value - row2Value;
|
|
527
|
+
function parseNumeric(text) {
|
|
528
|
+
var cleaned = text.replace(/[^0-9.\-]+/g, ''); // conserva dígitos, punto y signo
|
|
529
|
+
if (cleaned === '' || cleaned === '-' || cleaned === '.') {
|
|
530
|
+
return NaN;
|
|
540
531
|
}
|
|
532
|
+
return parseFloat(cleaned);
|
|
533
|
+
}
|
|
541
534
|
|
|
542
|
-
|
|
543
|
-
|
|
535
|
+
var comparator = function (row1, row2) {
|
|
536
|
+
var cell1 = row1.children[columnIndex];
|
|
537
|
+
var cell2 = row2.children[columnIndex];
|
|
538
|
+
if (cell1.classList.contains('align-top') || cell2.classList.contains('align-top')) {
|
|
539
|
+
return 0;
|
|
544
540
|
}
|
|
541
|
+
var row1Text = cell1.innerText;
|
|
542
|
+
var row2Text = cell2.innerText;
|
|
545
543
|
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
544
|
+
var row1Value = parseNumeric(row1Text);
|
|
545
|
+
var row2Value = parseNumeric(row2Text);
|
|
549
546
|
|
|
550
|
-
|
|
551
|
-
|
|
547
|
+
var result;
|
|
548
|
+
if (!isNaN(row1Value) && !isNaN(row2Value)) {
|
|
549
|
+
result = row1Value - row2Value; // signo y cero correctos
|
|
550
|
+
} else {
|
|
551
|
+
result = row1Text.toString().localeCompare(row2Text.toString()); // fallback string
|
|
552
552
|
}
|
|
553
553
|
|
|
554
|
+
return (sortType === aria.SortType.ASCENDING) ? result : -result;
|
|
554
555
|
};
|
|
555
556
|
|
|
556
557
|
this.sortRows(comparator);
|
|
@@ -574,9 +575,11 @@ export function dataGrid(aria) {
|
|
|
574
575
|
* Comparison function to sort the rows
|
|
575
576
|
*/
|
|
576
577
|
aria.Grid.prototype.sortRows = function (compareFn) {
|
|
577
|
-
var rows = this.gridNode.querySelectorAll("tbody tr");
|
|
578
|
+
var rows = Array.prototype.slice.call(this.gridNode.querySelectorAll("tbody tr"));
|
|
578
579
|
var rowWrapper = this.gridNode.querySelector('tbody');
|
|
579
|
-
|
|
580
|
+
// La fila de filtro contiene controles de búsqueda/selección; las filas de datos no.
|
|
581
|
+
var hasFilterRow = rows.length > 0 && rows[0].querySelector('input[type="search"], select');
|
|
582
|
+
var dataRows = hasFilterRow ? rows.slice(1) : rows;
|
|
580
583
|
|
|
581
584
|
dataRows.sort(compareFn);
|
|
582
585
|
|
package/src/js/aria/linksList.js
CHANGED
|
@@ -31,12 +31,34 @@ export function LinksList(aria) {
|
|
|
31
31
|
|
|
32
32
|
function wrapActiveElement(elementActive) {
|
|
33
33
|
const getText = elementActive.querySelector('div[data-element="c-links-list__text"]');
|
|
34
|
-
getText
|
|
34
|
+
wrapStrong(getText);
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
function deactivateElement(elementDeactivated) {
|
|
38
38
|
const getText = elementDeactivated.querySelector('div[data-element="c-links-list__text"]');
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
unwrapStrong(getText);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Wrap the existing children of `node` in a <strong> in place, preserving
|
|
43
|
+
// any markup the consumer passed in item.html (icons, spans, etc.).
|
|
44
|
+
function wrapStrong(node) {
|
|
45
|
+
if (!node || node.querySelector('strong')) return;
|
|
46
|
+
const strong = document.createElement('strong');
|
|
47
|
+
strong.className = 'font-bold';
|
|
48
|
+
while (node.firstChild) {
|
|
49
|
+
strong.appendChild(node.firstChild);
|
|
50
|
+
}
|
|
51
|
+
node.appendChild(strong);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Unwrap the <strong> of `node` in place, preserving the rest of its markup.
|
|
55
|
+
function unwrapStrong(node) {
|
|
56
|
+
if (!node) return;
|
|
57
|
+
const strong = node.querySelector('strong');
|
|
58
|
+
if (!strong) return;
|
|
59
|
+
while (strong.firstChild) {
|
|
60
|
+
strong.parentNode.insertBefore(strong.firstChild, strong);
|
|
61
|
+
}
|
|
62
|
+
strong.remove();
|
|
41
63
|
};
|
|
42
64
|
}
|
package/src/js/aria/listbox.js
CHANGED
|
@@ -463,7 +463,7 @@ export function listbox(aria) {
|
|
|
463
463
|
return;
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
-
currentItem = document.getElementById(this.activeDescendant);
|
|
466
|
+
var currentItem = document.getElementById(this.activeDescendant);
|
|
467
467
|
previousItem = currentItem.previousElementSibling;
|
|
468
468
|
|
|
469
469
|
if (previousItem) {
|
|
@@ -486,7 +486,7 @@ export function listbox(aria) {
|
|
|
486
486
|
return;
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
-
currentItem = document.getElementById(this.activeDescendant);
|
|
489
|
+
var currentItem = document.getElementById(this.activeDescendant);
|
|
490
490
|
nextItem = currentItem.nextElementSibling;
|
|
491
491
|
|
|
492
492
|
if (nextItem) {
|
|
@@ -565,7 +565,7 @@ export function listbox(aria) {
|
|
|
565
565
|
if (isActive(element, activeItem)) {
|
|
566
566
|
element.setAttribute('aria-selected', 'true');
|
|
567
567
|
if (!allowMultiple && getListBoxButton.dataset.change) {
|
|
568
|
-
getListBoxButtonText.
|
|
568
|
+
getListBoxButtonText.textContent = element.textContent;
|
|
569
569
|
}
|
|
570
570
|
} else {
|
|
571
571
|
element.setAttribute('aria-selected', 'false');
|
package/src/js/aria/tabs.js
CHANGED
|
@@ -229,7 +229,7 @@ export function tabs(aria) {
|
|
|
229
229
|
|
|
230
230
|
// Detect if a tab is deletable
|
|
231
231
|
function determineDeletable (event) {
|
|
232
|
-
target = event.target;
|
|
232
|
+
var target = event.target;
|
|
233
233
|
|
|
234
234
|
if (target.getAttribute('data-deletable') !== null) {
|
|
235
235
|
// Delete target tab
|
|
@@ -251,7 +251,8 @@ export function tabs(aria) {
|
|
|
251
251
|
// Deletes a tab and its panel
|
|
252
252
|
function deleteTab (event) {
|
|
253
253
|
var target = event.target;
|
|
254
|
-
var
|
|
254
|
+
var controls = target.getAttribute('aria-controls');
|
|
255
|
+
var panel = elem.querySelector('#' + controls);
|
|
255
256
|
|
|
256
257
|
target.parentElement.removeChild(target);
|
|
257
258
|
panel.parentElement.removeChild(panel);
|
|
@@ -281,17 +282,30 @@ export function tabs(aria) {
|
|
|
281
282
|
//Add active class to current tab
|
|
282
283
|
element.classList.add("c-tabs__link--is-active");
|
|
283
284
|
element.setAttribute('role', 'tab');
|
|
285
|
+
// Wrap the existing children in a <strong> in place, preserving any
|
|
286
|
+
// markup the consumer put in the label (icons, SVG, spans).
|
|
284
287
|
if(element.querySelector('strong') === null) {
|
|
285
|
-
|
|
288
|
+
const strong = document.createElement('strong');
|
|
289
|
+
strong.className = 'font-bold';
|
|
290
|
+
while (element.firstChild) {
|
|
291
|
+
strong.appendChild(element.firstChild);
|
|
292
|
+
}
|
|
293
|
+
element.appendChild(strong);
|
|
286
294
|
}
|
|
287
295
|
}
|
|
288
296
|
|
|
289
297
|
function removeActiveClass(element) {
|
|
290
298
|
element.classList.remove("c-tabs__link--is-active");
|
|
291
299
|
element.removeAttribute('role');
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
300
|
+
// Unwrap only the <strong> we added (direct child with font-bold),
|
|
301
|
+
// replacing it with its children. A consumer-provided <strong> nested
|
|
302
|
+
// in the label is left untouched.
|
|
303
|
+
const strong = element.querySelector(':scope > strong.font-bold');
|
|
304
|
+
if (strong) {
|
|
305
|
+
while (strong.firstChild) {
|
|
306
|
+
element.insertBefore(strong.firstChild, strong);
|
|
307
|
+
}
|
|
308
|
+
strong.remove();
|
|
295
309
|
}
|
|
296
310
|
}
|
|
297
311
|
|
package/src/js/aria/tree.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { escapeHtml, safeHref } from './utils.js';
|
|
2
|
+
|
|
1
3
|
export function Tree(aria) {
|
|
2
4
|
/*
|
|
3
5
|
* This content is licensed according to the W3C Software License at
|
|
@@ -49,7 +51,7 @@ export function Tree(aria) {
|
|
|
49
51
|
if (paramsRaw) {
|
|
50
52
|
this.params = JSON.parse(paramsRaw);
|
|
51
53
|
this.describedBy = this.params.describedBy ?? "";
|
|
52
|
-
if (this.params.fieldset
|
|
54
|
+
if (this.params.fieldset?.describedBy)
|
|
53
55
|
this.describedBy = this.params.fieldset.describedBy;
|
|
54
56
|
this.hasFieldset = this.params.fieldset ?? false;
|
|
55
57
|
}
|
|
@@ -95,6 +97,32 @@ export function Tree(aria) {
|
|
|
95
97
|
}
|
|
96
98
|
};
|
|
97
99
|
|
|
100
|
+
// Serializes dynamic attributes with a strict allow-list: only well-formed data-*/aria-*
|
|
101
|
+
// names or exactly {title, role, id, class}. The [a-z0-9-] pattern rejects keys with spaces,
|
|
102
|
+
// '=', quotes, '<', '>' or '/', closing the "data-x onmouseover=..." bypass. Both the name
|
|
103
|
+
// and the value are escaped.
|
|
104
|
+
function safeAttrs(obj) {
|
|
105
|
+
var out = '';
|
|
106
|
+
var dataAria = /^(data|aria)-[a-z0-9-]+$/;
|
|
107
|
+
var exact = { title: true, role: true, id: true, class: true };
|
|
108
|
+
for (var a in obj || {}) {
|
|
109
|
+
var key = String(a).toLowerCase();
|
|
110
|
+
if (!dataAria.test(key) && exact[key] !== true) continue;
|
|
111
|
+
out += ' ' + escapeHtml(a) + '="' + escapeHtml(obj[a]) + '"';
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Returns the consumer's intentional HTML (node.html) or, otherwise, the escaped text.
|
|
117
|
+
function htmlOrText(node) {
|
|
118
|
+
return node.html ? node.html : escapeHtml(node.text || '');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Suffix ' escaped-class' to concatenate after fixed classes, or '' if none.
|
|
122
|
+
function optClasses(cls) {
|
|
123
|
+
return cls ? ' ' + escapeHtml(cls) : '';
|
|
124
|
+
}
|
|
125
|
+
|
|
98
126
|
function checkboxItem(item, id, name, type, params, describedBy, hasFieldset) {
|
|
99
127
|
name = name || item.name || '';
|
|
100
128
|
var hasHint = item.hint && (item.hint.text || item.hint.html);
|
|
@@ -105,7 +133,7 @@ export function Tree(aria) {
|
|
|
105
133
|
// Wrapper
|
|
106
134
|
var html = '<div' +
|
|
107
135
|
((params.hasDividers ? ' class="border-t border-b border-neutral-base -mb-px' : '') +
|
|
108
|
-
(item.classes
|
|
136
|
+
optClasses(item.classes) + '"').replace('""', '') +
|
|
109
137
|
'>' +
|
|
110
138
|
'<div class="relative flex items-start py-xs">';
|
|
111
139
|
|
|
@@ -120,13 +148,13 @@ export function Tree(aria) {
|
|
|
120
148
|
((item.isIndeterminate && item.indeterminateChecked)
|
|
121
149
|
? ' c-checkboxes__indeterminate-active'
|
|
122
150
|
: '') + '"' +
|
|
123
|
-
' id="' + id + '-input"' +
|
|
124
|
-
' name="' + name + '"' +
|
|
151
|
+
' id="' + escapeHtml(id) + '-input"' +
|
|
152
|
+
' name="' + escapeHtml(name) + '"' +
|
|
125
153
|
(type === 'checkbox' ? ' type="checkbox" role="checkbox"' : ' type="radio"') +
|
|
126
|
-
' value="' + (item.value || '') + '"' +
|
|
154
|
+
' value="' + escapeHtml(item.value || '') + '"' +
|
|
127
155
|
(item.checked ? ' checked' : '') +
|
|
128
156
|
(item.disabled ? ' disabled' : '') +
|
|
129
|
-
(itemDescribed ? ' aria-describedby="' + itemDescribed + '"' : '') +
|
|
157
|
+
(itemDescribed ? ' aria-describedby="' + escapeHtml(itemDescribed) + '"' : '') +
|
|
130
158
|
(params.errorMessage ? ' aria-invalid="true"' : '');
|
|
131
159
|
|
|
132
160
|
if (item.isIndeterminate || item.indeterminateChecked) {
|
|
@@ -136,62 +164,58 @@ export function Tree(aria) {
|
|
|
136
164
|
'"';
|
|
137
165
|
}
|
|
138
166
|
html += '>' +
|
|
139
|
-
'</div>' + //
|
|
167
|
+
'</div>' + // closes flex items-center
|
|
140
168
|
'<div class="flex-1 pt-0.5 leading-5">';
|
|
141
169
|
|
|
142
170
|
// Label
|
|
143
171
|
var lblCls = (item.label && item.label.classes)
|
|
144
172
|
? item.label.classes
|
|
145
173
|
: 'block relative -top-xs -left-8 pl-8 py-xs';
|
|
146
|
-
html += '<label for="' + id + '-input" class="' + lblCls + '">';
|
|
147
|
-
html +=
|
|
174
|
+
html += '<label for="' + escapeHtml(id) + '-input" class="' + escapeHtml(lblCls) + '">';
|
|
175
|
+
html += htmlOrText(item);
|
|
148
176
|
html += '</label>';
|
|
149
177
|
|
|
150
178
|
// Hint
|
|
151
179
|
if (hasHint) {
|
|
152
|
-
html += '<div id="' + itemHintId + '"' +
|
|
153
|
-
(item.hint.classes ? ' class="' + item.hint.classes + '"' : '') +
|
|
180
|
+
html += '<div id="' + escapeHtml(itemHintId) + '"' +
|
|
181
|
+
(item.hint.classes ? ' class="' + escapeHtml(item.hint.classes) + '"' : '') +
|
|
154
182
|
'>';
|
|
155
|
-
html += item.hint
|
|
183
|
+
html += htmlOrText(item.hint);
|
|
156
184
|
html += '</div>';
|
|
157
185
|
}
|
|
158
|
-
html += '</div>'; //
|
|
186
|
+
html += '</div>'; // closes flex-1
|
|
159
187
|
}
|
|
160
188
|
|
|
161
189
|
// Navigation branch
|
|
162
190
|
if (type === 'navigation') {
|
|
163
191
|
if (item.href) {
|
|
164
192
|
html += '<a' +
|
|
165
|
-
(id ? ' id="' + id + '-link"' : '') +
|
|
166
|
-
' href="' + item.href + '"' +
|
|
193
|
+
(id ? ' id="' + escapeHtml(id) + '-link"' : '') +
|
|
194
|
+
' href="' + escapeHtml(safeHref(item.href)) + '"' +
|
|
167
195
|
' class="block px-xs focus:bg-warning-base focus:outline-hidden ' +
|
|
168
196
|
'focus:shadow-outline-focus focus:text-black' +
|
|
169
|
-
(item.classes
|
|
197
|
+
optClasses(item.classes) +
|
|
170
198
|
(!item.disabled ? ' hover:text-primary-base hover:underline'
|
|
171
199
|
: ' no-underline pointer-events-none') + '"' +
|
|
172
|
-
(item.title ? ' title="' + item.title + '"' : '') +
|
|
200
|
+
(item.title ? ' title="' + escapeHtml(item.title) + '"' : '') +
|
|
173
201
|
(item.active ? ' aria-current="page"' : '') +
|
|
174
202
|
(item.disabled ? ' aria-disabled="true" tabindex="-1"' : '') +
|
|
175
|
-
(item.target ? ' target="' + item.target + '"' : '') +
|
|
203
|
+
(item.target ? ' target="' + escapeHtml(item.target) + '"' : '') +
|
|
176
204
|
'>';
|
|
177
205
|
html += item.active
|
|
178
|
-
? '<strong class="font-bold">' +
|
|
179
|
-
(item
|
|
180
|
-
'</strong>'
|
|
181
|
-
: (item.html || item.text);
|
|
206
|
+
? '<strong class="font-bold">' + htmlOrText(item) + '</strong>'
|
|
207
|
+
: htmlOrText(item);
|
|
182
208
|
html += '</a>';
|
|
183
209
|
} else {
|
|
184
210
|
html += '<span' +
|
|
185
|
-
(id ? ' id="' + id + '-link"' : '') +
|
|
211
|
+
(id ? ' id="' + escapeHtml(id) + '-link"' : '') +
|
|
186
212
|
' class="block px-xs' +
|
|
187
|
-
(item.classes
|
|
188
|
-
(item.title ? ' title="' + item.title + '"' : '') +
|
|
213
|
+
optClasses(item.classes) + '"' +
|
|
214
|
+
(item.title ? ' title="' + escapeHtml(item.title) + '"' : '') +
|
|
189
215
|
'>';
|
|
190
216
|
html += item.active
|
|
191
|
-
? '<strong class="font-bold">' +
|
|
192
|
-
(item
|
|
193
|
-
'</strong>'
|
|
194
|
-
: (item.html || item.text);
|
|
217
|
+
? '<strong class="font-bold">' + htmlOrText(item) + '</strong>'
|
|
218
|
+
: htmlOrText(item);
|
|
195
219
|
html += '</span>';
|
|
196
220
|
}
|
|
197
221
|
}
|
|
@@ -209,10 +233,8 @@ export function Tree(aria) {
|
|
|
209
233
|
'<div class="flex items-center relative ' +
|
|
210
234
|
'focus:bg-warning-base focus:outline-hidden ' +
|
|
211
235
|
'focus:shadow-outline-focus focus:text-black text-left' +
|
|
212
|
-
(itemNode.classes
|
|
213
|
-
|
|
214
|
-
html += ' ' + a + '="' + itemNode.attributes[a] + '"';
|
|
215
|
-
}
|
|
236
|
+
optClasses(itemNode.classes) + '"';
|
|
237
|
+
html += safeAttrs(itemNode.attributes);
|
|
216
238
|
html += '>' +
|
|
217
239
|
'<span class="c-tree__icon absolute top-3 -left-4 flex items-center ' +
|
|
218
240
|
'w-4 h-2.5 text-primary-base font-bold ' +
|
|
@@ -238,10 +260,8 @@ export function Tree(aria) {
|
|
|
238
260
|
|
|
239
261
|
if (itemNode.sub && itemNode.sub.items) {
|
|
240
262
|
html += '<ul role="group" class="c-tree__itemgroup' +
|
|
241
|
-
(itemNode.sub.classes
|
|
242
|
-
|
|
243
|
-
html += ' ' + b + '="' + itemNode.sub.attributes[b] + '"';
|
|
244
|
-
}
|
|
263
|
+
optClasses(itemNode.sub.classes) + '"';
|
|
264
|
+
html += safeAttrs(itemNode.sub.attributes);
|
|
245
265
|
html += '>';
|
|
246
266
|
|
|
247
267
|
if (!lazyRendering || onlySub) {
|
|
@@ -259,7 +279,7 @@ export function Tree(aria) {
|
|
|
259
279
|
: '';
|
|
260
280
|
|
|
261
281
|
html += '<li class="' + clsBase + '" ' +
|
|
262
|
-
'id="' + subId + '" ' +
|
|
282
|
+
'id="' + escapeHtml(subId) + '" ' +
|
|
263
283
|
'role="treeitem" ' +
|
|
264
284
|
'data-module="c-tree__item"' +
|
|
265
285
|
expanded +
|
|
@@ -279,10 +299,10 @@ export function Tree(aria) {
|
|
|
279
299
|
);
|
|
280
300
|
} else {
|
|
281
301
|
html += '<div class="block' +
|
|
282
|
-
(subitem.classes
|
|
302
|
+
optClasses(subitem.classes) +
|
|
283
303
|
(subitem.active ? ' font-bold' : '') +
|
|
284
304
|
'"' +
|
|
285
|
-
(subitem.title ? ' title="' + subitem.title + '"' : '') +
|
|
305
|
+
(subitem.title ? ' title="' + escapeHtml(subitem.title) + '"' : '') +
|
|
286
306
|
(subitem.active ? ' aria-current="page"' : '') +
|
|
287
307
|
'>';
|
|
288
308
|
if (subitem.active) {
|
package/src/js/aria/treeitem.js
CHANGED
|
@@ -149,7 +149,7 @@ export function Treeitem(aria) {
|
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
if (event.
|
|
152
|
+
if (event.shiftKey) {
|
|
153
153
|
if (isPrintableCharacter(char)) {
|
|
154
154
|
printableCharacter(this);
|
|
155
155
|
}
|
|
@@ -158,14 +158,18 @@ export function Treeitem(aria) {
|
|
|
158
158
|
switch (event.keyCode) {
|
|
159
159
|
case this.keyCode.SPACE:
|
|
160
160
|
case this.keyCode.RETURN:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
// Navigation trees have no checkbox/radio: skip the toggle but still
|
|
162
|
+
// swallow the key so it doesn't propagate.
|
|
163
|
+
if (getCurrentCheckbox) {
|
|
164
|
+
const typeOfInput = getCurrentCheckbox.type
|
|
165
|
+
if (typeOfInput === 'radio') {
|
|
166
|
+
for (let item of getAllChildrenOfTree) {
|
|
167
|
+
item.checked = false
|
|
168
|
+
}
|
|
165
169
|
}
|
|
170
|
+
getCurrentCheckbox.checked = getCurrentCheckbox.checked === true ? false : true
|
|
171
|
+
targetElement.setAttribute('aria-checked', getCurrentCheckbox.checked ? 'true' : 'false');
|
|
166
172
|
}
|
|
167
|
-
getCurrentCheckbox.checked = getCurrentCheckbox.checked === true ? false : true
|
|
168
|
-
targetElement.setAttribute('aria-checked', getCurrentCheckbox.checked ? 'true' : 'false');
|
|
169
173
|
flag = true;
|
|
170
174
|
break;
|
|
171
175
|
|
|
@@ -279,11 +283,16 @@ export function Treeitem(aria) {
|
|
|
279
283
|
};
|
|
280
284
|
|
|
281
285
|
function arrayOrSingleElement(elementId, itemsIds, open) {
|
|
286
|
+
// Resolve the navigation flag once so every branch (array and single) treats
|
|
287
|
+
// tree-navigation items consistently.
|
|
288
|
+
const getElement = document.getElementById(elementId);
|
|
289
|
+
const isTreeNavigation = getElement ? getElement.hasAttribute('data-tree-navigation') : false;
|
|
290
|
+
|
|
282
291
|
if (typeof itemsIds === 'object' && open !== null) {
|
|
283
292
|
itemsIds.forEach((item) => {
|
|
284
293
|
const selectItem = document.querySelector(`#${elementId} #${item}`)
|
|
285
294
|
if (selectItem) {
|
|
286
|
-
activateElement(selectItem, open)
|
|
295
|
+
activateElement(selectItem, open, isTreeNavigation)
|
|
287
296
|
} else {
|
|
288
297
|
returnMessage()
|
|
289
298
|
}
|
|
@@ -291,8 +300,6 @@ export function Treeitem(aria) {
|
|
|
291
300
|
}
|
|
292
301
|
|
|
293
302
|
if (typeof itemsIds !== 'object' && open !== null) {
|
|
294
|
-
const getElement = document.querySelector(`#${elementId}`);
|
|
295
|
-
const isTreeNavigation = getElement.hasAttribute('data-tree-navigation');
|
|
296
303
|
const selectItem = document.querySelector(`#${elementId} #${itemsIds}`)
|
|
297
304
|
if (selectItem) {
|
|
298
305
|
activateElement(selectItem, open, isTreeNavigation)
|
|
@@ -305,7 +312,7 @@ export function Treeitem(aria) {
|
|
|
305
312
|
itemsIds.forEach((item) => {
|
|
306
313
|
const selectItem = document.querySelector(`#${elementId} #${item[0]}`)
|
|
307
314
|
if (selectItem) {
|
|
308
|
-
activateElement(selectItem, item[1])
|
|
315
|
+
activateElement(selectItem, item[1], isTreeNavigation)
|
|
309
316
|
} else {
|
|
310
317
|
console.log('There is no item with this id in the document.');
|
|
311
318
|
return null;
|
|
@@ -319,8 +326,10 @@ export function Treeitem(aria) {
|
|
|
319
326
|
item.setAttribute('aria-expanded', 'true');
|
|
320
327
|
if (treeNav) {
|
|
321
328
|
const getLink = item.querySelector('a')
|
|
322
|
-
getLink
|
|
323
|
-
|
|
329
|
+
if (getLink) {
|
|
330
|
+
getLink.setAttribute("aria-current", "page");
|
|
331
|
+
getLink.innerHTML = `<strong class="font-bold">${getLink.textContent}</strong>`;
|
|
332
|
+
}
|
|
324
333
|
}
|
|
325
334
|
recursiveParent(item)
|
|
326
335
|
} else {
|
package/src/js/aria/utils.js
CHANGED
|
@@ -125,6 +125,27 @@ export function utils(aria) {
|
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
var HTML_ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
129
|
+
|
|
130
|
+
// Escapes & " ' < > for safe use in HTML content and attributes. null/undefined -> ''.
|
|
131
|
+
export function escapeHtml(value) {
|
|
132
|
+
if (value === null || value === undefined) return '';
|
|
133
|
+
return String(value).replace(/[&<>"']/g, function (c) { return HTML_ESCAPES[c]; });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Neutralizes dangerous schemes in href via an allow-list: relative URLs or http/https/mailto/tel.
|
|
137
|
+
// Strips C0 control chars (tab/LF/CR included) before probing, since the browser's URL parser would
|
|
138
|
+
// remove them and reconstruct schemes like "java\tscript:". Any other scheme returns '#'.
|
|
139
|
+
export function safeHref(value) {
|
|
140
|
+
if (value === null || value === undefined) return '';
|
|
141
|
+
var v = String(value);
|
|
142
|
+
var probe = v.replace(/[\u0000-\u001F]/g, '').trim().toLowerCase();
|
|
143
|
+
var scheme = probe.match(/^([a-z][a-z0-9+.-]*):/);
|
|
144
|
+
if (!scheme) return v;
|
|
145
|
+
var allowed = { http: true, https: true, mailto: true, tel: true };
|
|
146
|
+
return allowed[scheme[1]] === true ? v : '#';
|
|
147
|
+
}
|
|
148
|
+
|
|
128
149
|
// Function to convert ISO date(s) to DD-MM-YYYY format
|
|
129
150
|
export function formatDateToDDMMYYYY(isoDate) {
|
|
130
151
|
if (!isoDate) return '';
|
package/vite.config.js
CHANGED
|
@@ -40,6 +40,13 @@ function serveFileWithMimeType(filePath, res) {
|
|
|
40
40
|
return true;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
// Checks that the resolved filePath stays inside baseDir (defense against path traversal)
|
|
44
|
+
function isPathInside(filePath, baseDir) {
|
|
45
|
+
if (!filePath) return false;
|
|
46
|
+
const resolved = path.resolve(filePath);
|
|
47
|
+
return resolved === baseDir || resolved.startsWith(baseDir + path.sep);
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
// Helper functions
|
|
44
51
|
function kebabCaseToPascalCase(value) {
|
|
45
52
|
return value
|
|
@@ -705,10 +712,34 @@ function customNunjucksPlugin() {
|
|
|
705
712
|
});
|
|
706
713
|
});
|
|
707
714
|
|
|
715
|
+
// Per-asset base directories used to validate containment of served
|
|
716
|
+
// paths. Each middleware confines requests to its own folder (not cwd),
|
|
717
|
+
// so a `..` in the URL cannot escape into sensitive files.
|
|
718
|
+
const brandingDir = path.resolve(process.cwd(), 'branding');
|
|
719
|
+
const publicImagesDir = path.resolve(process.cwd(), 'public/images');
|
|
720
|
+
const brandingLogosDir = path.resolve(process.cwd(), 'branding/logos');
|
|
721
|
+
const brandingImagesDir = path.resolve(process.cwd(), 'branding/images');
|
|
722
|
+
|
|
708
723
|
// Middleware to serve branding assets in development
|
|
709
724
|
middlewares.use(async (req, res, next) => {
|
|
710
725
|
if (req.url.startsWith('/branding/')) {
|
|
711
|
-
|
|
726
|
+
let decodedUrl;
|
|
727
|
+
try {
|
|
728
|
+
decodedUrl = decodeURIComponent(req.url); // neutralizes %2e%2e
|
|
729
|
+
} catch {
|
|
730
|
+
res.statusCode = 400;
|
|
731
|
+
res.end('Bad Request');
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
// Strip the /branding/ prefix and resolve relative to brandingDir,
|
|
735
|
+
// so any `..` is contained to the branding folder (not cwd).
|
|
736
|
+
const relPath = decodedUrl.slice('/branding/'.length);
|
|
737
|
+
const filePath = path.resolve(brandingDir, '.' + path.sep + relPath);
|
|
738
|
+
if (!isPathInside(filePath, brandingDir)) {
|
|
739
|
+
res.statusCode = 403;
|
|
740
|
+
res.end('Forbidden');
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
712
743
|
if (serveFileWithMimeType(filePath, res)) return;
|
|
713
744
|
}
|
|
714
745
|
next();
|
|
@@ -733,30 +764,37 @@ function customNunjucksPlugin() {
|
|
|
733
764
|
// Middleware to serve branding assets from /images/ in development
|
|
734
765
|
middlewares.use(async (req, res, next) => {
|
|
735
766
|
if (req.url.startsWith('/images/')) {
|
|
736
|
-
let
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const foundInLogos = findFileRecursive(brandingLogosPath, fileName);
|
|
744
|
-
|
|
745
|
-
if (foundInLogos) {
|
|
746
|
-
filePath = foundInLogos;
|
|
747
|
-
} else {
|
|
748
|
-
// Search in branding/images
|
|
749
|
-
const brandingImagesPath = path.join(process.cwd(), 'branding/images');
|
|
750
|
-
const foundInImages = findFileRecursive(brandingImagesPath, fileName);
|
|
751
|
-
if (foundInImages) {
|
|
752
|
-
filePath = foundInImages;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
767
|
+
let decodedUrl;
|
|
768
|
+
try {
|
|
769
|
+
decodedUrl = decodeURIComponent(req.url); // neutralizes %2e%2e
|
|
770
|
+
} catch {
|
|
771
|
+
res.statusCode = 400;
|
|
772
|
+
res.end('Bad Request');
|
|
773
|
+
return;
|
|
755
774
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
775
|
+
// connect does not normalize req.url, so reject any literal `..`
|
|
776
|
+
// before touching the filesystem.
|
|
777
|
+
if (decodedUrl.includes('..')) {
|
|
778
|
+
res.statusCode = 403;
|
|
779
|
+
res.end('Forbidden');
|
|
780
|
+
return;
|
|
759
781
|
}
|
|
782
|
+
|
|
783
|
+
// Primary lookup: strip the /images/ prefix, resolve under
|
|
784
|
+
// public/images and validate containment there.
|
|
785
|
+
const relPath = decodedUrl.slice('/images/'.length);
|
|
786
|
+
let filePath = path.resolve(publicImagesDir, '.' + path.sep + relPath);
|
|
787
|
+
if (isPathInside(filePath, publicImagesDir) && serveFileWithMimeType(filePath, res)) return;
|
|
788
|
+
|
|
789
|
+
// Fallbacks: search branding folders by basename, each candidate
|
|
790
|
+
// validated against its own base before serving.
|
|
791
|
+
const fileName = path.basename(decodedUrl);
|
|
792
|
+
|
|
793
|
+
const foundInLogos = findFileRecursive(brandingLogosDir, fileName);
|
|
794
|
+
if (foundInLogos && isPathInside(foundInLogos, brandingLogosDir) && serveFileWithMimeType(foundInLogos, res)) return;
|
|
795
|
+
|
|
796
|
+
const foundInImages = findFileRecursive(brandingImagesDir, fileName);
|
|
797
|
+
if (foundInImages && isPathInside(foundInImages, brandingImagesDir) && serveFileWithMimeType(foundInImages, res)) return;
|
|
760
798
|
}
|
|
761
799
|
next();
|
|
762
800
|
});
|
|
@@ -765,10 +803,9 @@ function customNunjucksPlugin() {
|
|
|
765
803
|
middlewares.use(async (req, res, next) => {
|
|
766
804
|
if (req.url.startsWith('/branding/logos/')) {
|
|
767
805
|
const fileName = path.basename(req.url);
|
|
768
|
-
const
|
|
769
|
-
const filePath = findFileRecursive(brandingLogosPath, fileName);
|
|
806
|
+
const filePath = findFileRecursive(brandingLogosDir, fileName);
|
|
770
807
|
|
|
771
|
-
if (serveFileWithMimeType(filePath, res)) return;
|
|
808
|
+
if (isPathInside(filePath, brandingLogosDir) && serveFileWithMimeType(filePath, res)) return;
|
|
772
809
|
}
|
|
773
810
|
next();
|
|
774
811
|
});
|
|
@@ -777,10 +814,9 @@ function customNunjucksPlugin() {
|
|
|
777
814
|
middlewares.use(async (req, res, next) => {
|
|
778
815
|
if (req.url.startsWith('/branding/images/')) {
|
|
779
816
|
const fileName = path.basename(req.url);
|
|
780
|
-
const
|
|
781
|
-
const filePath = findFileRecursive(brandingImagesPath, fileName);
|
|
817
|
+
const filePath = findFileRecursive(brandingImagesDir, fileName);
|
|
782
818
|
|
|
783
|
-
if (serveFileWithMimeType(filePath, res)) return;
|
|
819
|
+
if (isPathInside(filePath, brandingImagesDir) && serveFileWithMimeType(filePath, res)) return;
|
|
784
820
|
}
|
|
785
821
|
next();
|
|
786
822
|
});
|
|
@@ -959,7 +995,10 @@ export default defineConfig({
|
|
|
959
995
|
watch: {
|
|
960
996
|
ignored: ['!**/docs/**', '!**/src/templates/**']
|
|
961
997
|
},
|
|
962
|
-
|
|
998
|
+
// 0.0.0.0 exposes the dev-server to the whole network; default to localhost
|
|
999
|
+
// only. Set DEV_HOST=0.0.0.0 in environments like Replit, where allowedHosts
|
|
1000
|
+
// includes the specific *.replit.dev host needed for that public access.
|
|
1001
|
+
host: process.env.DEV_HOST || '127.0.0.1',
|
|
963
1002
|
allowedHosts: [
|
|
964
1003
|
'82c7d68c-1131-4db5-bf36-165d8f8b1d98-00-2b55tnaqky432.worf.replit.dev',
|
|
965
1004
|
'localhost'
|