Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
2d10275 to
47a5ca9
Compare
There was a problem hiding this comment.
Approval is waiting on an engineering-owner decision. No current spec governs List.onReorder, ListItem.isReorderDisabled or the UIKit reorder model (rows slide aside, springs kept under Reduce Motion, no insertion line), so an owner needs to settle that contract and which record governs it: an amended List contract, or spec:AST-094 with its draft requirements approved. Once the governing record states it, any engineering owner, the author included, can record the decision by commenting /approve-spec <full head SHA>. These still need changes whatever that decision is:
- The diff against
mainis about 31,000 authored lines from the predecessor stack. Land those pull requests or update the branch so this one carries only the reorder change. - Keyboard focus on a reorder handle draws two focus rings, the handle's and the whole row's, and the handle's ring is clipped at the list's edge.
- The stacked touch row minimum heights change default List row geometry, which the List contract's FR2 says
paddedpreserves.
[Automated review]
PR Analysis Report
Generated by PR Enrichment workflow | View current CI run |
47a5ca9 to
d67ffca
Compare
…al List reorder
There was a problem hiding this comment.
Approval is waiting on an engineering-owner decision: no current spec governs List.onReorder, ListItem.isReorderDisabled or the UIKit reorder model (rows slide aside, springs kept under Reduce Motion, no insertion line), so an owner needs to settle that contract and which record governs it, an amended List contract or spec:AST-094 with its draft requirements approved. Once the governing record states it, any engineering owner, the author included, can record the decision by commenting /approve-spec <full head SHA>. These still need changes whatever that decision is:
- The diff against
mainis about 31,000 authored lines from the predecessor stack; land those pull requests or update the branch so this one carries only the reorder change. - Keyboard focus on a reorder handle draws two focus rings, the handle's and the whole row's, and the handle's ring is clipped at the list's edge.
- The stacked touch row minimum heights change default List row geometry, which the List contract's FR2 says
paddedpreserves. spec:AST-094FR7 cites an AST-090 record that does not exist in the repository.
[Automated review]
|
On the predecessor stack: this pull request is stacked, as its first line says, and only its last commit (d67ffca, 10 files, +1,703 −7) is its own. It rebases onto main as each predecessor lands, and the diff then shrinks to that commit. |
|
Posted by Cindy's agent on her behalf. Parts of this PR re-implement behavior that already exists, or is being specified, as a shared part of core. Converging keeps one implementation per behavior:
This is about where the behavior lives, not about the PR's intent. |
|
Agreed: one implementation.
The reorder also now keys its motion on the resolver's touch presentation ( |
|
/approve-spec ee13b42 |
There was a problem hiding this comment.
This pull request is reviewed as part of its stack; see the review on #8163.
There was a problem hiding this comment.
Changes requested. The List reorder follows spec:AST-103 and the reorder design, and both its drag and its reorder mode work, but two layout reads need fixing before it can land.
- Miss:
showLine(packages/core/src/List/listReorderEngine.tsx:297-302) readsrow.offsetWidthright after writing the line's--astryx-reorder-line-start, so each place a drag crosses, and each reorder-mode move, lays the page out again, whicharchitecture:react-component-runtimeINV18 rules out; read the dragged row'soffsetLeftandoffsetWidthonce when the drag begins (and with the row's other reads inplaceModeLine) and pass them in. - Newly live check: #8106's
measure, which this branch carries (packages/core/src/List/listLayoutUpdates.ts:461-482), readsgetBoundingClientRectinrowsMeetingand in eachmeasureBefore()after writing the settles, so a change with a swiped row near the view lays the page out an extra time (INV18); fix it in #8106 by taking the visible range from the first pass and measuring before the first settle is written, then bring that fix in here.
Two smaller notes are inline. The stack lines in #8143 and #8163 still build on this branch's previous commit, so rebase them once this head settles. This pull request lands only after #8106.
[Automated review]
| inlineSize: `${row.offsetWidth}px`, | ||
| boxSizing: 'border-box', | ||
| boxShadow: 'none', | ||
| opacity: MOVING_OPACITY, |
There was a problem hiding this comment.
Non-blocking: this opacity applies to the preview's background too, so the row under the preview shows through it (mid-drag, "Sent" and "Spam" read on top of each other), although the background is there to cover it (lines 1120-1123). Dimming the preview's content and keeping its background opaque would keep both legible.
There was a problem hiding this comment.
Approved pending CI. CI has not run on this head yet: the pull request is a draft from a fork, so CI runs once it is marked ready for review and a maintainer adds the ci:run label.
The earlier blocker is fixed: the insertion line now reads the dragged row's span once at pick-up, and the new trace spec shows no forced style or layout pass as a drag crosses rows (it fails with the previous engine).
Non-blocking:
- In right-to-left, when rows are inset more at one inline edge than the other (
edge="outset"in a host with unequal inline padding), the moving copy and the insertion line sit offset from the row by the difference; details are inline. - The earlier notes on the preview's opacity, the
reorderPickedUptranslator note and theanchoredHint.tsxheader still stand.
This pull request lands only after #8106, and the layout read in #8106's measure is judged on #8106.
[Automated review]
| * The span of `row` in `list`'s coordinates, read before anything in the | ||
| * frame writes a style. The list may not be the row's offset parent yet. | ||
| */ | ||
| function lineSpanOf(row: HTMLElement, list: HTMLElement): LineSpan { |
There was a problem hiding this comment.
Non-blocking (missed in earlier reviews): this span is measured from the list's left edge, but showLine writes it to inset-inline-start, which counts from the right in RTL, and previewOf does the same with row.offsetLeft (line 276). With edge="outset" in a host whose inline padding is 48 px at the start and 8 px at the end, an RTL row spans 17–535 px while the line and the moving copy span 57–575 px. In RTL, write list.clientWidth - (start + size) instead, for both. The new RTL spec case passes only because its rows fill the list.
There was a problem hiding this comment.
This pull request is reviewed as part of its stack; see the review on #8163.
There was a problem hiding this comment.
This pull request is reviewed as part of its stack; see the review on #8163.
There was a problem hiding this comment.
This pull request is reviewed as part of its stack; see the review on #8163.
|
/approve-spec 76cd760 |
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.
createReorderable's discovery rule lists a create*.doc.mjs documented as a hook. createAnimator, createPanGesture and createDragToSnapPoints are documented as listed beside their own hooks, and each names that hook in relatedHooks. Such a factory now stays out of the hook list, so hook --list matches main for them and adds only createReorderable. The CLI's search goldens match main's again.
Five comments in the reorder's engine, touch glide, drop model and UIKit table each held one sentence over 25 words, and the reorder mode spec's header ran two sentences together. Each is now split, with the same meaning.
The list attaches its hold with manageStyle: false, so its handles keep their own touch-action. The gesture host then leaves the hold's selection lock to the list as well, and the list brought none. On iOS a still finger on a handle started a text selection on the row's label, and the drag grew it across the rows. The list now locks selection on itself from a finger's or a pen's touch-down on a handle until it lifts, through utils/selectionLock. A mouse never takes the lock.
The insertion line read the dragged row's offsetLeft and offsetWidth right after writing the line's position, at each crossing and each reorder-mode move. Each read forced a style pass. The drag now reads the row's span once at the pick-up, in the list's coordinates and beside its other measurements, and reorder mode reads everything before its first write. A comment on the touch hold now says that the list locks text selection itself.
Main moved touchHitArea.stylex from utils to internal. The List reorder handle and its engine now import it from there.
|
/approve-spec 13fab2d |
There was a problem hiding this comment.
Approval waits on an engineering owner's decision, and three fixes are needed.
Edge autoscroll during a reorder drag (listReorderEngine.tsx:1281-1319, reorderModel.ts autoscrollStep, clampTop) is behavior that no current record states: spec:AST-103 FR3 does not mention it, and spec:AST-094 FR30 covers menus only. An engineering owner records the decision by commenting /approve-spec <full head SHA> once the governing record states it (the author may do so as an engineering owner), or the drag stops scrolling.
Blocking findings:
- In right-to-left, a dragged row with an inline-start margin draws its preview and insertion line shifted by that margin (a row indented 32 px gets both 32 px past its edge), because both use the physical
offsetLeftasinset-inline-start(listReorderEngine.tsx:276and:303-305,ListReorderHandle.tsx:201); place them with the physicalleft, or convert under RTL, and add an indented RTL case. - Miss: picking a row up lays the page out twice before its first frame, because
previewOfreadsoffsetLeftandoffsetWidthafterbeginwrites the rows'translateand the list'sposition(listReorderEngine.tsx:1151-1159,:276-277), andtrack'sseton the new preview reads its style again (:1234); size the preview from thelineSpanread at:1116and start it where the firsttrackputs it (architecture:react-component-runtimeINV18). - Regression:
isScrollContainerandscrollerOf(listReorderEngine.tsx:358-363,:401-418) copy the walkutils/scrollingAncestor.tsnow shares; callscrollingAncestor(list)and keep only the band reads here.
CI has not run on this head (a draft from a fork); a maintainer adds the ci:run label so CI runs.
[Automated review]
| position: number; | ||
| } | ||
|
|
||
| function scrollerOf(list: HTMLElement): Scroller { |
There was a problem hiding this comment.
This walk, with isScrollContainer above it, is utils/scrollingAncestor.ts scrollingAncestor plus the document fallback that keyboard/keyboardTarget.ts nearestScroller adds. Call scrollingAncestor(list) and keep only the band and position reads here, so a fix to the shared walk reaches reorderable lists.
The reorder engine walked a list's ancestors for its scroller with its own copy of the walk that scrollingAncestor shares. nearestScroller, which wraps that walk and falls back to the document's scroller, moves from the keyboard module to utils/scrollingAncestor.ts, and the reorder takes its scroller from there. The keyboard module re-exports it, so its importers keep their imports. New browser cases hold a dragged row near a scroll edge. The page scrolls, and stops once the list's end is in view, with the preview still under the pointer. A box the list scrolls in scrolls while the page stays.
The preview and the insertion line took the row's physical left as their inline start. In right-to-left that is the right edge, so a row set in from the list's start drew both shifted by its inset. Each now converts the left to an inline start: the distance from the list's right edge in right-to-left. A new browser case drags a row set in 32 px from the list's start, in both directions, and matches the preview and the line to the row.
The pick-up read the list's position after the rows' first writes and read the dragged row's box again to place its preview once the list was positioned. Then it wrote the preview's first offset, and the preview's animator read its style back twice. Each read forced a style or layout pass before the first frame, which architecture:react-component-runtime INV18 rules out. The pick-up now makes every read first. The preview's span is measured from the list's padding edge before the list is positioned, and its first offset, none, needs no write. The preview holds its own will-change, so its animator reads no stylesheet at its first move. A new browser case traces the pick-up under a finger: six forced passes before, none after.
FR3 now states the reorder's edge autoscroll in the spec owners' words: a held drag near the scroll container's edge scrolls it toward that edge, faster the nearer it is, and stops at the end. Its verification row names the browser checks of a drag held near the page's edge and near the edge of a box the list scrolls in, the unit checks of the autoscroll step and its timer, and the failures they catch.
|
/approve-spec d5c3f6b |
There was a problem hiding this comment.
This pull request is reviewed as part of its stack; see the review on #8163.
The preview was a plain clone of the row, so it kept each control's name. Laid beside the row, a copied checked radio took its group from the row's own: the row's radio showed unchecked for the whole drag, and a drop in place left no radio checked. The clone that takes no part in the page moves from transientCopy.ts into its own module, inertClone.ts, with paintedBackground: names, ids and labels stripped, no form, inert and hidden. The swap's and the layer exit's copies keep it through inertCopy, and the transient copy mechanism stays theirs. The preview is now that clone, and its background comes from paintedBackground. The clone reads the row's styles, so it is made before the pick-up's first write. A boundary test keeps the clone to the copy mechanism and the reorder's preview. A new browser case drags a row whose radio is checked: the radio showed unchecked during the drag before, and stays checked now, through a drop in place.
|
/approve-spec c010af7 |
The drag placed its preview and line from the pick-up's snapshot, plus its own autoscroll. A wheel or trackpad scroll during a mouse drag moved the rows under a still pointer: the preview and the line rode with them, and the drop landed where the pointer had been. The engine now watches its scroller's scroll events through subscribeInput, and skips the ones its own autoscroll causes. After any other, it reads the scroller's offset once, before its writes: at the next move, the next autoscroll tick or the drop, or else in a later frame once the writes so far have rendered. So the read forces no layout. The change in the offset joins the drag's own scrolled distance, and the preview and the line are placed again. Two new browser cases wheel three rows mid-drag. On the previous head the preview and the line rode 111 px from the pointer; now both stay under it, and the drop lands three rows further. A layout trace of the wheel with the pointer moving finds no forced pass, and at most one offset read per scrolled frame. A build that reads in the scroll event itself forces a style pass there.
INV18 names one more read a scroll path may make, in the spec owners' words: during a reorder drag, the engine reads its scroller's scroll offset once per frame after a scroll it didn't cause, before its writes. A verification row names the browser checks: a wheel scroll mid-drag keeps the preview and the drop line under the pointer, with no forced layout and one read per such frame.
|
/approve-spec a395e20 |
There was a problem hiding this comment.
This pull request is reviewed as part of its stack; see the review on #8163.
Stack: #8139 → #8143 → #8163, on main (#8106 has merged)
#8106 has merged, so this sits on main and its own commits are the whole diff. It holds fifteen commits: List's reorder (be86903); reorder mode's hint, with its move buttons and Done (03572ec), which was #8337, folded in as the stack review asked; a move that renames a row keeps reorder mode (d5b4b15); the CLI lists a
create…factory beside its own hook (3ceafec); comment sentences split at 25 words (5d5acf7); a held finger selects no text (bb24139); a drag reads no layout (7d5ca01); the touch hit area's import from its new place (13fab2d); and the stack review's fixes with their two records commits (a025edb, aa6f013, 6ff0ba0, d5c3f6b, c010af7, 5668583, a395e20).Why
People reorder playlists, to-do lists and settings on their phones.
Listcan't do it, so apps build their own, often without a keyboard or screen-reader path.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-103defines a reorderable List:createReorderable({onReorder})makes the behavior,Listtakes it asreorderable, and eachListItemwith anitemKeygets a handle.onReorder({key, to})once.This builds
spec:AST-103FR1 (butpositions()andlabels) and FR2 to FR8. AST-103 gains "Implementation gap: not built" notes for FR1'spositions()and FR4b'sattach, beside itslabelsnote.component:List's readiness line saysreorderableis stable and built, but for itslabelsandpositions.family:overlay-dismissallists the hint among its members. The UIKit table's measured reorder values and the drop model come in here with their first user. With ananimatedLayouthandle, a drag's commits stay the reorder's own. The CLI listscreateReorderablewith the hooks.Options and tradeoffs
Why this choice
design:ordered-collection-reorderingchose still rows and a line, because they need no layout work during the drag and look the same everywhere. The hold is UIKit's measured 150 ms. Only the handle is added (spec:AST-103FR5), so rows keep their layout. DEC-5 chose the hint, because it shows the moves where the row is, without opening a menu. It is the keyboard hint's own layer, and each button's tap area grows toward 44 px on touch through the shared touch hit area. The API isspec:AST-103FR1's: one behavior object, with no hook twin.Rebuilt on current main
This head is the change rebuilt on current main, which now has #8106. Five commits follow it:
create…factory documented as a hook lists with the hooks unless its doc names its ownuse…hook inrelatedHooks. Then it is listed beside that hook. Soastryx hook --listadds only createReorderable; createAnimator, createPanGesture and createDragToSnapPoints stay beside their hooks, as their docs say.manageStyle: false, so its handles keep their owntouch-action, and the gesture host then leaves the hold's selection lock to the list, which brought none. The list now locks selection on itself from a finger's or a pen's touch-down on a handle until it lifts, throughutils/selectionLock; a mouse never takes the lock. A unit test and a Chromium touch case fail without it.architecture:react-component-runtimeINV18: a per-frame path reads no layout, never interleaved with writes). A new Chromium case traces the crossings of a three-row touch drag: on the previous head each crossing forced a style pass, 4 in all; on this head none.touchHitArea.stylexfromutils/tointernal/, so the reorder handle and its engine import it from there.The stack review's fixes
spec:AST-103FR3. FR3 gains their bullet verbatim, "a held drag near the scroll container's edge scrolls it toward that edge, faster the nearer it is, and stops at the end", and a verification row with its failure expectation. The engine keeps UIKit's measured autoscroll. It starts with the dragged row's edge 8 px inside the scroll edge, at 2 px per 20 ms tick (100 px/s), and rises to 10 px per tick (500 px/s) with that edge 22 px past it. The steps were measured on aUITableViewon the iOS 26.2 simulator; the start inset is a guess inside the measured bracket of 3 to 12 pt.nearestScroller, which wraps that walk and falls back to the document's scroller, moves from the keyboard module toutils/scrollingAncestor.ts, and the reorder takes its scroller from there. The keyboard module re-exports it, so its importers keep their imports. The reorder's modules gain no keyboard module, a List withoutreorderablebundles to the same bytes, and the keyboard target's bundle does not grow.motion/transientCopy.tsinto its own module,motion/inertClone.ts, withpaintedBackground. The swap's and the layer exit's copies keep building on it, and the transient-copy mechanism stays theirs (architecture:react-component-runtimeINV12's boundary test, which gains a case naming the clone's users). The preview is now that clone, with its background frompaintedBackground; both read before the pick-up's first write. A row holding a canvas that reads back empty drags as its bare box.architecture:react-component-runtimeINV18 gains their clause with a verification row: "during a reorder drag, the engine reads its scroller's scroll offset once per frame after a scroll it didn't cause, before its writes". A known limit: while autoscroll runs, a wheel delta that coalesces with the engine's own scroll in one frame is undone by the next tick's write.will-change(architecture:react-component-runtimeINV18).How to check it
In Storybook
Core/List, on a phone or with touch emulation:Gates at the stack's top, 87f5aad (#8163, merged with main), which carries this head:
check:knowledge,check:repo,check:i18n-catalog,check:i18n-runtime,check:use-client,check:package-boundariesand the changeset check.pnpm install --frozen-lockfile: List,createReorderable, motion, Layer, Core's utils and internal entries, the keyboard module, Button, the CLI's hook discovery, every Lab test and the touch-support bundle check, 5,271 of 5,271. The full UI project (22,409 tests) and the full node project (17,368 tests) pass.Bundle cost
Measured with the PR size report's build options on the built package (the eager chunks, minified, gzip): List and ListItem import 70.0 KB. With
createReorderablethey import 114.8 KB, so the reorder adds 44.8 KB where it is used. A List withoutreorderableimports no reorder, gesture or animator module, only the small module that names the object's key. The stack review's fixes add about 0.9 KB gzip to the reorder's modules, and nothing to a List withoutreorderable: the same minified bytes (measured on the source with the same options).Writing rubric results
Over
component:List(the readiness line andverified_by),spec:AST-103(two gap notes, and FR3's edge-autoscroll bullet with its verification row),family:overlay-dismissal(the hint joins the members) andarchitecture:react-component-runtime(INV18's reorder scroll read with its verification row):Pull request size
This is a behavior-module pull request: 9,235 authored lines in 57 files, without the generated API report: 4,678 of source, 4,125 of tests, and the rest stories, strings, docs and records. The reorder engine, its touch layer, its model, its handle and its hint work only together, so they land as one module.
Evidence
On the iOS simulator, a held drag on a handle before and after the selection lock: the long list before and after, and the ListInput stress story before and after. Before, the loupe and a blue selection show in 2 of 3 takes of the long list and 1 of 2 of ListInput; after, none in 3 and 2 takes.
On iOS 26.2 Safari: a held drag, the drop with the main thread blocked, and tap, Move down, Done. The last was captured before Done took the hint's text role, so Done there is darker and bolder than it is now.