Skip to content

Commit 6e8dffd

Browse files
authored
Merge pull request #185 from zeroqs/feat/useScrollTo
feat: add useScrollTo
2 parents a96aff0 + 765ba0e commit 6e8dffd

File tree

4 files changed

+208
-112
lines changed

4 files changed

+208
-112
lines changed

src/hooks/index.ts

Lines changed: 74 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,74 @@
1-
export * from './useActiveElement/useActiveElement';
2-
export * from './useAsync/useAsync';
3-
export * from './useBattery/useBattery';
4-
export * from './useBoolean/useBoolean';
5-
export * from './useBreakpoints/useBreakpoints';
6-
export * from './useBrowserLanguage/useBrowserLanguage';
7-
export * from './useClickOutside/useClickOutside';
8-
export * from './useClipboard/useClipboard';
9-
export * from './useConst/useConst';
10-
export * from './useConst/useConst';
11-
export * from './useCounter/useCounter';
12-
export * from './useCssVar/useCssVar';
13-
export * from './useDebounceCallback/useDebounceCallback';
14-
export * from './useDebounceValue/useDebounceValue';
15-
export * from './useDefault/useDefault';
16-
export * from './useDeviceMotion/useDeviceMotion';
17-
export * from './useDeviceOrientation/useDeviceOrientation';
18-
export * from './useDidUpdate/useDidUpdate';
19-
export * from './useDisclosure/useDisclosure';
20-
export * from './useDocumentEvent/useDocumentEvent';
21-
export * from './useDocumentTitle/useDocumentTitle';
22-
export * from './useDocumentVisibility/useDocumentVisibility';
23-
export * from './useElementSize/useElementSize';
24-
export * from './useEvent/useEvent';
25-
export * from './useEventListener/useEventListener';
26-
export * from './useEyeDropper/useEyeDropper';
27-
export * from './useFavicon/useFavicon';
28-
export * from './useField/useField';
29-
export * from './useFileDialog/useFileDialog';
30-
export * from './useFocus/useFocus';
31-
export * from './useFps/useFps';
32-
export * from './useFullscreen/useFullscreen';
33-
export * from './useGamepad/useGamepad';
34-
export * from './useGeolocation/useGeolocation';
35-
export * from './useHash/useHash';
36-
export * from './useHotkeys/useHotkeys';
37-
export * from './useHover/useHover';
38-
export * from './useIdle/useIdle';
39-
export * from './useImage/useImage';
40-
export * from './useInfiniteScroll/useInfiniteScroll';
41-
export * from './useIntersectionObserver/useIntersectionObserver';
42-
export * from './useInterval/useInterval';
43-
export * from './useIsFirstRender/useIsFirstRender';
44-
export * from './useIsomorphicLayoutEffect/useIsomorphicLayoutEffect';
45-
export * from './useKeyboard/useKeyboard';
46-
export * from './useKeyPress/useKeyPress';
47-
export * from './useKeyPressEvent/useKeyPressEvent';
48-
export * from './useKeysPressed/useKeysPressed';
49-
export * from './useLastChanged/useLastChanged';
50-
export * from './useLatest/useLatest';
51-
export * from './useList/useList';
52-
export * from './useLocalStorage/useLocalStorage';
53-
export * from './useLogger/useLogger';
54-
export * from './useLongPress/useLongPress';
55-
export * from './useMap/useMap';
56-
export * from './useMeasure/useMeasure';
57-
export * from './useMediaQuery/useMediaQuery';
58-
export * from './useMemory/useMemory';
59-
export * from './useMount/useMount';
60-
export * from './useMouse/useMouse';
61-
export * from './useMutation/useMutation';
62-
export * from './useMutationObserver/useMutationObserver';
63-
export * from './useNetwork/useNetwork';
64-
export * from './useOffsetPagination/useOffsetPagination';
65-
export * from './useOnline/useOnline';
66-
export * from './useOperatingSystem/useOperatingSystem';
67-
export * from './useOptimistic/useOptimistic';
68-
export * from './useOrientation/useOrientation';
69-
export * from './useOtpCredential/useOtpCredential';
70-
export * from './usePageLeave/usePageLeave';
71-
export * from './usePaint/usePaint';
72-
export * from './useParallax/useParallax';
73-
export * from './usePermission/usePermission';
74-
export * from './usePointerLock/usePointerLock';
75-
export * from './usePostMessage/usePostMessage';
76-
export * from './usePreferredColorScheme/usePreferredColorScheme';
77-
export * from './usePreferredContrast/usePreferredContrast';
78-
export * from './usePreferredDark/usePreferredDark';
79-
export * from './usePreferredLanguages/usePreferredLanguages';
80-
export * from './usePreferredReducedMotion/usePreferredReducedMotion';
81-
export * from './usePrevious/usePrevious';
82-
export * from './useQuery/useQuery';
83-
export * from './useQueue/useQueue';
84-
export * from './useRaf/useRaf';
85-
export * from './useRafValue/useRafValue';
86-
export * from './useRenderCount/useRenderCount';
87-
export * from './useRenderInfo/useRenderInfo';
88-
export * from './useRerender/useRerender';
89-
export * from './useResizeObserver/useResizeObserver';
90-
export * from './useScreenOrientation/useScreenOrientation';
91-
export * from './useScript/useScript';
92-
export * from './useScroll/useScroll';
93-
export * from './useSessionStorage/useSessionStorage';
94-
export * from './useSet/useSet';
95-
export * from './useShare/useShare';
96-
export * from './useStep/useStep';
97-
export * from './useStopwatch/useStopwatch';
98-
export * from './useStorage/useStorage';
99-
export * from './useTextDirection/useTextDirection';
100-
export * from './useTextSelection/useTextSelection';
101-
export * from './useTime/useTime';
102-
export * from './useTimeout/useTimeout';
103-
export * from './useTimer/useTimer';
104-
export * from './useToggle/useToggle';
105-
export * from './useUnmount/useUnmount';
106-
export * from './useVibrate/useVibrate';
107-
export * from './useWebSocket/useWebSocket';
108-
export * from './useWindowEvent/useWindowEvent';
109-
export * from './useWindowFocus/useWindowFocus';
110-
export * from './useWindowScroll/useWindowScroll';
111-
export * from './useWindowSize/useWindowSize';
112-
export * from './useWizard/useWizard';
1+
export { useBattery } from './useBattery/useBattery';
2+
export { useBoolean } from './useBoolean/useBoolean';
3+
export { useBrowserLanguage } from './useBrowserLanguage/useBrowserLanguage';
4+
export { useClickOutside } from './useClickOutside/useClickOutside';
5+
export { useCopyToClipboard } from './useCopyToClipboard/useCopyToClipboard';
6+
export { useCounter } from './useCounter/useCounter';
7+
export { useDebounceCallback } from './useDebounceCallback/useDebounceCallback';
8+
export { useDebounceValue } from './useDebounceValue/useDebounceValue';
9+
export { useDefault } from './useDefault/useDefault';
10+
export { useDidUpdate } from './useDidUpdate/useDidUpdate';
11+
export { useDisclosure } from './useDisclosure/useDisclosure';
12+
export { useDocumentEvent } from './useDocumentEvent/useDocumentEvent';
13+
export { useDocumentTitle } from './useDocumentTitle/useDocumentTitle';
14+
export { useDocumentVisibility } from './useDocumentVisibility/useDocumentVisibility';
15+
export { useEvent } from './useEvent/useEvent';
16+
export { useEventListener } from './useEventListener/useEventListener';
17+
export { useEyeDropper } from './useEyeDropper/useEyeDropper';
18+
export { useFavicon } from './useFavicon/useFavicon';
19+
export { useField } from './useField/useField';
20+
export { useFps } from './useFps/useFps';
21+
export { useFullscreen } from './useFullscreen/useFullscreen';
22+
export { useHash } from './useHash/useHash';
23+
export { useHotkeys } from './useHotkeys/useHotkeys';
24+
export { useHover } from './useHover/useHover';
25+
export { useIdle } from './useIdle/useIdle';
26+
export { useIntersectionObserver } from './useIntersectionObserver/useIntersectionObserver';
27+
export { useInterval } from './useInterval/useInterval';
28+
export { useIsFirstRender } from './useIsFirstRender/useIsFirstRender';
29+
export { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect/useIsomorphicLayoutEffect';
30+
export { useKeyboard } from './useKeyboard/useKeyboard';
31+
export { useKeyPress } from './useKeyPress/useKeyPress';
32+
export { useKeyPressEvent } from './useKeyPressEvent/useKeyPressEvent';
33+
export { useKeysPressed } from './useKeysPressed/useKeysPressed';
34+
export { useList } from './useList/useList';
35+
export { useLocalStorage } from './useLocalStorage/useLocalStorage';
36+
export { useLogger } from './useLogger/useLogger';
37+
export { useLongPress } from './useLongPress/useLongPress';
38+
export { useMap } from './useMap/useMap';
39+
export { useMediaQuery } from './useMediaQuery/useMediaQuery';
40+
export { useMemory } from './useMemory/useMemory';
41+
export { useMount } from './useMount/useMount';
42+
export { useMouse } from './useMouse/useMouse';
43+
export { useMutation } from './useMutation/useMutation';
44+
export { useNetwork } from './useNetwork/useNetwork';
45+
export { useOnline } from './useOnline/useOnline';
46+
export { useOperatingSystem } from './useOperatingSystem/useOperatingSystem';
47+
export { useOrientation } from './useOrientation/useOrientation';
48+
export { usePageLeave } from './usePageLeave/usePageLeave';
49+
export { usePaint } from './usePaint/usePaint';
50+
export { usePermission } from './usePermission/usePermission';
51+
export { usePreferredColorScheme } from './usePreferredColorScheme/usePreferredColorScheme';
52+
export { usePreferredLanguages } from './usePreferredLanguages/usePreferredLanguages';
53+
export { usePrevious } from './usePrevious/usePrevious';
54+
export { useQuery } from './useQuery/useQuery';
55+
export { useQueue } from './useQueue/useQueue';
56+
export { useRenderCount } from './useRenderCount/useRenderCount';
57+
export { useRenderInfo } from './useRenderInfo/useRenderInfo';
58+
export { useRerender } from './useRerender/useRerender';
59+
export { useScript } from './useScript/useScript';
60+
export { useScrollTo } from './useScrollTo/useScrollTo';
61+
export { useSessionStorage } from './useSessionStorage/useSessionStorage';
62+
export { useSet } from './useSet/useSet';
63+
export { useShare } from './useShare/useShare';
64+
export { useStep } from './useStep/useStep';
65+
export { useStorage } from './useStorage/useStorage';
66+
export { useTextSelection } from './useTextSelection/useTextSelection';
67+
export { useTime } from './useTime/useTime';
68+
export { useTimeout } from './useTimeout/useTimeout';
69+
export { useToggle } from './useToggle/useToggle';
70+
export { useUnmount } from './useUnmount/useUnmount';
71+
export { useWebSocket } from './useWebSocket/useWebSocket';
72+
export { useWindowEvent } from './useWindowEvent/useWindowEvent';
73+
export { useWindowSize } from './useWindowSize/useWindowSize';
74+
export { useWizard } from './useWizard/useWizard';
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useScrollTo } from "@/hooks/useScrollTo/useScrollTo";
2+
3+
const blockStyle = {
4+
border: '1px solid gray',
5+
height: 300,
6+
width: 300,
7+
}
8+
9+
const Demo = () => {
10+
const {targetToScroll, scrollToTarget} = useScrollTo<HTMLDivElement>();
11+
12+
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
13+
event.preventDefault();
14+
event.stopPropagation();
15+
scrollToTarget();
16+
};
17+
18+
19+
return (
20+
<>
21+
<button onClick={handleClick}>Scroll to block 3</button>
22+
<div style={{overflow: 'auto', height: '320px', position: 'relative' }}>
23+
<div style={blockStyle}>Block 1</div>
24+
<div style={blockStyle}>Block 2</div>
25+
<div ref={targetToScroll} style={blockStyle}>Block 3</div>
26+
</div>
27+
</>
28+
)
29+
}
30+
31+
export default Demo
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { act, renderHook } from '@testing-library/react';
2+
import { describe, expect, it, vi } from 'vitest';
3+
4+
import { useScrollTo } from './useScrollTo';
5+
6+
describe('useScrollTo', () => {
7+
it('should define scrollTo and targetRef', () => {
8+
const { result } = renderHook(() => useScrollTo());
9+
10+
expect(result.current.scrollToTarget).toBeDefined();
11+
expect(result.current.targetToScroll).toBeDefined();
12+
});
13+
14+
it('should scroll to target element', () => {
15+
const { result } = renderHook(() => useScrollTo());
16+
const scrollIntoViewMock = vi.fn();
17+
18+
const mockElement = document.createElement('div');
19+
mockElement.scrollIntoView = scrollIntoViewMock;
20+
21+
act(() => {
22+
(result.current.targetToScroll as React.MutableRefObject<HTMLElement | null>).current =
23+
mockElement;
24+
});
25+
26+
act(() => {
27+
result.current.scrollToTarget();
28+
});
29+
30+
expect(scrollIntoViewMock).toHaveBeenCalledWith({
31+
behavior: 'smooth',
32+
block: 'nearest',
33+
inline: 'nearest'
34+
});
35+
});
36+
37+
it('should not scroll if targetRef is not set', () => {
38+
const { result } = renderHook(() => useScrollTo());
39+
const scrollIntoViewMock = vi.fn();
40+
41+
act(() => {
42+
result.current.scrollToTarget();
43+
});
44+
45+
expect(scrollIntoViewMock).not.toHaveBeenCalled();
46+
});
47+
});

