@iyulab/flex-table 0.49.0 → 0.49.1

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.49.1] - 2026-10-06
4
+
5
+ ### Fixed
6
+
7
+ - **The column filter is a dialog with a keyboard contract.** It is announced as a dialog named for its
8
+ column ("Filter Customer"); Tab and Shift+Tab stay inside it (it floats over the table, so focus that
9
+ left it would leave it open behind the next control); Escape closes it and returns focus to that
10
+ column's menu button — before, focus dropped to the page. A click outside still closes it without
11
+ moving focus.
12
+
3
13
  ## [0.49.0] - 2026-10-06
4
14
 
5
15
  ### Added
package/README.md CHANGED
@@ -519,6 +519,7 @@ conformance claim for the success criteria it does not list.
519
519
  |---|---|---|
520
520
  | SC 2.5.8 Target Size (Minimum) | Sortable headers, column menu buttons, the open column menu, the open filter dropdown (text and number), the find/replace bar, the cell context menu and row selection checkboxes are at least 24×24 CSS px or meet the spacing exception (24px between centers, counting the neighbouring resize handles), and are actually hit at that position. Two small targets use the equivalent-control exception: the 6px column resize handle (every resize is also in the column menu) and the hidden-column marker (its column menu offers **Show: …**) | `tests/browser/target-size.browser.test.ts` (real Chromium) |
521
521
  | SC 2.1.1 Keyboard (pointer-cursor check) | Nothing the grid renders shows a pointer cursor without being an interactive element. Sorting by header click also has a keyboard path — the column menu's **Sort ascending** / **Sort descending** (the check alone cannot see that, because the header cell wraps the menu button) | `tests/browser/target-size.browser.test.ts` · `src/flex-table.test.ts` |
522
+ | SC 2.4.3 Focus Order · SC 2.1.2 No Keyboard Trap | The column filter is a dialog named for its column: opening it from the column menu moves focus into it, Tab and Shift+Tab cycle inside it, and Escape closes it and returns focus to that column's menu button (Escape is the way out) | `tests/browser/column-menu.browser.test.ts` |
522
523
  | SC 2.5.7 Dragging Movements | Resizing never requires a drag — the column menu's **Auto-fit width**, **Wider** and **Narrower** are single clicks | `tests/browser/column-menu.browser.test.ts` |
523
524
  | SC 1.3.1 Info and Relationships (virtualized grid) | Only the visible rows and columns are in the DOM, so every row carries `aria-rowindex` and every cell `aria-colindex` (1-based, in display order), and the grid's `aria-rowcount` / `aria-colcount` give the full size — the header row is row 1, data rows start at 2, and the footer row (if any) is last. A screen reader announces "row 621 of 10001" after scrolling instead of counting the rows that happen to be rendered. The footer row's cells are grid cells | `tests/browser/aria-grid-index.browser.test.ts` |
524
525
 
