Skip to content

feat(List): the reorder works for every pointer (spec:AST-094 FR43) - #8683

Draft
vjeux wants to merge 1954 commits into
meta:mainfrom
vjeux:uikit/c13-list-reorder-fr43
Draft

vjeux wants to merge 1954 commits into
meta:mainfrom
vjeux:uikit/c13-list-reorder-fr43

Conversation

@vjeux

@vjeux vjeux commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Depends on #8140, and carries #8665 and #8666 (the every-pointer gesture host and its pen test driver). Review only this PR's change, its one commit: vjeux/astryx@8d0e69a...df13e2c

Why

People reorder with whatever they hold: a finger on a laptop's touch screen, a pen, or a mouse. The List reorder's touch hold worked only where the main pointer is a finger, and a pen always had to hold first, even on a desktop.

What led to it

This is part of the UIKit-parity program, which makes Astryx's components behave like their iOS counterparts on touch screens. spec:AST-094 FR43, now merged, makes gestures work for every pointer, and has a mouse, trackpad or pen start a reorder by dragging the handle without a hold. Here:

  • The touch hold works on any device. Until a press, it costs one passive listener.
  • On a desktop, a pen drags at once, like a mouse.
  • On a phone, a flick on the handle still scrolls, and a pen holds first, like a finger.
  • Only a primary click drags. A right click or a pen's barrel button opens the row's menu.

Options and tradeoffs

The handle's touch-action could be none everywhere, manipulation everywhere, or depend on the main pointer. none breaks the phone's flick-to-scroll; manipulation lets a pen pan the page instead of dragging.

Why this choice

It depends on the main pointer: none on a desktop, so a pen stroke can't become a pan, and manipulation on a phone, so a flick still scrolls. Wheel autoscroll, mouse press feedback and removing the row-menu stopgap stay in their own PRs.

How to check it

In Storybook Core/List:

  1. Reorderable, on a laptop with a touch screen: hold a grip with a finger, then drag. The row moves.
  2. With a pen on a desktop, drag a grip. The row moves at once, and the page doesn't scroll.
  3. Reorder From Your UI: right-click a grip. The menu opens, and nothing is dragged.

I ran the browser and unit tests for each step, and all pass; each one failed first. WebKit skips the pen cases, since its driver has no pen. Two reds come from elsewhere: touch-support-bundle, which #8671 fixes, and pr-rtl, main's newer RTL test from 0472c4e.

Evidence

The List reorder's browser tests.

Stack (merge in this order): … → #8139 → #8337 → #8140 → #8665 → this.

vjeux and others added 30 commits October 26, 2026 19:51
…ter stays the touch hit layer

Under a finger, an interactive row's `::after` is its invisible hit layer
(List FR10, listRow.stylex.ts). The surface a row an update slides across
others rides on was the same pseudo-element, so the two merged: the hit
layer took the surface's colour and its `pointer-events: none`. The surface
is now an inset shadow across the row's padding box, which paints in the
same place (over the row's background, under the press layer and the
content, inside the divider) with no pseudo-element. The proof reads the
shadow's colour instead of the layer's.
…eta#8061 9adbe2d, meta#8072 9800eb5) into the gesture watcher

The README's touch-action table takes meta#8061's edge-pan rows beside this
PR's watcher row.
…es its recognizers, and the group keeps answering afterwards

Under a fine pointer the host is not installed, so usesTouchEvents keeps
its initial true, and a group press's own touch pointermove and pointerup
went down the touch-pairing path and returned: the finger's moves and lift
never reached the group, its contact stayed in the arena, and the group
answered no press after it, a mouse's included. The group press now feeds
its own events through the pointer path for every pointer type.

Test (failing first): under a fine pointer a finger tap on a group
recognizes, and a mouse tap afterwards does too.
… are all passive

spec:AST-094 FR20: a watcher registers only passive listeners, but a
watcher-only page still got the host's cancelable document touchend and
its contextmenu and window click listeners. The document touchend is now
passive until an enabled claimer (a recognizer that claims touches, or a
consumer) is attached, and the contextmenu and click listeners are on only
then, swapped as attachments change. The new test (a lone watcher leaves
no cancelable document or window listener; a claiming pan brings the
three) fails on the previous head.
…r watchers) into the host line

