Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ All notable changes to this project will be documented in this file. The format
- **withScrolledInView:** improve performance ([#645](https://github.com/studiometa/js-toolkit/pull/645), [83b65f16](https://github.com/studiometa/js-toolkit/commit/83b65f16))
- **useScroll:** avoid layout thrashing ([#645](https://github.com/studiometa/js-toolkit/pull/645), [158d0b66](https://github.com/studiometa/js-toolkit/commit/158d0b66))

### Fixed

- **scrollTo:** fix scroll to be always instant ([#644](https://github.com/studiometa/js-toolkit/pull/644), [6e018553](https://github.com/studiometa/js-toolkit/commit/6e018553))

## [v3.0.4](https://github.com/studiometa/js-toolkit/compare/3.0.3..3.0.4) (2025-05-12)

### Changed
Expand Down
1 change: 1 addition & 0 deletions packages/js-toolkit/utils/scrollTo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export function scrollTo(
rootElement.scrollTo({
left: lerp(initialScrollPosition.left, targetScrollPosition.left, progress),
top: lerp(initialScrollPosition.top, targetScrollPosition.top, progress),
behavior: 'instant',
});
},
{
Expand Down
88 changes: 80 additions & 8 deletions packages/tests/utils/scrollTo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,45 +48,45 @@ describe('The `scrollTo` function', () => {
expect(fn).not.toHaveBeenCalled();
scrollTo('div');
await advanceTimersByTimeAsync(2000);
expect(fn).toHaveBeenLastCalledWith({ left: 0, top: 5000 });
expect(fn).toHaveBeenLastCalledWith({ left: 0, top: 5000, behavior: 'instant' });
});

it('should scroll to an element', async () => {
expect(fn).not.toHaveBeenCalled();
scrollTo(element);
await advanceTimersByTimeAsync(2000);
expect(fn).toHaveBeenLastCalledWith({ left: 0, top: 5000 });
expect(fn).toHaveBeenLastCalledWith({ left: 0, top: 5000, behavior: 'instant' });
});

it('should scroll to a numeric value', async () => {
expect(fn).not.toHaveBeenCalled();
scrollTo(800);
await advanceTimersByTimeAsync(2000);
expect(fn).toHaveBeenLastCalledWith({ left: 0, top: 800 });
expect(fn).toHaveBeenLastCalledWith({ left: 0, top: 800, behavior: 'instant' });
});

it('should scroll to a specific top numeric value', async () => {
expect(fn).not.toHaveBeenCalled();
scrollTo({ top: 1600 });
await advanceTimersByTimeAsync(2000);
expect(fn).toHaveBeenLastCalledWith({ left: 0, top: 1600 });
expect(fn).toHaveBeenLastCalledWith({ left: 0, top: 1600, behavior: 'instant' });
});

it('should scroll to a specific left numeric value', async () => {
expect(fn).not.toHaveBeenCalled();
scrollTo({ left: 1600 });
await advanceTimersByTimeAsync(2000);
expect(fn).toHaveBeenLastCalledWith({ left: 1600, top: 0 });
expect(fn).toHaveBeenLastCalledWith({ left: 1600, top: 0, behavior: 'instant' });
});

it('should scroll to a left and top numeric value', async () => {
expect(fn).not.toHaveBeenCalled();
scrollTo({ left: 1600, top: 1600 }, { axis: scrollTo.axis.both });
await advanceTimersByTimeAsync(2000);
expect(fn).toHaveBeenLastCalledWith({ top: 1600, left: 1600 });
expect(fn).toHaveBeenLastCalledWith({ top: 1600, left: 1600, behavior: 'instant' });
scrollTo({ left: 800 }, { axis: scrollTo.axis.x });
await advanceTimersByTimeAsync(2000);
expect(fn).toHaveBeenLastCalledWith({ top: 1600, left: 800 });
expect(fn).toHaveBeenLastCalledWith({ top: 1600, left: 800, behavior: 'instant' });
});

it('should not scroll to an inexistant element', async () => {
Expand All @@ -105,7 +105,7 @@ describe('The `scrollTo` function', () => {
const maxScroll = document.documentElement.scrollHeight - window.innerHeight;
scrollTo(element);
await advanceTimersByTimeAsync(2000);
expect(fn).toHaveBeenLastCalledWith({ left: 0, top: maxScroll });
expect(fn).toHaveBeenLastCalledWith({ left: 0, top: maxScroll, behavior: 'instant' });
});

it('should stop scrolling with wheel event', async () => {
Expand All @@ -117,6 +117,7 @@ describe('The `scrollTo` function', () => {
await advanceTimersByTimeAsync(100);
expect(fn2).toHaveBeenCalledTimes(1);
const [args] = fn.mock.calls.pop();
delete args.behavior;
expect(fn2).toHaveBeenLastCalledWith(args);
});

Expand All @@ -129,6 +130,7 @@ describe('The `scrollTo` function', () => {
await advanceTimersByTimeAsync(100);
expect(fn2).toHaveBeenCalledTimes(1);
const [args] = fn.mock.calls.pop();
delete args.behavior;
expect(fn2).toHaveBeenLastCalledWith(args);
});

Expand Down Expand Up @@ -161,4 +163,74 @@ describe('The `scrollTo` function', () => {
expect(div.scrollTop).toBe(2000);
expect(div.scrollLeft).toBe(2000);
});

it('should respect scroll margin properties', async () => {
elementSpy.mockImplementation(() => ({
top: 1000,
left: 500,
}));

const getComputedStyleSpy = vi.spyOn(window, 'getComputedStyle');
// @ts-expect-error partial mock
getComputedStyleSpy.mockReturnValue({
scrollMarginTop: '20px',
scrollMarginLeft: '10px',
});

scrollTo(element);
await advanceTimersByTimeAsync(2000);

expect(fn).toHaveBeenLastCalledWith({ left: 0, top: 980, behavior: 'instant' });

getComputedStyleSpy.mockRestore();
});

it('should apply offset to scroll position', async () => {
expect(fn).not.toHaveBeenCalled();
scrollTo(1000, { offset: 100 });
await advanceTimersByTimeAsync(2000);
expect(fn).toHaveBeenLastCalledWith({ left: 0, top: 900, behavior: 'instant' });
});

it('should apply offset to element scroll position', async () => {
elementSpy.mockImplementation(() => ({
top: 1000,
left: 500,
}));

const getComputedStyleSpy = vi.spyOn(window, 'getComputedStyle');
// @ts-expect-error partial mock
getComputedStyleSpy.mockReturnValue({
scrollMarginTop: '0px',
scrollMarginLeft: '0px',
});

scrollTo(element, { offset: 200 });
await advanceTimersByTimeAsync(2000);

expect(fn).toHaveBeenLastCalledWith({ left: 0, top: 800, behavior: 'instant' });

getComputedStyleSpy.mockRestore();
});

it('should apply offset to both axes', async () => {
scrollTo({ left: 1000, top: 1000 }, { offset: 100, axis: scrollTo.axis.both });
await advanceTimersByTimeAsync(2000);
expect(fn).toHaveBeenLastCalledWith({ left: 900, top: 900, behavior: 'instant' });
});

it('should use behavior instant in scrollTo call', async () => {
const scrollToSpy = vi.spyOn(window, 'scrollTo');

scrollTo(500);
await advanceTimersByTimeAsync(100);

expect(scrollToSpy).toHaveBeenCalledWith(
expect.objectContaining({
behavior: 'instant',
}),
);

scrollToSpy.mockRestore();
});
});
Loading