Skip to content

Hold focus inside the library's two panels - #442

Open
Kisnov wants to merge 3 commits into
Moonfin-Client:mainfrom
Kisnov:fix/panel-focus-containment
Open

Kisnov wants to merge 3 commits into
Moonfin-Client:mainfrom
Kisnov:fix/panel-focus-containment

Conversation

@Kisnov

@Kisnov Kisnov commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Pull Request

Summary

The Sort & Filter panel and the settings panel beside it let 5-way walk straight out of them: Up from the first row, or Left from anywhere, landed on the poster grid or the A–Z letter strip while the panel was still open.

Scoped deliberately to these two panels so the approach can be checked on a real set before anything wider. See #440 for the full analysis.

Related Issues

Type of Change

  • Bug fix

Changes Made

Both panels were declared restrict: 'self-only', which never took effect:

const SortPanelContainer = SpotlightContainerDecorator({enterTo: 'last-focused', restrict: 'self-only'}, 'div');

SpotlightContainerDecorator reads restrict from its spotlightRestrict prop, not from this config object, and useSpotlightContainer then applies that prop with setProps({…, restrict}) — overwriting whatever the config carried. The prop defaults to 'self-first', and self-first is exactly the reported behaviour: prefer what is inside, but leave when nothing lies that way. enterTo is unaffected, which is why the panels behave correctly in every other respect and this went unnoticed.

The fix is the one the rest of the app already uses. leaveFor does reach the container, and 28 containers already carry it, the settings screens among them — which is precisely why those hold focus and these two did not:

// views/Settings/settingsSpottables.js
// Every screen holds focus on its own, so a 5-way press at the edge stays put rather than
// jumping to whatever the panel is sitting on top of.
export const ViewContainer = SpotlightContainerDecorator({
	enterTo: 'last-focused',
	restrict: 'self-only',
	leaveFor: {left: '', right: '', up: '', down: ''}
}, 'div');

Both Library panels now share one config object so they cannot drift apart again. The inert restrict is kept to match the containers that already pair the two.

Platform

  • Both / Shared code

Testing

  • Tested on emulator
  • Tested on physical device
  • Manual testing completed
  • Not tested

Manually tested by @Kisnov on a Samsung TU43DU7105KXXC running Tizen 9: 5-way no longer walks out of either panel, and picking a filter leaves focus where it was, with and without a letter active on the alphabet strip.

enact lint clean. Full suite 1641 passed, 3 failed — those 3 are the pre-existing locale-sensitive tests in personCredits and seerrBadges that fail identically on a clean tree.

Test Steps

  1. Open a library, open the Sort & Filter panel.
  2. From the first row press Up — focus should stay on the first row, not jump to the grid or the letter strip.
  3. Press Left from anywhere in the panel — focus should stay inside.
  4. Navigate down through the sections and back up — normal movement unchanged.
  5. Repeat for the settings panel beside it.
  6. Confirm Back still dismisses each panel and returns focus where it was.
  7. Sanity-check the grid and toolbar behind them still navigate normally once dismissed.

Screenshots (if applicable)

Not applicable — no visual change; this is 5-way containment only.

Checklist

  • Code builds successfully
  • Code follows project style and conventions
  • No unnecessary commented-out code
  • No new warnings introduced — enact lint clean

Not included

52 container declarations do not carry leaveFor, but most of them should not — rows, tab bars, the nav bar and the grid are all meant to let focus out. Only panels and overlays that sit on top of something want it. Deciding which of the rest qualify is a judgement call per container and belongs in its own pass, not bundled here.

🤖 Generated with Claude Code

Both panels sit on top of the grid they were opened from, and a 5-way press at
their edge walked straight out of them: up from the first row, or left from
anywhere, landed on the poster grid or the A to Z strip while the panel was
still open.

Both were declared `restrict: 'self-only'`, which never took effect.
SpotlightContainerDecorator reads restrict from its `spotlightRestrict` prop
rather than from this config object, and useSpotlightContainer then applies
that prop with setProps, so the config's value is overwritten by the prop
default of self-first. self-first is what was running: prefer what is inside,
but leave when nothing lies that way.