The host keeps this PR's isRtlElement and its fingers held before a
recognizer begins (a kept press, a scroll-edge pan), beside meta#8061's view
size and meta#8309's pointer-events finger lookup. meta#8589's claimer check
counts only participants active on this pointer (isActive,
isConsumerActive), and a group press's click listener now stays while a
claimer keeps it installed. The browser spec keeps the fine-pointer
describe beside the FR18-FR22 real-touch states.
…'s passive-only watcher listeners) into the arena fixes

The host-owner changeset keeps this PR's own listeners for the scroll
lock and finger-scroll watch, and adds meta#8336's scroll-edge hold.
A List row's target is its pressable root (Item's `li`), not the control
inside it: that inner button, link or checkbox is only the label column,
inside the row's padding. Without the root among the interactive elements, a
row's watch measures the next row through that inner control, 10 px away
instead of 2, and both 36 px rows grow 4 px into the 2 px gap, overlapping
by 6 px. With it, each takes 1 px and the two areas meet in the gap.
…h the pressable-root line offered there) into the touch row heights change
…inter reaches its recognizers) as a dependency
…s the diagonal pocket and lets keyboard clicks through

- The live watch read neighbours within 22 px only, but two small targets
  reach each other from up to 44 px apart: two 8 px dots 24 px apart kept
  18 px of growth toward each other. It reads within the minimum size now.
- A wide target and a tall one, diagonally, could each stay short of the
  other's half of the gap on one axis yet meet at full growth (32 x 32
  beside 40 x 24: 4 x 4 px). A neighbour now also counts when the two full
  areas meet.
- The one-shot click swallower lets a keyboard or assistive-technology
  activation (detail 0) through, and a cancelled touch (pointercancel)
  disarms it, so a touch the browser took for a scroll leaves nothing
  armed.
- A second watch of an element starts as visible as the first, so it hears
  its insets.

Each fix has a test that fails on the previous head.
… under a fine pointer) as a dependency
…tion group's finger under a fine pointer) into the gesture watcher
Main's meta#8521 is its head 25b468c squashed, which this pull request already
carries: every file it touches is byte-identical to 25b468c. In the two
files that conflicted (the motion README and animator.ts), the resolution is
this side's version, main's merged meta#8521 with this pull request's later work
on top. Nothing of main is dropped.
… control's label included

The guard ended on its click, a cancelled touch, or the next press on the
control itself; a tap on the control's own label lands outside it, so a
touch that ended with neither a click nor a cancel could leave the label's
click to the input swallowed. While armed, the guard now also listens for
the next pointerdown anywhere in the document. The test's label-tap case
fails on the previous head.
A List row's target is its pressable root (Item's `li`), not the control
inside it: that inner button, link or checkbox is only the label column,
inside the row's padding. Without the root among the interactive elements, a
row's watch measures the next row through that inner control, 10 px away
instead of 2, and both 36 px rows grow 4 px into the 2 px gap, overlapping
by 6 px. With it, each takes 1 px and the two areas meet in the gap.
…ta#8521) into meta#8270

# Conflicts:
#	packages/core/src/gestures/__tests__/GestureHost.test.ts
…g) into meta#8293

# Conflicts:
#	packages/core/src/motion/animator.ts
The note's reword joined its next sentence onto one long line; it now
wraps like the rest of the file. No code change.
Rows move by transform (an update's slide, a swipe, edit mode's slide), and
the shared touch hit area measures them as they stand in one frame: during a
removal it re-measured a frame into the motion and wrote fractional insets,
a layout and a paint in the middle of a compositor-only motion. A
measurement that arrives while the row's list has a finite animation running
or held is now dropped, and once those animations end the row is watched
afresh, so its insets are taken at rest.
vjeux added 10 commits October 27, 2026 06:37
…inst a fine pointer

astracat's 13:21Z review: the finger-target case capped every hint button,
Done included, below 44 px, which neither shows that no control grows for a
finger (spec:AST-094 FR42) nor allows a button that is larger by itself. It
now opens the same hint where the primary pointer is fine and requires each
button to add the same space around what it shows (its glyph, or its text's
width and line) under both pointers; the theme's type scale may set larger
text for a coarse pointer, which the comparison leaves out. The invisible hit
layer still only grows, up to 44 px or the button's own size.

