@lotics/ui 2.6.1 → 3.1.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 +2 -15
- package/src/react_native.d.ts +2 -2
- package/src/segmented_control.tsx +201 -0
- package/src/cell_date.tsx +0 -30
- package/src/cell_date_format.test.ts +0 -32
- package/src/cell_date_format.ts +0 -73
- package/src/cell_number.test.ts +0 -42
- package/src/cell_number.tsx +0 -25
- package/src/cell_number_format.ts +0 -42
- package/src/cell_select.tsx +0 -68
- package/src/cell_text.tsx +0 -45
- package/src/grid/data_grid.tsx +0 -2003
- package/src/grid/data_grid_columns.test.ts +0 -72
- package/src/grid/data_grid_columns.ts +0 -30
- package/src/grid/data_grid_context.ts +0 -119
- package/src/grid/dispatch_safely.ts +0 -39
- package/src/grid/engine.module.css +0 -114
- package/src/grid/engine.tsx +0 -1042
- package/src/grid/helpers.ts +0 -205
- package/src/grid/layout.test.ts +0 -515
- package/src/grid/layout.ts +0 -425
- package/src/grid/recycling.test.ts +0 -236
- package/src/grid/recycling.ts +0 -172
- package/src/grid/row_cell.module.css +0 -105
- package/src/grid/row_cell.tsx +0 -313
- package/src/grid/search_highlight.ts +0 -71
- package/src/grid/select_cell.tsx +0 -58
- package/src/grid/select_group_summary_cell.tsx +0 -76
- package/src/grid/select_header_cell.tsx +0 -32
- package/src/grid/skeleton_row.module.css +0 -34
- package/src/grid/skeleton_row.tsx +0 -20
- package/src/grid/use_grid_groups.ts +0 -311
- package/src/grid/use_scroll_to_cell.ts +0 -135
- package/src/grid/use_virtual_grid.ts +0 -383
- package/src/grid/visibility.test.ts +0 -208
- package/src/grid/visibility.ts +0 -77
- package/src/kanban/constants.ts +0 -18
- package/src/kanban/default_renderers.tsx +0 -160
- package/src/kanban/drag_preview.tsx +0 -157
- package/src/kanban/index.ts +0 -13
- package/src/kanban/insert_card_zone.tsx +0 -135
- package/src/kanban/kanban_board.tsx +0 -635
- package/src/kanban/kanban_card.tsx +0 -321
- package/src/kanban/kanban_column.tsx +0 -499
- package/src/kanban/placeholders.tsx +0 -54
- package/src/kanban/types.ts +0 -116
package/src/grid/recycling.ts
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import React, { useMemo, useRef } from "react";
|
|
2
|
-
import { differenceBy, intersectBy, isEmpty, max, maxBy, min } from "./helpers";
|
|
3
|
-
import {
|
|
4
|
-
GridColumn,
|
|
5
|
-
GridDataRow,
|
|
6
|
-
GridGroupHeadingRow,
|
|
7
|
-
GridGroupSummaryRow,
|
|
8
|
-
GridRow,
|
|
9
|
-
GridSpacerRow,
|
|
10
|
-
} from "./layout";
|
|
11
|
-
|
|
12
|
-
export interface RecycledDataRow extends GridDataRow {
|
|
13
|
-
key: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface RecycledGroupHeadingRow extends GridGroupHeadingRow {
|
|
17
|
-
key: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface RecycledGroupSummaryRow extends GridGroupSummaryRow {
|
|
21
|
-
key: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface RecycledSpacerRow extends GridSpacerRow {
|
|
25
|
-
key: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export type RecycledRow =
|
|
29
|
-
| RecycledDataRow
|
|
30
|
-
| RecycledGroupHeadingRow
|
|
31
|
-
| RecycledGroupSummaryRow
|
|
32
|
-
| RecycledSpacerRow;
|
|
33
|
-
|
|
34
|
-
export interface RecycledColumn extends GridColumn {
|
|
35
|
-
key: number;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface UseRowsRecyclerProps {
|
|
39
|
-
rows: GridRow[];
|
|
40
|
-
startIndex: number;
|
|
41
|
-
endIndex: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface UseColumnsRecyclerProps {
|
|
45
|
-
columns: GridColumn[];
|
|
46
|
-
startIndex: number;
|
|
47
|
-
endIndex: number;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface RecycleItemsParams<T, K extends T> {
|
|
51
|
-
items: T[];
|
|
52
|
-
prevItems: K[];
|
|
53
|
-
toRecycledItem: (item: T, key: number) => K;
|
|
54
|
-
/** Value to recycle by */
|
|
55
|
-
getValue: (item: T | K) => number;
|
|
56
|
-
getKey: (item: K) => number;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
interface RecycleRowsParams {
|
|
60
|
-
rows: GridRow[];
|
|
61
|
-
prevRows: RecycledRow[];
|
|
62
|
-
startIndex: number;
|
|
63
|
-
endIndex: number;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface RecycleColumnsParams {
|
|
67
|
-
columns: GridColumn[];
|
|
68
|
-
prevColumns: RecycledColumn[];
|
|
69
|
-
startIndex: number;
|
|
70
|
-
endIndex: number;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function recycleItems<T, K extends T>(params: RecycleItemsParams<T, K>): K[] {
|
|
74
|
-
const { items, prevItems, getValue, toRecycledItem, getKey } = params;
|
|
75
|
-
|
|
76
|
-
const reusedItems = intersectBy(prevItems, items, getValue, true);
|
|
77
|
-
const recycledItems = differenceBy(prevItems, items, getValue);
|
|
78
|
-
const newItems = differenceBy(items, prevItems, getValue);
|
|
79
|
-
|
|
80
|
-
if (isEmpty(prevItems)) {
|
|
81
|
-
return items.map((item, key) => toRecycledItem(item, key));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const recycledKeys = recycledItems.map((c) => getKey(c));
|
|
85
|
-
if (recycledKeys.length < newItems.length) {
|
|
86
|
-
let maxKey = maxBy(prevItems, getKey) as number;
|
|
87
|
-
|
|
88
|
-
while (recycledKeys.length !== newItems.length) {
|
|
89
|
-
recycledKeys.push(++maxKey);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return reusedItems
|
|
94
|
-
.concat(newItems.map((item, i) => toRecycledItem(item, recycledKeys[i])))
|
|
95
|
-
.sort((a, b) => getValue(a) - getValue(b));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function useRowsRecycler(props: UseRowsRecyclerProps): RecycledRow[] {
|
|
99
|
-
const { rows, startIndex, endIndex } = props;
|
|
100
|
-
|
|
101
|
-
const prevRecycledRowsRef = useRef<RecycledRow[]>([]);
|
|
102
|
-
|
|
103
|
-
const recycledRows = useMemo(
|
|
104
|
-
(): RecycledRow[] =>
|
|
105
|
-
recycleRows({
|
|
106
|
-
rows,
|
|
107
|
-
prevRows: prevRecycledRowsRef.current,
|
|
108
|
-
startIndex,
|
|
109
|
-
endIndex,
|
|
110
|
-
}),
|
|
111
|
-
[rows, startIndex, endIndex],
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
prevRecycledRowsRef.current = recycledRows;
|
|
115
|
-
|
|
116
|
-
return recycledRows;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function useColumnsRecycler(props: UseColumnsRecyclerProps): RecycledColumn[] {
|
|
120
|
-
const { columns, startIndex, endIndex } = props;
|
|
121
|
-
|
|
122
|
-
const prevRecycledColumnsRef = useRef<RecycledColumn[]>([]);
|
|
123
|
-
|
|
124
|
-
const recycledColumns = useMemo(
|
|
125
|
-
(): RecycledColumn[] =>
|
|
126
|
-
recycleColumns({
|
|
127
|
-
columns,
|
|
128
|
-
prevColumns: prevRecycledColumnsRef.current,
|
|
129
|
-
startIndex,
|
|
130
|
-
endIndex,
|
|
131
|
-
}),
|
|
132
|
-
[columns, startIndex, endIndex],
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
prevRecycledColumnsRef.current = recycledColumns;
|
|
136
|
-
|
|
137
|
-
return recycledColumns;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function recycleRows(params: RecycleRowsParams) {
|
|
141
|
-
const { rows, prevRows, startIndex, endIndex } = params;
|
|
142
|
-
|
|
143
|
-
const nextRows = rows.slice(startIndex, endIndex + 1);
|
|
144
|
-
|
|
145
|
-
return recycleItems({
|
|
146
|
-
items: nextRows,
|
|
147
|
-
prevItems: prevRows,
|
|
148
|
-
getValue: (row) => row.y,
|
|
149
|
-
getKey: (row) => row.key,
|
|
150
|
-
toRecycledItem: (row, key) => ({
|
|
151
|
-
...row,
|
|
152
|
-
key,
|
|
153
|
-
}),
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function recycleColumns(params: RecycleColumnsParams) {
|
|
158
|
-
const { columns, prevColumns, startIndex, endIndex } = params;
|
|
159
|
-
|
|
160
|
-
const nextColumns = columns.slice(startIndex, endIndex + 1);
|
|
161
|
-
|
|
162
|
-
return recycleItems({
|
|
163
|
-
items: nextColumns,
|
|
164
|
-
prevItems: prevColumns,
|
|
165
|
-
getValue: (column) => column.column,
|
|
166
|
-
getKey: (column) => column.key,
|
|
167
|
-
toRecycledItem: (column, key) => ({
|
|
168
|
-
...column,
|
|
169
|
-
key,
|
|
170
|
-
}),
|
|
171
|
-
});
|
|
172
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
.row_cell {
|
|
2
|
-
width: 100%;
|
|
3
|
-
height: 100%;
|
|
4
|
-
position: relative;
|
|
5
|
-
outline: none;
|
|
6
|
-
border-bottom: 1px solid var(--color-border);
|
|
7
|
-
box-sizing: border-box;
|
|
8
|
-
overflow: hidden;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/* Active cell (most recently selected) */
|
|
12
|
-
.row_cell_active {
|
|
13
|
-
outline: 2px solid var(--color-blue-500);
|
|
14
|
-
outline-offset: -2px;
|
|
15
|
-
z-index: 2;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.row_cell_frozen {
|
|
19
|
-
background-color: var(--color-background);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.row_cell_in_selected_row {
|
|
23
|
-
background-color: var(--color-blue-50);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/* Selected cell styling */
|
|
27
|
-
.row_cell_selected {
|
|
28
|
-
background-color: var(--color-blue-50);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/* Selected but not active */
|
|
32
|
-
.row_cell_selected:not(.row_cell_active) {
|
|
33
|
-
outline: 1px solid var(--color-zinc-300);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
.row_cell_fill_handle {
|
|
37
|
-
position: absolute;
|
|
38
|
-
bottom: -4px;
|
|
39
|
-
right: -4px;
|
|
40
|
-
width: 10px;
|
|
41
|
-
height: 10px;
|
|
42
|
-
background-color: var(--color-blue-500);
|
|
43
|
-
border: 1px solid var(--color-white);
|
|
44
|
-
cursor: crosshair;
|
|
45
|
-
z-index: 10;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
.row_cell_fill_handle:hover {
|
|
49
|
-
background-color: var(--color-blue-600);
|
|
50
|
-
transform: scale(1.2);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/* Fill preview styling */
|
|
54
|
-
.row_cell_fill_preview {
|
|
55
|
-
background-color: var(--color-blue-100);
|
|
56
|
-
outline: 1px solid var(--color-blue-400);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/* Value change animation */
|
|
60
|
-
.row_cell_value_changed {
|
|
61
|
-
animation: valueChangeFlash 0.6s ease-out;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
@keyframes valueChangeFlash {
|
|
65
|
-
0% {
|
|
66
|
-
box-shadow: inset 0 0 0 1000px var(--color-blue-200);
|
|
67
|
-
}
|
|
68
|
-
100% {
|
|
69
|
-
box-shadow: inset 0 0 0 1000px transparent;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/* Search highlight animation alternates class names so repeated reveals on
|
|
74
|
-
the same row restart the animation. */
|
|
75
|
-
.row_cell_search_highlight_even {
|
|
76
|
-
animation: searchHighlightEven 1.5s ease-out;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
.row_cell_search_highlight_odd {
|
|
80
|
-
animation: searchHighlightOdd 1.5s ease-out;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
@keyframes searchHighlightEven {
|
|
84
|
-
0% {
|
|
85
|
-
box-shadow: inset 0 0 0 1000px var(--color-amber-100);
|
|
86
|
-
}
|
|
87
|
-
70% {
|
|
88
|
-
box-shadow: inset 0 0 0 1000px var(--color-amber-100);
|
|
89
|
-
}
|
|
90
|
-
100% {
|
|
91
|
-
box-shadow: inset 0 0 0 1000px transparent;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
@keyframes searchHighlightOdd {
|
|
96
|
-
0% {
|
|
97
|
-
box-shadow: inset 0 0 0 1000px var(--color-amber-100);
|
|
98
|
-
}
|
|
99
|
-
70% {
|
|
100
|
-
box-shadow: inset 0 0 0 1000px var(--color-amber-100);
|
|
101
|
-
}
|
|
102
|
-
100% {
|
|
103
|
-
box-shadow: inset 0 0 0 1000px transparent;
|
|
104
|
-
}
|
|
105
|
-
}
|
package/src/grid/row_cell.tsx
DELETED
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
-
import { DataGridColumn, RowPathKey } from "./data_grid";
|
|
3
|
-
import { RowId, useCellAnimation } from "./data_grid_context";
|
|
4
|
-
import { useSearchHighlight } from "./search_highlight";
|
|
5
|
-
|
|
6
|
-
import styles from "./row_cell.module.css";
|
|
7
|
-
|
|
8
|
-
export type NavigationDirection = "up" | "down" | "left" | "right";
|
|
9
|
-
|
|
10
|
-
interface RowCellProps<TRow, TValue> {
|
|
11
|
-
rowKey: RowPathKey;
|
|
12
|
-
rowId: RowId;
|
|
13
|
-
row: TRow;
|
|
14
|
-
value: TValue;
|
|
15
|
-
column: DataGridColumn<TRow>;
|
|
16
|
-
columnIdx: number;
|
|
17
|
-
active: boolean;
|
|
18
|
-
selected: boolean;
|
|
19
|
-
frozen: boolean;
|
|
20
|
-
inSelectedRow: boolean;
|
|
21
|
-
editing: boolean;
|
|
22
|
-
/** Row index for fill operations */
|
|
23
|
-
rowIndex: number;
|
|
24
|
-
/** Whether this cell is in the fill range (target cells during drag) */
|
|
25
|
-
inFillRange: boolean;
|
|
26
|
-
/** Background color for the row (from conditional coloring) */
|
|
27
|
-
rowBackgroundColor?: string;
|
|
28
|
-
onStartEditing: (rowKey: RowPathKey, columnIdx: number) => void;
|
|
29
|
-
onSelectStart: (rowKey: RowPathKey, columnIdx: number) => void;
|
|
30
|
-
onSelectUpdate: (rowKey: RowPathKey, columnIdx: number) => void;
|
|
31
|
-
onCommit: (nextValue: unknown, columnKey: string, rowKey: RowPathKey) => void;
|
|
32
|
-
onCancelEditing: () => void;
|
|
33
|
-
onNavigate: (direction: NavigationDirection, extend: boolean) => void;
|
|
34
|
-
onFillStart: (sourceRowIndex: number, column: number) => void;
|
|
35
|
-
onRequestLockedFieldChange?: (row: TRow, columnKey: string) => void;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const RowCell = memo(function RowCell<TRow, TValue>(props: RowCellProps<TRow, TValue>) {
|
|
39
|
-
const {
|
|
40
|
-
rowKey,
|
|
41
|
-
rowId,
|
|
42
|
-
row,
|
|
43
|
-
value,
|
|
44
|
-
column,
|
|
45
|
-
columnIdx,
|
|
46
|
-
active,
|
|
47
|
-
selected,
|
|
48
|
-
frozen,
|
|
49
|
-
inSelectedRow,
|
|
50
|
-
editing,
|
|
51
|
-
rowIndex,
|
|
52
|
-
inFillRange,
|
|
53
|
-
rowBackgroundColor,
|
|
54
|
-
onStartEditing,
|
|
55
|
-
onSelectStart,
|
|
56
|
-
onSelectUpdate,
|
|
57
|
-
onCommit,
|
|
58
|
-
onCancelEditing,
|
|
59
|
-
onNavigate,
|
|
60
|
-
onFillStart,
|
|
61
|
-
onRequestLockedFieldChange,
|
|
62
|
-
} = props;
|
|
63
|
-
|
|
64
|
-
// Cell animation hook resolved via context. Records page provides one
|
|
65
|
-
// bound to useRealtimeChangeAnimation(tableId, …); iframe apps fall through
|
|
66
|
-
// to the noop default.
|
|
67
|
-
const isAnimating = useCellAnimation(rowId, column.key);
|
|
68
|
-
const searchHighlightRevealId = useSearchHighlight(rowId);
|
|
69
|
-
|
|
70
|
-
const editable = column.isCellEditable?.(row, column.key) ?? true;
|
|
71
|
-
|
|
72
|
-
const handleMouseDown = useCallback(
|
|
73
|
-
(event: React.MouseEvent) => {
|
|
74
|
-
// Only handle left mouse button
|
|
75
|
-
if (event.button !== 0) return;
|
|
76
|
-
if (event.shiftKey) {
|
|
77
|
-
// Shift+click extends selection
|
|
78
|
-
onSelectUpdate(rowKey, columnIdx);
|
|
79
|
-
} else {
|
|
80
|
-
// Start new selection
|
|
81
|
-
onSelectStart(rowKey, columnIdx);
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
[onSelectStart, onSelectUpdate, rowKey, columnIdx],
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
const handleMouseEnter = useCallback(
|
|
88
|
-
(event: React.MouseEvent) => {
|
|
89
|
-
// Extend selection if mouse button is pressed (dragging)
|
|
90
|
-
if (event.buttons === 1) {
|
|
91
|
-
onSelectUpdate(rowKey, columnIdx);
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
[onSelectUpdate, rowKey, columnIdx],
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
// Fill handle: start drag (just triggers callback, global listeners managed by DataGrid)
|
|
98
|
-
const handleFillHandleMouseDown = useCallback(
|
|
99
|
-
(event: React.MouseEvent) => {
|
|
100
|
-
event.preventDefault();
|
|
101
|
-
event.stopPropagation();
|
|
102
|
-
onFillStart(rowIndex, columnIdx);
|
|
103
|
-
},
|
|
104
|
-
[rowIndex, columnIdx, onFillStart],
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
const handleDoubleClick = useCallback(() => {
|
|
108
|
-
if (editable && column.renderEditCell) {
|
|
109
|
-
onStartEditing(rowKey, columnIdx);
|
|
110
|
-
} else if (!editable && onRequestLockedFieldChange) {
|
|
111
|
-
onRequestLockedFieldChange(row, column.key);
|
|
112
|
-
}
|
|
113
|
-
}, [
|
|
114
|
-
editable,
|
|
115
|
-
column.renderEditCell,
|
|
116
|
-
column.key,
|
|
117
|
-
onStartEditing,
|
|
118
|
-
onRequestLockedFieldChange,
|
|
119
|
-
row,
|
|
120
|
-
rowKey,
|
|
121
|
-
columnIdx,
|
|
122
|
-
rowId,
|
|
123
|
-
]);
|
|
124
|
-
|
|
125
|
-
// Stable callback for cell value changes (avoids inline closure in renderCell)
|
|
126
|
-
const handleValueChange = useCallback(
|
|
127
|
-
(nextValue: TValue) => {
|
|
128
|
-
onCommit(nextValue, column.key, rowKey);
|
|
129
|
-
},
|
|
130
|
-
[onCommit, column.key, rowKey],
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
// Compute CSS class based on active and selected states
|
|
134
|
-
let className = styles.row_cell;
|
|
135
|
-
if (active) {
|
|
136
|
-
className += ` ${styles.row_cell_active}`;
|
|
137
|
-
}
|
|
138
|
-
if (selected) {
|
|
139
|
-
className += ` ${styles.row_cell_selected}`;
|
|
140
|
-
}
|
|
141
|
-
if (frozen) {
|
|
142
|
-
className += ` ${styles.row_cell_frozen}`;
|
|
143
|
-
}
|
|
144
|
-
if (inSelectedRow) {
|
|
145
|
-
className += ` ${styles.row_cell_in_selected_row}`;
|
|
146
|
-
}
|
|
147
|
-
if (inFillRange) {
|
|
148
|
-
className += ` ${styles.row_cell_fill_preview}`;
|
|
149
|
-
}
|
|
150
|
-
if (isAnimating) {
|
|
151
|
-
className += ` ${styles.row_cell_value_changed}`;
|
|
152
|
-
}
|
|
153
|
-
if (searchHighlightRevealId !== null) {
|
|
154
|
-
className +=
|
|
155
|
-
searchHighlightRevealId % 2 === 0
|
|
156
|
-
? ` ${styles.row_cell_search_highlight_even}`
|
|
157
|
-
: ` ${styles.row_cell_search_highlight_odd}`;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Show fill handle on active cell when editable and has renderEditCell
|
|
161
|
-
const showFillHandle = active && !editing && editable;
|
|
162
|
-
|
|
163
|
-
// Render editing cell as a separate component so it mounts fresh,
|
|
164
|
-
// initializing draft state correctly without flash
|
|
165
|
-
if (editing && editable && column.renderEditCell) {
|
|
166
|
-
return (
|
|
167
|
-
<RowCellEditor
|
|
168
|
-
className={className}
|
|
169
|
-
rowKey={rowKey}
|
|
170
|
-
rowId={rowId}
|
|
171
|
-
row={row}
|
|
172
|
-
value={value}
|
|
173
|
-
column={column}
|
|
174
|
-
onCommit={onCommit}
|
|
175
|
-
onCancelEditing={onCancelEditing}
|
|
176
|
-
onNavigate={onNavigate}
|
|
177
|
-
/>
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Apply row background color when not selected
|
|
182
|
-
const cellStyle =
|
|
183
|
-
rowBackgroundColor && !selected && !inSelectedRow
|
|
184
|
-
? { backgroundColor: rowBackgroundColor }
|
|
185
|
-
: undefined;
|
|
186
|
-
|
|
187
|
-
return (
|
|
188
|
-
<div
|
|
189
|
-
data-row-index={rowIndex}
|
|
190
|
-
data-row-key={rowKey}
|
|
191
|
-
data-row-id={rowId}
|
|
192
|
-
data-column={columnIdx}
|
|
193
|
-
onMouseDown={handleMouseDown}
|
|
194
|
-
onMouseEnter={handleMouseEnter}
|
|
195
|
-
onDoubleClick={handleDoubleClick}
|
|
196
|
-
className={className}
|
|
197
|
-
style={cellStyle}
|
|
198
|
-
>
|
|
199
|
-
{column.renderCell?.({
|
|
200
|
-
rowKey,
|
|
201
|
-
rowId,
|
|
202
|
-
row,
|
|
203
|
-
columnKey: column.key,
|
|
204
|
-
value,
|
|
205
|
-
active,
|
|
206
|
-
editable,
|
|
207
|
-
onValueChange: handleValueChange,
|
|
208
|
-
})}
|
|
209
|
-
{showFillHandle && (
|
|
210
|
-
<div className={styles.row_cell_fill_handle} onMouseDown={handleFillHandleMouseDown} />
|
|
211
|
-
)}
|
|
212
|
-
</div>
|
|
213
|
-
);
|
|
214
|
-
}) as <TRow, TValue>(props: RowCellProps<TRow, TValue>) => React.ReactNode;
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Separate component for editing mode.
|
|
218
|
-
* Mounts fresh each time editing starts, so useState initializes correctly.
|
|
219
|
-
*/
|
|
220
|
-
interface RowCellEditorProps<TRow, TValue> {
|
|
221
|
-
className: string;
|
|
222
|
-
rowKey: RowPathKey;
|
|
223
|
-
rowId: RowId;
|
|
224
|
-
row: TRow;
|
|
225
|
-
value: TValue;
|
|
226
|
-
column: DataGridColumn<TRow>;
|
|
227
|
-
onCommit: (nextValue: unknown, columnKey: string, rowKey: RowPathKey) => void;
|
|
228
|
-
onCancelEditing: () => void;
|
|
229
|
-
onNavigate: (direction: NavigationDirection, extend: boolean) => void;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function RowCellEditor<TRow, TValue>(props: RowCellEditorProps<TRow, TValue>) {
|
|
233
|
-
const { className, rowKey, row, rowId, value, column, onCommit, onCancelEditing, onNavigate } =
|
|
234
|
-
props;
|
|
235
|
-
|
|
236
|
-
// State initializes fresh each time this component mounts (editing starts)
|
|
237
|
-
const [draftValue, setDraftValue] = useState<TValue>(value);
|
|
238
|
-
const draftValueRef = useRef<TValue>(value);
|
|
239
|
-
const cancelledRef = useRef(false);
|
|
240
|
-
|
|
241
|
-
// Refs for stable access in unmount cleanup (no stale closures)
|
|
242
|
-
const onCommitRef = useRef(onCommit);
|
|
243
|
-
onCommitRef.current = onCommit;
|
|
244
|
-
const rowKeyRef = useRef(rowKey);
|
|
245
|
-
rowKeyRef.current = rowKey;
|
|
246
|
-
const columnKeyRef = useRef(column.key);
|
|
247
|
-
columnKeyRef.current = column.key;
|
|
248
|
-
|
|
249
|
-
// Single commit path: flush draft value on unmount unless cancelled.
|
|
250
|
-
// All exit paths (Enter, Tab, blur, click-outside) unmount this component;
|
|
251
|
-
// the cleanup commits the final draft value exactly once.
|
|
252
|
-
// Uses rowKeyRef to identify the correct row regardless of selection changes.
|
|
253
|
-
useEffect(() => {
|
|
254
|
-
return () => {
|
|
255
|
-
if (!cancelledRef.current) {
|
|
256
|
-
onCommitRef.current(draftValueRef.current, columnKeyRef.current, rowKeyRef.current);
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
260
|
-
}, []);
|
|
261
|
-
|
|
262
|
-
const handleValueChange = useCallback((nextValue: TValue) => {
|
|
263
|
-
setDraftValue(nextValue);
|
|
264
|
-
draftValueRef.current = nextValue;
|
|
265
|
-
}, []);
|
|
266
|
-
|
|
267
|
-
const handleCancel = useCallback(() => {
|
|
268
|
-
cancelledRef.current = true;
|
|
269
|
-
onCancelEditing();
|
|
270
|
-
}, [onCancelEditing]);
|
|
271
|
-
|
|
272
|
-
const handleKeyDown = useCallback(
|
|
273
|
-
(event: React.KeyboardEvent) => {
|
|
274
|
-
if (event.key === "Escape") {
|
|
275
|
-
event.preventDefault();
|
|
276
|
-
event.stopPropagation();
|
|
277
|
-
handleCancel();
|
|
278
|
-
return;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (event.key === "Enter" && !event.shiftKey) {
|
|
282
|
-
event.preventDefault();
|
|
283
|
-
event.stopPropagation();
|
|
284
|
-
onCancelEditing();
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (event.key === "Tab") {
|
|
289
|
-
event.preventDefault();
|
|
290
|
-
event.stopPropagation();
|
|
291
|
-
onCancelEditing();
|
|
292
|
-
onNavigate(event.shiftKey ? "left" : "right", false);
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
},
|
|
296
|
-
[handleCancel, onCancelEditing, onNavigate],
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
return (
|
|
300
|
-
<div className={className} onKeyDown={handleKeyDown}>
|
|
301
|
-
{column.renderEditCell!({
|
|
302
|
-
rowKey,
|
|
303
|
-
rowId,
|
|
304
|
-
row,
|
|
305
|
-
columnKey: column.key,
|
|
306
|
-
value: draftValue,
|
|
307
|
-
onValueChange: handleValueChange,
|
|
308
|
-
onClose: onCancelEditing,
|
|
309
|
-
onCancel: handleCancel,
|
|
310
|
-
})}
|
|
311
|
-
</div>
|
|
312
|
-
);
|
|
313
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { useSyncExternalStore } from "react";
|
|
2
|
-
import { dispatchSafely } from "./dispatch_safely";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Lightweight external store for search-highlight state.
|
|
6
|
-
* Only the row cells matching the highlighted row re-render
|
|
7
|
-
* (useSyncExternalStore compares the boolean selector with Object.is).
|
|
8
|
-
*
|
|
9
|
-
* Supports ownership: callers pass an `owner` token when setting/clearing.
|
|
10
|
-
* A clear with a mismatched owner is a no-op, preventing stale unmount
|
|
11
|
-
* cleanup from wiping a newer highlight.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
let highlightedId: string | null = null;
|
|
15
|
-
let highlightedRevealId: number | null = null;
|
|
16
|
-
let currentOwner: string | null = null;
|
|
17
|
-
const listeners = new Set<() => void>();
|
|
18
|
-
|
|
19
|
-
function notify() {
|
|
20
|
-
dispatchSafely(listeners, (listener) => listener());
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface SearchHighlightUpdate {
|
|
24
|
-
owner?: string;
|
|
25
|
-
revealId?: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function setSearchHighlight(id: string | null, update?: SearchHighlightUpdate) {
|
|
29
|
-
if (id === null) {
|
|
30
|
-
// Clear only if the caller owns the current highlight (or no owner specified)
|
|
31
|
-
if (update?.owner && currentOwner !== update.owner) return;
|
|
32
|
-
highlightedId = null;
|
|
33
|
-
highlightedRevealId = null;
|
|
34
|
-
currentOwner = null;
|
|
35
|
-
} else {
|
|
36
|
-
highlightedId = id;
|
|
37
|
-
highlightedRevealId = update?.revealId ?? null;
|
|
38
|
-
currentOwner = update?.owner ?? null;
|
|
39
|
-
}
|
|
40
|
-
notify();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function subscribe(listener: () => void) {
|
|
44
|
-
listeners.add(listener);
|
|
45
|
-
return () => {
|
|
46
|
-
listeners.delete(listener);
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Returns the current reveal token when this row is highlighted, or null otherwise.
|
|
52
|
-
* This lets the same row re-render on repeated reveals so the animation can restart.
|
|
53
|
-
*/
|
|
54
|
-
export function useSearchHighlight(rowId: string): number | null {
|
|
55
|
-
return useSyncExternalStore(
|
|
56
|
-
subscribe,
|
|
57
|
-
() => (highlightedId === rowId ? highlightedRevealId : null),
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Exposed for tests only. */
|
|
62
|
-
export function _getHighlightState() {
|
|
63
|
-
return { highlightedId, highlightedRevealId, currentOwner };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Exposed for tests only — resets to clean state between tests. */
|
|
67
|
-
export function _resetHighlightState() {
|
|
68
|
-
highlightedId = null;
|
|
69
|
-
highlightedRevealId = null;
|
|
70
|
-
currentOwner = null;
|
|
71
|
-
}
|
package/src/grid/select_cell.tsx
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { Checkbox } from "@lotics/ui/checkbox";
|
|
2
|
-
import { Text } from "@lotics/ui/text";
|
|
3
|
-
import { useRowSelection } from "./data_grid_context";
|
|
4
|
-
import type { RowPathKey } from "@lotics/ui/grid/layout";
|
|
5
|
-
|
|
6
|
-
import { memo, useCallback, useMemo } from "react";
|
|
7
|
-
import { Pressable, View } from "react-native";
|
|
8
|
-
|
|
9
|
-
/** Props for checkbox cell - needs rowKey for row selection */
|
|
10
|
-
export interface SelectCellProps {
|
|
11
|
-
rowKey: RowPathKey;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const SelectCell = memo(function SelectCell(props: SelectCellProps) {
|
|
15
|
-
const { rowKey } = props;
|
|
16
|
-
const { isRowSelected, onRowSelectionChange } = useRowSelection();
|
|
17
|
-
|
|
18
|
-
const checked = useMemo(() => isRowSelected(rowKey), [isRowSelected, rowKey]);
|
|
19
|
-
|
|
20
|
-
const handlePress = useCallback(() => {
|
|
21
|
-
onRowSelectionChange({
|
|
22
|
-
rowKey: rowKey,
|
|
23
|
-
checked: !checked,
|
|
24
|
-
isShiftClick: false,
|
|
25
|
-
});
|
|
26
|
-
}, [onRowSelectionChange, rowKey, checked]);
|
|
27
|
-
|
|
28
|
-
// Extract the row number from the key (last segment after the final comma)
|
|
29
|
-
const rowNumber = useMemo(() => {
|
|
30
|
-
const parts = rowKey.split(",");
|
|
31
|
-
return Number(parts[parts.length - 1]) + 1;
|
|
32
|
-
}, [rowKey]);
|
|
33
|
-
|
|
34
|
-
return (
|
|
35
|
-
<Pressable
|
|
36
|
-
onPress={handlePress}
|
|
37
|
-
style={{
|
|
38
|
-
paddingHorizontal: 6,
|
|
39
|
-
alignItems: "center",
|
|
40
|
-
flexDirection: "row",
|
|
41
|
-
height: "100%",
|
|
42
|
-
cursor: "pointer",
|
|
43
|
-
}}
|
|
44
|
-
>
|
|
45
|
-
{({ hovered }) =>
|
|
46
|
-
hovered || checked ? (
|
|
47
|
-
<Checkbox checked={checked} />
|
|
48
|
-
) : (
|
|
49
|
-
<View style={{ width: 24, alignItems: "center" }}>
|
|
50
|
-
<Text color="zinc-500" size="xs" userSelect="none">
|
|
51
|
-
{rowNumber}
|
|
52
|
-
</Text>
|
|
53
|
-
</View>
|
|
54
|
-
)
|
|
55
|
-
}
|
|
56
|
-
</Pressable>
|
|
57
|
-
);
|
|
58
|
-
});
|