Skip to content

Commit 80933c1

Browse files
author
Roy Lin
committed
feat: align collection move and reorder semantics
1 parent 9ccdd79 commit 80933c1

22 files changed

Lines changed: 984 additions & 179 deletions

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,10 @@ versioned [component matrix](docs/react-aria-component-matrix.json) pins
7272
wildcard, multi-item, and per-format payload negotiation plus layout-driven
7373
collection `root` and keyed `before`/`on`/`after` targets. Selected
7474
collection items drag as one session, and ListBox, GridList, Tree, and Table
75-
share the same RSX contract; external transfer, reorder/move policy, pixels,
75+
share the same RSX contract. External `onInsert`/`onRootDrop`, internal
76+
`onMove`, same-parent `onReorder`, multi-callback routing, equivalent adjacent
77+
boundaries, and self/descendant guards now follow the React Aria collection
78+
policy; dynamic acceptance, OS/cross-application transfer, previews, pixels,
7679
accessibility, and real hosts still keep every affected family below
7780
conformance;
7881
- Checkbox, Radio, and Switch Field/Button parts plus ToastList and
@@ -308,10 +311,13 @@ independently.
308311
and every MIME/custom representation, filter compatible target items,
309312
negotiate wildcards plus copy/move/link/cancel operations, aggregate stable
310313
selected collection keys, and resolve root/item/insertion targets from
311-
self-drawn layout geometry. A collection is one keyboard Tab stop during a
312-
drag, with arrow/Home/End navigation inside it; all paths share event-loop
313-
deadlines, stable action bubbling, keyed reconciliation, and reducer
314-
rollback in one staged interaction session
314+
self-drawn layout geometry. Collection policy distinguishes external insert
315+
and root drops from internal move/reorder, rejects self and descendant
316+
targets, and preserves one logical boundary between adjacent items. A
317+
collection is one keyboard Tab stop during a drag, with arrow/Home/End
318+
navigation inside it; all paths share event-loop deadlines, stable action
319+
bubbling, keyed reconciliation, and reducer rollback in one staged
320+
interaction session
315321
- an identical-frame fast path that performs no layout, scene, host, or
316322
presentation work, plus semantic-only commits that skip pixel presentation
317323
- a software Graphics presenter and interactive shared

docs/platform-hosts.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -392,11 +392,14 @@ Landed evidence:
392392
wildcard types, and negotiates copy/move/link/cancel operations; pointer
393393
drags report target-local coordinates, keyboard Enter/Tab/Escape provides the
394394
same accessible source/compatible-target lifecycle, and keyed frames plus
395-
reducer errors preserve or roll back the entire session atomically
395+
reducer errors preserve or roll back the entire session atomically. Collection
396+
targets additionally distinguish external insertion/root drops from internal
397+
move and same-parent reorder, reject self/descendant drops, and coalesce
398+
adjacent insertion boundaries
396399
- reducer errors restore the staged interaction state and sequence before the
397400
event is exposed as successful; successful frame reconciliation preserves
398401
focused stable ids, while rejected frames do not touch them
399-
- 53 focused runtime/software tests and four recursive feature/source
402+
- 59 focused runtime/software tests and four recursive feature/source
400403
firewall tests pass without any legacy renderer or OS toolkit dependency
401404
- `self_drawn_calculator` reproduces layout fingerprint
402405
`16529597026056060935`, scene fingerprint `2100550662756266801`, and

docs/react-aria-native.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -155,16 +155,22 @@ The first shared interaction milestone is available in the portable runtime:
155155
filters every target item by accepted type, and exposes `draggingKeys`,
156156
`target`, and `isInternal` in the shared context. During keyboard drag, each
157157
collection is one Tab stop and Arrow/Home/End keys navigate its internal
158-
targets.
158+
targets. Opposite `after A`/`before B` descriptors for adjacent items remain
159+
one logical target without spurious exit/enter transitions.
159160
- `use_drag`, `use_drop`, `UiDraggable`, `UiDroppable`, and `UiDropZone` lower
160161
their source/target metadata and focusable keyboard affordances into that
161162
shared runtime. ListBox, GridList, Tree, and Table roots additionally lower
162-
`onRootDrop`, `onItemDrop`, `onInsert`, and low-level `onDrop` precedence;
163-
their items/rows and explicit DropIndicator parts use the same self-drawn
164-
target model. Reorder/move policy, dynamic acceptance, external files and
165-
directories, cross-application transfer, drag previews, and conformance
166-
evidence remain separate M7 work, so no affected family is marked
167-
conformant.
163+
`onRootDrop`, `onItemDrop`, `onInsert`, `onReorder`, `onMove`, and low-level
164+
`onDrop` precedence; their items/rows and explicit DropIndicator parts use
165+
the same self-drawn target model. Insert and root callbacks accept external
166+
sessions, move accepts internal item/on-or-between targets, and reorder is
167+
limited to internal same-parent boundaries. Valid combined callbacks retain
168+
React Aria order, while low-level `onDrop` overrides high-level dispatch.
169+
Selected descendants are removed when their selected ancestor is dragged,
170+
and internal self/descendant targets are rejected. Dynamic item acceptance
171+
and operation callbacks, external files/directories and cross-application
172+
transfer, drag previews, and conformance evidence remain separate M7 work,
173+
so no affected family is marked conformant.
168174