Failing-first: with a coarse-only 2 px of block padding on the buttons, the
old cap passed and this case fails (Move up adds 4 px of height).
…e touch target size from the theme) under the reorder values
astracat's 13:19Z review: the browser proofs read only the order a drop
leaves, and the story's moveKey is idempotent, so an onReorder called twice
for one drop passed them, while spec:AST-103 FR3 asks for one call per drag
and none for a cancelled one. The List stories count the moves their
onReorder reports on the list (data-moves); the touch drop and the mouse drop
each add exactly one, and a cancelled drag adds none.

Failing-first: with the engine reporting each drop twice, the touch and
mouse drop proofs failed (expected 1, received 2); the cancel proof passed,
as it should.
…c381 and meta#8336 c3c119a through meta#8139's base) under the List reorder's browser proofs
… with X1's web input drivers (ad7aa98) under the List reorder's FR43 follow-up
…r-target proof; meta#8106 781c381 below it) under the List reorder's FR43 follow-up
@github-actions

Copy link
Copy Markdown
Contributor

PR Analysis Report

Current run: The current CI run concluded cancelled. Current analysis is unavailable.

View Storybook for this PR · View Sandbox for this PR


Generated by PR Enrichment workflow | View current CI run

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 27, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs an engineering owner's decision before it can be approved: List's every-pointer reorder cites spec:AST-094 FR43, which is not on main yet (it is proposed in #8646), while the current FR19 attaches no recognizer where the primary pointer is fine. An engineering owner needs to identify the governing spec for this behavior, including whether a finger's flick may stop on the handle on touchscreen laptops where spec:AST-103 FR3 says it still scrolls, and records the decision by commenting /approve-spec af760b20424a59f6bb7ea2b326ab1f6741ad1aa9 on this pull request; the author, as an engineering owner, may do so. Independently of that decision:

  • The diff against main carries the whole stack (261 files, about 47,800 lines), so land #8139, #8337, #8140, #8665 and #8666 first and rebase until this pull request shows only its own commit.
  • ListReorderHandle.tsx sets touch-action: none under a fine pointer, which drops the pinch-zoom spec:AST-094 FR18 keeps, while pinch-zoom would still stop a pen's pan.
  • List's docs and release note (List.tsx, List.doc.mjs, createReorderable.doc.mjs, .changeset/list-reorderable.md) still say a flick on the handle scrolls and do not mention pens.

[Automated review]

Comment on lines +1904 to +1911
// A pen drags at once where the primary pointer is fine, as a mouse does,
// since the handle allows it no pan there; where it is coarse, the handle
// keeps the page's pan, so a pen holds as a finger does (an iPad Pencil's
// touches are claimable; spec:AST-094 FR43, FR18).
useIsomorphicLayoutEffect(
() => controller.attachMouse(!isTouch),
[controller, isTouch],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This effect changed (it now depends on isTouch and admits pens), and its comment explains pens but names no external system. architecture:react-component-runtime INV3 asks a new or changed Effect to name what it synchronizes: here the list's pointer listeners that attachMouse adds and its returned detach removes.

'@astryxdesign/core': patch
---

[feat] `createReorderable({onReorder, isReorderable})` makes a collection's reorder behavior, and `List` takes it as `reorderable`: each `ListItem` with an `itemKey` gets a reorder handle at its end, and each committed move calls `onReorder({key, to})` once, with the row's key and its index after the move. On a touch screen a finger held still on a handle picks up its row the way an iOS table's reorder control does (a flick on it still scrolls): the row stays in its place, dimmed, while a copy of it follows the finger and an accent line marks where it would land; the other rows stay still, the page scrolls near the edges, and on release the rows rearrange, gliding to their new places. A mouse drags a handle at once with the same copy and line, and rows take their places without motion; a cancelled drag puts everything back at once. Without a drag, a click, Space or Enter on a handle turns on reorder mode for its row: each arrow key moves it a place at once, announced with its place and neighbours, and Escape or another activation ends the mode, keeping the moves. The object's `start(key)` enters the same mode from the app's own UI, such as a row's context menu. A List without `reorderable` loads none of the reorder code.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description relies on this release note, but with the own commit a flick on the handle no longer scrolls on a touchscreen laptop, and a pen drags at once there and holds where the primary pointer is coarse; this note still says "a flick on it still scrolls" and names only a mouse's immediate drag.

@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf: closed and reopened only to restart this PR's CI on the current workflow after tonight's queue-wide cancel; nothing in the PR changed.

cixzhang and others added 6 commits October 27, 2026 20:51
… hold

A divided List row keeps its dividers while it is pressed, as it does today, and Core keeps no selection hold: products show a row kept selected through ListItem's isSelected. This removes the touch press's divider hiding (its ListItem styles, docs, changeset, tests and browser proofs) and the selection hold (selectionHold.ts, its held press state in the shared press code, its internal deselect constants, the row focus-return lines, story, changeset and proofs). The shared test helpers, the press-coverage guard and the press release's own clock stay.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…tion hold) as a dependency

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
The previous merge left List.tsx's hasDividers JSDoc unresolved and the List stories empty. This restores the stories (without the selection hold demo) and the plain hasDividers line.

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…tion hold) as a dependency

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…tion hold) as a dependency

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
…tion hold) as a dependency

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
…tion hold) as a dependency

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs an engineering owner's decision before it can be approved: with spec:AST-094 FR43 now on main the every-pointer reorder has its spec, but on a touchscreen laptop the handle's touch-action: none gives a pen its drag without a hold by stopping a finger's flick from the handle, while spec:AST-103 FR3 asks for both. An engineering owner records which wins: commenting /approve-spec 85a30f173854866a1a0735180e5b75fb32f16c8d on this pull request keeps the pen's drag as written, and otherwise the handle has to keep the flick; the author may record it as an engineering owner. Independently of that decision:

  • The diff against main carries at least 18 other open pull requests (252 files, about 45,600 lines) and no longer merges cleanly (TreeList), so land the stack and rebase until only this pull request's own commit remains.
  • ListReorderHandle.tsx drops the pinch-zoom spec:AST-094 FR18 keeps; pinch-zoom in place of none would still stop a pen's pan.
  • The carried hasAnimatedChanges should be the animatedLayout handle List.spec.md FR12 specifies, and List's docs and release note still say a flick on the handle scrolls.

