fix: responsive: timeline glitch and keyboard-accessible scrubber ()

* fix: responsive: timeline glitch

* lint

* fix margin-right on mobile

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Min Idzelis 2025-04-14 12:56:40 -04:00 committed by GitHub
parent 664c99278a
commit 5a51ad3622
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 427 additions and 142 deletions
web/src/lib/components/photos-page

View file

@ -78,13 +78,19 @@
let scrubBucketPercent = $state(0);
let scrubBucket: { bucketDate: string | undefined } | undefined = $state();
let scrubOverallPercent: number = $state(0);
let scrubberWidth = $state(0);
// 60 is the bottom spacer element at 60px
let bottomSectionHeight = 60;
let leadout = $state(false);
const maxMd = $derived(mobileDevice.maxMd);
const usingMobileDevice = $derived(mobileDevice.pointerCoarse);
$effect(() => {
assetStore.rowHeight = maxMd ? 100 : 235;
});
const scrollTo = (top: number) => {
element?.scrollTo({ top });
showSkeleton = false;
@ -162,7 +168,13 @@
const updateIsScrolling = () => (assetStore.scrolling = true);
// note: don't throttle, debounch, or otherwise do this function async - it causes flicker
const updateSlidingWindow = () => assetStore.updateSlidingWindow(element?.scrollTop || 0);
const compensateScrollCallback = (delta: number) => element?.scrollBy(0, delta);
const compensateScrollCallback = ({ delta, top }: { delta?: number; top?: number }) => {
if (delta) {
element?.scrollBy(0, delta);
} else if (top) {
element?.scrollTo({ top });
}
};
const topSectionResizeObserver: OnResizeCallback = ({ height }) => (assetStore.topSectionHeight = height);
onMount(() => {
@ -267,10 +279,21 @@
bucket = assetStore.buckets[i];
bucketHeight = assetStore.buckets[i].bucketHeight;
}
let next = top - bucketHeight * maxScrollPercent;
if (next < 0) {
// instead of checking for < 0, add a little wiggle room for subpixel resolution
if (next < -1 && bucket) {
scrubBucket = bucket;
scrubBucketPercent = top / (bucketHeight * maxScrollPercent);
// allowing next to be at least 1 may cause percent to go negative, so ensure positive percentage
scrubBucketPercent = Math.max(0, top / (bucketHeight * maxScrollPercent));
// compensate for lost precision/rouding errors advance to the next bucket, if present
if (scrubBucketPercent > 0.9999 && i + 1 < bucketsLength - 1) {
scrubBucket = assetStore.buckets[i + 1];
scrubBucketPercent = 0;
}
found = true;
break;
}
@ -689,7 +712,6 @@
{#if assetStore.buckets.length > 0}
<Scrubber
invisible={showSkeleton}
{assetStore}
height={assetStore.viewportHeight}
timelineTopOffset={assetStore.topSectionHeight}
@ -699,6 +721,7 @@
{scrubBucketPercent}
{scrubBucket}
{onScrub}
bind:scrubberWidth
onScrubKeyDown={(evt) => {
evt.preventDefault();
let amount = 50;
@ -720,12 +743,8 @@
<!-- Right margin MUST be equal to the width of immich-scrubbable-scrollbar -->
<section
id="asset-grid"
class={[
'scrollbar-hidden h-full overflow-y-auto outline-none',
{ 'm-0': isEmpty },
{ 'ml-0': !isEmpty },
{ 'mr-[60px]': !isEmpty && !usingMobileDevice },
]}
class={['scrollbar-hidden h-full overflow-y-auto outline-none', { 'm-0': isEmpty }, { 'ml-0': !isEmpty }]}
style:margin-right={(usingMobileDevice ? 0 : scrubberWidth) + 'px'}
tabindex="-1"
bind:clientHeight={assetStore.viewportHeight}
bind:clientWidth={null, (v) => ((assetStore.viewportWidth = v), updateSlidingWindow())}
@ -763,7 +782,7 @@
style:transform={`translate3d(0,${absoluteHeight}px,0)`}
style:width="100%"
>
<Skeleton height={bucket.bucketHeight} title={bucket.bucketDateFormatted} />
<Skeleton height={bucket.bucketHeight - bucket.store.headerHeight} title={bucket.bucketDateFormatted} />
</div>
{:else if display}
<div
@ -788,7 +807,14 @@
</div>
{/if}
{/each}
<!-- <div class="h-[60px]" style:position="absolute" style:left="0" style:right="0" style:bottom="0"></div> -->
<!-- spacer for leadout -->
<div
class="h-[60px]"
style:position="absolute"
style:left="0"
style:right="0"
style:transform={`translate3d(0,${assetStore.timelineHeight}px,0)`}
></div>
</section>
</section>