169175
- `NativeInputModality` represents keyboard, mouse, touch, pen, virtual, and
170176
unknown input.
@@ -598,7 +604,7 @@ props:
598604
| Priority | Area | Required outcome |
599605
| --- | --- | --- |
600606
| P0 | Self-drawn component accounting | Keep all 51 React Aria 1.19.0 families in the executable matrix, implement the eight recorded public-part gaps, and require every upstream catalog delta to update code, matrix, tests, and milestones together. |
601-
| P0 | Shared self-drawn interaction | Extend the landed stable-id pointer, keyboard, Tab-focus, hover, press, scheduled generic long press, incremental captured move, typed source/target drag negotiation, collection item/between/root delegates, cancellation, wheel, bubbling, and reducer path with reorder/move policy, dynamic item acceptance, external file/directory transfer, drag previews, collection long-press selection mode, focus-scope restoration, overlay gestures, text editing/IME, and accessibility activation. |
607+
| P0 | Shared self-drawn interaction | Extend the landed stable-id pointer, keyboard, Tab-focus, hover, press, scheduled generic long press, incremental captured move, typed source/target drag negotiation, collection item/between/root delegates plus reorder/move policy, cancellation, wheel, bubbling, and reducer path with dynamic item/operation acceptance, external file/directory transfer, drag previews, collection long-press selection mode, focus-scope restoration, overlay gestures, text editing/IME, and accessibility activation. |
602608
| P0 | Native input conformance | WinUI's complete 98-case V1 manifest passes real OS automation. Populate the AppKit and GTK4 manifests with platform-run mouse, pen, touch where applicable, keyboard, assistive activation, disabled, cancellation, and keyed-rerender fixtures for every role currently marked native; then close or retain evidence-backed menu/item exceptions. |
603609
| P1 | Event propagation | Add platform-run conformance fixtures for conditional `Stop`/`Continue` across nested native controls. |
604610
| P1 | Focus management | Add platform-run conformance fixtures for post-mount `autoFocus`, nested containment, and restoration. |

docs/roadmap.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -513,14 +513,17 @@ Landed evidence:
513513
- collection drag sources aggregate the stable keys, payloads, and visual
514514
dragging state of selected draggable items. The layout-backed delegate emits
515515
React Aria-shaped root and keyed before/on/after targets, routes
516-
`onRootDrop`/`onItemDrop`/`onInsert` with low-level `onDrop` precedence, and
517-
treats a collection as one Tab target with arrow/Home/End navigation inside
518-
it. ListBox, GridList, Tree, Table, and explicit DropIndicator authoring all
519-
lower to this shared self-drawn path
516+
external `onRootDrop`/`onInsert`, item `onItemDrop`, internal `onMove`, and
517+
same-parent `onReorder` with ordered multi-callback dispatch and low-level
518+
`onDrop` precedence. It filters selected descendants, rejects internal
519+
self/descendant targets, treats adjacent insertion descriptors as one target,
520+
and exposes each collection as one Tab stop with arrow/Home/End navigation.
521+
ListBox, GridList, Tree, Table, and explicit DropIndicator authoring all lower
522+
to this shared self-drawn path
520523
- the shared 410x620 calculator preserves its reviewed layout and scene
521524
fingerprints, routes eight fake-host events through four reducer actions,
522525
commits the resulting frames, and reaches display value `10`
523-
- 53 focused runtime/software tests plus four recursive H1 firewall tests are
526+
- 59 focused runtime/software tests plus four recursive H1 firewall tests are
524527
included in `just verify`
525528

526529
Remaining H1 work:
@@ -529,10 +532,10 @@ Remaining H1 work:
529532
pinned Graphics commit `8748fab` owns only a surface-independent texture and
530533
readback today, so its safe host-owned surface attachment/recovery contract
531534
must land before GUI can implement this edge without duplicating `wgpu`
532-
- collection reorder/move policy and acceptance callbacks, native
533-
file/directory and cross-application transfer, drag previews, text editing,
534-
IME, overlay gestures, and component-specific pixel/accessibility/real-host
535-
conformance remain explicit M4 and M6-M8 work
535+
- dynamic collection item/operation acceptance callbacks, native file/directory
536+
and cross-application transfer, drag previews, text editing, IME, overlay
537+
gestures, and component-specific pixel/accessibility/real-host conformance
538+
remain explicit M4 and M6-M8 work
536539

