react-native-layout-dnd 0.1.0-alpha.0 → 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 +130 -63
- 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
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# React Native Layout DnD
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/react-native-layout-dnd)
|
|
4
|
+
[](https://github.com/baesumin/react-native-layout-dnd/actions/workflows/ci.yml)
|
|
5
|
+
|
|
3
6
|
Drag and drop for React Native grids and lists, built on Reanimated worklets and
|
|
4
7
|
Gesture Handler.
|
|
5
8
|
|
|
@@ -8,8 +11,8 @@ list, placed by coordinates in a grid, or moved between the two, and every move
|
|
|
8
11
|
arrives at your state layer as one complete candidate that you can accept or
|
|
9
12
|
reject.
|
|
10
13
|
|
|
11
|
-
> **
|
|
12
|
-
>
|
|
14
|
+
> **Alpha.** Only one combination of peer versions has been built and run, and
|
|
15
|
+
> device verification is incomplete. Read
|
|
13
16
|
> [Status and limitations](#status-and-limitations) before adopting it.
|
|
14
17
|
|
|
15
18
|
## Why this library
|
|
@@ -48,13 +51,8 @@ React is a peer too, at 19 or newer, but your React Native version already
|
|
|
48
51
|
decides it. The New Architecture is required, because Reanimated 4 supports
|
|
49
52
|
nothing else.
|
|
50
53
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
Reanimated 4.0.0, and `scheduleOnUI`, `scheduleOnRN` and `runOnUISync` in
|
|
54
|
-
Worklets 0.5.0. Only the **Verified** column has actually been built and run
|
|
55
|
-
end to end, so treat the rest of the range as accepted rather than tested, and
|
|
56
|
-
pin what works for you. Reanimated and Worklets constrain your React Native
|
|
57
|
-
version further through their own peer ranges.
|
|
54
|
+
Only the **Verified** column has been built and run end to end. The rest of
|
|
55
|
+
each range is accepted but untested, so pin what works for you.
|
|
58
56
|
|
|
59
57
|
Your app must also render the provider under `GestureHandlerRootView` and enable
|
|
60
58
|
the Worklets Babel plugin. This package ships worklet directives uncompiled so
|
|
@@ -70,23 +68,18 @@ module.exports = {
|
|
|
70
68
|
|
|
71
69
|
## Installation
|
|
72
70
|
|
|
73
|
-
Not published yet. Build a tarball from a clone and install it by path:
|
|
74
|
-
|
|
75
71
|
```sh
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
yarn install
|
|
79
|
-
npm pack
|
|
72
|
+
yarn add react-native-layout-dnd
|
|
73
|
+
# or: npm install react-native-layout-dnd
|
|
80
74
|
```
|
|
81
75
|
|
|
76
|
+
Install the peers too if your app does not have them yet, then rebuild the
|
|
77
|
+
native app:
|
|
78
|
+
|
|
82
79
|
```sh
|
|
83
|
-
|
|
84
|
-
yarn add /absolute/path/to/react-native-layout-dnd-0.1.0-alpha.0.tgz
|
|
80
|
+
yarn add react-native-gesture-handler react-native-reanimated react-native-worklets
|
|
85
81
|
```
|
|
86
82
|
|
|
87
|
-
Installing straight from the Git URL does not work, because the entry points
|
|
88
|
-
come from a build that only runs while packing.
|
|
89
|
-
|
|
90
83
|
## Quick start
|
|
91
84
|
|
|
92
85
|
A sortable vertical list. `useDndState` holds the layout and acknowledges each
|
|
@@ -147,34 +140,80 @@ footprint, so `getGridItemLayout` decides the span an incoming item takes. Keep
|
|
|
147
140
|
that function stable, because it is captured once per drag.
|
|
148
141
|
|
|
149
142
|
```tsx
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
143
|
+
import { Text, View } from 'react-native';
|
|
144
|
+
import {
|
|
145
|
+
DndProvider,
|
|
146
|
+
DragHandle,
|
|
147
|
+
SortableGrid,
|
|
148
|
+
SortableList,
|
|
149
|
+
useDndState,
|
|
150
|
+
type CellSpan,
|
|
151
|
+
type GridItemLayoutResolverArgs,
|
|
152
|
+
type LayoutState,
|
|
153
|
+
} from 'react-native-layout-dnd';
|
|
154
|
+
|
|
155
|
+
type Card = { title: string; span: CellSpan };
|
|
156
|
+
|
|
157
|
+
const initial: LayoutState<Card> = {
|
|
158
|
+
revision: 0,
|
|
159
|
+
items: [
|
|
160
|
+
{ id: 'notes', data: { title: 'Notes', span: { rows: 1, cols: 2 } } },
|
|
161
|
+
],
|
|
162
|
+
zones: [
|
|
163
|
+
{
|
|
164
|
+
id: 'library',
|
|
165
|
+
kind: 'list',
|
|
166
|
+
orientation: 'vertical',
|
|
167
|
+
itemIds: ['notes'],
|
|
168
|
+
},
|
|
169
|
+
{ id: 'board', kind: 'grid', rows: 3, columns: 4, placements: [] },
|
|
170
|
+
],
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
function getGridItemLayout({ item }: GridItemLayoutResolverArgs<Card>) {
|
|
174
|
+
return { span: item.data.span, placement: 'insert' as const };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function Board() {
|
|
178
|
+
const { value, onChange } = useDndState(initial);
|
|
179
|
+
return (
|
|
180
|
+
<DndProvider
|
|
181
|
+
value={value}
|
|
182
|
+
onChange={onChange}
|
|
183
|
+
getGridItemLayout={getGridItemLayout}
|
|
184
|
+
style={{ flex: 1 }}
|
|
185
|
+
>
|
|
186
|
+
<SortableList<Card>
|
|
187
|
+
zoneId="library"
|
|
188
|
+
virtualization
|
|
189
|
+
style={{ flex: 1 }}
|
|
190
|
+
estimatedItemSize={48}
|
|
191
|
+
renderItem={({ item }) => (
|
|
192
|
+
<DragHandle>
|
|
193
|
+
<Text>{item.data.title}</Text>
|
|
194
|
+
</DragHandle>
|
|
195
|
+
)}
|
|
196
|
+
/>
|
|
197
|
+
<SortableGrid<Card>
|
|
198
|
+
zoneId="board"
|
|
199
|
+
style={{ height: 240 }}
|
|
200
|
+
geometry={{
|
|
201
|
+
mode: 'fit',
|
|
202
|
+
rowGap: 8,
|
|
203
|
+
columnGap: 8,
|
|
204
|
+
padding: { top: 8, right: 8, bottom: 8, left: 8 },
|
|
205
|
+
}}
|
|
206
|
+
renderItem={({ item, width, height }) => (
|
|
207
|
+
<View style={{ width, height }}>
|
|
208
|
+
<DragHandle>
|
|
209
|
+
<Text>{item.data.title}</Text>
|
|
210
|
+
</DragHandle>
|
|
211
|
+
</View>
|
|
212
|
+
)}
|
|
213
|
+
/>
|
|
214
|
+
</DndProvider>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
178
217
|
```
|
|
179
218
|
|
|
180
219
|
A grid zone with `rows`, `columns` and `placements` is spatial. Add `itemSpan`
|
|
@@ -212,6 +251,43 @@ const result = computeLayoutMove({
|
|
|
212
251
|
if (result.status === 'ok' && result.changed) setValue(result.value);
|
|
213
252
|
```
|
|
214
253
|
|
|
254
|
+
## Validation issues
|
|
255
|
+
|
|
256
|
+
`DndProvider` and `GridController` report issues through two callbacks. In
|
|
257
|
+
`DndProvider`, any issue also disables dragging until it is resolved.
|
|
258
|
+
|
|
259
|
+
- `onValidationError(issues)` is called when new issues appear. It is never
|
|
260
|
+
called when they are resolved.
|
|
261
|
+
- `onValidationChange(issues)` is called with the complete current list
|
|
262
|
+
whenever it changes, and with `[]` once everything is resolved. It runs after
|
|
263
|
+
the commit that changed the list has settled, so it does not see the
|
|
264
|
+
in-between states of one update.
|
|
265
|
+
|
|
266
|
+
Use `onValidationChange` to show an error state that also clears itself:
|
|
267
|
+
|
|
268
|
+
```tsx
|
|
269
|
+
import { useState } from 'react';
|
|
270
|
+
import type { LayoutValidationIssue } from 'react-native-layout-dnd';
|
|
271
|
+
|
|
272
|
+
function useValidationState() {
|
|
273
|
+
const [issues, setIssues] = useState<LayoutValidationIssue[]>([]);
|
|
274
|
+
return { issues, onValidationChange: setIssues };
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
What an issue means depends on where it comes from. Some codes come from more
|
|
279
|
+
than one source, so read `zoneId`, `itemId` and `message` together with the code.
|
|
280
|
+
|
|
281
|
+
| Source | Codes | Resolved when |
|
|
282
|
+
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
|
|
283
|
+
| The value (data) | `invalid-structure`, `duplicate-id` (item IDs), `invalid-dimension`, `invalid-span`, `invalid-placement`, `missing-item-span`, `span-mismatch`, `invalid-position`, `out-of-bounds`, `overlap`, `capacity-exceeded`, and for `LayoutState` also `invalid-revision`, `unknown-item`, `unplaced-item`, `duplicate-reference`, `invalid-orientation` | The app passes a corrected value |
|
|
284
|
+
| Props and component tree | `invalid-budget`, `invalid-configuration` (provider, list or handle options, a handle outside its item, a zone component whose zone is not in the value), `invalid-geometry` for a malformed geometry object, `duplicate-id` for two components mounted for one zone | The props or the mounted components change |
|
|
285
|
+
| Layout | `invalid-geometry` when a fixed geometry does not fit the laid-out zone | The zone or the geometry changes so that cells fit |
|
|
286
|
+
|
|
287
|
+
A layout issue follows the measured size of the zone. The grid checks a fixed
|
|
288
|
+
geometry against the size of the same commit, so a geometry and a zone size that
|
|
289
|
+
change together, as on rotation, are not reported.
|
|
290
|
+
|
|
215
291
|
## Documentation
|
|
216
292
|
|
|
217
293
|
- [Lists and scrolling](docs/list-scroll-api.md) covers `DndProvider`,
|
|
@@ -220,30 +296,21 @@ if (result.status === 'ok' && result.changed) setValue(result.value);
|
|
|
220
296
|
- [Grids and mixed layouts](docs/mixed-layout-api.md) covers `SortableGrid`,
|
|
221
297
|
spans, transfers between zones, approval and failure reasons.
|
|
222
298
|
- [Grid contracts and the `GridController` API](docs/api.md) covers the shared
|
|
223
|
-
layout contract, movement policies and preview rules, plus
|
|
224
|
-
|
|
299
|
+
layout contract, movement policies and preview rules, plus `GridController`,
|
|
300
|
+
a grid-only interface over the same provider.
|
|
225
301
|
- [Contributing](CONTRIBUTING.md) lists the checks and the native test
|
|
226
302
|
checklist.
|
|
227
303
|
|
|
228
304
|
Runnable examples live in [`example/`](example/), including a 580-item
|
|
229
|
-
`FlatList` tab and a mixed grid and list screen
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
yarn install
|
|
233
|
-
yarn example start
|
|
234
|
-
yarn example ios # or: yarn example android
|
|
235
|
-
```
|
|
305
|
+
`FlatList` tab and a mixed grid and list screen. To run them, clone the
|
|
306
|
+
repository and follow
|
|
307
|
+
[Run the native example](CONTRIBUTING.md#run-the-native-example).
|
|
236
308
|
|
|
237
309
|
## Status and limitations
|
|
238
310
|
|
|
239
|
-
Automated verification passes 51 suites and 1,215 tests, seven
|
|
240
|
-
performance-tooling tests, TypeScript, ESLint, the build, and an isolated check
|
|
241
|
-
of the packed API and its types.
|
|
242
|
-
|
|
243
311
|
Known gaps, stated so you can judge the risk:
|
|
244
312
|
|
|
245
|
-
- **
|
|
246
|
-
met, and the public API may still change.
|
|
313
|
+
- **Alpha.** The public API may still change before `0.1.0`.
|
|
247
314
|
- **One verified combination.** The peer ranges above come from the APIs this
|
|
248
315
|
package calls, not from a test matrix. Only the versions in the Verified
|
|
249
316
|
column have been built and run, and Expo has not been tried.
|
|
@@ -66,6 +66,7 @@ export function DndProvider({
|
|
|
66
66
|
onDragEnd,
|
|
67
67
|
onDragSettled,
|
|
68
68
|
onValidationError,
|
|
69
|
+
onValidationChange,
|
|
69
70
|
onDiagnostic,
|
|
70
71
|
responseTimeoutMs,
|
|
71
72
|
getGridItemLayout,
|
|
@@ -137,6 +138,39 @@ export function DndProvider({
|
|
|
137
138
|
if (!deferred) reportSettled(event);
|
|
138
139
|
}
|
|
139
140
|
}, [reportSettled]);
|
|
141
|
+
// Issues of the value and options (from the coordinator) and of mounted
|
|
142
|
+
// adapters and handles (by owner), published together as one list. A new
|
|
143
|
+
// value reaches the adapters over several synchronous commits, so the list
|
|
144
|
+
// is read once they are done; intermediate lists are never published.
|
|
145
|
+
const issues = useRef(new Map());
|
|
146
|
+
const sessionIssues = useRef([]);
|
|
147
|
+
const validation = useRef({
|
|
148
|
+
published: '[]',
|
|
149
|
+
queued: false,
|
|
150
|
+
onChange: onValidationChange
|
|
151
|
+
}).current;
|
|
152
|
+
const mounted = useRef(true);
|
|
153
|
+
const publishValidation = useCallback(() => {
|
|
154
|
+
if (!mounted.current || validation.queued) return;
|
|
155
|
+
validation.queued = true;
|
|
156
|
+
globalThis.queueMicrotask(() => {
|
|
157
|
+
validation.queued = false;
|
|
158
|
+
if (!mounted.current) return;
|
|
159
|
+
const current = [...sessionIssues.current, ...Array.from(issues.current.keys()).sort().flatMap(owner => issues.current.get(owner).issues)];
|
|
160
|
+
const key = JSON.stringify(current);
|
|
161
|
+
if (key === validation.published) return;
|
|
162
|
+
validation.published = key;
|
|
163
|
+
try {
|
|
164
|
+
validation.onChange?.(current);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error('[LayoutDnD] Validation callback failed', error);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}, [validation]);
|
|
170
|
+
const reportSessionIssues = useCallback(next => {
|
|
171
|
+
sessionIssues.current = next;
|
|
172
|
+
publishValidation();
|
|
173
|
+
}, [publishValidation]);
|
|
140
174
|
const [coordinator] = useState(() => new DndSessionCoordinator({
|
|
141
175
|
value,
|
|
142
176
|
onChange,
|
|
@@ -144,6 +178,7 @@ export function DndProvider({
|
|
|
144
178
|
onDragStart,
|
|
145
179
|
onDragEnd: endSession,
|
|
146
180
|
onValidationError,
|
|
181
|
+
onValidationChange: reportSessionIssues,
|
|
147
182
|
onDiagnostic,
|
|
148
183
|
responseTimeoutMs,
|
|
149
184
|
getGridItemLayout,
|
|
@@ -195,9 +230,7 @@ export function DndProvider({
|
|
|
195
230
|
const nextHandleId = useRef(0);
|
|
196
231
|
const [gestureRegistry] = useState(createDndGestureRegistry);
|
|
197
232
|
const registry = useRef(new Map());
|
|
198
|
-
const issues = useRef(new Map());
|
|
199
233
|
const observer = useRef(onValidationError);
|
|
200
|
-
const mounted = useRef(true);
|
|
201
234
|
const handlePublication = useRef({
|
|
202
235
|
queued: false,
|
|
203
236
|
generation: 0,
|
|
@@ -354,10 +387,14 @@ export function DndProvider({
|
|
|
354
387
|
}, [handlePublication, publishHandles]);
|
|
355
388
|
const reportIssues = useCallback((owner, next) => {
|
|
356
389
|
const key = JSON.stringify(next);
|
|
357
|
-
const old = issues.current.get(owner);
|
|
358
|
-
if (next.length) issues.current.set(owner,
|
|
390
|
+
const old = issues.current.get(owner)?.key;
|
|
391
|
+
if (next.length) issues.current.set(owner, {
|
|
392
|
+
key,
|
|
393
|
+
issues: next
|
|
394
|
+
});else issues.current.delete(owner);
|
|
359
395
|
if (!mounted.current) return;
|
|
360
396
|
setRegistryInvalid(issues.current.size > 0);
|
|
397
|
+
publishValidation();
|
|
361
398
|
if (next.length && old !== key) {
|
|
362
399
|
coordinator.cancel('disabled');
|
|
363
400
|
try {
|
|
@@ -366,7 +403,7 @@ export function DndProvider({
|
|
|
366
403
|
console.error('[LayoutDnD] Validation callback failed', error);
|
|
367
404
|
}
|
|
368
405
|
}
|
|
369
|
-
}, [coordinator]);
|
|
406
|
+
}, [coordinator, publishValidation]);
|
|
370
407
|
useLayoutEffect(() => {
|
|
371
408
|
mounted.current = true;
|
|
372
409
|
if (currentCanDrop.current !== canDrop || committedValue.current !== value) policyRevision.current++;
|
|
@@ -381,6 +418,7 @@ export function DndProvider({
|
|
|
381
418
|
});
|
|
382
419
|
});
|
|
383
420
|
observer.current = onValidationError;
|
|
421
|
+
validation.onChange = onValidationChange;
|
|
384
422
|
if (currentConfig.current.dragAxis !== dragAxis || currentConfig.current.delayMs !== activation?.delayMs) coordinator.cancel('input-changed');
|
|
385
423
|
currentConfig.current = {
|
|
386
424
|
dragAxis,
|
|
@@ -398,6 +436,7 @@ export function DndProvider({
|
|
|
398
436
|
onDragStart,
|
|
399
437
|
onDragEnd: endSession,
|
|
400
438
|
onValidationError,
|
|
439
|
+
onValidationChange: reportSessionIssues,
|
|
401
440
|
onDiagnostic,
|
|
402
441
|
responseTimeoutMs,
|
|
403
442
|
getGridItemLayout,
|