@pine-ds/core 4.6.0 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/components/pds-combobox.js +1 -1
  2. package/components/pds-drawer.js +1 -1
  3. package/components/pds-dropdown-menu.js +1 -1
  4. package/components/pds-modal2.js +1 -1
  5. package/components/pds-multiselect.js +1 -1
  6. package/components/pds-popover.js +1 -1
  7. package/dist/cjs/loader.cjs.js +1 -1
  8. package/dist/cjs/pds-combobox.cjs.entry.js +33 -3
  9. package/dist/cjs/pds-drawer.cjs.entry.js +23 -10
  10. package/dist/cjs/pds-dropdown-menu.cjs.entry.js +9 -5
  11. package/dist/cjs/pds-modal.cjs.entry.js +116 -31
  12. package/dist/cjs/pds-multiselect.cjs.entry.js +6 -5
  13. package/dist/cjs/pds-popover.cjs.entry.js +170 -26
  14. package/dist/cjs/pine-core.cjs.js +1 -1
  15. package/dist/collection/components/pds-combobox/pds-combobox.js +33 -3
  16. package/dist/collection/components/pds-drawer/pds-drawer.css +4 -1
  17. package/dist/collection/components/pds-drawer/pds-drawer.js +54 -17
  18. package/dist/collection/components/pds-drawer/stories/pds-drawer.stories.js +6 -1
  19. package/dist/collection/components/pds-dropdown-menu/pds-dropdown-menu.js +15 -5
  20. package/dist/collection/components/pds-modal/pds-modal.js +117 -32
  21. package/dist/collection/components/pds-multiselect/pds-multiselect.js +8 -7
  22. package/dist/collection/components/pds-popover/pds-popover.js +171 -27
  23. package/dist/docs.json +42 -7
  24. package/dist/esm/loader.js +1 -1
  25. package/dist/esm/pds-combobox.entry.js +33 -3
  26. package/dist/esm/pds-drawer.entry.js +23 -10
  27. package/dist/esm/pds-dropdown-menu.entry.js +9 -5
  28. package/dist/esm/pds-modal.entry.js +116 -31
  29. package/dist/esm/pds-multiselect.entry.js +6 -5
  30. package/dist/esm/pds-popover.entry.js +171 -27
  31. package/dist/esm/pine-core.js +1 -1
  32. package/dist/esm-es5/loader.js +1 -1
  33. package/dist/esm-es5/pds-combobox.entry.js +1 -1
  34. package/dist/esm-es5/pds-drawer.entry.js +1 -1
  35. package/dist/esm-es5/pds-dropdown-menu.entry.js +1 -1
  36. package/dist/esm-es5/pds-modal.entry.js +1 -1
  37. package/dist/esm-es5/pds-multiselect.entry.js +1 -1
  38. package/dist/esm-es5/pds-popover.entry.js +1 -1
  39. package/dist/esm-es5/pine-core.js +1 -1
  40. package/dist/pine-core/p-153ba77b.entry.js +1 -0
  41. package/dist/pine-core/p-21fe6ee1.system.entry.js +1 -0
  42. package/dist/pine-core/p-24fbbb74.system.entry.js +1 -0
  43. package/dist/pine-core/{p-df539c42.system.entry.js → p-5a10784e.system.entry.js} +1 -1
  44. package/dist/pine-core/p-74a05bcc.entry.js +1 -0
  45. package/dist/pine-core/p-92ae6082.entry.js +1 -0
  46. package/dist/pine-core/p-945fb0b1.entry.js +1 -0
  47. package/dist/pine-core/p-CuSNnuFu.system.js +1 -1
  48. package/dist/pine-core/p-a0344e30.entry.js +2 -0
  49. package/dist/pine-core/p-a787b34a.system.entry.js +1 -0
  50. package/dist/pine-core/{p-646afbca.system.entry.js → p-b60ac7eb.system.entry.js} +1 -1
  51. package/dist/pine-core/p-bf5887e8.system.entry.js +1 -0
  52. package/dist/pine-core/p-e21ff692.entry.js +1 -0
  53. package/dist/pine-core/pine-core.esm.js +1 -1
  54. package/dist/types/components/pds-combobox/pds-combobox.d.ts +1 -0
  55. package/dist/types/components/pds-drawer/pds-drawer.d.ts +21 -9
  56. package/dist/types/components/pds-dropdown-menu/pds-dropdown-menu.d.ts +1 -0
  57. package/dist/types/components/pds-modal/pds-modal.d.ts +10 -3
  58. package/dist/types/components/pds-multiselect/pds-multiselect.d.ts +2 -1
  59. package/dist/types/components/pds-popover/pds-popover.d.ts +31 -1
  60. package/dist/types/components.d.ts +45 -30
  61. package/dist/vscode.html-data.json +11 -4
  62. package/hydrate/index.js +369 -90
  63. package/hydrate/index.mjs +369 -90
  64. package/package.json +2 -2
  65. package/dist/pine-core/p-01c9b8f3.entry.js +0 -1
  66. package/dist/pine-core/p-3c27088f.system.entry.js +0 -1
  67. package/dist/pine-core/p-73eb47fa.entry.js +0 -1
  68. package/dist/pine-core/p-7f367db4.system.entry.js +0 -1
  69. package/dist/pine-core/p-9a0083dd.system.entry.js +0 -1
  70. package/dist/pine-core/p-a3735199.entry.js +0 -1
  71. package/dist/pine-core/p-da103a54.system.entry.js +0 -1
  72. package/dist/pine-core/p-ec5428fb.entry.js +0 -1
  73. package/dist/pine-core/p-ed4b979e.entry.js +0 -1
  74. package/dist/pine-core/p-f1f17940.entry.js +0 -2
@@ -86,6 +86,34 @@ function containsComposed(ancestor, node) {
86
86
  }
87
87
  return false;
88
88
  }
