@iyulab/flex-table 0.49.1 → 0.50.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.50.0] - 2026-10-06
4
+
5
+ ### Fixed
6
+
7
+ - **Tab no longer traps the focus in the table.** Tab past the last cell (Shift+Tab before the first)
8
+ stayed on that cell, so keyboard focus could never leave the table (WCAG 2.1.2). Tab still walks the
9
+ cells; at either end it now moves on to the next (previous) control on the page.
10
+
11
+ ### Added
12
+
13
+ - **The header row is reachable by keyboard.** ArrowUp from the first row moves onto the column's header
14
+ cell; ArrowLeft / ArrowRight / Home / End move along the header and ArrowDown returns to the body. On a
15
+ header cell, Enter or Space sorts (Shift adds the column to the sort), Alt+ArrowDown or the context-menu
16
+ key opens the column menu, and Alt+ArrowLeft / Alt+ArrowRight resize the column. Escape from that menu
17
+ returns to the same header cell. An empty table keeps the header row reachable.
18
+ - Keyboard focus arriving on a table with nothing active activates the first cell, so the first arrow
19
+ key already moves and no Tab is spent entering.
20
+
21
+ ### Changed
22
+
23
+ - **The column menu buttons are no longer Tab stops** (`tabindex="-1"`, like the row checkboxes) — the
24
+ table is one Tab stop and the header row reaches each column's menu. Pages that relied on tabbing to a
25
+ menu button reach it from the header row instead.
26
+
3
27
  ## [0.49.1] - 2026-10-06
4
28
 
5
29
  ### Fixed
package/README.md CHANGED
@@ -437,8 +437,8 @@ package dependency; CSS custom properties are resolved at render time, not impor
437
437
 
438
438
  | Key | Action |
439
439
  |-----|--------|
440
- | Arrow keys | Navigate cells |
441
- | Tab / Shift+Tab | Move to next/previous cell |
440
+ | Arrow keys | Navigate cells (ArrowUp from the first row moves onto the header row) |
441
+ | Tab / Shift+Tab | Move to next/previous cell; past the last (before the first) cell, leave the table |
442
442
  | Enter / F2 | Start editing (Enter on a non-editable cell fires `row-activate`) |
443
443
  | Typing a printable character | Start editing an editable cell with that character |
444
444
  | Escape | Cancel edit / clear selection / close the find panel |
@@ -455,9 +455,15 @@ package dependency; CSS custom properties are resolved at render time, not impor
455
455
  | Ctrl+Shift+Z / Ctrl+Y | Redo |
456
456
  | Alt+ArrowLeft / Alt+ArrowRight | Resize current column (±20px) |
457
457
  | Ctrl+Click header | Select entire column |
458
+ | ArrowLeft / ArrowRight / Home / End (header row) | Move along the header; ArrowDown returns to the first row |
459
+ | Enter / Space (header row) | Sort the column (ascending → descending → none); with Shift, add it to the sort |
460
+ | Alt+ArrowDown or the context-menu key (header row) | Open the column menu |
458
461
  | Enter / Space on a column menu button | Open the column menu |
459
462
  | ArrowUp / ArrowDown / Home / End (column menu) | Move between menu items |
460
- | Escape (column menu) | Close the menu and return focus to its button |
463
+ | Escape (column menu) | Close the menu and return focus to where it was opened from |
464
+
465
+ The table is one Tab stop: keyboard focus arriving on it activates the first cell (the first header cell
466
+ when there are no rows), and the column menu buttons and row checkboxes are reached by keys, not Tab.
461
467
 
462
468
  ## Localization
463
469
 
