@colixsystems/widget-sdk 0.246.0 → 0.248.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.
Files changed (54) hide show
  1. package/README.md +76 -6
  2. package/dist/board-auto-scroll.js +59 -0
  3. package/dist/board-view.js +327 -0
  4. package/dist/chart-logic.js +575 -0
  5. package/dist/chart-tokens.js +131 -0
  6. package/dist/chart-view.js +262 -0
  7. package/dist/chart.js +21 -0
  8. package/dist/chart.native.js +10 -0
  9. package/dist/contract.cjs +119 -7
  10. package/dist/contract.js +119 -7
  11. package/dist/data-table-logic.js +179 -0
  12. package/dist/data-table-view.js +388 -0
  13. package/dist/data-table.js +22 -0
  14. package/dist/data-table.native.js +14 -0
  15. package/dist/drag-kit-view.js +310 -0
  16. package/dist/file-upload-view.js +13 -2
  17. package/dist/filter-bar-logic.js +143 -0
  18. package/dist/filter-bar-view.js +381 -0
  19. package/dist/filter-bar.js +15 -0
  20. package/dist/filter-bar.native.js +13 -0
  21. package/dist/form-control-logic.js +20 -3
  22. package/dist/form-control-view.js +18 -0
  23. package/dist/form-layout-logic.js +92 -0
  24. package/dist/form-view.js +268 -0
  25. package/dist/form.js +15 -0
  26. package/dist/form.native.js +9 -0
  27. package/dist/grid-logic.js +222 -0
  28. package/dist/grid-view.js +109 -0
  29. package/dist/grid.js +7 -0
  30. package/dist/grid.native.js +7 -0
  31. package/dist/host.d.ts +6 -0
  32. package/dist/host.js +2 -0
  33. package/dist/index.d.ts +606 -7
  34. package/dist/index.js +15 -0
  35. package/dist/index.native.js +15 -0
  36. package/dist/menu-view.js +5 -2
  37. package/dist/people-picker-logic.js +203 -0
  38. package/dist/people-picker-view.js +250 -0
  39. package/dist/people-picker.js +11 -0
  40. package/dist/people-picker.native.js +9 -0
  41. package/dist/radio-group-view.js +7 -3
  42. package/dist/select-search-logic.js +150 -0
  43. package/dist/select-view.js +251 -18
  44. package/dist/select.js +6 -2
  45. package/dist/select.native.js +5 -3
  46. package/dist/sortable-list-view.js +228 -0
  47. package/dist/sortable-logic.js +273 -0
  48. package/dist/sortable-tokens.js +120 -0
  49. package/dist/sortable-view.js +24 -0
  50. package/dist/sortable.js +39 -0
  51. package/dist/sortable.native.js +30 -0
  52. package/dist/text-field-view.js +7 -2
  53. package/package.json +1 -1
  54. package/src/contract.js +119 -7