89
+ /**
90
+ * Every open modal, with the order it opened in, which ranks top-layer modals and the modals
91
+ * inside them. Kept here rather than queried from the document, which can't see a modal inside
92
+ * another component's shadow root.
93
+ */
94
+ const openModals = new Map();
95
+ let openSequence = 0;
96
+ /** Whether an open auto popover's panel sits in `root`, including inside shadow roots under it. */
97
+ function hasOpenPopover(root) {
98
+ if (root.querySelector('[data-pds-popover-open]'))
99
+ return true;
100
+ return Array.from(root.querySelectorAll('*')).some(el => !!el.shadowRoot && hasOpenPopover(el.shadowRoot));
101
+ }
102
+ /**
103
+ * Whether `a` comes before `b` in the page. `compareDocumentPosition` is arbitrary across a shadow
104
+ * boundary, so each side climbs to its host until both are in the same tree.
105
+ */
106
+ function precedesComposed(a, b) {
107
+ // A document has no host, which ends each climb.
108
+ for (let x = a; x; x = x.getRootNode().host) {
109
+ for (let y = b; y; y = y.getRootNode().host) {
110
+ if (x.getRootNode() === y.getRootNode()) {
111
+ return !!(x.compareDocumentPosition(y) & Node.DOCUMENT_POSITION_FOLLOWING);
112
+ }
113
+ }
114
+ }
115
+ return false;
116
+ }
89
117
  /** The focused element, followed down through shadow roots to the control that has focus. */
