Carousel ref.scrollTo() sometimes results in unresponsive carousel

:warning: Before you continue


Before submitting a bug report, please review our troubleshooting documentation at https://developer.amazon.com/docs/kepler-tv/troubleshoot-overview.html.

If you still want to file a bug report, please make sure to fill in all the details below and provide the necessary information.

NOTE: PLEASE ONLY REPORT A SINGLE BUG USING THIS TEMPLATE.
If you’re experiencing multiple issues, please file a separate report for each.


:backhand_index_pointing_right: Bug Description


1. Summary

Provide a brief description of the bug in the SDK and its impact on app functionality.

App Name:
App Link on Amazon Appstore (found through Developer Console → Actions column in App List → View on Amazon.com):

Bug Severity
Select one that applies

  • :check_mark: Impacts operation of app
  • Blocks current development
  • Improvement suggestion
  • Issue with documentation (If selected, please share the doc link and describe the issue)
  • Other

2. Steps to Reproduce

  1. Run a release build on device or simulator with the provided code
  2. Use the season selector panel on the left to jump between cards. Do this until the carousel becomes unresponsive as described below in Observed Behavior. The bug happens at least 30% of the time.
  3. A wide variety of jumps can produce the bug, for example Season 14 to Season 8.
  4. Bug can be reproduced with or without animation in the ref.scrollTo() call
import {
  Carousel,
  type CarouselRef,
  type ItemInfo,
} from '@amzn/kepler-ui-components'
import {
  useEffect,
  useRef,
  useState,
} from 'react'
import {
  FocusManager,
  Pressable,
  ScrollView,
  Text,
  findNodeHandle,
} from 'react-native'
import { StyleSheet, View } from 'react-native'

/**
 * A simplified example of the program list implemented using Kepler Carousel.
 */
const ITEM_HEIGHT = 180
const GAP = 24
const WIDTH = 920

// BATCH_SIZE and MAX_OFFSET_ITEMS require higher values than carousel defaults to avoid empty views when jumping to season
const BATCH_SIZE = 16
const MAX_OFFSET_ITEMS = 8

const episodeCounts = [
  6, 21, 11, 19, 26, 17, 7, 22, 17, 3, 4, 2, 5, 5, 19, 5, 2,
] // number of episodes per season

const data: string[] = []
episodeCounts.forEach((count, seasonIndex) => {
  for (let i = 0; i < count; i++) {
    data.push(`Season ${seasonIndex} E${i} Saturday Night Live!`)
  }
})

const seasonIndexes = [0]
let sum = 0
for (const count of episodeCounts) {
  sum += count
  seasonIndexes.push(sum)
} // [0, 16, 24 ...] // to scroll to season 1, go to 16. season 2, 24, etc.

const Card = (
  { text, index, focusRequest, completeFocusRequest }: { text: string, index: number, focusRequest?: number, completeFocusRequest: () => void },
) => {
  const ref = useRef<View>(null)
  const [isFocused, setIsFocused] = useState(false)

  useEffect(() => {
    if (focusRequest === index) {
      const handle = findNodeHandle(ref.current)
      FocusManager.focus(handle)
      completeFocusRequest()
    }
  }, [completeFocusRequest, focusRequest, index])

  return (
    <Pressable
        style={[styles.card, isFocused ? { backgroundColor: 'pink' } : {}]}
        ref={ref}
        onFocus={() => setIsFocused(true)}
        onBlur={() => setIsFocused(false)}
      >
        <Text style={styles.text}>{text}</Text>
      </Pressable>
    )
  }

const Button = ({ text, onPress }: { text: string; onPress: () => void }) => {
  const [isFocused, setIsFocused] = useState(false)

  return (
    <Pressable
      onPress={onPress}
      style={[styles.button, isFocused ? { backgroundColor: 'pink' } : {}]}
      onFocus={() => setIsFocused(true)}
      onBlur={() => setIsFocused(false)}
    >
      <Text style={styles.text}>{text}</Text>
    </Pressable>
  )
}

const itemInfo: ItemInfo[] = [
  {
    view: Card,
    dimension: {
      width: WIDTH,
      height: ITEM_HEIGHT,
    },
  },
]

const getItemForIndex = () => Card