[Automated review]

* Off by default; with a mouse nothing moves.
* @default false
*/
hasAnimatedChanges?: boolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current List.spec.md FR12 says "List MUST take an animatedLayout handle (spec:AST-105 FR1)", and AST-105 FR5 has the glide apply "under every pointer, a mouse included". This boolean animates only under a finger and sits beside the useAnimatedLayout hook main already exports, so a caller who adopts it holds an API the record does not have. Taking FR12's animatedLayout handle instead keeps one public shape.

if (target.closest('button, a, input, select, textarea')) {
return;
}
if (onClick == null && href != null && !isLinkRoot) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This forwards any click in a link row's box to its anchor, under every pointer, so a mouse click in a ListItem row's padding, which did nothing before, now navigates (the new Item.test.tsx case asserts it with a plain click). List.spec.md FR10 grows the target only under a coarse pointer and says "A fine pointer keeps today's hit target". Forwarding only from the coarse-pointer hit area would keep a mouse's target as it was.

* development). On an object passed to two collections, it starts the
* mode in the one mounted last.
*/
start(key: TKey): void;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

start is the object's only method, and the options carry only onReorder and isReorderable. Current spec:AST-103 FR4b says "The object exposes attach(container, {items})", and FR1 gives the options "an optional positions()"; FR6 makes the object stable from its first release, which this pull request is.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 28, 2026
meta#8360's gesture host

With meta#8106 on meta#8360's foundation, the gesture host takes a finger from
pointer events where it can: mounting two reorderable lists on a touch
screen now puts a pointerdown listener on the document, and no touch
listener until a press. The test asked for touch listeners only, so it
found none (expected 0 to be greater than 0). It now counts touch and
pointer listeners, as meta#8683's version of it already does, and still
requires every one of them to go when the lists unmount.
@vjeux
vjeux marked this pull request as draft October 29, 2026 22:39

This branch was successfully deployed

1 active deployment
Preview — 85a30f17 Deployed Oct 28, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review needs:spec-owner-review Current knowledge records await owner approval

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants