11'use client' ;
2+
3+ import { usePathname } from 'next/navigation' ;
24import React from 'react' ;
35
4- export const HashContext = React . createContext < {
6+ export const NavigationStatusContext = React . createContext < {
57 hash : string | null ;
68 /**
7- * Updates the hash value from the URL provided here.
8- * It will then be used by the `useHash` hook.
9+ * Updates the navigation state from the URL provided here.
910 * URL can be relative or absolute.
1011 */
11- updateHashFromUrl : ( href : string ) => void ;
12+ onNavigationClick : ( href : string ) => void ;
13+ /**
14+ * Indicates if a link has been clicked recently.
15+ * Becomes true after a click and resets to false when pathname changes.
16+ * It is debounced to avoid flickering on fast navigations.
17+ * Debounce time is 400ms (= doherty threshold for responsiveness).
18+ */
19+ isNavigating : boolean ;
1220} > ( {
1321 hash : null ,
14- updateHashFromUrl : ( ) => { } ,
22+ onNavigationClick : ( ) => { } ,
23+ isNavigating : false ,
1524} ) ;
1625
1726function getHash ( ) : string | null {
@@ -21,20 +30,62 @@ function getHash(): string | null {
2130 return window . location . hash . slice ( 1 ) ;
2231}
2332
24- export const HashProvider : React . FC < React . PropsWithChildren < { } > > = ( { children } ) => {
33+ export const NavigationStatusProvider : React . FC < React . PropsWithChildren > = ( { children } ) => {
2534 const [ hash , setHash ] = React . useState < string | null > ( getHash ) ;
26- const updateHashFromUrl = React . useCallback ( ( href : string ) => {
35+ const [ isNavigating , setIsNavigating ] = React . useState ( false ) ;
36+ const timeoutRef = React . useRef < number | null > ( null ) ;
37+ const pathname = usePathname ( ) ;
38+ const pathnameRef = React . useRef ( pathname ) ;
39+
40+ // Reset isNavigating when pathname changes
41+ React . useEffect ( ( ) => {
42+ if ( pathnameRef . current !== pathname ) {
43+ setIsNavigating ( false ) ;
44+ if ( timeoutRef . current ) {
45+ clearTimeout ( timeoutRef . current ) ;
46+ timeoutRef . current = null ;
47+ }
48+ pathnameRef . current = pathname ;
49+ }
50+ } , [ pathname ] ) ;
51+
52+ // Cleanup timeout on unmount
53+ React . useEffect ( ( ) => {
54+ return ( ) => {
55+ if ( timeoutRef . current ) {
56+ clearTimeout ( timeoutRef . current ) ;
57+ }
58+ } ;
59+ } , [ ] ) ;
60+
61+ const onNavigationClick = React . useCallback ( ( href : string ) => {
2762 const url = new URL (
2863 href ,
2964 typeof window !== 'undefined' ? window . location . origin : 'http://localhost'
3065 ) ;
3166 setHash ( url . hash . slice ( 1 ) ) ;
67+
68+ if ( timeoutRef . current ) {
69+ clearTimeout ( timeoutRef . current ) ;
70+ }
71+ if ( pathnameRef . current !== url . pathname ) {
72+ timeoutRef . current = window . setTimeout ( ( ) => {
73+ setIsNavigating ( true ) ;
74+ timeoutRef . current = null ;
75+ return ;
76+ } , 400 ) ; // 400ms timeout - doherty threshold for responsiveness
77+ }
3278 } , [ ] ) ;
79+
3380 const memoizedValue = React . useMemo (
34- ( ) => ( { hash, updateHashFromUrl } ) ,
35- [ hash , updateHashFromUrl ]
81+ ( ) => ( { hash, onNavigationClick, isNavigating } ) ,
82+ [ hash , onNavigationClick , isNavigating ]
83+ ) ;
84+ return (
85+ < NavigationStatusContext . Provider value = { memoizedValue } >
86+ { children }
87+ </ NavigationStatusContext . Provider >
3688 ) ;
37- return < HashContext . Provider value = { memoizedValue } > { children } </ HashContext . Provider > ;
3889} ;
3990
4091/**
@@ -45,8 +96,16 @@ export const HashProvider: React.FC<React.PropsWithChildren<{}>> = ({ children }
4596 * Since we have a single Link component that handles all links, we can use a context to share the hash.
4697 */
4798export function useHash ( ) {
48- // const params = useParams();
49- const { hash } = React . useContext ( HashContext ) ;
99+ const { hash } = React . useContext ( NavigationStatusContext ) ;
50100
51101 return hash ;
52102}
103+
104+ /**
105+ * Hook to get the current navigation state.
106+ * @returns True if a navigation has been triggered recently. False otherwise, it also resets to false when the navigation is complete.
107+ */
108+ export function useIsNavigating ( ) {
109+ const { isNavigating : hasBeenClicked } = React . useContext ( NavigationStatusContext ) ;
110+ return hasBeenClicked ;
111+ }
0 commit comments