@@ -3186,6 +3186,7 @@ var K = {
3186
3186
  ${o >= 0 ? i`<span class="ft-sort-order">${o + 1}</span>` : ""}
3187
3187
  <button class="ft-column-menu-btn ${s ? "ft-filter-active" : ""}"
3188
3188
  type="button"
3189
+ data-key=${e.key}
3189
3190
  title=${k("columnMenu")}
3190
3191
  aria-label=${k("columnMenuFor", { header: e.label })}
3191
3192
  aria-haspopup="menu"
@@ -3424,11 +3425,28 @@ var K = {
3424
3425
  let s = Math.max(4, Math.min(n.left, i - r.width - 4));
3425
3426
  e.style.left = `${s}px`, e.style.top = `${o}px`;
3426
3427
  }
3428
+ _closeFilterDropdown(e = !0) {
3429
+ 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
+ });
3433
+ }
3434
+ _onFilterDialogKeydown(e) {
3435
+ if (e.key !== "Tab") return;
3436
+ let t = [...e.currentTarget.querySelectorAll("input, select, textarea, button, u-date-picker")].filter((e) => !e.disabled && e.getClientRects().length > 0);
3437
+ if (t.length === 0) return;
3438
+ let n = e.composedPath(), r = t.findIndex((e) => n.includes(e)), i = t.length - 1;
3439
+ e.shiftKey && r <= 0 ? (e.preventDefault(), t[i].focus()) : !e.shiftKey && (r === i || r === -1) && (e.preventDefault(), t[0].focus());
3440
+ }
3427
3441
  _renderFilterDropdown(e) {
3428
3442
  let t = e.type ?? "text";
3429
3443
  return i`
3430
- <div class="ft-filter-dropdown" role="group" aria-label=${k("filterFor", { header: e.label })}
3431
- @click=${(e) => e.stopPropagation()}>
3444
+ <div class="ft-filter-dropdown" role="dialog" aria-label=${k("filterFor", { header: e.label })}
3445
+ @click=${(e) => e.stopPropagation()}
3446
+ @keydown=${{
3447
+ handleEvent: (e) => this._onFilterDialogKeydown(e),
3448
+ capture: !0
3449
+ }}>
3432
3450
  ${t === "boolean" ? this._renderBooleanFilter(e) : t === "number" ? this._renderNumberFilter(e) : t === "date" || t === "datetime" ? this._renderDateFilter(e) : this._renderTextFilter(e)}
3433
3451
  <div class="ft-filter-actions">
3434
3452
  <button class="ft-filter-clear" @click=${() => this._clearColumnFilter(e.key)}>${k("clear")}</button>
@@ -3472,7 +3490,7 @@ var K = {
3472
3490
  (r === "" || r === "empty" || r === "non-empty") && (r ? this._emptyFilterState.set(e, r) : this._emptyFilterState.delete(e), t?.(), this._applyFilterForKey(e));
3473
3491
  }}
3474
3492
  @keydown=${(e) => {
3475
- e.key === "Escape" && (this._openFilterKey = null), e.stopPropagation();
3493
+ e.key === "Escape" && this._closeFilterDropdown(), e.stopPropagation();
3476
3494
  }}>
3477
3495
  <option value="">${k("blankAll")}</option>
3478
3496
  <option value="empty">${k("emptyOnly")}</option>
@@ -3550,7 +3568,7 @@ var K = {
3550
3568
  }), this._applyTextFilter(e.key);
3551
3569
  }}
3552
3570
  @keydown=${(e) => {
3553
- e.key === "Escape" && (this._openFilterKey = null), e.stopPropagation();
3571
+ e.key === "Escape" && this._closeFilterDropdown(), e.stopPropagation();
3554
3572
  }}>
3555
3573
  <option value="contains">${k("contains")}</option>
3556
3574
  <option value="starts">${k("startsWith")}</option>
@@ -3571,7 +3589,7 @@ var K = {
3571
3589
  }), this._emptyFilterState.delete(e.key), this._applyTextFilter(e.key);
3572
3590
  }}
3573
3591
  @keydown=${(e) => {
3574
- e.key === "Escape" && (this._openFilterKey = null), e.stopPropagation();
3592
+ e.key === "Escape" && this._closeFilterDropdown(), e.stopPropagation();
3575
3593
  }}>
3576
3594
  ${this._renderEmptyFilterRow(e.key, () => {
3577
3595
  this._textFilterState.delete(e.key);
@@ -3605,7 +3623,7 @@ var K = {
3605
3623
  }, this._numberFilterState.set(e, i), this._emptyFilterState.delete(e), this._applyFilterForKey(e);
3606
3624
  }}
3607
3625
  @keydown=${(e) => {
3608
- e.key === "Escape" && (this._openFilterKey = null), e.stopPropagation();
3626
+ e.key === "Escape" && this._closeFilterDropdown(), e.stopPropagation();
3609
3627
  }}>
3610
3628
  ${Object.keys(K).map((e) => i`<option value=${e}>${K[e]}</option>`)}
3611
3629
  </select>
@@ -3622,7 +3640,7 @@ var K = {
3622
3640
  }, this._numberFilterState.set(e, i), this._emptyFilterState.delete(e), this._applyFilterForKey(e);
3623
3641
  }}
3624
3642
  @keydown=${(e) => {
3625
- e.key === "Escape" && (this._openFilterKey = null), e.stopPropagation();
3643
+ e.key === "Escape" && this._closeFilterDropdown(), e.stopPropagation();
3626
3644
  }}>
3627
3645
  </div>
3628
3646
  `;
@@ -3639,7 +3657,7 @@ var K = {
3639
3657
  r.join = n, this._numberFilterState.set(e.key, r), this._emptyFilterState.delete(e.key), this._applyFilterForKey(e.key);
3640
3658
  }}
