@pure-ds/core 0.7.16 → 0.7.18

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.
Files changed (32) hide show
  1. package/.cursorrules +22 -11
  2. package/.github/copilot-instructions.md +22 -11
  3. package/custom-elements.json +38 -0
  4. package/dist/types/pds.d.ts +0 -1
  5. package/dist/types/public/assets/js/pds-manager.d.ts +44 -44
  6. package/dist/types/public/assets/js/pds-manager.d.ts.map +1 -1
  7. package/dist/types/public/assets/pds/components/pds-form.d.ts.map +1 -1
  8. package/dist/types/public/assets/pds/components/pds-omnibox.d.ts.map +1 -1
  9. package/dist/types/public/assets/pds/components/pds-treeview.d.ts +9 -0
  10. package/dist/types/public/assets/pds/components/pds-treeview.d.ts.map +1 -1
  11. package/dist/types/src/js/pds-core/pds-live.d.ts.map +1 -1
  12. package/dist/types/src/js/pds-core/pds-runtime.d.ts.map +1 -1
  13. package/package.json +1 -1
  14. package/packages/pds-cli/bin/pds-mcp-health.js +2 -1
  15. package/packages/pds-cli/lib/pds-mcp-core.js +95 -3
  16. package/packages/pds-cli/lib/pds-mcp-eval-cases.json +13 -0
  17. package/public/assets/js/app.js +4 -4
  18. package/public/assets/js/pds-manager.js +144 -162
  19. package/public/assets/js/pds.js +2 -2
  20. package/public/assets/pds/components/pds-calendar.js +19 -11
  21. package/public/assets/pds/components/pds-form.js +85 -2
  22. package/public/assets/pds/components/pds-omnibox.js +9 -6
  23. package/public/assets/pds/components/pds-treeview.js +321 -24
  24. package/public/assets/pds/core/pds-manager.js +144 -162
  25. package/public/assets/pds/core.js +2 -2
  26. package/public/assets/pds/vscode-custom-data.json +4 -0
  27. package/readme.md +12 -59
  28. package/src/js/pds-core/pds-generator.js +7 -7
  29. package/src/js/pds-core/pds-live.js +1 -13
  30. package/src/js/pds-core/pds-ontology.js +2 -2
  31. package/src/js/pds-core/pds-runtime.js +18 -2
  32. package/src/js/pds.d.ts +0 -1
@@ -12,10 +12,12 @@ import { PDS } from "#pds";
12
12
  * @attr {boolean} required - Requires a selected node for form validity.
13
13
  * @attr {boolean} display-only - Renders as browse-only tree without selection.
14
14
  * @attr {boolean} expanded-all - Expands all branch nodes on load.
15
+ * @attr {"off"|"checkboxes"|"auto"} multiselect - Selection mode. "off" keeps single-select behavior.
15
16
  * @attr {string} src - Optional URL source for tree JSON.
16
17
  *
17
18
  * @property {object} settings - Data and behavior settings object.
18
19
  * @property {object} options - Alias for settings.
20
+ * @property {string[]} values - Selected values when multiselect is enabled.
19
21
  * @property {Function} settings.getChildren - Optional async child loader invoked on first expand per node.
20
22
  *
21
23
  * @fires treeview-load Fired after root data has been loaded and indexed.
