@iyulab/flex-table 0.34.0 → 0.34.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,34 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.34.1] - 2026-09-10
4
+
5
+ ### Changed
6
+
7
+ - **Fallback literals for `--u-txt-color-weak` follow `@iyulab/components` 1.40.0.** They are
8
+ only used when that stylesheet is not loaded; with it, nothing changes here.
9
+
10
+ ### Fixed
11
+
12
+ - **A column pinned to the right was placed against the scrolled content instead of the
13
+ viewport, so it sat outside the visible area exactly when a pin is needed.** Cells are
14
+ positioned absolutely inside a box as wide as the whole row, so the emitted
15
+ `right: ${-scrollLeft + offset}` measured from the content's right edge rather than the
16
+ viewport's: at `scrollLeft: 0` the column rendered past the right border of the scroll area
17
+ and never came into view. The negative offset also pushed the cell beyond that box, which
18
+ extended the scrollable width — measured in Chromium, `scrollWidth` grew 968 -> 1160 -> 1543
19
+ across three scroll steps, so the horizontal scrollbar could not reach its own end.
20
+
21
+ Right pins are now expressed the same way left pins already were, as a `left` offset that
22
+ tracks the viewport, clamped to the column's natural position. A right-pinned column stays
23
+ against the right edge of the scroll area at every scroll offset, several of them stack in
24
+ column order, and a table whose columns fit without overflowing leaves the column where it
25
+ naturally sits rather than inventing a scrollable area. Header, body and footer cells all
26
+ take the same path.
27
+
28
+ `position: sticky` — the usual way to build a pinned column — is not available in this
29
+ layout: every cell is absolutely positioned for horizontal virtualization, and a sticky box
30
+ needs a flow position to stick relative to.
31
+
3
32
  ## [0.34.0] - 2026-09-08
4
33
 
5
34
  ### Fixed
package/README.md CHANGED
@@ -19,7 +19,8 @@ npm install @iyulab/flex-table
19
19
  ## Quick Start
20
20
 
