Skip to content

Demo: use the system on-screen keyboard for TV search - #5727

Closed
sztomek wants to merge 28 commits into
feat/tv-home-auth-feedsfrom
feat/tv-search-system-keyboard
Closed

Demo: use the system on-screen keyboard for TV search#5727
sztomek wants to merge 28 commits into
feat/tv-home-auth-feedsfrom
feat/tv-search-system-keyboard

Conversation

@sztomek

@sztomek sztomek commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Description

Temporary demo / spike — not intended to merge.

Temporarily swaps our custom on-screen keyboard on the TV Search screen for Android TV's default system on-screen keyboard (the leanback IME described in Manage on-screen keyboards), so we can record a demo for design to compare the two approaches.

TvSearchField becomes a real editable BasicTextField (search icon + placeholder preserved, ImeAction.Search, single line) that auto-focuses and shows the system keyboard on entry. TvSearchScreen now drives the query from the field's onValueChange and no longer renders our hand-rolled TvSearchKeyboard.

The custom keyboard code (TvSearchKeyboard.kt and its unit test) is left in the tree, unused, so it's trivial to restore once the demo has served its purpose.

Branch is off feat/tv-home-auth-feeds (which carries the TV search work), so this diff shows only the keyboard swap.

Testing Instructions

  1. Build & install the TV app: ./gradlew :tv:installDebug
  2. Launch on an Android TV device/emulator and open the Search tab (magnifier in the top bar).
  3. The text field auto-focuses and the system on-screen keyboard appears.
  4. Type with the D-pad (or a connected keyboard) and confirm characters land in the field, autocomplete suggestions show, and Delete/Space behave as the system keyboard dictates.

Screenshots or Screencast

Demo video: scratchpad/tv-search-system-keyboard-demo.mp4
Screenshot: scratchpad/tv-search-system-keyboard.png

Checklist

  • If this is a user-facing change, I have added an entry in CHANGELOG.md — n/a, temporary demo spike, TV search is not yet shipped
  • Ensure the linter passes (./gradlew spotlessApply to automatically apply formatting/linting)
  • I have considered whether it makes sense to add tests for my changes — no new logic; existing TvSearchKeyboardStateTest retained
  • All strings that need to be localized are in modules/services/localization/.../strings.xml — reuses existing search string
  • Any jetpack compose components I added or changed are covered by compose previews — TvSearchFieldPreview updated
  • I have updated the Event Horizon schema — n/a, no analytics changes

I have tested any UI changes...

  • with different themes — TV is dark-only
  • with a landscape orientation — TV is landscape-only
  • with the device set to have a large display and font size
  • for accessibility with TalkBack

@claude

claude Bot commented Aug 12, 2026

Copy link
Copy Markdown

Claude finished @sztomek's task in 2m 34s —— View job


Review

  • Gather context (diff vs feat/tv-home-auth-feeds)
  • Read TvSearchField.kt / TvSearchScreen.kt in full
  • Check surrounding TV search code (keyboard, view model, manifest)
  • Post review (4 inline comments)

I've reviewed this as a spike whose job is to produce a fair demo for design, not as merge-ready code — so the bar I applied is "does the recording show the system IME at its best". Mostly yes; a few things would make the comparison misleading.

Blocking for the demo

1. Auto-focus can re-fire and steal focus back. TvSearchField.kt:46-51 — the field lives in a LazyColumn item {}, so scrolling down to the discover rows disposes it; scrolling back up re-runs LaunchedEffect(Unit) and pops the system keyboard over the browsing UI. The code being replaced guarded this with hasAutoFocused hoisted outside the LazyColumn; that guard was dropped. This is the one behavioural regression in the diff and it's the kind of thing that will show up on camera.

2. ImeAction.Search has no keyboardActions. TvSearchField.kt:59-63 — pressing Search does nothing and the full-screen leanback IME stays up, so there's no clean way to get back to results. Add onSearch = { hide + clearFocus }.

3. No focus indication on the field. TvSearchScreen.kt:67-71BasicTextField draws nothing, and it's now the only focusable in the header. Without a focus state, design would partly be comparing "our keyboard has focus affordances, the system one doesn't", which isn't the question being asked.

Worth fixing, cheap

  • runCatching { focusRequester.requestFocus() } swallows FocusRequester is not initialized — turning a crash into a silent "keyboard never appears". Log it.
  • keyboardController?.show() is redundant (BasicTextField requests the IME on focus) and racy — it fires in the same frame as requestFocus(), before focus is dispatched.
  • Caret is drawn over the "S" of the Search placeholder while focused-and-empty, which is the screen's opening state.
  • The field wraps its text instead of filling the row, so the focusable region is ~zero-width on an empty query — relevant once you add a focus indicator.
  • autoCorrectEnabled = false probably suppresses the suggestion strip, but the testing instructions ask reviewers to confirm "autocomplete suggestions show". Pick one.

No concerns

  • Security: nothing here — no new input handling beyond the standard IME path, no data leaves the screen.
  • Performance: each keystroke recomposes TvSearchContent and flips query.isBlank(), restructuring the LazyColumn. Fine at this scale, and the IME covers it on TV anyway.
  • Caret-visibility for long queries: the removed horizontalScroll + scrollTo(maxValue) is handled internally by singleLine BasicTextField, so nothing regressed.
  • Dead code: TvSearchKeyboard.kt is unreferenced from main but still covered by TvSearchKeyboardStateTest, so it compiles clean with no unused-warning fallout under -Werror. Deliberate and documented — fine for a spike, just don't let it drift.

