dioxus-dnd 3.1.0

Modular, accessible drag-and-drop for Dioxus: sortable lists, kanban boards, trees, grids, file drops, multi-select, touch support and more
Documentation
# Virtualized lists

A windowed (virtualized) list renders only the visible slice, so its rows -
and the drop zones on them - constantly appear and disappear. The crate
needs nothing special for this: rows are ordinary `DropZone`s, and the zone
registry is built for churn.

API reference: the registry section of [api/core.md](../api/core.md).
Live demo: the
[Archive](https://kindintelligence.github.io/dioxus-dnd/archive) page runs
the full pattern at 10,000 rows, keyboard navigation included.

## The mental model

Zones churn; the registry does not care:

- A row scrolling into view mounts a `DropZone`, which registers itself
  and **measures itself the moment it mounts**. That mount-time measurement
  is what makes the pattern work: a row appearing *mid-drag* missed the
  pickup measurement and the last scroll ping, yet it is hit-testable the
  instant it exists.
- A row scrolling out unmounts, and `use_drop` unregisters its zone.
- **Stable, index-derived ids** tie it together: give each row
  `ZoneId(BASE + index)` so a recycled row re-registers as itself, and a
  remount replaces the old registration in place. Handlers capture the row
  index, so the model update is a plain indexed write - no bookkeeping
  about which DOM node happened to host the row.

Auto-generated ids start at 2^32, so any `BASE + index` scheme that stays
in `u32` range can never collide with them. Pick a `BASE` clear of your
other explicit ids (the archive page uses 20,000).

## A worked example

The archive page's shape, trimmed. Fixed-height rows make the window
`scroll_top / row_height` plus a buffer, drawn inside a full-height canvas
with a `translateY` offset - the standard virtualization shape:

```rust,ignore
AutoScroll {
    style: "height: 420px; overflow-y: auto;",
    on_scroll: move |offset: Point| scroll_top.set(offset.y), // edge auto-scroll
    div { style: "position: relative; height: {ROWS as f64 * ROW_H}px;",
        div { style: "position: absolute; top: 0; width: 100%;
                      transform: translateY({first as f64 * ROW_H}px);",
            for ix in first..last {
                DropZone::<Tag> {
                    key: "{ix}",
                    id: ZoneId(BASE + ix as u64),   // stable per row
                    label: format!("Record {}", ix + 1),
                    on_drop: move |o: DropOutcome<Tag>| tag_row(ix, o.payload),
                    "aria-setsize": "{ROWS}",
                    "aria-posinset": "{ix + 1}",
                    div {
                        // rows double as scroll sentinels (see below)
                        onvisible: move |evt| resync_window(ix, evt),
                        Row { ix }
                    }
                }
            }
        }
    }
}
```

Ten thousand rows, a few dozen mounted zones at any moment - and every one
of them a live drop target. The library side is unremarkable on purpose:
swap in any virtual list, keep the zones.

## The windowing signal

dioxus-web 0.7 delivers no element-level scroll events - an `onscroll`
handler on the container never fires. Dioxus's documented virtual-list tool
is `onvisible`, and the rendered rows double as their own scroll sentinels:
whenever a row crosses the container's clip, its IntersectionObserver entry
fires, and the entry's rect plus the row's known canvas position
(`index * row_height`) recover the scroll offset. That covers wheel,
scrollbar and programmatic scrolls alike, idle or mid-drag. Give the window
a buffer of rows on each side so a crossing fires before blank space can
show.

## Scrolling during the drag

Wrap the container in `AutoScroll` and a drag hovering near its edges
scrolls the archive under the pointer. Two things make that safe here:

- `on_scroll` reports the container's offset after every scroll the
  component observes - its own edge-scrolling above all - so the window
  re-slices while the drag is still in flight, and new rows register as the
  old ones unregister.
- Every observed scroll also pings the rect-refresh channel, so already
  registered zones re-measure. Hover highlighting and the eventual drop
  land on the row the user actually sees, not where rows sat at pickup.

If your scroll surface is not an `AutoScroll`, grab the channel yourself
with `use_rect_refresh()` and call `refresh_all()` from your own scroll
signal. See [Auto-scroll](autoscroll.md).

## Announcing position in the full list

`DropZone` forwards arbitrary attributes, so each row can carry
`aria-setsize` (the full length) and `aria-posinset` (this row's 1-based
position). Screen readers then announce "record 5,732 of 10,000" even
though only a few dozen rows exist in the DOM. Pair with `role: "list"` on
the canvas and `role: "listitem"` on the rows, and give each zone a `label`
so keyboard hovers announce something meaningful. See
[Accessibility](accessibility.md).

## Gotchas

- **Keyboard drags reach mounted rows only.** Arrow keys walk the
  registered zones in spatial order, and only the window's rows are
  registered. Scroll first, then pick up.
- **Auto ids break recycling.** An auto-generated id is minted per mount,
  so a recycled row would register as a stranger and your handler could not
  name it. Index-derived ids are the contract.
- **Key the rows by index.** `key: "{ix}"` keeps the remount aligned with
  the registration it replaces.
- **The sentinel signal is edge-triggered.** `onvisible` fires on clip
  crossings, not continuously; the buffer rows are what turn crossings into
  a timely window. Do not shrink the buffer to zero.

## Related

- [Auto-scroll]autoscroll.md: edge-scrolling, `on_scroll`, and the
  rect-refresh channel.
- [Drag and drop]drag-and-drop.md: what a `DropZone` registers and
  measures.
- [Architecture]architecture.md: the registry that absorbs the churn.
- [Accessibility]accessibility.md: announcements and labels.