3641
3659
  @keydown=${(e) => {
3642
- e.key === "Escape" && (this._openFilterKey = null), e.stopPropagation();
3660
+ e.key === "Escape" && this._closeFilterDropdown(), e.stopPropagation();
3643
3661
  }}>
3644
3662
  <option value="and">AND</option>
3645
3663
  <option value="or">OR</option>
@@ -3671,7 +3689,7 @@ var K = {
3671
3689
  }
3672
3690
  _renderDateFilter(e) {
3673
3691
  let t = e.type === "datetime", n = this._dateFilterState.get(e.key) ?? {}, r = (e) => e?.slice(0, 10), o = (e) => {
3674
- e.key === "Escape" && $(e.currentTarget) || (e.stopPropagation(), e.key === "Escape" && (this._openFilterKey = null));
3692
+ e.key === "Escape" && $(e.currentTarget) || (e.stopPropagation(), e.key === "Escape" && this._closeFilterDropdown());
3675
3693
  };
3676
3694
  return i`
3677
3695
  <div class="ft-filter-range ft-filter-range-dates">
@@ -3703,7 +3721,7 @@ var K = {
3703
3721
  n === "all" ? this.removeFilter(e.key) : this.setFilter(e.key, (e) => !!e == (n === "true"));
3704
3722
  }}
3705
3723
  @keydown=${(e) => {
3706
- e.key === "Escape" && (this._openFilterKey = null), e.stopPropagation();
3724
+ e.key === "Escape" && this._closeFilterDropdown(), e.stopPropagation();
3707
3725
  }}>
3708
3726
  <option value="all">${k("all")}</option>
3709
3727
  <option value="true">\u2714 ${k("booleanTrue")}</option>
@@ -484,6 +484,18 @@ export declare class FlexTable extends LitElement {
484
484
  /** Keep a fixed-position menu inside the viewport — a menu opened near the right or bottom edge would spill off-screen. */
485
485
  private _keepMenuInView;
486
486
  private _adjustFilterDropdown;
487
+ /**
488
+ * Close the filter dialog. From the keyboard (Escape) focus goes back to the column's menu button — the
489
+ * dialog leaves the DOM, and focus inside it would otherwise drop to the page (APG dialog). A click
490
+ * outside closes it without moving focus: the click already put focus where the user meant.
491
+ */
492
+ private _closeFilterDropdown;
493
+ /**
494
+ * Tab stays inside the filter dialog (it floats over the table, fixed to the header — a focus that left
495
+ * it would leave it open and unreachable behind the next control). Capture phase: the controls stop
496
+ * their own keydowns from reaching the grid.
497
+ */
498
+ private _onFilterDialogKeydown;
487
499
  private _renderFilterDropdown;
488
500
  private _buildTextPredicate;
489
501
  private _applyTextFilter;
@@ -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-BsxF9Q3O.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-CpAhHipx.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-BsxF9Q3O.js";
1
+ import { t as e } from "./flex-table-CpAhHipx.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.0",
3
+ "version": "0.49.1",
4
4
  "description": "A minimalist, input-centric data grid web component",
5
5
  "type": "module",
6
6
  "main": "./dist/flex-table.js",