@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
@@ -2,6 +2,7 @@ import * as i0 from '@angular/core';
2
2
  import { input, output, viewChild, ChangeDetectionStrategy, Component, contentChildren, computed, effect, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
3
3
  import { NgTemplateOutlet } from '@angular/common';
4
4
  import { html, unsafeCSS, LitElement, nothing } from 'lit';
5
+ import { LiveAnnouncerController } from '@mintplayer/ng-bootstrap/web-components/a11y';
5
6
 
6
7
  /**
7
8
  * One tile inside a `<bs-tile-manager>`.
@@ -382,7 +383,7 @@ const styles = unsafeCSS(`:host {
382
383
 
383
384
  .tile[data-resizing=true] {
384
385
  z-index: 4;
385
- transition: none;
386
+ transition: width 150ms cubic-bezier(0.2, 0, 0, 1), height 150ms cubic-bezier(0.2, 0, 0, 1);
386
387
  }
387
388
 
388
389
  .tile[data-blocked=true] {
@@ -504,7 +505,8 @@ const styles = unsafeCSS(`:host {
504
505
  display: none;
505
506
  }
506
507
 
507
- .tile-grid__live-region {
508
+ .tile-grid__live-region,
509
+ .tile-grid__sr-only {
508
510
  position: absolute;
509
511
  width: 1px;
510
512
  height: 1px;
@@ -534,6 +536,8 @@ const styles = unsafeCSS(`:host {
534
536
  outline-offset: -2px;
535
537
  }`);
536
538
 
539
+ const TILE_INSTRUCTIONS = 'Press M to enter move mode. In move mode, arrow keys move the tile, Shift with arrow keys resize it, Enter commits, Escape cancels.';
540
+ let tileManagerInstanceCounter = 0;
537
541
  const TOUCH_LONG_PRESS_MS = 600;
538
542
  const TOUCH_LONG_PRESS_SLOP_PX = 10;
539
543
  const TOUCH_PRESS_FEEDBACK_DELAY_MS = 150;
@@ -557,7 +561,9 @@ class MintTileManagerElement extends LitElement {
557
561
  this.gestureKind = 'idle';
558
562
  this.blocked = false;
559
563
  this.effectiveColumnCount = 1;
560
- this.liveRegionMessage = '';
564
+ this.focusedTileId = null;
565
+ this.liveAnnouncer = new LiveAnnouncerController(this);
566
+ this.instanceId = `mp-tile-manager-${++tileManagerInstanceCounter}`;
561
567
  this.hostResizeObserver = null;
562
568
  this.flipPreviousRects = new Map();
563
569
  // Cached layout metrics. Refreshed lazily in updated()/firstUpdated() and on
@@ -605,27 +611,36 @@ class MintTileManagerElement extends LitElement {
605
611
  blocked: { state: true },
606
612
  effectiveColumnCount: { state: true },
607
613
  keyboardMode: { state: true },
608
- liveRegionMessage: { state: true },
614
+ focusedTileId: { state: true },
609
615
  }; }
616
+ get instructionsId() {
617
+ return `${this.instanceId}-instructions`;
618
+ }
610
619
  render() {
611
620
  const layoutSource = this.previewLayout ?? this.tiles.map((t) => ({ id: t.id, position: t.position }));
612
621
  const tileById = new Map(this.tiles.map((t) => [t.id, t]));
613
622
  const gridStyle = this.computeGridStyle();
614
- // ARIA grid hierarchy is grid > row > gridcell. A single role="row"
615
- // wrapper with display: contents lets us satisfy that without disturbing
616
- // the CSS Grid placement of the gridcell children.
623
+ // role="region" + button-per-tile (PRD §10 Q1): a tile board's dimensions
624
+ // change under user reflow, so the static grid contract (rowindex/colindex)
625
+ // is a poor fit. Tiles are activatable buttons whose move/resize is
626
+ // discoverable through aria-describedby's instructions string.
617
627
  return html `
618
- <div class="tile-grid" role="grid" aria-label=${this.label ?? nothing} style=${gridStyle}>
619
- <div role="row" style="display: contents;">
620
- ${layoutSource.map((entry) => {
628
+ <div
629
+ class="tile-grid"
630
+ role="region"
631
+ aria-label=${this.label ?? 'Tile board'}
632
+ aria-describedby=${this.instructionsId}
633
+ style=${gridStyle}
634
+ >
635
+ ${layoutSource.map((entry) => {
621
636
  const tile = tileById.get(entry.id);
622
637
  if (!tile)
623
638
  return nothing;
624
639
  return this.renderTile(tile, entry.position);
625
640
  })}
626
- </div>
627
641
  </div>
628
- <div class="tile-grid__live-region" aria-live="polite" aria-atomic="true">${this.liveRegionMessage}</div>
642
+ <div id=${this.instructionsId} class="tile-grid__sr-only">${TILE_INSTRUCTIONS}</div>
643
+ ${this.liveAnnouncer.template()}
629
644
  `;
630
645
  }
631
646
  renderTile(tile, pos) {
@@ -634,18 +649,34 @@ class MintTileManagerElement extends LitElement {
634
649
  const isBlocked = (isDragging || isResizing) && this.blocked;
635
650
  const isPressing = this.gestureKind === 'arming-touch-drag' && this.activeTileId() === tile.id;
636
651
  const transform = this.computeActiveTransform(tile.id);
652
+ // Inline width/height during a pointer-resize gesture so the tile pixel
653
+ // size can ride a CSS transition between span states. The grid still
654
+ // allocates the new span area instantly; the active tile's visible size
655
+ // lags 150ms behind. Without this, grid-column/grid-row span changes are
656
+ // not animatable in CSS and the tile snaps. Reduced-motion is gated by
657
+ // the global `@media (prefers-reduced-motion: reduce)` rule in SCSS.
658
+ const cell = this.cellMetrics;
659
+ const inlineSize = isResizing && cell.width > 0
660
+ ? {
661
+ width: pos.colSpan * cell.width + (pos.colSpan - 1) * cell.gapX,
662
+ height: pos.rowSpan * cell.height + (pos.rowSpan - 1) * cell.gapY,
663
+ }
664
+ : null;
637
665
  const style = [
638
666
  `grid-column: ${pos.colStart} / span ${pos.colSpan}`,
639
667
  `grid-row: ${pos.rowStart} / span ${pos.rowSpan}`,
640
668
  transform ? `transform: ${transform}` : '',
669
+ inlineSize ? `width: ${inlineSize.width}px` : '',
670
+ inlineSize ? `height: ${inlineSize.height}px` : '',
641
671
  ]
642
672
  .filter(Boolean)
643
673
  .join('; ');
674
+ const isFocusableStop = this.resolveFocusableTileId() === tile.id;
644
675
  return html `
645
676
  <div
646
677
  class="tile"
647
- role="gridcell"
648
- tabindex="0"
678
+ role="button"
679
+ tabindex=${isFocusableStop ? '0' : '-1'}
649
680
  data-tile-id=${tile.id}
650
681
  data-dragging=${isDragging ? 'true' : 'false'}
651
682
  data-resizing=${isResizing ? 'true' : 'false'}
@@ -658,6 +689,7 @@ class MintTileManagerElement extends LitElement {
658
689
  style=${style}
659
690
  @pointerdown=${(e) => this.onTilePointerDown(e, tile)}
660
691
  @keydown=${(e) => this.onTileKeyDown(e, tile)}
692
+ @focus=${() => this.onTileFocus(tile)}
661
693
  >
662
694
  <div class="tile__header-shell">
663
695
  <slot name=${`${tile.id}-header`}></slot>
@@ -742,9 +774,6 @@ class MintTileManagerElement extends LitElement {
742
774
  // ---------------- Lifecycle ----------------
743
775
  connectedCallback() {
744
776
  super.connectedCallback();
745
- if (!this.hasAttribute('role')) {
746
- this.setAttribute('role', 'application');
747
- }
748
777
  document.addEventListener('visibilitychange', this.onVisibilityChange);
749
778
  }
750
779
  disconnectedCallback() {
@@ -1005,6 +1034,7 @@ class MintTileManagerElement extends LitElement {
1005
1034
  this.gestureKind = 'drag';
1006
1035
  this.lastPointerPosition = { x: startX, y: startY };
1007
1036
  this.attachWindowListeners();
1037
+ this.announceDragBegin(tile);
1008
1038
  this.requestUpdate();
1009
1039
  }
1010
1040
  beginDrag(event, tile) {
@@ -1027,6 +1057,7 @@ class MintTileManagerElement extends LitElement {
1027
1057
  this.gestureKind = 'drag';
1028
1058
  this.lastPointerPosition = { x: event.clientX, y: event.clientY };
1029
1059
  this.attachWindowListeners();
1060
+ this.announceDragBegin(tile);
1030
1061
  this.runPackerForCurrentGesture();
1031
1062
  }
1032
1063
  beginResize(event, tile, mode) {
@@ -1043,8 +1074,17 @@ class MintTileManagerElement extends LitElement {
1043
1074
  this.gestureKind = 'resize';
1044
1075
  this.lastPointerPosition = { x: event.clientX, y: event.clientY };
1045
1076
  this.attachWindowListeners();
1077
+ this.announceResizeBegin(tile);
1046
1078
  this.runPackerForCurrentGesture();
1047
1079
  }
1080
+ announceDragBegin(tile) {
1081
+ const label = tile.label ?? `tile at row ${tile.position.rowStart}, column ${tile.position.colStart}`;
1082
+ this.liveAnnouncer.announce(`Dragging ${label}.`);
1083
+ }
1084
+ announceResizeBegin(tile) {
1085
+ const label = tile.label ?? `tile at row ${tile.position.rowStart}, column ${tile.position.colStart}`;
1086
+ this.liveAnnouncer.announce(`Resizing ${label}.`);
1087
+ }
1048
1088
  attachWindowListeners() {
1049
1089
  window.addEventListener('pointermove', this.onWindowPointerMove);
1050
1090
  window.addEventListener('pointerup', this.onWindowPointerUp);
@@ -1110,7 +1150,11 @@ class MintTileManagerElement extends LitElement {
1110
1150
  const cols = this.effectiveColumnCount;
1111
1151
  const result = pack(this.tiles.map((t) => ({ id: t.id, position: t.position, locked: t.disableMove })), { id: tile.id, rect }, cols);
1112
1152
  this.previewLayout = result.layout;
1153
+ const wasBlocked = g.blocked;
1113
1154
  g.blocked = result.blocked;
1155
+ if (result.blocked && !wasBlocked) {
1156
+ this.liveAnnouncer.announce('Move blocked by a locked tile.');
1157
+ }
1114
1158
  this.blocked = result.blocked;
1115
1159
  this.requestUpdate();
1116
1160
  }
@@ -1191,10 +1235,9 @@ class MintTileManagerElement extends LitElement {
1191
1235
  }));
1192
1236
  const movedTile = finalLayout.find((p) => p.id === g.tileId);
1193
1237
  if (movedTile) {
1194
- this.liveRegionMessage =
1195
- g.kind === 'drag'
1196
- ? `Tile moved to row ${movedTile.position.rowStart}, column ${movedTile.position.colStart}`
1197
- : `Tile resized to ${movedTile.position.colSpan} columns by ${movedTile.position.rowSpan} rows`;
1238
+ this.liveAnnouncer.announce(g.kind === 'drag'
1239
+ ? `Tile moved to row ${movedTile.position.rowStart}, column ${movedTile.position.colStart}`
1240
+ : `Tile resized to ${movedTile.position.colSpan} columns by ${movedTile.position.rowSpan} rows`);
1198
1241
  }
1199
1242
  this.cleanupGesture();
1200
1243
  }
@@ -1250,16 +1293,20 @@ class MintTileManagerElement extends LitElement {
1250
1293
  }
1251
1294
  // ---------------- Keyboard ----------------
1252
1295
  onTileKeyDown(event, tile) {
1253
- if (tile.disableMove && tile.disableResize)
1254
- return;
1255
1296
  const km = this.keyboardState;
1256
1297
  if (km.kind === 'idle') {
1257
- if (event.key === ' ' && !tile.disableMove) {
1298
+ // Outside move mode: arrow keys traverse focus between tiles, Home/End
1299
+ // jump to first/last, M enters move mode if the tile allows it.
1300
+ if (event.key === 'm' || event.key === 'M') {
1301
+ if (tile.disableMove && tile.disableResize)
1302
+ return;
1258
1303
  event.preventDefault();
1259
1304
  this.keyboardState = { kind: 'move', tileId: tile.id };
1260
- this.liveRegionMessage = 'Move mode enabled. Use arrow keys to move; Enter to commit, Escape to cancel.';
1305
+ this.liveAnnouncer.announce('Move mode enabled. Use arrow keys to move, Shift with arrow keys to resize, Enter to commit, Escape to cancel.');
1261
1306
  return;
1262
1307
  }
1308
+ if (this.handleFocusNavigation(event, tile))
1309
+ return;
1263
1310
  return;
1264
1311
  }
1265
1312
  if (km.tileId !== tile.id)
@@ -1267,7 +1314,7 @@ class MintTileManagerElement extends LitElement {
1267
1314
  if (event.key === 'Escape' || event.key === 'Enter') {
1268
1315
  event.preventDefault();
1269
1316
  this.keyboardState = { kind: 'idle' };
1270
- this.liveRegionMessage = event.key === 'Enter' ? 'Move committed.' : 'Move cancelled.';
1317
+ this.liveAnnouncer.announce(event.key === 'Enter' ? 'Move committed.' : 'Move cancelled.');
1271
1318
  return;
1272
1319
  }
1273
1320
  if (event.key.startsWith('Arrow')) {
@@ -1276,6 +1323,73 @@ class MintTileManagerElement extends LitElement {
1276
1323
  this.applyKeyboardStep(tile, event.key, isResize);
1277
1324
  }
1278
1325
  }
1326
+ /**
1327
+ * Outside move mode, arrow keys move *focus* between tiles (row-major
1328
+ * order — top-to-bottom, left-to-right by gridcell position). Home/End
1329
+ * jump to first/last enabled tile. Returns true if the event was handled.
1330
+ */
1331
+ handleFocusNavigation(event, current) {
1332
+ const navKey = event.key === 'ArrowUp' || event.key === 'ArrowDown' ||
1333
+ event.key === 'ArrowLeft' || event.key === 'ArrowRight' ||
1334
+ event.key === 'Home' || event.key === 'End';
1335
+ if (!navKey)
1336
+ return false;
1337
+ if (this.tiles.length <= 1)
1338
+ return false;
1339
+ const ordered = [...this.tiles].sort((a, b) => {
1340
+ if (a.position.rowStart !== b.position.rowStart)
1341
+ return a.position.rowStart - b.position.rowStart;
1342
+ return a.position.colStart - b.position.colStart;
1343
+ });
1344
+ const idx = ordered.findIndex((t) => t.id === current.id);
1345
+ if (idx < 0)
1346
+ return false;
1347
+ let nextIdx = idx;
1348
+ if (event.key === 'Home')
1349
+ nextIdx = 0;
1350
+ else if (event.key === 'End')
1351
+ nextIdx = ordered.length - 1;
1352
+ else if (event.key === 'ArrowRight' || event.key === 'ArrowDown')
1353
+ nextIdx = (idx + 1) % ordered.length;
1354
+ else
1355
+ nextIdx = (idx - 1 + ordered.length) % ordered.length;
1356
+ if (nextIdx === idx)
1357
+ return true;
1358
+ event.preventDefault();
1359
+ const target = ordered[nextIdx];
1360
+ this.focusedTileId = target.id;
1361
+ this.requestUpdate();
1362
+ queueMicrotask(() => {
1363
+ const el = this.shadowRoot?.querySelector(`.tile[data-tile-id="${target.id}"]`);
1364
+ el?.focus();
1365
+ });
1366
+ return true;
1367
+ }
1368
+ onTileFocus(tile) {
1369
+ if (this.focusedTileId !== tile.id) {
1370
+ this.focusedTileId = tile.id;
1371
+ this.requestUpdate();
1372
+ }
1373
+ }
1374
+ /**
1375
+ * The single tile that should carry `tabindex="0"` to act as the board's
1376
+ * tab stop. Defaults to the previously-focused tile, then the first tile in
1377
+ * row-major order. All other tiles render with `tabindex="-1"` so Tab from
1378
+ * outside the board lands on exactly one tile.
1379
+ */
1380
+ resolveFocusableTileId() {
1381
+ if (this.tiles.length === 0)
1382
+ return null;
1383
+ if (this.focusedTileId && this.tiles.some((t) => t.id === this.focusedTileId)) {
1384
+ return this.focusedTileId;
1385
+ }
1386
+ const ordered = [...this.tiles].sort((a, b) => {
1387
+ if (a.position.rowStart !== b.position.rowStart)
1388
+ return a.position.rowStart - b.position.rowStart;
1389
+ return a.position.colStart - b.position.colStart;
1390
+ });
1391
+ return ordered[0]?.id ?? null;
1392
+ }
1279
1393
  applyKeyboardStep(tile, key, isResize) {
1280
1394
  const cols = this.effectiveColumnCount;
1281
1395
  const dx = key === 'ArrowLeft' ? -1 : key === 'ArrowRight' ? 1 : 0;
@@ -1295,7 +1409,7 @@ class MintTileManagerElement extends LitElement {
1295
1409
  };
1296
1410
  const result = pack(this.tiles.map((t) => ({ id: t.id, position: t.position, locked: t.disableMove })), { id: tile.id, rect: newRect }, cols);
1297
1411
  if (result.blocked) {
1298
- this.liveRegionMessage = 'Move blocked.';
1412
+ this.liveAnnouncer.announce('Move blocked.');
1299
1413
  return;
1300
1414
  }
1301
1415
  const newTiles = this.tiles.map((t) => {
@@ -1315,9 +1429,9 @@ class MintTileManagerElement extends LitElement {
1315
1429
  composed: true,
1316
1430
  }));
1317
1431
  });
1318
- this.liveRegionMessage = isResize
1432
+ this.liveAnnouncer.announce(isResize
1319
1433
  ? `Tile resized to ${newRect.colSpan} columns by ${newRect.rowSpan} rows`
1320
- : `Tile moved to row ${newRect.rowStart}, column ${newRect.colStart}`;
1434
+ : `Tile moved to row ${newRect.rowStart}, column ${newRect.colStart}`);
1321
1435
  }
1322
1436
  }
1323
1437
  if (typeof customElements !== 'undefined' && !customElements.get('mp-tile-manager')) {