fix: TV focus, scroll, layout and reanimated fixes from the Plex client embed - #117
Merged
Conversation
A sibling that fully encloses the source on the movement axis (a screen scene behind a floating header) keeps its focusable content beyond the source, so enter it from the source's far edge instead of rejecting it on its near edge. Down-only, gated on strict vertical enclosure.
…s every arrival - A transform is a complete snapshot, so an axis it omits returned to identity: reset a previously-written pixel translate instead of leaving the stale offset (the side-nav drawer slides on-screen when opened). - Forward FocusGroup destinations on every arrival, not just the first, so a reopened group returns to its declared destination (the drawer's selected item). RNG-636
Add a focusRestorationExcluded focus option so a catcher is reachable by a deliberate directional move but never chosen for fallback/mount-default focus, and gate the withheld-paint reveal on a settled translate to kill the open flash. (RNG-634)
Plain ScrollView never followed focus (only VirtualList did), so on Lightning the side-nav drawer and other lists stayed pinned while focus moved off-screen. Subscribe to focusPathChanged and reveal the focused descendant with a half-item margin, matching native TV auto-scroll. RNG-641
Down/Up in the Live TV guide landed on the channel header instead of the airing under the focused column. Two framework gaps: - directional nav beamed from the group's immediate focused child, losing the deep leaf's cross-axis position, so descent fell to the first child (the header). Beam from the deepest focused leaf and descend by geometry into the chosen sibling; a redirect node (the row's airings guide) is handed to the focus manager to forward to its anchored destination. - the airings guide redirects to one of its own cells (an internal redirect). _focusNode's upward walk re-fired that redirect on the way back up, targeting a descendant it had just visited, self-cycling and aborting the move so focus stranded on the guide's first child. The upward walk now only forwards external redirects; internal ones are already satisfied by the downward-arrival redirect.
…aque color values
…e-evenly distinctly
_focusNode only emitted childFocused for the leaf's immediate focus-parent, so a VirtualList whose cells nest their own focus group never learned that focus had moved to a different cell and stopped scrolling to follow focus (row scrollers frozen on Right, Categories grid on Down). Walk the focus chain and emit childFocused for each ancestor whose focusedElement actually changed, matching tvOS onChildFocused semantics.
…lign Pass the 100-900 numeric/keyword font weights through instead of collapsing to bold/normal, parsing numeric strings the renderer would otherwise treat as 400. Map textAlign auto->left and warn+fall-back justify->left. Drop the inert text-shadow styles (SDF renderer has no shadow) with a dev warning.
Case-transform Text children for uppercase/lowercase/capitalize, and fall back head/middle ellipsize to tail since the renderer only truncates at the tail.
…aths The single-step renderer-loop path only looped for count -1 (so 0 never repeated and other negatives set a nonsensical repeat), and the composed program path treated 0 as zero plays — the opposite of native, where any count <= 0 repeats forever. Unify both to native semantics.
setStyles cancelled and restarted every in-flight program on the view on any updater recompute, so a looping pulse reset every time an unrelated shared value (a scroll-linked prop in the same hook) changed. Key the runners by style prop and diff against the new schedule: unchanged keys keep running, only added/removed/changed keys are (re)started.
Bind a per-target layout handler so each ResizeObserverEntry carries its own target's rect, and deliver only the target that actually changed instead of a batch of every observed target.
…eader/footer edge
VirtualList now fires onMomentumScrollBegin/onMomentumScrollEnd once around a focus-driven animated scroll (begin on start, end on settle or cancel), threaded through a balance tracker so the pair can never go unbalanced. The reanimated useAnimatedScrollHandler shim routes the onMomentumBegin/onMomentumEnd handler keys by event name instead of only ever calling onScroll.
RuudBurger
force-pushed
the
promote/focus-layout-text-fixes
branch
from
August 17, 2026 12:21
af4e4f4 to
ecfdc2f
Compare
RuudBurger
marked this pull request as ready for review
August 17, 2026 12:29
DouweBos
approved these changes
Aug 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Framework fixes developed against the client's
vendor/react-lightningembed, promoted back here. Mostly TV focus/scroll parity, plus flexbox, color, text and reanimated gaps we walked into. Client-side integration stays inreact-native-client.Focus and scroll
Viewwithfocusableis registered as a spatial-focus target, like tvOS/Android TV. AddsfocusRestorationExcludedfor directional-only catchers, and those are skipped by restoration and the mount-time defaultdestinationsforwards focus on every arrival, not only the first, matchingTVFocusGuideViewScrollViewreveals a focused descendant with a minimal ensure-visible scroll, andsnapToAlignmentonly counts as deliberate placement forcenter/endVirtualList
onEndReachedon mountgetNativeScrollRef()implemented; momentum scroll callbacks surfacedonLayout(Grid re-keyed the list on every guess-measure cycle, an infinite remount loop when it mounted without a laid-out size)Layout, color, text
transformis a full snapshot, so an omitted axis clears its pixel inset instead of leaving a stale offset a partial push never repaintsalignSelf: 'auto'resolves to the parent'salignItems;space-evenlymaps to yoga's own valuehsl(), and space-formrgb()parse;PlatformColor/OpaqueColorValuedrop with a warning instead of throwingfontWeightkeeps the full 100-900 scale (numeric strings collapsed to 400 before); text shadows drop with a warning since the SDF renderer has no shadowTextappliestextTransformand honorshead/middleellipsizeReanimated
withTimingdefaults toEasing.inOut(Easing.quad)instead of linearwithRepeattreats any count <= 0 as infinite on both pathsuseAnimatedStylerestarts only the keys that changed, so an unrelated key isn't restarted mid-flightNotes
mainwith paths rooted here, so the tree matches our embed exactly.oxlintis clean across all touched files. I left formatting alone:mainisn't oxfmt-clean on several of these files either, and reformatting would have buried the actual changes.fix(vendor): tighten compat layer types...is mostly typing, but it also drops areact-lightning/srcdeep import and makessnapToAlignment: 'item'fall back to'start'.Validated in the Plex client against the embedded copy, not in isolation here, so a second pair of eyes on the focus changes would be good. Draft until CI reports back.