feat(mobile): add initialPage prop to Carousel - #864
Conversation
🟡 Heimdall Review Status
🟡
|
| Code Owner | Status | Calculation | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| ui-systems-eng-team |
🟡
0/1
|
Denominator calculation
|
327254c to
f3126e3
Compare
f3126e3 to
4422d4f
Compare
4422d4f to
25d7132
Compare
hcopp
left a comment
There was a problem hiding this comment.
Thanks for the PR! Had some feedback
| ## 9.23.0 ((8/27/2026, 05:05 PM PST)) | ||
|
|
||
| This is an artificial version bump with no new change. | ||
|
|
There was a problem hiding this comment.
@caitlin-coyiuto-cb are we able to get web parity here? 🙏
| * page with no animation, and `onChangePage` does not fire on mount. Out-of-range values | ||
| * are clamped to `[0, totalPages - 1]`. | ||
| * @default 0 | ||
| */ |
There was a problem hiding this comment.
nit: can we shorten this so it is readable on our docs? Maybe something like
/**
* Zero-based page index to show on first layout. Out-of-range values are clamped.
* @default 0
*/
initialPageIndex?: number;
| loopLength, | ||
| animationApi.x, | ||
| updateVisibleCarouselItems, | ||
| ]); |
There was a problem hiding this comment.
What if we had a single useLayoutEffect instead, maybe something like
useLayoutEffect(() => {
if (didInitRef.current || totalPages === 0) return;
didInitRef.current = true;
applyPage(initialPageIndex); // here we could modify applyPage to have some sort of toggle for notifying and animation
}, [totalPages, initialPageIndex, applyPage]);
There was a problem hiding this comment.
+1 the useLayoutEffect sounds like the correct React primitive to use since our side-effect deals with visual measurements
| // Kept in sync each render so the one-time seek effect reads the value current at first | ||
| // measurement (the target page may arrive asynchronously after mount). | ||
| const initialPageRef = useRef(initialPage); | ||
| initialPageRef.current = initialPage; |
There was a problem hiding this comment.
How big of a concern is it if the prop is changed between initial load and when values are set? I think it should be fine without a ref here.
| // Kept in sync each render so the one-time seek effect reads the value current at first | ||
| // measurement (the target page may arrive asynchronously after mount). | ||
| const initialPageRef = useRef(initialPage); | ||
| initialPageRef.current = initialPage; |
There was a problem hiding this comment.
this will update the ref when the initial page changes which shouldn't matter unless the component remounts and in that instance the useRef will evaluate again
| }); | ||
|
|
||
| const [activePageIndex, setActivePageIndex] = useState(0); | ||
| const [activePageIndex, setActivePageIndex] = useState(() => Math.max(0, initialPage ?? 0)); |
There was a problem hiding this comment.
we should probably use the value from the ref here in case the initialPage prop does in fact change. If that were the case then the activePage state would also change
| if (didInitRef.current || !hasCalculatedDimensions || totalPages === 0) return; | ||
| didInitRef.current = true; | ||
|
|
||
| const target = Math.max(0, Math.min(totalPages - 1, initialPageRef.current ?? 0)); |
There was a problem hiding this comment.
const target = Math.max(0, Math.min(totalPages - 1, initialPageRef.current ?? 0));
if (target === 0) return;
const baseOffset = pageOffsets[target];
const seekOffset = isLoopingActive
? findNearestLoopOffset(carouselScrollX.current, [baseOffset], loopLength).offset
: baseOffset;
carouselScrollX.current = seekOffset;
animationApi.x.set(seekOffset);
updateVisibleCarouselItems(seekOffset);
This logic looks very similar to what we do on normal page navigation. Is there a way to cleanly share the logic?
Add an uncontrolled `initialPage` prop to the cds-mobile and cds-web Carousel so a consumer can open it on a non-zero page with no animation. Seeds `activePageIndex` at init (no `onChangePage` on mount) and performs a one-time, `didInitRef`-guarded non-animated seek on the first successful measurement — reading `initialPage` via a ref and routing through `findNearestLoopOffset` when looping. Adds `initialPage` tests and a story example on both platforms. CB1-2048 Co-Authored-By: Claude <noreply@anthropic.com>
25d7132 to
6adea0d
Compare
CB1-2048
What changed? Why?
initialPageprop to the cds-mobileCarouselso consumers can open it on a non-zero page with no animation.activePageIndexat init + a one-time,didInitRef-guarded non-animated seek on first measurement (instant.set(), no slide from page 0); readsinitialPagevia a ref since data can arrive async.onChangePageon mount; clamps out-of-range; routes throughfindNearestLoopOffsetwhen looping; a latergoToPage/drag still wins.requestAnimationFrame+goToPageworkaround (clearsTODO(CB1-2026)); unblocks the RN adoption PR (PR 2).Root cause (required for bugfixes)
N/A — feature, not a bugfix.
UI changes
Adds an "Initial Page (opens on page 3)" example to the mobile Carousel stories. Web unaffected.
CB1-2048-cds.mov
Testing
How has it been tested?
Testing instructions
Automated: New
Initial Pagetests inCarousel.test.tsx— opens oninitialPage;onChangePagenot called on mount; out-of-range clamps (99→6,-5→0); latergoToPageand drag override the initial page. Full file 97/97 pass;mobiletypecheck + lint clean.Manual:
yarn nx run expo-app:ios→ open the Carousel route → the "Initial Page (opens on page 3)" example lands on page 3 with no slide from page 1; dragging/pagination still work.Illustrations/Icons Checklist
N/A — no changes under
packages/illustrations/**orpackages/icons/**.Change management
type=routine
risk=low
impact=sev5
automerge=false