`leaveFor` is what the rest of the app holds panels with, and it does reach the
container. Twenty-eight of them already carry it, the settings screens among
them, which is why those hold focus and these two did not. Both now share one
config so they cannot drift apart again.

The inert `restrict` is left in place to match the containers that already pair
the two.

Related to Moonfin-Client#440

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 19, 2026

Copy link
Copy Markdown

✅ Build Successful

All platform builds and the test suite passed. You can download the artifacts below.

Platform Status Artifact
webOS ✅ Passed Moonfin_webOS_*.ipk
Tizen Regular ✅ Passed Moonfin_Tizen_Regular_*.wgt
Tizen Oblong ✅ Passed Moonfin_Tizen_Oblong_*.wgt
Tizen Legacy ✅ Passed Moonfin_Tizen_Legacy_*.wgt
Property Value
Commit 46189d5
Workflow run Build #298

Holding the panel against 5-way was not enough: picking a filter still threw
focus out to the grid, because something else was reaching in and moving it.

useStartLetter hands focus to the grid once the narrowed list has settled,
which means watching the rebuilt list to know when that is. The trouble is the
list rebuilds for anything — a filter toggled in the panel, a search typed in
the header — so the effect answered those too and pulled focus to the grid
while the viewer was still working in the panel. It fired whenever items.length
or isLoading changed, with nothing to say whether the letter had anything to do
with it.

It now remembers which letter it last moved for and stays put until that
changes, so a rebuild it did not ask for goes by unnoticed. Clearing the letter
forgets it, so picking the same one again still counts as a fresh pick.

Tests cover the narrowing and the toggle as well, since the hook had none. The
one that matters is the rebuild under an unchanged letter: it fails without
this and passes with it.

Related to Moonfin-Client#440

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Kisnov

Kisnov commented Sep 19, 2026

Copy link
Copy Markdown
Contributor Author

Second commit: something else was reaching in and moving focus

The first commit holds the panel against 5-way, and on a set that part works. But picking a filter still threw focus out to the grid — a different bug wearing the same clothes.

useStartLetter hands focus to the grid once a picked letter's narrowed list has settled, which means watching the rebuilt list to know when that is:

useEffect(() => {
    if (!startLetter || items.length === 0 || isLoading) return undefined;
    const id = setTimeout(() => Spotlight.focus(gridSpotlightId), 100);
    return () => clearTimeout(id);
}, [startLetter, items.length, isLoading, gridSpotlightId]);

The list rebuilds for anything — a filter toggled in the panel, a search typed in the header — so the effect answered those too, with nothing in it to say whether the letter had anything to do with the rebuild. Any reload while a letter was active pulled focus to the grid, out of whatever panel the viewer was working in.

It now remembers which letter it last moved for and stays put until that changes. Clearing the letter forgets it, so picking the same one again still counts as a fresh pick.

leaveFor could never have covered this: it constrains 5-way, and this was a programmatic Spotlight.focus.

Tests

The hook had none, so this adds six covering the narrowing and the same-letter toggle as well. The one that matters is "leaves focus alone when the list rebuilds under the same letter" — verified it fails without this commit and passes with it, with the other five passing either way.

Full suite: 1641 passed, 3 failed — the pre-existing locale-sensitive ones. enact lint clean.

Scope

useStartLetter also backs the Favorites grid, so that gets the same fix.

Still unverified on a set: the reporter confirmed the first commit works and found this while testing it. This second one wants the same check.

🤖 Generated with Claude Code

Picking a filter with no letter selected still threw focus to the grid, so the
alphabet strip was not the only thing reaching in. This is the one that fires
every time.

The grid takes focus once the library has something to show, guarded by a flag
so it only happens on arrival. The reload effect clears that flag, and it runs
for every filter in its dependency list — tags, genres, years, languages, all
of them. So each pick cleared the flag, the reload came back, and the grid took
focus while the panel was still open and being used.

The grid now waits while either panel is on screen. A panel up is the viewer
still choosing; the handlers that close a panel already hand focus to the grid
themselves, so nothing is lost by leaving it to them.

Sits below the panel state it now reads, rather than above it where the
dependency list would have been evaluated before those were declared.

Related to Moonfin-Client#440

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Bug Something isn't working Tizen webOS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant