Skip to content

feat(lab): TransferList reorders through core's createReorderable - #8143

Draft
vjeux wants to merge 5 commits into
meta:mainfrom
vjeux:uikit/c13-transferlist-reorder
Draft

vjeux wants to merge 5 commits into
meta:mainfrom
vjeux:uikit/c13-transferlist-reorder

Conversation

@vjeux

@vjeux vjeux commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Stack: #8106 → #8139 → #8143 → #8163

This merges after #8139 and #8106. Review only this PR's change: vjeux/astryx@ee13b42...164d15d

Why

TransferList lets people order the items they picked, such as columns to show. Its grip grabbed every touch at once, so swiping over grips on a phone didn't scroll the panel.

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. Moving Lab's reorders onto core's is part of it. TransferList had its own drag code, duplicating core List's reorder (#8139). The selected panel becomes a core List with reorderable: hold to drag, flick to scroll, pinch to zoom, and keyboard reorder mode. Following Cindy's direction (her commits are here), reorderable takes the createReorderable object, with isReorderable(value) and onReorder({key, to}). TransferListSelector takes it too: in staged mode a move edits the draft, and Cancel drops it.

Before: <TransferList isReorderable />. After: <TransferList reorderable={createReorderable({onReorder})} />.

This changes desktop behavior too: reordering is opt-in through the owner's object, so a TransferList without reorderable no longer reorders with a mouse either (the old isReorderable defaulted to true). Owner rule, Nov 4 2026 (Cindy): anything Lab-only gets an OK. Her commits are here, and she asked that TransferList's doc page and this description carry the migration below.

To migrate:

// Before: isReorderable turned reordering on, and an option opted out.
<TransferList options={[{value: 'name', label: 'Name', isReorderDisabled: true}, ...rest]} isReorderable value={fields} onChange={setFields} />

// After: reordering is opt-in through the object createReorderable makes.
const reorderable = useMemo(
  () =>
    createReorderable<string>({
      onReorder: ({key, to}) => setFields(fields => moveTo(fields, key, to)),
      isReorderable: value => value !== 'name',
    }),
  [],
);
<TransferList options={[{value: 'name', label: 'Name'}, ...rest]} reorderable={reorderable} value={fields} onChange={setFields} />

TransferList's doc page carries the same example, "Migrating from isReorderable to reorderable".

Core's internal entry @astryxdesign/core/internal/reorder comes in here with its first user. It adapts the owner's object, so the panel runs the owner's engine with its own moves, and the owner's start(key) still finds the panel's list. Like Core's other internal entries, it is undocumented and outside the public API.

Options and tradeoffs

  • Keep TransferList's own drag and patch its touch handling, or reuse core's reorder and change the API.
  • An option that can't move: a disabled grip, or no grip at all.

Why this choice

Reusing core's reorder means one implementation, and TransferList gets the hold, the keyboard path and the announcements. The cost is the API change; Lab is private, so there's no changeset. An option that can't move shows no grip (spec:AST-103 FR1). Reordering pauses while a search filters the panel, where a position is ambiguous. Cindy's internal/reorder.ts takes string or number keys to match her #8592, named here for her review.

How to check it

In Storybook Lab/TransferList:

  1. Move a few options to the selected panel.
  2. On a phone, hold a grip, then drag. The row moves, and a flick scrolls the panel.
  3. Tab to a grip, press Enter, then use the arrow keys; Escape ends the mode.
  4. Locked: locked options have no grip, and nothing drops above a locked first row.

