Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ fun AppUpdateModal(
val focusRequester = remember { FocusRequester() }

LaunchedEffect(Unit) {
focusRequester.requestFocus()
runCatching { focusRequester.requestFocus() }
}

BackHandler {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ fun ContextMenu(
if (isVisible) {
focusedIndex = 0 // Reset to first item
if (!isMobile) {
focusRequester.requestFocus()
runCatching { focusRequester.requestFocus() }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ fun MediaContextMenu(
if (isVisible) {
focusedIndex = 0
if (!isMobile) {
focusRequester.requestFocus()
runCatching { focusRequester.requestFocus() }
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ fun QuickActionMenu(
if (isVisible) {
focusedIndex = 0
ignoreNextEnter = true
focusRequester.requestFocus()
runCatching { focusRequester.requestFocus() }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ fun StreamSelector(
// Request focus when visible
LaunchedEffect(isVisible) {
if (isVisible) {
focusRequester.requestFocus()
runCatching { focusRequester.requestFocus() }
focusedIndex = 0
focusedTabIndex = 0
selectedTabIndex = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ fun TextInputModal(
// Request focus on input when modal becomes visible and show keyboard
LaunchedEffect(isVisible) {
if (isVisible) {
inputFocusRequester.requestFocus()
runCatching { inputFocusRequester.requestFocus() }
focusedButton = -1
// Delay to ensure focus is set before showing keyboard
kotlinx.coroutines.delay(200)
Expand Down Expand Up @@ -160,7 +160,7 @@ fun TextInputModal(
Key.DirectionUp -> {
if (focusedButton >= 0) {
focusedButton = -1
inputFocusRequester.requestFocus()
runCatching { inputFocusRequester.requestFocus() }
showKeyboard()
}
true
Expand Down
19 changes: 12 additions & 7 deletions app/src/main/kotlin/com/arflix/tv/ui/focus/ArvioDpadFocus.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,27 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.input.key.Key
import kotlin.Unit

@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
fun Modifier.arvioDpadFocusGroup(
restoreFocusRequester: FocusRequester? = null,
enableFocusRestorer: Boolean = true
): Modifier {
val restorer = when {
!enableFocusRestorer -> Modifier
restoreFocusRequester != null -> Modifier.focusRestorer { restoreFocusRequester }
else -> Modifier.focusRestorer()
}
// Compose's focusRestorer(onRestoreFailed = { restoreFocusRequester }) captures the
// requester at modifier-application time and, on a focus enter whose automatic restore
// fails, calls requestFocus() ITSELF — deep inside the modifier, without a try/catch.
// When the restore target lives in a LazyColumn row that is not yet composed/attached
// (the case right after categories finish loading on a fresh TV entry), that internal
// requestFocus() throws IllegalStateException: FocusRequester is not initialized and
// crashes the app, unreachable by the runCatching guards wrapping direct calls.
//
// Keep focus restoration limited to Compose's own saved focus history. Callers that
// need an initial focus target should request it after the target is attached instead
// of supplying a fallback that Compose can invoke before a lazy item is composed.
val restorer = if (enableFocusRestorer) Modifier.focusRestorer() else Modifier
return this.then(restorer).focusGroup()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,12 +312,12 @@ fun SearchScreen(
try { filtersFocusRequester.requestFocus() } catch (_: Exception) {}
} else {
focusZone = FocusZone.SEARCH_INPUT
searchFocusRequester.requestFocus()
runCatching { searchFocusRequester.requestFocus() }
}
}
FocusZone.FILTERS -> {
focusZone = FocusZone.SEARCH_INPUT
searchFocusRequester.requestFocus()
runCatching { searchFocusRequester.requestFocus() }
}
FocusZone.SEARCH_INPUT -> {
focusZone = FocusZone.SIDEBAR
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,7 @@ fun CategorySidebar(
// hands it the selector on entry and again every time the lazy list
// recomposes underneath the focused row — which is what pinned the
// selector in the search box while the playlist loaded.
.arvioDpadFocusGroup(
restoreFocusRequester = if (categoriesLoaded) selectedCategoryFocusRequester else null,
)
.arvioDpadFocusGroup()
.onFocusChanged { focusState ->
if (focusState.hasFocus) {
onFocusEnter()
Expand Down
Loading