Skip to content

Commit a2c0fbe

Browse files
author
Roy Lin
committed
feat: align timed drop activation semantics
1 parent 80933c1 commit a2c0fbe

36 files changed

Lines changed: 608 additions & 60 deletions

README.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,10 @@ versioned [component matrix](docs/react-aria-component-matrix.json) pins
7575
share the same RSX contract. External `onInsert`/`onRootDrop`, internal
7676
`onMove`, same-parent `onReorder`, multi-callback routing, equivalent adjacent
7777
boundaries, and self/descendant guards now follow the React Aria collection
78-
policy; dynamic acceptance, OS/cross-application transfer, previews, pixels,
79-
accessibility, and real hosts still keep every affected family below
80-
conformance;
78+
policy. Ordinary targets and collection items also share the exact 800ms
79+
`onDropActivate` hold lifecycle across pointer and keyboard drags; dynamic
80+
acceptance, OS/cross-application transfer, previews, pixels, accessibility,
81+
and real hosts still keep every affected family below conformance;
8182
- Checkbox, Radio, and Switch Field/Button parts plus ToastList and
8283
ToastContent are recorded as eight explicit API gaps;
8384
- a component reaches `conformant` only with behavior, layout/hit, Graphics
@@ -315,9 +316,11 @@ independently.
315316
and root drops from internal move/reorder, rejects self and descendant
316317
targets, and preserves one logical boundary between adjacent items. A
317318
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
319+
navigation inside it. Valid ordinary targets and collection items schedule
320+
one React Aria-compatible `DropActivate` after 800ms without delaying the
321+
deadline as the pointer moves inside the same target; all paths share
322+
event-loop deadlines, stable action bubbling, keyed reconciliation, and
323+
reducer rollback in one staged interaction session
321324
- an identical-frame fast path that performs no layout, scene, host, or
322325
presentation work, plus semantic-only commits that skip pixel presentation
323326
- a software Graphics presenter and interactive shared
@@ -333,9 +336,9 @@ independently.
333336
| M1 · GUI integration | Complete | Pinned Graphics boundary, semantic-only dependency gate, renderer inventory, reference/GPU wrappers, first generic adapter |
334337
| M2 · GPU backend | Implementation landed | Graphics commit `8748fab`; Metal and Vulkan CI parity evidence remains |
335338
| M3 · Layout and Scene | Current | Generic calculator rectangle slice landed; full flex, stacking, redraw scheduling, cross-platform fingerprints, and thin-host presentation remain |
336-
| M4 · Text and interaction cutover | In progress | Stable-id raw input, long press, move, typed and collection drag/drop foundations landed; shaping, glyphs, editing/IME, accessibility bridges, overlays, and complete calculator scenarios remain |
339+
| M4 · Text and interaction cutover | In progress | Stable-id raw input, long press, move, typed and collection drag/drop plus timed drop activation landed; shaping, glyphs, editing/IME, accessibility bridges, overlays, and complete calculator scenarios remain |
337340
| M5 · Default cutover | Planned | Make self-drawn content the default, then delete the three legacy widget renderers |
338-
| H0-H5 · Thin platform hosts | H0 complete; H1 in progress | Atomic frames, lifecycle recovery, stable-id raw input/reducers, long press, captured move, typed drag/drop negotiation, zero-toolkit firewalls, and an interactive calculator landed; the Graphics raw-surface edge remains |
341+
| H0-H5 · Thin platform hosts | H0 complete; H1 in progress | Atomic frames, lifecycle recovery, stable-id raw input/reducers, long press, captured move, typed drag/drop negotiation and timed target activation, zero-toolkit firewalls, and an interactive calculator landed; the Graphics raw-surface edge remains |
339342
| T0-T5 · TSX native authoring | Proposed | Automatic JSX runtime, versioned Node-to-host session, state/event runtime, self-drawn native window, packages, and stable SDK |
340343
| M6-M8 · React Aria components | Catalog pinned; conformance planned | 51/51 families mapped; collection DnD authoring/behavior slice landed; eight public parts, full software, accessibility, and three-OS self-drawn evidence remain |
341344

