@rebasepro/ui 0.12.1-canary.gf5f1d39 → 0.13.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/dist/components/Button.d.ts +2 -1
- package/dist/components/Menubar.d.ts +9 -2
- package/dist/components/SearchBar.d.ts +1 -1
- package/dist/components/VirtualTable/VirtualTable.d.ts +12 -6
- package/dist/components/VirtualTable/fields/VirtualTableDateField.d.ts +1 -0
- package/dist/components/index.d.ts +0 -1
- package/dist/icons/GitHubIcon.d.ts +1 -1
- package/dist/icons/HandleIcon.d.ts +3 -1
- package/dist/index.css +29 -61
- package/dist/index.d.ts +2 -1
- package/dist/index.es.js +484 -298
- package/dist/index.es.js.map +1 -1
- package/dist/src/index.css +29 -61
- package/dist/src/theme.css +109 -0
- package/dist/styles.d.ts +28 -0
- package/dist/theme.css +109 -0
- package/dist/util/chip_colors.d.ts +29 -3
- package/dist/views/CollectionView/index.d.ts +0 -6
- package/dist/views/Kanban/BoardColumn.d.ts +1 -0
- package/dist/views/Kanban/BoardSortableList.d.ts +2 -1
- package/dist/views/Kanban/board_types.d.ts +10 -0
- package/dist/views/Kanban/placement.d.ts +26 -0
- package/dist/views/index.d.ts +0 -1
- package/package.json +31 -29
- package/src/components/BooleanSwitch.tsx +44 -17
- package/src/components/BooleanSwitchWithLabel.tsx +6 -6
- package/src/components/Button.tsx +9 -13
- package/src/components/Checkbox.tsx +16 -6
- package/src/components/Chip.tsx +5 -2
- package/src/components/ColorPicker.tsx +56 -43
- package/src/components/DateTimeField.tsx +18 -6
- package/src/components/DialogActions.tsx +3 -1
- package/src/components/IconButton.tsx +35 -6
- package/src/components/Menubar.tsx +19 -3
- package/src/components/MultiSelect.tsx +9 -5
- package/src/components/SearchBar.tsx +6 -4
- package/src/components/Select.tsx +6 -6
- package/src/components/TextField.tsx +17 -7
- package/src/components/VirtualTable/VirtualTable.performance.test.tsx +35 -341
- package/src/components/VirtualTable/VirtualTable.tsx +19 -1
- package/src/components/VirtualTable/fields/VirtualTableDateField.tsx +6 -1
- package/src/components/index.tsx +2 -1
- package/src/icons/GitHubIcon.tsx +11 -5
- package/src/icons/HandleIcon.tsx +15 -2
- package/src/index.css +29 -61
- package/src/index.ts +2 -1
- package/src/styles.ts +47 -0
- package/src/theme.css +109 -0
- package/src/util/chip_colors.ts +109 -67
- package/src/views/CollectionView/index.ts +6 -11
- package/src/views/Kanban/Board.tsx +80 -117
- package/src/views/Kanban/BoardColumn.tsx +3 -0
- package/src/views/Kanban/BoardSortableList.tsx +16 -4
- package/src/views/Kanban/board_types.ts +10 -0
- package/src/views/Kanban/placement.ts +62 -0
- package/src/views/index.ts +1 -1
- package/dist/views/TableView.d.ts +0 -6
- package/src/views/TableView.tsx +0 -7
|
@@ -147,8 +147,8 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
|
|
|
147
147
|
{
|
|
148
148
|
"min-h-[28px]": size === "smallest",
|
|
149
149
|
"min-h-[32px]": size === "small",
|
|
150
|
-
"min-h-[
|
|
151
|
-
"min-h-[
|
|
150
|
+
"min-h-[40px]": size === "medium",
|
|
151
|
+
"min-h-[48px]": size === "large",
|
|
152
152
|
"w-fit": !fullWidth,
|
|
153
153
|
"w-full": fullWidth
|
|
154
154
|
}
|
|
@@ -177,8 +177,8 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
|
|
|
177
177
|
{
|
|
178
178
|
"min-h-[28px]": size === "smallest",
|
|
179
179
|
"min-h-[32px]": size === "small",
|
|
180
|
-
"min-h-[
|
|
181
|
-
"min-h-[
|
|
180
|
+
"min-h-[40px]": size === "medium",
|
|
181
|
+
"min-h-[48px]": size === "large",
|
|
182
182
|
"w-full": fullWidth,
|
|
183
183
|
"w-fit": !fullWidth
|
|
184
184
|
},
|
|
@@ -193,8 +193,8 @@ export const Select = forwardRef<HTMLDivElement, SelectProps>(({
|
|
|
193
193
|
{
|
|
194
194
|
"min-h-[28px]": size === "smallest",
|
|
195
195
|
"min-h-[32px]": size === "small",
|
|
196
|
-
"min-h-[
|
|
197
|
-
"min-h-[
|
|
196
|
+
"min-h-[40px]": size === "medium",
|
|
197
|
+
"min-h-[48px]": size === "large"
|
|
198
198
|
}
|
|
199
199
|
)}>
|
|
200
200
|
<SelectPrimitive.Value
|
|
@@ -156,13 +156,23 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | numb
|
|
|
156
156
|
{
|
|
157
157
|
"min-h-[28px]": size === "smallest",
|
|
158
158
|
"min-h-[32px]": size === "small",
|
|
159
|
-
"min-h-[
|
|
160
|
-
"min-h-[
|
|
159
|
+
"min-h-[40px]": size === "medium",
|
|
160
|
+
"min-h-[48px]": size === "large"
|
|
161
161
|
},
|
|
162
|
+
/* A shrunk label occupies roughly the first 16px of the
|
|
163
|
+
box (`top-[-1px]`, translated down 2px, ~15px tall at
|
|
164
|
+
scale-75). `pt-4` put the value's first line at exactly
|
|
165
|
+
16px, so label and content shared a row: a labelled
|
|
166
|
+
`small` field with a placeholder rendered "Name" on top
|
|
167
|
+
of "e.g. Analytics Pipeline". `large` was never affected
|
|
168
|
+
— it reserves `pt-8`. */
|
|
162
169
|
label
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
170
|
+
// Labelled fields sit exactly 16px above the unlabelled
|
|
171
|
+
// scale (44/48/56/64 against 28/32/40/48). smallest,
|
|
172
|
+
// small and medium all used to share `pt-5 pb-1.5`, so
|
|
173
|
+
// they rendered at an identical 50px and `size` looked
|
|
174
|
+
// inert whenever a label was present.
|
|
175
|
+
? { smallest: "pt-4 pb-1", small: "pt-5 pb-1", medium: "pt-6 pb-2", large: "pt-8 pb-2" }[size]
|
|
166
176
|
: size === "smallest"
|
|
167
177
|
? "py-0.5"
|
|
168
178
|
: size === "small"
|
|
@@ -194,8 +204,8 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps<string | numb
|
|
|
194
204
|
{
|
|
195
205
|
"min-h-[28px]": size === "smallest",
|
|
196
206
|
"min-h-[32px]": size === "small",
|
|
197
|
-
"min-h-[
|
|
198
|
-
"min-h-[
|
|
207
|
+
"min-h-[40px]": size === "medium",
|
|
208
|
+
"min-h-[48px]": size === "large"
|
|
199
209
|
},
|
|
200
210
|
className
|
|
201
211
|
)}
|
|
@@ -2,21 +2,18 @@
|
|
|
2
2
|
* @jest-environment jsdom
|
|
3
3
|
*/
|
|
4
4
|
import React from "react";
|
|
5
|
-
import { render
|
|
5
|
+
import { render } from "@testing-library/react";
|
|
6
6
|
import { VirtualTable } from "./VirtualTable";
|
|
7
7
|
import { VirtualTableProps } from "./VirtualTableProps";
|
|
8
8
|
|
|
9
|
-
//
|
|
9
|
+
// Counts executions of the row component body. The real VirtualTableRow is
|
|
10
|
+
// swapped for a memoized stub so the count reflects React's bail-out decision
|
|
11
|
+
// and nothing else.
|
|
10
12
|
const renderCallback = jest.fn();
|
|
11
13
|
jest.mock("./VirtualTableRow", () => {
|
|
12
|
-
|
|
13
14
|
const React = require("react");
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// The issue we are testing is CONTEXT.
|
|
17
|
-
// VirtualTableRow consumes context. If context value changes, it re-renders even if props are same.
|
|
18
|
-
// So we don't even need to pass props to it in the mock, but the real one does.
|
|
19
|
-
|
|
15
|
+
// Memoized like the real component: without this the stub would re-render
|
|
16
|
+
// on any parent render and the count would say nothing about the context.
|
|
20
17
|
const VirtualTableRow = React.memo((props: any) => {
|
|
21
18
|
renderCallback();
|
|
22
19
|
return <div data-testid="row">{props.children}</div>;
|
|
@@ -25,7 +22,8 @@ jest.mock("./VirtualTableRow", () => {
|
|
|
25
22
|
return { VirtualTableRow };
|
|
26
23
|
});
|
|
27
24
|
|
|
28
|
-
//
|
|
25
|
+
// Fixed bounds: the real hook measures nothing in jsdom, so the list would
|
|
26
|
+
// render zero rows.
|
|
29
27
|
jest.mock("react-use-measure", () => {
|
|
30
28
|
return () => [
|
|
31
29
|
(element: any) => {},
|
|
@@ -40,357 +38,53 @@ y: 0 }
|
|
|
40
38
|
];
|
|
41
39
|
});
|
|
42
40
|
|
|
43
|
-
//
|
|
41
|
+
// jsdom has no ResizeObserver, and VirtualTable passes the global in as the
|
|
42
|
+
// `react-use-measure` polyfill — the identifier has to resolve even though the
|
|
43
|
+
// mock above means it is never constructed.
|
|
44
44
|
global.ResizeObserver = class ResizeObserver {
|
|
45
|
-
callback: any;
|
|
46
|
-
constructor(callback: any) {
|
|
47
|
-
this.callback = callback;
|
|
48
|
-
// Expose callback globally or on the instance to trigger it
|
|
49
|
-
(global as any).triggerResize = callback;
|
|
50
|
-
}
|
|
51
45
|
observe() {}
|
|
52
46
|
unobserve() {}
|
|
53
47
|
disconnect() {}
|
|
54
48
|
};
|
|
55
49
|
|
|
56
50
|
describe("VirtualTable Performance", () => {
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
renderCallback.mockClear();
|
|
54
|
+
});
|
|
55
|
+
|
|
57
56
|
it("does not re-render rows when VirtualTable re-renders but data is unchanged", async () => {
|
|
58
57
|
const columns = [{ key: "col1",
|
|
59
58
|
title: "Column 1",
|
|
60
59
|
width: 100 }];
|
|
61
60
|
const data = Array.from({ length: 10 }).map((_, i) => ({ col1: `Value ${i}` }));
|
|
61
|
+
const cellRenderer = () => <div>Cell</div>;
|
|
62
62
|
|
|
63
63
|
const props: VirtualTableProps<any> = {
|
|
64
64
|
data,
|
|
65
65
|
columns,
|
|
66
66
|
rowHeight: 50,
|
|
67
|
-
cellRenderer
|
|
67
|
+
cellRenderer
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
-
render(<VirtualTable {...props}
|
|
70
|
+
const { rerender } = render(<VirtualTable {...props}
|
|
71
|
+
className={"a"}/>);
|
|
71
72
|
|
|
72
|
-
// Initial render count
|
|
73
|
-
// 10 items + potentially overscan.
|
|
74
|
-
// Let's just store the current count.
|
|
75
73
|
const initialRenderCount = renderCallback.mock.calls.length;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const afterResizeRenderCount = renderCallback.mock.calls.length;
|
|
92
|
-
console.log("After resize render count:", afterResizeRenderCount);
|
|
93
|
-
|
|
94
|
-
// The Fix:
|
|
95
|
-
// If virtualListController is memoized, the context value shouldn't change.
|
|
96
|
-
// VirtualTableRow consumes context. If context is stable, it shouldn't re-render (since it is React.memo'd and its props didn't change).
|
|
97
|
-
// Wait, VirtualTableRow receives props from MemoizedList -> Row.
|
|
98
|
-
// Row renders <VirtualTableRow ...>.
|
|
99
|
-
// If VirtualTable re-renders -> MemoizedList re-renders?
|
|
100
|
-
// VirtualTable renders:
|
|
101
|
-
// <VirtualListContext.Provider value={virtualListController}>
|
|
102
|
-
// <MemoizedList ... />
|
|
103
|
-
// </VirtualListContext.Provider>
|
|
104
|
-
|
|
105
|
-
// MemoizedList is defined outside or inside?
|
|
106
|
-
// It is defined OUTSIDE VirtualTable as:
|
|
107
|
-
// function MemoizedList(...) { ... }
|
|
108
|
-
// BUT, inside VirtualTable it is used as <MemoizedList ... />.
|
|
109
|
-
// If VirtualTable re-renders, React.createElement(MemoizedList, ...) is called.
|
|
110
|
-
// If props to MemoizedList are same, does it re-render?
|
|
111
|
-
// MemoizedList is NOT wrapped in React.memo (in the file it's just `function MemoizedList`).
|
|
112
|
-
// BUT, `react-window`'s `FixedSizeList` (which MemoizedList returns) IS optimized.
|
|
113
|
-
// However, `MemoizedList` itself is a functional component. If parent renders, it renders.
|
|
114
|
-
// AND `MemoizedList` defines `const Row = useCallback(...)`.
|
|
115
|
-
// If `MemoizedList` re-renders, `Row` might be recreated if deps change?
|
|
116
|
-
// `Row` has `[]` deps. So `Row` is stable!
|
|
117
|
-
|
|
118
|
-
// Wait, `MemoizedList` passes `Row` to `List`.
|
|
119
|
-
// `List` (react-window) renders `Row`.
|
|
120
|
-
// `Row` contains `<VirtualListContext.Consumer>`.
|
|
121
|
-
// So `Row` renders. It consumes context.
|
|
122
|
-
// If context value changed, `Row` re-renders (or the consumer part does).
|
|
123
|
-
// Inside Consumer: `return <VirtualTableRow ... />`.
|
|
124
|
-
// If context value changed, the function inside Consumer runs.
|
|
125
|
-
// It creates new props for VirtualTableRow?
|
|
126
|
-
// `columns` comes from context. `data` comes from context.
|
|
127
|
-
// If `virtualListController` (context value) is new, `columns` and `data` might be same ref, but the context object is new.
|
|
128
|
-
// Consumer runs.
|
|
129
|
-
// `<VirtualTableRow ... />` is created.
|
|
130
|
-
// `VirtualTableRow` is `React.memo`.
|
|
131
|
-
// It compares new props vs old props.
|
|
132
|
-
// `columns` (from context) -> same ref? Yes (from useState in VirtualTable).
|
|
133
|
-
// `data` (from context) -> same ref? Yes (from props).
|
|
134
|
-
// `onRowClick` -> same ref? Yes (if useCallback/useMemo used properly in VirtualTable).
|
|
135
|
-
|
|
136
|
-
// So actually, even if context object changes, if the *properties* of the context object are stable, `VirtualTableRow` might NOT re-render if it only receives those properties as props.
|
|
137
|
-
// Let's check `VirtualTable.tsx` again.
|
|
138
|
-
|
|
139
|
-
// In `MemoizedList` -> `Row`:
|
|
140
|
-
/*
|
|
141
|
-
<VirtualListContext.Consumer>
|
|
142
|
-
{({ data, columns, ... }) => {
|
|
143
|
-
// ...
|
|
144
|
-
return <VirtualTableRow data={data} columns={columns} ... />
|
|
145
|
-
}}
|
|
146
|
-
</VirtualListContext.Consumer>
|
|
147
|
-
*/
|
|
148
|
-
|
|
149
|
-
// If context object changes (new reference), Consumer re-runs.
|
|
150
|
-
// It calls the render prop.
|
|
151
|
-
// The render prop returns `<VirtualTableRow ... />`.
|
|
152
|
-
// React sees a `VirtualTableRow` element.
|
|
153
|
-
// It checks `React.memo` (equal).
|
|
154
|
-
// `equal(prevProps, nextProps)`.
|
|
155
|
-
// `prevProps.columns` vs `nextProps.columns`.
|
|
156
|
-
// If `virtualListController` was recreated:
|
|
157
|
-
/*
|
|
158
|
-
const virtualListController = {
|
|
159
|
-
data, // stable ref
|
|
160
|
-
columns, // stable ref (state)
|
|
161
|
-
...
|
|
162
|
-
onColumnResize: ... // stable (useCallback)
|
|
163
|
-
};
|
|
164
|
-
*/
|
|
165
|
-
|
|
166
|
-
// So `virtualListController` is a NEW object, but its properties are largely STABLE.
|
|
167
|
-
// So Consumer runs, but `VirtualTableRow` receives same props (shallowly equal).
|
|
168
|
-
// `react-fast-compare` (used in VirtualTableRow) should return true.
|
|
169
|
-
// So `VirtualTableRow` should NOT re-render even if context changes?
|
|
170
|
-
|
|
171
|
-
// WAIT. `VirtualTableRow` does NOT consume the context itself. `Row` (the render prop of ListIcon) consumes it.
|
|
172
|
-
// So `Row` renders -> Consumer renders -> Render Prop runs -> returns VirtualTableRow.
|
|
173
|
-
// VirtualTableRow is memoized.
|
|
174
|
-
// So if props are equal, it shouldn't render.
|
|
175
|
-
|
|
176
|
-
// WHY did the Codebase Investigator say: "This object is passed to a `React.Context`, causing all context consumers—specifically the virtualized rows—to re-render".
|
|
177
|
-
// Maybe I missed something.
|
|
178
|
-
// Does `VirtualTableRow` use the context?
|
|
179
|
-
// No, `VirtualTableRow.tsx` does not use `useContext`.
|
|
180
|
-
|
|
181
|
-
// However, `VirtualTableCell`?
|
|
182
|
-
// `VirtualTableCell` is rendered by `VirtualTableRow`.
|
|
183
|
-
|
|
184
|
-
// Let's look at `VirtualTable.tsx` again.
|
|
185
|
-
// The `Row` component:
|
|
186
|
-
/*
|
|
187
|
-
const Row = useCallback(({ index, style }) => {
|
|
188
|
-
return <VirtualListContext.Consumer>
|
|
189
|
-
{(context) => {
|
|
190
|
-
// ...
|
|
191
|
-
return <VirtualTableRow ...> ... </VirtualTableRow>
|
|
192
|
-
}}
|
|
193
|
-
</VirtualListContext.Consumer>
|
|
194
|
-
}, []);
|
|
195
|
-
*/
|
|
196
|
-
|
|
197
|
-
// The `Row` component is passed to `react-window` `List`.
|
|
198
|
-
// `List` renders `Row`.
|
|
199
|
-
// `Row` renders `Consumer`.
|
|
200
|
-
// If Context Provider value changes, ALL Consumers re-render.
|
|
201
|
-
// So the function inside `Consumer` runs.
|
|
202
|
-
// It returns `VirtualTableRow`.
|
|
203
|
-
// React attempts to update `VirtualTableRow`.
|
|
204
|
-
// `VirtualTableRow` is `React.memo`.
|
|
205
|
-
// It compares props.
|
|
206
|
-
|
|
207
|
-
// Are props different?
|
|
208
|
-
// `columns` -> state. Stable.
|
|
209
|
-
// `data` -> prop. Stable.
|
|
210
|
-
// `onRowClick` -> `virtualListController.onRowClick`.
|
|
211
|
-
// In `VirtualTable.tsx`:
|
|
212
|
-
// `const virtualListController = { ... onRowClick, ... }`
|
|
213
|
-
// `onRowClick` comes from props. Stable.
|
|
214
|
-
|
|
215
|
-
// `cellRenderer` -> prop. Stable.
|
|
216
|
-
|
|
217
|
-
// `rowClassName` -> prop. Stable.
|
|
218
|
-
|
|
219
|
-
// So... if all props are stable, why is it a bottleneck?
|
|
220
|
-
// Maybe `virtualListController` creation is expensive? No.
|
|
221
|
-
// Maybe `react-window` does something?
|
|
222
|
-
|
|
223
|
-
// Or maybe one of the properties IS unstable?
|
|
224
|
-
// `onColumnResizeInternal`: useCallback. Stable.
|
|
225
|
-
// `onColumnResizeEndInternal`: useCallback. Stable.
|
|
226
|
-
// `onFilterUpdateInternal`: useCallback. Stable.
|
|
227
|
-
// `onColumnSort`: useCallback. Stable.
|
|
228
|
-
|
|
229
|
-
// `filterRef.current` -> ref. Stable.
|
|
230
|
-
|
|
231
|
-
// Wait, `virtualListController` object ITSELF is new.
|
|
232
|
-
// `VirtualListContext.Provider value={virtualListController}` receives new object.
|
|
233
|
-
// Provider updates.
|
|
234
|
-
// All Consumers update.
|
|
235
|
-
// The overhead of running the Consumer function for 100 rows is non-zero, but `VirtualTableRow` (the heavy part?) shouldn't re-render if props are same.
|
|
236
|
-
|
|
237
|
-
// UNLESS... `VirtualTableCell`?
|
|
238
|
-
// `VirtualTableRow` has children:
|
|
239
|
-
/*
|
|
240
|
-
{columns.map((column, columnIndex) => {
|
|
241
|
-
return <VirtualTableCell ... />
|
|
242
|
-
})}
|
|
243
|
-
*/
|
|
244
|
-
// If `VirtualTableRow` doesn't re-render, its children don't re-render.
|
|
245
|
-
|
|
246
|
-
// So where is the bottleneck?
|
|
247
|
-
// "The root cause is the re-creation of the `virtualListController` object... This triggers a cascade of re-renders in all consumer components (every visible row and cell)"
|
|
248
|
-
|
|
249
|
-
// If `Consumer` runs, it means React has to traverse down to that node.
|
|
250
|
-
// If `VirtualTableRow` props are equal, it stops there.
|
|
251
|
-
|
|
252
|
-
// Maybe `VirtualTableRow` props are NOT equal?
|
|
253
|
-
// Let's verify `VirtualTableRow` props.
|
|
254
|
-
/*
|
|
255
|
-
<VirtualTableRow
|
|
256
|
-
key={`row_${index}`}
|
|
257
|
-
rowData={rowData}
|
|
258
|
-
rowIndex={index}
|
|
259
|
-
onRowClick={onRowClick}
|
|
260
|
-
columns={columns}
|
|
261
|
-
hoverRow={hoverRow}
|
|
262
|
-
rowClassName={rowClassName}
|
|
263
|
-
style={{...}}
|
|
264
|
-
rowHeight={rowHeight}
|
|
265
|
-
>
|
|
266
|
-
*/
|
|
267
|
-
|
|
268
|
-
// `style` comes from `react-window`'s `Row` props. `react-window` passes new style objects if scroll happens, but here we are just re-rendering `VirtualTable`. `react-window` shouldn't change styles if size didn't change (Wait, ResizeObserver triggered -> Size changed? No, we just triggered it, assuming size is same but callback fired).
|
|
269
|
-
// Actually `style` in `Row` callback comes from `react-window`.
|
|
270
|
-
|
|
271
|
-
// `onRowClick`: `virtualListController.onRowClick` (from props).
|
|
272
|
-
|
|
273
|
-
// BUT, what about `VirtualTableCell`?
|
|
274
|
-
// Inside `VirtualTableRow` children:
|
|
275
|
-
/*
|
|
276
|
-
<VirtualTableCell
|
|
277
|
-
cellRenderer={cellRenderer}
|
|
278
|
-
column={column}
|
|
279
|
-
columns={columns}
|
|
280
|
-
...
|
|
281
|
-
/>
|
|
282
|
-
*/
|
|
283
|
-
// `cellRenderer` comes from context.
|
|
284
|
-
|
|
285
|
-
// If `VirtualTableRow` re-renders, `VirtualTableCell` is re-created (React Element).
|
|
286
|
-
// `VirtualTableCell` is `React.memo`.
|
|
287
|
-
// Comparison runs.
|
|
288
|
-
|
|
289
|
-
// If `VirtualTableRow` does NOT re-render, then `VirtualTableCell` is not even touched.
|
|
290
|
-
|
|
291
|
-
// So the key is whether `VirtualTableRow` re-renders.
|
|
292
|
-
// If I can prove `VirtualTableRow` renders fewer times with the fix, I'm good.
|
|
293
|
-
|
|
294
|
-
// Why would `VirtualTableRow` props be different?
|
|
295
|
-
// Maybe `equal` (react-fast-compare) returns false for some reason?
|
|
296
|
-
// Or maybe `virtualListController` has some property that IS new every time?
|
|
297
|
-
|
|
298
|
-
// `const virtualListController = { ... }`.
|
|
299
|
-
// `filter: filterRef.current`.
|
|
300
|
-
// `currentSort`.
|
|
301
|
-
// `sortByProperty`.
|
|
302
|
-
|
|
303
|
-
// If `VirtualTableRow` props are deeply equal, `react-fast-compare` returns true.
|
|
304
|
-
|
|
305
|
-
// Wait. `MemoizedList` component.
|
|
306
|
-
/*
|
|
307
|
-
function MemoizedList(...) {
|
|
308
|
-
const Row = useCallback(...)
|
|
309
|
-
return <ListIcon ...>{Row}</ListIcon>
|
|
310
|
-
}
|
|
311
|
-
*/
|
|
312
|
-
// `MemoizedList` is NOT memoized (it's a function).
|
|
313
|
-
// In `VirtualTable`:
|
|
314
|
-
/*
|
|
315
|
-
<MemoizedList ... />
|
|
316
|
-
*/
|
|
317
|
-
// Parent renders -> `MemoizedList` renders.
|
|
318
|
-
// `Row` is `useCallback(..., [])`. Dependency array is empty!
|
|
319
|
-
// So `Row` is STABLE.
|
|
320
|
-
|
|
321
|
-
// `List` (react-window) receives stable `Row`.
|
|
322
|
-
// `List` is `PureComponent` (usually).
|
|
323
|
-
// If `List` props (width, height, itemCount) are same, `List` should NOT re-render?
|
|
324
|
-
// `width` and `height` come from `bounds`. If `bounds` didn't change, they are same.
|
|
325
|
-
|
|
326
|
-
// So if `VirtualTable` re-renders, but `bounds` are same:
|
|
327
|
-
// `MemoizedList` renders.
|
|
328
|
-
// `List` receives same props.
|
|
329
|
-
// `List` does NOT re-render.
|
|
330
|
-
// So `Row`s are NOT re-rendered by `react-window`.
|
|
331
|
-
|
|
332
|
-
// BUT, `VirtualListContext.Provider` is ABOVE `MemoizedList`.
|
|
333
|
-
// <Provider value={new_value}>
|
|
334
|
-
// <MemoizedList />
|
|
335
|
-
// </Provider>
|
|
336
|
-
|
|
337
|
-
// If Provider value changes, it bypasses intermediate components (`MemoizedList`, `List`) and goes straight to Consumers?
|
|
338
|
-
// YES. Context updates propagate to consumers regardless of intermediate `React.memo` or `PureComponent`.
|
|
339
|
-
|
|
340
|
-
// So `Row` (which contains `Consumer`) will update?
|
|
341
|
-
// `Row` is a component definition passed to `List`.
|
|
342
|
-
// `List` renders instances of `Row`.
|
|
343
|
-
// These instances contain `Consumer`.
|
|
344
|
-
// Use of `<Context.Consumer>` or `useContext` creates a subscription.
|
|
345
|
-
// So the `Consumer` component inside the rendered `Row` updates.
|
|
346
|
-
|
|
347
|
-
// The children function of `Consumer` executes.
|
|
348
|
-
// It returns `<VirtualTableRow ... />`.
|
|
349
|
-
// React compares this new element with previous.
|
|
350
|
-
// If `VirtualTableRow` is `React.memo`, it checks props.
|
|
351
|
-
|
|
352
|
-
// The premise of the optimization is that checking `React.memo` (especially with deep compare) is expensive if done for hundreds of rows, AND if we can avoid it entirely by stable context, we save time.
|
|
353
|
-
// If Context is stable, `Consumer` doesn't update.
|
|
354
|
-
// So the function inside Consumer doesn't run.
|
|
355
|
-
// So `VirtualTableRow` comparison doesn't happen.
|
|
356
|
-
|
|
357
|
-
// So even if `VirtualTableRow` returns `true` for equality (no re-render), the *attempt* to render it (Consumer running, creating Element, running comparison) happens.
|
|
358
|
-
|
|
359
|
-
// My test counts `VirtualTableRow` *renders* (function execution).
|
|
360
|
-
// If `React.memo` works, the function body of `VirtualTableRow` should NOT run.
|
|
361
|
-
// The mock I wrote:
|
|
362
|
-
/*
|
|
363
|
-
const VirtualTableRow = React.memo((props) => {
|
|
364
|
-
renderCallback();
|
|
365
|
-
...
|
|
366
|
-
})
|
|
367
|
-
*/
|
|
368
|
-
// So `renderCallback` tracks when `VirtualTableRow` component function executes.
|
|
369
|
-
// If `React.memo` returns true (props equal), the function does NOT execute.
|
|
370
|
-
|
|
371
|
-
// So... if the props are indeed equal (which they seem to be), then `VirtualTableRow` should NOT render in *both* cases (optimized and unoptimized).
|
|
372
|
-
// The optimization saves the *overhead* of the Consumer update and the Memo comparison.
|
|
373
|
-
// But my test measures *Component Renders*.
|
|
374
|
-
// If `VirtualTableRow` never renders in both cases, the test won't show a difference.
|
|
375
|
-
|
|
376
|
-
// However, if `VirtualTableRow` *does* re-render currently (meaning props are NOT equal), then my fix will stop it (by not even triggering the update).
|
|
377
|
-
|
|
378
|
-
// Why would props be unequal?
|
|
379
|
-
// `columns` is `columnsProp` state.
|
|
380
|
-
// `data` is `data` prop.
|
|
381
|
-
// `style` ...
|
|
382
|
-
|
|
383
|
-
// If the test shows 0 re-renders in the "after" check, it's good.
|
|
384
|
-
// If it shows >0 in the "unoptimized" check, then I fixed something visible.
|
|
385
|
-
|
|
386
|
-
// Let's assume there is *some* prop instability or that checking all rows is the issue.
|
|
387
|
-
// Or maybe `react-fast-compare` is not as fast or returns false?
|
|
388
|
-
|
|
389
|
-
// Actually, I can also instrument the `Consumer`? No, that's inside the component.
|
|
390
|
-
|
|
391
|
-
// Let's just run the test and see. If "before" count is high and "after" is low, I win.
|
|
392
|
-
// If both are 0, I need a better test (maybe measure time?).
|
|
393
|
-
|
|
394
|
-
// I will write the test file now.
|
|
74
|
+
expect(initialRenderCount).toBe(data.length);
|
|
75
|
+
|
|
76
|
+
// `className` is deliberately NOT one of the context memo's dependencies:
|
|
77
|
+
// changing it forces VirtualTable's body to run again (React.memo on the
|
|
78
|
+
// table itself would otherwise bail out) while every value the rows read
|
|
79
|
+
// stays identical.
|
|
80
|
+
rerender(<VirtualTable {...props}
|
|
81
|
+
className={"b"}/>);
|
|
82
|
+
|
|
83
|
+
// Rows read everything through VirtualListContext, and the value handed
|
|
84
|
+
// to the provider is memoized. If that memo goes away the provider gets a
|
|
85
|
+
// fresh object, every Consumer re-runs, and each one hands the row a new
|
|
86
|
+
// `style` object and a new children array — which defeats React.memo and
|
|
87
|
+
// re-renders all 10 rows. So an unchanged count is the whole assertion.
|
|
88
|
+
expect(renderCallback.mock.calls.length).toBe(initialRenderCount);
|
|
395
89
|
});
|
|
396
90
|
});
|
|
@@ -108,7 +108,7 @@ zIndex: 20 }}>
|
|
|
108
108
|
*
|
|
109
109
|
* @group Components
|
|
110
110
|
*/
|
|
111
|
-
|
|
111
|
+
const VirtualTableInner = React.memo<VirtualTableProps<Record<string, unknown>>>(
|
|
112
112
|
function VirtualTable<T extends Record<string, unknown>>({
|
|
113
113
|
data,
|
|
114
114
|
onResetPagination,
|
|
@@ -427,6 +427,24 @@ export const VirtualTable = React.memo<VirtualTableProps<Record<string, unknown>
|
|
|
427
427
|
},
|
|
428
428
|
equal
|
|
429
429
|
);
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* `React.memo` is not generic-preserving: it takes the props type as its own
|
|
433
|
+
* type argument, so annotating the call with
|
|
434
|
+
* `VirtualTableProps<Record<string, unknown>>` pinned `T` at the boundary and
|
|
435
|
+
* discarded the `<T>` the inner function declares. The exported component was
|
|
436
|
+
* therefore not generic at all — every consumer got `Record<string, unknown>`,
|
|
437
|
+
* `columns` and `cellRenderer` were unrelated to `data`, and a caller writing
|
|
438
|
+
* `<VirtualTable<Post> …>` was told the component expects no type arguments.
|
|
439
|
+
*
|
|
440
|
+
* Re-asserting the call signature is the standard way to carry a type parameter
|
|
441
|
+
* across `memo`; it is the only cast here, and it restores the generic the
|
|
442
|
+
* implementation always had. Runtime behaviour is unchanged — this is the same
|
|
443
|
+
* memoized component under a type that describes it.
|
|
444
|
+
*/
|
|
445
|
+
export const VirtualTable = VirtualTableInner as <T extends Record<string, unknown>>(
|
|
446
|
+
props: VirtualTableProps<T>
|
|
447
|
+
) => React.ReactElement | null;
|
|
430
448
|
// Wrapper that applies sortable transforms to cells
|
|
431
449
|
const SortableCellWrapper = ({
|
|
432
450
|
columnKey,
|
|
@@ -12,16 +12,21 @@ export function VirtualTableDateField(props: {
|
|
|
12
12
|
updateValue: (newValue: (Date | null)) => void;
|
|
13
13
|
focused: boolean;
|
|
14
14
|
disabled: boolean;
|
|
15
|
+
small?: boolean;
|
|
15
16
|
locale?: string;
|
|
16
17
|
onBlur?: React.FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
|
|
17
18
|
}) {
|
|
18
|
-
const { disabled, error, mode, timezone, internalValue, updateValue, locale } = props;
|
|
19
|
+
const { disabled, error, mode, timezone, internalValue, updateValue, small = false, locale } = props;
|
|
19
20
|
|
|
20
21
|
return (
|
|
21
22
|
<DateTimeField
|
|
22
23
|
value={internalValue ?? undefined}
|
|
23
24
|
onChange={(dateValue) => updateValue(dateValue ?? null)}
|
|
24
25
|
invisible={true}
|
|
26
|
+
// Without this the field falls back to DateTimeField's own default of
|
|
27
|
+
// `size="large"` (min-h-64px), which overflows a table cell and makes
|
|
28
|
+
// the value collide with the cell border.
|
|
29
|
+
size={small ? "small" : "medium"}
|
|
25
30
|
inputClassName={cls("w-full h-full", focusedDisabled)}
|
|
26
31
|
className={cls("w-full h-full", focusedDisabled)}
|
|
27
32
|
mode={mode}
|
package/src/components/index.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// ./common/SelectInputLabel is internal — Select and MultiSelect render it for
|
|
2
|
+
// a string `label`. Pass a ReactNode `label` to override it.
|
|
2
3
|
export * from "./Autocomplete";
|
|
3
4
|
export * from "./Alert";
|
|
4
5
|
export * from "./Avatar";
|
package/src/icons/GitHubIcon.tsx
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
|
|
3
|
-
import { IconProps } from "./Icon";
|
|
3
|
+
import { colorClassesMapping, IconProps, iconSize } from "./Icon";
|
|
4
|
+
import { cls } from "../util";
|
|
4
5
|
|
|
5
|
-
export function GitHubIcon(
|
|
6
|
+
export function GitHubIcon({ size = 24, color, className, onClick, style }: IconProps) {
|
|
7
|
+
// `size` is `IconSize | number` — resolve the keyword, or an SVG attribute
|
|
8
|
+
// of width="small" is emitted and the icon renders at its intrinsic size.
|
|
9
|
+
const px = typeof size === "number" ? size : iconSize[size];
|
|
6
10
|
return <svg xmlns="http://www.w3.org/2000/svg"
|
|
7
|
-
className={
|
|
11
|
+
className={cls(color ? colorClassesMapping[color] : "", className)}
|
|
12
|
+
onClick={onClick}
|
|
13
|
+
style={style}
|
|
8
14
|
fill={"currentColor"}
|
|
9
|
-
width={
|
|
10
|
-
height={
|
|
15
|
+
width={px}
|
|
16
|
+
height={px}
|
|
11
17
|
viewBox="0 0 24 24">
|
|
12
18
|
<path
|
|
13
19
|
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
package/src/icons/HandleIcon.tsx
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
import { colorClassesMapping, IconProps, iconSize } from "./Icon";
|
|
4
|
+
import { cls } from "../util";
|
|
5
|
+
|
|
6
|
+
export function HandleIcon({ size = 24, color, className, onClick, style }: IconProps) {
|
|
7
|
+
const px = typeof size === "number" ? size : iconSize[size];
|
|
8
|
+
return <svg xmlns="http://www.w3.org/2000/svg"
|
|
9
|
+
className={cls(color ? colorClassesMapping[color] : "", className)}
|
|
10
|
+
onClick={onClick}
|
|
11
|
+
style={style}
|
|
12
|
+
width={px}
|
|
13
|
+
height={px}
|
|
14
|
+
viewBox="0 0 100 100"
|
|
15
|
+
fill="none">
|
|
3
16
|
<circle cx="28" cy="50" r="9" fill={"currentColor"}/>
|
|
4
17
|
<circle cx="28" cy="21" r="9" fill={"currentColor"}/>
|
|
5
18
|
<circle cx="71" cy="21" r="9" fill={"currentColor"}/>
|