desy-html 17.0.0 → 17.0.2

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.
@@ -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
- if(target.textContent.includes('Mostrar todo') || show === true) {
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
- const activeItem = document.querySelector(`#${menuId} #${activeItemId}`);
210
- if (activeItem) {
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, activeItem]
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 accordion with this id in the document.');
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.querySelector(`#${menu}`);
252
+ const getAccordion = document.getElementById(menu);
236
253
  const allowMultiple = getAccordion.hasAttribute('data-allow-multiple');
237
- const allowToggle = (allowMultiple) ? allowMultiple : accordion.hasAttribute('data-allow-toggle');
238
- const selectAllTriggers = document.querySelectorAll(`#${menu} .c-accordion__trigger`);
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 typeof activeItem === "object" && allowToggle
275
+ return Array.isArray(activeItem)
259
276
  ? activeItem.includes(id)
260
277
  : id === activeItem;
261
278
  }
@@ -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
- document.getElementById(alertId).innerHTML = document.getElementById('template-' + alertId).innerHTML;
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) {
@@ -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
- if (inputNode._comboboxInstance.multiselectable) {
573
- inputNode._comboboxInstance.toggleOption(option);
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
- inputNode._comboboxInstance.selectOption(option);
588
+ instance.selectOption(option);
576
589
  }
577
590
  return [comboboxNode, option];
578
591
  }
@@ -524,33 +524,34 @@ export function dataGrid(aria) {
524
524
  sortType = aria.SortType.ASCENDING;
525
525
  }
526
526
 
527
- var comparator = function (row1, row2) {
528
- if(row1.children[columnIndex].classList.contains('align-top') || row2.children[columnIndex].classList.contains('align-top')) {
529
- return
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
- if (sortType === aria.SortType.DESCENDING && orderValue === 'numbers') {
543
- return row2Value - row1Value;
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
- if (sortType === aria.SortType.ASCENDING && orderValue === 'strings') {
547
- return row1Value.toString().localeCompare(row2Value);
548
- }
544
+ var row1Value = parseNumeric(row1Text);
545
+ var row2Value = parseNumeric(row2Text);
549
546
 
550
- if (sortType === aria.SortType.DESCENDING && orderValue === 'strings') {
551
- return row2Value.toString().localeCompare(row1Value);
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
- var dataRows = Array.prototype.slice.call(rows, 1);
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
 
@@ -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.innerHTML = `<strong class="font-bold">${elementActive.textContent}</strong>`;
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
- const replaceStrong = getText.textContent.replace('<strong class="font-bold">', '').replace('<strong/>', '');
40
- getText.textContent = `${replaceStrong}`;
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
  }
@@ -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.innerHTML = element.textContent;
568
+ getListBoxButtonText.textContent = element.textContent;
569
569
  }
570
570
  } else {
571
571
  element.setAttribute('aria-selected', 'false');
@@ -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 panel = elem.getElementById(target.getAttribute('aria-controls'));
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
- element.innerHTML = `<strong class="font-bold">${element.innerHTML}</strong>` ;
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
- if(element.querySelector('strong')) {
293
- const replaceStrong = element.innerHTML.replace('<strong class="font-bold">', '').replace('<strong/>', '');
294
- element.innerHTML = `${replaceStrong}`;
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
 
@@ -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.describedBy)
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 ? ' ' + item.classes : '') + '"').replace('""', '') +
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>' + // cierre flex items-center
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 += item.html ? item.html : (item.text || '');
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.html || item.hint.text;
183
+ html += htmlOrText(item.hint);
156
184
  html += '</div>';
157
185
  }
158
- html += '</div>'; // cierre flex-1
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 ? ' ' + 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.html || item.text) +
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 ? ' ' + 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.html || item.text) +
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 ? ' ' + itemNode.classes : '') + '"';
213
- for (var a in itemNode.attributes || {}) {
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 ? ' ' + itemNode.sub.classes : '') + '"';
242
- for (var b in itemNode.sub.attributes || {}) {
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 ? ' ' + 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) {
@@ -149,7 +149,7 @@ export function Treeitem(aria) {
149
149
  return;
150
150
  }
151
151
 
152
- if (event.shift) {
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
- const typeOfInput = getCurrentCheckbox.type
162
- if (typeOfInput === 'radio') {
163
- for (let item of getAllChildrenOfTree) {
164
- item.checked = false
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.setAttribute("aria-current", "page");
323
- getLink.innerHTML = `<strong class="font-bold">${getLink.textContent}</strong>`;
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 {
@@ -125,6 +125,27 @@ export function utils(aria) {
125
125
  };
126
126
  }
127
127
 
128
+ var HTML_ESCAPES = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' };
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 '';