fix(web): improve scrubber behavior on scroll-limited timelines (#22917)

Improves scroll indicator positioning when scrubbing through timelines with limited scrollable content (e.g., small albums). When a timeline's scrollable height is less than 50% of the viewport height, the scroll position is now properly distributed across the entire scrubber height, making the indicator more responsive and accurate.

Changes:
- Add `limitedScroll` state to detect scroll-constrained timelines (threshold: 50%)
- Introduce `ViewportTopMonth` type to handle lead-in/lead-out sections
- Calculate `totalViewerHeight` including top/bottom sections for accurate positioning
- Refactor scrubber to treat lead-in and lead-out as distinct scroll segments
- Update scroll position calculations to use relative percentages on constrained timelines
This commit is contained in:
Min Idzelis 2025-10-15 13:13:05 -04:00 committed by GitHub
parent 9b5855f848
commit f1e03d0022
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 120 additions and 127 deletions

View file

@ -48,7 +48,9 @@ export class TimelineManager {
isInitialized = $state(false);
months: MonthGroup[] = $state([]);
topSectionHeight = $state(0);
timelineHeight = $derived(this.months.reduce((accumulator, b) => accumulator + b.height, 0) + this.topSectionHeight);
bottomSectionHeight = $state(60);
assetsHeight = $derived(this.months.reduce((accumulator, b) => accumulator + b.height, 0));
totalViewerHeight = $derived(this.topSectionHeight + this.assetsHeight + this.bottomSectionHeight);
assetCount = $derived(this.months.reduce((accumulator, b) => accumulator + b.assetsCount, 0));
albumAssets: Set<string> = new SvelteSet();
@ -62,6 +64,7 @@ export class TimelineManager {
top: this.#scrollTop,
bottom: this.#scrollTop + this.viewportHeight,
}));
limitedScroll = $derived(this.maxScrollPercent < 0.5);
initTask = new CancellableTask(
() => {
@ -383,7 +386,9 @@ export class TimelineManager {
updateGeometry(this, month, { invalidateHeight: changedWidth });
}
this.updateIntersections();
this.#createScrubberMonths();
if (changedWidth) {
this.#createScrubberMonths();
}
}
#createScrubberMonths() {
@ -394,7 +399,7 @@ export class TimelineManager {
title: month.monthGroupTitle,
height: month.height,
}));
this.scrubberTimelineHeight = this.timelineHeight;
this.scrubberTimelineHeight = this.totalViewerHeight;
}
createLayoutOptions() {
@ -408,6 +413,16 @@ export class TimelineManager {
};
}
get maxScrollPercent() {
const totalHeight = this.totalViewerHeight;
const max = (totalHeight - this.viewportHeight) / totalHeight;
return max;
}
get maxScroll() {
return this.totalViewerHeight - this.viewportHeight;
}
async loadMonthGroup(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }): Promise<void> {
let cancelable = true;
if (options) {