@payglocal_ui/flux-ui 0.3.1 → 0.3.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payglocal_ui/flux-ui",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Flux UI primitives — inputs, fields, dialog, data table, charts, calendar, and more (Tailwind v4 + Radix).",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -0,0 +1,77 @@
1
+ import { describe, expect, it, beforeAll } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { DataTable, type Column } from "../data-table";
4
+
5
+ /**
6
+ * A column's `width` is documented to accept `minmax(min, max)`, but the
7
+ * `<col>` it lands on is a real table column, not a CSS Grid track —
8
+ * `width: minmax(...)` is invalid CSS and the whole declaration is dropped
9
+ * silently, leaving the column with no floor at all. On a narrow viewport
10
+ * that squeezed the column down to nothing, and the header's deliberately
11
+ * un-truncated text spilled into the next column's header — "Batch" and
12
+ * "Currency" rendering as "Batchency".
13
+ */
14
+
15
+ beforeAll(() => {
16
+ if (!(globalThis as any).ResizeObserver) {
17
+ (globalThis as any).ResizeObserver = class {
18
+ observe() {}
19
+ unobserve() {}
20
+ disconnect() {}
21
+ };
22
+ }
23
+ });
24
+
25
+ interface Row {
26
+ id: string;
27
+ name: string;
28
+ }
29
+
30
+ const DATA: Row[] = [{ id: "1", name: "Row one" }];
31
+
32
+ function columns(width: string): Column<Row>[] {
33
+ return [
34
+ { key: "id", header: "Batch", width, render: (r) => r.id },
35
+ { key: "name", header: "Currency", width: "84px", render: (r) => r.name },
36
+ ];
37
+ }
38
+
39
+ /** The `<col>` for a given column key, inside the fixed-layout `<colgroup>`. */
40
+ function colFor(key: string, container: HTMLElement) {
41
+ const th = screen.getAllByRole("columnheader").find((h) => h.textContent === key);
42
+ expect(th).toBeTruthy();
43
+ const index = Array.from(th!.parentElement!.children).indexOf(th!);
44
+ const cols = container.querySelectorAll("colgroup col");
45
+ return cols[index] as HTMLTableColElement;
46
+ }
47
+
48
+ describe("DataTable minmax() column widths", () => {
49
+ it("gives a minmax(min, 1fr) column a real min-width instead of dropping it", () => {
50
+ const { container } = render(<DataTable columns={columns("minmax(250px, 1fr)")} data={DATA} rowKey={(r) => r.id} />);
51
+
52
+ const col = colFor("Batch", container);
53
+ // The floor survives as `min-width` — a legal CSS property — rather than
54
+ // being lost inside an illegal `width: minmax(...)` declaration.
55
+ expect(col.style.minWidth).toBe("250px");
56
+ // `1fr` is a Grid unit and means nothing as a table `width`; leaving it
57
+ // unset is what lets the column take whatever `table-layout: fixed`
58
+ // leaves over, the same as a grid track's `1fr` would.
59
+ expect(col.style.width).toBe("");
60
+ });
61
+
62
+ it("gives a minmax(min, max) column both bounds as real CSS", () => {
63
+ const { container } = render(<DataTable columns={columns("minmax(230px, 260px)")} data={DATA} rowKey={(r) => r.id} />);
64
+
65
+ const col = colFor("Batch", container);
66
+ expect(col.style.minWidth).toBe("230px");
67
+ expect(col.style.width).toBe("260px");
68
+ });
69
+
70
+ it("leaves a plain width untouched", () => {
71
+ const { container } = render(<DataTable columns={columns("180px")} data={DATA} rowKey={(r) => r.id} />);
72
+
73
+ const col = colFor("Batch", container);
74
+ expect(col.style.width).toBe("180px");
75
+ expect(col.style.minWidth).toBe("");
76
+ });
77
+ });
@@ -177,7 +177,25 @@ export type DataTableExpandable<T> = {
177
177
  export type Column<T> = {
178
178
  key: string;
179
179
  header: ReactNode;
180
- /** Table column width, e.g. `48px`, `18%`, `minmax(12rem,1fr)` (fixed layout) */
180
+ /**
181
+ * Table column width: `48px`, `18%`, or `minmax(12rem, 1fr)` for a floor
182
+ * that can still grow into whatever the other columns leave over.
183
+ *
184
+ * `minmax(min, max)` is translated rather than passed straight through: this
185
+ * is a real `<table>`/`<colgroup>`, and `minmax()` is a CSS Grid function
186
+ * that is not a legal `width` value outside a grid — the browser drops the
187
+ * whole declaration and the column gets no floor at all. `min` becomes the
188
+ * `<col>`'s `min-width`, and `max` becomes its `width` unless `max` is `1fr`
189
+ * (or any other flex unit), in which case no `width` is set and the column
190
+ * takes its share of whatever `table-layout: fixed` has left over, the same
191
+ * way a grid track's `1fr` would.
192
+ *
193
+ * `overflow-x-auto` on the table's own scroll container is what makes the
194
+ * floor mean something: once every column's minimum no longer fits, the
195
+ * table grows past its container and scrolls instead of every column
196
+ * shrinking under its `min-width` and the header text — deliberately not
197
+ * truncated, see the `<th>` render below — overlapping the column beside it.
198
+ */
181
199
  width?: string;
182
200
  minWidth?: number;
183
201
  maxWidth?: number;
@@ -425,6 +443,51 @@ function SortIndicator({ order }: { order: SortOrder | null }) {
425
443
  );
426
444
  }
427
445
 
446
+ /**
447
+ * `minmax(A, B)` → its two halves, tolerant of whichever whitespace someone
448
+ * wrote it with. `null` for anything else, including a plain length like
449
+ * `"180px"` — that already IS a valid `width` and passes through untouched.
450
+ */
451
+ function parseMinMaxWidth(width: string | undefined): { min: string; max: string } | null {
452
+ if (!width) return null;
453
+ const match = /^minmax\(\s*([^,]+?)\s*,\s*([^)]+?)\s*\)$/.exec(width.trim());
454
+ return match ? { min: match[1], max: match[2] } : null;
455
+ }
456
+
457
+ /** `1fr`, `2fr`, … — a Grid flex unit, meaningless as a table `width`. */
458
+ function isFlexUnit(value: string): boolean {
459
+ return /^[\d.]*fr$/.test(value.trim());
460
+ }
461
+
462
+ /**
463
+ * A column's `width` as the three real CSS properties a `<col>` understands.
464
+ *
465
+ * Anything that is not a `minmax(...)` string passes through exactly as it did
466
+ * before this existed, so a plain `"48px"` or `"18%"` column is untouched.
467
+ */
468
+ function colWidthStyle(col: Pick<Column<unknown>, "width" | "minWidth" | "maxWidth">): {
469
+ width: string | number | undefined;
470
+ minWidth: string | number | undefined;
471
+ maxWidth: string | number | undefined;
472
+ } {
473
+ const parsed = parseMinMaxWidth(col.width);
474
+ if (!parsed) {
475
+ return {
476
+ width: col.width ?? (col.minWidth != null ? `${col.minWidth}px` : undefined),
477
+ minWidth: col.minWidth,
478
+ maxWidth: col.maxWidth,
479
+ };
480
+ }
481
+ return {
482
+ width: isFlexUnit(parsed.max) ? undefined : parsed.max,
483
+ // An explicit numeric `minWidth`/`maxWidth` alongside a `minmax()` string
484
+ // is not a combination any caller uses today; the string wins because it
485
+ // is the more specific of the two.
486
+ minWidth: parsed.min,
487
+ maxWidth: isFlexUnit(parsed.max) ? col.maxWidth : undefined,
488
+ };
489
+ }
490
+
428
491
  export function DataTable<T>({
429
492
  columns,
430
493
  data,
@@ -706,14 +769,7 @@ export function DataTable<T>({
706
769
  <colgroup>
707
770
  {hasExpand ? <col style={{ width: 40 }} /> : null}
708
771
  {columns.map((col) => (
709
- <col
710
- key={col.key}
711
- style={{
712
- width: col.width ?? (col.minWidth != null ? `${col.minWidth}px` : undefined),
713
- minWidth: col.minWidth,
714
- maxWidth: col.maxWidth,
715
- }}
716
- />
772
+ <col key={col.key} style={colWidthStyle(col)} />
717
773
  ))}
718
774
  {/* Zero-width column: the action floats out of it as an overlay,
719
775
  so the last data column stays flush and nothing trails it. */}