Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/assets/natural-gallery-js/natural-gallery.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/assets/natural-gallery-js/natural-gallery.js.map

Large diffs are not rendered by default.

18 changes: 12 additions & 6 deletions src/js/galleries/AbstractGallery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -667,20 +667,26 @@ export abstract class AbstractGallery<Model extends ModelAttributes = ModelAttri
const scrollable = element;
const wrapper: HTMLElement = element instanceof Document ? element.documentElement : element;

const startScroll = debounce(() => this.elementRef.classList.add('scrolling'), 300, {edges: ['leading']});
const endScroll = debounce(() => this.elementRef.classList.remove('scrolling'), 300);
const startScroll = debounce(() => this.elementRef.classList.add('scrolling'), 100, {edges: ['leading']});
const endScroll = debounce(() => this.elementRef.classList.remove('scrolling'), 150);

scrollable.addEventListener('scroll', () => {
startScroll();
endScroll();

const endOfGalleryAt =
this.elementRef.offsetTop + this.elementRef.offsetHeight + this.options.infiniteScrollOffset;

// Avoid to expand gallery if we are scrolling up
// Calculate scroll position and delta
const current_scroll_top = wrapper.scrollTop - (wrapper.clientTop || 0);
const wrapperHeight = wrapper.clientHeight;
const scroll_delta = current_scroll_top - this.old_scroll_top;

// Only apply scrolling class if there's actual scroll position change
// This prevents the class from being applied when scroll events are triggered
// without actual scrolling (e.g., on mousehover in some browsers/zoom levels)
if (Math.abs(scroll_delta) > 0) {
startScroll();
endScroll();
}

this.old_scroll_top = current_scroll_top;

// "enableMoreLoading" is a setting coming from the BE bloking / enabling dynamic loading of thumbnail
Expand Down
62 changes: 62 additions & 0 deletions tests/e2e/natural.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,66 @@ test.describe('Natural', () => {
await setup.page.waitForTimeout(600); // wait debounce from gallery
await expect(countItems(setup.page)).toHaveCount(17);
});

test('should not disappear on hover at different zoom levels (issue #101)', async () => {
// Test the specific conditions mentioned in issue #101:
// Gallery disappearing on hover at 75% zoom levels and specific window sizes

// Set up conditions similar to issue #101 report
await setup.page.setViewportSize({width: 1024, height: 768});

// Test at 75% zoom level (mentioned in issue #101)
await setup.page.evaluate(() => {
document.body.style.zoom = '0.75';
});

// Wait for gallery to render
await expect(countItems(setup.page)).toHaveCount(9);

// Find a zoomable image to hover over
const firstImage = setup.page.locator('.figure .image').first();
await expect(firstImage).toBeVisible();

// Check that the gallery is initially visible (not in resizing state)
const galleryBody = setup.page.locator('.natural-gallery-js');
const hasResizingClass = await galleryBody.evaluate(el => el.classList.contains('resizing'));
expect(hasResizingClass).toBe(false);

// Hover over the image to trigger the hover transform
// CSS: .image.zoomable:hover { transform: rotate(1deg) scale(1.2); }
await firstImage.hover();

// Wait a moment for any potential resize detection to trigger
await setup.page.waitForTimeout(100);

// Verify the gallery is still visible (doesn't have resizing class)
const hasResizingClassAfterHover = await galleryBody.evaluate(el => el.classList.contains('resizing'));
expect(hasResizingClassAfterHover).toBe(false);

// Verify items are still visible (gallery hasn't disappeared)
await expect(countItems(setup.page)).toHaveCount(9);

// Test at different zoom levels that might trigger the issue
for (const zoom of ['0.5', '0.67', '0.9', '1.1', '1.25']) {
await setup.page.evaluate((zoomLevel) => {
document.body.style.zoom = zoomLevel;
}, zoom);

// Move mouse away and back to trigger hover again
await setup.page.mouse.move(0, 0);
await setup.page.waitForTimeout(50);
await firstImage.hover();
await setup.page.waitForTimeout(100);

// Gallery should remain visible at all zoom levels
const hasResizingAtZoom = await galleryBody.evaluate(el => el.classList.contains('resizing'));
expect(hasResizingAtZoom).toBe(false);
await expect(countItems(setup.page)).toHaveCount(9);
}

// Reset zoom
await setup.page.evaluate(() => {
document.body.style.zoom = '1';
});
});
});
36 changes: 36 additions & 0 deletions tests/unit/natural.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,40 @@ describe('Natural Gallery', () => {

expect(gallery.collection.map(getSize)).toEqual(result);
});

it('should not apply scrolling class when scroll events are triggered without actual scrolling', done => {
const images: ModelAttributes[] = [
{
thumbnailSrc: 'foo.jpg',
enlargedWidth: 6000,
enlargedHeight: 4000,
},
{
thumbnailSrc: 'bar.jpg',
enlargedWidth: 3648,
enlargedHeight: 5472,
},
];

const container = getContainerElement(500);
const gallery = new Natural(container, {rowHeight: 400});
gallery.addItems(images);

// Simulate a scroll event without actual scroll position change
// This simulates what happens when mouse hover triggers scroll events in some browsers
const scrollEvent = new Event('scroll');

// Initially, the gallery should not have the scrolling class
expect(container.classList.contains('scrolling')).toBe(false);

// Dispatch a scroll event without changing scroll position
document.dispatchEvent(scrollEvent);

// Wait for any potential debounced functions to trigger
setTimeout(() => {
// The scrolling class should NOT be applied since no actual scrolling occurred
expect(container.classList.contains('scrolling')).toBe(false);
done();
}, 200); // Wait longer than the debounce timeout
});
});
Loading