docs/architecture.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,8 @@ move lifecycle, drag/drop lifecycle, collection action, change, focus, toggle,
478478
selection change, key down, and key up back to serialized action identifiers
479479
such as `onClick`, `onLongPress`, `onMoveStart`, `onMove`, `onMoveEnd`,
480480
`onDragStart`, `onDragMove`, `onDragEnd`, `onDropEnter`, `onDropMove`,
481-
`onDropExit`, `onDrop`, `onChange`, `onInput`, `onFocusChange`, `onAction`,
481+
`onDropActivate`, `onDropExit`, `onDrop`, `onChange`, `onInput`,
482+
`onFocusChange`, `onAction`,
482483
`onExpandedChange`, `onKeyDown`, and `onKeyUp`.
483484
Self-drawn drag callbacks carry a typed item array. Each text item preserves its
484485
complete format map; source events receive every item, while target events

docs/platform-hosts.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,8 @@ src/platform_runtime/
215215
|- long_press_input.rs event-loop deadlines and terminal hold recognition
216216
|- move_input.rs captured incremental pointer and keyboard movement
217217
|- drag_drop.rs typed transfer data and source/target negotiation
218+
|- drag_drop_activation_input.rs
219+
| 800ms target hold deadline and activation routing
218220
|- drag_drop_input.rs captured pointer drag lifecycle and target routing
219221
|- drag_drop_keyboard_input.rs
220222
| Enter/Tab/Escape accessible drag lifecycle
@@ -225,6 +227,8 @@ src/platform_runtime/
225227
| keyboard and style-only drag/drop gates
226228
|- drag_drop_items_tests.rs
227229
| multi-item/per-format transfer and filtering gates
230+
|- drag_drop_activation_tests.rs
231+
| pointer/keyboard/item timing and rollback gates
228232
|- presenter.rs raw-surface prepare/publish contract and recorder
229233
|- reference_presenter.rs
230234
| transactional software Graphics evidence
@@ -395,11 +399,14 @@ Landed evidence:
395399
reducer errors preserve or roll back the entire session atomically. Collection
396400
targets additionally distinguish external insertion/root drops from internal
397401
move and same-parent reorder, reject self/descendant drops, and coalesce
398-
adjacent insertion boundaries
402+
adjacent insertion boundaries. Ordinary targets and collection items emit
403+
`DropActivate` after an exact 800ms hold through the same host deadline;
404+
collection roots do not activate, and target exit/change/cancel/drop clears
405+
or restarts the timer
399406
- reducer errors restore the staged interaction state and sequence before the
400407
event is exposed as successful; successful frame reconciliation preserves
401408
focused stable ids, while rejected frames do not touch them
402-
- 59 focused runtime/software tests and four recursive feature/source
409+
- 63 focused runtime/software tests and four recursive feature/source
403410
firewall tests pass without any legacy renderer or OS toolkit dependency
404411
- `self_drawn_calculator` reproduces layout fingerprint
405412
`16529597026056060935`, scene fingerprint `2100550662756266801`, and

