Bug Description
1. Summary
onFocus event callback fires with a delay in Kepler card component
App Name:
Frndly TV App
Bug Severity
- Impacts operation of app
2. Steps to Reproduce
HomeScreen –> movieGrid(flashList) vertical scrolling of carousels) –> movieCarousel(kepler-ui-carousel ) horizontal scrolling of Kepler cards –> videoTile(card – kepler-ui-component)
We are using multiple array of objects, by default focus is working, we want to control the focus handling as we have custom focus handling scenarios. So we need onFocus to be triggered fastly in card.
Check by clicking the remote button (D-pad) on the card. Add logs inside the card component’s onFocus function—you’ll notice a delay before the logs appear.
3. Observed Behaviour
When we click left or right D-pad button fastly from first card to nth card , focus shifting to nth card is happening with so much delay. Meanwhile if user navigating to any other cards without waiting focus is misbehaving.
4. Expected Behaviour
We need onFocus to trigger faster so the related UI functionalities can be handled without delay
4.a Possible Root Cause & Temporary Workaround
Fill out anything you have tried. If you don’t know, N/A is acceptable
N/A
6. Environment
Please fill out the fields related to your bug below:
-
SDK Version: 0.20.3351
-
OS Information
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/10153620)”
BUILD_FINGERPRINT=“1.0.15362.0(c449c30bb1fb0cc8)/10153620N:user/dev-keys”
BUILD_VARIANT=“user”
BUILD_TAGS=“dev-keys”
BUILD_DATE=“Tue Aug 12 00:43:13 UTC 2025”
BUILD_TIMESTAMP=“1754959393”
VERSION_NUMBER=“202025362030”
7. Example Code Snippet / Screenshots / Screengrabs
Include any relevant code or component setup in React Native that can help reproduce the bug.
MovieGrid:
<
FlashList
ref = {
flashListRef
}
data = {
data
}
scrollEnabled = {
false
}
renderItem = {
({
item,
index
}) => renderCarousel(item, index)
}
keyExtractor = {
(item, index) => `${item.heading}-${index}`
}
estimatedItemSize = {
calculateEstimatedItemSize(data)
}
estimatedListSize = {
{
height: Dimensions.get('screen').height,
width: Dimensions.get('screen').width,
}
}
ListEmptyComponent = {
renderEmptyMessage
}
/>
);
const renderCarousel = useCallback(
(
item: {
heading: string;
data: TitleData[];
sectioncontrols: TitleData;
},
index: number,
) => (
<
ScrollView
horizontal = {
isTV ? false : true
}
// scrollEnabled={Platform.isTV ? false : true}
scrollEnabled = {
false
}
showsHorizontalScrollIndicator = {
false
}
contentContainerStyle = {
styles.carouselContainer
}
nestedScrollEnabled = {
false
} >
<
MovieCarousel
from = {
from
}
heading = {
item.heading
}
data = {
item.data
}
sectioncontrols = {
item.sectioncontrols
}
row = {
index
}
cardDimensions = {
cardDimensionsInner
}
onTileFocus = {
handleCarouselTileFocus
}
onTileBlur = {
onTileBlur
}
onTilePress = {
onTilePress
}
isPriorityRow = {
index < ROWS_SHOWN_ON_SCREEN
}
hasTVPreferredFocus = {
hasTVPreferredFocus
}
triggerFetch = {
triggerFetch
}
isFetching = {
isFetching
}
/>
<
/ScrollView>
),
[cardDimensionsInner, data.length, handleCarouselTileFocus, isTV, onTileBlur, onTilePress],
);
const handleCarouselTileFocus = useCallback(
(rowIndex ? : number, colIndex ? : number) => {
if (rowIndex != null) {
flashListRef.current?.scrollToIndex({
index: rowIndex,
animated: true,
viewPosition: 1, // 0 = top, 0.5 = center, 1 = bottom
});
}
onTileFocus?.(rowIndex, colIndex);
},
[onTileFocus],
);
MovieCarousel:
{
visibleData.length > 0 && (
<
Carousel
ref = {
carouselRef
}
testID = {
`carousel-${testID}-${heading}`
}
containerStyle = {
computedStyle.listView
}
data = {
visibleData
}
orientation = "horizontal"
itemDimensions = {
viewInfos
}
itemPadding = {
30
}
renderItem = {
ItemView
}
maxToRenderPerBatch = {
24
}
getItemForIndex = {
getItemForIndex
}
keyProvider = {
(item, index) => item.searchFeedContentId
}
focusIndicatorType = "fixed"
firstItemOffset = {
SPACING.firstItemOffset
}
itemSelectionExpansion = {
{
widthScale: 1.2,
heightScale: 1.15,
}
}
itemScrollDelay = {
0.3
}
selectionBorder = {
{
enabled: true,
borderColor: '#00AD50',
borderWidth: 5,
borderRadius: 5,
borderStrokeRadius: 0
}
}
/>
)
}
const ItemView = useCallback(
({
item,
index
}: CarouselRenderInfo < TitleData > ): JSX.Element => {
return (
<
VideoTile
from = {
from
}
row = {
row
}
index = {
index
}
key = {
`${item.id} ${index}`
}
data = {
item
}
onFocus = {
() => onTileFocus && onTileFocus(row, index)
}
onBlur = {
() => onTileBlur && onTileBlur(row, index)
}
onPress = {
() => onTilePress && onTilePress(item, row, index)
}
tilePosition = {
calculateTilePosition(index)
}
sectioncontrols = {
sectioncontrols
}
iscarouselCard = {
true
}
hasTVPreferredFocus = {
hasTVPreferredFocus
}
triggerFetch = {
triggerFetch
}
isFetching = {
isFetching
}
/>
);
},
[firstElementRef, onCarouselTileInFocus, onTileBlur, row],
);
const onCarouselTileInFocus = useCallback((columnIndex: number, rowIndex: number, title: TitleData | undefined) => {
if (onTileFocus) {
setRowFocused(rowIndex);
}
},
[onTileFocus],
);
VideoTile: < Card
orientation = {
'vertical'
}
focusedStyle = {
iscarouselCard ? undefined : styles.focusedCard
}
style = {
[styles.card]
}
onFocus = {
onFocusHandler
}
onBlur = {
onBlurHandler
}
hasTVPreferredFocus = {
(from === 'viewAll' || from === 'search') ? hasTVPreferredFocus : index === 0 && row === 0 ? hasTVPreferredFocus : false
}
onPress = {
() => {
if (data.template != null && data.template != undefined) {
onPress?.(data);
} else {
navigatetoTargetPageTypeScreen(index);
}
}
} >
<
Image
style = {
styles.albumArt
}
source = {
getImageSource(imageUrl)
}
resizeMode = {
from === "viewAll" ? "stretch" : (data?.cardType === 'network_poster' || data?.cardType === 'network_poster_package')
||
data?.cardType === 'roller_poster'
?
'contain' : 'cover'
}
defaultSource = {
data.cardType === 'roller_poster'
?
require('../assets/placeholder_roller.jpg')
:
data.cardType === 'sheet_poster'
?
require('../assets/placeholder_sheet.jpg')
:
require('../assets/placeholder_sheet.jpg')
}
/>
<
/Card> const onFocusHandler = useCallback(() => {
setIsFocused(true);
if (onFocus) {
onFocus(data, row, index);
}
if (from === 'viewAll') {
requestAnimationFrame(() => {
animateScale(1.02, true);
});
}
}, [onFocus, data, row, index]);
const onBlurHandler = useCallback(() => {
setIsFocused(false)
onBlur?.(data, row, index);
if (from === 'viewAll') {
requestAnimationFrame(() => {
animateScale(1, true);
});
}
}, [onBlur, data, row, index]);