@iyulab/flex-table 0.40.2 → 0.41.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,58 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.41.0] - 2026-10-03
4
+
5
+ ### Added
6
+
7
+ - **`mergeRepeated` column option — a run of repeated values reads as one merged cell.** For lists of
8
+ child rows under a parent (order lines under an order, boxes under a shipment), the parent's columns
9
+ no longer repeat on every line: the value shows once, on the run's first row, the lines inside the run
10
+ are dropped, and the run keeps one background. `true` merges equal values (empty values never merge);
11
+ a function `(row, previousRow, col) => boolean` decides by any rule — e.g. merge a customer only within
12
+ one order. Every row keeps its own value, so sorting, filtering, copying, export and screen readers
13
+ see each row as before. Rows are compared in display order; the value is drawn again on the first row
14
+ in view and below frozen rows, so scrolling or paging through a run never hides it.
15
+
16
+ ### Fixed
17
+
18
+ - **Number entry reads a decimal comma.** The `number` cell editor and the number filter used the native
19
+ number input, which turns `1,5` into `15` or an empty value depending on the browser. Both are now text
20
+ fields with a decimal keyboard (`inputmode="decimal"`) that read numbers in the active `Locale` — `1,5`
21
+ is 1.5 and `1.234,5` is 1234.5 on a comma-decimal page — and the editor shows the current value with
22
+ that locale's decimal separator. The filter keeps what you typed while you type it and marks a condition
23
+ it cannot read (`aria-invalid`).
24
+ - **Pasting `1,5` into a number column stores 1.5.** Pasted text was read with `Number()`, so a value from a
25
+ comma-decimal spreadsheet stayed text (`"1,5"`) and sorted and summed wrongly. Plain notation (`1e3`) is
26
+ still read; text that is not a number still stays text.
27
+ - **The built-in editor rejects text that is not a number** in a `number` column. It fires
28
+ `validation-error` (`error`: "Enter a number") and keeps the old value, like a validator failure — the
29
+ native input never let such text through, so a text field must not store it.
30
+ - The strict-autocomplete error ("Value must be from the existing list") follows the `Locale` (new keys
31
+ `notANumber` and `notInList` in `flexTableLocale`; `ko` built in).
32
+
33
+ ### Changed
34
+
35
+ - **The `@iyulab/components` peer is `>=1.54.0`** — number parsing uses its `parseNumber`.
36
+
37
+ - The optional `@lit/react` peer is `^1.0.8` (was `^1.0.0`) — the version the React entry is tested
38
+ with.
39
+
40
+ ### Documentation
41
+
42
+ - README: the Accessibility section links the KWCAG 2.2 table in `@iyulab/components`.
43
+
44
+ ## [0.40.3] - 2026-09-30
45
+
46
+ ### Fixed
47
+
48
+ - **Screen readers get the position of a row and a cell in the whole table, not in the rendered part.**
49
+ The grid renders only the visible rows and columns, so without indices a screen reader announced the
50
+ first rendered row as row 1 after scrolling. Rows now carry `aria-rowindex` and cells, column headers
51
+ and footer cells `aria-colindex` (1-based, in display order). `aria-rowcount` now counts the header
52
+ row and the footer row as the ARIA grid pattern defines it (it counted data rows only, so it is one or
53
+ two higher than before). The footer row's cells now have the grid cell role — the footer was a row
54
+ without cells.
55
+
3
56
  ## [0.40.2] - 2026-09-30
4
57
 
5
58
  ### Fixed
@@ -575,8 +628,6 @@ All notable changes to this project will be documented in this file.
575
628
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
576
629
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
577
630
 
578
- ## [Unreleased]
579
-
580
631
  ## [0.23.1] - 2026-08-03
581
632
 
582
633
  ### Fixed
package/README.md CHANGED
@@ -67,7 +67,7 @@ npm install @iyulab/flex-table
67
67
  - **Data Mode** — Client-side or server-side sorting/filtering (`dataMode`)
68
68
  - **Context Menu** — `context-menu` event for custom right-click menus
69
69
  - **React Wrapper** — `@iyulab/flex-table/react` subpath for idiomatic React usage
70
- - **ARIA** — `role="grid"`, `aria-sort`, `aria-selected`, `aria-readonly`, `aria-invalid`, `aria-rowcount`, `aria-colcount`
70
+ - **ARIA** — `role="grid"`, `aria-sort`, `aria-selected`, `aria-readonly`, `aria-invalid`, `aria-rowcount`, `aria-colcount`, `aria-rowindex`, `aria-colindex`
71
71
 
72
72
  ## Sizing
73
73
 
@@ -150,6 +150,7 @@ interface ColumnDefinition {
150
150
  editor?: CellEditor; // Custom cell editor: (value, row, col) => TemplateResult
151
151
  validator?: CellValidator; // Validate before commit: (value, row, col) => string | null
152
152
  conditionalRules?: ConditionalRule[]; // Per-cell style rules, see below
153
+ mergeRepeated?: boolean | ((row, previousRow, col) => boolean); // Merge runs of repeated values, see below
153
154
  }