@@ -159,6 +159,11 @@ var x = r`
159
159
 
160
160
  .ft-header-cell.ft-sortable { cursor: pointer; }
161
161
  .ft-header-cell.ft-sortable:hover { background: var(--ft-header-hover-bg); }
162
+ /* The header cell the keyboard is on (ArrowUp from the first row) — same ring as the active body cell. */
163
+ .ft-header-cell.ft-header-active {
164
+ outline: 2px solid var(--ft-active-color);
165
+ outline-offset: -2px;
166
+ }
162
167
 
163
168
  .ft-header-cell.ft-header-align-center { justify-content: center; }
164
169
  .ft-header-cell.ft-header-align-end { justify-content: flex-end; }
@@ -1887,7 +1892,9 @@ var K = {
1887
1892
  lte: "≤"
1888
1893
  }, q = 120, J = 40, Y = 20, X = 32, Z = 5, Ze = 200, Q = class extends n {
1889
1894
  constructor(...e) {
1890
- super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this._rowHeightFromCss = X, this._targetFloor = 0, this.showRowNumbers = !1, this.theme = void 0, this.maxRows = 0, this.editable = !0, this.showFilters = !1, this.showContextMenu = !1, this.frozenRows = 0, this.emptyMessage = "No data", this.noMatchingMessage = "No matching data", this.loading = !1, this.importEnabled = !1, this.selectable = !1, this.dataMode = "client", this.footerData = null, this.stylesheets = [], this._scrollTop = 0, this._scrollLeft = 0, this._viewportHeight = 0, this._viewportWidth = 0, this._colLeftOffsets = [], this._totalRowWidth = 0, this._baseStyleSheets = [], this._activeCell = null, this._editingCell = null, this._sortCriteria = [], this._selection = new oe(), this._editing = new se(), this._rowSelection = new A(), this._undo = new j(), this._filters = [], this._filteredIndices = [], this._sortedIndices = [], this._openFilterKey = null, this._rowSelectionVersion = 0, this._lastCheckboxRowIndex = null, this._checkboxShiftPending = !1, this._autocompleteState = null, this._headerMenu = null, this._bodyContextMenu = null, this._commentPopup = null, this._viewDirty = !0, this._isDragging = !1, this._wasDrag = !1, this._resizing = null, this._resizeCleanup = null, this._wasHeaderDrag = !1, this._wasRowDrag = !1, this._colDrag = null, this._colDragIndicatorLeft = null, this._fillDrag = null, this._rowDrag = null, this._rowDragIndicatorY = null, this._findState = null, this._columnWidths = /* @__PURE__ */ new Map(), this._hostResizeObserver = null, this._comments = /* @__PURE__ */ new Map(), this._isDragOver = !1, this._warnedUnconstrained = !1, this._onCommentPopupOutsideClick = () => {
1895
+ super(...e), this.columns = [], this._data = [], this._inUndoRedo = !1, this.clearUndoOnDataChange = !1, this.clearSelectionOnDataChange = !1, this._rowHeightFromCss = X, this._targetFloor = 0, this.showRowNumbers = !1, this.theme = void 0, this.maxRows = 0, this.editable = !0, this.showFilters = !1, this.showContextMenu = !1, this.frozenRows = 0, this.emptyMessage = "No data", this.noMatchingMessage = "No matching data", this.loading = !1, this.importEnabled = !1, this.selectable = !1, this.dataMode = "client", this.footerData = null, this.stylesheets = [], this._scrollTop = 0, this._scrollLeft = 0, this._viewportHeight = 0, this._viewportWidth = 0, this._colLeftOffsets = [], this._totalRowWidth = 0, this._baseStyleSheets = [], this._activeCell = null, this._headerCol = null, this._editingCell = null, this._sortCriteria = [], this._selection = new oe(), this._editing = new se(), this._rowSelection = new A(), this._undo = new j(), this._filters = [], this._filteredIndices = [], this._sortedIndices = [], this._openFilterKey = null, this._rowSelectionVersion = 0, this._lastCheckboxRowIndex = null, this._checkboxShiftPending = !1, this._autocompleteState = null, this._headerMenu = null, this._bodyContextMenu = null, this._commentPopup = null, this._viewDirty = !0, this._isDragging = !1, this._wasDrag = !1, this._resizing = null, this._resizeCleanup = null, this._wasHeaderDrag = !1, this._wasRowDrag = !1, this._colDrag = null, this._colDragIndicatorLeft = null, this._fillDrag = null, this._rowDrag = null, this._rowDragIndicatorY = null, this._findState = null, this._columnWidths = /* @__PURE__ */ new Map(), this._hostResizeObserver = null, this._comments = /* @__PURE__ */ new Map(), this._isDragOver = !1, this._warnedUnconstrained = !1, this._onHostFocus = (e) => {
1896
+ e.composedPath()[0] !== this || this._activeCell || this._headerCol !== null || this.visibleColumns.length !== 0 && this.matches(":focus-visible") && (this._visibleRowCount > 0 ? (this._selection.setActive(0, 0), this._syncActiveCell()) : this._headerCol = 0);
1897
+ }, this._onCommentPopupOutsideClick = () => {
1891
1898
  this._commitCommentPopup();
1892
1899
  }, this._invalidCells = /* @__PURE__ */ new Map(), this._textFilterState = /* @__PURE__ */ new Map(), this._numberFilterState = /* @__PURE__ */ new Map(), this._dateFilterState = /* @__PURE__ */ new Map(), this._emptyFilterState = /* @__PURE__ */ new Map();
1893
1900
  }
@@ -2418,10 +2425,10 @@ var K = {
2418
2425
  };
2419
2426
  }
2420
2427
  connectedCallback() {
2421
- super.connectedCallback(), this._onScroll = this._onScroll.bind(this), this._onKeyDown = this._onKeyDown.bind(this), this._onDocumentClick = this._onDocumentClick.bind(this), this._onContextMenu = this._onContextMenu.bind(this), this._onDragover = this._onDragover.bind(this), this._onDragleave = this._onDragleave.bind(this), this._onDrop = this._onDrop.bind(this), this.addEventListener("scroll", this._onScroll, { passive: !0 }), this.addEventListener("keydown", this._onKeyDown), this.addEventListener("contextmenu", this._onContextMenu), this.addEventListener("dragover", this._onDragover), this.addEventListener("dragleave", this._onDragleave), this.addEventListener("drop", this._onDrop), this.hasAttribute("tabindex") || this.setAttribute("tabindex", "0"), this.setAttribute("role", "grid"), typeof ResizeObserver < "u" && (this._hostResizeObserver = new ResizeObserver(() => this._measureViewport()), this._hostResizeObserver.observe(this));
2428
+ super.connectedCallback(), this._onScroll = this._onScroll.bind(this), this._onKeyDown = this._onKeyDown.bind(this), this._onDocumentClick = this._onDocumentClick.bind(this), this._onContextMenu = this._onContextMenu.bind(this), this._onDragover = this._onDragover.bind(this), this._onDragleave = this._onDragleave.bind(this), this._onDrop = this._onDrop.bind(this), this.addEventListener("scroll", this._onScroll, { passive: !0 }), this.addEventListener("keydown", this._onKeyDown), this.addEventListener("focus", this._onHostFocus), this.addEventListener("contextmenu", this._onContextMenu), this.addEventListener("dragover", this._onDragover), this.addEventListener("dragleave", this._onDragleave), this.addEventListener("drop", this._onDrop), this.hasAttribute("tabindex") || this.setAttribute("tabindex", "0"), this.setAttribute("role", "grid"), typeof ResizeObserver < "u" && (this._hostResizeObserver = new ResizeObserver(() => this._measureViewport()), this._hostResizeObserver.observe(this));
2422
2429
  }
2423
2430
  disconnectedCallback() {
2424
- super.disconnectedCallback(), this.removeEventListener("scroll", this._onScroll), this.removeEventListener("keydown", this._onKeyDown), this.removeEventListener("contextmenu", this._onContextMenu), this.removeEventListener("dragover", this._onDragover), this.removeEventListener("dragleave", this._onDragleave), this.removeEventListener("drop", this._onDrop), document.removeEventListener("click", this._onDocumentClick), document.removeEventListener("mousedown", this._onCommentPopupOutsideClick), this._isDragging = !1, this._wasDrag = !1, this._resizeCleanup &&= (this._resizeCleanup(), null), this._hostResizeObserver &&= (this._hostResizeObserver.disconnect(), null);
2431
+ super.disconnectedCallback(), this.removeEventListener("scroll", this._onScroll), this.removeEventListener("keydown", this._onKeyDown), this.removeEventListener("focus", this._onHostFocus), this.removeEventListener("contextmenu", this._onContextMenu), this.removeEventListener("dragover", this._onDragover), this.removeEventListener("dragleave", this._onDragleave), this.removeEventListener("drop", this._onDrop), document.removeEventListener("click", this._onDocumentClick), document.removeEventListener("mousedown", this._onCommentPopupOutsideClick), this._isDragging = !1, this._wasDrag = !1, this._resizeCleanup &&= (this._resizeCleanup(), null), this._hostResizeObserver &&= (this._hostResizeObserver.disconnect(), null);
2425
2432
  }
2426
2433
  firstUpdated() {
2427
2434
  this._readDensityTokens(), this._measureViewport(), this.shadowRoot && Array.isArray(this.shadowRoot.adoptedStyleSheets) && (this._baseStyleSheets = [...this.shadowRoot.adoptedStyleSheets]), this._syncStylesheets();
@@ -2528,28 +2535,34 @@ var K = {
2528
2535
  this._openFilterKey &&= null, this._headerMenu &&= null, this._bodyContextMenu &&= null;
2529
2536
  }
2530
2537
  _onContextMenu(e) {
2531
- let t = e.composedPath().find((e) => e instanceof HTMLElement && e.classList.contains("ft-cell"));
2532
- if (!t) return;
2538
+ let t = e.composedPath(), n = t.find((e) => e instanceof HTMLElement && e.classList.contains("ft-cell"));
2539
+ if (!n) {
2540
+ if (t[0] === this && this._headerCol !== null) {
2541
+ let t = this.visibleColumns[this._headerCol];
2542
+ t && (e.preventDefault(), this._openColumnMenu(t));
2543
+ }
2544
+ return;
2545
+ }
2533
2546
  e.preventDefault();
2534
- let n = t.parentElement;
2535
- if (!n || !n.classList.contains("ft-row")) return;
2536
- let r = parseInt(t.dataset.colIndex ?? "-1", 10), i = n.style.top, a = Math.round(parseInt(i, 10) / this.rowHeight);
2537
- if (r < 0 || a < 0) return;
2538
- let o = this._toDataIndex(a), s = this.visibleColumns[r];
2539
- if (!s) return;
2540
- let c = this._emit("context-menu", {
2547
+ let r = n.parentElement;
2548
+ if (!r || !r.classList.contains("ft-row")) return;
2549
+ let i = parseInt(n.dataset.colIndex ?? "-1", 10), a = r.style.top, o = Math.round(parseInt(a, 10) / this.rowHeight);
2550
+ if (i < 0 || o < 0) return;
2551
+ let s = this._toDataIndex(o), c = this.visibleColumns[i];
2552
+ if (!c) return;
2553
+ let l = this._emit("context-menu", {
2541
2554
  x: e.clientX,
2542
2555
  y: e.clientY,
2543
- row: o,
2544
- col: r,
2545
- key: s.key,
2546
- value: this.data[o]?.[s.key],
2547
- rowData: this.data[o]
2556
+ row: s,
2557
+ col: i,
2558
+ key: c.key,
2559
+ value: this.data[s]?.[c.key],
2560
+ rowData: this.data[s]
2548
2561
  }, { cancelable: !0 });
2549
- this._cancelCommentPopup(), this.showContextMenu && !c.defaultPrevented && (this._bodyContextMenu = {
2550
- rowIndex: a,
2551
- colIndex: r,
2552
- dataIndex: o,
2562
+ this._cancelCommentPopup(), this.showContextMenu && !l.defaultPrevented && (this._bodyContextMenu = {
2563
+ rowIndex: o,
2564
+ colIndex: i,
2565
+ dataIndex: s,
2553
2566
  x: e.clientX,
2554
2567
  y: e.clientY
2555
2568
  }, requestAnimationFrame(() => {
@@ -2775,7 +2788,7 @@ var K = {
2775
2788
  this._autocompleteState = null, this._applyEdit(e), this._selection.moveDown(), this._syncActiveCell();
2776
2789
  }
2777
2790
  _syncActiveCell() {
2778
- this._activeCell = this._selection.activeCell ? { ...this._selection.activeCell } : null, this._scrollToActiveCell(), this._dispatchSelectionEvent();
2791
+ this._activeCell = this._selection.activeCell ? { ...this._selection.activeCell } : null, this._activeCell && (this._headerCol = null), this._scrollToActiveCell(), this._dispatchSelectionEvent();
2779
2792
  }
2780
2793
  _onKeyDown(e) {
2781
2794
  if (this._editing.current || this._openFilterKey && e.composedPath().some((e) => e instanceof Element && e.classList.contains("ft-filter-dropdown"))) return;
@@ -2789,7 +2802,22 @@ var K = {
2789
2802
  "y"
2790
2803
  ].includes(t) && this._handleCtrlKey(e)) return;
2791
2804
  }
2792
- if (t.length !== 0 && this._visibleRowCount !== 0 && (e.composedPath()[0] === this || e.ctrlKey || e.metaKey)) {
2805
+ if (t.length !== 0 && (e.composedPath()[0] === this || e.ctrlKey || e.metaKey)) {
2806
+ if (this._headerCol !== null) {
2807
+ this._onHeaderKeyDown(e, t);
2808
+ return;
2809
+ }
2810
+ if (this._visibleRowCount === 0) {
2811
+ [
2812
+ "ArrowDown",
2813
+ "ArrowUp",
2814
+ "ArrowLeft",
2815
+ "ArrowRight",
2816
+ "Home",
2817
+ "End"
2818
+ ].includes(e.key) && (e.preventDefault(), this._headerCol = 0);
2819
+ return;
2820
+ }
2793
2821
  if ((e.key === "Enter" || e.key === "F2") && this._activeCell && !_(e)) {
2794
2822
  e.preventDefault();
2795
2823
  let n = t[this._activeCell.col];
@@ -2802,7 +2830,6 @@ var K = {
2802
2830
  "ArrowUp",
2803
2831
  "ArrowLeft",
2804
2832
  "ArrowRight",
2805
- "Tab",
2806
2833
  "Home",
2807
2834
  "End"
2808
2835
  ].includes(e.key)) {
@@ -2825,10 +2852,47 @@ var K = {
2825
2852
  e && this._isCellEditable(e) && this._startEdit();
2826
2853
  return;
2827
2854
  }
2855
+ if (e.key === "ArrowUp" && !e.shiftKey && !e.altKey && this._activeCell?.row === 0) {
2856
+ e.preventDefault(), this._enterHeader(this._activeCell.col);
2857
+ return;
2858
+ }
2828
2859
  this._handleNavigation(e) && (e.preventDefault(), this._syncActiveCell());
2829
2860
  }
2830
2861
  }
2831
2862
  }
2863
+ _onHeaderKeyDown(e, t) {
2864
+ if (e.ctrlKey || e.metaKey) return;
2865
+ let n = Math.min(this._headerCol ?? 0, t.length - 1), r = t[n], i;
2866
+ switch (e.key) {
2867
+ case "ArrowLeft":
2868
+ case "ArrowRight":
2869
+ if (e.altKey) {
2870
+ e.preventDefault(), this._resizeColumnBy(r.key, e.key === "ArrowRight" ? Y : -20);
2871
+ return;
2872
+ }
2873
+ i = e.key === "ArrowLeft" ? Math.max(0, n - 1) : Math.min(t.length - 1, n + 1);
2874
+ break;
2875
+ case "Home":
2876
+ i = 0;
2877
+ break;
2878
+ case "End":
2879
+ i = t.length - 1;
2880
+ break;
2881
+ case "ArrowDown":
2882
+ e.preventDefault(), e.altKey ? this._openColumnMenu(r) : this._visibleRowCount > 0 && (this._selection.setActive(0, n), this._syncActiveCell());
2883
+ return;
2884
+ case "Enter":
2885
+ case " ":
2886
+ if (_(e)) return;
2887
+ e.preventDefault(), r.sortable !== !1 && this._toggleSort(r, e.shiftKey);
2888
+ return;
2889
+ default: return;
2890
+ }
2891
+ e.preventDefault(), this._headerCol = i, this._scrollColumnIntoView(i);
2892
+ }
2893
+ _enterHeader(e) {
2894
+ this._selection.clear(), this._syncActiveCell(), this._headerCol = e, this._scrollColumnIntoView(e);
2895
+ }
2832
2896
  _handleCtrlKey(e) {
2833
2897
  if (!(e.ctrlKey || e.metaKey) || e.altKey) return !1;
2834
2898
  switch (e.key.toLowerCase()) {
@@ -2874,7 +2938,10 @@ var K = {
2874
2938
  case "ArrowDown": return e.shiftKey ? this._selection.shiftMoveDown() : this._selection.moveDown(), !0;
2875
2939
  case "ArrowLeft": return e.shiftKey ? this._selection.shiftMoveLeft() : this._selection.moveLeft(), !0;
2876
2940
  case "ArrowRight": return e.shiftKey ? this._selection.shiftMoveRight() : this._selection.moveRight(), !0;
2877
- case "Tab": return e.shiftKey ? this._selection.movePrev() : this._selection.moveNext(), !0;
2941
+ case "Tab": {
2942
+ let t = this._selection.activeCell, n = e.shiftKey ? this._selection.movePrev() : this._selection.moveNext();
2943
+ return !(t && n && t.row === n.row && t.col === n.col);
2944
+ }
2878
2945
  case "Home": return e.ctrlKey ? this._selection.moveToStart() : this._selection.moveToRowStart(), !0;
2879
2946
  case "End": return e.ctrlKey ? this._selection.moveToEnd() : this._selection.moveToRowEnd(), !0;
2880
2947
  case "Escape": return this._findState ? (this._closeFindPanel(), !0) : (this._selection.clear(), !0);
@@ -3102,24 +3169,29 @@ var K = {
3102
3169
  _scrollToActiveCell() {
3103
3170
  if (!this._activeCell) return;
3104
3171
  let e = this._activeCell.row * this.rowHeight + this.headerHeight, t = e + this.rowHeight;
3105
- this._activeCell.row < this._frozenRowCount || (e < this.scrollTop + this.headerHeight + this.frozenRowsHeight ? this.scrollTop = e - this.headerHeight - this.frozenRowsHeight : t > this.scrollTop + this.clientHeight && (this.scrollTop = t - this.clientHeight));
3106
- let n = this.visibleColumns, r = this._activeCell.col, i = this._colLeftOffsets[r] ?? 0, a = i + this._getColWidth(n[r]);
3107
- i < this.scrollLeft ? this.scrollLeft = i : a > this.scrollLeft + this.clientWidth && (this.scrollLeft = a - this.clientWidth);
3172
+ this._activeCell.row < this._frozenRowCount || (e < this.scrollTop + this.headerHeight + this.frozenRowsHeight ? this.scrollTop = e - this.headerHeight - this.frozenRowsHeight : t > this.scrollTop + this.clientHeight && (this.scrollTop = t - this.clientHeight)), this._scrollColumnIntoView(this._activeCell.col);
3173
+ }
3174
+ _scrollColumnIntoView(e) {
3175
+ let t = this.visibleColumns, n = this._colLeftOffsets[e] ?? 0, r = n + this._getColWidth(t[e]);
3176
+ n < this.scrollLeft ? this.scrollLeft = n : r > this.scrollLeft + this.clientWidth && (this.scrollLeft = r - this.clientWidth);
3108
3177
  }
3109
3178
  _dispatchSelectionEvent() {
3110
3179
  this._emit("cell-select", this._activeCell ? { ...this._activeCell } : null);
3111
3180
  }
3112
- _onHeaderClick(t, n) {
3181
+ _onHeaderClick(e, t) {
3113
3182
  if (this._wasHeaderDrag) {
3114
3183
  this._wasHeaderDrag = !1;
3115
3184
  return;
3116
3185
  }
3117
- let r = this.visibleColumns.indexOf(n);
3118
- if (t.ctrlKey || t.metaKey) {
3119
- this._selectColumn(r);
3186
+ let n = this.visibleColumns.indexOf(t);
3187
+ if (e.ctrlKey || e.metaKey) {
3188
+ this._selectColumn(n);
3120
3189
  return;
3121
3190
  }
3122
- n.sortable !== !1 && (this._sortCriteria = e(this._sortCriteria, n.key, t.shiftKey), this.dataMode !== "server" && this._recomputeView(), this.requestUpdate(), this._emit("sort-change", { criteria: [...this._sortCriteria] }));
3191
+ t.sortable !== !1 && this._toggleSort(t, e.shiftKey);
3192
+ }
3193
+ _toggleSort(t, n) {
3194
+ this._sortCriteria = e(this._sortCriteria, t.key, n), this.dataMode !== "server" && this._recomputeView(), this.requestUpdate(), this._emit("sort-change", { criteria: [...this._sortCriteria] });
3123
3195
  }
3124
3196
  _selectColumn(e) {
3125
3197
  let t = this._visibleRowCount;
@@ -3158,7 +3230,8 @@ var K = {
3158
3230
  n ? "ft-sortable" : "",
3159
3231
  u ? "ft-pinned" : "",
3160
3232
  d === "center" ? "ft-header-align-center" : "",
3161
- d === "end" ? "ft-header-align-end" : ""
3233
+ d === "end" ? "ft-header-align-end" : "",
3234
+ this._headerCol === t ? "ft-header-active" : ""
3162
3235
  ].filter(Boolean).join(" "), p = n ? r ? r.direction === "asc" ? "ascending" : "descending" : "none" : void 0, m = this._getColWidth(e), h = this.headerHeight, g = this._colLeftOffsets[t] ?? 0, _;
3163
3236
  _ = c ? `position: absolute; top: 0; left: ${this._scrollLeft + this._getPinnedLeft(t)}px; width: ${m}px; height: ${h}px; z-index: 4;` : l ? `position: absolute; top: 0; left: ${this._getPinnedRightLeft(t)}px; width: ${m}px; height: ${h}px; z-index: 4;` : `left: ${g}px; width: ${m}px; height: ${h}px;`;
3164
3237
  let v = this._colDrag?.active && this._colDrag.colIndex === t, y = [...f.split(" "), v ? "ft-col-dragging" : ""].filter(Boolean).join(" "), b = this.columns, x = b.findIndex((t) => t.key === e.key), S = [];
@@ -3175,7 +3248,7 @@ var K = {
3175
3248
  @mousedown=${(n) => this._onHeaderMouseDown(n, e, t)}
3176
3249
  @click=${n ? (t) => this._onHeaderClick(t, e) : void 0}>
3177
3250
  ${C ? i`