90
118
  function deepActiveElement() {
91
119
  var _a;
@@ -101,7 +129,8 @@ const PdsModal = class {
101
129
  this.pdsModalClose = index.createEvent(this, "pdsModalClose");
102
130
  this.focusableElements = [];
103
131
  /**
104
- * Whether the modal can be dismissed by clicking the backdrop
132
+ * Whether the modal can be dismissed by clicking the backdrop, pressing Escape, or the browser's
133
+ * own close request (such as a back gesture)
105
134
  * @default true
106
135
  */
107
136
  this.backdropDismiss = true;
@@ -149,19 +178,33 @@ const PdsModal = class {
149
178
  * Stores the list of focusable elements in the modal
150
179
  */
151
180
  this.focusableElementsArray = [];
181
+ // The browser's own close request (an Escape nothing prevented, a back gesture) would close the
182
+ // dialog without updating `open`, so close it through hideModal() instead, or not at all.
183
+ this.handleCancel = (e) => {
184
+ e.preventDefault();
185
+ if (!this.open)
186
+ return;
187
+ // The request reaches the top-layer dialog even when a disableTopLayer modal inside it is on
188
+ // top; that one closes first, as it would on Escape.
189
+ const top = this.innermostOpenModal();
190
+ const target = (top && containsComposed(this.el, top) ? top : this.el);
191
+ if (target.backdropDismiss)
192
+ target.hideModal();
193
+ };
152
194
  this.handleBackdropClick = (e) => {
153
195
  if (!this.backdropDismiss || !this.open)
154
196
  return;
155
- if (e.target.classList.contains('pds-modal__backdrop')) {
197
+ // This modal's own backdrop; a nested modal's backdrop handles its own clicks.
198
+ if (e.target === this.modalRef) {
156
199
  e.stopPropagation();
157
200
  // Only close if this is the innermost modal
158
- if (this.isInnermostModal()) {
201
+ if (this.innermostOpenModal() === this.el) {
159
202
  this.hideModal();
160
203
  }
161
204
  }
162
205
  };
163
206
  this.handleKeyDown = (e) => {
164
- var _a, _b, _c;
207
+ var _a, _b, _c, _d;
165
208
  // If the modal is not open, don't handle any keyboard events
166
209
  if (!this.open)
167
210
  return;
@@ -184,24 +227,39 @@ const PdsModal = class {
184
227
  if (!('__pdsActiveElementAtEscape' in eventSnapshot)) {
185
228
  const activeAtDispatch = document.activeElement;
186
229
  eventSnapshot.__pdsActiveElementAtEscape = activeAtDispatch;
230
+ // Followed into shadow roots: for a modal inside one, document.activeElement is only the host.
231
+ eventSnapshot.__pdsDeepActiveElementAtEscape = deepActiveElement();
187
232
  const topLayerDialog = (_a = activeAtDispatch === null || activeAtDispatch === void 0 ? void 0 : activeAtDispatch.closest('dialog')) !== null && _a !== void 0 ? _a : null;
188
233
  eventSnapshot.__pdsTopLayerDialogAtEscape = topLayerDialog;
189
234
  eventSnapshot.__pdsTopLayerDialogWasModalAtEscape = !!(topLayerDialog && topLayerDialog.matches(':modal'));
235
+ // A control that closed on this Escape (a combobox on its own element, a dropdown menu or
236
+ // multiselect in document's capture phase) has prevented it before this bubble listener.
237
+ // Read before any modal prevents it below.
238
+ eventSnapshot.__pdsEscapeHandledInside = e.defaultPrevented;
239
+ // Which modal is innermost is read once too: the inner one closing first would make its
240
+ // outer modal the innermost by the time that modal's listener runs.
241
+ eventSnapshot.__pdsInnermostModalAtEscape = this.innermostOpenModal();
190
242
  }
243
+ if (eventSnapshot.__pdsEscapeHandledInside)
244
+ return;
191
245
  // Leave Escape to a genuinely stacked-above overlay (disableTopLayer's
192
246
  // reason for existing) — not just to anything outside this modal.
193
247
  const active = eventSnapshot.__pdsActiveElementAtEscape;
194
248
  if (this.disableTopLayer &&
195
249
  active &&
196
250
  active !== document.body &&
197
- !this.el.contains(active) &&
198
- this.isStackedAboveOverlay(active, (_b = eventSnapshot.__pdsTopLayerDialogAtEscape) !== null && _b !== void 0 ? _b : null, (_c = eventSnapshot.__pdsTopLayerDialogWasModalAtEscape) !== null && _c !== void 0 ? _c : false)) {
251
+ !containsComposed(this.el, (_b = eventSnapshot.__pdsDeepActiveElementAtEscape) !== null && _b !== void 0 ? _b : active) &&
252
+ this.isStackedAboveOverlay(active, (_c = eventSnapshot.__pdsTopLayerDialogAtEscape) !== null && _c !== void 0 ? _c : null, (_d = eventSnapshot.__pdsTopLayerDialogWasModalAtEscape) !== null && _d !== void 0 ? _d : false)) {
199
253
  return;
200
254
  }
255
+ // An open popover inside takes Escape from its own document listener, which runs after this
256
+ // one, so leave the key for it. Its preventDefault() keeps the native dialog open.
257
+ if (this.modalRef && hasOpenPopover(this.modalRef))
258
+ return;
201
259
  // Always prevent native dialog close behavior
202
260
  e.preventDefault();
203
261
  // Only close if backdropDismiss is enabled and this is the innermost modal
204
- if (this.backdropDismiss && this.isInnermostModal()) {
262
+ if (this.backdropDismiss && eventSnapshot.__pdsInnermostModalAtEscape === this.el) {
205
263
  this.hideModal();
206
264
  }
207
265
  return;
@@ -261,8 +319,6 @@ const PdsModal = class {
261
319
  }
262
320
  componentDidLoad() {
263
321
  this.modalRef = this.el.querySelector('.pds-modal__backdrop');
264
- // Add keyboard event listener
265
- document.addEventListener('keydown', this.handleKeyDown);
266
322
  // @Watch('open') only fires on a later change, not the prop's initial
267
323
  // value — a modal mounted with `open` already true (static markup, or a
268
324
  // framework passing it on first render) would otherwise never call
@@ -278,9 +334,18 @@ const PdsModal = class {
278
334
  componentDidRender() {
279
335
  reconnectedContent.unwrapReconnectedContent(this.el.querySelector('.pds-modal'), 'dialog.pds-modal__backdrop', '.pds-modal');
280
336
  }
337
+ // Here rather than in componentDidLoad, which runs once: a modal taken out of the page and put
338
+ // back (a morph or a framework moving it) gets its keyboard handling, and its place among the
339
+ // open modals, back too. Adding the same listener twice is a no-op.
340
+ connectedCallback() {
341
+ document.addEventListener('keydown', this.handleKeyDown);
342
+ if (this.open && !openModals.has(this.el))
343
+ openModals.set(this.el, ++openSequence);
344
+ }
281
345
  disconnectedCallback() {
282
346
  // Clean up event listener
283
347
  document.removeEventListener('keydown', this.handleKeyDown);
348
+ openModals.delete(this.el);
284
349
  }
285
350
  handleOpenChange(newValue) {
286
351
  if (newValue) {
@@ -345,6 +410,9 @@ const PdsModal = class {
345
410
  this.modalRef.showModal();
346
411
  }
347
412
  this.open = true;
413
+ // Showing an open modal again doesn't move it above one opened after it.
414
+ if (!openModals.has(this.el))
415
+ openModals.set(this.el, ++openSequence);
348
416
  // Update focusable elements and set initial focus
349
417
  // Using a longer timeout to ensure all components are fully rendered
350
418
  setTimeout(() => {
@@ -407,6 +475,7 @@ const PdsModal = class {
407
475
  const focusAlreadyMovedElsewhere = activeElement !== null && activeElement !== document.body && !this.modalRef.contains(activeElement);
408
476
  this.modalRef.close();
409
477
  this.open = false;
478
+ openModals.delete(this.el);
410
479
  // Restore focus to the element that was focused before the modal was opened
411
480
  if (!focusAlreadyMovedElsewhere &&
412
481
  this.previousActiveElement &&
@@ -425,7 +494,9 @@ const PdsModal = class {
425
494
  */
426
495
  getBackdropZIndex(modal) {
427
496
  const backdrop = modal.querySelector('.pds-modal__backdrop');
428
- return backdrop ? parseInt(getComputedStyle(backdrop).zIndex, 10) : -1;
497
+ // `auto` (no z-index token loaded) parses to NaN, which no comparison can rank.
498
+ const zIndex = backdrop ? parseInt(getComputedStyle(backdrop).zIndex, 10) : -1;
499
+ return Number.isNaN(zIndex) ? 0 : zIndex;
429
500
  }
430
501
  /**
431
502
  * Whether `active` sits inside a surface actually stacked above this modal.
@@ -452,7 +523,8 @@ const PdsModal = class {
452
523
  * lose that evidence depending on listener order.
453
524
  */
454
525
  isStackedAboveOverlay(active, topLayerDialog, topLayerDialogWasModal) {
455
- if (topLayerDialog && topLayerDialog !== this.modalRef && topLayerDialogWasModal) {
526
+ // A top-layer dialog this modal sits inside is underneath it, not an overlay above.
527
+ if (topLayerDialog && topLayerDialog !== this.modalRef && topLayerDialogWasModal && !containsComposed(topLayerDialog, this.el)) {
456
528
  return true;
457
529
  }
458
530
  const ownZIndex = this.getBackdropZIndex(this.el);
@@ -468,24 +540,37 @@ const PdsModal = class {
468
540
  return false;
469
541
  }
470
542
  /**
471
- * Checks if this modal is the innermost (highest z-index) modal
543
+ * The open modal that takes Escape or a backdrop click, among those with no other open modal
544
+ * inside them (a nested modal's backdrop can share its outer modal's z-index). A top-layer dialog
545
+ * (or a modal inside one) paints above anything outside it, and within the top layer the last
546
+ * opened is on top. Modals in the same layer (the page, or one top-layer dialog) then rank by
547
+ * the highest backdrop, then the one later in the page.
472
548
  */
473
- isInnermostModal() {
474
- // Find all open modals
475
- const openModals = Array.from(document.querySelectorAll('pds-modal')).filter(modal => modal.open);
476
- if (openModals.length === 0)
477
- return false;
478
- // Get this modal's backdrop element
479
- const thisBackdrop = this.el.querySelector('.pds-modal__backdrop');
480
- if (!thisBackdrop)
481
- return false;
482
- // Get computed z-index of all open modal backdrops
483
- const modalZIndexes = openModals.map(modal => this.getBackdropZIndex(modal));
484
- // Get the highest z-index
485
- const maxZIndex = Math.max(...modalZIndexes);
486
- // Check if this modal's backdrop has the highest z-index
487
- const thisZIndex = this.getBackdropZIndex(this.el);
488
- return thisZIndex === maxZIndex;
549
+ innermostOpenModal() {
550
+ const open = Array.from(openModals.keys()).filter(modal => modal.isConnected);
551
+ const innermost = open.filter(modal => !open.some(other => other !== modal && containsComposed(modal, other)));
552
+ const byDocumentOrder = [...innermost].sort((a, b) => (precedesComposed(a, b) ? -1 : 1));
553
+ // A modal inside a top-layer dialog paints in that dialog's place in the top layer, even when it
554
+ // isn't in the top layer itself (disableTopLayer), so that dialog's open order ranks it first.
555
+ const rank = (modal) => {
556
+ var _a;
557
+ const backdrop = modal.querySelector('.pds-modal__backdrop');
558
+ const topLayerDialog = backdrop ? closest.closest('dialog:modal', backdrop) : null;
559
+ const topLayerModal = topLayerDialog === null || topLayerDialog === void 0 ? void 0 : topLayerDialog.closest('pds-modal');
560
+ const layer = topLayerDialog ? ((_a = (topLayerModal && openModals.get(topLayerModal))) !== null && _a !== void 0 ? _a : 0) : 0;
561
+ return [topLayerDialog ? 1 : 0, layer, this.getBackdropZIndex(modal), byDocumentOrder.indexOf(modal)];
562
+ };
563
+ let top = null;
564
+ let topRank = [-1, -1, -1, -1];
565
+ for (const modal of innermost) {
566
+ const modalRank = rank(modal);
567
+ const above = modalRank.findIndex((value, i) => value !== topRank[i]);
568
+ if (above >= 0 && modalRank[above] > topRank[above]) {
569
+ top = modal;
570
+ topRank = modalRank;
571
+ }
572
+ }
573
+ return top;
489
574
  }
490
575
  /** Whether a modal dialog other than this one sits between the focused element and this modal. */
491
576
  modalDialogAbove(active) {
@@ -525,14 +610,14 @@ const PdsModal = class {
525
610
  }
526
611
  render() {
527
612
  const [sizeClass = 'md', customSize] = this.resolveSize();
528
- return (index.h("dialog", { key: '12ea38928ea287f16ffa33e246caa2bfde6fc130', class: {
613
+ return (index.h("dialog", { key: '0633d4e8a7cb81f2f9280e754564960a00a4d60b', class: {
529
614
  'pds-modal__backdrop': true,
530
615
  'open': this.open
531
- }, "aria-modal": this.disableTopLayer ? 'false' : 'true', "aria-labelledby": `${this.componentId}-heading`, onClick: this.handleBackdropClick }, index.h("div", { key: 'b9d83cabda56c315d9115f7030e3bdbcf923ec08', class: {
616
+ }, "aria-modal": this.disableTopLayer ? 'false' : 'true', "aria-labelledby": `${this.componentId}-heading`, onClick: this.handleBackdropClick, onCancel: this.handleCancel }, index.h("div", { key: '466f8a3476cf066817554f47ec7d3a0e3ab10b89', class: {
532
617
  'pds-modal': true,
533
618
  [`pds-modal--${sizeClass}`]: true,
534
619
  'pds-modal--scrollable': this.scrollable
535
- }, style: customSize !== undefined ? { '--pds-modal-max-width': customSize } : undefined, part: "modal" }, index.h("slot", { key: '3c5edad1482cb124e503197adf411890573b076d' }))));
620
+ }, style: customSize !== undefined ? { '--pds-modal-max-width': customSize } : undefined, part: "modal" }, index.h("slot", { key: '01ca45c96540416216a1082bad52b137720f0f4d' }))));
536
621
  }
537
622
  get el() { return index.getElement(this); }
538
623
  static get watchers() { return {
@@ -539,11 +539,12 @@ const PdsMultiselect = class {
539
539
  }
540
540
  /**
541
541
  * Handle global keyboard events for accessibility.
542
- * Closes dropdown on Escape key press regardless of focus location.
542
+ * Closes dropdown on Escape key press regardless of focus location. Taken in document's capture
543
+ * phase, before a modal around the multiselect hears it on document and closes too.
543
544
  */
544
545
  handleWindowKeyDown(event) {
545
546
  var _a;
546
- if (!this.isOpen)
547
+ if (!this.isOpen || event.defaultPrevented)
547
548
  return;
548
549
  if (event.key === 'Escape') {
549
550
  event.preventDefault();
@@ -1233,10 +1234,10 @@ const PdsMultiselect = class {
1233
1234
  }
1234
1235
  render() {
1235
1236
  const hasSelections = this.selectedItems.length > 0;
1236
- return (index.h(index.Host, { key: '0ef6791666063e0d951d28f980b3bca89cf24287', "aria-disabled": this.disabled ? 'true' : null }, index.h("div", { key: 'f0ccbabfbd2dcf38f6df52f73f2152eb4bd2828e', class: "pds-multiselect", onFocusout: this.handleContainerFocusOut }, this.label && (index.h("label", { key: '88e01c744644ce1f75c8d5b71dfaeeb79d1ef7df', id: `${this.componentId}-label`, htmlFor: this.componentId, class: {
1237
+ return (index.h(index.Host, { key: 'fcc35a82c16d5cf35a49ce997f19618104f9cc45', "aria-disabled": this.disabled ? 'true' : null }, index.h("div", { key: '72d1fdf759d9173f54bac80ec50aff651fd2b357', class: "pds-multiselect", onFocusout: this.handleContainerFocusOut }, this.label && (index.h("label", { key: 'e28f02c2beb23dc2c0deefadbd0645b20d938d91', id: `${this.componentId}-label`, htmlFor: this.componentId, class: {
1237
1238
  'pds-multiselect__label': true,
1238
1239
  'visually-hidden': this.hideLabel,
1239
- } }, this.label)), index.h("div", { key: '0bf4a3b1378ff18e6d6b449507ebbae1bfb279e4', class: "pds-multiselect__wrapper", ref: el => (this.containerEl = el), style: { width: this.triggerWidth } }, this.slottedTriggerEl ? (index.h("slot", { name: "trigger" })) : this.selectedDisplay === 'pill' && this.pillPosition === 'inline' ? (index.h("div", { ref: el => (this.pillInlineTriggerEl = el || undefined), part: "trigger", class: {
1240
+ } }, this.label)), index.h("div", { key: '06539bf2c4b8da2889505a9f042d5cd6721b1853', class: "pds-multiselect__wrapper", ref: el => (this.containerEl = el), style: { width: this.triggerWidth } }, this.slottedTriggerEl ? (index.h("slot", { name: "trigger" })) : this.selectedDisplay === 'pill' && this.pillPosition === 'inline' ? (index.h("div", { ref: el => (this.pillInlineTriggerEl = el || undefined), part: "trigger", class: {
1240
1241
  'pds-multiselect__trigger': true,
1241
1242
  'pds-multiselect__trigger--open': this.isOpen,
1242
1243
  'pds-multiselect__trigger--disabled': this.disabled,
@@ -1252,7 +1253,7 @@ const PdsMultiselect = class {
1252
1253
  }, id: this.componentId, disabled: this.disabled, "aria-required": this.required ? 'true' : undefined, "aria-expanded": this.isOpen ? 'true' : 'false', "aria-haspopup": "listbox", "aria-describedby": form.assignDescription(this.componentId, this.invalid || !!this.errorMessage, this.helperMessage, this.errorMessage), "aria-invalid": this.invalid || !!this.errorMessage ? 'true' : undefined, onClick: this.handleTriggerClick, onKeyDown: this.handleTriggerKeyDown }, index.h("span", { class: {
1253
1254
  'pds-multiselect__trigger-text': true,
1254
1255
  'pds-multiselect__trigger-text--placeholder': !hasSelections || (this.selectedDisplay === 'pill' && this.pillPosition === 'below'),
1255
- } }, this.getTriggerText()), index.h("pds-icon", { "aria-hidden": "true", class: "pds-multiselect__icon", icon: index$1.enlarge }))), this.renderDropdown()), this.selectedDisplay === 'pill' && this.pillPosition === 'below' && this.renderBelowPills(), index.h("span", { key: '5202a263b219d06891bda739f98db0162bf33c48', class: "visually-hidden", "aria-live": "polite", "aria-atomic": "true" }, this.removalAnnouncement), this.helperMessage && !(this.errorMessage && this.errorMessage.length > 0) && (index.h("p", { key: '47f7b55863c5b72d41c4ef1893b9364988fc0b56', class: "pds-multiselect__helper", id: form.messageId(this.componentId, 'helper') }, this.helperMessage)), this.errorMessage && (index.h("p", { key: '3930dc30b2df7179d178873c470aa1da0f6f258c', class: "pds-multiselect__error", id: form.messageId(this.componentId, 'error') }, index.h("pds-icon", { key: 'e31d55db3fd0173c56faa23e34e0be92371983d4', icon: index$1.danger, size: "small" }), this.errorMessage)), index.h("div", { key: 'b3326cc1a5e5886be05327a066e0a7ddd36ccefc', style: { display: 'none' } }, index.h("slot", { key: '91559cb9510abce6084348a17516460fd8545ef7' })))));
1256
+ } }, this.getTriggerText()), index.h("pds-icon", { "aria-hidden": "true", class: "pds-multiselect__icon", icon: index$1.enlarge }))), this.renderDropdown()), this.selectedDisplay === 'pill' && this.pillPosition === 'below' && this.renderBelowPills(), index.h("span", { key: '3364fc00bc744fcd599436a9cb6dc563564d89d9', class: "visually-hidden", "aria-live": "polite", "aria-atomic": "true" }, this.removalAnnouncement), this.helperMessage && !(this.errorMessage && this.errorMessage.length > 0) && (index.h("p", { key: 'bf712c65b9acb66e2998a43686fed408fc1ff48a', class: "pds-multiselect__helper", id: form.messageId(this.componentId, 'helper') }, this.helperMessage)), this.errorMessage && (index.h("p", { key: 'b485db6c2c3ae312cce2b90db60e850ad6dfdcf5', class: "pds-multiselect__error", id: form.messageId(this.componentId, 'error') }, index.h("pds-icon", { key: '20daf861cf9584f33f5a9574e9581acc487e7dfe', icon: index$1.danger, size: "small" }), this.errorMessage)), index.h("div", { key: '018d333254771695a8c191f1cea40af6e78427a2', style: { display: 'none' } }, index.h("slot", { key: '2ef4a01fbe5d294a9ead5cc1183c28341834539b' })))));
1256
1257
  }
1257
1258
  static get formAssociated() { return true; }
1258
1259
  get el() { return index.getElement(this); }
@@ -50,6 +50,16 @@ const cssEscape = (value) => {
50
50
 
51
51
  const pdsPopoverCss = () => `:host{--sizing-max-width-default:352px;display:inline-block}:host .pds-popover__trigger-wrapper{display:inline-block}:host .pds-popover__content-slot-wrapper{display:none}`;
52
52
 
53
+ /** Open auto popovers, in the order they opened: Escape goes to the last one it applies to. */
54
+ const openAutoPopovers = [];
55
+ /** The focused element, followed down through shadow roots to the control that has focus. */
56
+ function deepActiveElement() {
57
+ var _a;
58
+ let active = document.activeElement;
59
+ while ((_a = active === null || active === void 0 ? void 0 : active.shadowRoot) === null || _a === void 0 ? void 0 : _a.activeElement)
60
+ active = active.shadowRoot.activeElement;
61
+ return active;
62
+ }
53
63
  const PdsPopover = class {
54
64
  constructor(hostRef) {
55
65
  index.registerInstance(this, hostRef);
@@ -81,7 +91,7 @@ const PdsPopover = class {
81
91
  */
82
92
  this.triggerObserver = null;
83
93
  /**
84
- * Portal element rendered in document.body
94
+ * Portal element: in document.body, or in the open dialog the popover is in
85
95
  */
86
96
  this.portalEl = null;
87
97
  /**
@@ -177,6 +187,26 @@ const PdsPopover = class {
177
187
  break;
178
188
  }
179
189
  };
190
+ /** The focus style copied into a shadow root the panel opened in, which `document.head` can't reach. */
191
+ this.shadowFocusStyle = null;
192
+ /** The dialog the panel was opened in, closed along with it. */
193
+ this.watchedDialog = null;
194
+ this.onDialogClose = () => {
195
+ this.hide();
196
+ };
197
+ /**
198
+ * Inside a dialog, the panel can sit in a form that wraps the dialog, which a body portal never
199
+ * did. A submit from the panel (a button in it, or Enter in a field in it) is stopped, so the
200
+ * panel behaves as it does in the body.
201
+ */
202
+ this.handleSubmitFromPanel = (event) => {
203
+ if (!this.portalEl || !closest.containsComposed(event.target, this.portalEl))
204
+ return;
205
+ if (closest.containsComposed(this.portalEl, event.submitter) || closest.containsComposed(this.portalEl, deepActiveElement())) {
206
+ event.preventDefault();
207
+ event.stopPropagation();
208
+ }
209
+ };
180
210
  }
181
211
  restorePlacementDefault(newValue) {
182
212
  this.placement = reflectedAttributeDefault.restoreReflectedDefault(this.el, 'placement', newValue, 'right');
@@ -239,19 +269,23 @@ const PdsPopover = class {
239
269
  (_a = this.triggerEl) === null || _a === void 0 ? void 0 : _a.addEventListener('click', this.handleTriggerClick);
240
270
  this.observeTrigger();
241
271
  this.linkTriggerToPortal();
242
- // Still open after a move: the trigger moved, so the portal follows it.
272
+ // Still open after a move: the trigger moved, so the portal follows it, into the dialog it
273
+ // is in now, if any.
243
274
  if (this.active) {
275
+ this.placePortal();
244
276
  requestAnimationFrame(() => this.handlePopoverPositioning());
245
277
  }
246
278
  }
247
279
  disconnectedCallback() {
248
280
  var _a;
249
281
  // Moved in one operation (a DOM morph, or a framework reparenting it), the popover is
250
- // already back in the page. Its portal is in <body>, untouched, so it stays open, with focus
251
- // where it was.
282
+ // already back in the page. Its portal is outside the popover, so it stays open, with focus
283
+ // where it was; connectedCallback moves it to follow the popover.
252
284
  if (this.el.isConnected)
253
285
  return;
254
286
  this.isComponentMounted = false;
287
+ this.unwatchDialogClose();
288
+ this.forgetOpen();
255
289
  // Open, the content sits in the portal; put it back before the portal goes. Only listeners on
256
290
  // the popover itself hear the close event now, but those are the ones mirroring its state.
257
291
  if (this.active) {
@@ -272,6 +306,7 @@ const PdsPopover = class {
272
306
  // Clean up all event listeners
273
307
  this.removeLightDismissListeners();
274
308
  this.removeScrollAndResizeListeners();
309
+ document.removeEventListener('submit', this.handleSubmitFromPanel, true);
275
310
  // Clear any pending debounce timers
276
311
  this.clearDebounceTimers();
277
312
  // Remove portal from DOM
@@ -316,9 +351,6 @@ const PdsPopover = class {
316
351
  else {
317
352
  this.portalEl.id = `${this.componentId}-portal`;
318
353
  }
319
- // Add accessibility attributes for screen readers
320
- // Note: Native Popover API doesn't add a specific role, keeping semantic HTML
321
- this.portalEl.setAttribute('aria-modal', 'false'); // Not a modal, can interact with rest of page
322
354
  // Append to body
323
355
  document.body.appendChild(this.portalEl);
324
356
  // Add global focus styles to match Pine design system
@@ -402,7 +434,27 @@ const PdsPopover = class {
402
434
  // Clear tracking array
403
435
  this.movedNodes = [];
404
436
  }
437
+ copyFocusStylesInto(root) {
438
+ var _a, _b, _c;
439
+ // Outside a shadow root the document's own style applies, so drop a copy left in one.
440
+ if (!(root instanceof ShadowRoot)) {
441
+ (_a = this.shadowFocusStyle) === null || _a === void 0 ? void 0 : _a.remove();
442
+ this.shadowFocusStyle = null;
443
+ return;
444
+ }
445
+ if (((_b = this.shadowFocusStyle) === null || _b === void 0 ? void 0 : _b.parentNode) === root || !this.portalEl)
446
+ return;
447
+ const source = document.querySelector(`style[data-pds-popover-focus="${cssEscape(this.portalEl.id)}"]`);
448
+ if (!source)
449
+ return;
450
+ (_c = this.shadowFocusStyle) === null || _c === void 0 ? void 0 : _c.remove();
451
+ this.shadowFocusStyle = source.cloneNode(true);
452
+ root.appendChild(this.shadowFocusStyle);
453
+ }
405
454
  removePortal() {
455
+ var _a;
456
+ (_a = this.shadowFocusStyle) === null || _a === void 0 ? void 0 : _a.remove();
457
+ this.shadowFocusStyle = null;
406
458
  if (this.portalEl) {
407
459
  // Remove the portal element from DOM
408
460
  if (this.portalEl.parentNode) {
@@ -427,9 +479,13 @@ const PdsPopover = class {
427
479
  // Record open timestamp to prevent immediate close from opening click
428
480
  this.openTimestamp = Date.now();
429
481
  this.linkTriggerToPortal();
482
+ this.placePortal();
430
483
  // Update portal content with latest slot content
431
484
  this.updatePortalContent();
432
- // Show portal
485
+ // Show portal. The attribute tells a modal around it that this popover takes Escape first; only
486
+ // an auto popover listens for Escape.
487
+ if (this.popoverType === 'auto')
488
+ this.portalEl.setAttribute('data-pds-popover-open', '');
433
489
  this.portalEl.style.display = 'block';
434
490
  this.portalEl.style.opacity = '1';
435
491
  this.portalEl.style.visibility = 'visible';
@@ -441,8 +497,10 @@ const PdsPopover = class {
441
497
  });
442
498
  // Add scroll and resize listeners for repositioning (always)
443
499
  this.addScrollAndResizeListeners();
500
+ document.addEventListener('submit', this.handleSubmitFromPanel, true);
444
501
  // Add document listeners for light dismiss and escape key (auto type only)
445
502
  if (this.popoverType === 'auto') {
503
+ openAutoPopovers.push(this);
446
504
  this.addLightDismissListeners();
447
505
  }
448
506
  // Emit open event
@@ -458,12 +516,16 @@ const PdsPopover = class {
458
516
  if (!this.active || !this.portalEl)
459
517
  return;
460
518
  this.active = false;
461
- // Restoring the content drops its focus to the body, so check where focus is first.
462
- const focusWasInside = this.portalEl.contains(document.activeElement);
519
+ this.unwatchDialogClose();
520
+ // Restoring the content drops its focus to the body, so check where focus is first. In a
521
+ // shadow root, document.activeElement is only its host.
522
+ const focusWasInside = closest.containsComposed(this.portalEl, deepActiveElement());
463
523
  this.linkTriggerToPortal();
464
524
  // Restore content back to original slot positions
465
525
  this.restorePortalContent();
466
- // Hide portal
526
+ // Hide portal. The dark-mode border set in a shadow root is worked out again on the next open.
527
+ this.portalEl.style.border = '';
528
+ this.portalEl.removeAttribute('data-pds-popover-open');
467
529
  this.portalEl.style.display = 'none';
468
530
  this.portalEl.style.opacity = '0';
469
531
  this.portalEl.style.visibility = 'hidden';
@@ -473,8 +535,10 @@ const PdsPopover = class {
473
535
  this.returnFocusToTrigger();
474
536
  }
475
537
  // Remove all listeners
538
+ this.forgetOpen();
476
539
  this.removeLightDismissListeners();
477
540
  this.removeScrollAndResizeListeners();
541
+ document.removeEventListener('submit', this.handleSubmitFromPanel, true);
478
542
  // Clear any pending timers
479
543
  this.clearDebounceTimers();
480
544
  // Emit close event
@@ -494,6 +558,59 @@ const PdsPopover = class {
494
558
  this.show();
495
559
  }
496
560
  }
561
+ /**
562
+ * Puts the panel where it shows above everything around the popover. In an open dialog, it goes
563
+ * inside it: a modal dialog sits in the top layer above the body and makes the rest of the page
564
+ * inert, and a non-modal one sits above it by z-index. Right after the popover, it's next in the
565
+ * Tab order. In another component's slot, though, it would have no slot to render in, and in a
566
+ * form its buttons would submit it, so there it goes at the end of the dialog.
567
+ */
568
+ placePortal() {
569
+ var _a, _b;
570
+ if (!this.portalEl)
571
+ return;
572
+ const dialog = closest.closest('dialog[open]', this.el);
573
+ if (!dialog) {
574
+ this.unwatchDialogClose();
575
+ this.portalEl.style.border = '';
576
+ if (this.portalEl.parentElement !== document.body)
577
+ document.body.appendChild(this.portalEl);
578
+ this.copyFocusStylesInto(document);
579
+ return;
580
+ }
581
+ const form = closest.closest('form', this.el);
582
+ const besidePopover = !((_a = this.el.parentElement) === null || _a === void 0 ? void 0 : _a.shadowRoot) && !(form && closest.containsComposed(dialog, form));
583
+ if (besidePopover) {
584
+ if (this.el.nextElementSibling !== this.portalEl)
585
+ (_b = this.el.parentNode) === null || _b === void 0 ? void 0 : _b.insertBefore(this.portalEl, this.el.nextSibling);
586
+ }
587
+ else if (this.portalEl.parentNode !== dialog) {
588
+ dialog.appendChild(this.portalEl);
589
+ }
590
+ this.watchDialogClose(dialog);
591
+ const root = this.portalEl.getRootNode();
592
+ this.copyFocusStylesInto(root);
593
+ // In a shadow root, the copied dark-mode rule can't see a [data-theme="dark"] on the page.
594
+ this.portalEl.style.border = root instanceof ShadowRoot && closest.closest('[data-theme="dark"]', this.el) ? 'var(--pine-border)' : '';
595
+ }
596
+ forgetOpen() {
597
+ const index = openAutoPopovers.indexOf(this);
598
+ if (index >= 0)
599
+ openAutoPopovers.splice(index, 1);
600
+ }
601
+ /**
602
+ * Whether an Escape is this popover's. One from a dialog the popover isn't in belongs to that
603
+ * dialog when it's above it: a top-layer dialog or a pds-modal. A plain non-modal dialog inside
604
+ * the popover's modal doesn't close on Escape, so the popover takes it.
605
+ */
606
+ takesEscapeFrom(event) {
607
+ var _a, _b;
608
+ const origin = ((_b = (_a = event.composedPath) === null || _a === void 0 ? void 0 : _a.call(event)[0]) !== null && _b !== void 0 ? _b : event.target);
609
+ const dialog = closest.closest('dialog[open]', origin);
610
+ if (!dialog || closest.containsComposed(dialog, this.portalEl))
611
+ return true;
612
+ return !(dialog.classList.contains('pds-modal__backdrop') || dialog.matches(':modal'));
613
+ }
497
614
  /**
498
615
  * Adds light dismiss listeners (click outside and escape key)
499
616
  */
@@ -502,6 +619,19 @@ const PdsPopover = class {
502
619
  document.addEventListener('click', this.boundClickOutsideHandler, true);
503
620
  document.addEventListener('keydown', this.boundEscapeKeyHandler);
504
621
  }
622
+ /** Closes the popover when the dialog it opened in closes, so it isn't left open inside. */
623
+ watchDialogClose(dialog) {
624
+ if (this.watchedDialog === dialog)
625
+ return;
626
+ this.unwatchDialogClose();
627
+ this.watchedDialog = dialog;
628
+ dialog.addEventListener('close', this.onDialogClose);
629
+ }
630
+ unwatchDialogClose() {
631
+ var _a;
632
+ (_a = this.watchedDialog) === null || _a === void 0 ? void 0 : _a.removeEventListener('close', this.onDialogClose);
633
+ this.watchedDialog = null;
634
+ }
505
635
  /**
506
636
  * Removes light dismiss listeners
507
637
  */
@@ -527,7 +657,7 @@ const PdsPopover = class {
527
657
  * Handles clicks outside the popover for light dismiss (auto type only)
528
658
  */
529
659
  handleClickOutside(event) {
530
- var _a, _b, _c, _d;
660
+ var _a, _b;
531
661
  // Ignore events that occurred at or before the popover opened
532
662
  // This prevents the opening click from immediately closing the popover
533
663
  // Convert event.timeStamp (DOMHighResTimeStamp from performance.now()) to epoch time
@@ -538,16 +668,18 @@ const PdsPopover = class {
538
668
  if (eventTime <= this.openTimestamp) {
539
669
  return;
540
670
  }
541
- const target = event.target;
671
+ // In a shadow root, event.target is only its host.
672
+ const target = ((_b = (_a = event.composedPath) === null || _a === void 0 ? void 0 : _a.call(event)[0]) !== null && _b !== void 0 ? _b : event.target);
542
673
  // A datepicker's calendar is portaled to the body: a click in the calendar of a datepicker
543
- // inside this popover, in its content or its trigger, isn't outside it.
544
- const calendar = (_b = (_a = target).closest) === null || _b === void 0 ? void 0 : _b.call(_a, '.pds-datepicker-calendar-portal');
674
+ // inside this popover, in its content or its trigger, isn't outside it. The target can be
675
+ // inside the calendar's shadow root, so look up across it.
676
+ const calendar = closest.closest('.pds-datepicker-calendar-portal', target);
545
677
  const owner = calendar === null || calendar === void 0 ? void 0 : calendar.pdsDatepicker;
546
678
  if (owner && (closest.containsComposed(this.portalEl, owner) || closest.containsComposed(this.triggerEl, owner)))
547
679
  return;
548
680
  // Check if click is outside both the popover portal and the trigger
549
- const clickedInsidePopover = (_c = this.portalEl) === null || _c === void 0 ? void 0 : _c.contains(target);
550
- const clickedInsideTrigger = (_d = this.triggerEl) === null || _d === void 0 ? void 0 : _d.contains(target);
681
+ const clickedInsidePopover = closest.containsComposed(this.portalEl, target);
682
+ const clickedInsideTrigger = closest.containsComposed(this.triggerEl, target);
551
683
  if (!clickedInsidePopover && !clickedInsideTrigger) {
552
684
  this.hide();
553
685
  }
@@ -568,14 +700,19 @@ const PdsPopover = class {
568
700
  * Mirrors native Popover API: Escape closes and returns focus to trigger
569
701
  */
570
702
  handleEscapeKey(event) {
571
- if (event.key === 'Escape') {
572
- // Prevent default Escape behavior and stop propagation
573
- // This mirrors native Popover API which handles Escape exclusively
574
- event.preventDefault();
575
- event.stopPropagation();
576
- // Close popover and return focus to trigger
577
- this.hide();
578
- }
703
+ // A control inside that closes on Escape (a datepicker, a combobox) has handled it already.
704
+ if (event.key !== 'Escape' || event.defaultPrevented)
705
+ return;
706
+ // Like the native Popover API, Escape closes only the top popover: the last one opened that
707
+ // it applies to. Every open popover hears it, so each checks whether it's that one.
708
+ const top = [...openAutoPopovers].reverse().find(popover => popover.takesEscapeFrom(event));
709
+ if (top !== this)
710
+ return;
711
+ // Take the key, so the dialog or modal around it doesn't close too.
712
+ event.preventDefault();
713
+ event.stopPropagation();
714
+ // Close popover and return focus to trigger
715
+ this.hide();
579
716
  }
580
717
  /**
581
718
  * Handles scroll events to reposition the popover (debounced for performance)
@@ -680,13 +817,20 @@ const PdsPopover = class {
680
817
  }
681
818
  this.portalEl.style.top = `${top}px`;
682
819
  this.portalEl.style.left = `${left}px`;
820
+ // `position: fixed` measures from the viewport unless an ancestor becomes its containing block,
821
+ // as a modal dialog does with its backdrop filter. Shift by however far it landed from the spot.
822
+ const placed = this.portalEl.getBoundingClientRect();
823
+ if (Math.abs(placed.top - top) > 0.5 || Math.abs(placed.left - left) > 0.5) {
824
+ this.portalEl.style.top = `${2 * top - placed.top}px`;
825
+ this.portalEl.style.left = `${2 * left - placed.left}px`;
826
+ }
683
827
  // Reset the repositioning guard after a short delay
684
828
  setTimeout(() => {
685
829
  this.isRepositioning = false;
686
830
  }, 16); // ~1 frame at 60fps
687
831
  }
688
832
  render() {
689
- return (index.h(index.Host, { key: 'd804db8c73e0dc2acf0d1bbca20edb77993ff7fd', id: this.componentId }, index.h("span", { key: '5408d1856c9b13b327ea13b43cffc23af925a82f', class: "pds-popover__trigger-wrapper" }, index.h("slot", { key: '9d0ff01292c67bdaa88d49114162c3c65ff8a20d', name: "trigger", onSlotchange: this.handleTriggerSlotChange })), index.h("div", { key: 'cdd38016453ea1838d5718fb8e190cd4304ac8cc', class: "pds-popover__content-slot-wrapper" }, index.h("slot", { key: '9faf60d7be7a2ff3c5fe6c3e4de374bafadf848a', onSlotchange: this.handleContentSlotChange }))));
833
+ return (index.h(index.Host, { key: 'da8a0c715df1199231b1265085f3f9ae0234032d', id: this.componentId }, index.h("span", { key: 'f35023492399420829cec2e88556462f5ac1663e', class: "pds-popover__trigger-wrapper" }, index.h("slot", { key: '38ebe92eebb87665467b3f92ff94dc8095450063', name: "trigger", onSlotchange: this.handleTriggerSlotChange })), index.h("div", { key: '8917930f4520544d14bbd68b4c7936dd60af19e3', class: "pds-popover__content-slot-wrapper" }, index.h("slot", { key: '28d3f9112256edd95721a79a7072a9e35a3712bf', onSlotchange: this.handleContentSlotChange }))));
690
834
  }
691
835
  get el() { return index.getElement(this); }
692
836
  static get watchers() { return {