Gates at fddf9c6, whose own change this head carries on the new stack, plus the doc page's migration example:

  • Core and Lab typecheck 0 (with Lab's docs), ESLint 0, INV29 0. check:knowledge, check:changesets, check:repo and the exports sync: 0. review:pre: only the size note, counted from main.
  • Units: TransferList, TransferListSelector, Core's internal reorder entry and core List, 243 of 243. They cover moves in the owner's terms and the staged draft.
  • Chromium browser specs: a preview that stops at the panel's end, and a pinch on a grip that zooms and keeps the order, 11 of 11.
  • The accessibility audit over the TransferList and TransferListSelector stories: no new violations.

On the stack's top (#8163 at ac07610, which carries this change): the full UI units, 21,206 tests, pass; Lab's Chromium specs pass 14 of 14; the accessibility audit over the TransferList and TransferListSelector stories finds nothing new.

Bundle cost

Measured with the PR size report's build options on the built packages (the eager chunks, minified, gzip). List and ListItem import 70.0 KB, and 114.8 KB with createReorderable. TransferList imports 87.8 KB, and 132.7 KB with createReorderable (45.0 KB more, 141.3 KB minified). Without reorderable, TransferList's import holds no reorder engine, gesture or animator module: the engine comes with the owner's object.

Evidence

Before: the grip drags at once, and on iOS 26.2: after, hold, drag and drop, and a pinch zooms.

Pull request size

1,042 lines added and 1,090 removed in 15 files: 231 added of source and 679 of tests. TransferList's own drag code goes.

@vercel

vercel Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Nov 5, 2026 12:20pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

AI review status for this pull request.

Review status Updated
❓ Needs maintainer input (for maintainers only) Nov 5, 2026, 12:37 PM UTC

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Oct 24, 2026
@github-actions github-actions Bot added the needs:design-review Affects visuals — Design should review label Oct 24, 2026
@vjeux
vjeux force-pushed the uikit/c13-transferlist-reorder branch from e4d7ed7 to 2c3fd8d Compare October 24, 2026 08:09

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approval waits on an owner decision. TransferList's move to core List's reorder model (owner decisions 1–4 in the description) and the draft AST-094 have no approved spec, so an engineering owner needs to provide or identify the governing records and record the decision by commenting /approve-spec <full head SHA> with this pull request's current head. The author may do this if they are an engineering owner. Whatever the decision, these changes are needed:

  • Land #8139 and #8126 first, or rebase once they merge, so this pull request carries only its 1,023-line TransferList commit instead of 32,385 lines.
  • In reorderSelected, keep values that have no option in their own slots: today Space, Home, Escape on Owner with ['name', 'retired', 'owner', 'status'] reports ['name', 'owner', 'retired', 'status'].
  • With renderOption, name each reorder handle after its option rather than "Reorder item N".
  • The description says the keys keep their behavior, but arrow keys now move a row without a pick-up, so please include that in the owner decision.

[Automated review]

Comment on lines +526 to +531
// Next to the row it lands on, in the full value (which may hold
// values without an option).
const nextValue = currentValueRef.current.filter(item => item !== moved);
const at = nextValue.indexOf(anchor);
nextValue.splice(toIndex > fromIndex ? at + 1 : at, 0, moved);
commit(nextValue);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Escape replays the inverse move through onReorder(index, lifted.origin), but re-inserting the moved value next to its anchor lets entries with no option drift: with ['name', 'retired', 'owner', 'status'], Space, Home, Escape on Owner ends at ['name', 'owner', 'retired', 'status'] while List announces "back to position 2 of 3", and ArrowDown then ArrowUp on Name ends at ['retired', 'name', 'owner', 'status']. Writing the moved visible order back into the visible values' own indices keeps hidden entries in their slots and makes every move reversible.

return (
<ListItem
key={option.value}
label={renderOption?.(option) ?? option.label}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With renderOption this label is a node, so ListReorderHandle names every handle "Reorder item N" by position (ListReorderHandle.tsx:197-211), while the row's other action still reads "Remove Name"; the old grip was always "Reorder {option.label}". Giving the handle the option's text, and asserting Reorder Name in the renderOption test, would keep the handles distinguishable for screen-reader and voice-control users.

Comment on lines +78 to +98
useIsomorphicLayoutEffect(() => {
const element = listRef.current;
if (element == null) {
return;
}
const rows = rowsOf(element);
const places = new WeakMap<HTMLElement, number>();
for (const row of rows) {
// Inside the list: content above it changing size moves no row.
const base = row.offsetParent === element ? 0 : element.offsetTop;
places.set(row, row.offsetTop - base);
}
const before = placesRef.current;
placesRef.current = places;
const height = element.offsetHeight;
const grew = height - heightRef.current;
heightRef.current = height;
// The first commit, motion off, or a reorder session that owns the rows:
// remember where rows are, move nothing.
if (
before == null ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This layout Effect reads every row's offsetTop and the list's offsetHeight after every commit before it checks animates, so every List, including those without hasAnimatedChanges, forces a layout on each commit. Returning before the reads when enabled is false (and clearing the remembered places) limits the cost to lists that opt in.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approval waits on an owner decision: TransferList's move to core List's reorder model (owner decisions 1–4 in the description, plus arrow keys that now move a row without a Space pick-up) and the draft AST-094 have no approved spec. An engineering owner needs to provide or identify the governing records and record the decision by commenting /approve-spec <full head SHA> with this pull request's current head; the author may do so if they are an engineering owner. Whatever the decision, these changes are needed:

  • Land #8139 and #8126 first, or rebase once they merge, so this pull request carries only its 1,023-line TransferList commit instead of 32,445 lines.
  • In reorderSelected, keep values that have no option in their own slots: Space, Home, Escape on Owner with ['name', 'retired', 'owner', 'status'] reports ['name', 'owner', 'retired', 'status'].
  • Keep the selected rows' Remove buttons in one column and centred: beside the 44 px handle, movable rows grow to 44 px and Remove moves 36 px inward and 4 px up (visible in the Locked story).
  • With renderOption, name each reorder handle after its option rather than "Reorder item N".

[Automated review]

<ListItem
key={option.value}
label={renderOption?.(option) ?? option.label}
endContent={optionActions(option, side, index)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The List handle that follows this end content is at least 44 × 44 (ListReorderHandle.tsx:56-64), so in this compact list a movable row grows to 44 px while fixed rows and every available row stay 36 px, and its Remove button sits 36 px further from the edge and 4 px above the row's centre. In the Locked story the Remove column zigzags between Owner/Last updated and Name/Status. Reserving the handle's slot on rows without one, and letting the handle fit a compact row instead of growing it, would keep one Remove column and one row height.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

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


Generated by PR Enrichment workflow | View current CI run

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approval waits on an owner decision: TransferList's move to core List's reorder model (owner decisions 1–4 in the description, plus arrow keys that now move a row without a Space pick-up) and the draft AST-094 have no approved spec, so an engineering owner needs to provide or identify the governing records and record the decision by commenting /approve-spec <full head SHA> on this pull request, which the author may do if they are an engineering owner. Whatever the decision, these changes are needed:

  • Land #8139 first, or rebase once it merges, so this pull request carries only its 1,128-line TransferList commit instead of 32,359 lines.
  • In reorderSelected, keep values that have no option in their own slots: Space, Home, Escape on Owner with ['name', 'retired', 'owner', 'status'] reports ['name', 'owner', 'retired', 'status'].
  • Keep the selected rows' Remove buttons in one column beside the 44 px handle, and name each handle after its option under renderOption instead of "Reorder item N".
  • Add the List drag browser spec that ListReorder.test.tsx and useListReorder.ts cite, or correct those references.

[Automated review]

* nothing, and a disabled row's handle stays focusable but inert
* @position Tests for List/useListReorder.ts and List/ListReorderHandle.tsx;
* the touch and mouse drags are proven in a real browser by
* ListReorder.interaction.browser.spec.ts

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This names ListReorder.interaction.browser.spec.ts as the proof of the touch and mouse drags, but no such spec exists in this pull request (useListReorder.ts:43-44 and TransferList.test.tsx:6 point to it too). The mouse drag that replaced TransferList's pointer tests has no test; adding the spec, or correcting the three references, would keep the coverage claim true.

@vjeux

vjeux commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

On the predecessor stack: this pull request is stacked, as its first line says, and only its last commit (03b352e, 4 files, +156 −972) is its own. It rebases onto main as each predecessor lands, and the diff then shrinks to that commit.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approval waits on the spec records behind the new @astryxdesign/core/internal/reorder export: spec:AST-017 FR21 and architecture:public-component-api INV12 still say a behavior or utility module is never exported and that anything reachable from a package path is public, so an engineering owner needs to update both records on main (or the pull request reaches the reorderable object without a package export) and record the decision by commenting /approve-spec <full head SHA> on this pull request; the author may do so as an engineering owner. Whatever that decision, these changes are needed:

  • Land #8337 and its predecessors first, or rebase onto them as they merge and resolve the conflict with main, so this pull request carries only its 2,117-line TransferList change and CI can run.
  • Once CI runs, cite its base-against-head size report for TransferList and TransferListSelector, as architecture:component-style-authoring INV6 asks.
  • Type ListItem's itemKey as a string, as spec:AST-103 FR1 states (it reaches this pull request from #8139).
  • Correct the description's claim that a value with no option keeps its place, and the FR21 citations in packages/core/src/internal/reorder.ts and scripts/sync-exports.js.

[Automated review]

"types": "./dist/Markdown/remark.d.ts",
"default": "./dist/Markdown/remark.js"
},
"./internal/reorder": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This entry makes reorderableOptions and adaptReorderable reachable from Core's package exports. Current spec:AST-017 FR21 says a behavior or utility module without a public contract "is never exported, as experimental or otherwise", and architecture:public-component-api INV12 counts anything reachable from a supported package path as public, whatever its comment says. Either an engineering owner amends both records on main first, or Lab reaches the owner's object through a seam that is not in exports.

Comment thread packages/core/src/List/ListItem.tsx Outdated
* asked about. A row without one has no reorder handle. Separate from
* React's `key`, which stays React's own. No effect elsewhere.
*/
itemKey?: Key;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

spec:AST-103 FR1 says List reads each ListItem's itemKey, "a string"; Key also admits numbers and bigints, and the descriptor documents string | number | bigint. Typing it as string, or amending FR1, keeps this stable prop inside its claim.

vjeux added a commit to vjeux/astryx that referenced this pull request Oct 26, 2026
@github-actions github-actions Bot added the needs:spec-owner-review Current knowledge records await owner approval label Oct 26, 2026
cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested: TransferList's move onto createReorderable follows spec:AST-103, and the internal/reorder subpath now fits the ./internal/* exception on main, but a few fixes are needed before it can land.

  • Land #8337 and its predecessors first (or rebase as they merge), so this pull request carries only its 2,117-line TransferList change instead of 41,890 lines.
  • Update TransferList.doc.mjs (line 421), the first test in TransferListReorder.a11y.chromium.spec.ts (line 113) and the description to the drag List now performs: the rows stay still while the dimmed source, a following copy and an insertion line show the move.
  • Merge after #8561, or carry its lint rule, so @astryxdesign/core/internal/reorder is limited to Core and Lab as spec:AST-017 FR21 requires.
  • The stacked List drag draws a copy of the moving row, which current spec:AST-103 FR5 rules out; that needs settling in #8139.

[Automated review]

@@ -420,7 +418,7 @@ export const docs = {
{
guidance: true,
description:
'Commit add, remove, bulk, and keyboard reorder changes immediately. During pointer reordering, keep rows stationary, lock the translucent preview to its source-column position, use vertical pointer movement to determine insertion, and commit once on release. Keep the preview in the nearest popover or dialog top layer.',
"Commit add, remove, and bulk changes immediately. A drag reorders the way an iOS table does: the row follows the finger or pointer, the rows it passes move out of its way, and the reorderable object's onReorder runs once on release.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This describes rows that move out of the dragged row's way, but at this head List keeps the rows still, dims the dragged row and moves a copy of it with an insertion line (packages/core/src/List/listReorderEngine.tsx:1048-1070); List's own descriptor says so. A sentence that defers to List's drag stays true whichever look #8139 lands with.

const held = await geometry();
// The finger is 200 px past the list's end; the row stopped with its
// bottom on that end, so none of it hangs under the next panel.
expect(held.rowTop).toBeGreaterThan(start.rowTop + 40);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

grip resolves to the source row's handle (List appends the preview after the rows), and with List's current drag the source row stays in place, dimmed, so held.rowTop stays at start.rowTop and this assertion can't pass as written. Measuring [data-astryx-reorder-preview] would check the end-of-list clamp the header describes.

"types": "./dist/Markdown/remark.d.ts",
"default": "./dist/Markdown/remark.js"
},
"./internal/reorder": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Current spec:AST-017 FR21 and architecture:public-component-api INV12 accept an ./internal/* subpath only as one limited by lint to Core and Lab, and nothing at this head or on main enforces that yet; #8561 adds @astryx/no-core-internal-import. Merging after it, or carrying its rule, closes the gap.


/** The preview: a copy of `row`, at its place, opaque and lifted. */
function previewOf(row: HTMLElement, top: number, background: string | null) {
const preview = row.cloneNode(true) as HTMLElement;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

spec:AST-103 FR5 (current) says the moving row is the row itself, moved in place, not a copy, and its verification fails a copy of the moving row; this clone is that copy. #8625 makes the draft design record current but leaves FR5 and component-style-authoring INV7 as they are, so this needs an owner amendment on main or a row that moves itself (in #8139).

cixzhang added a commit to vjeux/astryx that referenced this pull request Oct 27, 2026
…s, Done on a drag, the own-ring move, the glide on springs.settle) as a dependency

ListInput's tests type the reorderable's key as string | number, the key type the reorder now reports by default (spec:AST-103 FR1).

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested: TransferList's move onto createReorderable follows spec:AST-103, but this head needs a few fixes before it can land.

  • Land #8337 and its predecessors first (or rebase as they merge): the compare is 42,806 lines, only 2,117 of them TransferList's, and its motion files now conflict with main, so CI did not run.
  • List's reorder handle is now 28 px, so the fixed rows' 36 px Remove clearance in TransferList.tsx (line 154) leaves their Remove 16 px off the column, and the Locked-rows check in TransferListReorder.a11y.chromium.spec.ts (line 161) fails.
  • TransferList.doc.mjs (line 421), the first test in TransferListReorder.a11y.chromium.spec.ts (line 113) and decision 1 in the description still describe rows that slide aside, while List now keeps them still behind a preview and an insertion line.
  • Merge after #8561, or carry its lint rule, so @astryxdesign/core/internal/reorder is limited to Core and Lab as spec:AST-017 FR21 requires; the stacked drag's moving copy, which spec:AST-103 FR5 rules out, needs settling in #8139.

[Automated review]

Comment on lines +151 to +154
// Core List's reorder handle: a 44 px target overlapping the row's end
// padding by 8 px. A row without one keeps its end content clear of it.
clearsHandle: {
marginInlineEnd: 36,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

At this head List's reorder handle is 28 px (ListReorderHandle.tsx:68) with the same 8 px end overlap, so a movable row's Remove ends 32 px from the row's end while this margin puts a fixed row's at 48 px: 16 px off the column, and TransferListReorder.a11y.chromium.spec.ts:161 fails on it. Sizing the clearance from a value List owns (20 px today) keeps the column when the handle changes again.

const [first] = rows;
for (const row of rows) {
expect(row.height, row.label).toBeCloseTo(first.height, 0);
expect(row.removeRight, row.label).toBeCloseTo(first.removeRight ?? 0, 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With this head's 28 px handle, a fixed row's removeRight is 48 px and a movable row's 32 px, so this check fails until the clearance at TransferList.tsx:154 follows the handle.

@cixzhang

Copy link
Copy Markdown
Contributor

Posted by Cindy's agent on her behalf.

Cindy's decision: our own components use the public gesture and motion APIs wherever they cover the job. That means createPanGesture/usePanGesture, createDragToSnapPoints/useDragToSnapPoints, createAnimator/useAnimator, createReorderable and useAnimatedLayout, rather than the internals underneath. Internals stay only for jobs the public layer doesn't expose, such as press feedback, the action-group tracker and the layer holds. Each of those uses is listed as a known gap.

The public factories and hooks are being specified now (spec:AST-107). Once they land, this PR moves its pan, snap-point and animator code onto them. Nothing needs to change before then.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested: TransferList's move onto createReorderable follows spec:AST-103 and the current reordering design, and the issues from the last review (the fixed rows' Remove column, the drag guidance and the drag test) are fixed. What remains is scope: the compare still carries about 41,000 lines from the open predecessor stack (#8337, #8562 and the pull requests under them) beside TransferList's 2,123, so please land those first, or rebase onto main as each one merges, so that review and CI cover only TransferList's change.

[Automated review]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested: TransferList's move onto createReorderable follows spec:AST-103 and the current reordering design, and the issues raised earlier in TransferList's own files are fixed. Two things remain, both from the stacked predecessors this branch carries:

  • Land #8337, #8562 and the pull requests under them first, or rebase onto main as each one merges, so this pull request carries only TransferList's 2,122 changed lines instead of 41,791.
  • scripts/touch-support-bundle.test.mjs ("is absent from the root entry") fails on this head and passes on main, because the root createReorderable export now brings the gesture host into Core's root entry; please resolve it in the predecessors before this lands.

[Automated review]

Comment thread packages/core/src/index.ts Outdated
@@ -172,6 +172,9 @@ export * from './Outline';
// Overflow list
export * from './OverflowList';

// Reorder (a collection's `reorderable` prop)
export * from './reorderable';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With this export, Core's root entry carries the reorder engine and with it 14 gesture modules (GestureHost.ts, GestureArena.ts and more), so scripts/touch-support-bundle.test.mjs › "is absent from the root entry" fails at this head while it passes on main. Built alone, @astryxdesign/core/reorderable carries 15 gesture modules; TransferList's and List's entries carry none. Keeping the engine out of the root entry, or changing the test with an owner's agreement, would let the node tests pass.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested: TransferList's move onto createReorderable follows spec:AST-103 and the current reordering design, and no defect turned up in TransferList's own files. Two things remain, both from the stacked predecessors this branch carries:

  • Land #8337, #8562 and the pull requests under them first, or rebase onto main as each one merges, so this pull request carries only TransferList's 2,122 changed lines instead of 42,120.
  • scripts/touch-support-bundle.test.mjs ("is absent from the root entry") fails on this head and passes on main, because the root createReorderable export brings the gesture host into Core's root entry; please resolve it in #8139 before this lands.

[Automated review]

Comment thread packages/core/src/index.ts Outdated
@@ -172,6 +172,9 @@ export * from './Outline';
// Overflow list
export * from './OverflowList';

// Reorder (a collection's `reorderable` prop)
export * from './reorderable';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This root export brings createReorderable's engine, and with it 14 gesture modules (GestureHost.ts, GestureArena.ts, LongPressGestureRecognizer.ts among them), into Core's root entry, so scripts/touch-support-bundle.test.mjs › "is absent from the root entry" fails on this head and passes on main. Please settle it in #8139, by keeping the engine out of the root entry or by changing the check with an owner's agreement; the description points to the open #8671 for that.

@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.

@vjeux

vjeux commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, and sorry for the late reply. These are all in: reorderable takes only the createReorderable(...) object, TransferListSelector takes the owner's object and builds its staged one through @astryxdesign/core/internal/reorder (carried from #8562), and the description no longer says a value with no option keeps its place. On the public APIs: its reorder already takes the public createReorderable object.

@astracat-bot astracat-bot Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested. TransferList's own switch to createReorderable matches spec:AST-103 and the direction given on this pull request, but the branch as issued can't land.

Blocking:

  • The compare carries 42,120 authored lines, 40,012 of them from the predecessor pull requests (#8337, #8139, #8138 and those below) and the closed #8562, and no longer merges with main; land those first or rebase onto main as each merges, so this pull request carries only TransferList's 2,122-line increment.
  • scripts/touch-support-bundle.test.mjs fails at this head ("is absent from the root entry") because the root createReorderable export pulls the gesture host into Core's root entry; fix it in #8139 against main's current version of that check.
  • Desktop behavior change needs an owner decision: with default props, TransferList no longer lets a mouse drag selected rows to reorder (the old isReorderable defaulted to true); gate it to touch input or cite the decision that covers it in the description.
  • The carried List reorder engine gives a pen on a desktop (fine primary pointer) no drag at all (listReorderEngine.tsx:877, :1907), while current spec:AST-103 FR3 says such a pen drags without a hold; fix it in #8139.

The Stack (merge in this order) line differs across the listed pull requests, so this pull request is reviewed as its own change. Two non-blocking comment suggestions are inline.

[Automated review]

* the move. Paused while a search filters the panel. Without it, the
* selected options keep their order.
*/
reorderable?: Reorderable<T>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Desktop behavior change needs an owner decision: with default props a mouse can no longer drag selected rows to reorder (the old isReorderable defaulted to true). Gate it to touch input, or cite the decision that covers default-off reordering in the description.

}
},
[beginReorder, finishReorder],
// The selected panel's rows, in order, as List reports a move among them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking (comment style): this line describes nothing at isOrdering. Suggest deleting it and keeping only the reason:

// Reordering is off while a search filters the panel. A hidden row could
// otherwise move past the shown rows.

Comment on lines +570 to +573
// The owner's object, adapted so List's moves among the shown rows reach
// the owner in the value's terms; its engine is the owner's, so nothing
// here loads the reorder (spec:AST-103 FR2), and the owner's `start(key)`
// finds this list (FR4a).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking (comment style): one 40-word sentence with three topics. Suggest:

// The engine is the owner's, so this adapter loads no reorder code
// (spec:AST-103 FR2). The owner's `start(key)` finds this list (FR4a).

@cixzhang

cixzhang commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 5cb0176

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested. TransferList's own switch to createReorderable follows spec:AST-103 and the direction given on this pull request, but the branch as issued can't land yet.

Blocking:

  • The compare carries 12,026 authored lines, 9,899 of them from the open #8106, #8139 and #8337, whose Stack: lines differ, so land those first (or rebase onto main as each merges) and this pull request carries only TransferList's 2,133-line increment.
  • Desktop behavior change needs an owner decision: with default props, TransferList no longer lets a mouse drag selected rows to reorder (the old isReorderable defaulted to true), so gate the change to touch input or cite the decision that covers it in the description.
  • The List reorder engine gives a pen on a desktop (fine primary pointer) no drag at all (listReorderEngine.tsx:887), while current spec:AST-103 FR3 says such a pen drags without a hold; fix it in #8139 with a browser test for a pen on a fine and on a coarse primary pointer.
  • spec:AST-103 FR2 asks for the measured cost to be recorded in the pull request, and the description records none for this head, so add List's and TransferList's import size with and without reorderable.

One non-blocking comment suggestion is inline.

[Automated review]

return () => {};
}
const onPointerDown = (event: PointerEvent) => {
if (event.pointerType !== 'mouse' || event.button !== 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

spec:AST-103 FR3 says "with a mouse, or a pen where the primary pointer is fine, a drag from the handle starts without a hold". This returns for every pointer but a mouse, and the touch hold attaches only under the touch presentation, so a pen on a desktop gets no drag. Accept pen here when the primary pointer is fine, and add a browser test for a pen on a fine and on a coarse primary pointer (FR3's verification row).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in #8139 (9a1afa9), where the engine lives: a pen drags at once where the primary pointer is fine. Its browser proofs cover a pen on a fine and on a coarse primary pointer.

}
},
[announce, isReorderable, setReorderSession, t],
// The selected panel's rows, in order, as List reports a move among them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking (INV29): this first sentence describes nothing in isOrdering. Suggest keeping only the reason: "Reordering is off while a search filters the panel. A hidden row could otherwise move past the shown rows."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Taken in fddf9c6.

@cixzhang

cixzhang commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

/approve-spec fddf9c6

A List that takes the handle useAnimatedLayout returns animates its row
changes on one shared motion, for every pointer. An inserted row enters,
a removed row collapses in place, and a row that crosses rows moving the
other way rides above them. The rows after a change, and the content
after the list, glide to their new places. Rows match across renders by
their itemKey; a row without one changes at once.

The row marks the update module writes (item key, leaving, travelling)
live in their own private-attribute owner modules. A list under the
handle isolates for every pointer, so a sliding row stays below a
page-level sticky header.

The List and ListItem records now say the row updates and itemKey are
implemented, and their verified_by lists the files that verify them.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested. TransferList's own switch to createReorderable follows spec:AST-103 and the direction given on this pull request, and the desktop pen drag and the measured bundle cost are now in, but the branch as issued can't land yet.

Blocking:

  • The compare carries 12,292 authored lines, 10,166 of them from the open #8106, #8139 and #8337, and the stack declaration does not hold because #8106 has no Stack: line and #8163's line differs, so add the identical Stack: #8106 → #8139 → #8337 → #8143 → #8163 line to both (or land the predecessors first) so this pull request carries only TransferList's 2,132-line increment.
  • Desktop behavior change needs an owner decision: with default props, TransferList no longer lets a mouse drag selected rows to reorder (the old isReorderable defaulted to true, TransferList.tsx:135), so gate the change to touch input or cite the decision that covers it in the description.

One non-blocking comment suggestion is inline.

[Automated review]

Comment on lines +1900 to +1901
* strings and presentation current, and attaches the list's listeners. The
* touch hold attaches only where the primary pointer is coarse. It also

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Non-blocking (comment): this, and the file header at lines 52-53, still say the hold attaches only where the primary pointer is coarse, but attachTouch(isTouch) below now attaches it under every primary pointer. Suggested wording: "It attaches the list's listeners. The touch hold takes a finger under every primary pointer, and a pen where the primary pointer is coarse."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in #8139 (5e893e4), which this stacks on: the Host doc now says the hold attaches under every primary pointer, and the file header names the finger and the pen.

@cixzhang

cixzhang commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 82a3af6

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Changes requested. TransferList's own switch to createReorderable follows spec:AST-103 and the direction given on this pull request, but the branch as issued can't land yet.

Blocking:

  • The compare carries 12,378 authored lines, 10,220 of them from the open #8106, #8139 and #8337, and the stack declaration does not hold because #8163's Stack: line differs and its branch does not contain this head, so put the identical Stack: #8106 → #8139 → #8337 → #8143 → #8163 line in #8163 and rebase it onto this pull request (or land the predecessors first) so this pull request carries only TransferList's 2,164-line increment.
  • Desktop behavior change needs an owner decision: with default props, TransferList no longer lets a mouse drag selected rows to reorder (the old isReorderable defaulted to true, TransferList.tsx:135), so gate the change to touch input or cite the decision that covers it in the description.
  • The doc page's new "Migrating from isReorderable to reorderable" example calls createReorderable({...}) with no type argument, so its Reorderable<string | number> fails to type-check against TransferList's reorderable (TransferList.doc.mjs:594); write createReorderable<string>(...) there and in the description's migration snippet.
  • Miss: the "Independent transfer and reorder constraints" example has the same type error against TransferListSelector (TransferList.doc.mjs:556), so type its call the same way.

[Automated review]

* search filters the panel. Without it, the selected options keep their
* order.
*/
reorderable?: Reorderable<T>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Desktop behavior change needs an owner decision: with default props, a mouse can no longer drag selected rows to reorder (the old isReorderable defaulted to true). Gate it to touch input, or cite the decision that covers it in the description.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The description now cites the owner basis: "Owner rule, Nov 4 2026 (Cindy): anything Lab-only gets an OK."

// AFTER: reordering is opt-in through the object createReorderable makes.
const reorderable = useMemo(
() =>
createReorderable({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Without a type argument this object is Reorderable<string | number>, which TransferList's reorderable?: Reorderable<T> refuses (TS2322). Every story and test writes it typed:

    createReorderable<string>({

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Typed as createReorderable<string> in 164d15d, and in the description's snippet.

code: `<TransferListSelector
code: `const reorderable = useMemo(
() =>
createReorderable({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Miss: the same untyped call here gives TransferListSelector a Reorderable<string | number>, which its reorderable?: Reorderable<T> refuses (TS2322). Type it as createReorderable<string>({...}).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Typed the same way in 164d15d.

vjeux added 4 commits November 5, 2026 04:00
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.

Under a finger, a 150 ms hold on a handle picks its row up, and a
flick on it still scrolls. The rows stay still while a dimmed source,
a preview and an insertion line show the move. On release they glide
on the shared settle role. A pen holds the same way where the primary
pointer is coarse. A mouse, or a pen where the primary pointer is
fine, drags a handle at once. A click, Space or Enter turns on reorder
mode: the insertion line marks the row's place, the arrow keys move
the row and announce its place, and Escape ends the mode.

The reorder's machinery ships with the object, so a List without
`reorderable` loads none of it. The UIKit table's reorder values and
the drop model come in here with their first user. With an
animatedLayout handle, a drag's commits stay the reorder's own. The
CLI lists createReorderable with the hooks.
While a reorderable List row is in reorder mode, a hint anchored below
its handle shows Move up, Move down and Done until the mode ends. Each
move does what its key does, with the same announcement, and leaves the
focus on the handle. Done ends the mode as Escape does, keeping the
moves. So a pointer or a finger can reorder by taps alone, as WCAG
2.5.7 asks. A screen reader that moves its focus into the hint keeps
the mode, and the focus goes back to the handle.

The hint shares the keyboard hint's layer: hooks/anchoredHint.tsx holds
the anchored hint both render, and useKeyboardHint renders its hint
through it unchanged. The hint joins the shared dismissal stack, so an
Escape in it ends the mode and leaves a Dialog below it open.
A row named by its place, as ListInput names its records, gets a new
name after each move. The handle now records its name apart from its
registration, so a new name leaves the row in reorder mode. A move is
announced by the names the rows have when it lands.

Comments now say that a drag ends reorder mode with its announcement,
and test comments match the hold and blur behavior.
The selected panel is a core List with `reorderable`, its rows
ListItems keyed by value, each ending with core's reorder handle. The
panel's own pointer drag, preview, grip and keyboard session go. A
finger holds the handle a moment before it drags, so a flick scrolls
and a pinch zooms. Reorder mode moves a row from the keyboard or the
hint's buttons.

TransferList and TransferListSelector take the owner's object as
`reorderable`: `isReorderable(value)` says which values move, and each
move goes to `onReorder({key, to})`. A staged selector aims the moves at
its draft, so Cancel drops them. An adapter in core's internal reorder
entry runs the owner's engine with those moves, and the owner's
`start(key)` still finds the panel's list.
@cixzhang

cixzhang commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

/approve-spec 164d15d

This branch was successfully deployed

1 active deployment
Preview — 164d15de Deployed Nov 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci:run-a11y Run the accessibility suite on this PR; write access, next push (AST-030 FR12) CLA Signed This label is managed by the Meta Open Source bot. needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants