use-scroller 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,62 +1,39 @@
1
- import { useCallback, useRef, useState } from 'react'
2
- import { scroller } from '../scroller'
1
+ import { useRef, useState } from 'react'
2
+ import { createScroller } from '../scroller'
3
3
  import { ScrollOptions } from '../types'
4
4
  import { defaultScrollOptions, getElement } from '../utils'
5
5
  import { useIsoMorphicEffect } from './use-iso-morphic-effect'
6
6
 
7
+ const initialState = {
8
+ left: 0, // Current scroll position on the X-axis
9
+ top: 0, // Current scroll position on the Y-axis
10
+ isScrollable: false, // Determines if the container has content that can be scrolled
11
+ isScrolledLeft: true, // Determines if the container is at its leftmost position
12
+ isScrolledRight: false, // Determines if the container is at its rightmost position
13
+ isScrolledTop: true, // Determines if the container is at its topmost position
14
+ isScrolledBottom: false, // Determines if the container is at its bottommost position
15
+ maxScrollLeft: 0, // Maximum possible scroll on the X-axis
16
+ maxScrollTop: 0, // Maximum possible scroll on the Y-axis
17
+ scrollPercentageX: 0, // Percentage of horizontal scroll covered
18
+ scrollPercentageY: 0, // Percentage of vertical scroll covered
19
+ }
20
+
7
21
  export function useScroll<T extends HTMLElement>(props?: Partial<ScrollOptions>) {
8
22
  const ref = useRef<T>(null)
9
- const [state, setState] = useState(() => ({
10
- left: 0,
11
- top: 0,
12
- isScrollable: false,
13
- isScrolledLeft: false,
14
- isScrolledRight: false,
15
- isScrolledTop: false,
16
- isScrolledBottom: false,
17
- }))
18
23
  const scrollOpt = { ...defaultScrollOptions, ...props }
19
-
20
- const scroll = useCallback((from: number, to: number) => {
21
- const container = getElement(ref)
22
- const scrollContainer = scroller({ container, options: scrollOpt })
23
- scrollContainer.scrollTo(from, to)
24
- }, [])
25
-
26
- const scrollCenter = useCallback(() => {
27
- const container = getElement(ref)
28
- const scrollContainer = scroller({ container, options: scrollOpt })
29
- scrollContainer.scrollToCenter()
30
- }, [])
31
-
32
- const scrollTop = useCallback((offset?: number) => {
33
- const container = getElement(ref)
34
- const scrollContainer = scroller({ container, options: scrollOpt })
35
- scrollContainer.scrollToTop(offset)
36
- }, [])
37
-
38
- const scrollBottom = useCallback((offset?: number) => {
39
- const container = getElement(ref)
40
- const scrollContainer = scroller({ container, options: scrollOpt })
41
- scrollContainer.scrollToBottom(offset)
42
- }, [])
43
-
44
- const scrollRight = useCallback((offset?: number) => {
45
- const container = getElement(ref)
46
- const scrollContainer = scroller({ container, options: scrollOpt })
47
- scrollContainer.scrollToRight(offset)
48
- }, [])
49
-
50
- const scrollLeft = useCallback((offset?: number) => {
51
- const container = getElement(ref)
52
- const scrollContainer = scroller({ container, options: scrollOpt })
53
- scrollContainer.scrollToLeft(offset)
54
- }, [])
24
+ const scrollerRef = useRef<ReturnType<typeof createScroller> | null>(null)
25
+ const [state, setState] = useState(() => initialState)
55
26
 
56
27
  useIsoMorphicEffect(() => {
57
28
  const scrollContainer = ref.current
58
29
  if (!scrollContainer) return
59
30
 
31
+ // Create the scroller instance only once inside the effect
32
+ if (!scrollerRef.current) {
33
+ const container = getElement(ref)
34
+ scrollerRef.current = createScroller(container, scrollOpt)
35
+ }
36
+
60
37
  const { scrollWidth, clientWidth, clientHeight, scrollHeight } = scrollContainer
61
38
  const isScrollable = clientWidth !== scrollWidth || clientHeight !== scrollHeight
62
39
  setState((prev) => ({ ...prev, isScrollable }))
@@ -64,14 +41,26 @@ export function useScroll<T extends HTMLElement>(props?: Partial<ScrollOptions>)
64
41
  const handleScroll = () => {
65
42
  if (scrollContainer) {
66
43
  const { scrollLeft, scrollTop } = scrollContainer
44
+ const isAtLeftBoundary = scrollLeft === 0
45
+ const isAtRightBoundary = scrollLeft >= scrollWidth - clientWidth
46
+ const isAtTopBoundary = scrollTop === 0
47
+ const isAtBottomBoundary = scrollTop >= scrollHeight - clientHeight
67
48
  const scrollState = {
68
49
  ...state,
69
- isScrolledLeft: scrollLeft === 0,
70
- isScrolledRight: scrollLeft >= scrollWidth - clientWidth,
71
- isScrolledTop: scrollTop === 0,
72
- isScrolledBottom: scrollTop >= scrollHeight - clientHeight,
50
+ isScrolledLeft: isAtLeftBoundary,
51
+ isScrolledRight: isAtRightBoundary,
52
+ isScrolledTop: isAtTopBoundary,
53
+ isScrolledBottom: isAtBottomBoundary,
73
54
  left: scrollLeft,
74
55
  top: scrollTop,
56
+ maxScrollLeft: scrollWidth - clientWidth,
57
+ maxScrollTop: scrollHeight - clientHeight,
58
+ scrollPercentageX: isAtLeftBoundary
59
+ ? 0
60
+ : (scrollLeft / (scrollWidth - clientWidth)) * 100,
61
+ scrollPercentageY: isAtTopBoundary
62
+ ? 0
63
+ : (scrollTop / (scrollHeight - clientHeight)) * 100,
75
64
  }
76
65
  setState(scrollState)
77
66
  }
@@ -80,16 +69,16 @@ export function useScroll<T extends HTMLElement>(props?: Partial<ScrollOptions>)
80
69
  handleScroll()
81
70
  scrollContainer?.addEventListener('scroll', handleScroll)
82
71
  return () => scrollContainer?.removeEventListener('scroll', handleScroll)
83
- }, [])
72
+ }, [state])
84
73
 