537540
Gates:
538541

@@ -742,9 +745,9 @@ self-drawn conformance.
742745
- close the executable 1.19.0 behavior deltas: embedded-control keyboard
743746
navigation for GridList/Tree, Menu action key plus value, arbitrary Popover
744747
target rectangles, and multi-MIME/wildcard drag type negotiation
745-
- build on the landed shared collection root/item/insertion delegate with
746-
reorder, cross-collection move, dynamic item acceptance, drag previews, and
747-
software/accessibility/three-host conformance stories
748+
- build on the landed shared collection root/item/insertion delegate and
749+
reorder/move policies with dynamic item/operation acceptance, OS transfer,
750+
drag previews, and software/accessibility/three-host conformance stories
748751

749752
### M8 - Date, color, tables, and advanced data
750753

docs/rsx.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -601,10 +601,12 @@ the same typed transfer context.
601601

602602
`UiListBox`, `UiGridList`, `UiTree`, and `UiTable` also expose the shared
603603
collection contract. Their roots accept `onRootDrop`, `onItemDrop`, `onInsert`,
604-
the low-level overriding `onDrop`, lifecycle handlers, `acceptedDragTypes`,
605-
`dropOperation`, `allowedDropOperations`, and `dropOrientation`. Item/row
606-
components accept a stable `id` plus `isDraggable`, `dragType`, `dragValue`,
607-
and `dragItems`; `UiDropIndicator.targetKey` references that `id`.
604+
`onReorder`, `onMove`, the low-level overriding `onDrop`, lifecycle handlers,
605+
`acceptedDragTypes`, `dropOperation`, `allowedDropOperations`, and
606+
`dropOrientation`. Collection `onMove` lowers to a dedicated native event so
607+
it does not collide with the generic move-gesture callback. Item/row components
608+
accept a stable `id` plus `isDraggable`, `dragType`, `dragValue`, and
609+
`dragItems`; `UiDropIndicator.targetKey` references that `id`.
608610
Dragging a selected item aggregates all selected draggable item keys and
609611
payloads; dragging an unselected item keeps a single-item session.
610612

@@ -614,9 +616,15 @@ descriptor shaped as `{type: "root"}` or
614616
delegation derives targets from the committed self-drawn boxes. During a
615617
keyboard drag, Tab visits the collection once and Arrow/Home/End keys navigate
616618
inside it. `UiDropIndicator targetKey="..." dropPosition="before|after"`
617-
provides a stable explicit insertion visual. External OS files/directories,
618-
cross-application transfer, drag previews, reorder/move policy, and final
619-
accessibility/host conformance remain separate work.
619+
provides a stable explicit insertion visual. `onInsert` and `onRootDrop` accept
620+
external collection sessions; internal `onMove` accepts item/on-or-between
621+
targets, while `onReorder` accepts same-parent boundaries. `onItemDrop` plus
622+
`onMove`, or `onMove` plus `onReorder`, may both run in React Aria order;
623+
low-level `onDrop` replaces this routing. Self and descendant targets are
624+
rejected, and adjacent `after`/`before` boundaries are equivalent. Dynamic
625+
acceptance/operation callbacks, external OS files/directories,
626+
cross-application transfer, drag previews, and final accessibility/host
627+
conformance remain separate work.
620628

621629
Runtime lifecycle and effects:
622630

