Quellcode durchsuchen

Merge pull request #7831 from overleaf/ii-scroll-to-position

Add scroll to position custom hook

GitOrigin-RevId: e2202e390ad674e31b61eb0f0ff6bda9346b34b8
ilkin-overleaf vor 4 Jahren
Ursprung
Commit
f7977cdca5

+ 2 - 0
services/web/frontend/js/features/settings/components/root.tsx

@@ -14,10 +14,12 @@ import * as eventTracking from '../../../infrastructure/event-tracking'
 import { UserProvider } from '../../../shared/context/user-context'
 import { SSOProvider } from '../context/sso-context'
 import useWaitForI18n from '../../../shared/hooks/use-wait-for-i18n'
+import useScrollToIdOnLoad from '../../../shared/hooks/use-scroll-to-id-on-load'
 import { ExposedSettings } from '../../../../../types/exposed-settings'
 
 function SettingsPageRoot() {
   const { isReady } = useWaitForI18n()
+  useScrollToIdOnLoad()
 
   useEffect(() => {
     eventTracking.sendMB('settings-view')

+ 111 - 0
services/web/frontend/js/shared/hooks/use-scroll-to-id-on-load.ts

@@ -0,0 +1,111 @@
+import { useEffect, useState, useRef } from 'react'
+
+const DEFAULT_TIMEOUT = 3000
+const hash = window.location.hash.substring(1)
+const events = <const>['keydown', 'touchmove', 'wheel']
+
+const isKeyboardEvent = (event: Event): event is KeyboardEvent => {
+  return event.constructor.name === 'KeyboardEvent'
+}
+
+type UseScrollToIdOnLoadProps = {
+  timeout?: number
+}
+
+function UseScrollToIdOnLoad({
+  timeout = DEFAULT_TIMEOUT,
+}: UseScrollToIdOnLoadProps = {}) {
+  const [offsetTop, setOffsetTop] = useState<number | null>(null)
+  const requestRef = useRef<number | null>(null)
+  const targetRef = useRef<HTMLElement | null>(null)
+
+  const cancelAnimationFrame = () => {
+    if (requestRef.current) {
+      window.cancelAnimationFrame(requestRef.current)
+    }
+  }
+
+  const cancelEventListeners = () => {
+    events.forEach(eventType => {
+      window.removeEventListener(eventType, eventListenersCallbackRef.current)
+    })
+  }
+
+  const eventListenersCallback = (
+    event: KeyboardEvent | TouchEvent | WheelEvent
+  ) => {
+    const keys = new Set(['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown'])
+
+    if (!isKeyboardEvent(event) || keys.has(event.key)) {
+      // Remove scroll checks
+      cancelAnimationFrame()
+      // Remove event listeners
+      cancelEventListeners()
+    }
+  }
+
+  const eventListenersCallbackRef = useRef(eventListenersCallback)
+
+  // Scroll to the target
+  useEffect(() => {
+    if (!offsetTop) {
+      return
+    }
+
+    window.scrollTo({
+      top: offsetTop,
+    })
+  }, [offsetTop])
+
+  // Bail out from scrolling automatically in `${timeout}` milliseconds
+  useEffect(() => {
+    if (!hash) {
+      return
+    }
+
+    setTimeout(() => {
+      cancelAnimationFrame()
+      cancelEventListeners()
+    }, timeout)
+  }, [timeout])
+
+  // Scroll to target by recursively looking for the target element
+  useEffect(() => {
+    if (!hash) {
+      return
+    }
+
+    const offsetTop = () => {
+      if (targetRef.current) {
+        setOffsetTop(targetRef.current.offsetTop)
+      } else {
+        targetRef.current = document.getElementById(hash)
+      }
+
+      requestRef.current = window.requestAnimationFrame(offsetTop)
+    }
+
+    requestRef.current = window.requestAnimationFrame(offsetTop)
+
+    return () => {
+      cancelAnimationFrame()
+    }
+  }, [])
+
+  // Set up the event listeners that will cancel the target element lookup
+  useEffect(() => {
+    if (!hash) {
+      return
+    }
+
+    events.forEach(eventType => {
+      window.addEventListener(eventType, eventListenersCallbackRef.current)
+    })
+
+    return () => {
+      cancelEventListeners()
+    }
+  }, [])
+}
+
+export default UseScrollToIdOnLoad