85
74
  return {
86
75
  ref,
87
76
  state,
88
- scrollLeft,
89
- scrollRight,
90
- scrollBottom,
91
- scrollTop,
92
- scrollCenter,
93
- scroll,
77
+ scrollLeft: scrollerRef.current?.scrollToLeft,
78
+ scrollRight: scrollerRef.current?.scrollToRight,
79
+ scrollBottom: scrollerRef.current?.scrollToBottom,
80
+ scrollTop: scrollerRef.current?.scrollToTop,
81
+ scrollCenter: scrollerRef.current?.scrollToCenter,
82
+ scrollTo: scrollerRef.current?.scrollTo,
94
83
  }
95
84
  }
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useState } from 'react'
2
- import { scroller } from '../scroller'
3
- import { ScrollerProps, ScrollOptions } from '../types'
2
+ import { createScroller } from '../scroller'
3
+ import { ScrollOptions } from '../types'
4
4
  import { useIsoMorphicEffect } from './use-iso-morphic-effect'
5
5
  import { defaultScrollOptions, resolveScrollValues } from '../utils'
6
6
 
@@ -12,49 +12,62 @@ export function useWindowScroll(props?: Partial<Omit<ScrollOptions, 'direction'>
12
12
  isScrolledBottom: false,
13
13
  }))
14
14
 
15
- const scrollArgs: ScrollerProps = {
16
- container: window,
17
- options: { ...defaultScrollOptions, ...props },
18
- }
19
- const scroll = useCallback((from: number, to: number) => {
20
- const scrollContainer = scroller(scrollArgs)
21
- scrollContainer.scrollTo(from, to)
22
- }, [])
23
-
24
- const scrollToTarget = useCallback((currentTarget: HTMLElement) => {
25
- const scrollContainer = scroller(scrollArgs)
26
- scrollContainer.scrollToTarget(currentTarget)
27
- }, [])
28
-
29
- const scrollTop = useCallback((offset?: number | undefined) => {
30
- const scrollContainer = scroller(scrollArgs)
31
- scrollContainer.scrollToTop(offset)
32
- }, [])
33
-
34
- const scrollBottom = useCallback((offset?: number | undefined) => {
35
- const scrollContainer = scroller(scrollArgs)
36
- scrollContainer.scrollToBottom(offset)
37
- }, [])
15
+ const container = window
16
+ const scrollOptions = { ...defaultScrollOptions, ...props }
38
17
 