154
155
  ```
155
156
 
@@ -157,6 +158,8 @@ The `editor` callback must return a Lit `TemplateResult` containing an input ele
157
158
 
158
159
  The `validator` callback returns `null` if valid, or an error message string. On failure, the cell shows a red border for 3 seconds and a `validation-error` event is dispatched.
159
160
 
161
+ A `number` column's built-in editor reads numbers the way people type them in the active `Locale` — `1,5` on a comma-decimal page is 1.5, `1.234,5` is 1234.5 — and shows the value with that locale's decimal separator. Text that is not a number is rejected the same way as a validator failure (`error` is the localized "Enter a number"). The number filter's conditions and pasted values are read the same way; pasted text that is not a number stays text.
162
+
160
163
  ### `format` vs `render`
161
164
 
162
165
  Both control how a cell's raw value is displayed, but they differ in what they replace:
@@ -193,6 +196,22 @@ const columns: ColumnDefinition<Order>[] = [
193
196
 
194
197
  Rules are evaluated in order and combined; later matching rules override earlier ones for overlapping style properties.
195
198
 
199
+ ### Merging Repeated Values
200
+
201
+ A list of child rows under a parent — order lines under an order, boxes under a shipment — repeats the parent's columns on every line. `mergeRepeated` draws each run of repeated values as one merged cell: the value shows once, on the run's first row, and the lines between the run's rows are dropped.
202
+
203
+ ```typescript
204
+ const columns: ColumnDefinition<OrderLine>[] = [
205
+ { key: 'orderNo', label: 'Order', mergeRepeated: true },
206
+ // Two adjacent orders can share a customer — merge only within one order.
207
+ { key: 'customer', label: 'Customer', mergeRepeated: (row, prev) => row.orderNo === prev.orderNo },
208
+ { key: 'product', label: 'Product' },
209
+ { key: 'qty', label: 'Qty', type: 'number' },
210
+ ];
211
+ ```
212
+
213
+ Every row keeps its own value. Sorting, filtering, copying, CSV/XLSX export and screen readers see each row exactly as without merging — a filter that drops a run's first row leaves the rest of the run labelled, and an export pivots. Rows are compared in display order, so sort by the merged column (or keep the server's order) for runs to form. The value is drawn again on the first row in view and on the first row below frozen rows, so scrolling or paging through a run never hides it. With `true`, empty values (`null`, `undefined`, `''`) never merge.
214
+
196
215
  ## Methods
197
216
 
198
217
  ### Row Operations
@@ -209,7 +228,7 @@ Rules are evaluated in order and combined; later matching rules override earlier
209
228
  | Method | Returns | Description |
210
229
  |--------|---------|-------------|
211
230
  | `addColumn(def, index?)` | `ColumnDefinition` | Add column at position (default: end) |
212
- | `deleteColumn(key)` | `void` | Remove column + cleanup filters/sort/widths |
231
+ | `deleteColumn(key)` | `void` | Remove column + cleanup filters/sort/widths. Row objects keep that key's values (undo restores the column with them); delete the key from `data` yourself if you need it gone |
213
232
  | `moveColumn(key, newIndex)` | `void` | Reorder column to target index (clamped) |
214
233
  | `getColumnWidth(key)` | `number \| undefined` | Get internal resize width for column |
215
234
  | `selectColumn(colIndex)` | `void` | Select entire column (range selection) |
@@ -257,7 +276,7 @@ All events use `CustomEvent` with `bubbles: true, composed: true`.
257
276
  | `cell-edit-start` | `{ row, col, key, value }` | Cell editing started |
258
277
  | `cell-edit-commit` | `{ row, col, key, oldValue, newValue }` | Cell value committed |
259
278
  | `cell-edit-cancel` | `{ row, col }` | Cell edit cancelled (Escape) |
260
- | `validation-error` | `{ row, col, key, value, error }` | Cell validator rejected value |
279
+ | `validation-error` | `{ row, col, key, value, error }` | Cell validator rejected value, or a `number` cell got text that is not a number |
261
280
 
262
281
  ### Data Events
263
282
 
@@ -471,10 +490,13 @@ conformance claim for the success criteria it does not list.
471
490
  | 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) |
472
491
  | 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` |
473
492
  | 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` |
493
+ | 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` |
474
494
 
475
495
  Not yet measured: the boolean and date filter dropdowns and the comment popup. Color contrast comes
476
496
  from the `@iyulab/components` tokens this package reads.
477
497
 
498
+ For **KWCAG 2.2** (the Korean web accessibility standard), the `@iyulab/components` README has a table of all 33 check items — which are guaranteed by a test across the sibling packages, which are shared with the app, and which do not apply: [KWCAG 2.2 대응표](https://github.com/iyulab/node-components#kwcag-22-대응표).
499
+
478
500
  ## Usage Guide
479
501
 
480
502
  ### React
@@ -6,3 +6,8 @@ import type { ColumnDefinition, DataRow } from '../models/types.js';
6
6
  */
7
7
  export declare function copyToClipboard(data: DataRow[], columns: ColumnDefinition[], range: CellRange): string;
8
8
  export declare function parseValueForColumn(raw: string, col: ColumnDefinition): unknown;
9
+ /**
10
+ * A number as a person edits it: the active locale's decimal separator, no grouping, every digit
11
+ * kept (`1234.5` → `1234,5` on a German page). The inverse is {@link parseValueForColumn}.
12
+ */
13
+ export declare function editableNumber(value: number): string;