src/hooks/useScrollTo/useScrollTo.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useRef } from 'react';
2+
3+
const OPTIONS_DEFAULT: ScrollIntoViewOptions = {
4+
/**
5+
* Defines the transition animation.
6+
* One of 'auto' or 'smooth'.
7+
*
8+
* @default 'smooth'
9+
*/
10+
behavior: 'smooth',
11+
/**
12+
* Defines vertical alignment.
13+
* One of `'start'`, `'center'`, `'end'`, or `'nearest'`
14+
*
15+
* @default 'nearest'
16+
*/
17+
block: 'nearest',
18+
/**
19+
* Defines horizontal alignment.
20+
* One of `start`, `center`, `end`, or `nearest`. Defaults to nearest.
21+
*
22+
* @default 'nearest'
23+
*/
24+
inline: 'nearest'
25+
};
26+
27+
interface UseScrollToReturn<T extends HTMLElement> {
28+
targetToScroll: React.RefObject<T>;
29+
scrollToTarget: () => void;
30+
}
31+
32+
/**
33+
* @name useScrollTo
34+
* @description Hook that provides a function to smoothly scroll to a target element.
35+
*
36+
* @param {ScrollIntoViewOptions} [options=OPTIONS_DEFAULT] - Options for the scrollIntoView method.
37+
*
38+
* @returns {Object} An object containing the reference to the target element and the function to scroll to it.
39+
* @returns {React.RefObject<T | null>} targetToScroll - The ref object to be attached to the element you want to scroll to.
40+
* @returns {function} scrollToTarget - The function to call to scroll to the target element.
41+
*
42+
* @example
43+
* const { targetToScroll, scrollToTarget } = useScrollTo();
44+
*/
45+
export const useScrollTo = <T extends HTMLElement>(
46+
options: ScrollIntoViewOptions = OPTIONS_DEFAULT
47+
): UseScrollToReturn<T> => {
48+
const targetToScroll = useRef<T | null>(null);
49+
50+
const scrollToTarget = () => {
51+
if (!targetToScroll.current) return;
52+
targetToScroll.current.scrollIntoView(options);
53+
};
54+
55+
return { targetToScroll, scrollToTarget };
56+
};

0 commit comments

Comments
 (0)