docs/react-aria-native.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ The first shared interaction milestone is available in the portable runtime:
136136
event-loop timer without exporting a toolkit timer into portable state.
137137
Deadline recognition and release-time fallback share the same ordering and
138138
reducer rollback, while pointer leave/re-entry cancels and restarts the hold.
139+
- The same portable deadline channel now implements React Aria's 800ms
140+
`onDropActivate` hold. Moving inside one logical target refreshes event
141+
context without postponing activation; changing targets restarts the timer,
142+
while exit, drop, cancellation, and invalid keyed reconciliation clear it.
143+
Pointer and keyboard drags share the lifecycle, and collection activation is
144+
intentionally limited to item targets rather than `{type: root}`.
139145
- Self-drawn move routing starts on the first non-zero delta, keeps the
140146
initiating pointer captured outside its original hit region, preserves
141147
incremental position across keyed frames, and reference-counts concurrent
@@ -161,11 +167,12 @@ The first shared interaction milestone is available in the portable runtime:
161167
their source/target metadata and focusable keyboard affordances into that
162168
shared runtime. ListBox, GridList, Tree, and Table roots additionally lower
163169
`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.
170+
`onDrop` precedence plus `onDropActivate`; their items/rows and explicit
171+
DropIndicator parts use the same self-drawn target model. Insert and root
172+
callbacks accept external sessions, move accepts internal item/on-or-between
173+
targets, and reorder is limited to internal same-parent boundaries. Valid
174+
combined callbacks retain React Aria order, while low-level `onDrop`
175+
overrides high-level dispatch.
169176
Selected descendants are removed when their selected ancestor is dragged,
170177
and internal self/descendant targets are rejected. Dynamic item acceptance
171178
and operation callbacks, external files/directories and cross-application

docs/roadmap.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,12 @@ Landed evidence:
506506
custom exact types, and `all`; `DropEnter`/`DropMove`/`DropExit` ordering,
507507
`data-[dragging]`/`data-[drop-target]` state, keyed-frame reconciliation, and
508508
reducer failure rollback share the same transactional interaction session
509+
- valid ordinary targets and collection item targets schedule React Aria's
510+
800ms `DropActivate` lifecycle through the same monotonic host deadline;
511+
movement inside an equivalent target updates context without postponing the
512+
timer, while target changes, exit, drop, cancellation, and invalid
513+
reconciliation reset or clear it. Keyboard and pointer paths are identical,
514+
and collection root targets intentionally do not activate
509515
- drag sources retain multiple text items and every per-item MIME/custom
510516
representation; target callbacks receive only matching items without losing
511517
the other representations on those items, while legacy `dragType` plus
@@ -523,7 +529,7 @@ Landed evidence:
523529
- the shared 410x620 calculator preserves its reviewed layout and scene
524530
fingerprints, routes eight fake-host events through four reducer actions,
525531
commits the resulting frames, and reaches display value `10`
526-
- 59 focused runtime/software tests plus four recursive H1 firewall tests are
532+
- 63 focused runtime/software tests plus four recursive H1 firewall tests are
527533
included in `just verify`
528534

529535
Remaining H1 work:

docs/rsx.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -597,12 +597,16 @@ Target callbacks receive only matching items, with every representation on
597597
those items preserved. `UiDraggable`, `UiDroppable`, and `UiDropZone` include a
598598
keyboard-focusable affordance: Enter starts or commits a drag, Tab cycles
599599
compatible targets, and Escape cancels. Pointer and keyboard callbacks receive
600-
the same typed transfer context.
600+
the same typed transfer context. `UiDroppable` and `UiDropZone` accept
601+
`onDropActivate`; a valid target held for 800ms emits it once, movement inside
602+
that target keeps the original deadline, and exit, drop, cancellation, or a
603+
target change clears or restarts it.
601604

602605
`UiListBox`, `UiGridList`, `UiTree`, and `UiTable` also expose the shared
603606
collection contract. Their roots accept `onRootDrop`, `onItemDrop`, `onInsert`,
604-
`onReorder`, `onMove`, the low-level overriding `onDrop`, lifecycle handlers,
605-
`acceptedDragTypes`, `dropOperation`, `allowedDropOperations`, and
607+
`onReorder`, `onMove`, the low-level overriding `onDrop`, lifecycle handlers
608+
including `onDropActivate`, `acceptedDragTypes`, `dropOperation`,
609+
`allowedDropOperations`, and
606610
`dropOrientation`. Collection `onMove` lowers to a dedicated native event so
607611
it does not collide with the generic move-gesture callback. Item/row components
608612
accept a stable `id` plus `isDraggable`, `dragType`, `dragValue`, and
@@ -621,7 +625,9 @@ external collection sessions; internal `onMove` accepts item/on-or-between
621625
targets, while `onReorder` accepts same-parent boundaries. `onItemDrop` plus
622626
`onMove`, or `onMove` plus `onReorder`, may both run in React Aria order;
623627
low-level `onDrop` replaces this routing. Self and descendant targets are
624-
rejected, and adjacent `after`/`before` boundaries are equivalent. Dynamic
628+
rejected, and adjacent `after`/`before` boundaries are equivalent.
629+
`onDropActivate` runs only for collection item descriptors, never the root
630+
descriptor. Dynamic
625631
acceptance/operation callbacks, external OS files/directories,
626632
cross-application transfer, drag previews, and final accessibility/host
627633
conformance remain separate work.

docs/tsx-native-runtime.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,16 @@ The action registry retains callbacks for the active committed revision and
289289
one rollback revision. A commit atomically replaces the active action scope.
290290
Late events for an older revision are rejected before callback dispatch.
291291

292+
Callbacks that decide hit testing before an event is dispatched are a distinct
293+
protocol concern. In particular, React Aria's `shouldAcceptItemDrop` and
294+
`getDropOperation` require a synchronous answer while resolving the current
295+
collection target. They must not be encoded as ordinary action ids, serialized
296+
closures, or post-event reducers. Before these APIs are exposed to TSX, the
297+
session protocol must define a revision-scoped policy decision/query contract
298+
with bounded execution, stale-revision rejection, and `cancel` as the safe
299+
timeout/failure result (or an equivalent declarative decision table). Node
300+
continues to own JavaScript callbacks; the Rust host never executes JS.
301+
292302
### Components and Hooks
293303

294304
The initial runtime is intentionally smaller than React:

src/compiler.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2563,6 +2563,7 @@ fn drop_zone_scope_value(props: &CompiledProps) -> GuiResult<JsonValue> {
25632563
.on_drag_leave(non_empty_prop_action(props.events.get("onDragLeave")))
25642564
.on_drop_enter(non_empty_prop_action(props.events.get("onDropEnter")))
25652565
.on_drop_move(non_empty_prop_action(props.events.get("onDropMove")))
2566+
.on_drop_activate(non_empty_prop_action(props.events.get("onDropActivate")))
25662567
.on_drop_exit(non_empty_prop_action(props.events.get("onDropExit")))
25672568
.accepted_drag_types(non_empty_attribute(
25682569
props,
@@ -2615,6 +2616,7 @@ fn drop_scope_value(props: &CompiledProps) -> GuiResult<JsonValue> {
26152616
.on_drop_enter(non_empty_prop_action(props.events.get("onDropEnter")))
26162617
.on_drop_exit(non_empty_prop_action(props.events.get("onDropExit")))
26172618
.on_drop_move(non_empty_prop_action(props.events.get("onDropMove")))
2619+
.on_drop_activate(non_empty_prop_action(props.events.get("onDropActivate")))
26182620
.accepted_drag_types(non_empty_attribute(
26192621
props,
26202622
&["acceptedDragTypes", "data-accepted-drag-types"],

src/event.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ pub enum NativeEventKind {
6969
DragEnd,
7070
DropEnter,
7171
DropMove,
72+
DropActivate,
7273
DropExit,
7374
Drop,
7475
Action,
@@ -209,6 +210,7 @@ impl NativeEvent {
209210
| NativeEventKind::DragEnd
210211
| NativeEventKind::DropEnter
211212
| NativeEventKind::DropMove
213+
| NativeEventKind::DropActivate
212214
| NativeEventKind::DropExit
213215
| NativeEventKind::Drop
214216
| NativeEventKind::Wheel => NativeInputModality::Mouse,

src/interaction.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,9 @@ impl InteractionState {
489489
NativeEventKind::DragEnd => {
490490
after.dragging = false;
491491
}
492-
NativeEventKind::DropEnter | NativeEventKind::DropMove => {
492+
NativeEventKind::DropEnter
493+
| NativeEventKind::DropMove
494+
| NativeEventKind::DropActivate => {
493495
after.drop_target = true;
494496
}
495497
NativeEventKind::DropExit | NativeEventKind::Drop => {
@@ -574,6 +576,7 @@ impl InteractionState {
574576
| NativeEventKind::DragEnd
575577
| NativeEventKind::DropEnter
576578
| NativeEventKind::DropMove
579+
| NativeEventKind::DropActivate
577580
| NativeEventKind::DropExit
578581
| NativeEventKind::Drop
579582
| NativeEventKind::Focus

0 commit comments

Comments
 (0)