3178
- <button class="ft-hidden-col-indicator"
3251
+ <button class="ft-hidden-col-indicator" tabindex="-1"
3179
3252
  title=${k("showHiddenColumns")}
3180
3253
  @click=${(t) => {
3181
3254
  t.stopPropagation(), this._showHiddenBefore(e);
@@ -3186,6 +3259,7 @@ var K = {
3186
3259
  ${o >= 0 ? i`<span class="ft-sort-order">${o + 1}</span>` : ""}
3187
3260
  <button class="ft-column-menu-btn ${s ? "ft-filter-active" : ""}"
3188
3261
  type="button"
3262
+ tabindex="-1"
3189
3263
  data-key=${e.key}
3190
3264
  title=${k("columnMenu")}
3191
3265
  aria-label=${k("columnMenuFor", { header: e.label })}
@@ -3232,12 +3306,15 @@ var K = {
3232
3306
  this._headerMenu = null;
3233
3307
  return;
3234
3308
  }
3235
- let n = e.currentTarget.getBoundingClientRect();
3309
+ this._openColumnMenu(t, e.currentTarget);
3310
+ }
3311
+ _openColumnMenu(e, t = this._columnMenuButton(e.key)) {
3312
+ let n = t?.getBoundingClientRect();
3236
3313
  this._openFilterKey = null, this._headerMenu = {
3237
- key: t.key,
3238
- x: n.left,
3239
- y: n.bottom,
3240
- hiddenNeighbors: this._hiddenNeighbors(t)
3314
+ key: e.key,
3315
+ x: n?.left ?? 0,
3316
+ y: n?.bottom ?? 0,
3317
+ hiddenNeighbors: this._hiddenNeighbors(e)
3241
3318
  }, requestAnimationFrame(() => {
3242
3319
  document.addEventListener("click", this._onDocumentClick, { once: !0 });
3243
3320
  }), this.updateComplete.then(() => this._menuItems()[0]?.focus());
@@ -3251,7 +3328,7 @@ var K = {
3251
3328
  }
3252
3329
  _closeHeaderMenu(e) {
3253
3330
  let t = this._headerMenu?.key;
3254
- this._headerMenu = null, e && t && this._columnMenuButton(t)?.focus();
3331
+ this._headerMenu = null, e && t && (this._headerCol === null ? this._columnMenuButton(t)?.focus() : this.focus());
3255
3332
  }
3256
3333
  _onHeaderMenuKeydown(e) {
3257
3334
  this._onMenuKeydown(e, ".ft-header-menu", (e) => this._closeHeaderMenu(e));
@@ -3427,9 +3504,15 @@ var K = {
3427
3504
  }
3428
3505
  _closeFilterDropdown(e = !0) {
3429
3506
  let t = this._openFilterKey;
3430
- this._openFilterKey = null, e && t !== null && this.updateComplete.then(() => {
3431
- [...this.shadowRoot?.querySelectorAll(".ft-column-menu-btn") ?? []].find((e) => e.dataset.key === t)?.focus();
3432
- });
3507
+ if (this._openFilterKey = null, e && t !== null) {
3508
+ if (this._headerCol !== null) {
3509
+ this.focus();
3510
+ return;
3511
+ }
3512
+ this.updateComplete.then(() => {
3513
+ [...this.shadowRoot?.querySelectorAll(".ft-column-menu-btn") ?? []].find((e) => e.dataset.key === t)?.focus();
3514
+ });
3515
+ }
3433
3516
  }
3434
3517
  _onFilterDialogKeydown(e) {
3435
3518
  if (e.key !== "Tab") return;
@@ -4486,7 +4569,7 @@ G([s({ type: Array })], Q.prototype, "columns", void 0), G([s({
4486
4569
  })], Q.prototype, "footerData", void 0), G([s({ attribute: !1 })], Q.prototype, "stylesheets", void 0), G([s({
4487
4570
  type: Number,
4488
4571
  attribute: "max-undo-size"
4489
- })], Q.prototype, "maxUndoSize", null), G([c()], Q.prototype, "_scrollTop", void 0), G([c()], Q.prototype, "_scrollLeft", void 0), G([c()], Q.prototype, "_viewportHeight", void 0), G([c()], Q.prototype, "_viewportWidth", void 0), G([c()], Q.prototype, "_activeCell", void 0), G([c()], Q.prototype, "_editingCell", void 0), G([c()], Q.prototype, "_sortCriteria", void 0), G([c()], Q.prototype, "_openFilterKey", void 0), G([c()], Q.prototype, "_rowSelectionVersion", void 0), G([c()], Q.prototype, "_autocompleteState", void 0), G([c()], Q.prototype, "_headerMenu", void 0), G([c()], Q.prototype, "_bodyContextMenu", void 0), G([c()], Q.prototype, "_commentPopup", void 0), G([c()], Q.prototype, "_isDragOver", void 0), Q = G([o("flex-table")], Q);
4572
+ })], Q.prototype, "maxUndoSize", null), G([c()], Q.prototype, "_scrollTop", void 0), G([c()], Q.prototype, "_scrollLeft", void 0), G([c()], Q.prototype, "_viewportHeight", void 0), G([c()], Q.prototype, "_viewportWidth", void 0), G([c()], Q.prototype, "_activeCell", void 0), G([c()], Q.prototype, "_headerCol", void 0), G([c()], Q.prototype, "_editingCell", void 0), G([c()], Q.prototype, "_sortCriteria", void 0), G([c()], Q.prototype, "_openFilterKey", void 0), G([c()], Q.prototype, "_rowSelectionVersion", void 0), G([c()], Q.prototype, "_autocompleteState", void 0), G([c()], Q.prototype, "_headerMenu", void 0), G([c()], Q.prototype, "_bodyContextMenu", void 0), G([c()], Q.prototype, "_commentPopup", void 0), G([c()], Q.prototype, "_isDragOver", void 0), Q = G([o("flex-table")], Q);
4490
4573
  function Qe(e, t) {
4491
4574
  if (t) {
4492
4575
  let t = fe(e);
@@ -108,6 +108,11 @@ export declare class FlexTable extends LitElement {
108
108
  /** The grid's own compiled styles, captured once so `stylesheets` merges without dropping them. */
109
109
  private _baseStyleSheets;
110
110
  private _activeCell;
111
+ /**
112
+ * The header cell the keyboard is on — the header row sits above the first body row (ArrowUp from
113
+ * it). Exclusive with an active body cell: entering one leaves the other.
114
+ */
115
+ private _headerCol;
111
116
  private _editingCell;
112
117
  private _sortCriteria;
113
118
  private _selection;
@@ -394,6 +399,20 @@ export declare class FlexTable extends LitElement {
394
399
  private _selectAutocompleteCandidate;
395
400
  private _syncActiveCell;
396
401
  private _onKeyDown;
402
+ /**
403
+ * The keyboard on the header row: arrows / Home / End move along it, ArrowDown returns to the body,
404
+ * Enter or Space sorts (Shift adds to the sort), Alt+ArrowDown opens the column menu, Alt+Arrow
405
+ * resizes. Tab leaves the grid.
406
+ */
407
+ private _onHeaderKeyDown;
408
+ /** Move the keyboard from the body onto the header cell of visible column `colIndex`. */
409
+ private _enterHeader;
410
+ /**
411
+ * Keyboard focus arriving on a grid with nothing active places it on the first cell (the first header
412
+ * cell when the body is empty), so the first arrow already moves and Tab is not spent entering. A
413
+ * pointer focus places the cell it pressed instead.
414
+ */
415
+ private _onHostFocus;
397
416
  private _handleCtrlKey;
398
417
  private _handleAltKey;
399
418
  /** Widen (positive) or narrow (negative) a visible column by `delta` px, never below its minimum. */
@@ -423,8 +442,12 @@ export declare class FlexTable extends LitElement {
423
442
  private _clearRanges;
424
443
  private _clearRange;
425
444
  private _scrollToActiveCell;
445
+ /** Scroll horizontally so the visible column at `colIndex` is in view — cached column offsets. */
446
+ private _scrollColumnIntoView;
426
447
  private _dispatchSelectionEvent;
427
448
  private _onHeaderClick;
449
+ /** Cycle `col`'s sort (asc → desc → none); `multi` adds it to the existing criteria (Shift). */
450
+ private _toggleSort;
428
451
  private _selectColumn;
429
452
  /** Public API: select an entire column by index. */
430
453
  selectColumn(colIndex: number): void;
@@ -456,6 +479,8 @@ export declare class FlexTable extends LitElement {
456
479
  * and resizing without dragging the 6px handle (WCAG 2.2 SC 2.5.7 / 2.5.8).
457
480
  */
458
481
  private _onColumnMenuBtnClick;
482
+ /** Open `col`'s column menu under `anchor` (its menu button by default) and focus the first item. */
483
+ private _openColumnMenu;
459
484
  private _menuItems;
460
485
  private _columnMenuButton;
461
486
  private _closeHeaderMenu;
@@ -1,2 +1,2 @@
1
- import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./flex-table-CpAhHipx.js";
1
+ import { a as e, i as t, n, o as r, r as i, s as a, t as o } from "./flex-table-B64ScsFY.js";
2
2
  export { o as FlexTable, e as RowSelectionState, t as UndoStack, n as effectiveAlign, i as exportData, r as flexTableLocale, a as renderCell };
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as e } from "./flex-table-CpAhHipx.js";
1
+ import { t as e } from "./flex-table-B64ScsFY.js";
2
2
  import { i as t, t as n } from "./query-Dw4iO0ir.js";
3
3
  import { t as r } from "./view-DiLr-TdY.js";
4
4
  import i, { useCallback as a, useEffect as o, useMemo as s, useRef as c, useState as l } from "react";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/flex-table",
3
- "version": "0.49.1",
3
+ "version": "0.50.0",
4
4
  "description": "A minimalist, input-centric data grid web component",
5
5
  "type": "module",
6
6
  "main": "./dist/flex-table.js",
@@ -68,7 +68,10 @@ flex-table {
68
68
 
69
69
  | Key | Action |
70
70
  |---|---|
71
- | Arrows / Tab / Shift+Tab | Move between cells |
71
+ | Arrows / Tab / Shift+Tab | Move between cells; Tab past the last (Shift+Tab before the first) leaves the table |
72
+ | ArrowUp on the first row | Move onto the header row (arrows / Home / End along it, ArrowDown back) |
73
+ | Enter / Space on a header cell | Sort (Shift adds to the sort) |
74
+ | Alt+ArrowDown or context-menu key on a header cell | Open the column menu |
72
75
  | Home / End, Ctrl+Home / Ctrl+End | Row start/end, table start/end |
73
76
  | Shift+Arrow, Shift+Click | Extend range selection |
74
77
  | Shift+Space | Select / deselect the active cell's row (`selectable`; row checkboxes are not Tab stops) |
@@ -84,6 +87,9 @@ flex-table {
84
87
  | Alt+ArrowLeft / Alt+ArrowRight | Resize current column |
85
88
  | Enter / Space on a column menu button | Open the column menu (arrows, Home/End navigate; Escape closes) |
86
89
 
90
+ The table is one Tab stop — keyboard focus arriving on it activates the first cell; the column menu
91
+ buttons are not Tab stops (the header row reaches them).
92
+
87
93
  Ctrl also matches Cmd on macOS. Editing shortcuts are ignored when `editable` is `false`.
88
94
 
89
95
  ## Localization