diff --git a/app/components/place-photos-carousel.gjs b/app/components/place-photos-carousel.gjs
index d4ac75e..46e5b6d 100644
--- a/app/components/place-photos-carousel.gjs
+++ b/app/components/place-photos-carousel.gjs
@@ -123,11 +123,11 @@ export default class PlacePhotosCarousel extends Component {
{{#if photo.thumbUrl}}
{{/if}}
{
element.classList.remove('loaded');
element.classList.remove('loaded-instant');
- // Create an off-DOM image to reliably check cache status
- // without waiting for the actual DOM element to load it
- const img = new Image();
- img.src = url;
-
- if (img.complete) {
- // Already in browser cache, skip the animation
- element.classList.add('loaded-instant');
- return;
- }
+ let observer;
const handleLoad = () => {
- element.classList.add('loaded');
+ // Only apply the fade-in animation if it wasn't already loaded instantly
+ if (!element.classList.contains('loaded-instant')) {
+ element.classList.add('loaded');
+ }
};
element.addEventListener('load', handleLoad);
+ const loadWhenVisible = (entries, obs) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ // Stop observing once we start loading
+ obs.unobserve(element);
+
+ // Check if the image is already in the browser cache
+ // Create an off-DOM image to reliably check cache status
+ // without waiting for the actual DOM element to load it
+ const img = new Image();
+ img.src = url;
+
+ if (img.complete) {
+ // Already in browser cache, skip the animation
+ element.classList.add('loaded-instant');
+ }
+
+ // If this image is inside a tag, we also need to swap tags
+ const parent = element.parentElement;
+ if (parent && parent.tagName === 'PICTURE') {
+ const sources = parent.querySelectorAll('source');
+ sources.forEach((source) => {
+ if (source.dataset.srcset) {
+ source.srcset = source.dataset.srcset;
+ }
+ });
+ }
+
+ // Swap data-src to src to trigger the actual network fetch (or render from cache)
+ if (element.dataset.src) {
+ element.src = element.dataset.src;
+ } else {
+ // Fallback if data-src wasn't used but the modifier was called
+ element.src = url;
+ }
+ }
+ });
+ };
+
+ // Setup Intersection Observer to only load when the image enters the viewport
+ observer = new IntersectionObserver(loadWhenVisible, {
+ root: null, // Use the viewport as the root
+ rootMargin: '100px 100%', // Load one full viewport width ahead/behind
+ threshold: 0, // Trigger immediately when any part enters the expanded margin
+ });
+
+ observer.observe(element);
+
return () => {
element.removeEventListener('load', handleLoad);
+ if (observer) {
+ observer.disconnect();
+ }
};
});
diff --git a/tests/integration/components/place-photos-carousel-test.gjs b/tests/integration/components/place-photos-carousel-test.gjs
index 2c8f5c5..6c3488d 100644
--- a/tests/integration/components/place-photos-carousel-test.gjs
+++ b/tests/integration/components/place-photos-carousel-test.gjs
@@ -43,7 +43,7 @@ module('Integration | Component | place-photos-carousel', function (hooks) {
assert.dom('.carousel-slide').exists({ count: 1 }, 'it renders one slide');
assert
.dom('img.place-header-photo')
- .hasAttribute('src', 'photo1.jpg', 'it renders the photo');
+ .hasAttribute('data-src', 'photo1.jpg', 'it sets the data-src correctly');
// There should be no chevrons when there's only 1 photo
assert