Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
There was a problem hiding this comment.
This spec needs an engineering owner's decision before it can be approved, and some record fixes are needed either way.
Approval waits on an engineering owner approving the new Carousel snap contract (FR5–FR10, AR3, DEC-2, DEP-0018, CLN-0029) and the spec:AST-094 FR3 and DEC-25 edits, ruling OQ1 (is a snap-paging page one item or one view?), and deciding whether snap: boolean | SnapPaging stands as an exception to the <thing>Behavior rule in architecture:public-component-api INV2/INV3. An engineering owner records that decision by commenting /approve-spec <full head SHA> on this pull request, and the author may do so as an engineering owner.
Blocking:
- Carousel FR7 turns snap paging off for a fine-pointer trackpad, but
spec:AST-094FR43, which this pull request leaves unchanged, says gestures attach for every pointer and a pager follows a trackpad swipe, so FR7 or FR43 needs to change. spec:AST-107FR4 still nameshasSnapas Carousel's path and keeps everything else in the gesture and motion modules internal, so it needs amending forsnap,useSnapPagingandcreateSnapPaging.- FR6 requires
scrollend,IntersectionObserverandinline mandatory, which shipped Carousel does not use (it readsuseScrollOverflowand setsx mandatory), so please state the observable rule instead and drop "from an effect" from FR8. review_triggerslistsapiandbundle, which are not supported triggers and are dropped by the routing tools, so usepublic-api.- FR7 is marked
proposedinside a current record; once OQ1 is ruled, mark itsettledand fold OQ1 into DEC-2.
Smaller: DEC-2 narrates the decision ("On 2026-11-02 the owner decided…") and carries survey sentences and an unsourced "about 40 KB" figure, which the writing rubric moves out of the record.
[Automated review]
| | FR4 | An edge scroll button that is available behaves as an ordinary Button. | `family:buttons` | settled | | ||
| | FR5 | `snap` takes `boolean \| SnapPaging`. Absent or `false`: the scroll area does not snap. `true` (written `snap`): native snap, FR6. A `SnapPaging` value: snap paging, FR7. `SnapPaging` is opaque and made only by `useSnapPaging`. A new `SnapPaging` value on a later render does not restart a settle or attach the gesture again. | DEC-2 | settled | | ||
| | FR6 | Native snap uses CSS scroll snap only: `scroll-snap-type: inline mandatory` on the scroll area, `scroll-snap-align: start` on each direct child, and `scroll-padding` matching the carousel's padding. Every item is a stop, also when several items show at once, and the platform's own momentum picks where a swipe rests. The edge buttons scroll with `scrollBy`, and the current position comes from `scrollend` and `IntersectionObserver`. Native snap adds no gesture code. It does not depend on `::scroll-button()`, `::scroll-marker`, `:target-current`, scroll-state container queries, snap events, or `interactivity: inert`; one of these MAY be added under `@supports` only where it changes no behavior, name, role, or focus order. | DEC-2 | settled | | ||
| | FR7 | Snap paging uses native snap's stops (FR6): each direct child's start edge, measured from the inline start and mirrored in right-to-left layout, with the last stops clamped where the row ends so no empty space shows. A gap sits between stops and is never a stop. Under a finger, and under a trackpad where the primary pointer is coarse, a release picks its rest stop by `spec:AST-094` FR3's paging rule, with one stop as one page: at most one stop from the stop the drag started on. The settle starts as `spec:AST-094` FR6's paging paragraph says. Where the primary pointer is fine, a mouse or trackpad keeps native snapping. An edge button steps about one view: the viewport width less half an item, landing on the nearest stop and moving at least one stop. Open question OQ1 asks whether one page stays one item. | DEC-2; `spec:AST-094` FR3, FR6, DEC-25 | proposed | |
There was a problem hiding this comment.
This turns snap paging off for a fine-pointer trackpad, but spec:AST-094 FR43 (unchanged here) says "Gestures attach for every pointer, a mouse, a pen, and a finger, not only a coarse primary pointer" and "A pager follows a trackpad swipe". Either follow FR43's trackpad pager path here, or amend FR43 with this exception and its reason. The FR7 verification row ("a fine-pointer scroll that runs the pager fails") needs the same decision.
| | FR3 | The handoff happens once per edge transition, and only while the edge button held focus. An edge reached by swipe, resize, or wheel while focus is elsewhere moves nothing. | `architecture:interaction-modality` INV8 | settled | | ||
| | FR4 | An edge scroll button that is available behaves as an ordinary Button. | `family:buttons` | settled | | ||
| | FR5 | `snap` takes `boolean \| SnapPaging`. Absent or `false`: the scroll area does not snap. `true` (written `snap`): native snap, FR6. A `SnapPaging` value: snap paging, FR7. `SnapPaging` is opaque and made only by `useSnapPaging`. A new `SnapPaging` value on a later render does not restart a settle or attach the gesture again. | DEC-2 | settled | | ||
| | FR6 | Native snap uses CSS scroll snap only: `scroll-snap-type: inline mandatory` on the scroll area, `scroll-snap-align: start` on each direct child, and `scroll-padding` matching the carousel's padding. Every item is a stop, also when several items show at once, and the platform's own momentum picks where a swipe rests. The edge buttons scroll with `scrollBy`, and the current position comes from `scrollend` and `IntersectionObserver`. Native snap adds no gesture code. It does not depend on `::scroll-button()`, `::scroll-marker`, `:target-current`, scroll-state container queries, snap events, or `interactivity: inert`; one of these MAY be added under `@supports` only where it changes no behavior, name, role, or focus order. | DEC-2 | settled | |
There was a problem hiding this comment.
Shipped Carousel reads its edges from useScrollOverflow (a scroll listener plus the shared resize observer) and sets scrollSnapType: 'x mandatory', so pinning scrollend, IntersectionObserver and inline mandatory makes today's hasSnap non-conforming, with no gap listed. Stating the observable rule instead (edge-button state follows every scroll, momentum and programmatic included) keeps the equivalent implementation valid.
| approved_at: 2026-10-22 | ||
| owners: [cixzhang] | ||
| review_triggers: [behavior, accessibility] | ||
| review_triggers: [behavior, accessibility, api, bundle] |
There was a problem hiding this comment.
api and bundle are not supported review triggers: pnpm specs:for drops them, and the baseline routing script rejects api. The supported name is public-api, so [public-api, behavior, accessibility] routes what this change meant to route.
PR Analysis Report🔎 Preview ·
|
|
/approve-spec 1bd9215 |
There was a problem hiding this comment.
This spec is owner-approved at this head; two record fixes are still needed before it merges.
Blocking:
- Carousel FR6 requires the position to come from
scrollendandIntersectionObserver(withinline mandatory), which shipped Carousel does not use, while FR5 and the at-a-glance table say native snap is unchanged and no gap is listed, so please state the observable rule (or list the gap) so today'shasSnapstays conforming and no second position tracker is needed. review_triggerslistsapiandbundle, which the routing tools reject or drop, so please usepublic-api.
Smaller: the new INV3 entry names only the third state, not the INV2 union rule or the per-device paging value; DEC-2 and that entry still narrate the decision ("On 2026-11-02 the owner decided…"); and the "about 40 KB" figure has no measurement behind it.
[Automated review]
| | FR3 | The handoff happens once per edge transition, and only while the edge button held focus. An edge reached by swipe, resize, or wheel while focus is elsewhere moves nothing. | `architecture:interaction-modality` INV8 | settled | | ||
| | FR4 | An edge scroll button that is available behaves as an ordinary Button. | `family:buttons` | settled | | ||
| | FR5 | `hasSnap` takes `boolean \| SnapPaging`. Omitted or `false`: the scroll area does not snap. `true` (written `hasSnap`): native snap, FR6, exactly as released, with no gesture code. A `SnapPaging` value, made only by `useSnapPaging`: snap paging, FR7; the carousel snaps on native snap's per-item stops and the pager settles on those same stops. `true` never pages. A new `SnapPaging` value on a later render does not restart a settle or attach the gesture again. The third state is a known deviation from `architecture:public-component-api` INV3, which keeps a `has…` boolean to a true on/off (DEC-2). | DEC-2 | settled | | ||
| | FR6 | Native snap uses CSS scroll snap only: `scroll-snap-type: inline mandatory` on the scroll area, `scroll-snap-align: start` on each direct child, and `scroll-padding` matching the carousel's padding. Every item is a stop, also when several items show at once, and the platform's own momentum picks where a swipe rests. The edge buttons scroll with `scrollBy`, and the current position comes from `scrollend` and `IntersectionObserver`. Native snap adds no gesture code. It does not depend on `::scroll-button()`, `::scroll-marker`, `:target-current`, scroll-state container queries, snap events, or `interactivity: inert`; one of these MAY be added under `@supports` only where it changes no behavior, name, role, or focus order. | DEC-2 | settled | |
There was a problem hiding this comment.
Shipped Carousel sets x mandatory and reads its edges from the shared useScrollOverflow hook (a scroll listener plus the shared resize observer, also used by TabList and Table sticky columns), so pinning inline mandatory, scrollend and IntersectionObserver here makes today's hasSnap non-conforming even though FR5 calls it "exactly as released". Stating the observable rule (edge-button availability follows every scroll, momentum and programmatic included, and every resize) keeps the shared tracker valid; otherwise list the difference as an implementation gap.
Exact decision needed
How a Carousel opts into touch paging, what a plain snapping Carousel ships, and what one page is on a carousel that shows several items. All three are decided: one prop,
hasSnap={useSnapPaging()}, and a page is one view.Existing authority searched
component:Carousel;spec:AST-094FR3, FR6 and DEC-25 (the pager's paging rule);component:LightboxFR7 (gallery paging);spec:AST-057FR1 and FR10 (theswipe={createSwipe()}config prop);spec:AST-107FR6 andarchitecture:public-component-apiINV22 (hook pluscreate…factory);architecture:react-component-runtimeINV13 (no lazyimport()). Open pull requests #8146 (internal pager) and #8153 (Carousel adopting it underhasSnap) are the overlapping work: this record makes that paging opt-in. No current claim decides the opt-in, its name, or the page size.Decision
hasSnap, whose type widens frombooleantoboolean | SnapPaging(FR5). An additive patch: no rename, deprecation, or codemod.hasSnapomitted orfalse: no snapping.hasSnap/hasSnap={true}: native CSS scroll snap, exactly as released, with no pager code (FR6).hasSnap={useSnapPaging(options?)}: snap paging on the same per-item stops (FR7).createSnapPaging(element, options?)is the non-React path (FR9).{pages: 'item'}moves one item per flick. Under right-to-left, pages run from the inline start. The item option's feel is not yet measured against UIKit's group paging (evidence gap in FR7).useSnapPaging/createSnapPagingmodule imports the pager, tree-shaken by module boundary with no lazyimport(), so a Carousel without the paging config bundles none of the pager's roughly 40 KB (FR8).::scroll-button(),::scroll-marker, scroll-state queries, snap events); those may only be behavior-neutral@supportsextras (FR6).hasSnap's third state deviates from the on/off rule foris…/has…booleans inarchitecture:public-component-apiINV3. The owner accepted it on 2026-11-02 to keep one snap prop; it is listed with that record's known deviations.hasSnapprop, the rule deviation, the page size, and theuseSnap<Variant>/createSnap<Variant>naming family.Changed records
component:Carousel: at-a-glance, compatibility, ownership, public concepts (hasSnapasboolean | SnapPaging,SnapPaging,SnapPagingOptionswithpages), FR5–FR9, representative states, verification map (finger and iPad-style trackpad, default and{pages: 'item'}, preview stories, bundle test), DEC-2, open questions (none).spec:AST-094: FR3's paging paragraph and DEC-25 name a Carousel givenhasSnap={useSnapPaging()}.architecture:public-component-api: a known-deviation entry for CarouselhasSnapunder INV3.Rejected alternatives
hasSnap: two props would describe one choice.Claim scope and non-goals
Snapping and snap paging on Carousel only. Looping, mouse-drag paging, autoplay, dots and slide semantics stay uncontracted.
Evidence and implementation relationship
Implementation follows in #8146 and #8153, which will move paging behind
useSnapPagingand add view paging as the default.Writing rubric results
Scope
Validation
pnpm install --frozen-lockfilepnpm check:knowledgegit diff --check