@izumisy-tailor/tailor-data-viewer 0.2.29 → 0.2.31

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,7 +1,7 @@
1
1
  {
2
2
  "name": "@izumisy-tailor/tailor-data-viewer",
3
3
  "private": false,
4
- "version": "0.2.29",
4
+ "version": "0.2.31",
5
5
  "type": "module",
6
6
  "description": "Flexible data viewer component for Tailor Platform",
7
7
  "files": [
@@ -1,7 +1,7 @@
1
1
  import { render, screen, fireEvent } from "@testing-library/react";
2
2
  import { describe, it, expect, vi } from "vitest";
3
- import type { Column } from "./types";
4
- import { createTestProviders } from "../tests/helpers";
3
+ import type { Column } from "../types";
4
+ import { createTestProviders } from "../../tests/helpers";
5
5
  import { ColumnSelector } from "./column-selector";
6
6
 
7
7
  // =============================================================================
@@ -1,4 +1,5 @@
1
- import { useDataTableContext } from "./data-table/data-table-context";
1
+ import { useDataTableContext } from "./data-table-context";
2
+ import { getLabels } from "./i18n";
2
3
 
3
4
  /**
4
5
  * Column visibility toggle dropdown.
@@ -20,12 +21,14 @@ export function ColumnSelector() {
20
21
  toggleColumn,
21
22
  showAllColumns,
22
23
  hideAllColumns,
24
+ locale,
23
25
  } = useDataTableContext();
26
+ const labels = getLabels(locale);
24
27
  return (
25
28
  <div className="relative inline-block">
26
29
  <details className="group">
27
30
  <summary className="inline-flex cursor-pointer items-center gap-1 rounded-md border px-3 py-1.5 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground select-none">
28
- Columns
31
+ {labels.columnSelector.columns}
29
32
  </summary>
30
33
  <div className="absolute right-0 z-50 mt-1 min-w-[180px] rounded-md border bg-popover p-2 text-popover-foreground shadow-md">
31
34
  <div className="flex gap-2 border-b pb-2 mb-2">
@@ -34,14 +37,14 @@ export function ColumnSelector() {
34
37
  className="text-xs text-muted-foreground hover:text-foreground"
35
38
  onClick={showAllColumns}
36
39
  >
37
- Select all
40
+ {labels.columnSelector.selectAll}
38
41
  </button>
39
42
  <button
40
43
  type="button"
41
44
  className="text-xs text-muted-foreground hover:text-foreground"
42
45
  onClick={hideAllColumns}
43
46
  >
44
- Deselect all
47
+ {labels.columnSelector.deselectAll}
45
48
  </button>
46
49
  </div>
47
50
  {columns.map((col) => {
@@ -1,7 +1,7 @@
1
1
  import { render, screen, fireEvent } from "@testing-library/react";
2
2
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
3
- import type { Column } from "./types";
4
- import { createTestProviders } from "../tests/helpers";
3
+ import type { Column } from "../types";
4
+ import { createTestProviders } from "../../tests/helpers";
5
5
  import { CsvButton } from "./csv-button";
6
6
 
7
7
  // =============================================================================
@@ -1,4 +1,5 @@
1
- import { useDataTableContext } from "./data-table/data-table-context";
1
+ import { useDataTableContext } from "./data-table-context";
2
+ import { getLabels } from "./i18n";
2
3
 
3
4
  /**
4
5
  * CSV export button.
@@ -15,7 +16,8 @@ import { useDataTableContext } from "./data-table/data-table-context";
15
16
  * ```
16
17
  */
17
18
  export function CsvButton({ filename = "export" }: { filename?: string }) {
18
- const { visibleColumns, rows } = useDataTableContext();
19
+ const { visibleColumns, rows, locale } = useDataTableContext();
20
+ const labels = getLabels(locale);
19
21
  const handleExport = () => {
20
22
  // Build header row
21
23
  const headers = visibleColumns.map((col) => {
@@ -62,7 +64,7 @@ export function CsvButton({ filename = "export" }: { filename?: string }) {
62
64
  className="inline-flex items-center justify-center rounded-md border px-3 py-1.5 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground"
63
65
  onClick={handleExport}
64
66
  >
65
- Export CSV
67
+ {labels.csvExport}
66
68
  </button>
67
69
  );
68
70
  }
@@ -6,6 +6,7 @@ import type {
6
6
  RowOperations,
7
7
  SortState,
8
8
  } from "../types";
9
+ import type { DataTableLocale } from "./i18n";
9
10
 
10
11
  /**
11
12
  * Context value provided by `DataTable.Provider`.
@@ -43,6 +44,9 @@ export interface DataTableContextValue<TRow extends Record<string, unknown>> {
43
44
  onClickRow?: (row: TRow) => void;
44
45
  /** Row action definitions for the actions column */
45
46
  rowActions?: RowAction<TRow>[];
47
+
48
+ /** Locale for i18n labels */
49
+ locale: DataTableLocale;
46
50
  }
47
51
 
48
52
  // Using `any` for the context default since generic contexts need a base type.
@@ -68,6 +68,7 @@ function createCtx(
68
68
  showAllColumns: () => {},
69
69
  hideAllColumns: () => {},
70
70
  pageInfo: defaultPageInfo,
71
+ locale: "en",
71
72
  ...noopRowOps,
72
73
  ...overrides,
73
74
  };
@@ -16,6 +16,7 @@ import {
16
16
  DataTableContext,
17
17
  type DataTableContextValue,
18
18
  } from "./data-table-context";
19
+ import { getLabels } from "./i18n";
19
20
 
20
21
  // =============================================================================
21
22
  // DataTable.Root
@@ -99,6 +100,7 @@ function DataTableProviderComponent<TRow extends Record<string, unknown>>({
99
100
  pageInfo: value.pageInfo,
100
101
  onClickRow: value.onClickRow,
101
102
  rowActions: value.rowActions,
103
+ locale: value.locale,
102
104
  };
103
105
 
104
106
  const collectionValue = value.collection ?? null;
@@ -130,7 +132,8 @@ function DataTableHeaders({ className }: { className?: string }) {
130
132
  "<DataTable.Headers> must be used within <DataTable.Provider>",
131
133
  );
132
134
  }
133
- const { columns, sortStates, onSort, rowActions } = ctx;
135
+ const { columns, sortStates, onSort, rowActions, locale } = ctx;
136
+ const labels = getLabels(locale);
134
137
 
135
138
  return (
136
139
  <Table.Headers className={className}>
@@ -176,7 +179,7 @@ function DataTableHeaders({ className }: { className?: string }) {
176
179
  })}
177
180
  {rowActions && rowActions.length > 0 && (
178
181
  <Table.HeaderCell style={{ width: 50 }}>
179
- <span className="sr-only">操作</span>
182
+ <span className="sr-only">{labels.actionsHeader}</span>
180
183
  </Table.HeaderCell>
181
184
  )}
182
185
  </Table.HeaderRow>
@@ -209,7 +212,8 @@ function DataTableBody({
209
212
  "<DataTable.Body> must be used within <DataTable.Provider>",
210
213
  );
211
214
  }
212
- const { columns, rows, loading, error, onClickRow, rowActions } = ctx;
215
+ const { columns, rows, loading, error, onClickRow, rowActions, locale } = ctx;
216
+ const labels = getLabels(locale);
213
217
  const hasRowActions = rowActions && rowActions.length > 0;
214
218
  const totalColSpan = (columns?.length ?? 1) + (hasRowActions ? 1 : 0);
215
219
 
@@ -224,21 +228,23 @@ function DataTableBody({
224
228
  {loading && (!rows || rows.length === 0) && (
225
229
  <Table.Row>
226
230
  <Table.Cell colSpan={totalColSpan} className="h-24 text-center">
227
- <span className="text-muted-foreground">Loading...</span>
231
+ <span className="text-muted-foreground">{labels.loading}</span>
228
232
  </Table.Cell>
229
233
  </Table.Row>
230
234
  )}
231
235
  {error && (
232
236
  <Table.Row>
233
237
  <Table.Cell colSpan={totalColSpan} className="h-24 text-center">
234
- <span className="text-destructive">Error: {error.message}</span>
238
+ <span className="text-destructive">
239
+ {labels.errorPrefix} {error.message}
240
+ </span>
235
241
  </Table.Cell>
236
242
  </Table.Row>
237
243
  )}
238
244
  {!loading && !error && (!rows || rows.length === 0) && (
239
245
  <Table.Row>
240
246
  <Table.Cell colSpan={totalColSpan} className="h-24 text-center">
241
- <span className="text-muted-foreground">No data</span>
247
+ <span className="text-muted-foreground">{labels.noData}</span>
242
248
  </Table.Cell>
243
249
  </Table.Row>
244
250
  )}
@@ -264,7 +270,11 @@ function DataTableBody({
264
270
  style={{ width: 50 }}
265
271
  onClick={(e) => e.stopPropagation()}
266
272
  >
267
- <RowActionsMenu actions={rowActions} row={row} />
273
+ <RowActionsMenu
274
+ actions={rowActions}
275
+ row={row}
276
+ ariaLabel={labels.rowActions}
277
+ />
268
278
  </Table.Cell>
269
279
  )}
270
280
  </Table.Row>
@@ -350,9 +360,11 @@ function formatValue(value: unknown): ReactNode {
350
360
  function RowActionsMenu<TRow extends Record<string, unknown>>({
351
361
  actions,
352
362
  row,
363
+ ariaLabel,
353
364
  }: {
354
365
  actions: RowAction<TRow>[];
355
366
  row: TRow;
367
+ ariaLabel: string;
356
368
  }) {
357
369
  const [open, setOpen] = useState(false);
358
370
  const menuRef = useRef<HTMLDivElement>(null);
@@ -384,7 +396,7 @@ function RowActionsMenu<TRow extends Record<string, unknown>>({
384
396
  type="button"
385
397
  className="inline-flex h-8 w-8 items-center justify-center rounded-md text-sm hover:bg-accent"
386
398
  onClick={() => setOpen((prev) => !prev)}
387
- aria-label="Row actions"
399
+ aria-label={ariaLabel}
388
400
  aria-haspopup="true"
389
401
  aria-expanded={open}
390
402
  >
@@ -0,0 +1,153 @@
1
+ import type { FilterOperator } from "../types";
2
+
3
+ export type DataTableLocale = "en" | "ja";
4
+
5
+ export interface DataTableLabels {
6
+ // DataTable.Body
7
+ loading: string;
8
+ noData: string;
9
+ errorPrefix: string;
10
+
11
+ // DataTable.Headers (sr-only for actions column)
12
+ actionsHeader: string;
13
+
14
+ // RowActionsMenu aria-label
15
+ rowActions: string;
16
+
17
+ // Pagination
18
+ pagination: {
19
+ first: string;
20
+ previous: string;
21
+ next: string;
22
+ last: string;
23
+ rowsPerPage: string;
24
+ };
25
+
26
+ // ColumnSelector
27
+ columnSelector: {
28
+ columns: string;
29
+ selectAll: string;
30
+ deselectAll: string;
31
+ };
32
+
33
+ // CsvButton
34
+ csvExport: string;
35
+
36
+ // SearchFilterForm
37
+ searchFilter: {
38
+ search: string;
39
+ searchFilter: string;
40
+ clearAll: string;
41
+ activeFilters: string;
42
+ addFilter: string;
43
+ selectField: string;
44
+ enterValue: string;
45
+ selectValue: string;
46
+ add: string;
47
+ noFilterableFields: string;
48
+ allFieldsFiltered: string;
49
+ operatorLabels: Record<FilterOperator, string>;
50
+ };
51
+ }
52
+
53
+ const en: DataTableLabels = {
54
+ loading: "Loading...",
55
+ noData: "No data",
56
+ errorPrefix: "Error:",
57
+ actionsHeader: "Actions",
58
+ rowActions: "Row actions",
59
+ pagination: {
60
+ first: "First page",
61
+ previous: "Previous page",
62
+ next: "Next page",
63
+ last: "Last page",
64
+ rowsPerPage: "Rows per page",
65
+ },
66
+ columnSelector: {
67
+ columns: "Columns",
68
+ selectAll: "Select all",
69
+ deselectAll: "Deselect all",
70
+ },
71
+ csvExport: "Export CSV",
72
+ searchFilter: {
73
+ search: "Search",
74
+ searchFilter: "Search Filter",
75
+ clearAll: "Clear all",
76
+ activeFilters: "Active filters",
77
+ addFilter: "Add filter",
78
+ selectField: "Select field...",
79
+ enterValue: "Enter value...",
80
+ selectValue: "Select...",
81
+ add: "Add",
82
+ noFilterableFields: "No filterable fields",
83
+ allFieldsFiltered: "All fields have filters applied",
84
+ operatorLabels: {
85
+ eq: "=",
86
+ ne: "≠",
87
+ contains: "contains",
88
+ startsWith: "starts with",
89
+ endsWith: "ends with",
90
+ gt: ">",
91
+ gte: "≥",
92
+ lt: "<",
93
+ lte: "≤",
94
+ between: "between",
95
+ in: "in",
96
+ notIn: "not in",
97
+ },
98
+ },
99
+ };
100
+
101
+ const ja: DataTableLabels = {
102
+ loading: "読み込み中...",
103
+ noData: "データがありません",
104
+ errorPrefix: "エラー:",
105
+ actionsHeader: "操作",
106
+ rowActions: "行の操作",
107
+ pagination: {
108
+ first: "最初のページ",
109
+ previous: "前のページ",
110
+ next: "次のページ",
111
+ last: "最後のページ",
112
+ rowsPerPage: "表示件数",
113
+ },
114
+ columnSelector: {
115
+ columns: "列の表示",
116
+ selectAll: "すべて選択",
117
+ deselectAll: "すべて解除",
118
+ },
119
+ csvExport: "CSV出力",
120
+ searchFilter: {
121
+ search: "検索",
122
+ searchFilter: "検索フィルター",
123
+ clearAll: "すべてクリア",
124
+ activeFilters: "適用中のフィルター",
125
+ addFilter: "フィルターを追加",
126
+ selectField: "フィールドを選択...",
127
+ enterValue: "値を入力...",
128
+ selectValue: "選択...",
129
+ add: "追加",
130
+ noFilterableFields: "フィルタ可能なフィールドがありません",
131
+ allFieldsFiltered: "すべてのフィールドにフィルターが適用されています",
132
+ operatorLabels: {
133
+ eq: "等しい",
134
+ ne: "等しくない",
135
+ contains: "含む",
136
+ startsWith: "で始まる",
137
+ endsWith: "で終わる",
138
+ gt: "より大きい",
139
+ gte: "以上",
140
+ lt: "より小さい",
141
+ lte: "以下",
142
+ between: "範囲内",
143
+ in: "含まれる",
144
+ notIn: "含まれない",
145
+ },
146
+ },
147
+ };
148
+
149
+ const locales: Record<DataTableLocale, DataTableLabels> = { en, ja };
150
+
151
+ export function getLabels(locale: DataTableLocale): DataTableLabels {
152
+ return locales[locale];
153
+ }
@@ -1,7 +1,7 @@
1
1
  import { render, screen, fireEvent } from "@testing-library/react";
2
2
  import { describe, it, expect, vi } from "vitest";
3
- import type { Column } from "./types";
4
- import { createTestProviders } from "../tests/helpers";
3
+ import type { Column } from "../types";
4
+ import { createTestProviders } from "../../tests/helpers";
5
5
  import { Pagination } from "./pagination";
6
6
 
7
7
  // =============================================================================
@@ -1,5 +1,6 @@
1
- import { useDataTableContext } from "./data-table/data-table-context";
2
- import { useCollectionContext } from "./collection/collection-provider";
1
+ import { useDataTableContext } from "./data-table-context";
2
+ import { useCollectionContext } from "../collection/collection-provider";
3
+ import { getLabels } from "./i18n";
3
4
 
4
5
  // =============================================================================
5
6
  // Inline SVG Icons (lucide-style, no external dependency)
@@ -126,7 +127,7 @@ const btnClass =
126
127
  * ```
127
128
  */
128
129
  export function Pagination({ labels, pageSizeOptions }: PaginationProps = {}) {
129
- const { pageInfo } = useDataTableContext();
130
+ const { pageInfo, locale } = useDataTableContext();
130
131
  const {
131
132
  nextPage,
132
133
  prevPage,
@@ -140,11 +141,12 @@ export function Pagination({ labels, pageSizeOptions }: PaginationProps = {}) {
140
141
  setPageSize,
141
142
  } = useCollectionContext();
142
143
 
143
- const firstLabel = labels?.first ?? "First page";
144
- const previousLabel = labels?.previous ?? "Previous page";
145
- const nextLabel = labels?.next ?? "Next page";
146
- const lastLabel = labels?.last ?? "Last page";
147
- const rowsPerPageLabel = labels?.rowsPerPage ?? "Rows per page";
144
+ const pl = getLabels(locale).pagination;
145
+ const firstLabel = labels?.first ?? pl.first;
146
+ const previousLabel = labels?.previous ?? pl.previous;
147
+ const nextLabel = labels?.next ?? pl.next;
148
+ const lastLabel = labels?.last ?? pl.last;
149
+ const rowsPerPageLabel = labels?.rowsPerPage ?? pl.rowsPerPage;
148
150
 
149
151
  return (
150
152
  <div className="flex items-center justify-end gap-2 py-2">
@@ -1,8 +1,8 @@
1
1
  import { render, screen, fireEvent } from "@testing-library/react";
2
2
  import userEvent from "@testing-library/user-event";
3
3
  import { describe, it, expect, vi } from "vitest";
4
- import type { Column, Filter } from "./types";
5
- import { createTestProviders } from "../tests/helpers";
4
+ import type { Column, Filter } from "../types";
5
+ import { createTestProviders } from "../../tests/helpers";
6
6
  import { SearchFilterForm } from "./search-filter-form";
7
7
 
8
8
  // =============================================================================
@@ -6,10 +6,16 @@ import {
6
6
  type KeyboardEvent,
7
7
  type ReactNode,
8
8
  } from "react";
9
- import type { FilterOperator, FilterConfig, SearchFilterLabels } from "./types";
10
- import { OPERATORS_BY_FILTER_TYPE, DEFAULT_OPERATOR_LABELS } from "./types";
11
- import { useDataTableContext } from "./data-table/data-table-context";
12
- import { useCollectionContext } from "./collection/collection-provider";
9
+ import type {
10
+ Filter,
11
+ FilterOperator,
12
+ FilterConfig,
13
+ SearchFilterLabels,
14
+ } from "../types";
15
+ import { OPERATORS_BY_FILTER_TYPE } from "../types";
16
+ import { useDataTableContext } from "./data-table-context";
17
+ import { useCollectionContext } from "../collection/collection-provider";
18
+ import { getLabels } from "./i18n";
13
19
 
14
20
  /**
15
21
  * Composite search filter form.
@@ -38,9 +44,10 @@ export function SearchFilterForm({
38
44
  /** Custom trigger element. When provided, replaces the default trigger button content. */
39
45
  trigger?: ReactNode;
40
46
  } = {}) {
41
- const { columns } = useDataTableContext();
47
+ const { columns, locale } = useDataTableContext();
42
48
  const { filters, addFilter, removeFilter, clearFilters } =
43
49
  useCollectionContext();
50
+ const sf = getLabels(locale).searchFilter;
44
51
  const filterableColumns = columns.filter(
45
52
  (col) => col.kind === "field" && col.filter,
46
53
  );
@@ -70,16 +77,18 @@ export function SearchFilterForm({
70
77
 
71
78
  // Label helpers
72
79
  const l = (key: string, fallback: string) =>
73
- (labels as Record<string, string> | undefined)?.[key] ?? fallback;
80
+ (labels as Record<string, string> | undefined)?.[key] ??
81
+ (sf as unknown as Record<string, string>)[key] ??
82
+ fallback;
74
83
 
75
84
  const getOperatorLabel = useCallback(
76
85
  (filterType: FilterConfig["type"], operator: FilterOperator): string => {
77
86
  if (labels?.getOperatorLabel) {
78
87
  return labels.getOperatorLabel(filterType, operator);
79
88
  }
80
- return DEFAULT_OPERATOR_LABELS[operator] ?? operator;
89
+ return sf.operatorLabels[operator] ?? operator;
81
90
  },
82
- [labels],
91
+ [labels, sf],
83
92
  );
84
93
 
85
94
  const getFieldLabel = useCallback(
@@ -100,10 +109,7 @@ export function SearchFilterForm({
100
109
  }): string => {
101
110
  const fieldLabel = getFieldLabel(filter.field);
102
111
  if (labels?.formatFilterDisplay) {
103
- return labels.formatFilterDisplay(
104
- filter as import("./types").Filter,
105
- fieldLabel,
106
- );
112
+ return labels.formatFilterDisplay(filter as Filter, fieldLabel);
107
113
  }
108
114
  const value =
109
115
  typeof filter.value === "boolean"
@@ -111,8 +117,7 @@ export function SearchFilterForm({
111
117
  ? "true"
112
118
  : "false"
113
119
  : String(filter.value);
114
- const opLabel =
115
- DEFAULT_OPERATOR_LABELS[filter.operator] ?? filter.operator;
120
+ const opLabel = sf.operatorLabels[filter.operator] ?? filter.operator;
116
121
  return `${fieldLabel} ${opLabel} ${value}`;
117
122
  },
118
123
  [getFieldLabel, labels],
@@ -42,6 +42,7 @@ export function useDataTable<TRow extends Record<string, unknown>>(
42
42
  collection,
43
43
  onClickRow,
44
44
  rowActions,
45
+ locale = "en",
45
46
  } = options;
46
47
 
47
48
  // ---------------------------------------------------------------------------
@@ -255,5 +256,8 @@ export function useDataTable<TRow extends Record<string, unknown>>(
255
256
  // Row interaction (passthrough for DataTable.Provider)
256
257
  onClickRow,
257
258
  rowActions,
259
+
260
+ // Locale
261
+ locale,
258
262
  };
259
263
  }
@@ -63,13 +63,17 @@ export { Table } from "./table";
63
63
  export { DataTable } from "./data-table/data-table";
64
64
  export { useDataTable } from "./data-table/use-data-table";
65
65
  export { useDataTableContext } from "./data-table/data-table-context";
66
+ export type { DataTableLocale } from "./data-table/i18n";
66
67
 
67
68
  // Field helpers
68
69
  export { createColumnHelper } from "./field-helpers";
69
70
 
70
- // Utility components
71
- export { Pagination } from "./pagination";
72
- export type { PaginationProps, PaginationLabels } from "./pagination";
73
- export { ColumnSelector } from "./column-selector";
74
- export { CsvButton } from "./csv-button";
75
- export { SearchFilterForm } from "./search-filter-form";
71
+ // Utility components (co-located with DataTable)
72
+ export { Pagination } from "./data-table/pagination";
73
+ export type {
74
+ PaginationProps,
75
+ PaginationLabels,
76
+ } from "./data-table/pagination";
77
+ export { ColumnSelector } from "./data-table/column-selector";
78
+ export { CsvButton } from "./data-table/csv-button";
79
+ export { SearchFilterForm } from "./data-table/search-filter-form";
@@ -12,7 +12,8 @@ interface TableRootProps extends ComponentProps<"table"> {
12
12
 
13
13
  function Root({ className, tableLayout = "fixed", ...props }: TableRootProps) {
14
14
  return (
15
- <div className="relative w-full overflow-x-auto rounded-md border bg-background">
15
+ /* TODO: bg-card should be something specific (bg-table?) */
16
+ <div className="relative w-full overflow-x-auto rounded-md border bg-card">
16
17
  <table
17
18
  className={cn(
18
19
  "w-full caption-bottom text-sm",
@@ -515,6 +515,8 @@ export interface UseDataTableOptions<TRow extends Record<string, unknown>> {
515
515
  onClickRow?: (row: TRow) => void;
516
516
  /** Row action definitions for the actions column */
517
517
  rowActions?: RowAction<TRow>[];
518
+ /** Locale for i18n labels (default: "en") */
519
+ locale?: "en" | "ja";
518
520
  }
519
521
 
520
522
  /**
@@ -623,6 +625,9 @@ export interface UseDataTableReturn<TRow extends Record<string, unknown>> {
623
625
  onClickRow?: (row: TRow) => void;
624
626
  /** Row action definitions for the actions column */
625
627
  rowActions?: RowAction<TRow>[];
628
+
629
+ /** Resolved locale */
630
+ locale: "en" | "ja";
626
631
  }
627
632
 
628
633
  // =============================================================================
@@ -37,6 +37,7 @@ export function createMockDataTableContext<T extends Record<string, unknown>>(
37
37
  hasPreviousPage: false,
38
38
  startCursor: null,
39
39
  },
40
+ locale: "en",
40
41
  ...overrides,
41
42
  };
42
43
  }
@@ -1,349 +0,0 @@
1
- import { render, screen } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
- import { describe, it, expect, vi } from "vitest";
4
- import { Table } from "./table";
5
- import { DataTable } from "./data-table/data-table";
6
- import { DataTableContext } from "./data-table/data-table-context";
7
- import type { DataTableContextValue } from "./data-table/data-table-context";
8
- import type { Column } from "./types";
9
-
10
- describe("Table (static)", () => {
11
- it("renders a basic static table", () => {
12
- render(
13
- <Table.Root>
14
- <Table.Headers>
15
- <Table.HeaderRow>
16
- <Table.HeaderCell>Name</Table.HeaderCell>
17
- <Table.HeaderCell>Status</Table.HeaderCell>
18
- </Table.HeaderRow>
19
- </Table.Headers>
20
- <Table.Body>
21
- <Table.Row>
22
- <Table.Cell>Alice</Table.Cell>
23
- <Table.Cell>Active</Table.Cell>
24
- </Table.Row>
25
- <Table.Row>
26
- <Table.Cell>Bob</Table.Cell>
27
- <Table.Cell>Inactive</Table.Cell>
28
- </Table.Row>
29
- </Table.Body>
30
- </Table.Root>,
31
- );
32
-
33
- expect(screen.getByText("Name")).toBeInTheDocument();
34
- expect(screen.getByText("Status")).toBeInTheDocument();
35
- expect(screen.getByText("Alice")).toBeInTheDocument();
36
- expect(screen.getByText("Bob")).toBeInTheDocument();
37
- });
38
- });
39
-
40
- type TestRow = {
41
- id: string;
42
- name: string;
43
- status: string;
44
- };
45
-
46
- const testColumns: Column<TestRow>[] = [
47
- {
48
- kind: "field",
49
- dataKey: "name",
50
- label: "Name",
51
- sort: { type: "string" },
52
- },
53
- {
54
- kind: "field",
55
- dataKey: "status",
56
- label: "Status",
57
- },
58
- {
59
- kind: "display",
60
- id: "actions",
61
- label: "Actions",
62
- render: (row) => <button>Edit {row.name}</button>,
63
- },
64
- ];
65
-
66
- const testRows: TestRow[] = [
67
- { id: "1", name: "Alice", status: "Active" },
68
- { id: "2", name: "Bob", status: "Inactive" },
69
- ];
70
-
71
- const noopRowOps = {
72
- updateRow: () => ({ rollback: () => {} }),
73
- deleteRow: () => ({
74
- rollback: () => {},
75
- deletedRow: {} as TestRow,
76
- }),
77
- insertRow: () => ({ rollback: () => {} }),
78
- };
79
-
80
- const defaultPageInfo = {
81
- hasNextPage: false,
82
- endCursor: null,
83
- hasPreviousPage: false,
84
- startCursor: null,
85
- };
86
-
87
- function createCtx(
88
- overrides: Partial<DataTableContextValue<TestRow>> = {},
89
- ): DataTableContextValue<TestRow> {
90
- return {
91
- columns: testColumns,
92
- rows: testRows,
93
- loading: false,
94
- error: null,
95
- sortStates: [],
96
- visibleColumns: testColumns,
97
- isColumnVisible: () => true,
98
- toggleColumn: () => {},
99
- showAllColumns: () => {},
100
- hideAllColumns: () => {},
101
- pageInfo: defaultPageInfo,
102
- ...noopRowOps,
103
- ...overrides,
104
- };
105
- }
106
-
107
- describe("DataTable", () => {
108
- it("renders data-bound table with auto-generated rows", () => {
109
- render(
110
- <DataTableContext.Provider value={createCtx()}>
111
- <DataTable.Root>
112
- <DataTable.Headers />
113
- <DataTable.Body />
114
- </DataTable.Root>
115
- </DataTableContext.Provider>,
116
- );
117
-
118
- expect(screen.getByText("Name")).toBeInTheDocument();
119
- expect(screen.getByText("Status")).toBeInTheDocument();
120
- expect(screen.getByText("Alice")).toBeInTheDocument();
121
- expect(screen.getByText("Bob")).toBeInTheDocument();
122
- expect(screen.getByText("Active")).toBeInTheDocument();
123
- });
124
-
125
- it("renders display columns via render function", () => {
126
- render(
127
- <DataTableContext.Provider value={createCtx()}>
128
- <DataTable.Root>
129
- <DataTable.Headers />
130
- <DataTable.Body />
131
- </DataTable.Root>
132
- </DataTableContext.Provider>,
133
- );
134
-
135
- expect(screen.getByText("Edit Alice")).toBeInTheDocument();
136
- expect(screen.getByText("Edit Bob")).toBeInTheDocument();
137
- });
138
-
139
- it("shows loading state", () => {
140
- render(
141
- <DataTableContext.Provider value={createCtx({ rows: [], loading: true })}>
142
- <DataTable.Root>
143
- <DataTable.Headers />
144
- <DataTable.Body />
145
- </DataTable.Root>
146
- </DataTableContext.Provider>,
147
- );
148
-
149
- expect(screen.getByText("Loading...")).toBeInTheDocument();
150
- });
151
-
152
- it("shows error state", () => {
153
- const err = new Error("Something went wrong");
154
- render(
155
- <DataTableContext.Provider value={createCtx({ rows: [], error: err })}>
156
- <DataTable.Root>
157
- <DataTable.Headers />
158
- <DataTable.Body />
159
- </DataTable.Root>
160
- </DataTableContext.Provider>,
161
- );
162
-
163
- expect(screen.getByText("Error: Something went wrong")).toBeInTheDocument();
164
- });
165
-
166
- it("shows empty state", () => {
167
- render(
168
- <DataTableContext.Provider value={createCtx({ rows: [] })}>
169
- <DataTable.Root>
170
- <DataTable.Headers />
171
- <DataTable.Body />
172
- </DataTable.Root>
173
- </DataTableContext.Provider>,
174
- );
175
-
176
- expect(screen.getByText("No data")).toBeInTheDocument();
177
- });
178
-
179
- it("renders sort indicator on sorted column", () => {
180
- render(
181
- <DataTableContext.Provider
182
- value={createCtx({
183
- sortStates: [{ field: "name", direction: "Asc" }],
184
- })}
185
- >
186
- <DataTable.Root>
187
- <DataTable.Headers />
188
- <DataTable.Body />
189
- </DataTable.Root>
190
- </DataTableContext.Provider>,
191
- );
192
-
193
- expect(screen.getByText("▲")).toBeInTheDocument();
194
- });
195
-
196
- it("supports custom rendering with children", () => {
197
- render(
198
- <DataTableContext.Provider value={createCtx()}>
199
- <DataTable.Root>
200
- <DataTable.Headers />
201
- <DataTable.Body>
202
- <DataTable.Row>
203
- <DataTable.Cell>Custom Cell</DataTable.Cell>
204
- </DataTable.Row>
205
- </DataTable.Body>
206
- </DataTable.Root>
207
- </DataTableContext.Provider>,
208
- );
209
-
210
- expect(screen.getByText("Custom Cell")).toBeInTheDocument();
211
- });
212
-
213
- describe("header sort click", () => {
214
- const sortableColumns: Column<TestRow>[] = [
215
- {
216
- kind: "field",
217
- dataKey: "name",
218
- label: "Name",
219
- sort: { type: "string" },
220
- },
221
- {
222
- kind: "field",
223
- dataKey: "status",
224
- label: "Status",
225
- sort: { type: "string" },
226
- },
227
- ];
228
-
229
- it("calls onSort with Asc when clicking unsorted column", async () => {
230
- const onSort = vi.fn();
231
- render(
232
- <DataTableContext.Provider
233
- value={createCtx({
234
- columns: sortableColumns,
235
- visibleColumns: sortableColumns,
236
- sortStates: [],
237
- onSort,
238
- })}
239
- >
240
- <DataTable.Root>
241
- <DataTable.Headers />
242
- <DataTable.Body />
243
- </DataTable.Root>
244
- </DataTableContext.Provider>,
245
- );
246
-
247
- await userEvent.click(screen.getByText("Name"));
248
- expect(onSort).toHaveBeenCalledWith("name", "Asc");
249
- });
250
-
251
- it("cycles Asc → Desc on second click", async () => {
252
- const onSort = vi.fn();
253
- render(
254
- <DataTableContext.Provider
255
- value={createCtx({
256
- columns: sortableColumns,
257
- visibleColumns: sortableColumns,
258
- sortStates: [{ field: "name", direction: "Asc" }],
259
- onSort,
260
- })}
261
- >
262
- <DataTable.Root>
263
- <DataTable.Headers />
264
- <DataTable.Body />
265
- </DataTable.Root>
266
- </DataTableContext.Provider>,
267
- );
268
-
269
- await userEvent.click(screen.getByText("Name"));
270
- expect(onSort).toHaveBeenCalledWith("name", "Desc");
271
- });
272
-
273
- it("cycles Desc → remove (undefined) on third click", async () => {
274
- const onSort = vi.fn();
275
- render(
276
- <DataTableContext.Provider
277
- value={createCtx({
278
- columns: sortableColumns,
279
- visibleColumns: sortableColumns,
280
- sortStates: [{ field: "name", direction: "Desc" }],
281
- onSort,
282
- })}
283
- >
284
- <DataTable.Root>
285
- <DataTable.Headers />
286
- <DataTable.Body />
287
- </DataTable.Root>
288
- </DataTableContext.Provider>,
289
- );
290
-
291
- await userEvent.click(screen.getByText("Name"));
292
- expect(onSort).toHaveBeenCalledWith("name", undefined);
293
- });
294
-
295
- it("does not call onSort for non-sortable column", async () => {
296
- const onSort = vi.fn();
297
- const mixedColumns: Column<TestRow>[] = [
298
- {
299
- kind: "field",
300
- dataKey: "name",
301
- label: "Name",
302
- sort: { type: "string" },
303
- },
304
- { kind: "field", dataKey: "status", label: "Status" },
305
- ];
306
- render(
307
- <DataTableContext.Provider
308
- value={createCtx({
309
- columns: mixedColumns,
310
- visibleColumns: mixedColumns,
311
- sortStates: [],
312
- onSort,
313
- })}
314
- >
315
- <DataTable.Root>
316
- <DataTable.Headers />
317
- <DataTable.Body />
318
- </DataTable.Root>
319
- </DataTableContext.Provider>,
320
- );
321
-
322
- await userEvent.click(screen.getByText("Status"));
323
- expect(onSort).not.toHaveBeenCalled();
324
- });
325
-
326
- it("shows sort indicators for multiple sorted columns", () => {
327
- render(
328
- <DataTableContext.Provider
329
- value={createCtx({
330
- columns: sortableColumns,
331
- visibleColumns: sortableColumns,
332
- sortStates: [
333
- { field: "name", direction: "Asc" },
334
- { field: "status", direction: "Desc" },
335
- ],
336
- })}
337
- >
338
- <DataTable.Root>
339
- <DataTable.Headers />
340
- <DataTable.Body />
341
- </DataTable.Root>
342
- </DataTableContext.Provider>,
343
- );
344
-
345
- expect(screen.getByText("▲")).toBeInTheDocument();
346
- expect(screen.getByText("▼")).toBeInTheDocument();
347
- });
348
- });
349
- });