Skip to content

fix: TV focus, scroll, layout and reanimated fixes from the Plex client embed - #117

Merged
RuudBurger merged 22 commits into
mainfrom
promote/focus-layout-text-fixes
Aug 17, 2026
Merged

fix: TV focus, scroll, layout and reanimated fixes from the Plex client embed#117
RuudBurger merged 22 commits into
mainfrom
promote/focus-layout-text-fixes

Conversation

@RuudBurger

Copy link
Copy Markdown
Member

Framework fixes developed against the client's vendor/react-lightning embed, promoted back here. Mostly TV focus/scroll parity, plus flexbox, color, text and reanimated gaps we walked into. Client-side integration stays in react-native-client.

Focus and scroll

  • Directional focus can enter a sibling that fully encloses the source (screen scene behind a floating header no longer traps focus in the header)
  • A View with focusable is registered as a spatial-focus target, like tvOS/Android TV. Adds focusRestorationExcluded for directional-only catchers, and those are skipped by restoration and the mount-time default
  • Every ancestor focus group hears about focus crossing its children, not just the group above the leaf (VirtualList scroll-to-focus stopped following when cells nested their own group)
  • destinations forwards focus on every arrival, not only the first, matching TVFocusGuideView
  • ScrollView reveals a focused descendant with a minimal ensure-visible scroll, and snapToAlignment only counts as deliberate placement for center/end
  • Center-aligned focus targets stay mid-viewport instead of snapping to the header/footer edge
  • EPG directional focus is anchored per-airing

VirtualList

  • Separator size reserved between grid rows (content size was short by a separator per row)
  • Scroll offset reconciled against the latest content size: a shrink reclamps, and a short list primes onEndReached on mount
  • getNativeScrollRef() implemented; momentum scroll callbacks surfaced
  • The default-size guess no longer goes out through onLayout (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

  • A transform is a full snapshot, so an omitted axis clears its pixel inset instead of leaving a stale offset a partial push never repaints
  • alignSelf: 'auto' resolves to the parent's alignItems; space-evenly maps to yoga's own value
  • ResizeObserver shim reports each entry against its own target's rect
  • Alpha hex (8/4 digit), hsl(), and space-form rgb() parse; PlatformColor/OpaqueColorValue drop with a warning instead of throwing
  • fontWeight keeps the full 100-900 scale (numeric strings collapsed to 400 before); text shadows drop with a warning since the SDF renderer has no shadow
  • Text applies textTransform and honors head/middle ellipsize

Reanimated

  • withTiming defaults to Easing.inOut(Easing.quad) instead of linear
  • withRepeat treats any count <= 0 as infinite on both paths
  • useAnimatedStyle restarts only the keys that changed, so an unrelated key isn't restarted mid-flight

Notes

  • Each fix carries a patch changeset. The commits are the ones written in the client repo, replayed onto current main with paths rooted here, so the tree matches our embed exactly.
  • oxlint is clean across all touched files. I left formatting alone: main isn'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 a react-lightning/src deep import and makes snapToAlignment: '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.

RuudBurger and others added 22 commits August 17, 2026 14:20
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.
_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.
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
RuudBurger force-pushed the promote/focus-layout-text-fixes branch from af4e4f4 to ecfdc2f Compare August 17, 2026 12:21
@RuudBurger
RuudBurger requested a review from DouweBos August 17, 2026 12:29
@RuudBurger
RuudBurger marked this pull request as ready for review August 17, 2026 12:29
@RuudBurger
RuudBurger merged commit 55f5849 into main Aug 17, 2026
2 checks passed
@RuudBurger
RuudBurger deleted the promote/focus-layout-text-fixes branch August 17, 2026 13:04
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