From 30696d037d1d43d03965ce298c19f92d1dd6724b Mon Sep 17 00:00:00 2001 From: 4gray Date: Wed, 1 Jul 2026 18:04:32 +0200 Subject: [PATCH] fix(epg): honor the controlled selectedDate in the timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The timeline declared a `selectedDate` input but never read it: `viewDayKey` seeded to today and only local `commitDay` navigation mutated it, so a programmatic host change was ignored and a preserved non-today date reset to today on (re)mount — the panel showed today/empty-day while the host pointed at another day (the old EpgList honored the controlled input). Flagged by Codex. Seed `viewDayKey` from `selectedDate` via `linkedSignal` so it initializes from and follows the host, while local day navigation still overrides via `.set()`. Steady state is unchanged: the host echoes back the emitted key, so the signal recomputes to the same value (no loop, no flicker). Regression coverage: two specs assert a controlled non-today `selectedDate` renders the ribbon on that day and follows a programmatic change. ui-epg 80, portal-shared-ui + playlist-m3u green; lint clean; component 399 lines. Co-Authored-By: Claude Opus 4.8 --- .../epg-timeline.component.spec.ts | 27 +++++++++++++++++++ .../epg-timeline/epg-timeline.component.ts | 10 +++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.spec.ts b/libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.spec.ts index 087def864..54d88a3b5 100644 --- a/libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.spec.ts +++ b/libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.spec.ts @@ -23,6 +23,13 @@ function programAt( }; } +/** Local yyyy-MM-dd day-key for a programme start (matches the component). */ +function localDateKey(iso: string): string { + const d = new Date(iso); + const pad = (n: number) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; +} + describe('EpgTimelineComponent', () => { let fixture: ComponentFixture; let component: EpgTimelineComponent; @@ -86,6 +93,26 @@ describe('EpgTimelineComponent', () => { expect(component.renderState()).toBe('empty-day'); }); + it('honours a controlled non-today selectedDate instead of resetting to today', () => { + // Data three days out (today empty). Without honouring the controlled + // input the panel would show empty-day for today; with it, the ribbon + // renders on the host-selected day. + const future = programAt(3 * 1440, 60); + setInputs({ + programs: [future], + selectedDate: localDateKey(future.start), + }); + expect(component.renderState()).toBe('ribbon'); + }); + + it('follows a programmatic selectedDate change from the host', () => { + const future = programAt(3 * 1440, 60); + setInputs({ programs: [future] }); + expect(component.renderState()).toBe('empty-day'); // viewing today + setInputs({ selectedDate: localDateKey(future.start) }); + expect(component.renderState()).toBe('ribbon'); // followed the host + }); + it('shows the ribbon controls (Now + zoom) only while a ribbon renders', () => { setInputs({ programs: [programAt(0, 120, 'Now')] }); expect(component.showRibbonControls()).toBe(true); diff --git a/libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.ts b/libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.ts index 9bfef738f..270a636b0 100644 --- a/libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.ts +++ b/libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.ts @@ -7,6 +7,7 @@ import { ElementRef, inject, input, + linkedSignal, output, signal, untracked, @@ -120,8 +121,13 @@ export class EpgTimelineComponent { private readonly nowMs = signal(Date.now()); readonly selectedKey = signal(null); - /** Day currently centred in the ribbon viewport. */ - private readonly viewDayKey = signal(getTodayEpgDateKey()); + /** Day centred in the ribbon. Seeded from the controlled `selectedDate` so a + * non-today date survives (re)mount and follows host changes; local + * navigation overrides via `commitDay`. Falls back to today when unset. */ + private readonly viewDayKey = linkedSignal(() => { + const key = this.selectedDate()?.trim(); + return key ? key : getTodayEpgDateKey(); + }); /** Ribbon scrolling + channel-select auto-focus, extracted from the view. */ private readonly scroll = new TimelineScrollController({