@mintplayer/ng-bootstrap 21.30.0 → 21.31.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 (153) hide show
  1. package/fesm2022/mintplayer-ng-bootstrap-a11y.mjs +455 -0
  2. package/fesm2022/mintplayer-ng-bootstrap-a11y.mjs.map +1 -0
  3. package/fesm2022/mintplayer-ng-bootstrap-accordion.mjs +8 -5
  4. package/fesm2022/mintplayer-ng-bootstrap-accordion.mjs.map +1 -1
  5. package/fesm2022/mintplayer-ng-bootstrap-breadcrumb.mjs +10 -4
  6. package/fesm2022/mintplayer-ng-bootstrap-breadcrumb.mjs.map +1 -1
  7. package/fesm2022/mintplayer-ng-bootstrap-button-group.mjs +7 -4
  8. package/fesm2022/mintplayer-ng-bootstrap-button-group.mjs.map +1 -1
  9. package/fesm2022/mintplayer-ng-bootstrap-calendar.mjs +131 -3
  10. package/fesm2022/mintplayer-ng-bootstrap-calendar.mjs.map +1 -1
  11. package/fesm2022/mintplayer-ng-bootstrap-carousel.mjs +80 -48
  12. package/fesm2022/mintplayer-ng-bootstrap-carousel.mjs.map +1 -1
  13. package/fesm2022/mintplayer-ng-bootstrap-code-snippet.mjs +4 -1
  14. package/fesm2022/mintplayer-ng-bootstrap-code-snippet.mjs.map +1 -1
  15. package/fesm2022/mintplayer-ng-bootstrap-color-picker.mjs +218 -14
  16. package/fesm2022/mintplayer-ng-bootstrap-color-picker.mjs.map +1 -1
  17. package/fesm2022/mintplayer-ng-bootstrap-datatable.mjs +4 -3
  18. package/fesm2022/mintplayer-ng-bootstrap-datatable.mjs.map +1 -1
  19. package/fesm2022/mintplayer-ng-bootstrap-datepicker.mjs +2 -2
  20. package/fesm2022/mintplayer-ng-bootstrap-datepicker.mjs.map +1 -1
  21. package/fesm2022/mintplayer-ng-bootstrap-dock.mjs +294 -3
  22. package/fesm2022/mintplayer-ng-bootstrap-dock.mjs.map +1 -1
  23. package/fesm2022/mintplayer-ng-bootstrap-dropdown-menu.mjs +163 -18
  24. package/fesm2022/mintplayer-ng-bootstrap-dropdown-menu.mjs.map +1 -1
  25. package/fesm2022/mintplayer-ng-bootstrap-dropdown.mjs +179 -7
  26. package/fesm2022/mintplayer-ng-bootstrap-dropdown.mjs.map +1 -1
  27. package/fesm2022/mintplayer-ng-bootstrap-file-upload.mjs +14 -4
  28. package/fesm2022/mintplayer-ng-bootstrap-file-upload.mjs.map +1 -1
  29. package/fesm2022/mintplayer-ng-bootstrap-has-overlay.mjs +14 -0
  30. package/fesm2022/mintplayer-ng-bootstrap-has-overlay.mjs.map +1 -1
  31. package/fesm2022/mintplayer-ng-bootstrap-list-group.mjs +2 -1
  32. package/fesm2022/mintplayer-ng-bootstrap-list-group.mjs.map +1 -1
  33. package/fesm2022/mintplayer-ng-bootstrap-marquee.mjs +7 -4
  34. package/fesm2022/mintplayer-ng-bootstrap-marquee.mjs.map +1 -1
  35. package/fesm2022/mintplayer-ng-bootstrap-modal.mjs +70 -6
  36. package/fesm2022/mintplayer-ng-bootstrap-modal.mjs.map +1 -1
  37. package/fesm2022/mintplayer-ng-bootstrap-multiselect.mjs +5 -4
  38. package/fesm2022/mintplayer-ng-bootstrap-multiselect.mjs.map +1 -1
  39. package/fesm2022/mintplayer-ng-bootstrap-navbar-toggler.mjs +6 -6
  40. package/fesm2022/mintplayer-ng-bootstrap-navbar-toggler.mjs.map +1 -1
  41. package/fesm2022/mintplayer-ng-bootstrap-navbar.mjs +45 -13
  42. package/fesm2022/mintplayer-ng-bootstrap-navbar.mjs.map +1 -1
  43. package/fesm2022/mintplayer-ng-bootstrap-offcanvas.mjs +51 -5
  44. package/fesm2022/mintplayer-ng-bootstrap-offcanvas.mjs.map +1 -1
  45. package/fesm2022/mintplayer-ng-bootstrap-pagination.mjs +5 -3
  46. package/fesm2022/mintplayer-ng-bootstrap-pagination.mjs.map +1 -1
  47. package/fesm2022/mintplayer-ng-bootstrap-placeholder.mjs +18 -4
  48. package/fesm2022/mintplayer-ng-bootstrap-placeholder.mjs.map +1 -1
  49. package/fesm2022/mintplayer-ng-bootstrap-playlist-toggler.mjs +6 -6
  50. package/fesm2022/mintplayer-ng-bootstrap-playlist-toggler.mjs.map +1 -1
  51. package/fesm2022/mintplayer-ng-bootstrap-popover.mjs +61 -6
  52. package/fesm2022/mintplayer-ng-bootstrap-popover.mjs.map +1 -1
  53. package/fesm2022/mintplayer-ng-bootstrap-priority-nav.mjs +19 -4
  54. package/fesm2022/mintplayer-ng-bootstrap-priority-nav.mjs.map +1 -1
  55. package/fesm2022/mintplayer-ng-bootstrap-progress-bar.mjs +8 -5
  56. package/fesm2022/mintplayer-ng-bootstrap-progress-bar.mjs.map +1 -1
  57. package/fesm2022/mintplayer-ng-bootstrap-range.mjs +4 -3
  58. package/fesm2022/mintplayer-ng-bootstrap-range.mjs.map +1 -1
  59. package/fesm2022/mintplayer-ng-bootstrap-rating.mjs +34 -4
  60. package/fesm2022/mintplayer-ng-bootstrap-rating.mjs.map +1 -1
  61. package/fesm2022/mintplayer-ng-bootstrap-reduced-motion.mjs +59 -0
  62. package/fesm2022/mintplayer-ng-bootstrap-reduced-motion.mjs.map +1 -0
  63. package/fesm2022/mintplayer-ng-bootstrap-resizable.mjs +91 -2
  64. package/fesm2022/mintplayer-ng-bootstrap-resizable.mjs.map +1 -1
  65. package/fesm2022/mintplayer-ng-bootstrap-scheduler.mjs +16 -5
  66. package/fesm2022/mintplayer-ng-bootstrap-scheduler.mjs.map +1 -1
  67. package/fesm2022/mintplayer-ng-bootstrap-scrollspy.mjs +2 -2
  68. package/fesm2022/mintplayer-ng-bootstrap-scrollspy.mjs.map +1 -1
  69. package/fesm2022/mintplayer-ng-bootstrap-searchbox.mjs +28 -5
  70. package/fesm2022/mintplayer-ng-bootstrap-searchbox.mjs.map +1 -1
  71. package/fesm2022/mintplayer-ng-bootstrap-select.mjs +4 -3
  72. package/fesm2022/mintplayer-ng-bootstrap-select.mjs.map +1 -1
  73. package/fesm2022/mintplayer-ng-bootstrap-select2.mjs +18 -4
  74. package/fesm2022/mintplayer-ng-bootstrap-select2.mjs.map +1 -1
  75. package/fesm2022/mintplayer-ng-bootstrap-signature-pad.mjs +4 -3
  76. package/fesm2022/mintplayer-ng-bootstrap-signature-pad.mjs.map +1 -1
  77. package/fesm2022/mintplayer-ng-bootstrap-tab-control.mjs +2 -2
  78. package/fesm2022/mintplayer-ng-bootstrap-tab-control.mjs.map +1 -1
  79. package/fesm2022/mintplayer-ng-bootstrap-table.mjs +10 -3
  80. package/fesm2022/mintplayer-ng-bootstrap-table.mjs.map +1 -1
  81. package/fesm2022/mintplayer-ng-bootstrap-tile-manager.mjs +143 -29
  82. package/fesm2022/mintplayer-ng-bootstrap-tile-manager.mjs.map +1 -1
  83. package/fesm2022/mintplayer-ng-bootstrap-timepicker.mjs +2 -2
  84. package/fesm2022/mintplayer-ng-bootstrap-timepicker.mjs.map +1 -1
  85. package/fesm2022/mintplayer-ng-bootstrap-toast.mjs +7 -4
  86. package/fesm2022/mintplayer-ng-bootstrap-toast.mjs.map +1 -1
  87. package/fesm2022/mintplayer-ng-bootstrap-toggle-button.mjs +42 -21
  88. package/fesm2022/mintplayer-ng-bootstrap-toggle-button.mjs.map +1 -1
  89. package/fesm2022/mintplayer-ng-bootstrap-tooltip.mjs +33 -4
  90. package/fesm2022/mintplayer-ng-bootstrap-tooltip.mjs.map +1 -1
  91. package/fesm2022/mintplayer-ng-bootstrap-treeview.mjs +17 -7
  92. package/fesm2022/mintplayer-ng-bootstrap-treeview.mjs.map +1 -1
  93. package/fesm2022/mintplayer-ng-bootstrap-typeahead.mjs +50 -8
  94. package/fesm2022/mintplayer-ng-bootstrap-typeahead.mjs.map +1 -1
  95. package/fesm2022/mintplayer-ng-bootstrap-virtual-datatable.mjs +34 -12
  96. package/fesm2022/mintplayer-ng-bootstrap-virtual-datatable.mjs.map +1 -1
  97. package/fesm2022/mintplayer-ng-bootstrap-web-components-a11y.mjs +74 -0
  98. package/fesm2022/mintplayer-ng-bootstrap-web-components-a11y.mjs.map +1 -0
  99. package/fesm2022/mintplayer-ng-bootstrap-web-components-scheduler.mjs +1476 -71
  100. package/fesm2022/mintplayer-ng-bootstrap-web-components-scheduler.mjs.map +1 -1
  101. package/fesm2022/mintplayer-ng-bootstrap-web-components-splitter.mjs +194 -2
  102. package/fesm2022/mintplayer-ng-bootstrap-web-components-splitter.mjs.map +1 -1
  103. package/fesm2022/mintplayer-ng-bootstrap-web-components-tab-control.mjs +4 -0
  104. package/fesm2022/mintplayer-ng-bootstrap-web-components-tab-control.mjs.map +1 -1
  105. package/package.json +14 -2
  106. package/types/mintplayer-ng-bootstrap-a11y.d.ts +196 -0
  107. package/types/mintplayer-ng-bootstrap-accordion.d.ts +4 -2
  108. package/types/mintplayer-ng-bootstrap-breadcrumb.d.ts +2 -1
  109. package/types/mintplayer-ng-bootstrap-button-group.d.ts +2 -1
  110. package/types/mintplayer-ng-bootstrap-calendar.d.ts +32 -0
  111. package/types/mintplayer-ng-bootstrap-carousel.d.ts +56 -3
  112. package/types/mintplayer-ng-bootstrap-code-snippet.d.ts +1 -0
  113. package/types/mintplayer-ng-bootstrap-color-picker.d.ts +75 -4
  114. package/types/mintplayer-ng-bootstrap-datatable.d.ts +1 -1
  115. package/types/mintplayer-ng-bootstrap-dock.d.ts +51 -0
  116. package/types/mintplayer-ng-bootstrap-dropdown-menu.d.ts +54 -9
  117. package/types/mintplayer-ng-bootstrap-dropdown.d.ts +57 -2
  118. package/types/mintplayer-ng-bootstrap-file-upload.d.ts +4 -1
  119. package/types/mintplayer-ng-bootstrap-has-overlay.d.ts +14 -0
  120. package/types/mintplayer-ng-bootstrap-marquee.d.ts +2 -1
  121. package/types/mintplayer-ng-bootstrap-modal.d.ts +25 -1
  122. package/types/mintplayer-ng-bootstrap-multiselect.d.ts +2 -1
  123. package/types/mintplayer-ng-bootstrap-navbar-toggler.d.ts +4 -2
  124. package/types/mintplayer-ng-bootstrap-navbar.d.ts +25 -1
  125. package/types/mintplayer-ng-bootstrap-offcanvas.d.ts +23 -1
  126. package/types/mintplayer-ng-bootstrap-pagination.d.ts +3 -1
  127. package/types/mintplayer-ng-bootstrap-placeholder.d.ts +5 -1
  128. package/types/mintplayer-ng-bootstrap-playlist-toggler.d.ts +4 -2
  129. package/types/mintplayer-ng-bootstrap-popover.d.ts +21 -1
  130. package/types/mintplayer-ng-bootstrap-priority-nav.d.ts +4 -1
  131. package/types/mintplayer-ng-bootstrap-progress-bar.d.ts +4 -2
  132. package/types/mintplayer-ng-bootstrap-range.d.ts +2 -1
  133. package/types/mintplayer-ng-bootstrap-rating.d.ts +3 -0
  134. package/types/mintplayer-ng-bootstrap-reduced-motion.d.ts +36 -0
  135. package/types/mintplayer-ng-bootstrap-resizable.d.ts +4 -0
  136. package/types/mintplayer-ng-bootstrap-scheduler.d.ts +42 -9
  137. package/types/mintplayer-ng-bootstrap-scrollspy.d.ts +1 -1
  138. package/types/mintplayer-ng-bootstrap-searchbox.d.ts +8 -1
  139. package/types/mintplayer-ng-bootstrap-select.d.ts +2 -1
  140. package/types/mintplayer-ng-bootstrap-select2.d.ts +3 -0
  141. package/types/mintplayer-ng-bootstrap-signature-pad.d.ts +2 -1
  142. package/types/mintplayer-ng-bootstrap-table.d.ts +8 -1
  143. package/types/mintplayer-ng-bootstrap-tile-manager.d.ts +21 -2
  144. package/types/mintplayer-ng-bootstrap-toast.d.ts +6 -1
  145. package/types/mintplayer-ng-bootstrap-toggle-button.d.ts +11 -0
  146. package/types/mintplayer-ng-bootstrap-tooltip.d.ts +5 -0
  147. package/types/mintplayer-ng-bootstrap-treeview.d.ts +12 -1
  148. package/types/mintplayer-ng-bootstrap-typeahead.d.ts +11 -3
  149. package/types/mintplayer-ng-bootstrap-virtual-datatable.d.ts +14 -1
  150. package/types/mintplayer-ng-bootstrap-web-components-a11y.d.ts +34 -0
  151. package/types/mintplayer-ng-bootstrap-web-components-scheduler-core.d.ts +35 -11
  152. package/types/mintplayer-ng-bootstrap-web-components-scheduler.d.ts +246 -0
  153. package/types/mintplayer-ng-bootstrap-web-components-splitter.d.ts +95 -37