@@ -33,7 +35,7 @@ export class PdsTreeview extends HTMLElement {
33
35
  static formAssociated = true;
34
36
 
35
37
  static get observedAttributes() {
36
- return ["name", "value", "disabled", "required", "display-only", "expanded-all", "src"];
38
+ return ["name", "value", "disabled", "required", "display-only", "expanded-all", "multiselect", "src"];
37
39
  }
38
40
 
39
41
  #root;
@@ -44,31 +46,58 @@ export class PdsTreeview extends HTMLElement {
44
46
  #nodeById = new Map();
45
47
  #parentById = new Map();
46
48
  #selectedId = null;
49
+ #selectedIds = new Set();
47
50
  #focusedId = null;
48
51
  #defaultValue = "";
49
52
  #loadToken = 0;
50
53
  #childrenLoadPromises = new Map();
54
+ #multiselectAutoQuery =
55
+ typeof window !== "undefined" && typeof window.matchMedia === "function"
56
+ ? window.matchMedia("(hover: none), (pointer: coarse)")
57
+ : null;
58
+ #onMultiselectAutoQueryChange = () => {
59
+ if (this.multiselect === "auto") {
60
+ this.#renderTree();
61
+ }
62
+ };
51
63
 
52
64
  constructor() {
53
65
  super();
54
66
  this.#root = this.attachShadow({ mode: "open" });
55
67
  this.#internals = this.attachInternals();
68
+ if (!this.hasAttribute("tabindex")) {
69
+ this.tabIndex = -1;
70
+ }
56
71
  this.#renderShell();
57
72
  void this.#adoptStyles();
58
73
  }
59
74
 
60
75
  connectedCallback() {
61
76
  this.#defaultValue = this.getAttribute("value") || "";
77
+ if (this.#multiselectAutoQuery?.addEventListener) {
78
+ this.#multiselectAutoQuery.addEventListener("change", this.#onMultiselectAutoQueryChange);
79
+ }
62
80
  this.#syncAttributes();
63
81
  void this.refresh();
64
82
  }
65
83
 
84
+ disconnectedCallback() {
85
+ if (this.#multiselectAutoQuery?.removeEventListener) {
86
+ this.#multiselectAutoQuery.removeEventListener("change", this.#onMultiselectAutoQueryChange);
87
+ }
88
+ }
89
+
66
90
  attributeChangedCallback(name, oldValue, newValue) {
67
91
  if (oldValue === newValue) return;
68
92
  if (name === "src") {
69
93
  void this.refresh();
70
94
  return;
71
95
  }
96
+ if (name === "multiselect") {
97
+ this.#syncAttributes();
98
+ this.#renderTree();
99
+ return;
100
+ }
72
101
  this.#syncAttributes();
73
102
  if (name === "value" && this.#nodes.length) {
74
103
  this.selectByValue(newValue ?? "");
@@ -83,11 +112,26 @@ export class PdsTreeview extends HTMLElement {
83
112
  }
84
113
 
85
114
  formResetCallback() {
86
- this.value = this.#defaultValue;
87
- this.selectByValue(this.#defaultValue);
115
+ if (this.#isMultiSelectEnabled()) {
116
+ this.values = [];
117
+ this.value = "";
118
+ } else {
119
+ this.value = this.#defaultValue;
120
+ this.selectByValue(this.#defaultValue);
121
+ }
88
122
  }
89
123
 
90
124
  formStateRestoreCallback(state) {
125
+ if (this.#isMultiSelectEnabled()) {
126
+ if (Array.isArray(state)) {
127
+ this.values = state;
128
+ return;
129
+ }
130
+ if (state instanceof FormData) {
131
+ this.values = this.name ? state.getAll(this.name) : [];
132
+ return;
133
+ }
134
+ }
91
135
  this.value = state ?? "";
92
136
  this.selectByValue(this.value);
93
137
  }
@@ -128,6 +172,21 @@ export class PdsTreeview extends HTMLElement {
128
172
  else this.setAttribute("value", next);
129
173
  }
130
174
 
175
+ get values() {
176
+ return this.selectedNodes.map((node) => String(node.value));
177
+ }
178
+
179
+ set values(values) {
180
+ if (!Array.isArray(values)) {
181
+ this.#selectedIds.clear();
182
+ this.#selectedId = null;
183
+ this.#syncValue();
184
+ this.#renderTree();
185
+ return;
186
+ }
187
+ this.selectByValues(values);
188
+ }
189
+
131
190
  get disabled() {
132
191
  return this.hasAttribute("disabled");
133
192
  }
@@ -164,14 +223,40 @@ export class PdsTreeview extends HTMLElement {
164
223
  else this.removeAttribute("expanded-all");
165
224
  }
166
225
 
226
+ get multiselect() {
227
+ return this.#normalizeMultiselect(this.getAttribute("multiselect"));
228
+ }
229
+
230
+ set multiselect(value) {
231
+ const next = this.#normalizeMultiselect(value);
232
+ if (next === "off") {
233
+ this.removeAttribute("multiselect");
234
+ } else {
235
+ this.setAttribute("multiselect", next);
236
+ }
237
+ }
238
+
167
239
  get selectedNode() {
168
- return this.#selectedId ? this.#nodeById.get(this.#selectedId) || null : null;
240
+ const selectedId = this.#selectedId && this.#selectedIds.has(this.#selectedId)
241
+ ? this.#selectedId
242
+ : this.#selectedIds.values().next().value || null;
243
+ return selectedId ? this.#nodeById.get(selectedId) || null : null;
244
+ }
245
+
246
+ get selectedNodes() {
247
+ return Array.from(this.#selectedIds)
248
+ .map((id) => this.#nodeById.get(id))
249
+ .filter(Boolean);
169
250
  }
170
251
 
171
252
  getSelectedNode() {
172
253
  return this.selectedNode;
173
254
  }
174
255
 
256
+ getSelectedNodes() {
257
+ return this.selectedNodes;
258
+ }
259
+
175
260
  async refresh() {
176
261
  const loadToken = ++this.#loadToken;
177
262
  const host = this.#root.querySelector(".tv-host");
@@ -199,6 +284,7 @@ export class PdsTreeview extends HTMLElement {
199
284
  this.#parentById.clear();
200
285
  this.#expandedIds.clear();
201
286
  this.#selectedId = null;
287
+ this.#selectedIds.clear();
202
288
  this.#focusedId = null;
203
289
  this.#renderTree();
204
290
  if (host) host.dataset.state = "error";
@@ -226,7 +312,7 @@ export class PdsTreeview extends HTMLElement {
226
312
 
227
313
  selectById(id) {
228
314
  if (!id || !this.#nodeById.has(id)) return false;
229
- this.#selectNode(id, { user: false, focus: true });
315
+ this.#selectNode(id, { user: false, focus: true, mode: "exclusive" });
230
316
  return true;
231
317
  }
232
318
 
@@ -234,6 +320,7 @@ export class PdsTreeview extends HTMLElement {
234
320
  const normalized = value == null ? "" : String(value);
235
321
  if (!normalized) {
236
322
  this.#selectedId = null;
323
+ this.#selectedIds.clear();
237
324
  this.#syncValue();
238
325
  this.#renderTree();
239
326
  return true;
@@ -241,13 +328,53 @@ export class PdsTreeview extends HTMLElement {
241
328
  for (const [id, node] of this.#nodeById.entries()) {
242
329
  if (String(node.value) === normalized) {
243
330
  this.#expandAncestors(id);
244
- this.#selectNode(id, { user: false, focus: false });
331
+ this.#selectNode(id, { user: false, focus: false, mode: "exclusive" });
245
332
  return true;
246
333
  }
247
334
  }
248
335
  return false;
249
336
  }
250
337
 
338
+ selectByValues(values) {
339
+ if (!Array.isArray(values)) return false;
340
+ const normalized = values
341
+ .map((value) => String(value))
342
+ .filter((value) => value.length > 0);
343
+ if (!normalized.length) {
344
+ this.#selectedIds.clear();
345
+ this.#selectedId = null;
346
+ this.#syncValue();
347
+ this.#renderTree();
348
+ return true;
349
+ }
350
+
351
+ const selectedIds = [];
352
+ for (const expected of normalized) {
353
+ for (const [id, node] of this.#nodeById.entries()) {
354
+ if (String(node.value) !== expected) continue;
355
+ if (!selectedIds.includes(id)) {
356
+ selectedIds.push(id);
357
+ this.#expandAncestors(id);
358
+ }
359
+ break;
360
+ }
361
+ }
362
+
363
+ if (!selectedIds.length) {
364
+ return false;
365
+ }
366
+
367
+ if (this.#isMultiSelectEnabled()) {
368
+ this.#selectedIds = new Set(selectedIds);
369
+ this.#selectedId = selectedIds[0] || null;
370
+ this.#renderTree();
371
+ this.#syncValue();
372
+ return true;
373
+ }
374
+
375
+ return this.selectById(selectedIds[0]);
376
+ }
377
+
251
378
  checkValidity() {
252
379
  this.#syncValidity();
253
380
  return this.#internals.checkValidity();
@@ -258,6 +385,16 @@ export class PdsTreeview extends HTMLElement {
258
385
  return this.#internals.reportValidity();
259
386
  }
260
387
 
388
+ focus(options) {
389
+ const targetId =
390
+ this.#focusedId || this.#selectedId || this.#selectedIds.values().next().value || this.#firstVisibleId();
391
+ if (targetId) {
392
+ this.#focusRow(targetId);
393
+ return;
394
+ }
395
+ super.focus(options);
396
+ }
397
+
261
398
  #renderShell() {
262
399
  this.#root.innerHTML = `
263
400
  <div class="tv-host" data-state="ready">
@@ -281,10 +418,19 @@ export class PdsTreeview extends HTMLElement {
281
418
  const id = row.getAttribute("data-node-id");
282
419
  if (!id) return;
283
420
 
421
+ const checkbox = target.closest(".tv-checkbox-input");
422
+ if (checkbox) {
423
+ if (!this.displayOnly) {
424
+ this.#selectNode(id, { user: true, focus: true, mode: "toggle" });
425
+ }
426
+ return;
427
+ }
428
+
284
429
  if (event instanceof MouseEvent && event.detail === 2) {
285
430
  void this.#toggleNode(id, true);
286
431
  if (!this.displayOnly) {
287
- this.#selectNode(id, { user: true, focus: true });
432
+ const mode = this.#isMultiSelectEnabled() ? "exclusive" : "exclusive";
433
+ this.#selectNode(id, { user: true, focus: true, mode });
288
434
  } else {
289
435
  this.#focusRow(id);
290
436
  }
@@ -292,7 +438,16 @@ export class PdsTreeview extends HTMLElement {
292
438
  }
293
439
 
294
440
  if (!this.displayOnly) {
295
- this.#selectNode(id, { user: true, focus: true });
441
+ const mouseEvent = event instanceof MouseEvent ? event : null;
442
+ const useToggleMode =
443
+ this.#isCheckboxSelectionMode() ||
444
+ Boolean(mouseEvent?.ctrlKey || mouseEvent?.metaKey);
445
+ const mode = !this.#isMultiSelectEnabled()
446
+ ? "exclusive"
447
+ : useToggleMode
448
+ ? "toggle"
449
+ : "exclusive";
450
+ this.#selectNode(id, { user: true, focus: true, mode });
296
451
  } else {
297
452
  this.#focusRow(id);
298
453
  }
@@ -317,6 +472,8 @@ export class PdsTreeview extends HTMLElement {
317
472
  event.stopPropagation();
318
473
  }
319
474
  });
475
+
476
+ this.#syncAriaMultiselect();
320
477
  }
321
478
 
322
479
  async #adoptStyles() {
@@ -352,7 +509,7 @@ export class PdsTreeview extends HTMLElement {
352
509
  .tv-row {
353
510
  min-height: var(--tv-row-height);
354
511
  display: grid;
355
- grid-template-columns: var(--tv-toggle-size) auto 1fr;
512
+ grid-template-columns: var(--tv-toggle-size) 1fr;
356
513
  align-items: center;
357
514
  gap: var(--spacing-1);
358
515
  border-radius: var(--radius-sm);
@@ -360,6 +517,18 @@ export class PdsTreeview extends HTMLElement {
360
517
  outline: none;
361
518
  }
362
519
 
520
+ .tv-row.tv-row-has-prefix {
521
+ grid-template-columns: var(--tv-toggle-size) auto 1fr;
522
+ }
523
+
524
+ .tv-row.tv-row-has-checkbox {
525
+ grid-template-columns: var(--tv-toggle-size) auto 1fr;
526
+ }
527
+
528
+ .tv-row.tv-row-has-prefix.tv-row-has-checkbox {
529
+ grid-template-columns: var(--tv-toggle-size) auto auto 1fr;
530
+ }
531
+
363
532
  .tv-row[aria-selected="true"] {
364
533
  background: var(--color-surface-hover);
365
534
  color: var(--color-text-primary);
@@ -397,6 +566,18 @@ export class PdsTreeview extends HTMLElement {
397
566
  color: var(--color-text-primary);
398
567
  }
399
568
 
569
+ .tv-check {
570
+ display: inline-flex;
571
+ align-items: center;
572
+ justify-content: center;
573
+ width: var(--tv-toggle-size);
574
+ height: var(--tv-toggle-size);
575
+ }
576
+
577
+ .tv-checkbox-input {
578
+ margin: var(--spacing-0);
579
+ }
580
+
400
581
  :host([disabled]) .tv-row,
401
582
  :host([disabled]) .tv-toggle,
402
583
  .tv-host[data-state="loading"] .tv-row {
@@ -440,10 +621,34 @@ export class PdsTreeview extends HTMLElement {
440
621
  }
441
622
  `);
442
623
 
624
+ const existingSheets = Array.isArray(this.#root.adoptedStyleSheets)
625
+ ? this.#root.adoptedStyleSheets
626
+ : [];
627
+ this.#root.adoptedStyleSheets = [
628
+ ...existingSheets.filter((sheet) => sheet !== componentStyles),
629
+ componentStyles,
630
+ ];
631
+
443
632
  await PDS.adoptLayers(this.#root, LAYERS, [componentStyles]);
444
633
  }
445
634
 
446
635
  #syncAttributes() {
636
+ if (!this.#isMultiSelectEnabled() && this.#selectedIds.size > 1) {
637
+ const keepId =
638
+ (this.#selectedId && this.#selectedIds.has(this.#selectedId) && this.#selectedId) ||
639
+ this.#selectedIds.values().next().value ||
640
+ null;
641
+ this.#selectedIds = keepId ? new Set([keepId]) : new Set();
642
+ this.#selectedId = keepId;
643
+ }
644
+ if (this.#selectedId && !this.#selectedIds.has(this.#selectedId)) {
645
+ this.#selectedIds.add(this.#selectedId);
646
+ }
647
+ if (!this.#selectedId && this.#selectedIds.size > 0) {
648
+ this.#selectedId = this.#selectedIds.values().next().value || null;
649
+ }
650
+
651
+ this.#syncAriaMultiselect();
447
652
  this.#syncValue();
448
653
  this.#syncTabStops();
449
654
  }
@@ -578,9 +783,10 @@ export class PdsTreeview extends HTMLElement {
578
783
  const tree = this.#root.querySelector(ROOT_SELECTOR);
579
784
  if (!tree) return;
580
785
  tree.innerHTML = this.#renderNodes(this.#nodes, 1, this.#canRenderLinks());
786
+ this.#syncAriaMultiselect();
581
787
 
582
788
  if (!this.#focusedId) {
583
- this.#focusedId = this.#selectedId || this.#firstVisibleId() || null;
789
+ this.#focusedId = this.#selectedId || this.#selectedIds.values().next().value || this.#firstVisibleId() || null;
584
790
  }
585
791
  this.#syncTabStops();
586
792
  this.#syncValue();
@@ -588,17 +794,25 @@ export class PdsTreeview extends HTMLElement {
588
794
 
589
795
  #renderNodes(nodes, level, linksEnabled) {
590
796
  if (!nodes.length) return "";
797
+ const useCheckboxes = this.#isCheckboxSelectionMode();
591
798
 
592
799
  return nodes
593
800
  .map((node) => {
594
801
  const expanded = this.#expandedIds.has(node.id);
595
802
  const hasChildren = Boolean(node.hasChildren);
596
- const selected = this.#selectedId === node.id;
803
+ const hasPrefix = Boolean(node.image || node.icon);
804
+ const selected = this.#selectedIds.has(node.id);
597
805
  const toggleGlyph = node.loadingChildren ? "…" : expanded ? "−" : "+";
598
806
  const toggle = hasChildren
599
807
  ? `<button type="button" class="tv-toggle icon-only" data-node-id="${this.#escapeAttribute(node.id)}" aria-label="${expanded ? "Collapse" : "Expand"} ${this.#escapeAttribute(node.text)}" ${node.loadingChildren ? "disabled" : ""}>${toggleGlyph}</button>`
600
808
  : `<span class="tv-toggle-gap" aria-hidden="true"></span>`;
809
+ const checkbox = useCheckboxes
810
+ ? `<span class="tv-check"><input class="tv-checkbox-input" type="checkbox" data-node-id="${this.#escapeAttribute(node.id)}" aria-label="Select ${this.#escapeAttribute(node.text)}" ${selected ? "checked" : ""} ${this.disabled ? "disabled" : ""}></span>`
811
+ : "";
601
812
  const prefix = this.#renderPrefix(node);
813
+ const rowClassParts = ["tv-row", hasPrefix ? "tv-row-has-prefix" : "tv-row-no-prefix"];
814
+ if (useCheckboxes) rowClassParts.push("tv-row-has-checkbox");
815
+ const rowClass = rowClassParts.join(" ");
602
816
  const label = linksEnabled && node.link
603
817
  ? `<a class="tv-label tv-label-link" href="${this.#escapeAttribute(node.link)}">${this.#escapeHtml(node.text)}</a>`
604
818
  : `<span class="tv-label">${this.#escapeHtml(node.text)}</span>`;
@@ -610,7 +824,7 @@ export class PdsTreeview extends HTMLElement {
610
824
  return `
611
825
  <li class="tv-item" role="none">
612
826
  <div
613
- class="tv-row"
827
+ class="${rowClass}"
614
828
  role="treeitem"
615
829
  aria-level="${level}"
616
830
  ${hasChildren ? `aria-expanded="${expanded ? "true" : "false"}"` : ""}
@@ -620,6 +834,7 @@ export class PdsTreeview extends HTMLElement {
620
834
  tabindex="-1"
621
835
  >
622
836
  ${toggle}
837
+ ${checkbox}
623
838
  ${prefix}
624
839
  ${label}
625
840
  </div>
@@ -641,7 +856,7 @@ export class PdsTreeview extends HTMLElement {
641
856
  if (node.icon) {
642
857
  return `<span class="tv-prefix"><pds-icon icon="${this.#escapeAttribute(node.icon)}"></pds-icon></span>`;
643
858
  }
644
- return `<span class="tv-prefix" aria-hidden="true"></span>`;
859
+ return "";
645
860
  }
646
861
 
647
862
  #handleKeydown(event) {
@@ -706,7 +921,15 @@ export class PdsTreeview extends HTMLElement {
706
921
 
707
922
  if (key === "Enter" || key === " ") {
708
923
  if (!this.displayOnly) {
709
- this.#selectNode(activeId, { user: true, focus: true });
924
+ if (!this.#isMultiSelectEnabled()) {
925
+ this.#selectNode(activeId, { user: true, focus: true, mode: "exclusive" });
926
+ } else if (this.#isCheckboxSelectionMode()) {
927
+ this.#selectNode(activeId, { user: true, focus: true, mode: "toggle" });
928
+ } else if (key === " " && (event.ctrlKey || event.metaKey)) {
929
+ this.#selectNode(activeId, { user: true, focus: true, mode: "toggle" });
930
+ } else {
931
+ this.#selectNode(activeId, { user: true, focus: true, mode: "exclusive" });
932
+ }
710
933
  } else if (currentNode.hasChildren) {
711
934
  void this.#toggleNode(activeId, true);
712
935
  }
@@ -836,19 +1059,44 @@ export class PdsTreeview extends HTMLElement {
836
1059
  return source;
837
1060
  }
838
1061
 
839
- #selectNode(id, { user, focus }) {
1062
+ #selectNode(id, { user, focus, mode = "exclusive" }) {
840
1063
  const node = this.#nodeById.get(id);
841
1064
  if (!node) return;
842
1065
 
843
- this.#selectedId = id;
1066
+ if (!this.#isMultiSelectEnabled()) {
1067
+ this.#selectedId = id;
1068
+ this.#selectedIds = new Set([id]);
1069
+ } else if (mode === "toggle") {
1070
+ if (this.#selectedIds.has(id)) {
1071
+ this.#selectedIds.delete(id);
1072
+ if (this.#selectedId === id) {
1073
+ this.#selectedId = this.#selectedIds.values().next().value || null;
1074
+ }
1075
+ } else {
1076
+ this.#selectedIds.add(id);
1077
+ this.#selectedId = id;
1078
+ }
1079
+ } else {
1080
+ this.#selectedId = id;
1081
+ this.#selectedIds = new Set([id]);
1082
+ }
1083
+
844
1084
  this.#expandAncestors(id);
845
1085
  this.#renderTree();
846
1086
  if (focus) this.#focusRow(id);
847
1087
  this.#syncValue();
1088
+ const selectedNodes = this.selectedNodes;
1089
+ const selectedValues = selectedNodes.map((selectedNode) => String(selectedNode.value));
848
1090
 
849
1091
  this.dispatchEvent(
850
1092
  new CustomEvent("node-select", {
851
- detail: { node, value: node.value, user: Boolean(user) },
1093
+ detail: {
1094
+ node,
1095
+ value: node.value,
1096
+ values: selectedValues,
1097
+ selectedNodes,
1098
+ user: Boolean(user),
1099
+ },
852
1100
  bubbles: true,
853
1101
  composed: true,
854
1102
  }),
@@ -866,12 +1114,19 @@ export class PdsTreeview extends HTMLElement {
866
1114
  if (preferred) {
867
1115
  if (!this.selectByValue(preferred)) {
868
1116
  this.#selectedId = null;
1117
+ this.#selectedIds.clear();
869
1118
  }
870
1119
  } else if (this.#selectedId && this.#nodeById.has(this.#selectedId)) {
1120
+ if (!this.#selectedIds.size) this.#selectedIds.add(this.#selectedId);
871
1121
  this.#expandAncestors(this.#selectedId);
872
1122
  this.#renderTree();
1123
+ } else if (this.#selectedIds.size) {
1124
+ this.#selectedIds = new Set(Array.from(this.#selectedIds).filter((id) => this.#nodeById.has(id)));
1125
+ this.#selectedId = this.#selectedIds.values().next().value || null;
1126
+ this.#renderTree();
873
1127
  } else {
874
1128
  this.#selectedId = null;
1129
+ this.#selectedIds.clear();
875
1130
  this.#syncValue();
876
1131
  }
877
1132
  }
@@ -885,28 +1140,48 @@ export class PdsTreeview extends HTMLElement {
885
1140
  }
886
1141
 
887
1142
  #syncValue() {
888
- const selected = this.selectedNode;
889
- const nextValue = this.displayOnly ? "" : selected?.value ? String(selected.value) : "";
1143
+ if (this.displayOnly) {
1144
+ if (this.hasAttribute("value")) this.removeAttribute("value");
1145
+ this.#internals.setFormValue("");
1146
+ this.#syncValidity();
1147
+ return;
1148
+ }
1149
+
1150
+ const selectedNodes = this.selectedNodes;
1151
+ const selectedValues = selectedNodes.map((node) => String(node.value));
1152
+ const nextValue = selectedValues[0] || "";
890
1153
 
891
1154
  if (nextValue) {
892
1155
  if (this.getAttribute("value") !== nextValue) {
893
1156
  this.setAttribute("value", nextValue);
894
1157
  }
895
- this.#internals.setFormValue(nextValue);
896
1158
  } else {
897
1159
  if (this.hasAttribute("value")) this.removeAttribute("value");
898
- this.#internals.setFormValue("");
1160
+ }
1161
+
1162
+ if (this.#isMultiSelectEnabled() && selectedValues.length > 0 && this.name) {
1163
+ const formValue = new FormData();
1164
+ for (const value of selectedValues) {
1165
+ formValue.append(this.name, value);
1166
+ }
1167
+ this.#internals.setFormValue(formValue);
1168
+ } else {
1169
+ this.#internals.setFormValue(nextValue || "");
899
1170
  }
900
1171
 
901
1172
  this.#syncValidity();
902
1173
  }
903
1174
 
904
1175
  #syncValidity() {
905
- if (this.required && !this.displayOnly && !this.selectedNode) {
1176
+ if (this.required && !this.displayOnly && this.#selectedIds.size === 0) {
1177
+ const focusTarget =
1178
+ this.#root.querySelector('.tv-row[tabindex="0"]') ||
1179
+ this.#root.querySelector(".tv-row") ||
1180
+ this;
906
1181
  this.#internals.setValidity(
907
1182
  { valueMissing: true },
908
1183
  "Please select a node.",
909
- this,
1184
+ focusTarget,
910
1185
  );
911
1186
  return;
912
1187
  }
@@ -925,7 +1200,7 @@ export class PdsTreeview extends HTMLElement {
925
1200
  const rows = this.#root.querySelectorAll(".tv-row");
926
1201
  if (!rows.length) return;
927
1202
 
928
- const fallbackId = this.#selectedId || this.#firstVisibleId();
1203
+ const fallbackId = this.#selectedId || this.#selectedIds.values().next().value || this.#firstVisibleId();
929
1204
  if (!this.#focusedId || !this.#root.querySelector(`.tv-row[data-node-id="${CSS.escape(this.#focusedId)}"]`)) {
930
1205
  this.#focusedId = fallbackId || null;
931
1206
  }
@@ -967,6 +1242,28 @@ export class PdsTreeview extends HTMLElement {
967
1242
  #escapeAttribute(value) {
968
1243
  return this.#escapeHtml(value);
969
1244
  }
1245
+
1246
+ #normalizeMultiselect(value) {
1247
+ const normalized = String(value || "off").toLowerCase();
1248
+ if (normalized === "checkboxes" || normalized === "auto") return normalized;
1249
+ return "off";
1250
+ }
1251
+
1252
+ #isMultiSelectEnabled() {
1253
+ return this.multiselect !== "off";
1254
+ }
1255
+
1256
+ #isCheckboxSelectionMode() {
1257
+ if (this.multiselect === "checkboxes") return true;
1258
+ if (this.multiselect !== "auto") return false;
1259
+ return Boolean(this.#multiselectAutoQuery?.matches);
1260
+ }
1261
+
1262
+ #syncAriaMultiselect() {
1263
+ const tree = this.#root.querySelector(ROOT_SELECTOR);
1264
+ if (!tree) return;
1265
+ tree.setAttribute("aria-multiselectable", this.#isMultiSelectEnabled() ? "true" : "false");
1266
+ }
970
1267
  }
971
1268
 
972
1269
  if (!customElements.get("pds-treeview")) {