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
169 changes: 169 additions & 0 deletions packages/core/src/hooks/useSticky/useSticky.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { act, renderHook } from '@testing-library/react';
import { useSticky, UseStickyReturn } from './useSticky';
import { renderHookServer } from '@/tests';
import { target } from '@/utils/helpers';
import { StateRef } from '../state';

const DEFAULT_MOCK_BOUNDING_DATA = {
top: 0,
left: 0,
bottom: 200,
right: 0,
width: 100,
height: 100,
x: 0,
y: 100,
toJSON: () => {}
};

const targets = [
undefined,
target('#target'),
target(document.getElementById('target')!),
target(() => document.getElementById('target')!),
{ current: document.getElementById('target') }
];

const root = target(document.getElementById('target-root')!);
const element = document.getElementById('target') as HTMLDivElement;

const getBoundingClientRectSpy = vi.spyOn(element, 'getBoundingClientRect');

targets.forEach((target) => {
describe(`${target}`, () => {
it('Should use sticky', () => {
const { result } = renderHook(() => {
if (target)
return useSticky(target) as unknown as {
ref: StateRef<HTMLDivElement>;
stuck: UseStickyReturn;
};
return useSticky<HTMLDivElement>();
});

if (!target) {
expect(result.current.ref).toBeTypeOf('function');
expect(result.current.stuck).toBeTypeOf('boolean');
} else {
expect(result.current).toBeTypeOf('boolean');
}
});

it('Should use sticky on server side', () => {
const { result } = renderHookServer(() => {
if (target)
return useSticky(target) as unknown as {
ref: StateRef<HTMLDivElement>;
stuck: UseStickyReturn;
};
return useSticky<HTMLDivElement>();
});

if (!target) {
expect(result.current.ref).toBeTypeOf('function');
expect(result.current.stuck).toBeTypeOf('boolean');
} else {
expect(result.current).toBeTypeOf('boolean');
}
});

it('Should update stuck element', () => {
const { result } = renderHook(() => {
if (target)
return useSticky(target, { root }) as unknown as {
ref: StateRef<HTMLDivElement>;
stuck: UseStickyReturn;
};
return useSticky<HTMLDivElement>();
});

act(() => {
getBoundingClientRectSpy.mockReturnValue({
...DEFAULT_MOCK_BOUNDING_DATA,
top: 200
});
window.dispatchEvent(new Event('resize'));
});

if (target) {
expect(result.current).toBe(false);
}

act(() => {
getBoundingClientRectSpy.mockReturnValue({
...DEFAULT_MOCK_BOUNDING_DATA,
top: 0
});
window.dispatchEvent(new Event('resize'));
});

if (target) {
expect(result.current).toBe(true);
}
});

it('Should update stuck element with horizontal axis', () => {
const { result } = renderHook(() => {
if (target)
return useSticky(target, {
root,
axis: 'horizontal'
}) as unknown as {
ref: StateRef<HTMLDivElement>;
stuck: UseStickyReturn;
};
return useSticky<HTMLDivElement>();
});

act(() => {
getBoundingClientRectSpy.mockReturnValue({
...DEFAULT_MOCK_BOUNDING_DATA,
left: 200
});
window.dispatchEvent(new Event('resize'));
});

if (target) {
expect(result.current).toBe(false);
}

act(() => {
getBoundingClientRectSpy.mockReturnValue({
...DEFAULT_MOCK_BOUNDING_DATA,
left: 0
});
window.dispatchEvent(new Event('resize'));
});

if (target) {
expect(result.current).toBe(true);
}
});

it('Should clean up on unmount', () => {
const removeEventListenerWindowSpy = vi.spyOn(window, 'removeEventListener');

const removeEventListenerSpy = vi.spyOn(document.documentElement, 'removeEventListener');

const { result, unmount } = renderHook(() => {
if (target)
return useSticky(target) as unknown as {
ref: StateRef<HTMLDivElement>;
stuck: UseStickyReturn;
};
return useSticky<HTMLDivElement>();
});

if (!target) act(() => result.current.ref(element));

unmount();

expect(removeEventListenerSpy).toHaveBeenCalledWith('scroll', expect.any(Function));
expect(removeEventListenerWindowSpy).toHaveBeenCalledWith('resize', expect.any(Function));
expect(removeEventListenerWindowSpy).toHaveBeenCalledWith(
'orientationchange',
expect.any(Function)
);
});
});
});
16 changes: 11 additions & 5 deletions packages/core/src/hooks/useSticky/useSticky.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,20 +71,26 @@ export const useSticky = ((...params: any[]) => {

if (!element) return;

const root = (options?.root ? isTarget.getElement(options.root) : document) as Element;
const elementOffsetTop =
element.getBoundingClientRect().top + root.scrollTop - root.getBoundingClientRect().top;
const elementOffsetLeft =
element.getBoundingClientRect().left + root.scrollLeft - root.getBoundingClientRect().left;
const root = (
options?.root ? isTarget.getElement(options.root) : document.documentElement
) as Element;

const onSticky = () => {
if (axis === 'vertical') {
const scrollTop = root.scrollTop;

const elementOffsetTop =
element.getBoundingClientRect().top + scrollTop - root.getBoundingClientRect().top;

setStuck(scrollTop >= elementOffsetTop);
}

if (axis === 'horizontal') {
const scrollLeft = root.scrollLeft;

const elementOffsetLeft =
element.getBoundingClientRect().left + scrollLeft - root.getBoundingClientRect().left;

setStuck(scrollLeft >= elementOffsetLeft);
}
};
Expand Down
8 changes: 7 additions & 1 deletion packages/core/tests/setupTests.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
if (typeof document !== 'undefined') {
const targetRoot = document.createElement('div');
const target = document.createElement('div');

targetRoot.id = 'target-root';

target.id = 'target';
target.tabIndex = 0;
target.textContent = 'target';
document.body.appendChild(target);

targetRoot.appendChild(target);
document.body.appendChild(targetRoot);
}

afterEach(() => {
Expand Down