@@ -0,0 +1,179 @@
1
+ // The `<DataTable>` rules without a renderer: which layout a width gets, how
2
+ // its columns read, and how rows sort. React-free, so both hosts and the SDK's
3
+ // own tests share one answer.
4
+
5
+ import { widthTier } from "./container-width.js";
6
+
7
+ /** The rows' scroll box stops here unless `maxHeight` says otherwise. */
8
+ export const DATA_TABLE_DEFAULT_MAX_HEIGHT = 480;
9
+ /** Placeholder rows while the first page loads. */
10
+ export const DATA_TABLE_SKELETON_ROWS = 3;
11
+ /** A sortable header is a button, so it keeps the touch-target floor. */
12
+ export const DATA_TABLE_HEADER_TARGET = 44;
13
+ export const DATA_TABLE_ALIGNS = Object.freeze(["left", "center", "right"]);
14
+ export const DATA_TABLE_LAYOUTS = Object.freeze({
15
+ narrow: "stacked",
16
+ medium: "cards",
17
+ wide: "columns",
18
+ });
19
+ /** The English words a sort is spoken with; a widget passes its own language. */
20
+ export const DEFAULT_SORT_LABELS = Object.freeze({
21
+ sort: "Sort",
22
+ ascending: "sorted ascending",
23
+ descending: "sorted descending",
24
+ });
25
+
26
+ const JUSTIFY = Object.freeze({ left: "flex-start", center: "center", right: "flex-end" });
27
+
28
+ function isText(value) {
29
+ return typeof value === "string" && value.length > 0;
30
+ }
31
+
32
+ function positive(value, fallback) {
33
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
34
+ }
35
+
36
+ /**
37
+ * The layout a MEASURED width calls for: stacked label/value cards when
38
+ * narrow, one card per row at medium, aligned columns when wide. An
39
+ * unmeasured width is "columns", as `widthTier(0)` is "wide".
40
+ */
41
+ export function dataTableLayout(width) {
42
+ return DATA_TABLE_LAYOUTS[widthTier(width)];
43
+ }
44
+
45
+ function normaliseColumn(entry) {
46
+ const column = typeof entry === "string" ? { key: entry } : entry;
47
+ if (!column || typeof column !== "object" || !isText(column.key)) return null;
48
+ return {
49
+ key: column.key,
50
+ label: column.label !== undefined && column.label !== null ? column.label : column.key,
51
+ flex: positive(column.flex, 1),
52
+ align: DATA_TABLE_ALIGNS.includes(column.align) ? column.align : "left",
53
+ minWidth: positive(column.minWidth, 0),
54
+ sortable: column.sortable === true,
55
+ render: typeof column.render === "function" ? column.render : null,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * The columns as the table draws them. A bare string is a column of that
61
+ * key; an entry without a key is dropped; a repeated key keeps its first.
62
+ */
63
+ export function normaliseColumns(columns) {
64
+ if (!Array.isArray(columns)) return [];
65
+ const seen = new Set();
66
+ const out = [];
67
+ for (const entry of columns) {
68
+ const column = normaliseColumn(entry);
69
+ if (!column || seen.has(column.key)) continue;
70
+ seen.add(column.key);
71
+ out.push(column);
72
+ }
73
+ return out;
74
+ }
75
+
76
+ /** A `{ key, direction }` sort, or null when it names no column. */
77
+ export function normaliseSort(sort) {
78
+ if (!sort || typeof sort !== "object" || !isText(sort.key)) return null;
79
+ return { key: sort.key, direction: sort.direction === "desc" ? "desc" : "asc" };
80
+ }
81
+
82
+ /** A header tap: a new column sorts ascending, the sorted one flips. */
83
+ export function nextSort(current, key) {
84
+ const now = normaliseSort(current);
85
+ const flip = now && now.key === key && now.direction === "asc";
86
+ return { key, direction: flip ? "desc" : "asc" };
87
+ }
88
+
89
+ /** The text a cell shows for a record value; nothing for an empty one. */
90
+ export function cellText(value) {
91
+ if (value === null || value === undefined) return "";
92
+ if (Array.isArray(value)) return value.map(cellText).filter(Boolean).join(", ");
93
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
94
+ return String(value);
95
+ }
96
+ if (typeof value === "object") {
97
+ for (const name of ["label", "name", "id"]) {
98
+ if (typeof value[name] === "string") return value[name];
99
+ }
100
+ }
101
+ return "";
102
+ }
103
+
104
+ function sortKey(value) {
105
+ if (typeof value === "number" && Number.isFinite(value)) return value;
106
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value.getTime();
107
+ if (typeof value === "boolean") return value ? 1 : 0;
108
+ const text = cellText(value);
109
+ return text === "" ? null : text;
110
+ }
111
+
112
+ function compareKeys(a, b) {
113
+ if (typeof a === "number" && typeof b === "number") return a - b;
114
+ return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: "base" });
115
+ }
116
+
117
+ /**
118
+ * The rows in `sort` order, as a new array; stable, with empty values last
119
+ * in either direction. Without a sort, the rows as given.
120
+ */
121
+ export function sortRows(rows, sort) {
122
+ const list = Array.isArray(rows) ? rows : [];
123
+ const by = normaliseSort(sort);
124
+ if (!by) return list;
125
+ const sign = by.direction === "desc" ? -1 : 1;
126
+ return list
127
+ .map((row, index) => ({ row, index, key: sortKey(row && row[by.key]) }))
128
+ .sort((a, b) => {
129
+ if (a.key === null || b.key === null) {
130
+ return a.key === b.key ? a.index - b.index : a.key === null ? 1 : -1;
131
+ }
132
+ return sign * compareKeys(a.key, b.key) || a.index - b.index;
133
+ })
134
+ .map((entry) => entry.row);
135
+ }
136
+
137
+ /** A row's React key: its `keyField` value, else its position. */
138
+ export function rowKey(row, keyField, index) {
139
+ const value = row && typeof row === "object" ? row[keyField] : undefined;
140
+ return value !== undefined && value !== null && value !== "" ? String(value) : `row-${index}`;
141
+ }
142
+
143
+ /** A sortable column's sort state: "none", "ascending" or "descending". */
144
+ export function ariaSortFor(column, sort) {
145
+ const by = normaliseSort(sort);
146
+ if (!by || by.key !== column.key) return "none";
147
+ return by.direction === "desc" ? "descending" : "ascending";
148
+ }
149
+
150
+ /** A header's spoken name: its label, and the sort when it is the sorted one. */
151
+ export function sortSpokenLabel(label, column, sort, labels = DEFAULT_SORT_LABELS) {
152
+ const words = sortStateWords(column, sort, labels);
153
+ return words ? `${label}, ${words}` : label;
154
+ }
155
+
156
+ /** The words a sorted column's state is spoken with; "" when it is not sorted. */
157
+ export function sortStateWords(column, sort, labels = DEFAULT_SORT_LABELS) {
158
+ const state = ariaSortFor(column, sort);
159
+ if (state === "none") return "";
160
+ return state === "descending" ? labels.descending : labels.ascending;
161
+ }
162
+
163
+ /** The rows' scroll-box height: a positive number, else the default. */
164
+ export function resolveMaxHeight(maxHeight) {
165
+ return positive(maxHeight, DATA_TABLE_DEFAULT_MAX_HEIGHT);
166
+ }
167
+
168
+ /** How a column's content sits across its cell. */
169
+ export function justifyFor(align) {
170
+ return JUSTIFY[align] || JUSTIFY.left;
171
+ }
172
+
173
+ /**
174
+ * Who orders the rows. With `onSortChange` the widget re-queries (server
175
+ * sort) and the table draws the rows as given; without it the table sorts.
176
+ */
177
+ export function sortsClientSide(onSortChange) {
178
+ return typeof onSortChange !== "function";
179
+ }
@@ -0,0 +1,388 @@
1
+ // The ONE implementation behind `<DataTable>`. The bindings (`data-table.js`
2
+ // / `data-table.native.js`) differ only in where the primitives and the list,
3
+ // loading, menu and button parts come from, so the hosts cannot drift.
4
+ //
5
+ // A record list in three tiers from its own measured width: stacked
6
+ // label/value cards, one card per row, then aligned columns under a sticky
7
+ // header. Every tier is drawn by `<List>` / `<ListRow>` / `<ListHeader>`, so
8
+ // the theme's `list` scope (and `card` scope for the cards) styles it.
9
+
10
+ import React from "react";
11
+ import { useContainerWidth, useHostTheme, useHostSharedText } from "./hooks.js";
12
+ import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
13
+ import { CONTRACT } from "./contract.js";
14
+ import { Icon } from "./icon.js";
15
+ import { LIST_TOKEN_NAMES, resolveListTokens } from "./list-tokens.js";
16
+ import { textRoleStyle } from "./text-roles.js";
17
+ import {
18
+ DATA_TABLE_HEADER_TARGET,
19
+ DATA_TABLE_SKELETON_ROWS,
20
+ DEFAULT_SORT_LABELS,
21
+ ariaSortFor,
22
+ cellText,
23
+ dataTableLayout,
24
+ justifyFor,
25
+ nextSort,
26
+ normaliseColumns,
27
+ normaliseSort,
28
+ resolveMaxHeight,
29
+ rowKey,
30
+ sortRows,
31
+ sortSpokenLabel,
32
+ sortStateWords,
33
+ sortsClientSide,
34
+ } from "./data-table-logic.js";
35
+
36
+ const h = React.createElement;
37
+ const EMPTY_FALLBACK = CONTRACT.sharedTranslationKeys.no_results.en;
38
+ const SORT_ICON_SIZE = 14;
39
+ const SORT_GLYPHS = Object.freeze({
40
+ none: "chevrons-up-down",
41
+ ascending: "arrow-up",
42
+ descending: "arrow-down",
43
+ });
44
+ const VISUALLY_HIDDEN = {
45
+ position: "absolute",
46
+ width: 1,
47
+ height: 1,
48
+ overflow: "hidden",
49
+ opacity: 0,
50
+ };
51
+
52
+ function useTableTheme() {
53
+ return useHostTheme() || DEFAULT_THEME_TOKENS;
54
+ }
55
+
56
+ function listOverrides(props) {
57
+ const out = {};
58
+ for (const name of LIST_TOKEN_NAMES) {
59
+ if (props[name] !== undefined && props[name] !== null) out[name] = props[name];
60
+ }
61
+ return out;
62
+ }
63
+
64
+ function testId(base, part) {
65
+ return base ? `${base}-${part}` : undefined;
66
+ }
67
+
68
+ function cellBox(column) {
69
+ return { flex: column.flex, minWidth: column.minWidth, justifyContent: "center" };
70
+ }
71
+
72
+ /**
73
+ * Bind the shared table to one platform's primitives and parts.
74
+ *
75
+ * @param {{View: any, Text: any, Pressable: any, ScrollView: any, Platform: any,
76
+ * AccessibilityInfo: any}} rn
77
+ * @param {{List: any, ListRow: any, ListHeader: any, EmptyState: any,
78
+ * Skeleton: any, Menu: any, Button: any}} parts — the same host's components.
79
+ */
80
+ export function makeDataTable(rn, parts) {
81
+ const { View, Text, Pressable, ScrollView, Platform, AccessibilityInfo } = rn;
82
+ const { List, ListRow, ListHeader, EmptyState, Skeleton, Menu, Button } = parts;
83
+ const web = Platform.OS === "web";
84
+
85
+ // A string or number becomes text in the slot's style; a node is drawn as given.
86
+ function cellNode(column, row, renderCell, style, lines) {
87
+ const own = column.render
88
+ ? column.render(row, column)
89
+ : typeof renderCell === "function"
90
+ ? renderCell(row, column)
91
+ : undefined;
92
+ const value = own !== undefined ? own : cellText(row && row[column.key]) || "—";
93
+ if (typeof value !== "string" && typeof value !== "number") return value;
94
+ return h(Text, { style: [style, { textAlign: column.align }], numberOfLines: lines }, value);
95
+ }
96
+
97
+ // `change` is null when nobody can act on a sort: a controlled one without
98
+ // `onSortChange` would be announced but never happen.
99
+ function useSort({ sort, defaultSort, onSortChange, labels, columns }) {
100
+ const [own, setOwn] = React.useState(() => normaliseSort(defaultSort));
101
+ const [spoken, setSpoken] = React.useState("");
102
+ const controlled = sort !== undefined;
103
+ const current = controlled ? normaliseSort(sort) : own;
104
+ const reported = typeof onSortChange === "function";
105
+ // The web refocuses the menu's trigger, whose name then says the sort.
106
+ const change = (key, { announce = true } = {}) => {
107
+ const next = nextSort(current, key);
108
+ if (!controlled) setOwn(next);
109
+ if (reported) onSortChange(next);
110
+ if (!announce) return;
111
+ const column = columns.find((c) => c.key === key);
112
+ const text = sortSpokenLabel(String(column ? column.label : key), { key }, next, labels);
113
+ if (web) setSpoken(text);
114
+ else if (AccessibilityInfo && typeof AccessibilityInfo.announceForAccessibility === "function") {
115
+ AccessibilityInfo.announceForAccessibility(text);
116
+ }
117
+ };
118
+ return [current, controlled && !reported ? null : change, spoken];
119
+ }
120
+
121
+ // The sort state is the button's description, so the tap's announcement
122
+ // is the one thing a reader says when it changes.
123
+ function HeaderCell({ column, sort, onSort, labels, testID }) {
124
+ const { colors, typography } = useTableTheme();
125
+ const stateId = React.useId();
126
+ const label = h(
127
+ Text,
128
+ {
129
+ style: [textRoleStyle("overline", typography, colors), { textAlign: column.align, flexShrink: 1 }],
130
+ numberOfLines: 1,
131
+ },
132
+ column.label,
133
+ );
134
+ if (!column.sortable || !onSort) return h(View, { style: cellBox(column) }, label);
135
+ const state = ariaSortFor(column, sort);
136
+ const words = sortStateWords(column, sort, labels);
137
+ const described = words ? (web ? { "aria-describedby": stateId } : { accessibilityHint: words }) : null;
138
+ return h(
139
+ View,
140
+ { style: cellBox(column) },
141
+ web && words ? h(Text, { nativeID: stateId, "aria-hidden": true, style: VISUALLY_HIDDEN }, words) : null,
142
+ h(
143
+ Pressable,
144
+ {
145
+ onPress: () => onSort(column.key),
146
+ accessibilityRole: "button",
147
+ accessibilityLabel: String(column.label),
148
+ ...described,
149
+ testID: testId(testID, `sort-${column.key}`),
150
+ style: {
151
+ minHeight: DATA_TABLE_HEADER_TARGET,
152
+ flexDirection: "row",
153
+ alignItems: "center",
154
+ justifyContent: justifyFor(column.align),
155
+ gap: 4,
156
+ },
157
+ },
158
+ label,
159
+ h(Icon, {
160
+ name: SORT_GLYPHS[state],
161
+ size: SORT_ICON_SIZE,
162
+ color: state === "none" ? colors.onSurfaceMuted : colors.onSurface,
163
+ }),
164
+ ),
165
+ );
166
+ }
167
+
168
+ // Inside a card's scope, so the text takes that card's own inks.
169
+ function StackedBody({ columns, row, renderCell }) {
170
+ const { colors, typography, spacing } = useTableTheme();
171
+ const caption = textRoleStyle("caption", typography, colors);
172
+ const body = textRoleStyle("body", typography, colors);
173
+ return h(
174
+ View,
175
+ { style: { flex: 1, minWidth: 0, gap: spacing.xs } },
176
+ ...columns.map((column) =>
177
+ h(
178
+ View,
179
+ { key: column.key, style: { gap: 2 } },
180
+ h(Text, { style: caption }, column.label),
181
+ cellNode({ ...column, align: "left" }, row, renderCell, body, undefined),
182
+ ),
183
+ ),
184
+ );
185
+ }
186
+
187
+ function CardBody({ columns, row, renderCell }) {
188
+ const { colors, typography, spacing } = useTableTheme();
189
+ const [first, ...details] = columns;
190
+ const caption = textRoleStyle("caption", typography, colors);
191
+ const body = textRoleStyle("body", typography, colors);
192
+ return h(
193
+ View,
194
+ { style: { flex: 1, minWidth: 0, gap: 2 } },
195
+ cellNode({ ...first, align: "left" }, row, renderCell, textRoleStyle("subtitle", typography, colors), 1),
196
+ details.length > 0
197
+ ? h(
198
+ View,
199
+ { style: { flexDirection: "row", alignItems: "center", gap: spacing.md, minWidth: 0 } },
200
+ ...details.map((column) =>
201
+ h(
202
+ View,
203
+ {
204
+ key: column.key,
205
+ style: { flexDirection: "row", alignItems: "center", gap: spacing.xs, flexShrink: 1, minWidth: 0 },
206
+ },
207
+ h(Text, { style: caption, numberOfLines: 1 }, column.label),
208
+ cellNode({ ...column, align: "left" }, row, renderCell, body, 1),
209
+ ),
210
+ ),
211
+ )
212
+ : null,
213
+ );
214
+ }
215
+
216
+ function ColumnCells({ columns, row, renderCell }) {
217
+ const { colors, typography } = useTableTheme();
218
+ const body = textRoleStyle("body", typography, colors);
219
+ return columns.map((column) =>
220
+ h(View, { key: column.key, style: cellBox(column) }, cellNode(column, row, renderCell, body, 1)),
221
+ );
222
+ }
223
+
224
+ function rowsOf(table, layout) {
225
+ const { columns, rows, keyField, renderCell, onRowPress, overrides, testID } = table;
226
+ const Body = layout === "stacked" ? StackedBody : layout === "cards" ? CardBody : ColumnCells;
227
+ return h(
228
+ List,
229
+ { ...overrides, variant: layout === "columns" ? "plain" : "card" },
230
+ ...rows.map((row, index) => {
231
+ const key = rowKey(row, keyField, index);
232
+ return h(
233
+ ListRow,
234
+ {
235
+ key,
236
+ onPress: typeof onRowPress === "function" ? () => onRowPress(row) : undefined,
237
+ testID: testId(testID, `row-${key}`),
238
+ },
239
+ h(Body, { columns, row, renderCell }),
240
+ );
241
+ }),
242
+ );
243
+ }
244
+
245
+ function bodyOf(table, layout) {
246
+ if (table.loading && table.rows.length === 0) {
247
+ return h(Skeleton, { shape: "listRow", count: DATA_TABLE_SKELETON_ROWS, leading: false });
248
+ }
249
+ if (table.rows.length === 0) {
250
+ return h(EmptyState, {
251
+ title: table.emptyTitle,
252
+ message: table.emptyMessage,
253
+ testID: testId(table.testID, "empty"),
254
+ });
255
+ }
256
+ return rowsOf(table, layout);
257
+ }
258
+
259
+ function SortMenu({ table }) {
260
+ const { columns, sort, onSort, labels, testID } = table;
261
+ const sortable = columns.filter((c) => c.sortable);
262
+ if (sortable.length === 0 || !onSort) return null;
263
+ const active = sort ? sortable.find((c) => c.key === sort.key) : undefined;
264
+ const state = active ? ariaSortFor(active, sort) : "none";
265
+ const spoken = active ? sortSpokenLabel(String(active.label), active, sort, labels) : "";
266
+ return h(
267
+ View,
268
+ { style: { flexDirection: "row", justifyContent: "flex-end" } },
269
+ h(Menu, {
270
+ accessibilityLabel: labels.sort,
271
+ testID: testId(testID, "sort-menu"),
272
+ trigger: h(Button, {
273
+ variant: "ghost",
274
+ title: active ? `${labels.sort}: ${active.label}` : labels.sort,
275
+ icon: SORT_GLYPHS[state],
276
+ iconPosition: "trailing",
277
+ accessibilityLabel: active ? `${labels.sort}: ${spoken}` : labels.sort,
278
+ testID: testId(testID, "sort"),
279
+ }),
280
+ items: sortable.map((column) => ({
281
+ key: column.key,
282
+ label: String(column.label),
283
+ icon: active && active.key === column.key ? SORT_GLYPHS[state] : undefined,
284
+ accessibilityLabel: sortSpokenLabel(String(column.label), column, sort, labels),
285
+ onPress: () => onSort(column.key, { announce: !web }),
286
+ testID: testId(testID, `sort-${column.key}`),
287
+ })),
288
+ }),
289
+ );
290
+ }
291
+
292
+ // Opaque, or scrolled rows show through: the rows' own Lists fill first.
293
+ function headerFill(theme, overrides) {
294
+ return resolveListTokens(theme, overrides).background || theme.colors.surface;
295
+ }
296
+
297
+ // Inside the page's scroller; Android scrolls only the outer one without it.
298
+ function ColumnsLayout({ table }) {
299
+ const theme = useTableTheme();
300
+ return h(
301
+ ScrollView,
302
+ {
303
+ style: { maxHeight: table.maxHeight },
304
+ nestedScrollEnabled: true,
305
+ stickyHeaderIndices: [0],
306
+ testID: testId(table.testID, "scroll"),
307
+ },
308
+ h(
309
+ ListHeader,
310
+ {
311
+ ...table.overrides,
312
+ style: { backgroundColor: headerFill(theme, table.overrides) },
313
+ testID: testId(table.testID, "header"),
314
+ },
315
+ ...table.columns.map((column) =>
316
+ h(HeaderCell, {
317
+ key: column.key,
318
+ column,
319
+ sort: table.sort,
320
+ onSort: table.onSort,
321
+ labels: table.labels,
322
+ testID: table.testID,
323
+ }),
324
+ ),
325
+ ),
326
+ bodyOf(table, "columns"),
327
+ );
328
+ }
329
+
330
+ function CardsLayout({ table, layout }) {
331
+ const { spacing } = useTableTheme();
332
+ return h(
333
+ View,
334
+ { style: { gap: spacing.sm } },
335
+ h(SortMenu, { table }),
336
+ h(
337
+ ScrollView,
338
+ { style: { maxHeight: table.maxHeight }, nestedScrollEnabled: true, testID: testId(table.testID, "scroll") },
339
+ bodyOf(table, layout),
340
+ ),
341
+ );
342
+ }
343
+
344
+ /**
345
+ * @param {object} props — see `DataTable` in index.d.ts.
346
+ */
347
+ function DataTable(props) {
348
+ const { rows, keyField = "id", sort, defaultSort, onSortChange, sortLabels, style, testID } = props;
349
+ const [width, onLayout] = useContainerWidth();
350
+ const layout = dataTableLayout(width);
351
+ const columns = normaliseColumns(props.columns);
352
+ const labels = { ...DEFAULT_SORT_LABELS, ...sortLabels };
353
+ const [current, onSort, spoken] = useSort({ sort, defaultSort, onSortChange, labels, columns });
354
+ const given = Array.isArray(rows) ? rows : [];
355
+ const emptyFallback = useHostSharedText("no_results", EMPTY_FALLBACK);
356
+ const table = {
357
+ columns,
358
+ rows: sortsClientSide(onSortChange) ? sortRows(given, current) : given,
359
+ keyField,
360
+ renderCell: props.renderCell,
361
+ onRowPress: props.onRowPress,
362
+ sort: current,
363
+ onSort,
364
+ labels,
365
+ loading: props.loading === true,
366
+ emptyTitle: props.emptyTitle,
367
+ emptyMessage: props.emptyMessage ?? (props.emptyTitle ? undefined : emptyFallback),
368
+ maxHeight: resolveMaxHeight(props.maxHeight),
369
+ overrides: listOverrides(props),
370
+ testID,
371
+ };
372
+ const content =
373
+ columns.length === 0
374
+ ? null
375
+ : layout === "columns"
376
+ ? h(ColumnsLayout, { table })
377
+ : h(CardsLayout, { table, layout });
378
+ return h(
379
+ View,
380
+ { testID, accessibilityLabel: props.accessibilityLabel, style: [{ alignSelf: "stretch" }, style] },
381
+ h(View, { onLayout }, content),
382
+ web ? h(Text, { "aria-live": "polite", style: VISUALLY_HIDDEN }, spoken) : null,
383
+ );
384
+ }
385
+ DataTable.displayName = "DataTable";
386
+
387
+ return DataTable;
388
+ }
@@ -0,0 +1,22 @@
1
+ // `<DataTable>` (web). Binds ./data-table-view.js to react-native-web and the
2
+ // web list, loading, menu and button parts; see ./primitives.js for why not
3
+ // the bare specifier.
4
+
5
+ import * as ReactNative from "react-native-web";
6
+ import { List, ListRow, ListHeader, EmptyState } from "./list.js";
7
+ import { Skeleton } from "./loading.js";
8
+ import { Menu } from "./popover.js";
9
+ import { Button } from "./button.js";
10
+ import { makeDataTable } from "./data-table-view.js";
11
+
12
+ export const DataTable = makeDataTable(
13
+ {
14
+ View: ReactNative.View,
15
+ Text: ReactNative.Text,
16
+ Pressable: ReactNative.Pressable,
17
+ ScrollView: ReactNative.ScrollView,
18
+ Platform: ReactNative.Platform,
19
+ AccessibilityInfo: ReactNative.AccessibilityInfo,
20
+ },
21
+ { List, ListRow, ListHeader, EmptyState, Skeleton, Menu, Button },
22
+ );
@@ -0,0 +1,14 @@
1
+ // `<DataTable>` (native). Binds ./data-table-view.js to React Native's own
2
+ // primitives and the native parts; only the imports differ from ./data-table.js.
3
+
4
+ import { View, Text, Pressable, ScrollView, Platform, AccessibilityInfo } from "react-native";
5
+ import { List, ListRow, ListHeader, EmptyState } from "./list.native.js";
6
+ import { Skeleton } from "./loading.native.js";
7
+ import { Menu } from "./popover.native.js";
8
+ import { Button } from "./button.native.js";
9
+ import { makeDataTable } from "./data-table-view.js";
10
+
11
+ export const DataTable = makeDataTable(
12
+ { View, Text, Pressable, ScrollView, Platform, AccessibilityInfo },
13
+ { List, ListRow, ListHeader, EmptyState, Skeleton, Menu, Button },
14
+ );