export const CarouselSandbox = () => {
  const carouselRef = useRef<CarouselRef<string>>(null)

  // Simple race condition proof setup to focus cards, works even if the card is not presently mounted.
  // Index of a card. Card checks for this in a useEffect and focuses itself if matched.
  const [focusRequest, setFocusRequest] = useState<number>()
  // When card focuses itself, call this to clear the request and avoids executing it again if the card recycles.
  const completeFocusRequest = () => setFocusRequest(undefined)

  const jumpToSeason = (seasonIndex: number) => {
    const episodeIndex = seasonIndexes[seasonIndex]
    carouselRef.current?.scrollTo(episodeIndex, false)
    setFocusRequest(episodeIndex)
  }

  return (
    <View style={styles.container}>
      <ScrollView style={styles.controls}>
        {episodeCounts.map((_, seasonIndex) => (
          <Button
            key={seasonIndex}
            text={`Season ${seasonIndex}`}
            onPress={() => jumpToSeason(seasonIndex)}
          />
        ))}
      </ScrollView>
      <Carousel
        ref={carouselRef}
        hasTVPreferredFocus
        containerStyle={styles.carousel}
        orientation={'vertical'}
        data={data}
        itemDimensions={itemInfo}
        itemPadding={GAP}
        getItemForIndex={getItemForIndex}
        keyProvider={(item) => item}
        numOffsetItems={MAX_OFFSET_ITEMS}
        maxToRenderPerBatch={BATCH_SIZE}
        firstItemOffset={2 * (ITEM_HEIGHT + GAP)} // focus on center card
        focusIndicatorType="fixed"
        renderItem={({ item, index }) => {
          return (
            <Card text={item} index={index} focusRequest={focusRequest} completeFocusRequest={completeFocusRequest} />
          )
        }}
      />
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    width: '100%',
    height: 1000,
    justifyContent: 'space-between',
    paddingLeft: 48,
  },
  controls: {
    flexGrow: 1,
    height: 1000,
  },
  carousel: {
    width: WIDTH,
    height: 1000,
  },
  card: {
    width: WIDTH,
    height: ITEM_HEIGHT,
    backgroundColor: 'gray',
  },
  text: {
    fontSize: 32,
    color: 'white',
  },
  button: {
    backgroundColor: 'gray',
    width: 200,
  },
})

3. Observed Behavior

Explain what actually happened, noting any discrepancies or malfunctions.

30 to 50% of the time I jump to a card in the carousel, the carousel enters a broken state.

- Only 4 cards are visible instead of 5
- Carousel becomes unresponsive to top and bottom DPad buttons
- Can exit focus from carousel by pressing horizontally

4. Expected Behavior

Describe what you expected the SDK to do under normal operation.

5 cards should be visible and the carousel should scroll normally

4.a Possible Root Cause & Temporary Workaround

I’ve previously reported this issue involving scrollTo

I can likely get the jump to card working correctly using the initialStartIndex prop and passing a new key to the carousel. I'll have to experiment and see if that works.

5. Logs or crash report

(Please make sure to provide relevant logs as attachment and share VPKG file with your Amazon contact)

For crash issues, please refer this guide for faster troubleshooting: https://developer.amazon.com/docs/kepler-tv/detect-crash.html.

  • App/Device Logs

  • Crash Logs

  • Crash Report

  • For issues with Kepler Studio Extension, please share log files from below folders:

     ~/.vscode/extensions/amazon.kepler-extension-<version>/ExtensionLogs
     ~/.vscode/extensions/amazon.kepler-ui-extension-<version>/ExtensionLogs
    

6. Environment

Please fill out the fields related to your bug below:

  • SDK Version: 0.20.3207

  • App State: Foreground

  • OS Information
    Please ssh into the device via kepler exec vda shelland copy the output from cat /etc/os-releaseinto the answer section below. Note, if you don’t have a simulator running or device attached kepler exec vda shell will respond with vda: no devices/emulators found

    sh(com.amazon.dev.shell):/$ cat /etc/os-release
    NAME="OS"
    OE_VERSION="4.0.0"
    OS_MAJOR_VERSION="1"
    OS_MINOR_VERSION="1"
    RELEASE_ID="2"
    OS_VERSION="1.1"
    BRANCH_CODE="VegaMainlineTvIntegration"
    BUILD_DESC="OS 1.1 (VegaMainlineTvIntegration/4418)"
    BUILD_FINGERPRINT="4.0.155683.0(3072cab629675a74)/4418N:user-external/release-keys"
    BUILD_VARIANT="user-external"
    BUILD_TAGS="release-keys"
    BUILD_DATE="Wed Sep 03 05:40:19 UTC 2025"
    BUILD_TIMESTAMP="1756878019"
    VERSION_NUMBER="201010441850"
    

Q: Would you like to be contacted to share your latest VPKG compiled with latest SDK:

Y

Q: VPN or Login needed to verify functionality in VPKG?

[Y/N] [Share privately with your Amazon contact]

Q: If applicable, please provide your media/content url
If this is created dynamically, tokenized, etc please provide a way for us to access it

[N/A or Content / Media Url for testing] [Share privately with your Amazon contact]

Q: Are there any special headers required to reproduce the issue you are facing?

[N/A or Insert Headers]

Additionally please provide the following if possible
Provide Screenshots / Screengrabs / Logs. Please include as much information as you can that will help debug.

<!-- Answer here if applicable --> 

:backhand_index_pointing_right: Additional Context


Any Additional Context you would like to provide?
Add any other relevant information, such as recent updates to the SDK, dependencies, or device OS that may affect the bug.

<!-- Answer here if applicable  --> 

Hi @anthony3662 ,

This has been reported to our internal team.

Thanks,
Rohit

Hi @anthony3662 ,

Checking in. Is this still an issue for you with the latest SDK update?

Thanks,
Rohit

Hi Rohit,

I just tested this again and it is still an issue.

❯ vega -v
Active SDK Version: 0.22.6150
Vega CLI Version: 1.2.18

Hi @anthony3662 ,

Thank you for the update.
Could you please share logs for this from the active SDK version?

Thanks,
Rohit

Hi @anthony3662 ,

Could you please share the latest logs?

Thanks,
Rohit