21
21
  ```html
22
- <flex-table id="table" row-height="32" show-row-numbers></flex-table>
22
+ <!-- The table is its own scroll container, so give it a height — see Sizing below. -->
23
+ <flex-table id="table" style="height: 400px" row-height="32" show-row-numbers></flex-table>
23
24
 
24
25
  <script type="module">
25
26
  import '@iyulab/flex-table';
@@ -67,6 +68,31 @@ npm install @iyulab/flex-table
67
68
  - **React Wrapper** — `@iyulab/flex-table/react` subpath for idiomatic React usage
68
69
  - **ARIA** — `role="grid"`, `aria-sort`, `aria-selected`, `aria-readonly`, `aria-invalid`, `aria-rowcount`, `aria-colcount`
69
70
 
71
+ ## Sizing
72
+
73
+ The table is its own scroll container (`overflow: auto` on the host) and it measures the viewport
74
+ from the host's own height. **Give it a height** — directly, or through a constrained parent:
75
+
76
+ ```css
77
+ flex-table { height: 400px; }
78
+ ```
79
+
80
+ ```css
81
+ /* or let a flex parent constrain it */
82
+ .page { height: 100%; display: flex; flex-direction: column; }
83
+ .page flex-table { flex: 1 1 auto; min-height: 0; }
84
+ ```
85
+
86
+ ⚠ **Without a height constraint the virtual scroll has nothing to virtualise against.** The body
87
+ carries the full content height, so an unconstrained host grows to match it and the viewport
88
+ measurement ends up equal to the content — every row is rendered. Measured with 1,000 rows and no
89
+ height: the element becomes ~32,000px tall and all 1,000 rows are in the DOM. The same data in a
90
+ 300px-tall host renders only the visible window (well under a fifth of the rows). That is the
91
+ difference between the "100,000+ rows" claim in Features and a page that stalls.
92
+
93
+ Small fixed data sets are fine unconstrained — the point is that the virtual-scroll guarantee is a
94
+ guarantee about a *constrained* host. `height-model.browser.test.ts` pins both sides of this.
95
+
70
96
  ## Properties
71
97
 
72
98
  | Property | Attribute | Type | Default | Description |
@@ -41,12 +41,12 @@ var l = r`
41
41
  --ft-active-color: var(--u-primary-color, #1976D2);
42
42
  --ft-selection-bg: var(--u-primary-bg-color, #E3F2FD);
43
43
  --ft-bool-color: var(--u-primary-color, #1976D2);
44
- --ft-sort-indicator-color: var(--u-txt-color-weak, #757575);
44
+ --ft-sort-indicator-color: var(--u-txt-color-weak, #616161);
45
45
  --ft-editor-bg: var(--u-input-bg-color, #FFFFFF);
46
46
  /* #999 was 2.85 against --ft-bg (#fff); AA needs 4.5. The dark value (#9aa0a6 on
47
47
  #1e1e2e = 6.31) was already fine, so this was a light-only defect. #5f6368 is the
48
48
  same grey the sort indicator uses, which keeps the palette to one family. */
49
- --ft-empty-color: var(--u-txt-color-weak, #757575);
49
+ --ft-empty-color: var(--u-txt-color-weak, #616161);
50
50
 
51
51
  /* 상태 색 — 종전에는 규칙 안에 리터럴로 박혀 있어 소비자가 덮을 경로가 없었고
52
52
  테마도 따라오지 않았다(다크에서 라이트용 값 그대로).
@@ -85,9 +85,9 @@ var l = r`
85
85
  --ft-active-color: var(--u-primary-color, #1976D2);
86
86
  --ft-selection-bg: var(--u-primary-bg-color, #E3F2FD);
87
87
  --ft-bool-color: var(--u-primary-color, #1976D2);
88
- --ft-sort-indicator-color: var(--u-txt-color-weak, #757575);
88
+ --ft-sort-indicator-color: var(--u-txt-color-weak, #616161);
89
89
  --ft-editor-bg: var(--u-input-bg-color, #FFFFFF);
90
- --ft-empty-color: var(--u-txt-color-weak, #757575);
90
+ --ft-empty-color: var(--u-txt-color-weak, #616161);
91
91
  }
92
92
  }
93
93
 
@@ -106,9 +106,9 @@ var l = r`
106
106
  --ft-active-color: var(--u-primary-color, #1976D2);
107
107
  --ft-selection-bg: var(--u-primary-bg-color, #E3F2FD);
108
108
  --ft-bool-color: var(--u-primary-color, #1976D2);
109
- --ft-sort-indicator-color: var(--u-txt-color-weak, #757575);
109
+ --ft-sort-indicator-color: var(--u-txt-color-weak, #616161);
110
110
  --ft-editor-bg: var(--u-input-bg-color, #FFFFFF);
111
- --ft-empty-color: var(--u-txt-color-weak, #757575);
111
+ --ft-empty-color: var(--u-txt-color-weak, #616161);
112
112
  }
113
113
 
114
114
  /* --- Layout --- */
@@ -1497,7 +1497,7 @@ async function K(e) {
1497
1497
  };
1498
1498
  }
1499
1499
  //#endregion
1500
- //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
1500
+ //#region \0@oxc-project+runtime@0.149.0/helpers/esm/decorate.js
1501
1501
  function q(e, t, n, r) {
1502
1502
  var i = arguments.length, a = i < 3 ? t : r === null ? r = Object.getOwnPropertyDescriptor(t, n) : r, o;
1503
1503
  if (typeof Reflect == "object" && typeof Reflect.decorate == "function") a = Reflect.decorate(e, t, n, r);
@@ -2787,6 +2787,12 @@ var J = {
2787
2787
  for (let r = t.length - 1; r > e; r--) t[r].pinned === "right" && (n += this._getColWidth(t[r]));
2788
2788
  return n;
2789
2789
  }
2790
+ _getPinnedRightLeft(e) {
2791
+ let t = this.visibleColumns, n = this._colLeftOffsets[e] ?? 0;
2792
+ if (this._viewportWidth <= 0) return n;
2793
+ let r = this._getColWidth(t[e]), i = this._scrollLeft + this._viewportWidth - this._getPinnedRight(e) - r;
2794
+ return Math.min(n, i);
2795
+ }
2790
2796
  _renderHeaderCell(e, t) {
2791
2797
  let n = e.sortable !== !1, r = this._sortCriteria.find((t) => t.key === e.key), o = this._sortCriteria.length > 1 ? this._sortCriteria.findIndex((t) => t.key === e.key) : -1, s = this._filters.some((t) => t.key === e.key), c = e.pinned === "left", l = e.pinned === "right", u = [
2792
2798
  "ft-header-cell",
@@ -2795,7 +2801,7 @@ var J = {
2795
2801
  e.headerAlign === "center" ? "ft-header-align-center" : "",
2796
2802
  e.headerAlign === "end" ? "ft-header-align-end" : ""
2797
2803
  ].filter(Boolean).join(" "), d = n ? r ? r.direction === "asc" ? "ascending" : "descending" : "none" : void 0, f = this._getColWidth(e), p = this.headerHeight, m = this._colLeftOffsets[t] ?? 0, h;
2798
- h = c ? `position: absolute; top: 0; left: ${this._scrollLeft + this._getPinnedLeft(t)}px; width: ${f}px; height: ${p}px; z-index: 4;` : l ? `position: absolute; top: 0; right: ${-this._scrollLeft + this._getPinnedRight(t)}px; width: ${f}px; height: ${p}px; z-index: 4;` : `left: ${m}px; width: ${f}px; height: ${p}px;`;
2804
+ h = c ? `position: absolute; top: 0; left: ${this._scrollLeft + this._getPinnedLeft(t)}px; width: ${f}px; height: ${p}px; z-index: 4;` : l ? `position: absolute; top: 0; left: ${this._getPinnedRightLeft(t)}px; width: ${f}px; height: ${p}px; z-index: 4;` : `left: ${m}px; width: ${f}px; height: ${p}px;`;
2799
2805
  let g = this._colDrag?.active && this._colDrag.colIndex === t, _ = [...u.split(" "), g ? "ft-col-dragging" : ""].filter(Boolean).join(" "), v = this.columns, y = v.findIndex((t) => t.key === e.key), b = [];
2800
2806
  if (y > 0) for (let e = y - 1; e >= 0 && v[e].hidden; e--) b.push(v[e]);
2801
2807
  let x = b.length > 0;
@@ -3802,7 +3808,7 @@ var J = {
3802
3808
  }
3803
3809
  let { start: g, end: _ } = this.visibleRange, v = this._frozenRowCount, y = [];
3804
3810
  for (let e = g; e < _; e++) y.push(this._renderRow(e, s, c, l, (e - v) * this.rowHeight));
3805
- let b = this._renderFillHandle(), x = this._rowDragIndicatorY == null ? "" : i`<div class="ft-row-drop-indicator" style="top:${this._rowDragIndicatorY}px;width:${o + this._prefixWidth}px;"></div>`;
3811
+ let b = this._renderFillHandle(), x = this._rowDragIndicatorY == null ? "" : i`<div class="ft-row-drop-indicator" style="top:${this._rowDragIndicatorY}px;width:${o}px;"></div>`;
3806
3812
  return i`
3807
3813
  ${t}
3808
3814
  ${n}
@@ -3852,7 +3858,7 @@ var J = {
3852
3858
  let u = this.showRowNumbers ? i`<div class="ft-footer-cell ft-row-num"
3853
3859
  style="position: absolute; top: 0; left: ${s + c}px; width: 48px; height: ${a}px; z-index: 2;"></div>` : "", d = [];
3854
3860
  for (let e of n) {
3855
- let t = r[e], n = this._getColWidth(t), o = t.pinned === "right" ? `position: absolute; top: 0; right: ${-s + this._getPinnedRight(e)}px; width: ${n}px; height: ${a}px; z-index: 2;` : `position: absolute; top: 0; left: ${s + this._getPinnedLeft(e)}px; width: ${n}px; height: ${a}px; z-index: 2;`;
3861
+ let t = r[e], n = this._getColWidth(t), o = t.pinned === "right" ? `position: absolute; top: 0; left: ${this._getPinnedRightLeft(e)}px; width: ${n}px; height: ${a}px; z-index: 2;` : `position: absolute; top: 0; left: ${s + this._getPinnedLeft(e)}px; width: ${n}px; height: ${a}px; z-index: 2;`;
3856
3862
  d.push(i`
3857
3863
  <div class="ft-footer-cell ft-pinned" style=${o}>
3858
3864
  ${this.footerData[t.key] ?? ""}</div>
@@ -3901,7 +3907,7 @@ var J = {
3901
3907
  }
3902
3908
  _renderCell(e, t, n, r) {
3903
3909
  let o = this._activeCell?.row === n && this._activeCell?.col === r, s = this._editingCell?.row === n && this._editingCell?.col === r, c = this._selection.isInRange(n, r), l = t.pinned === "left", u = t.pinned === "right", d = l || u, f = this._getColWidth(t), p = this.rowHeight, h = this._colLeftOffsets[r] ?? 0, g;
3904
- g = l ? `position: absolute; top: 0; left: ${this._scrollLeft + this._getPinnedLeft(r)}px; width: ${f}px; height: ${p}px; z-index: 2;` : u ? `position: absolute; top: 0; right: ${-this._scrollLeft + this._getPinnedRight(r)}px; width: ${f}px; height: ${p}px; z-index: 2;` : `left: ${h}px; width: ${f}px; height: ${p}px;`;
3910
+ g = l ? `position: absolute; top: 0; left: ${this._scrollLeft + this._getPinnedLeft(r)}px; width: ${f}px; height: ${p}px; z-index: 2;` : u ? `position: absolute; top: 0; left: ${this._getPinnedRightLeft(r)}px; width: ${f}px; height: ${p}px; z-index: 2;` : `left: ${h}px; width: ${f}px; height: ${p}px;`;
3905
3911
  let _ = !this._isCellEditable(t);
3906
3912
  if (t.conditionalRules && t.conditionalRules.length > 0) {
3907
3913
  let n = e[t.key], r = {};
@@ -380,6 +380,20 @@ export declare class FlexTable extends LitElement {
380
380
  private _getPinnedLeft;
381
381
  /** Calculate cumulative right offset for a right-pinned column */
382
382
  private _getPinnedRight;
383
+ /**
384
+ * X offset for a right-pinned column, in the same content coordinate space the
385
+ * left-pinned branch uses (origin = the row/header box, prefix columns included).
386
+ *
387
+ * A CSS `right` cannot express this. Cells are absolutely positioned inside a box
388
+ * whose width is the full row width, so a fixed `right` rides the content instead of
389
+ * the viewport, and a scroll-compensating negative `right` pushes the cell past that
390
+ * box — which extends the scrollable area, so every scroll widens it again.
391
+ *
392
+ * Sticking is therefore expressed as a `left` clamped to the column's natural offset:
393
+ * the column tracks the viewport's right edge while the content is scrolled past it,
394
+ * and rests in place once the viewport reaches it.
395
+ */
396
+ private _getPinnedRightLeft;
383
397
  private _renderHeaderCell;
384
398
  private _onHeaderContextMenu;
385
399
  private _showHiddenBefore;
@@ -1,2 +1,2 @@
1
- import { a as e, i as t, n, r, t as i } from "./flex-table-D4W1C_QA.js";
1
+ import { a as e, i as t, n, r, t as i } from "./flex-table-hkmpmDxR.js";
2
2
  export { i as FlexTable, t as RowSelectionState, r as UndoStack, n as exportData, e as renderCell };
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as e } from "./flex-table-D4W1C_QA.js";
1
+ import { t as e } from "./flex-table-hkmpmDxR.js";
2
2
  import t from "react";
3
3
  import { createComponent as n } from "@lit/react";
4
4
  var r = n({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iyulab/flex-table",
3
- "version": "0.34.0",
3
+ "version": "0.34.1",
4
4
  "description": "A minimalist, input-centric data grid web component",
5
5
  "type": "module",
6
6
  "main": "./dist/flex-table.js",
@@ -43,7 +43,7 @@
43
43
  "dev": "vite serve demo",
44
44
  "build": "npm run typecheck && vite build && tsc --emitDeclarationOnly",
45
45
  "build:demo": "vite build --config vite.config.demo.ts",
46
- "typecheck": "tsc --noEmit",
46
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
47
47
  "test": "vitest run",
48
48
  "test:watch": "vitest",
49
49
  "test:coverage": "vitest run --coverage",
@@ -91,6 +91,7 @@
91
91
  "@types/react-dom": "^19.2.7",
92
92
  "@typescript-eslint/eslint-plugin": "^8.64.0",
93
93
  "@typescript-eslint/parser": "^8.64.0",
94
+ "@vitest/browser-playwright": "^4.1.10",
94
95
  "eslint": "^10.1.0",
95
96
  "eslint-plugin-lit": "^2.3.1",
96
97
  "jsdom": "^29.1.1",
@@ -98,7 +99,7 @@
98
99
  "react": "^19.2.7",
99
100
  "react-dom": "^19.2.8",
100
101
  "strip-literal": "^3.1.0",
101
- "typescript": "^5.9.3",
102
+ "typescript": "^6.0.2",
102
103
  "vite": "^8.1.4",
103
104
  "vitest": "^4.1.10"
104
105
  }