use-scroll-to-id-on-load.ts 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import { useEffect, useState, useRef } from 'react'
  2. const DEFAULT_TIMEOUT = 3000
  3. const hash = window.location.hash.substring(1)
  4. const events = <const>['keydown', 'touchmove', 'wheel']
  5. const isKeyboardEvent = (event: Event): event is KeyboardEvent => {
  6. return event.constructor.name === 'KeyboardEvent'
  7. }
  8. type UseScrollToIdOnLoadProps = {
  9. timeout?: number
  10. }
  11. function UseScrollToIdOnLoad({
  12. timeout = DEFAULT_TIMEOUT,
  13. }: UseScrollToIdOnLoadProps = {}) {
  14. const [offsetTop, setOffsetTop] = useState<number | null>(null)
  15. const requestRef = useRef<number | null>(null)
  16. const targetRef = useRef<HTMLElement | null>(null)
  17. const cancelAnimationFrame = () => {
  18. if (requestRef.current) {
  19. window.cancelAnimationFrame(requestRef.current)
  20. }
  21. }
  22. const cancelEventListeners = () => {
  23. events.forEach(eventType => {
  24. window.removeEventListener(eventType, eventListenersCallbackRef.current)
  25. })
  26. }
  27. const eventListenersCallback = (
  28. event: KeyboardEvent | TouchEvent | WheelEvent
  29. ) => {
  30. const keys = new Set(['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown'])
  31. if (!isKeyboardEvent(event) || keys.has(event.key)) {
  32. // Remove scroll checks
  33. cancelAnimationFrame()
  34. // Remove event listeners
  35. cancelEventListeners()
  36. }
  37. }
  38. const eventListenersCallbackRef = useRef(eventListenersCallback)
  39. // Scroll to the target
  40. useEffect(() => {
  41. if (!offsetTop) {
  42. return
  43. }
  44. window.scrollTo({
  45. top: offsetTop,
  46. })
  47. }, [offsetTop])
  48. // Bail out from scrolling automatically in `${timeout}` milliseconds
  49. useEffect(() => {
  50. if (!hash) {
  51. return
  52. }
  53. setTimeout(() => {
  54. cancelAnimationFrame()
  55. cancelEventListeners()
  56. }, timeout)
  57. }, [timeout])
  58. // Scroll to target by recursively looking for the target element
  59. useEffect(() => {
  60. if (!hash) {
  61. return
  62. }
  63. const offsetTop = () => {
  64. if (targetRef.current) {
  65. setOffsetTop(targetRef.current.offsetTop)
  66. } else {
  67. targetRef.current = document.getElementById(hash)
  68. }
  69. requestRef.current = window.requestAnimationFrame(offsetTop)
  70. }
  71. requestRef.current = window.requestAnimationFrame(offsetTop)
  72. return () => {
  73. cancelAnimationFrame()
  74. }
  75. }, [])
  76. // Set up the event listeners that will cancel the target element lookup
  77. useEffect(() => {
  78. if (!hash) {
  79. return
  80. }
  81. events.forEach(eventType => {
  82. window.addEventListener(eventType, eventListenersCallbackRef.current)
  83. })
  84. return () => {
  85. cancelEventListeners()
  86. }
  87. }, [])
  88. }
  89. export default UseScrollToIdOnLoad