I did not build or run the TV app, so the auto-focus re-fire and the caret/placeholder overlap are read off the code rather than observed on a device.
• Branch: feat/tv-search-system-keyboard

@dangermattic

Copy link
Copy Markdown
Collaborator
1 Message
📖 This PR is still a Draft: some checks will be skipped.

Generated by 🚫 Danger

Comment on lines +46 to +51
if (autoFocus) {
LaunchedEffect(Unit) {
runCatching { focusRequester.requestFocus() }
keyboardController?.show()
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auto-focus can re-fire and steal focus back from the content rows.

TvSearchField is composed inside a LazyColumn item { } (TvSearchScreen.kt:64-74), so when the user D-pads down far enough to browse the categories/discover rows, the header item is disposed. Scrolling back up recomposes it, focusRequester is remembered fresh, and LaunchedEffect(Unit) runs again → focus jumps into the field and the system IME pops open over the browsing UI.

The version being replaced guarded against exactly this with hasAutoFocused hoisted into TvSearchContent (outside the LazyColumn), and that guard is gone here. Either hoist a "has auto-focused" flag back up into TvSearchContent, or hold it in rememberSaveable next to query in TvSearchScreen so it survives item disposal.

Two smaller things in this block:

  • runCatching { … } silently swallows FocusRequester is not initialized. The failure mode it converts a crash into is "the field never focuses and the keyboard never appears" — i.e. exactly the thing this spike is trying to demo, failing invisibly. At minimum log it (Timber.e).
  • keyboardController?.show() is redundant and racy: BasicTextField requests the IME itself once it actually gains focus, and this call runs in the same frame as requestFocus(), before the focus event has been dispatched or the text input session started. If you want a safety net, hang it off an observed focus change (interactionSource.collectIsFocusedAsState() / Modifier.onFocusChanged) rather than firing it blind.

Comment on lines +59 to +63
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.None,
autoCorrectEnabled = false,
imeAction = ImeAction.Search,
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ImeAction.Search is declared but there is no keyboardActions, so pressing Search on the leanback IME does nothing and the keyboard stays up. On a phone that's a minor annoyance; on TV the IME is a full-screen overlay, so there's no obvious way to dismiss it and get to the results — which will read badly in the demo recording. Worth adding before you record:

keyboardActions = KeyboardActions(onSearch = {
    keyboardController?.hide()
    focusManager.clearFocus()
}),

Also: autoCorrectEnabled = false likely suppresses the IME's suggestion strip, but the testing instructions ask reviewers to "confirm … autocomplete suggestions show". If suggestions are part of what you want design to compare, this flag is working against you.

Comment on lines +74 to +83
Box(contentAlignment = Alignment.CenterStart) {
if (query.isEmpty()) {
Text(
text = stringResource(LR.string.search),
style = MaterialTheme.tvTypography.title2,
color = MaterialTheme.tvColors.textSecondary,
)
}
innerTextField()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two layout nits in the decoration box:

  1. Cursor sits on top of the placeholder. Box(contentAlignment = Alignment.CenterStart) left-aligns both the placeholder and innerTextField(). While the field is focused and empty — which is the state the screen opens in, given autoFocus = true — the blinking caret is drawn over the "S" of Search. Material's TextField hides the placeholder on focus for this reason; consider doing the same, or offsetting the placeholder.

  2. The field wraps its content instead of filling the row. modifier arrives with no width constraint, so the focusable/editable region is only as wide as the text (roughly zero when empty). That makes the focus target and any focus indicator you add effectively invisible on an empty query. Modifier.weight(1f) on the Box (or fillMaxWidth() at the call site) gives the field a stable, full-width hit area.

Separately: the old implementation kept the caret visible for long queries via horizontalScroll + scrollTo(maxValue). BasicTextField with singleLine = true handles that internally, so nothing is lost there — just flagging that I checked.

Comment on lines +67 to 71
TvSearchField(
query = query,
onQueryChange = onQueryChange,
autoFocus = true,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the custom keyboard gone, this text field is the only focusable element in the header, and BasicTextField draws no focus decoration of its own. On TV, D-pad focus has to be visible at all times — once the user moves down into the categories row there's no indication that the field above is focusable, and no highlight when focus returns to it. Worth adding a border/background driven by interactionSource.collectIsFocusedAsState() before showing this to design, otherwise the comparison is partly measuring "our keyboard has focus states, the system one doesn't".

Also note TvSearchScreen still only uses query to hide the discover rows (line 99) — nothing is dispatched to TvSearchViewModel, so typing won't produce search results in the demo. That's pre-existing on feat/tv-home-auth-feeds, not introduced here, but it does limit what the recording can show.

@sztomek
sztomek force-pushed the feat/tv-home-auth-feeds branch from 14ede72 to 425f5e8 Compare August 12, 2026 08:56
@sztomek

sztomek commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Demo served its purpose — design approved the system on-screen keyboard. The change has been folded into the search stack at #5717 (custom keyboard removed across the stack), so this demo PR is redundant. Closing and deleting the branch.

p1786523963874079-slack-C0ATWH7BNH3

@sztomek sztomek closed this Aug 12, 2026
@sztomek
sztomek deleted the feat/tv-search-system-keyboard branch August 12, 2026 08:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants