@cahyo-dimas/freeday 1.48.0 → 1.49.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
@@ -3,6 +3,44 @@
3
3
  Semua perubahan penting dicatat di sini. Format longgar mengikuti
4
4
  [Keep a Changelog](https://keepachangelog.com/); tiap versi = git tag.
5
5
 
6
+ ## [1.49.0] — 2026-08-20
7
+ ### Added
8
+ - **`FdyTableColumn.labelHidden`** (#026). A column of row CONTROLS — an edit button, a row menu —
9
+ could be named or quiet, not both: `label` renders as bare text in the `<th>`, so a designer who
10
+ does not want a word above a column of icons was left with an empty header, which assistive tech
11
+ announces as nothing. With `labelHidden: true` the label renders inside `.fdy-visually-hidden`:
12
+ the cell looks empty and the column is still named. The label keeps naming the column's filter
13
+ popover and sort button, so it stays meaningful either way.
14
+ Contract and ALL THREE typed adapters — Vue, React and Blazor. Each renders its own header, so
15
+ each spends the flag itself; Blazor takes `LabelHidden` on `FdyTableColumn<TRow>`.
16
+ ### Added — guards
17
+ - **The column contract now has to reach all three adapters** (#026). Vue and React consume
18
+ `table-model.d.ts` and TypeScript keeps them honest, but Blazor RE-DECLARES the column in C# — a
19
+ hand copy, and the surface that can fall behind silently. A test asserts every contract property
20
+ exists on Blazor's `FdyTableColumn<TRow>`; coverage, not equality, so it needs no exemption list.
21
+ - **A hidden label is clipped, not dropped** — asserted in a real browser for both JS adapters
22
+ (`browser/adapter.mjs`), because the claim is about computed geometry: a span carrying the class
23
+ with no CSS behind it passes any string match and still prints the word.
24
+
25
+ ## [1.48.1] — 2026-08-20
26
+ ### Fixed
27
+ - **A date column sorted correctly and filtered nothing** (#025). `FdyTable`'s client-mode date
28
+ filter read a cell with `dateOnly`, which **sliced** the string, while its date SORT reads the same
29
+ cell with `toTime`, which **parses**. A date column normally renders a formatted date — that is
30
+ what `value` is for — so `"18 Mar 2024"` sliced to `"18 Mar 202"` and compared as text against an
31
+ ISO bound: every row failed, silently. The working sort is what made the broken filter look
32
+ trustworthy.
33
+ - **A `Date` cell was read as its UTC day, not the reader's** (#025). `toISOString()` east of
34
+ Greenwich turns local midnight into the previous day, so at UTC+7 filtering from the 18th dropped
35
+ a row dated the 18th.
36
+ - Text that is not a date now yields `''` and is EXCLUDED by an active date filter, rather than
37
+ slicing to something that compares as less than every bound and matching everything.
38
+ ### Added — guards
39
+ - Three tests in `test/table-model.test.mjs`, each verified to fail against the old body: a
40
+ formatted `value`, a `Date` cell at a positive UTC offset, and unparseable text. The existing date
41
+ test only ever filtered a column with no accessor holding an already-ISO string, which is the one
42
+ shape neither bug can affect.
43
+
6
44
  ## [1.48.0] — 2026-08-20
7
45
  ### Reverted
8
46
  - **1.47.0's readonly focus ring (#024) is withdrawn.** The report was measured wrong. The control in
@@ -26,11 +26,11 @@
26
26
  <th scope="col" style="@AlignStyle(col)" aria-sort="@AriaSortOf(col)">
27
27
  @if (col.Sortable)
28
28
  {
29
- <button type="button" class="fdy-table__sortbtn" @onclick="() => OnSortClick(col)">@col.Label</button>
29
+ <button type="button" class="fdy-table__sortbtn" @onclick="() => OnSortClick(col)"><span class="@(col.LabelHidden ? "fdy-visually-hidden" : null)">@col.Label</span></button>
30
30
  }
31
31
  else
32
32
  {
33
- @col.Label
33
+ <span class="@(col.LabelHidden ? "fdy-visually-hidden" : null)">@col.Label</span>
34
34
  }
35
35
  @if (col.Filter is { } ft)
36
36
  {
@@ -47,6 +47,13 @@ public sealed class FdyTableColumn<TRow>
47
47
  /// <summary>Header label.</summary>
48
48
  public required string Label { get; init; }
49
49
 
50
+ /// <summary>Render the label for assistive tech only — the header cell looks empty.
51
+ /// For a column of row CONTROLS (an edit button, a row menu), where a visible heading is noise
52
+ /// above a column of icons but the column still has to be named: a <c>th</c> with no text is
53
+ /// announced as nothing, and a reader tabbing the header row cannot tell what it is. The label
54
+ /// still names the column's filter popover and its sort button, so it must stay meaningful.</summary>
55
+ public bool LabelHidden { get; init; }
56
+
50
57
  /// <summary>Show a sort toggle in the header.</summary>
51
58
  public bool Sortable { get; init; }
52
59
 
@@ -24,6 +24,16 @@ export interface FdyTableColumn<T> {
24
24
  key: string;
25
25
  /** Header label. */
26
26
  label: string;
27
+ /**
28
+ * Render the label for assistive tech only — the header cell looks empty.
29
+ *
30
+ * For a column of row CONTROLS (an edit button, a row menu), where a visible
31
+ * heading is noise above a column of icons but the column still has to be
32
+ * named: a `<th>` with no text is announced as nothing, and a reader tabbing
33
+ * the header row cannot tell what it is. The label still names the column's
34
+ * filter popover and its sort button, so it must stay meaningful.
35
+ */
36
+ labelHidden?: boolean;
27
37
  /** Show a sort toggle in the header. */
28
38
  sortable?: boolean;
29
39
  /** Offer a column filter of this type. */
@@ -23,10 +23,37 @@ function toTime(v) {
23
23
  return Number.isNaN(t) ? 0 : t;
24
24
  }
25
25
 
26
- /** @param {unknown} v @returns {string} ISO calendar day (yyyy-mm-dd) lexicographically comparable */
26
+ /** Already an ISO calendar day, so it needs no parsing. */
27
+ const ISO_DAY = /^\d{4}-\d{2}-\d{2}/;
28
+
29
+ /** @param {Date} d @returns {string} the LOCAL calendar day, never UTC's */
30
+ function localDay(d) {
31
+ const month = d.getMonth() + 1;
32
+ const day = d.getDate();
33
+ return `${d.getFullYear()}-${month < 10 ? '0' : ''}${month}-${day < 10 ? '0' : ''}${day}`;
34
+ }
35
+
36
+ /**
37
+ * @param {unknown} v @returns {string} ISO calendar day (yyyy-mm-dd) — lexicographically comparable
38
+ *
39
+ * Two bugs lived here, both of which let a column SORT by date correctly and
40
+ * FILTER by date wrongly — the worst pairing, because a working sort is what
41
+ * persuades you the header understands dates.
42
+ *
43
+ * 1. It sliced instead of parsing, while `toTime` (which the date SORT uses)
44
+ * parses. A column whose `value` returns a formatted date — the normal way
45
+ * to render one — gave `"18 Mar 2024"`, sliced to `"18 Mar 202"`, and
46
+ * compared as text against `"2024-03-18"`: every row failed, silently.
47
+ * 2. A `Date` went through `toISOString`, which is UTC. At UTC+7 a date picked
48
+ * as the 18th is `2024-03-17T17:00Z`, so filtering from the 18th dropped it.
49
+ * The calendar day a person means is their own, not Greenwich's.
50
+ */
27
51
  function dateOnly(v) {
28
- if (v instanceof Date) return Number.isNaN(v.getTime()) ? '' : v.toISOString().slice(0, 10);
29
- return String(v == null ? '' : v).slice(0, 10);
52
+ if (v instanceof Date) return Number.isNaN(v.getTime()) ? '' : localDay(v);
53
+ const s = String(v == null ? '' : v);
54
+ if (ISO_DAY.test(s)) return s.slice(0, 10);
55
+ const t = Date.parse(s);
56
+ return Number.isNaN(t) ? '' : localDay(new Date(t));
30
57
  }
31
58
 
32
59
  /** Raw cell value for a column: the column accessor if present, else row[key]. */
@@ -271,9 +271,11 @@ export function FdyTable<Row extends object>(props: FdyTableProps<Row>): JSX.Ele
271
271
  {props.columns.map((col: FdyTableColumn<Row>): JSX.Element => (
272
272
  <th key={col.key} scope="col" style={alignStyle(col)} aria-sort={ariaSortOf(col)}>
273
273
  {col.sortable ? (
274
- <button type="button" className="fdy-table__sortbtn" onClick={(): void => onSort(col)}>{col.label}</button>
274
+ <button type="button" className="fdy-table__sortbtn" onClick={(): void => onSort(col)}>
275
+ <span className={col.labelHidden ? 'fdy-visually-hidden' : undefined}>{col.label}</span>
276
+ </button>
275
277
  ) : (
276
- col.label
278
+ <span className={col.labelHidden ? 'fdy-visually-hidden' : undefined}>{col.label}</span>
277
279
  )}
278
280
  {col.filter && (
279
281
  <FdyTableFilter
@@ -280,8 +280,8 @@ function isExpanded(row: Row): boolean {
280
280
  type="button"
281
281
  class="fdy-table__sortbtn"
282
282
  @click="onSort(col)"
283
- >{{ col.label }}</button>
284
- <template v-else>{{ col.label }}</template>
283
+ ><span :class="col.labelHidden ? 'fdy-visually-hidden' : undefined">{{ col.label }}</span></button>
284
+ <span v-else :class="col.labelHidden ? 'fdy-visually-hidden' : undefined">{{ col.label }}</span>
285
285
  <FdyTableFilter
286
286
  v-if="col.filter"
287
287
  :label="col.label"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cahyo-dimas/freeday",
3
- "version": "1.48.0",
3
+ "version": "1.49.0",
4
4
  "description": "Freeday — token-driven, framework-agnostic UI KIT (design source-of-truth).",
5
5
  "type": "module",
6
6
  "license": "MIT",