src/platform_runtime/drag_drop.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ impl SelfDrawnDropTarget {
208208
"onRootDrop",
209209
"onItemDrop",
210210
"onInsert",
211+
"onReorder",
212+
"onCollectionMove",
211213
"onDropEnter",
212214
"onDropMove",
213215
"onDropExit",

src/platform_runtime/drag_drop_collection.rs

Lines changed: 82 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ impl SelfDrawnCollectionDropTarget {
4747
pub(super) struct SelfDrawnCollectionDropConfig {
4848
pub(super) target: SelfDrawnDropTarget,
4949
pub(super) orientation: Orientation,
50-
low_level_drop: bool,
51-
allows_root: bool,
52-
allows_item: bool,
53-
allows_insert: bool,
50+
low_level_drop: Option<String>,
51+
root_drop: Option<String>,
52+
item_drop: Option<String>,
53+
insert: Option<String>,
54+
reorder: Option<String>,
55+
move_within: Option<String>,
5456
}
5557

5658
impl SelfDrawnCollectionDropConfig {
@@ -62,47 +64,93 @@ impl SelfDrawnCollectionDropConfig {
6264
return None;
6365
}
6466
let target = target?;
65-
let low_level_drop = has_event(props, "onDrop");
66-
let allows_root = low_level_drop || has_event(props, "onRootDrop");
67-
let allows_item = low_level_drop || has_event(props, "onItemDrop");
68-
let allows_insert = low_level_drop || has_event(props, "onInsert");
69-
if !allows_root && !allows_item && !allows_insert {
67+
let low_level_drop = event_action(props, "onDrop");
68+
let root_drop = event_action(props, "onRootDrop");
69+
let item_drop = event_action(props, "onItemDrop");
70+
let insert = event_action(props, "onInsert");
71+
let reorder = event_action(props, "onReorder");
72+
let move_within = event_action(props, "onCollectionMove");
73+
if low_level_drop.is_none()
74+
&& root_drop.is_none()
75+
&& item_drop.is_none()
76+
&& insert.is_none()
77+
&& reorder.is_none()
78+
&& move_within.is_none()
79+
{
7080
return None;
7181
}
7282
Some(Self {
7383
target,
7484
orientation: drop_orientation(props),
7585
low_level_drop,
76-
allows_root,
77-
allows_item,
78-
allows_insert,
86+
root_drop,
87+
item_drop,
88+
insert,
89+
reorder,
90+
move_within,
7991
})
8092
}
8193

82-
pub(super) fn allows_root(&self) -> bool {
83-
self.allows_root
94+
pub(super) fn has_low_level_drop(&self) -> bool {
95+
self.low_level_drop.is_some()
8496
}
8597

86-
pub(super) fn allows_item(&self) -> bool {
87-
self.allows_item
98+
pub(super) fn has_root_drop(&self) -> bool {
99+
self.root_drop.is_some()
88100
}
89101

90-
pub(super) fn allows_insert(&self) -> bool {
91-
self.allows_insert
102+
pub(super) fn has_item_drop(&self) -> bool {
103+
self.item_drop.is_some()
92104
}
93105

94-
pub(super) fn drop_event_name(&self, target: &SelfDrawnCollectionDropTarget) -> &'static str {
95-
if self.low_level_drop {
96-
return "onDrop";
97-
}
98-
match target {
99-
SelfDrawnCollectionDropTarget::Root => "onRootDrop",
100-
SelfDrawnCollectionDropTarget::Item {
101-
drop_position: SelfDrawnDropPosition::On,
102-
..
103-
} => "onItemDrop",
104-
SelfDrawnCollectionDropTarget::Item { .. } => "onInsert",
105-
}
106+
pub(super) fn has_insert(&self) -> bool {
107+
self.insert.is_some()
108+
}
109+
110+
pub(super) fn has_reorder(&self) -> bool {
111+
self.reorder.is_some()
112+
}
113+
114+
pub(super) fn has_move(&self) -> bool {
115+
self.move_within.is_some()
116+
}
117+
118+
pub(super) fn allows_root(&self, is_internal: bool) -> bool {
119+
self.has_low_level_drop() || (!is_internal && self.has_root_drop())
120+
}
121+
122+
pub(super) fn allows_item(&self, is_internal: bool) -> bool {
123+
self.has_low_level_drop() || self.has_item_drop() || (is_internal && self.has_move())
124+
}
125+
126+
pub(super) fn allows_insert(&self, is_internal: bool) -> bool {
127+
self.has_low_level_drop()
128+
|| (!is_internal && self.has_insert())
129+
|| (is_internal && (self.has_reorder() || self.has_move()))
130+
}
131+
132+
pub(super) fn low_level_action(&self) -> Option<&str> {
133+
self.low_level_drop.as_deref()
134+
}
135+
136+
pub(super) fn root_action(&self) -> Option<&str> {
137+
self.root_drop.as_deref()
138+
}
139+
140+
pub(super) fn item_action(&self) -> Option<&str> {
141+
self.item_drop.as_deref()
142+
}
143+
144+
pub(super) fn insert_action(&self) -> Option<&str> {
145+
self.insert.as_deref()
146+
}
147+
148+
pub(super) fn reorder_action(&self) -> Option<&str> {
149+
self.reorder.as_deref()
150+
}
151+
152+
pub(super) fn move_action(&self) -> Option<&str> {
153+
self.move_within.as_deref()
106154
}
107155
}
108156

@@ -163,12 +211,14 @@ fn drop_orientation(props: &NativeProps) -> Orientation {
163211
}
164212
}
165213

166-
fn has_event(props: &NativeProps, name: &str) -> bool {
214+
fn event_action(props: &NativeProps, name: &str) -> Option<String> {
167215
props
168216
.web
169217
.events
170218
.get(name)
171-
.is_some_and(|action| !action.trim().is_empty())
219+
.map(|action| action.trim())
220+
.filter(|action| !action.is_empty())
221+
.map(str::to_string)
172222
}
173223

174224
fn bool_attribute(props: &NativeProps, name: &str) -> bool {

0 commit comments

Comments
 (0)