Skip to content

feat(carousel): add hasLoop wrap-around and handleRef imperative control - #4629

Open
freddymeta wants to merge 1 commit into
mainfrom
feat/carousel-loop-handle
Open

feat(carousel): add hasLoop wrap-around and handleRef imperative control#4629
freddymeta wants to merge 1 commit into
mainfrom
feat/carousel-loop-handle

Conversation

@freddymeta

Copy link
Copy Markdown
Contributor

Summary

Adds two capabilities to Carousel, both re-expressed in terms of its existing native-scroll machinery (no scroll engine introduced):

hasLoop?: boolean (default false)

Wrap-around scrolling. Pressing Next at the end returns to the start, and Prev at the start jumps to the end — for both the built-in navigation buttons and the imperative handle. The buttons stay active at both edges instead of hiding, since a scroll is always available. Only engages when the content actually overflows (a single-screen carousel is a no-op).

handleRef?: React.Ref<CarouselHandle>

An imperative handle for programmatic scroll control, following the established handleRef + {Name}Handle convention used by Tokenizer, Calendar, PowerSearch, and others (keeps the DOM ref intact).

interface CarouselHandle {
  scrollNext(): void;
  scrollPrev(): void;
  scrollTo(index: number): void;   // scrolls the item to the start edge; clamped
  canScrollNext(): boolean;        // live state
  canScrollPrev(): boolean;
}
const carousel = useRef<CarouselHandle>(null);

<Carousel handleRef={carousel} hasSnap hasButtons={false}>
  {items.map(i => <Card key={i.id} {...i} />)}
</Carousel>
<Button label="Next" onClick={() => carousel.current?.scrollNext()} />

Design notes

  • Wrap-around, not clone-based infinite. hasLoop implements bounded wrap-around by overshooting toward the opposite edge and letting the browser clamp — the honest native-scroll equivalent of a looping carousel. Clone-based seamless infinite scrolling is intentionally out of scope; the prop is forward-compatible with a future 'infinite' mode if it's ever needed.
  • canScroll* are methods, not getters. Matching how other handles expose reads (getValue(), getCollapseState()), they read live overflow state so callers can query them from an event handler.
  • scrollTo(index) stays contained. It scrolls the carousel container by a measured delta rather than calling scrollIntoView, so it never moves ancestors or the page. RTL-aware.
  • RTL and prefers-reduced-motion behavior is inherited from the existing scroll logic.
  • The interface stays compact (11 props); the legacy component is untouched.

Testing

  • packages/core/src/Carousel/Carousel.test.tsx: 23 tests pass (7 new — loop wrap-around both directions, buttons stay enabled at edges under loop, no-wrap when content fits, handle scrollNext/Prev/scrollTo, live canScroll*, loop reachability).
  • Typecheck clean, lint clean, sync/exports/changeset checks pass.
  • Storybook: added Loop and Imperative Control stories.

Add two capabilities to the native-scroll Carousel:

- hasLoop: wrap-around scrolling. Next at the end returns to the start and
  prev at the start jumps to the end, for both the built-in buttons and the
  imperative handle. Navigation buttons stay active at both edges. Only
  engages when the content overflows.
- handleRef: an imperative handle (CarouselHandle) exposing scrollNext,
  scrollPrev, scrollTo(index), canScrollNext() and canScrollPrev() for
  programmatic control, following the established handleRef convention.

Both are re-expressed in terms of the existing native-scroll machinery
(scrollBy + useScrollOverflow), so no scroll engine is introduced. RTL and
reduced-motion behavior is inherited from the existing scroll logic.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Aug 1, 2026
@github-actions github-actions Bot added community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge needs:design-review Affects visuals — Design should review labels Aug 1, 2026
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview Aug 1, 2026 6:32am

Request Review

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

Modified Components

Carousel (@astryxdesign/core) · View in Storybook
Metric Before After Delta
Bundle Size (ESM) N/A N/A N/A
Lines of Code N/A 423 -
Complexity N/A Very High (48) -

Bundle Size Summary

Package Size (ESM) Size (CJS) Gzipped
@astryxdesign/core N/A 4.9KB 1.3KB

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

github-actions Bot added a commit that referenced this pull request Aug 1, 2026
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. community Authored by a community contributor (not on the eng/design team) needs:code-review High-risk change (new package/component/API) — needs human code review before merge needs:design-review Affects visuals — Design should review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant