react-native-layout-dnd 0.1.0-alpha.1 → 0.1.0-alpha.2
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/README.md +124 -57
- package/lib/module/components/DndProvider.js +44 -5
- package/lib/module/components/DndProvider.js.map +1 -1
- package/lib/module/components/GridController.js +48 -0
- package/lib/module/components/GridController.js.map +1 -1
- package/lib/module/components/SortableGrid.js +31 -4
- package/lib/module/components/SortableGrid.js.map +1 -1
- package/lib/module/components/SortableList.js +15 -1
- package/lib/module/components/SortableList.js.map +1 -1
- package/lib/module/controller/dndSession.js +8 -1
- package/lib/module/controller/dndSession.js.map +1 -1
- package/lib/module/pointer/geometry.js +5 -2
- package/lib/module/pointer/geometry.js.map +1 -1
- package/lib/module/runtime/committedSize.js +42 -0
- package/lib/module/runtime/committedSize.js.map +1 -0
- package/lib/typescript/src/components/DndProvider.d.ts +1 -1
- package/lib/typescript/src/components/DndProvider.d.ts.map +1 -1
- package/lib/typescript/src/components/GridController.d.ts +1 -1
- package/lib/typescript/src/components/GridController.d.ts.map +1 -1
- package/lib/typescript/src/components/SortableGrid.d.ts.map +1 -1
- package/lib/typescript/src/components/SortableList.d.ts.map +1 -1
- package/lib/typescript/src/components/gridTypes.d.ts +6 -0
- package/lib/typescript/src/components/gridTypes.d.ts.map +1 -1
- package/lib/typescript/src/controller/dndSession.d.ts +6 -0
- package/lib/typescript/src/controller/dndSession.d.ts.map +1 -1
- package/lib/typescript/src/pointer/geometry.d.ts +1 -1
- package/lib/typescript/src/pointer/geometry.d.ts.map +1 -1
- package/lib/typescript/src/runtime/committedSize.d.ts +19 -0
- package/lib/typescript/src/runtime/committedSize.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/DndProvider.tsx +55 -5
- package/src/components/GridController.tsx +57 -0
- package/src/components/SortableGrid.tsx +65 -6
- package/src/components/SortableList.tsx +38 -1
- package/src/components/gridTypes.ts +6 -0
- package/src/controller/dndSession.ts +14 -0
- package/src/pointer/geometry.ts +4 -0
- package/src/runtime/committedSize.ts +65 -0
|
@@ -226,6 +226,7 @@ export function DndProvider<T>({
|
|
|
226
226
|
onDragEnd,
|
|
227
227
|
onDragSettled,
|
|
228
228
|
onValidationError,
|
|
229
|
+
onValidationChange,
|
|
229
230
|
onDiagnostic,
|
|
230
231
|
responseTimeoutMs,
|
|
231
232
|
getGridItemLayout,
|
|
@@ -312,6 +313,53 @@ export function DndProvider<T>({
|
|
|
312
313
|
},
|
|
313
314
|
[reportSettled],
|
|
314
315
|
);
|
|
316
|
+
// Issues of the value and options (from the coordinator) and of mounted
|
|
317
|
+
// adapters and handles (by owner), published together as one list. A new
|
|
318
|
+
// value reaches the adapters over several synchronous commits, so the list
|
|
319
|
+
// is read once they are done; intermediate lists are never published.
|
|
320
|
+
const issues = useRef(
|
|
321
|
+
new Map<string, { key: string; issues: LayoutValidationIssue[] }>(),
|
|
322
|
+
);
|
|
323
|
+
const sessionIssues = useRef<LayoutValidationIssue[]>([]);
|
|
324
|
+
const validation = useRef({
|
|
325
|
+
published: '[]',
|
|
326
|
+
queued: false,
|
|
327
|
+
onChange: onValidationChange,
|
|
328
|
+
}).current;
|
|
329
|
+
const mounted = useRef(true);
|
|
330
|
+
const publishValidation = useCallback(() => {
|
|
331
|
+
if (!mounted.current || validation.queued) return;
|
|
332
|
+
validation.queued = true;
|
|
333
|
+
(
|
|
334
|
+
globalThis as typeof globalThis & {
|
|
335
|
+
queueMicrotask(callback: () => void): void;
|
|
336
|
+
}
|
|
337
|
+
).queueMicrotask(() => {
|
|
338
|
+
validation.queued = false;
|
|
339
|
+
if (!mounted.current) return;
|
|
340
|
+
const current = [
|
|
341
|
+
...sessionIssues.current,
|
|
342
|
+
...Array.from(issues.current.keys())
|
|
343
|
+
.sort()
|
|
344
|
+
.flatMap(owner => issues.current.get(owner)!.issues),
|
|
345
|
+
];
|
|
346
|
+
const key = JSON.stringify(current);
|
|
347
|
+
if (key === validation.published) return;
|
|
348
|
+
validation.published = key;
|
|
349
|
+
try {
|
|
350
|
+
validation.onChange?.(current);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
console.error('[LayoutDnD] Validation callback failed', error);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}, [validation]);
|
|
356
|
+
const reportSessionIssues = useCallback(
|
|
357
|
+
(next: LayoutValidationIssue[]) => {
|
|
358
|
+
sessionIssues.current = next;
|
|
359
|
+
publishValidation();
|
|
360
|
+
},
|
|
361
|
+
[publishValidation],
|
|
362
|
+
);
|
|
315
363
|
const [coordinator] = useState(
|
|
316
364
|
() =>
|
|
317
365
|
new DndSessionCoordinator({
|
|
@@ -321,6 +369,7 @@ export function DndProvider<T>({
|
|
|
321
369
|
onDragStart,
|
|
322
370
|
onDragEnd: endSession,
|
|
323
371
|
onValidationError,
|
|
372
|
+
onValidationChange: reportSessionIssues,
|
|
324
373
|
onDiagnostic,
|
|
325
374
|
responseTimeoutMs,
|
|
326
375
|
getGridItemLayout,
|
|
@@ -380,9 +429,7 @@ export function DndProvider<T>({
|
|
|
380
429
|
const nextHandleId = useRef(0);
|
|
381
430
|
const [gestureRegistry] = useState(createDndGestureRegistry);
|
|
382
431
|
const registry = useRef(new Map<object, RegisteredList>());
|
|
383
|
-
const issues = useRef(new Map<string, string>());
|
|
384
432
|
const observer = useRef(onValidationError);
|
|
385
|
-
const mounted = useRef(true);
|
|
386
433
|
const handlePublication = useRef({
|
|
387
434
|
queued: false,
|
|
388
435
|
generation: 0,
|
|
@@ -648,11 +695,12 @@ export function DndProvider<T>({
|
|
|
648
695
|
const reportIssues = useCallback(
|
|
649
696
|
(owner: string, next: LayoutValidationIssue[]) => {
|
|
650
697
|
const key = JSON.stringify(next);
|
|
651
|
-
const old = issues.current.get(owner);
|
|
652
|
-
if (next.length) issues.current.set(owner, key);
|
|
698
|
+
const old = issues.current.get(owner)?.key;
|
|
699
|
+
if (next.length) issues.current.set(owner, { key, issues: next });
|
|
653
700
|
else issues.current.delete(owner);
|
|
654
701
|
if (!mounted.current) return;
|
|
655
702
|
setRegistryInvalid(issues.current.size > 0);
|
|
703
|
+
publishValidation();
|
|
656
704
|
if (next.length && old !== key) {
|
|
657
705
|
coordinator.cancel('disabled');
|
|
658
706
|
try {
|
|
@@ -662,7 +710,7 @@ export function DndProvider<T>({
|
|
|
662
710
|
}
|
|
663
711
|
}
|
|
664
712
|
},
|
|
665
|
-
[coordinator],
|
|
713
|
+
[coordinator, publishValidation],
|
|
666
714
|
);
|
|
667
715
|
useLayoutEffect(() => {
|
|
668
716
|
mounted.current = true;
|
|
@@ -677,6 +725,7 @@ export function DndProvider<T>({
|
|
|
677
725
|
targetSession.set({ ...current, policyRevision: nextPolicyRevision });
|
|
678
726
|
});
|
|
679
727
|
observer.current = onValidationError;
|
|
728
|
+
validation.onChange = onValidationChange;
|
|
680
729
|
if (
|
|
681
730
|
currentConfig.current.dragAxis !== dragAxis ||
|
|
682
731
|
currentConfig.current.delayMs !== activation?.delayMs
|
|
@@ -695,6 +744,7 @@ export function DndProvider<T>({
|
|
|
695
744
|
onDragStart,
|
|
696
745
|
onDragEnd: endSession,
|
|
697
746
|
onValidationError,
|
|
747
|
+
onValidationChange: reportSessionIssues,
|
|
698
748
|
onDiagnostic,
|
|
699
749
|
responseTimeoutMs,
|
|
700
750
|
getGridItemLayout,
|
|
@@ -58,6 +58,7 @@ export function GridController<T>({
|
|
|
58
58
|
onDragStart,
|
|
59
59
|
onDragEnd,
|
|
60
60
|
onValidationError,
|
|
61
|
+
onValidationChange,
|
|
61
62
|
onDiagnostic,
|
|
62
63
|
searchBudget,
|
|
63
64
|
activation,
|
|
@@ -130,6 +131,61 @@ export function GridController<T>({
|
|
|
130
131
|
[onValidationError, valueValid],
|
|
131
132
|
);
|
|
132
133
|
|
|
134
|
+
// The current list: the value's own issues while it is invalid, otherwise
|
|
135
|
+
// the provider's (zone geometry, handles, options) in grid API codes.
|
|
136
|
+
const providerIssues = useRef<LayoutValidationIssue[]>([]);
|
|
137
|
+
// Both sources settle at the end of the provider's commits; publish once.
|
|
138
|
+
const validation = useRef({
|
|
139
|
+
valueIssues: converted.issues,
|
|
140
|
+
onChange: onValidationChange,
|
|
141
|
+
published: '[]',
|
|
142
|
+
queued: false,
|
|
143
|
+
active: true,
|
|
144
|
+
}).current;
|
|
145
|
+
const publishValidation = useCallback(() => {
|
|
146
|
+
if (!validation.active || validation.queued) return;
|
|
147
|
+
validation.queued = true;
|
|
148
|
+
(
|
|
149
|
+
globalThis as typeof globalThis & {
|
|
150
|
+
queueMicrotask(callback: () => void): void;
|
|
151
|
+
}
|
|
152
|
+
).queueMicrotask(() => {
|
|
153
|
+
validation.queued = false;
|
|
154
|
+
if (!validation.active) return;
|
|
155
|
+
const current = validation.valueIssues.length
|
|
156
|
+
? validation.valueIssues
|
|
157
|
+
: gridIssues(providerIssues.current);
|
|
158
|
+
const key = JSON.stringify(current);
|
|
159
|
+
if (key === validation.published) return;
|
|
160
|
+
validation.published = key;
|
|
161
|
+
try {
|
|
162
|
+
validation.onChange?.(current);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
console.error('[LayoutDnD] Validation callback failed', error);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}, [validation]);
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
validation.active = true;
|
|
170
|
+
return () => {
|
|
171
|
+
validation.active = false;
|
|
172
|
+
};
|
|
173
|
+
}, [validation]);
|
|
174
|
+
useEffect(() => {
|
|
175
|
+
validation.valueIssues = converted.issues;
|
|
176
|
+
validation.onChange = onValidationChange;
|
|
177
|
+
publishValidation();
|
|
178
|
+
}, [converted.issues, onValidationChange, publishValidation, validation]);
|
|
179
|
+
const handleProviderChange = useCallback(
|
|
180
|
+
(issues: LayoutValidationIssue[]) => {
|
|
181
|
+
providerIssues.current = issues;
|
|
182
|
+
// A resolved issue that comes back is new again for onValidationError.
|
|
183
|
+
if (issues.length === 0) lastProviderIssues.current = null;
|
|
184
|
+
publishValidation();
|
|
185
|
+
},
|
|
186
|
+
[publishValidation],
|
|
187
|
+
);
|
|
188
|
+
|
|
133
189
|
const handleChange = useCallback(
|
|
134
190
|
(next: DndValue<T>, proposal: DndProposal<T>) => {
|
|
135
191
|
pendingProposal.current = proposal;
|
|
@@ -194,6 +250,7 @@ export function GridController<T>({
|
|
|
194
250
|
onDragStart={onDragStart}
|
|
195
251
|
onDragSettled={handleEnd}
|
|
196
252
|
onValidationError={handleProviderIssues}
|
|
253
|
+
onValidationChange={handleProviderChange}
|
|
197
254
|
onDiagnostic={onDiagnostic}
|
|
198
255
|
searchBudget={searchBudget}
|
|
199
256
|
activation={activation}
|
|
@@ -20,6 +20,11 @@ import {
|
|
|
20
20
|
import type { GridItemRenderArgs, SortableGridProps } from './dndTypes';
|
|
21
21
|
import { presentationRect } from '../runtime/dndPresentation';
|
|
22
22
|
import { getCellGeometry, itemRect } from '../pointer/geometry';
|
|
23
|
+
import {
|
|
24
|
+
readCommittedSize,
|
|
25
|
+
sameViewSize,
|
|
26
|
+
type ViewSize,
|
|
27
|
+
} from '../runtime/committedSize';
|
|
23
28
|
|
|
24
29
|
function GridCellContent<T>({
|
|
25
30
|
item,
|
|
@@ -191,9 +196,12 @@ export function SortableGrid<T>({
|
|
|
191
196
|
};
|
|
192
197
|
}, []);
|
|
193
198
|
const ref = useAnimatedRef();
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
199
|
+
// Last known native size: onLayout, or the committed layout read below.
|
|
200
|
+
const [size, setSize] = useState<ViewSize | null>(null);
|
|
201
|
+
// Size read from the latest commit that changed size-related inputs.
|
|
202
|
+
const committedSize = useRef<ViewSize | null>(null);
|
|
203
|
+
// Key of a misfit confirmed against native layout; such cells are hidden.
|
|
204
|
+
const [clipped, setClipped] = useState<string | null>(null);
|
|
197
205
|
const zone = snapshot.zone;
|
|
198
206
|
const display = snapshot.display;
|
|
199
207
|
const revision = snapshot.revision;
|
|
@@ -216,10 +224,45 @@ export function SortableGrid<T>({
|
|
|
216
224
|
};
|
|
217
225
|
});
|
|
218
226
|
const pixelRatio = PixelRatio.get();
|
|
219
|
-
const
|
|
227
|
+
const fitted =
|
|
220
228
|
zone?.kind === 'grid' && size
|
|
221
229
|
? getCellGeometry(zone, geometry, size.width, size.height, pixelRatio)
|
|
222
230
|
: null;
|
|
231
|
+
// When props and the view size change in one commit (rotation), `size` is
|
|
232
|
+
// still the previous one here. Fixed cells do not depend on it, so keep
|
|
233
|
+
// them mounted at their new positions; the layout effect decides fit
|
|
234
|
+
// against this commit's native size.
|
|
235
|
+
const unfitted =
|
|
236
|
+
!fitted && zone?.kind === 'grid' && size
|
|
237
|
+
? getCellGeometry(
|
|
238
|
+
zone,
|
|
239
|
+
geometry,
|
|
240
|
+
size.width,
|
|
241
|
+
size.height,
|
|
242
|
+
pixelRatio,
|
|
243
|
+
false,
|
|
244
|
+
)
|
|
245
|
+
: null;
|
|
246
|
+
const misfitKey =
|
|
247
|
+
unfitted && size && zone?.kind === 'grid'
|
|
248
|
+
? JSON.stringify([
|
|
249
|
+
geometry.rowGap,
|
|
250
|
+
geometry.columnGap,
|
|
251
|
+
geometry.padding.top,
|
|
252
|
+
geometry.padding.right,
|
|
253
|
+
geometry.padding.bottom,
|
|
254
|
+
geometry.padding.left,
|
|
255
|
+
unfitted.cellWidth,
|
|
256
|
+
unfitted.cellHeight,
|
|
257
|
+
zone.rows,
|
|
258
|
+
zone.columns,
|
|
259
|
+
size.width,
|
|
260
|
+
size.height,
|
|
261
|
+
pixelRatio,
|
|
262
|
+
])
|
|
263
|
+
: null;
|
|
264
|
+
const fits = fitted !== null;
|
|
265
|
+
const cells = fitted ?? (misfitKey !== clipped ? unfitted : null);
|
|
223
266
|
// Compare values so mutating a geometry object in place cannot preserve stale
|
|
224
267
|
// hit-test coordinates. Registered geometry never aliases caller-owned values.
|
|
225
268
|
const geometryKey = cells
|
|
@@ -268,7 +311,19 @@ export function SortableGrid<T>({
|
|
|
268
311
|
[snapshot.items],
|
|
269
312
|
);
|
|
270
313
|
useLayoutEffect(() => {
|
|
271
|
-
|
|
314
|
+
// Fabric has laid out this commit already. A size newer than the last
|
|
315
|
+
// onLayout event re-renders synchronously, before paint, with that size.
|
|
316
|
+
// Other commits, such as drag previews, skip the native read.
|
|
317
|
+
const next = readCommittedSize(ref.current);
|
|
318
|
+
committedSize.current = next;
|
|
319
|
+
if (next && !sameViewSize(next, size)) setSize(next);
|
|
320
|
+
}, [ref, size, style, zone, geometry, measured]);
|
|
321
|
+
useLayoutEffect(() => {
|
|
322
|
+
// The re-render scheduled above validates; never mix the new props with
|
|
323
|
+
// the previous size.
|
|
324
|
+
if (committedSize.current && !sameViewSize(committedSize.current, size))
|
|
325
|
+
return;
|
|
326
|
+
const invalid = zone?.kind !== 'grid' || (size !== null && !fits);
|
|
272
327
|
reportIssues(
|
|
273
328
|
`grid:${zoneId}`,
|
|
274
329
|
invalid
|
|
@@ -285,10 +340,12 @@ export function SortableGrid<T>({
|
|
|
285
340
|
]
|
|
286
341
|
: [],
|
|
287
342
|
);
|
|
288
|
-
if (zone?.kind !== 'grid' || !measured) {
|
|
343
|
+
if (zone?.kind !== 'grid' || !fits || !measured) {
|
|
289
344
|
unregisterZone(zoneId, identity);
|
|
345
|
+
if (misfitKey) setClipped(misfitKey);
|
|
290
346
|
return;
|
|
291
347
|
}
|
|
348
|
+
setClipped(null);
|
|
292
349
|
registerZone(
|
|
293
350
|
{
|
|
294
351
|
kind: 'grid',
|
|
@@ -328,6 +385,8 @@ export function SortableGrid<T>({
|
|
|
328
385
|
ref,
|
|
329
386
|
measured,
|
|
330
387
|
size,
|
|
388
|
+
fits,
|
|
389
|
+
misfitKey,
|
|
331
390
|
registerZone,
|
|
332
391
|
unregisterZone,
|
|
333
392
|
reportIssues,
|
|
@@ -50,6 +50,11 @@ import {
|
|
|
50
50
|
type DndZonePresentation,
|
|
51
51
|
} from '../runtime/dndPresentation';
|
|
52
52
|
import type { RetainSlotsRule } from '../runtime/dndRenderStore';
|
|
53
|
+
import {
|
|
54
|
+
readCommittedSize,
|
|
55
|
+
sameViewSize,
|
|
56
|
+
type ViewSize,
|
|
57
|
+
} from '../runtime/committedSize';
|
|
53
58
|
import type { ListItemRenderArgs, SortableListProps } from './dndTypes';
|
|
54
59
|
import { useListCellMotion } from '../pointer/listCellMotion';
|
|
55
60
|
|
|
@@ -447,7 +452,12 @@ export function SortableList<T>({
|
|
|
447
452
|
typeof itemSize === 'function' ? false : virtualized ? retainSlots : true,
|
|
448
453
|
);
|
|
449
454
|
const identity = useRef({}).current;
|
|
450
|
-
|
|
455
|
+
// Last known native viewport: onLayout, or the committed layout read below.
|
|
456
|
+
const [viewportSize, setViewportSize] = useState<ViewSize>({
|
|
457
|
+
width: 0,
|
|
458
|
+
height: 0,
|
|
459
|
+
});
|
|
460
|
+
const committedViewport = useRef<ViewSize | null>(null);
|
|
451
461
|
const [measurementVersion, setMeasurementVersion] = useState(0);
|
|
452
462
|
const zone = snapshot.zone;
|
|
453
463
|
const displayZone = snapshot.display;
|
|
@@ -520,6 +530,26 @@ export function SortableList<T>({
|
|
|
520
530
|
// frame. Its newer version must not be acknowledged by this older render.
|
|
521
531
|
}, [measurements, renderedMeasurementVersion]);
|
|
522
532
|
const scrollRef = useAnimatedRef<ScrollView>();
|
|
533
|
+
useLayoutEffect(() => {
|
|
534
|
+
// Fabric has laid out this commit already. A viewport newer than the last
|
|
535
|
+
// onLayout event re-renders synchronously, before paint, so cells never
|
|
536
|
+
// combine new props with the previous cross size. Other commits, such as
|
|
537
|
+
// drag previews, skip the native read.
|
|
538
|
+
const next = readCommittedSize(scrollRef.current);
|
|
539
|
+
committedViewport.current = next;
|
|
540
|
+
if (next && !sameViewSize(next, viewportSize)) setViewportSize(next);
|
|
541
|
+
}, [
|
|
542
|
+
scrollRef,
|
|
543
|
+
viewportSize,
|
|
544
|
+
style,
|
|
545
|
+
orientation,
|
|
546
|
+
virtualized,
|
|
547
|
+
itemSize,
|
|
548
|
+
estimatedItemSize,
|
|
549
|
+
gap,
|
|
550
|
+
paddingStart,
|
|
551
|
+
paddingEnd,
|
|
552
|
+
]);
|
|
523
553
|
const offset = useSharedValue(0);
|
|
524
554
|
const { motion, animation, presentation, committedRevision } = runtime;
|
|
525
555
|
// Virtual cells position their content with a React-committed slot offset
|
|
@@ -728,6 +758,12 @@ export function SortableList<T>({
|
|
|
728
758
|
);
|
|
729
759
|
});
|
|
730
760
|
useLayoutEffect(() => {
|
|
761
|
+
// The re-render scheduled with this commit's viewport registers instead.
|
|
762
|
+
if (
|
|
763
|
+
committedViewport.current &&
|
|
764
|
+
!sameViewSize(committedViewport.current, viewportSize)
|
|
765
|
+
)
|
|
766
|
+
return;
|
|
731
767
|
if (!base?.valid || !display?.valid || !validScroll || !windowing.valid) {
|
|
732
768
|
unregisterZone(zoneId, identity);
|
|
733
769
|
return;
|
|
@@ -771,6 +807,7 @@ export function SortableList<T>({
|
|
|
771
807
|
virtualized,
|
|
772
808
|
prepareDisplay,
|
|
773
809
|
items,
|
|
810
|
+
viewportSize,
|
|
774
811
|
]);
|
|
775
812
|
useLayoutEffect(
|
|
776
813
|
() => () => {
|
|
@@ -63,7 +63,13 @@ export type GridControllerProps<T> = Omit<ViewProps, 'children'> & {
|
|
|
63
63
|
onDragStart?(event: DragStartEvent): void;
|
|
64
64
|
/** Fires once the drag preview has settled into its cell, or at once when nothing animates. */
|
|
65
65
|
onDragEnd?(event: DragEndEvent): void;
|
|
66
|
+
/** New issues only; it is not called when issues are resolved. */
|
|
66
67
|
onValidationError?(issues: ValidationIssue[]): void;
|
|
68
|
+
/**
|
|
69
|
+
* The complete current issue list whenever it changes, `[]` once every
|
|
70
|
+
* issue is resolved. Not called while the list stays empty.
|
|
71
|
+
*/
|
|
72
|
+
onValidationChange?(issues: ValidationIssue[]): void;
|
|
67
73
|
/** Optional JS-only observation. Exceptions are ignored; omit to disable timing. */
|
|
68
74
|
onDiagnostic?(event: GridDiagnosticEvent): void;
|
|
69
75
|
/** Defaults to 10,000 bounded search steps. */
|
|
@@ -104,7 +104,13 @@ export type DndSessionOptions<T> = {
|
|
|
104
104
|
canDrop?(proposal: DndProposal<T>): DropDecision;
|
|
105
105
|
onDragStart?(event: DragStartEvent): void;
|
|
106
106
|
onDragEnd?(event: DndDragEndEvent<T>): void;
|
|
107
|
+
/** New issues only; it is not called when issues are resolved. */
|
|
107
108
|
onValidationError?(issues: LayoutValidationIssue[]): void;
|
|
109
|
+
/**
|
|
110
|
+
* The complete current issue list whenever it changes, `[]` once every
|
|
111
|
+
* issue is resolved. Not called while the list stays empty.
|
|
112
|
+
*/
|
|
113
|
+
onValidationChange?(issues: LayoutValidationIssue[]): void;
|
|
108
114
|
/** Optional JS-only observation. Exceptions are ignored; omit to disable timing. */
|
|
109
115
|
onDiagnostic?(event: GridDiagnosticEvent): void;
|
|
110
116
|
responseTimeoutMs?: number;
|
|
@@ -324,6 +330,7 @@ function checkOptions<T>(options: DndSessionOptions<T>) {
|
|
|
324
330
|
'onDragStart',
|
|
325
331
|
'onDragEnd',
|
|
326
332
|
'onValidationError',
|
|
333
|
+
'onValidationChange',
|
|
327
334
|
'getGridItemLayout',
|
|
328
335
|
].some(key => {
|
|
329
336
|
const callback = options[key as keyof DndSessionOptions<T>];
|
|
@@ -558,6 +565,13 @@ export class DndSessionCoordinator<T> extends SessionCore<
|
|
|
558
565
|
reportCallbackError(error);
|
|
559
566
|
}
|
|
560
567
|
}
|
|
568
|
+
if (typeof options.onValidationChange === 'function') {
|
|
569
|
+
try {
|
|
570
|
+
options.onValidationChange(checked.issues);
|
|
571
|
+
} catch (error) {
|
|
572
|
+
reportCallbackError(error);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
561
575
|
}
|
|
562
576
|
});
|
|
563
577
|
}
|
package/src/pointer/geometry.ts
CHANGED
|
@@ -108,6 +108,9 @@ export function getCellGeometry(
|
|
|
108
108
|
width: number,
|
|
109
109
|
height: number,
|
|
110
110
|
pixelRatio = 1,
|
|
111
|
+
// false: fixed cells even when they overflow the viewport. Renderers use it
|
|
112
|
+
// to keep cells mounted until a misfit is confirmed against native layout.
|
|
113
|
+
requireFit = true,
|
|
111
114
|
): CellGeometry | null {
|
|
112
115
|
'worklet';
|
|
113
116
|
if (
|
|
@@ -170,6 +173,7 @@ export function getCellGeometry(
|
|
|
170
173
|
return Number.isFinite(requiredWidth) &&
|
|
171
174
|
Number.isFinite(requiredHeight) &&
|
|
172
175
|
(geometry.mode !== 'fixed' ||
|
|
176
|
+
!requireFit ||
|
|
173
177
|
(requiredWidth - width < pixelSize &&
|
|
174
178
|
requiredHeight - height < pixelSize)) &&
|
|
175
179
|
Number.isFinite(cellWidth) &&
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export type ViewSize = { width: number; height: number };
|
|
2
|
+
|
|
3
|
+
type MeasurableHost = {
|
|
4
|
+
measureLayout(
|
|
5
|
+
relativeTo: unknown,
|
|
6
|
+
onSuccess: (x: number, y: number, width: number, height: number) => void,
|
|
7
|
+
onFail?: () => void,
|
|
8
|
+
): void;
|
|
9
|
+
getNativeScrollRef?(): unknown;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function hostOf(instance: unknown): MeasurableHost | null {
|
|
13
|
+
const candidate = instance as Partial<MeasurableHost> | null | undefined;
|
|
14
|
+
const host =
|
|
15
|
+
typeof candidate?.getNativeScrollRef === 'function'
|
|
16
|
+
? (candidate.getNativeScrollRef() as Partial<MeasurableHost> | null)
|
|
17
|
+
: candidate;
|
|
18
|
+
return host && typeof host.measureLayout === 'function'
|
|
19
|
+
? (host as MeasurableHost)
|
|
20
|
+
: null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The size the latest React commit gave a host view, read from a layout
|
|
25
|
+
* effect. Fabric commits and lays out the shadow tree before layout effects
|
|
26
|
+
* run and answers a self-relative `measureLayout` synchronously, with the
|
|
27
|
+
* same untransformed frame that onLayout later reports. Props that change in
|
|
28
|
+
* the same commit as the view size can therefore be validated against that
|
|
29
|
+
* size instead of the previous onLayout event.
|
|
30
|
+
*
|
|
31
|
+
* Returns null when no answer arrives synchronously or the view has no
|
|
32
|
+
* layout (unmounted, `display: 'none'`, a renderer without the API); callers
|
|
33
|
+
* keep their onLayout size then.
|
|
34
|
+
*/
|
|
35
|
+
export function readCommittedSize(instance: unknown): ViewSize | null {
|
|
36
|
+
const host = hostOf(instance);
|
|
37
|
+
if (!host) return null;
|
|
38
|
+
let size = null as ViewSize | null;
|
|
39
|
+
try {
|
|
40
|
+
host.measureLayout(
|
|
41
|
+
host,
|
|
42
|
+
(_x, _y, width, height) => {
|
|
43
|
+
size = { width, height };
|
|
44
|
+
},
|
|
45
|
+
() => {},
|
|
46
|
+
);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
// An answer that arrives after this returns is ignored.
|
|
51
|
+
const result = size;
|
|
52
|
+
return result &&
|
|
53
|
+
Number.isFinite(result.width) &&
|
|
54
|
+
Number.isFinite(result.height) &&
|
|
55
|
+
result.width >= 0 &&
|
|
56
|
+
result.height >= 0
|
|
57
|
+
? result
|
|
58
|
+
: null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function sameViewSize(a: ViewSize | null, b: ViewSize | null): boolean {
|
|
62
|
+
return (
|
|
63
|
+
a === b || (!!a && !!b && a.width === b.width && a.height === b.height)
|
|
64
|
+
);
|
|
65
|
+
}
|