39
- useIsoMorphicEffect(() => {
40
- const handleScroll = () => {
41
- if (window) {
42
- const { scrollLeft, scrollTop, scrollHeight, clientHeight } = resolveScrollValues(window)
43
- const scrollState = {
44
- ...state,
45
- isScrolledTop: scrollTop === 0,
46
- isScrolledBottom: scrollTop >= scrollHeight - clientHeight,
47
- left: scrollLeft,
48
- top: scrollTop,
49
- }
50
- setState(scrollState)
51
- }
18
+ const scrollContainer = createScroller(container, scrollOptions)
19
+
20
+ const handleScroll = useCallback(() => {
21
+ const { scrollLeft, scrollTop, clientHeight, scrollHeight } = resolveScrollValues(container)
22
+
23
+ const isAtTopBoundary = scrollTop === 0
24
+ const isAtBottomBoundary = scrollTop >= scrollHeight - clientHeight
25
+
26
+ const scrollState = {
27
+ left: scrollLeft,
28
+ top: scrollTop,
29
+ isScrolledTop: isAtTopBoundary,
30
+ isScrolledBottom: isAtBottomBoundary,
52
31
  }
53
32
 
33
+ setState(scrollState)
34
+ }, [container])
35
+
36
+ useIsoMorphicEffect(() => {
54
37
  handleScroll()
55
- window?.addEventListener('scroll', handleScroll)
56
- return () => window?.removeEventListener('scroll', handleScroll)
57
- }, [])
38
+ container.addEventListener('scroll', handleScroll)
39
+ return () => {
40
+ container.removeEventListener('scroll', handleScroll)
41
+ }
42
+ }, [handleScroll])
43
+
44
+ const scrollTo = useCallback(
45
+ (position: number) => {
46
+ scrollContainer.scrollTo(position)
47
+ },
48
+ [scrollContainer]
49
+ )
50
+
51
+ const scrollToTarget = useCallback(
52
+ (currentTarget: HTMLElement) => {
53
+ scrollContainer.scrollToTarget(currentTarget)
54
+ },
55
+ [scrollContainer]
56
+ )
57
+
58
+ const scrollTop = useCallback(
59
+ (offset?: number | undefined) => {
60
+ scrollContainer.scrollToTop(offset)
61
+ },
62
+ [scrollContainer]
63
+ )
64
+
65
+ const scrollBottom = useCallback(
66
+ (offset?: number | undefined) => {
67
+ scrollContainer.scrollToBottom(offset)
68
+ },
69
+ [scrollContainer]
70
+ )
58
71
 
59
- return { state, scrollTop, scrollBottom, scrollToTarget, scroll }
72
+ return { state, scrollTop, scrollBottom, scrollToTarget, scrollTo }
60
73
  }
package/src/scroller.ts CHANGED
@@ -1,44 +1,53 @@
1
- import { ScrollerProps } from './types'
1
+ import { ScrollOptions } from './types'
2
2
  import { defaultScrollOptions, getEasing, resolveScrollValues, validateElement } from './utils'
3
3
 
