mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
fix(epg): window the scroll position a zoom can actually reach
Review follow-up (Greptile, Codex): a zoom-out anchored right of centre at the ribbon's start computed a negative scroll position. The window centred on it, the browser kept scrollLeft at 0, and with the position unchanged no scroll event re-windowed, so the visible right-hand part stayed empty. The centre now uses the position clamped at 0, and once the frame applies scrollLeft the window re-centres on what the browser kept, which also covers the clamp at the end of the track. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5fc8ef3774
commit
623ffc672a
2 files changed
+64
-1
No files matched your search
@@ -44,6 +44,17 @@ function mockSchedule(): EpgProgram[] {
|
||||
);
|
||||
}
|
||||
|
||||
/** Half-hour slots from local midnight two days back: the axis start. */
|
||||
function midnightSchedule(): EpgProgram[] {
|
||||
const midnight = new Date(Date.now() - 2 * 24 * 60 * TIMELINE_MINUTE_MS);
|
||||
midnight.setHours(0, 0, 0, 0);
|
||||
const startOffsetMin =
|
||||
(midnight.getTime() - Date.now()) / TIMELINE_MINUTE_MS;
|
||||
return Array.from({ length: 240 }, (_, index) =>
|
||||
programAt(startOffsetMin + index * SLOT_MIN, `Slot ${index}`)
|
||||
);
|
||||
}
|
||||
|
||||
function item(key: string, leftPx: number, widthPx: number) {
|
||||
return { kind: 'group', key, leftPx, widthPx } as TimelineRenderItem;
|
||||
}
|
||||
@@ -291,6 +302,49 @@ describe('EpgTimelineComponent ribbon windowing', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// At the ribbon's start the anchored position is negative; the browser
|
||||
// keeps scrollLeft at 0 and fires no scroll event to re-window.
|
||||
it('windows the attainable range for a zoom-out at the ribbon start', () => {
|
||||
render(midnightSchedule());
|
||||
// From the deepest zoom, so the unclamped centre would be far off.
|
||||
component.onZoom(6);
|
||||
flushFrames();
|
||||
scrollRibbonTo(0);
|
||||
component.onRibbonWheel(
|
||||
new WheelEvent('wheel', {
|
||||
ctrlKey: true,
|
||||
clientX: ribbonWidth - 20,
|
||||
// 6 → about 1.4: above the grouping zoom, so blocks stay
|
||||
// separate and a missing one shows.
|
||||
deltaY: 727,
|
||||
})
|
||||
);
|
||||
fixture.detectChanges();
|
||||
expect(component.scale()).toBeGreaterThan(1.3);
|
||||
const visible = itemsIn(0, ribbonWidth);
|
||||
expect(visible.length).toBeGreaterThan(0);
|
||||
|
||||
const windowed = component.ribbonWindow.items().map((e) => e.key);
|
||||
expect(windowed).toEqual(expect.arrayContaining(visible));
|
||||
});
|
||||
|
||||
it('re-windows on the scroll position the browser applied', () => {
|
||||
render(mockSchedule());
|
||||
const end = blockLeftPx('Slot 239');
|
||||
component.onZoom(component.scale() * 2);
|
||||
// The browser clamps the anchored position to the end of the track.
|
||||
const scroller = ribbon();
|
||||
let applied = scroller.scrollLeft;
|
||||
Object.defineProperty(scroller, 'scrollLeft', {
|
||||
configurable: true,
|
||||
get: () => applied,
|
||||
set: () => (applied = end * 2),
|
||||
});
|
||||
flushFrames();
|
||||
|
||||
expect(renderedTitles()).toContain('Slot 239');
|
||||
});
|
||||
|
||||
it('windows an expanded group before the ribbon scrolls to it', () => {
|
||||
render(mockSchedule());
|
||||
const first = component
|
||||
|
||||
@@ -99,8 +99,11 @@ export class TimelineZoomController {
|
||||
const currentLeft = this.pendingScrollLeft ?? scroller.scrollLeft;
|
||||
const anchorMin = (currentLeft + anchorPx) / prev;
|
||||
this.pendingScrollLeft = anchorMin * next - anchorPx;
|
||||
// The browser clamps scrollLeft at 0: a zoom-out anchored right of
|
||||
// centre at the ribbon's start lands there, not left of it.
|
||||
this.ctx.centreOn?.(
|
||||
(this.pendingScrollLeft + scroller.clientWidth / 2) / next
|
||||
(Math.max(0, this.pendingScrollLeft) + scroller.clientWidth / 2) /
|
||||
next
|
||||
);
|
||||
if (this.frame === 0) {
|
||||
this.frame = requestAnimationFrame(() => this.flushScroll());
|
||||
@@ -115,6 +118,12 @@ export class TimelineZoomController {
|
||||
const scroller = this.ctx.ribbon();
|
||||
if (scroller && left !== null) {
|
||||
scroller.scrollLeft = left;
|
||||
// Where the browser put it: the end of the track clamps too, and
|
||||
// an unchanged position fires no scroll event to re-window.
|
||||
this.ctx.centreOn?.(
|
||||
(scroller.scrollLeft + scroller.clientWidth / 2) /
|
||||
this.ctx.scale()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user