@@ -4,6 +4,7 @@ import { DOCUMENT, NgTemplateOutlet } from '@angular/common';
4
4
  import { html, unsafeCSS, LitElement } from 'lit';
5
5
  import '@mintplayer/ng-bootstrap/web-components/tab-control';
6
6
  import '@mintplayer/ng-bootstrap/web-components/splitter';
7
+ import { LiveAnnouncerController } from '@mintplayer/ng-bootstrap/web-components/a11y';
7
8
 
8
9
  class BsDockPaneComponent {
9
10
  constructor() {
@@ -173,6 +174,27 @@ const styles = unsafeCSS(`:host {
173
174
  white-space: nowrap;
174
175
  }
175
176
 
177
+ .dock-floating__close {
178
+ flex: 0 0 auto;
179
+ appearance: none;
180
+ background: transparent;
181
+ border: 0;
182
+ color: var(--bs-body-color);
183
+ cursor: pointer;
184
+ font-size: 1.25rem;
185
+ line-height: 1;
186
+ padding: 0 0.4rem;
187
+ border-radius: 3px;
188
+ margin-top: -1px;
189
+ }
190
+ .dock-floating__close:hover {
191
+ background: rgba(var(--bs-primary-rgb), 0.15);
192
+ }
193
+ .dock-floating__close:focus-visible {
194
+ outline: 2px solid var(--bs-primary);
195
+ outline-offset: 1px;
196
+ }
197
+
176
198
  .dock-floating > .dock-stack {
177
199
  flex: 1 1 auto;
178
200
  min-width: 12rem;
@@ -288,7 +310,11 @@ const styles = unsafeCSS(`:host {
288
310
  background: rgba(var(--bs-primary-rgb), 0.35);
289
311
  border-color: var(--bs-primary);
290
312
  opacity: 1;
291
- outline: none;
313
+ }
314
+
315
+ .dock-intersection-handle:focus-visible {
316
+ outline: 2px solid var(--bs-primary);
317
+ outline-offset: 1px;
292
318
  }
293
319
 
294
320
  .dock-snap-marker {
@@ -417,6 +443,18 @@ const styles = unsafeCSS(`:host {
417
443
  display: block;
418
444
  min-width: 0;
419
445
  min-height: 0;
446
+ }
447
+
448
+ @media (prefers-reduced-motion: reduce) {
449
+ .dock-floating,
450
+ .dock-floating__resizer,
451
+ .dock-floating__close,
452
+ .dock-intersection-handle,
453
+ .dock-drop-indicator,
454
+ .dock-drop-joystick__button {
455
+ transition: none !important;
456
+ animation: none !important;
457
+ }
420
458
  }`);
421
459
 
422
460
  class MintDockManagerElement extends LitElement {
@@ -497,6 +535,14 @@ class MintDockManagerElement extends LitElement {
497
535
  }
498
536
  constructor() {
499
537
  super();
538
+ this.liveAnnouncer = new LiveAnnouncerController(this);
539
+ /**
540
+ * Active keyboard-driven pane move (Phase 7 of WC ARIA PRD). The user
541
+ * pressed M on a focused tab; until they pick a destination key (T/R/B/L
542
+ * to dock to a side, F to float) or Escape, we intercept letter keys and
543
+ * route them to the synthetic-drop pipeline.
544
+ */
545
+ this.paneMoveMode = null;
500
546
  this.dropJoystickTarget = null;
501
547
  this.rootLayout = null;
502
548
  this.floatingLayouts = [];
@@ -540,7 +586,7 @@ class MintDockManagerElement extends LitElement {
540
586
  this.onSplitterResize = this.onSplitterResize.bind(this);
541
587
  }
542
588
  render() {
543
- return template;
589
+ return html `${template}${this.liveAnnouncer.template()}`;
544
590
  }
545
591
  firstUpdated() {
546
592
  // Resolve document and window now that we are connected.
@@ -585,6 +631,9 @@ class MintDockManagerElement extends LitElement {
585
631
  // Drop targeting (drop indicator + joystick zone selection) runs entirely
586
632
  // off pointer-based hit-testing in updatePaneDragDropTargetFromPoint and
587
633
  // findDropZoneByPoint — no HTML5 dragover/drop/dragleave listeners needed.
634
+ // Keyboard pane-move (Phase 7). Capture phase so we run before mp-tab-control's
635
+ // shadow-internal handlers preventDefault on Arrow / Home / End / Enter.
636
+ root.addEventListener('keydown', (ev) => this.onRootKeyDown(ev), { capture: true });
588
637
  // Render any layout that was set before the shadow DOM existed.
589
638
  this.renderLayout();
590
639
  // Reactive triggers for intersection-handle re-rendering. Each observer
@@ -785,6 +834,14 @@ class MintDockManagerElement extends LitElement {
785
834
  index,
786
835
  segments: [],
787
836
  });
837
+ const titleText = this.getFloatingWindowTitle(floating);
838
+ const titleId = `${this.instanceId}-floating-${index}-title`;
839
+ // role=dialog announces the window-like region; aria-modal=false because
840
+ // floating panes don't trap focus — users can still interact with docked
841
+ // panes behind them.
842
+ wrapper.setAttribute('role', 'dialog');
843
+ wrapper.setAttribute('aria-labelledby', titleId);
844
+ wrapper.setAttribute('aria-modal', 'false');
788
845
  const { left, top, width, height } = floating.bounds;
789
846
  wrapper.style.left = `${left}px`;
790
847
  wrapper.style.top = `${top}px`;
@@ -797,8 +854,21 @@ class MintDockManagerElement extends LitElement {
797
854
  chrome.addEventListener('pointerdown', (event) => this.beginFloatingDrag(event, index, wrapper, chrome));
798
855
  const title = this.documentRef.createElement('div');
799
856
  title.classList.add('dock-floating__title');
800
- title.textContent = this.getFloatingWindowTitle(floating);
857
+ title.id = titleId;
858
+ title.textContent = titleText;
801
859
  chrome.appendChild(title);
860
+ const closeBtn = this.documentRef.createElement('button');
861
+ closeBtn.type = 'button';
862
+ closeBtn.classList.add('dock-floating__close');
863
+ closeBtn.setAttribute('aria-label', `Close pane: ${titleText}`);
864
+ closeBtn.textContent = '×';
865
+ closeBtn.addEventListener('click', (ev) => {
866
+ ev.stopPropagation();
867
+ this.closeFloatingPane(index);
868
+ });
869
+ // The chrome's pointerdown otherwise begins a drag on the close button.
870
+ closeBtn.addEventListener('pointerdown', (ev) => ev.stopPropagation());
871
+ chrome.appendChild(closeBtn);
802
872
  wrapper.appendChild(chrome);
803
873
  if (floating.root) {
804
874
  const content = this.renderNode(floating.root, [], index);
@@ -856,6 +926,26 @@ class MintDockManagerElement extends LitElement {
856
926
  resizerConfigs.forEach(({ classes, edges }) => {
857
927
  const resizer = this.documentRef.createElement('div');
858
928
  resizer.classList.add(...classes);
929
+ resizer.setAttribute('role', 'separator');
930
+ // For corner resizers (both axes), aria-orientation is omitted —
931
+ // separator's default is "horizontal" but corner == both, so neither
932
+ // value is correct. SR users still hear "separator" with the label.
933
+ const isPureHorizontal = edges.horizontal === 'none' && edges.vertical !== 'none';
934
+ const isPureVertical = edges.vertical === 'none' && edges.horizontal !== 'none';
935
+ if (isPureHorizontal)
936
+ resizer.setAttribute('aria-orientation', 'horizontal');
937
+ else if (isPureVertical)
938
+ resizer.setAttribute('aria-orientation', 'vertical');
939
+ const labelParts = [];
940
+ if (edges.vertical === 'top')
941
+ labelParts.push('top');
942
+ else if (edges.vertical === 'bottom')
943
+ labelParts.push('bottom');
944
+ if (edges.horizontal === 'left')
945
+ labelParts.push('left');
946
+ else if (edges.horizontal === 'right')
947
+ labelParts.push('right');
948
+ resizer.setAttribute('aria-label', `Resize pane ${labelParts.length === 2 ? 'corner ' : 'edge '}${labelParts.join('-')}`);
859
949
  resizer.addEventListener('pointerdown', (event) => this.beginFloatingResize(event, index, wrapper, resizer, edges));
860
950
  wrapper.appendChild(resizer);
861
951
  });
@@ -965,6 +1055,9 @@ class MintDockManagerElement extends LitElement {
965
1055
  handle.classList.add('dock-intersection-handle', 'glyph');
966
1056
  handle.setAttribute('role', 'separator');
967
1057
  handle.setAttribute('aria-label', 'Resize split intersection');
1058
+ // tabindex=0 — make the handle keyboard-reachable. Arrow keys then
1059
+ // resize the underlying h/v dividers (handled in onIntersectionKeyDown).
1060
+ handle.setAttribute('tabindex', '0');
968
1061
  const firstPair = group.pairs[0];
969
1062
  const key = `${firstPair.h.pathStr}:${firstPair.h.index}|${firstPair.v.pathStr}:${firstPair.v.index}`;
970
1063
  handle.dataset['key'] = key;
@@ -979,9 +1072,51 @@ class MintDockManagerElement extends LitElement {
979
1072
  const seedV = { path: this.parsePath(firstPair.v.pathStr), index: firstPair.v.index, container: this.findSplitterByPath(this.parsePath(firstPair.v.pathStr)?.segments ?? []) ?? this.rootEl, rect: new DOMRect() };
980
1073
  handle.addEventListener('pointerdown', (ev) => this.beginCornerResize(ev, seedH, seedV, handle));
981
1074
  handle.addEventListener('dblclick', (ev) => this.onIntersectionDoubleClick(ev, handle));
1075
+ handle.addEventListener('keydown', (ev) => this.onIntersectionKeyDown(ev, handle));
982
1076
  layer.appendChild(handle);
983
1077
  });
984
1078
  }
1079
+ /**
1080
+ * Keyboard resize on a focused intersection handle. ArrowLeft/Right resize
1081
+ * the vertical divider the handle sits on; ArrowUp/Down resize the
1082
+ * horizontal divider. Home/End shrink/grow the leading panel of whichever
1083
+ * axis is implied by the key. Shift = fine 1% step.
1084
+ *
1085
+ * Internally delegates to mp-splitter.resizeDividerBy() so the percent-step
1086
+ * + clamp math lives in one place. If a 2D handle straddles multiple split
1087
+ * pairs (sibling splitters whose dividers happen to overlap), the first
1088
+ * pair drives — same precedence as the pointer drag path.
1089
+ */
1090
+ onIntersectionKeyDown(event, handle) {
1091
+ const isVerticalAxis = event.key === 'ArrowLeft' || event.key === 'ArrowRight';
1092
+ const isHorizontalAxis = event.key === 'ArrowUp' || event.key === 'ArrowDown';
1093
+ const isHomeEnd = event.key === 'Home' || event.key === 'End';
1094
+ if (!isVerticalAxis && !isHorizontalAxis && !isHomeEnd)
1095
+ return;
1096
+ const pairsJson = handle.dataset['pairs'];
1097
+ if (!pairsJson)
1098
+ return;
1099
+ let pairs;
1100
+ try {
1101
+ pairs = JSON.parse(pairsJson);
1102
+ }
1103
+ catch {
1104
+ return;
1105
+ }
1106
+ if (pairs.length === 0)
1107
+ return;
1108
+ event.preventDefault();
1109
+ const fine = event.shiftKey;
1110
+ // Which divider does this key drive? Arrow{Left,Right} → vertical
1111
+ // (h-flow splitter, v-bar divider); Arrow{Up,Down} → horizontal. For
1112
+ // Home/End, default to the vertical-axis divider (matches APG's "drive
1113
+ // the splitter to its limit" convention on a horizontal layout).
1114
+ const drivesVerticalDivider = isVerticalAxis || isHomeEnd;
1115
+ const target = drivesVerticalDivider ? pairs[0].v : pairs[0].h;
1116
+ const path = this.parsePath(target.pathStr);
1117
+ const splitter = this.findSplitterByPath(path?.segments ?? []);
1118
+ splitter?.resizeDividerBy?.(target.index, event.key, fine);
1119
+ }
985
1120
  beginCornerResize(event, h, v, handle) {
986
1121
  event.preventDefault();
987
1122
  // Build pairs from dataset if available (grouped intersections), otherwise from the provided pair
@@ -2553,6 +2688,7 @@ class MintDockManagerElement extends LitElement {
2553
2688
  if (wrapper) {
2554
2689
  this.promoteFloatingPane(newIndex, wrapper);
2555
2690
  }
2691
+ this.liveAnnouncer.announce(`Pane ${this.titles[pane] ?? pane} torn off into a floating window.`);
2556
2692
  // Update drag state so subsequent moves reposition the floating window
2557
2693
  state.sourcePath = { type: 'floating', index: newIndex, segments: [] };
2558
2694
  state.floatingIndex = newIndex;
@@ -3526,6 +3662,161 @@ class MintDockManagerElement extends LitElement {
3526
3662
  }
3527
3663
  this.floatingLayouts.splice(index, 1);
3528
3664
  }
3665
+ /**
3666
+ * Keyboard handler on the dock root, registered in capture phase so it
3667
+ * runs before mp-tab-control's shadow-internal handlers swallow Arrow /
3668
+ * Home / End / Enter. M opens move mode on a focused tab; subsequent
3669
+ * letter keys (T/R/B/L/F) commit the move; Escape cancels.
3670
+ */
3671
+ onRootKeyDown(event) {
3672
+ if (this.paneMoveMode) {
3673
+ this.handlePaneMoveModeKey(event);
3674
+ return;
3675
+ }
3676
+ if (event.key !== 'm' && event.key !== 'M')
3677
+ return;
3678
+ const focused = this.findFocusedPaneOrigin();
3679
+ if (!focused)
3680
+ return;
3681
+ event.preventDefault();
3682
+ event.stopPropagation();
3683
+ this.paneMoveMode = focused;
3684
+ const title = this.titles[focused.paneName] ?? focused.paneName;
3685
+ this.liveAnnouncer.announce(`Move mode for pane ${title}. Press T, R, B, or L to dock to top, right, bottom, or left of the current stack. Press F to float. Escape to cancel.`);
3686
+ }
3687
+ handlePaneMoveModeKey(event) {
3688
+ if (event.key === 'Escape') {
3689
+ event.preventDefault();
3690
+ event.stopPropagation();
3691
+ this.paneMoveMode = null;
3692
+ this.liveAnnouncer.announce('Move cancelled.');
3693
+ return;
3694
+ }
3695
+ const key = event.key.toLowerCase();
3696
+ const zoneByKey = {
3697
+ t: 'top', r: 'right', b: 'bottom', l: 'left', f: 'float',
3698
+ };
3699
+ const choice = zoneByKey[key];
3700
+ if (!choice)
3701
+ return;
3702
+ event.preventDefault();
3703
+ event.stopPropagation();
3704
+ if (choice === 'float')
3705
+ this.commitPaneMoveAsFloat();
3706
+ else
3707
+ this.commitPaneMoveToZone(choice);
3708
+ }
3709
+ /**
3710
+ * Walk the focused element through nested shadow roots to find a focused
3711
+ * tab button, then map it back to the dock's pane name + source path via
3712
+ * the data attributes on the slotted .dock-tab span.
3713
+ */
3714
+ findFocusedPaneOrigin() {
3715
+ if (!this.shadowRoot)
3716
+ return null;
3717
+ let active = this.shadowRoot.activeElement;
3718
+ while (active && active.shadowRoot && active.shadowRoot.activeElement) {
3719
+ active = active.shadowRoot.activeElement;
3720
+ }
3721
+ if (!active || active.getAttribute('role') !== 'tab')
3722
+ return null;
3723
+ // mp-tab-control assigns the role=tab button id `${tabId}-header-button`,
3724
+ // while the dock's `.dock-tab` slot wrapper carries `data-tab-id=${tabId}`.
3725
+ // Strip the suffix so the lookup matches the slot wrapper.
3726
+ const tabId = active.id.replace(/-header-button$/, '');
3727
+ const headerSpan = this.shadowRoot.querySelector(`.dock-tab[data-tab-id="${tabId}"]`);
3728
+ if (!headerSpan)
3729
+ return null;
3730
+ const stack = headerSpan.closest('.dock-stack');
3731
+ if (!stack)
3732
+ return null;
3733
+ const path = this.parsePath(stack.dataset['path']);
3734
+ if (!path)
3735
+ return null;
3736
+ return { paneName: headerSpan.dataset['pane'], sourcePath: path };
3737
+ }
3738
+ /**
3739
+ * Synthesise a minimal dragState so handleDrop's existing pipeline can
3740
+ * commit the keyboard-driven move. handleDrop reads {pane, sourcePath,
3741
+ * dropHandled} — the rest of dragState is pointer-drag bookkeeping that
3742
+ * doesn't apply here.
3743
+ */
3744
+ synthesiseDragStateForKeyboardMove() {
3745
+ if (!this.paneMoveMode)
3746
+ return;
3747
+ this.dragState = {
3748
+ pane: this.paneMoveMode.paneName,
3749
+ sourcePath: this.clonePath(this.paneMoveMode.sourcePath),
3750
+ floatingIndex: this.paneMoveMode.sourcePath.type === 'floating'
3751
+ ? this.paneMoveMode.sourcePath.index
3752
+ : null,
3753
+ pointerOffsetX: 0,
3754
+ pointerOffsetY: 0,
3755
+ dropHandled: false,
3756
+ sourceStackEl: null,
3757
+ sourceHeaderBounds: null,
3758
+ startClientX: 0,
3759
+ startClientY: 0,
3760
+ };
3761
+ }
3762
+ commitPaneMoveToZone(zone) {
3763
+ if (!this.paneMoveMode)
3764
+ return;
3765
+ const move = this.paneMoveMode;
3766
+ this.synthesiseDragStateForKeyboardMove();
3767
+ // Source stack is also the target stack — handleDrop creates a sibling
3768
+ // split with the moved pane on the chosen side.
3769
+ this.handleDrop(move.sourcePath, zone);
3770
+ this.dragState = null;
3771
+ this.paneMoveMode = null;
3772
+ const title = this.titles[move.paneName] ?? move.paneName;
3773
+ this.liveAnnouncer.announce(`Pane ${title} docked to ${zone}.`);
3774
+ }
3775
+ commitPaneMoveAsFloat() {
3776
+ if (!this.paneMoveMode)
3777
+ return;
3778
+ const move = this.paneMoveMode;
3779
+ const source = this.resolveStackLocation(move.sourcePath);
3780
+ if (!source) {
3781
+ this.paneMoveMode = null;
3782
+ return;
3783
+ }
3784
+ this.removePaneFromLocation(source, move.paneName, true);
3785
+ const floatingLayout = {
3786
+ bounds: {
3787
+ left: 100 + this.floatingLayouts.length * 24,
3788
+ top: 100 + this.floatingLayouts.length * 24,
3789
+ width: 320,
3790
+ height: 200,
3791
+ },
3792
+ root: { kind: 'stack', panes: [move.paneName], activePane: move.paneName },
3793
+ activePane: move.paneName,
3794
+ };
3795
+ this.floatingLayouts.push(floatingLayout);
3796
+ this.normalizeAllLayouts();
3797
+ this.renderLayout();
3798
+ this.dispatchLayoutChanged();
3799
+ this.paneMoveMode = null;
3800
+ const title = this.titles[move.paneName] ?? move.paneName;
3801
+ this.liveAnnouncer.announce(`Pane ${title} torn off into a floating window.`);
3802
+ }
3803
+ /**
3804
+ * Close a floating pane via the chrome's close button (or any external
3805
+ * caller). Announces the close to SR users, redraws the dock, and emits a
3806
+ * layout-changed event. Phase 4 of the WC ARIA PRD — the floating chrome
3807
+ * had no close affordance before this; users had to programmatically pop
3808
+ * the pane out of `floatingLayouts`.
3809
+ */
3810
+ closeFloatingPane(index) {
3811
+ const floating = this.floatingLayouts[index];
3812
+ if (!floating)
3813
+ return;
3814
+ const title = this.getFloatingWindowTitle(floating);
3815
+ this.removeFloatingAt(index);
3816
+ this.renderLayout();
3817
+ this.dispatchLayoutChanged();
3818
+ this.liveAnnouncer.announce(`Closed floating pane ${title}.`);
3819
+ }
3529
3820
  removePaneFromFloating(index, path, pane, skipCleanup = false) {
3530
3821
  const floating = this.floatingLayouts[index];
3531
3822
  if (!floating || !floating.root) {