4
- export function scroller({ container, options = defaultScrollOptions }: ScrollerProps) {
4
+ export function createScroller(
5
+ container: HTMLElement | Window,
6
+ options: ScrollOptions = defaultScrollOptions
7
+ ) {
5
8
  validateElement(container)
6
-
7
9
  const { duration, easingOption, direction } = options
8
10
  const { scrollLeft, scrollTop, clientWidth, clientHeight, scrollWidth, scrollHeight } =
9
11
  resolveScrollValues(container)
10
12
 
11
- /**
12
- * Scroll the Element or Window to the given
13
- * position.
14
- *
15
- * @param {number} from the current scroll position.
16
- * @param {number} to the new position to scroll.
17
- * @param {string} layout the scroll direction.
18
- */
19
- function scrollTo(from: number, to: number, layout: 'horizontal' | 'vertical' = direction) {
13
+ function scroll(from: number, to: number, layout: ScrollOptions['direction'] = direction) {
20
14
  let startTime: number | null = null
21
15
  let requestId = 0
16
+ const distance = Math.abs(to - from)
22
17
 
23
- function loop(currentTime: number) {
24
- if (!startTime) startTime = currentTime
25
-
26
- const time = Math.min((currentTime - startTime) / duration, 1)
18
+ function calculateCurrentPosition(timeFraction: number): number {
27
19
  const easingFunc = getEasing(easingOption)
28
- const currentPositionInTime = easingFunc(time) * (to - from) + from
20
+ return easingFunc(timeFraction) * distance + from
21
+ }
29
22
 
23
+ function updateScrollPosition(currentPosition: number) {
30
24
  container.scrollTo({
31
- left: layout === 'horizontal' ? currentPositionInTime : undefined,
32
- top: layout === 'vertical' ? currentPositionInTime : undefined,
25
+ left: layout === 'horizontal' ? currentPosition : undefined,
26
+ top: layout === 'vertical' ? currentPosition : undefined,
33
27
  })
28
+ }
29
+
30
+ function animateScroll(currentTime: number) {
31
+ if (!startTime) startTime = currentTime
32
+ const elapsedTime = currentTime - startTime
33
+ const timeFraction = elapsedTime / duration
34
+ const currentPosition = calculateCurrentPosition(timeFraction)
35
+
36
+ updateScrollPosition(currentPosition)
34
37
 
35
- if (time < 1) {
36
- requestId = requestAnimationFrame(loop)
38
+ if (timeFraction < 1) {
39
+ requestId = requestAnimationFrame(animateScroll)
37
40
  } else {
38
41
  cancelAnimationFrame(requestId)
39
42
  }
40
43
  }
41
- requestId = requestAnimationFrame(loop)
44
+
45
+ requestId = requestAnimationFrame(animateScroll)
46
+ }
47
+
48
+ function scrollTo(to: number) {
49
+ const from = direction === 'vertical' ? scrollTop : scrollLeft
50
+ scroll(from, to)
42
51
  }
43
52
 
44
53
  function scrollToCenter() {
@@ -56,10 +65,10 @@ export function scroller({ container, options = defaultScrollOptions }: Scroller
56
65
  from = scrollLeft
57
66
  to = scrollXCenterPosition
58
67
  }
59
- scrollTo(from, to, direction)
68
+ scroll(from, to, direction)
60
69
  }
61
70
 
62
- function scrollToTarget(target: HTMLElement) {
71
+ function scrollToTarget(target: HTMLElement, offset = 0) {
63
72
  validateElement(target)
64
73
  const { offsetTop, offsetLeft } = target
65
74
  let from = 0
@@ -67,44 +76,32 @@ export function scroller({ container, options = defaultScrollOptions }: Scroller
67
76
 
68
77
  if (direction === 'vertical') {
69
78
  from = scrollTop
70
- to = offsetTop
79
+ to = offsetTop - offset
71
80
  }
72
81
 
73
82
  if (direction === 'horizontal') {
74
83
  from = scrollLeft
75
- to = offsetLeft
84
+ to = offsetLeft - offset
76
85
  }
77
86
 
78
- scrollTo(from, to, direction)
79
- }
80
-
81
- const scrollToLeft = (offset?: number) => {
82
- const scrollPosition = offset || scrollLeft - clientWidth
83
- scrollTo(scrollLeft, scrollPosition, 'horizontal')
84
- }
85
-
86
- const scrollToRight = (offset?: number) => {
87
- const scrollPosition = offset || scrollLeft + clientWidth
88
- scrollTo(scrollLeft, scrollPosition, 'horizontal')
89
- }
90
-
91
- const scrollToTop = (offset?: number) => {
92
- const scrollPosition = offset || scrollTop - clientHeight
93
- scrollTo(scrollTop, scrollPosition, 'vertical')
94
- }
95
-
96
- const scrollToBottom = (offset?: number) => {
97
- const scrollPosition = offset || scrollTop + clientHeight
98
- scrollTo(scrollTop, scrollPosition, 'vertical')
87
+ scroll(from, to, direction)
99
88
  }
100
89
 
101
90
  return {
102
- scrollToLeft,
103
- scrollToRight,
104
- scrollToTop,
105
- scrollToBottom,
106
- scrollToTarget,
107
- scrollToCenter,
108
91
  scrollTo,
92
+ scrollToCenter,
93
+ scrollToTarget,
94
+ scrollToLeft: (offset?: number) => {
95
+ scroll(scrollLeft, offset || scrollLeft - clientWidth, 'horizontal')
96
+ },
97
+ scrollToRight: (offset?: number) => {
98
+ scroll(scrollLeft, offset || scrollLeft + clientWidth, 'horizontal')
99
+ },
100
+ scrollToTop: (offset?: number) => {
101
+ scroll(scrollTop, offset || scrollTop - clientHeight, 'vertical')
102
+ },
103
+ scrollToBottom: (offset?: number) => {
104
+ scroll(scrollTop, offset || scrollTop + clientHeight, 'vertical')
105
+ },
109
106
  }
110
107
  }
package/src/utils.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import { RefObject } from 'react'
2
2
  import { EasingFunction, EasingFunctions, EasingOptions, ScrollOptions } from './types'
3
3
 
4
+ // Check if the code is running on the server-side.
4
5
  export const isServer = typeof window === 'undefined' || typeof document === 'undefined'
5
6
 
7
+ // Default scroll options used if no options are provided.
6
8
  export const defaultScrollOptions: ScrollOptions = {
7
9
  direction: 'vertical',
8
10
  duration: 300,
@@ -10,37 +12,37 @@ export const defaultScrollOptions: ScrollOptions = {
10
12
  }
11
13
 
12
14
  /**
13
- * Resolves scroll values based on the type of param.
14
- * @param {HTMLElement | Window} element
15
+ * Resolves scroll values based on the type of element.
16
+ * @param {HTMLElement | Window} element - The element to retrieve scroll values from.
17
+ * @returns {object} - An object containing scroll-related properties.
15
18
  */
16
19
  export const resolveScrollValues = (element: HTMLElement | Window) => {
17
- if (
18
- element === document.body ||
19
- element === document.documentElement ||
20
- element instanceof Window
21
- ) {
22
- return {
23
- scrollLeft: document.body.scrollLeft || document.documentElement.scrollLeft,
24
- scrollTop: document.body.scrollTop || document.documentElement.scrollTop,
25
- scrollWidth: document.body.scrollWidth || document.documentElement.scrollWidth,
26
- clientWidth: document.body.clientWidth || document.documentElement.clientWidth,
27
- clientHeight: document.body.clientHeight || document.documentElement.clientHeight,
28
- scrollHeight: document.body.scrollHeight || document.documentElement.scrollHeight,
29
- }
30
- }
20
+ const isWindow = element instanceof Window
21
+ const docElement = document.documentElement
22
+ const docBody = document.body
23
+
24
+ const scrollLeftMax = isWindow
25
+ ? Math.max(docBody.scrollLeft, docElement.scrollLeft)
26
+ : element.scrollWidth - element.clientWidth
27
+ const scrollTopMax = isWindow
28
+ ? Math.max(docBody.scrollTop, docElement.scrollTop)
29
+ : element.scrollHeight - element.clientHeight
30
+
31
31
  return {
32
- scrollLeft: element.scrollLeft,
33
- scrollTop: element.scrollTop,
34
- scrollWidth: element.scrollWidth,
35
- clientWidth: element.clientWidth,
36
- clientHeight: element.clientHeight,
37
- scrollHeight: element.scrollHeight,
32
+ scrollLeft: isWindow ? docBody.scrollLeft || docElement.scrollLeft : element.scrollLeft,
33
+ scrollTop: isWindow ? docBody.scrollTop || docElement.scrollTop : element.scrollTop,
34
+ scrollWidth: isWindow ? docBody.scrollWidth || docElement.scrollWidth : element.scrollWidth,
35
+ clientWidth: isWindow ? docBody.clientWidth || docElement.clientWidth : element.clientWidth,
36
+ clientHeight: isWindow ? docBody.clientHeight || docElement.clientHeight : element.clientHeight,
37
+ scrollHeight: isWindow ? docBody.scrollHeight || docElement.scrollHeight : element.scrollHeight,
38
+ scrollLeftMax,
39
+ scrollTopMax,
38
40
  }
39
41
  }
40
42
 
41
43
  /**
42
- * Validate if element passed to the function is valid.
43
- * @param {HTMLElement | Window} element
44
+ * Validate if the element passed to the function is valid.
45
+ * @param {HTMLElement | Window} element - The element to validate.
44
46
  */
45
47
  export function validateElement(element?: HTMLElement | Window) {
46
48
  if (element === undefined) {
@@ -56,6 +58,11 @@ export function validateElement(element?: HTMLElement | Window) {
56
58
  }
57
59
  }
58
60
 
61
+ /**
62
+ * Get the DOM element from a React ref object.
63
+ * @param {RefObject<HTMLElement>} ref - The React ref object.
64
+ * @returns {HTMLElement} - The DOM element.
65
+ */
59
66
  export const getElement = (ref: RefObject<HTMLElement>) => {
60
67
  if (!ref.current) {
61
68
  throw new Error(`Could not resolve ref object: ${ref.current}`)
@@ -63,6 +70,7 @@ export const getElement = (ref: RefObject<HTMLElement>) => {
63
70
  return ref.current
64
71
  }
65
72
 
73
+ // Map of easing functions for scroll animations.
66
74
  export const easingMap: EasingFunctions = {
67
75
  linear(t: number) {
68
76
  return t
@@ -78,6 +86,11 @@ export const easingMap: EasingFunctions = {
78
86
  },
79
87
  }
80
88
 
89
+ /**
90
+ * Get an easing function based on the provided easing option or the default option.
91
+ * @param {EasingOptions} easing - The easing option.
92
+ * @returns {EasingFunction} - The easing function.
93
+ */
81
94
  export const getEasing = (easing?: EasingOptions): EasingFunction => {
82
95
  const easeFunc = easingMap[easing || defaultScrollOptions.easingOption]
83
96
  if (!easeFunc) {