@lotics/ui 1.22.0 → 1.23.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/package.json +3 -4
- package/src/{grid/column_filter.tsx → column_filter.tsx} +41 -57
- package/src/table.tsx +9 -1
- package/src/table.web.tsx +18 -11
- package/src/{grid/data_grid_picker.tsx → table_picker.tsx} +96 -118
- package/src/table_types.ts +5 -0
- package/src/grid/sortable_header_cell.tsx +0 -58
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./tokens": "./src/tokens.ts",
|
|
@@ -151,9 +151,8 @@
|
|
|
151
151
|
"./grid/data_grid": "./src/grid/data_grid.tsx",
|
|
152
152
|
"./grid/data_grid_context": "./src/grid/data_grid_context.ts",
|
|
153
153
|
"./grid/search_highlight": "./src/grid/search_highlight.ts",
|
|
154
|
-
"./
|
|
155
|
-
"./
|
|
156
|
-
"./grid/data_grid_picker": "./src/grid/data_grid_picker.tsx"
|
|
154
|
+
"./column_filter": "./src/column_filter.tsx",
|
|
155
|
+
"./table_picker": "./src/table_picker.tsx"
|
|
157
156
|
},
|
|
158
157
|
"files": [
|
|
159
158
|
"src"
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { Popover, PopoverTrigger, PopoverContent } from "
|
|
10
|
-
import type { PickerOption } from "
|
|
1
|
+
import { View, StyleSheet } from "react-native";
|
|
2
|
+
import { Text } from "./text";
|
|
3
|
+
import { Icon } from "./icon";
|
|
4
|
+
import { colors } from "./colors";
|
|
5
|
+
import { TextInputField } from "./text_input_field";
|
|
6
|
+
import { NumberInput } from "./number_input";
|
|
7
|
+
import { PickerMenu } from "./picker_menu";
|
|
8
|
+
import { PillButton } from "./pill_button";
|
|
9
|
+
import { Popover, PopoverTrigger, PopoverContent } from "./popover";
|
|
10
|
+
import type { PickerOption } from "./picker";
|
|
11
11
|
|
|
12
12
|
/** A column the picker can filter on. `type` selects the control + operators. */
|
|
13
13
|
export interface FilterableColumn {
|
|
@@ -72,18 +72,39 @@ export function isColumnFilterActive(value: ColumnFilterValue | undefined): bool
|
|
|
72
72
|
return value.selected.length > 0;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/** A short human summary of an active value, for the pill label ("Vàng, Đỏ", "≥ 10"). */
|
|
76
|
+
export function columnFilterSummary(
|
|
77
|
+
column: FilterableColumn,
|
|
78
|
+
value: ColumnFilterValue | undefined,
|
|
79
|
+
): string {
|
|
80
|
+
if (!value) return "";
|
|
81
|
+
if (value.kind === "text") return value.query.trim();
|
|
82
|
+
if (value.kind === "number") {
|
|
83
|
+
const { min, max } = value;
|
|
84
|
+
if (min != null && max != null) return `${min}–${max}`;
|
|
85
|
+
if (min != null) return `≥ ${min}`;
|
|
86
|
+
if (max != null) return `≤ ${max}`;
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
return value.selected
|
|
90
|
+
.map((k) => column.options?.find((o) => o.value === k)?.label ?? k)
|
|
91
|
+
.join(", ");
|
|
92
|
+
}
|
|
93
|
+
|
|
75
94
|
export interface ColumnFilterProps {
|
|
76
95
|
column: FilterableColumn;
|
|
77
96
|
value: ColumnFilterValue | undefined;
|
|
78
97
|
onChange: (value: ColumnFilterValue | undefined) => void;
|
|
79
|
-
/** Accessible name for the clear control. Pass a translated string. Default "Clear". */
|
|
98
|
+
/** Accessible name for the clear (X) control. Pass a translated string. Default "Clear". */
|
|
80
99
|
clearLabel?: string;
|
|
81
100
|
}
|
|
82
101
|
|
|
83
102
|
/**
|
|
84
|
-
* A
|
|
85
|
-
*
|
|
86
|
-
*
|
|
103
|
+
* A toolbar-style filter pill: the same `PillButton` the table toolbar uses, so
|
|
104
|
+
* a picker's filters read identically. Inactive → "Label ⌄"; active →
|
|
105
|
+
* "Label: summary" with an X to clear. Pressing the pill opens a type-aware
|
|
106
|
+
* editor (text contains / number range / multi-select). Controlled — the
|
|
107
|
+
* consumer holds the `ColumnFilterValue` and maps it to query conditions via
|
|
87
108
|
* `columnFilterToConditions`. Pure UI; no data layer.
|
|
88
109
|
*/
|
|
89
110
|
export function ColumnFilter(props: ColumnFilterProps) {
|
|
@@ -93,17 +114,15 @@ export function ColumnFilter(props: ColumnFilterProps) {
|
|
|
93
114
|
return (
|
|
94
115
|
<Popover side="bottom" align="start">
|
|
95
116
|
<PopoverTrigger>
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
style={[styles.pill, active && styles.pillActive]}
|
|
117
|
+
<PillButton
|
|
118
|
+
onDismiss={active ? () => onChange(undefined) : undefined}
|
|
119
|
+
dismissTooltip={clearLabel}
|
|
100
120
|
>
|
|
101
|
-
<Icon name="list-filter" size={13} color={active ? colors.zinc["950"] : colors.zinc["500"]} />
|
|
102
121
|
<Text size="sm" weight="medium" color={active ? "zinc-900" : "zinc-700"} numberOfLines={1}>
|
|
103
|
-
{column.label}
|
|
122
|
+
{active ? `${column.label}: ${columnFilterSummary(column, value)}` : column.label}
|
|
104
123
|
</Text>
|
|
105
|
-
<Icon name="chevron-down" size={14} color={colors.zinc["400"]} />
|
|
106
|
-
</
|
|
124
|
+
{!active ? <Icon name="chevron-down" size={14} color={colors.zinc["400"]} /> : null}
|
|
125
|
+
</PillButton>
|
|
107
126
|
</PopoverTrigger>
|
|
108
127
|
<PopoverContent style={styles.content}>
|
|
109
128
|
{column.type === "text" ? (
|
|
@@ -142,40 +161,12 @@ export function ColumnFilter(props: ColumnFilterProps) {
|
|
|
142
161
|
onValueChange={(selected) => onChange({ kind: "select", selected })}
|
|
143
162
|
/>
|
|
144
163
|
)}
|
|
145
|
-
{active ? (
|
|
146
|
-
<PressableHighlight
|
|
147
|
-
accessibilityRole="button"
|
|
148
|
-
accessibilityLabel={clearLabel}
|
|
149
|
-
onPress={() => onChange(undefined)}
|
|
150
|
-
style={styles.clear}
|
|
151
|
-
>
|
|
152
|
-
<Icon name="x" size={14} color={colors.zinc["500"]} />
|
|
153
|
-
<Text size="sm" color="zinc-500">
|
|
154
|
-
{clearLabel}
|
|
155
|
-
</Text>
|
|
156
|
-
</PressableHighlight>
|
|
157
|
-
) : null}
|
|
158
164
|
</PopoverContent>
|
|
159
165
|
</Popover>
|
|
160
166
|
);
|
|
161
167
|
}
|
|
162
168
|
|
|
163
169
|
const styles = StyleSheet.create({
|
|
164
|
-
pill: {
|
|
165
|
-
flexDirection: "row",
|
|
166
|
-
alignItems: "center",
|
|
167
|
-
gap: 5,
|
|
168
|
-
height: 32,
|
|
169
|
-
paddingHorizontal: 10,
|
|
170
|
-
borderRadius: 8,
|
|
171
|
-
borderWidth: 1,
|
|
172
|
-
borderColor: colors.border,
|
|
173
|
-
backgroundColor: colors.background,
|
|
174
|
-
},
|
|
175
|
-
pillActive: {
|
|
176
|
-
borderColor: colors.zinc["400"],
|
|
177
|
-
backgroundColor: colors.zinc["100"],
|
|
178
|
-
},
|
|
179
170
|
content: {
|
|
180
171
|
minWidth: 240,
|
|
181
172
|
gap: 8,
|
|
@@ -185,11 +176,4 @@ const styles = StyleSheet.create({
|
|
|
185
176
|
alignItems: "center",
|
|
186
177
|
gap: 8,
|
|
187
178
|
},
|
|
188
|
-
clear: {
|
|
189
|
-
flexDirection: "row",
|
|
190
|
-
alignItems: "center",
|
|
191
|
-
gap: 6,
|
|
192
|
-
paddingVertical: 6,
|
|
193
|
-
paddingHorizontal: 4,
|
|
194
|
-
},
|
|
195
179
|
});
|
package/src/table.tsx
CHANGED
|
@@ -16,7 +16,7 @@ const stickyHeader = [0];
|
|
|
16
16
|
const CHEVRON_W = 44;
|
|
17
17
|
|
|
18
18
|
export function Table<TRow extends Record<string, unknown>>(props: TableProps<TRow>) {
|
|
19
|
-
const { columns, rows, rowKey, rowStyle, sort, onSortChange, renderDetail, expandedKeys, onToggleRow } = props;
|
|
19
|
+
const { columns, rows, rowKey, rowStyle, sort, onSortChange, renderDetail, expandedKeys, onToggleRow, onRowPress } = props;
|
|
20
20
|
const [internal, setInternal] = useState<Set<string>>(() => new Set());
|
|
21
21
|
const expanded = expandedKeys ?? internal;
|
|
22
22
|
const expandable = !!renderDetail;
|
|
@@ -89,6 +89,14 @@ export function Table<TRow extends Record<string, unknown>>(props: TableProps<TR
|
|
|
89
89
|
<Icon name={isOpen ? "chevron-up" : "chevron-down"} size={18} color={colors.zinc[400]} />
|
|
90
90
|
</View>
|
|
91
91
|
</Pressable>
|
|
92
|
+
) : onRowPress ? (
|
|
93
|
+
<Pressable
|
|
94
|
+
accessibilityRole="button"
|
|
95
|
+
onPress={() => onRowPress(row)}
|
|
96
|
+
style={({ pressed }) => [styles.bodyRow, pressed ? styles.rowPressed : null, extra]}
|
|
97
|
+
>
|
|
98
|
+
{cells}
|
|
99
|
+
</Pressable>
|
|
92
100
|
) : (
|
|
93
101
|
<View style={[styles.bodyRow, extra]}>{cells}</View>
|
|
94
102
|
)}
|
package/src/table.web.tsx
CHANGED
|
@@ -19,11 +19,14 @@ function colWidth<TRow extends Record<string, unknown>>(col: Column<TRow>): CSSP
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export function Table<TRow extends Record<string, unknown>>(props: TableProps<TRow>) {
|
|
22
|
-
const { columns, rows, rowKey, rowStyle, sort, onSortChange, renderDetail, expandedKeys, onToggleRow } = props;
|
|
22
|
+
const { columns, rows, rowKey, rowStyle, sort, onSortChange, renderDetail, expandedKeys, onToggleRow, onRowPress } = props;
|
|
23
23
|
const [internal, setInternal] = useState<Set<string>>(() => new Set());
|
|
24
24
|
const [hoverKey, setHoverKey] = useState<string | null>(null);
|
|
25
25
|
const expanded = expandedKeys ?? internal;
|
|
26
26
|
const expandable = !!renderDetail;
|
|
27
|
+
// A row reacts to clicks for one of two reasons: expansion (renderDetail) or
|
|
28
|
+
// selection (onRowPress). Expansion wins if both are set.
|
|
29
|
+
const pressable = expandable || !!onRowPress;
|
|
27
30
|
|
|
28
31
|
const toggle = (key: string, row: TRow) => {
|
|
29
32
|
onToggleRow?.(key, row);
|
|
@@ -97,8 +100,8 @@ export function Table<TRow extends Record<string, unknown>>(props: TableProps<TR
|
|
|
97
100
|
const extra = (rowStyle?.(row) as CSSProperties | undefined) ?? undefined;
|
|
98
101
|
const rowStyleFinal: CSSProperties = {
|
|
99
102
|
...bodyRowStyle,
|
|
100
|
-
cursor:
|
|
101
|
-
background: hoverKey === key &&
|
|
103
|
+
cursor: pressable ? "pointer" : "default",
|
|
104
|
+
background: hoverKey === key && pressable ? colors.zinc[50] : colors.white,
|
|
102
105
|
...extra,
|
|
103
106
|
};
|
|
104
107
|
return (
|
|
@@ -106,18 +109,19 @@ export function Table<TRow extends Record<string, unknown>>(props: TableProps<TR
|
|
|
106
109
|
<div
|
|
107
110
|
role="row"
|
|
108
111
|
onClick={
|
|
109
|
-
|
|
112
|
+
pressable
|
|
110
113
|
? (e: React.MouseEvent) => {
|
|
111
|
-
// Whole-row click
|
|
112
|
-
// (pickers/buttons) — they keep their own behaviour.
|
|
113
|
-
// chevron is the keyboard/AT affordance.
|
|
114
|
+
// Whole-row click acts (expand or select), EXCEPT clicks inside an
|
|
115
|
+
// interactive cell (pickers/buttons) — they keep their own behaviour.
|
|
116
|
+
// The disclosure chevron is the keyboard/AT affordance for expansion.
|
|
114
117
|
if ((e.target as HTMLElement).closest("[data-interactive]")) return;
|
|
115
|
-
toggle(key, row);
|
|
118
|
+
if (expandable) toggle(key, row);
|
|
119
|
+
else onRowPress?.(row);
|
|
116
120
|
}
|
|
117
121
|
: undefined
|
|
118
122
|
}
|
|
119
|
-
onMouseEnter={
|
|
120
|
-
onMouseLeave={
|
|
123
|
+
onMouseEnter={pressable ? () => setHoverKey(key) : undefined}
|
|
124
|
+
onMouseLeave={pressable ? () => setHoverKey((k) => (k === key ? null : k)) : undefined}
|
|
121
125
|
style={rowStyleFinal}
|
|
122
126
|
>
|
|
123
127
|
{columns.map((col) => (
|
|
@@ -165,7 +169,10 @@ export function Table<TRow extends Record<string, unknown>>(props: TableProps<TR
|
|
|
165
169
|
);
|
|
166
170
|
}
|
|
167
171
|
|
|
168
|
-
|
|
172
|
+
// `maxHeight: 100%` is a no-op in a content-sized parent (a card grows to its
|
|
173
|
+
// rows) but caps the table to a bounded parent (a modal's flex region), so
|
|
174
|
+
// `overflow: auto` then scrolls and the sticky header engages.
|
|
175
|
+
const containerStyle: CSSProperties = { width: "100%", maxHeight: "100%", overflow: "auto" };
|
|
169
176
|
const headerRowStyle: CSSProperties = {
|
|
170
177
|
display: "flex",
|
|
171
178
|
position: "sticky",
|
|
@@ -1,50 +1,46 @@
|
|
|
1
1
|
import { useCallback, useMemo } from "react";
|
|
2
2
|
import { StyleSheet, View } from "react-native";
|
|
3
|
-
import { colors } from "
|
|
4
|
-
import { Text } from "
|
|
5
|
-
import { SearchInput } from "
|
|
6
|
-
import { ActivityIndicator } from "
|
|
7
|
-
import { Dialog } from "
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
ColumnFilter,
|
|
13
|
-
type FilterableColumn,
|
|
14
|
-
type ColumnFilterValue,
|
|
15
|
-
} from "./column_filter";
|
|
3
|
+
import { colors } from "./colors";
|
|
4
|
+
import { Text } from "./text";
|
|
5
|
+
import { SearchInput } from "./search_input";
|
|
6
|
+
import { ActivityIndicator } from "./activity_indicator";
|
|
7
|
+
import { Dialog } from "./dialog";
|
|
8
|
+
import { Table } from "@lotics/ui/table";
|
|
9
|
+
import type { Column, TableSort } from "@lotics/ui/table_types";
|
|
10
|
+
import { Pagination } from "./pagination";
|
|
11
|
+
import { ColumnFilter, type FilterableColumn, type ColumnFilterValue } from "./column_filter";
|
|
16
12
|
|
|
17
|
-
/** A grid column the picker displays. Sorting
|
|
18
|
-
export interface PickerColumn<TRow
|
|
13
|
+
/** A grid column the picker displays. Sorting is opt-in per column. */
|
|
14
|
+
export interface PickerColumn<TRow extends Record<string, unknown>> {
|
|
19
15
|
key: string;
|
|
20
16
|
label: string;
|
|
21
|
-
/** Fixed pixel width. Omit to
|
|
17
|
+
/** Fixed pixel width. Omit to flex. */
|
|
22
18
|
width?: number;
|
|
19
|
+
align?: "left" | "right";
|
|
23
20
|
/** Show a sortable header for this column. Requires `sort`/`onSortChange`. */
|
|
24
21
|
sortable?: boolean;
|
|
25
|
-
/**
|
|
26
|
-
* Cell content for this column. Default: a single-line text of the row's
|
|
27
|
-
* value at `key` (objects render their `label`/`display`/`name`).
|
|
28
|
-
*/
|
|
22
|
+
/** Cell content. Default: the row's value at `key` rendered as text. */
|
|
29
23
|
renderCell?: (row: TRow) => React.ReactNode;
|
|
30
24
|
}
|
|
31
25
|
|
|
32
26
|
/** Single-column sort state — the picker sorts by one column at a time. */
|
|
33
|
-
export interface
|
|
27
|
+
export interface TablePickerSort {
|
|
34
28
|
key: string;
|
|
35
|
-
order:
|
|
29
|
+
order: "asc" | "desc";
|
|
36
30
|
}
|
|
37
31
|
|
|
38
|
-
export interface
|
|
32
|
+
export interface TablePickerProps<TRow extends Record<string, unknown>> {
|
|
39
33
|
open: boolean;
|
|
40
34
|
onOpenChange: (open: boolean) => void;
|
|
41
35
|
/** Heading shown above the search box. */
|
|
42
36
|
title?: string;
|
|
43
37
|
|
|
44
38
|
columns: PickerColumn<TRow>[];
|
|
39
|
+
/** Rows of the current page (the consumer paginates). */
|
|
45
40
|
rows: TRow[];
|
|
46
|
-
/**
|
|
47
|
-
|
|
41
|
+
/** The field holding each row's stable id — returned by selection and used to
|
|
42
|
+
* highlight the picked row (e.g. `"__source_record_id"`). */
|
|
43
|
+
rowKey: keyof TRow & string;
|
|
48
44
|
|
|
49
45
|
/** Currently selected row id, or null. Highlights the matching row. */
|
|
50
46
|
value: string | null;
|
|
@@ -59,16 +55,15 @@ export interface DataGridPickerProps<TRow extends Record<string, unknown>> {
|
|
|
59
55
|
searchPlaceholder?: string;
|
|
60
56
|
|
|
61
57
|
/**
|
|
62
|
-
* Controlled single-column sort. Omit `onSortChange` to disable sorting
|
|
63
|
-
*
|
|
58
|
+
* Controlled single-column sort. Omit `onSortChange` to disable sorting.
|
|
59
|
+
* Toggling a column cycles asc → desc → off.
|
|
64
60
|
*/
|
|
65
|
-
sort?:
|
|
66
|
-
onSortChange?: (sort:
|
|
61
|
+
sort?: TablePickerSort | null;
|
|
62
|
+
onSortChange?: (sort: TablePickerSort | null) => void;
|
|
67
63
|
|
|
68
64
|
/**
|
|
69
|
-
* Per-column
|
|
70
|
-
*
|
|
71
|
-
* `columnFilterToConditions`.
|
|
65
|
+
* Per-column filter pills. The consumer holds `filterValues` (keyed by column
|
|
66
|
+
* key) and maps them to query conditions via `columnFilterToConditions`.
|
|
72
67
|
*/
|
|
73
68
|
filters?: FilterableColumn[];
|
|
74
69
|
filterValues?: Record<string, ColumnFilterValue>;
|
|
@@ -76,15 +71,18 @@ export interface DataGridPickerProps<TRow extends Record<string, unknown>> {
|
|
|
76
71
|
/** Accessible name for the per-filter clear control. Pass a translated string. */
|
|
77
72
|
clearLabel?: string;
|
|
78
73
|
|
|
79
|
-
/**
|
|
80
|
-
|
|
74
|
+
/** Pagination (page-model — the consumer owns the page cursor). */
|
|
75
|
+
page: number;
|
|
76
|
+
pageSize: number;
|
|
77
|
+
/** Total rows across all pages, when known — drives "Page 1 of N". */
|
|
78
|
+
total?: number;
|
|
79
|
+
hasMore: boolean;
|
|
80
|
+
onPageChange: (page: number) => void;
|
|
81
81
|
|
|
82
|
-
/** A request is in flight.
|
|
82
|
+
/** A request is in flight. */
|
|
83
83
|
loading?: boolean;
|
|
84
84
|
/** Message when there are no rows and nothing is loading. */
|
|
85
85
|
emptyLabel?: string;
|
|
86
|
-
|
|
87
|
-
rowHeight?: number;
|
|
88
86
|
testID?: string;
|
|
89
87
|
}
|
|
90
88
|
|
|
@@ -103,22 +101,20 @@ function defaultCellText(value: unknown): string {
|
|
|
103
101
|
return "";
|
|
104
102
|
}
|
|
105
103
|
|
|
106
|
-
const DEFAULT_ROW_HEIGHT = 48;
|
|
107
|
-
|
|
108
104
|
/**
|
|
109
|
-
* A data-agnostic record-style picker: a modal holding a
|
|
110
|
-
*
|
|
111
|
-
* about records/tables/fields — the consumer supplies `columns` + `rows`
|
|
112
|
-
* owns the search/sort/filter/
|
|
113
|
-
* server
|
|
114
|
-
* domain hook (e.g. an app's `
|
|
115
|
-
* with any other row source for a different purpose.
|
|
105
|
+
* A data-agnostic record-style picker: a modal holding a table the user can
|
|
106
|
+
* browse (numbered pages), search, sort, and filter, then pick one row. It knows
|
|
107
|
+
* nothing about records/tables/fields — the consumer supplies `columns` + `rows`
|
|
108
|
+
* (one page) and owns the search/sort/filter/page state (so the same component
|
|
109
|
+
* drives a server query, an in-memory list, or any other source). Compose it
|
|
110
|
+
* with a domain hook (e.g. an app's `usePaginatedQuery`) to build a record
|
|
111
|
+
* picker, or with any other row source for a different purpose.
|
|
116
112
|
*
|
|
117
|
-
* Single-select: clicking
|
|
118
|
-
*
|
|
113
|
+
* Single-select: clicking a row picks its id; the selected row is highlighted;
|
|
114
|
+
* `closeOnSelect` (default) dismisses the modal on pick.
|
|
119
115
|
*/
|
|
120
|
-
export function
|
|
121
|
-
props:
|
|
116
|
+
export function TablePicker<TRow extends Record<string, unknown>>(
|
|
117
|
+
props: TablePickerProps<TRow>,
|
|
122
118
|
) {
|
|
123
119
|
const {
|
|
124
120
|
open,
|
|
@@ -126,7 +122,7 @@ export function DataGridPicker<TRow extends Record<string, unknown>>(
|
|
|
126
122
|
title,
|
|
127
123
|
columns,
|
|
128
124
|
rows,
|
|
129
|
-
|
|
125
|
+
rowKey,
|
|
130
126
|
value,
|
|
131
127
|
onValueChange,
|
|
132
128
|
closeOnSelect = true,
|
|
@@ -139,21 +135,25 @@ export function DataGridPicker<TRow extends Record<string, unknown>>(
|
|
|
139
135
|
filterValues,
|
|
140
136
|
onFilterChange,
|
|
141
137
|
clearLabel,
|
|
142
|
-
|
|
138
|
+
page,
|
|
139
|
+
pageSize,
|
|
140
|
+
total,
|
|
141
|
+
hasMore,
|
|
142
|
+
onPageChange,
|
|
143
143
|
loading = false,
|
|
144
144
|
emptyLabel,
|
|
145
|
-
rowHeight = DEFAULT_ROW_HEIGHT,
|
|
146
145
|
testID,
|
|
147
146
|
} = props;
|
|
148
147
|
|
|
149
148
|
const handleSelect = useCallback(
|
|
150
|
-
(
|
|
151
|
-
onValueChange(
|
|
149
|
+
(row: TRow) => {
|
|
150
|
+
onValueChange(String(row[rowKey]));
|
|
152
151
|
if (closeOnSelect) onOpenChange(false);
|
|
153
152
|
},
|
|
154
|
-
[onValueChange, closeOnSelect, onOpenChange],
|
|
153
|
+
[onValueChange, rowKey, closeOnSelect, onOpenChange],
|
|
155
154
|
);
|
|
156
155
|
|
|
156
|
+
// Toggle this column's sort: asc → desc → off. The consumer holds the state.
|
|
157
157
|
const toggleSort = useCallback(
|
|
158
158
|
(key: string) => {
|
|
159
159
|
if (!onSortChange) return;
|
|
@@ -164,51 +164,35 @@ export function DataGridPicker<TRow extends Record<string, unknown>>(
|
|
|
164
164
|
[sort, onSortChange],
|
|
165
165
|
);
|
|
166
166
|
|
|
167
|
-
const
|
|
167
|
+
const tableColumns = useMemo<Column<TRow>[]>(
|
|
168
168
|
() =>
|
|
169
169
|
columns.map((col) => ({
|
|
170
170
|
key: col.key,
|
|
171
|
-
|
|
171
|
+
label: col.label,
|
|
172
172
|
width: col.width,
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
: undefined,
|
|
184
|
-
renderCell: ({ row }: { row: TRow }) => (
|
|
185
|
-
<PressableHighlight
|
|
186
|
-
accessibilityRole="button"
|
|
187
|
-
onPress={() => handleSelect(rowIdGetter(row))}
|
|
188
|
-
style={styles.cell}
|
|
189
|
-
>
|
|
190
|
-
{col.renderCell ? (
|
|
191
|
-
col.renderCell(row)
|
|
192
|
-
) : (
|
|
193
|
-
<Text size="sm" color="zinc-900" numberOfLines={1}>
|
|
194
|
-
{defaultCellText(row[col.key])}
|
|
195
|
-
</Text>
|
|
196
|
-
)}
|
|
197
|
-
</PressableHighlight>
|
|
198
|
-
),
|
|
173
|
+
align: col.align,
|
|
174
|
+
sortable: col.sortable && !!onSortChange,
|
|
175
|
+
renderCell: ({ row }: { row: TRow }) =>
|
|
176
|
+
col.renderCell ? (
|
|
177
|
+
col.renderCell(row)
|
|
178
|
+
) : (
|
|
179
|
+
<Text size="sm" color="zinc-900" numberOfLines={1}>
|
|
180
|
+
{defaultCellText(row[col.key])}
|
|
181
|
+
</Text>
|
|
182
|
+
),
|
|
199
183
|
})),
|
|
200
|
-
[columns,
|
|
184
|
+
[columns, onSortChange],
|
|
201
185
|
);
|
|
202
186
|
|
|
203
|
-
const
|
|
204
|
-
() => (
|
|
205
|
-
[
|
|
187
|
+
const tableSort = useMemo<TableSort<TRow> | null>(
|
|
188
|
+
() => (sort ? { key: sort.key, dir: sort.order } : null),
|
|
189
|
+
[sort],
|
|
206
190
|
);
|
|
207
191
|
|
|
208
|
-
const
|
|
192
|
+
const rowStyle = useCallback(
|
|
209
193
|
(row: TRow) =>
|
|
210
|
-
value != null &&
|
|
211
|
-
[value,
|
|
194
|
+
value != null && String(row[rowKey]) === value ? { backgroundColor: colors.blue["50"] } : undefined,
|
|
195
|
+
[value, rowKey],
|
|
212
196
|
);
|
|
213
197
|
|
|
214
198
|
return (
|
|
@@ -221,7 +205,7 @@ export function DataGridPicker<TRow extends Record<string, unknown>>(
|
|
|
221
205
|
) : null}
|
|
222
206
|
|
|
223
207
|
<SearchInput
|
|
224
|
-
testID="
|
|
208
|
+
testID="table-picker-search"
|
|
225
209
|
value={searchQuery}
|
|
226
210
|
onChangeText={onSearchChange}
|
|
227
211
|
placeholder={searchPlaceholder}
|
|
@@ -243,16 +227,15 @@ export function DataGridPicker<TRow extends Record<string, unknown>>(
|
|
|
243
227
|
) : null}
|
|
244
228
|
|
|
245
229
|
<View style={styles.gridArea}>
|
|
246
|
-
{
|
|
247
|
-
<
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
onEndReached={onEndReached}
|
|
230
|
+
{rows.length > 0 ? (
|
|
231
|
+
<Table<TRow>
|
|
232
|
+
columns={tableColumns}
|
|
233
|
+
rows={rows}
|
|
234
|
+
rowKey={rowKey}
|
|
235
|
+
sort={tableSort}
|
|
236
|
+
onSortChange={onSortChange ? (key) => toggleSort(key as string) : undefined}
|
|
237
|
+
onRowPress={handleSelect}
|
|
238
|
+
rowStyle={rowStyle}
|
|
256
239
|
/>
|
|
257
240
|
) : (
|
|
258
241
|
<View style={styles.center}>
|
|
@@ -265,12 +248,17 @@ export function DataGridPicker<TRow extends Record<string, unknown>>(
|
|
|
265
248
|
)}
|
|
266
249
|
</View>
|
|
267
250
|
)}
|
|
268
|
-
{loading && groups.length > 0 ? (
|
|
269
|
-
<View style={styles.loadingOverlay} pointerEvents="none">
|
|
270
|
-
<ActivityIndicator />
|
|
271
|
-
</View>
|
|
272
|
-
) : null}
|
|
273
251
|
</View>
|
|
252
|
+
|
|
253
|
+
<Pagination
|
|
254
|
+
page={page}
|
|
255
|
+
pageSize={pageSize}
|
|
256
|
+
rowCount={rows.length}
|
|
257
|
+
hasMore={hasMore}
|
|
258
|
+
total={total}
|
|
259
|
+
loading={loading}
|
|
260
|
+
onPageChange={onPageChange}
|
|
261
|
+
/>
|
|
274
262
|
</View>
|
|
275
263
|
</Dialog>
|
|
276
264
|
);
|
|
@@ -297,26 +285,16 @@ const styles = StyleSheet.create({
|
|
|
297
285
|
},
|
|
298
286
|
gridArea: {
|
|
299
287
|
flex: 1,
|
|
288
|
+
minHeight: 0,
|
|
300
289
|
borderWidth: 1,
|
|
301
290
|
borderColor: colors.border,
|
|
302
291
|
borderRadius: 12,
|
|
303
292
|
overflow: "hidden",
|
|
304
293
|
},
|
|
305
|
-
cell: {
|
|
306
|
-
flex: 1,
|
|
307
|
-
height: "100%",
|
|
308
|
-
justifyContent: "center",
|
|
309
|
-
paddingHorizontal: 8,
|
|
310
|
-
},
|
|
311
294
|
center: {
|
|
312
295
|
flex: 1,
|
|
313
296
|
alignItems: "center",
|
|
314
297
|
justifyContent: "center",
|
|
315
298
|
padding: 24,
|
|
316
299
|
},
|
|
317
|
-
loadingOverlay: {
|
|
318
|
-
position: "absolute",
|
|
319
|
-
top: 8,
|
|
320
|
-
right: 8,
|
|
321
|
-
},
|
|
322
300
|
});
|
package/src/table_types.ts
CHANGED
|
@@ -35,6 +35,11 @@ export interface TableProps<TRow extends Record<string, unknown>> {
|
|
|
35
35
|
* parent owns the actual sorting of `rows` — this only drives the indicator. */
|
|
36
36
|
sort?: TableSort<TRow> | null;
|
|
37
37
|
onSortChange?: (key: keyof TRow) => void;
|
|
38
|
+
/** Click-to-select: pressing a row (except `interactive` cells) calls this —
|
|
39
|
+
* e.g. a picker that returns the chosen row. Mutually exclusive with
|
|
40
|
+
* `renderDetail` (a row either expands or selects); `renderDetail` wins if
|
|
41
|
+
* both are set. Pair with `rowStyle` to highlight the selected row. */
|
|
42
|
+
onRowPress?: (row: TRow) => void;
|
|
38
43
|
/** Render an inline detail panel, full-width below the row. When set, the
|
|
39
44
|
* whole row (except `interactive` cells) is click-to-expand. */
|
|
40
45
|
renderDetail?: (row: TRow) => ReactNode;
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { StyleSheet } from "react-native";
|
|
2
|
-
import { colors } from "../colors";
|
|
3
|
-
import { Text } from "../text";
|
|
4
|
-
import { Icon } from "../icon";
|
|
5
|
-
import { PressableHighlight } from "../pressable_highlight";
|
|
6
|
-
|
|
7
|
-
export type SortOrder = "asc" | "desc";
|
|
8
|
-
|
|
9
|
-
export interface SortableHeaderCellProps {
|
|
10
|
-
label: string;
|
|
11
|
-
/** Current sort order for THIS column, or null when another (or no) column
|
|
12
|
-
* is the sort key. */
|
|
13
|
-
order: SortOrder | null;
|
|
14
|
-
/** Toggle this column's sort. The consumer owns sort state and re-queries. */
|
|
15
|
-
onToggle: () => void;
|
|
16
|
-
testID?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* A clickable column header that cycles/toggles sort and shows the direction —
|
|
21
|
-
* drop it into any `DataGrid` column's `renderHeaderCell`. Pure presentation:
|
|
22
|
-
* the consumer holds the sort state and reorders/re-queries the rows. Reusable
|
|
23
|
-
* outside the picker (any sortable grid).
|
|
24
|
-
*/
|
|
25
|
-
export function SortableHeaderCell(props: SortableHeaderCellProps) {
|
|
26
|
-
const { label, order, onToggle, testID } = props;
|
|
27
|
-
const active = order !== null;
|
|
28
|
-
return (
|
|
29
|
-
<PressableHighlight
|
|
30
|
-
testID={testID}
|
|
31
|
-
onPress={onToggle}
|
|
32
|
-
accessibilityRole="button"
|
|
33
|
-
accessibilityLabel={label}
|
|
34
|
-
style={styles.cell}
|
|
35
|
-
>
|
|
36
|
-
<Text size="sm" weight="medium" color={active ? "zinc-900" : "zinc-500"} numberOfLines={1}>
|
|
37
|
-
{label}
|
|
38
|
-
</Text>
|
|
39
|
-
<Icon
|
|
40
|
-
name={order === "asc" ? "chevron-up" : order === "desc" ? "chevron-down" : "chevrons-up-down"}
|
|
41
|
-
size={14}
|
|
42
|
-
color={active ? colors.zinc["700"] : colors.zinc["400"]}
|
|
43
|
-
/>
|
|
44
|
-
</PressableHighlight>
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const styles = StyleSheet.create({
|
|
49
|
-
cell: {
|
|
50
|
-
flexDirection: "row",
|
|
51
|
-
alignItems: "center",
|
|
52
|
-
justifyContent: "space-between",
|
|
53
|
-
gap: 4,
|
|
54
|
-
flex: 1,
|
|
55
|
-
height: "100%",
|
|
56
|
-
paddingHorizontal: 8,
|
|
57
|
-
},
|
|
58
|
-
});
|