11// Copyright (c) 2026 IOTA Stiftung
22// SPDX-License-Identifier: Apache-2.0
33
4- import {
5- createContext ,
6- useCallback ,
7- useContext ,
8- useEffect ,
9- useMemo ,
10- useState ,
11- type ReactNode ,
12- } from 'react' ;
13- import { useLocation , useNavigate , useNavigationType , NavigationType } from 'react-router-dom' ;
4+ import { createContext , useContext , useEffect , useRef , useState , type ReactNode } from 'react' ;
5+ import { useLocation , useNavigationType } from 'react-router-dom' ;
146import { NAVBAR_ITEM_PATHS } from './navigation' ;
157
168export const HOME_PATH = NAVBAR_ITEM_PATHS . home ;
@@ -19,174 +11,44 @@ export const TAB_BAR_PATHS = new Set<string>(
1911 Object . values ( NAVBAR_ITEM_PATHS ) . filter ( ( p ) => p !== HOME_PATH ) ,
2012) ;
2113
22- const NAVIGATION_HISTORY_SESSION_KEY = 'navigation-history' ;
14+ const NavigationDepthContext = createContext < number > ( 0 ) ;
2315
24- type RouteKind = 'home' | 'tabBar' | 'other' ;
25-
26- interface HistoryEntry {
27- pathname : string ;
28- depth : number ;
29- }
30-
31- interface NavigationHistory {
32- idx : number ;
33- entries : ( HistoryEntry | null ) [ ] ;
34- }
35-
36- function getRouteKind ( pathname : string , search : string ) : RouteKind {
37- const hasMenuParam = new URLSearchParams ( search ) . has ( 'menu' ) ;
38- if ( hasMenuParam ) return 'other' ;
39- if ( pathname === HOME_PATH || pathname . startsWith ( HOME_PATH + '/' ) ) return 'home' ;
40- if ( TAB_BAR_PATHS . has ( pathname ) ) return 'tabBar' ;
41- return 'other' ;
42- }
43-
44- const FALLBACK_DEPTH_BY_ROUTE_KIND : Record < RouteKind , number > = {
45- home : 0 ,
46- tabBar : 1 ,
47- other : 2 ,
48- } ;
49-
50- function getHistoryIndex ( ) : number {
51- const idx = window . history . state ?. idx ;
52- return typeof idx === 'number' ? idx : 0 ;
16+ function getRootDepth ( { pathname, search } : { pathname : string ; search : string } ) : number | null {
17+ if ( new URLSearchParams ( search ) . has ( 'menu' ) ) return null ;
18+ if ( pathname === HOME_PATH || pathname . startsWith ( HOME_PATH + '/' ) ) return 0 ;
19+ if ( TAB_BAR_PATHS . has ( pathname ) ) return 1 ;
20+ return null ;
5321}
5422
55- function isHistoryEntry ( value : unknown ) : value is HistoryEntry {
56- const entry = value as Partial < HistoryEntry > | null ;
57- return typeof entry ?. pathname === 'string' && typeof entry . depth === 'number' ;
58- }
59-
60- function readStoredEntries ( ) : ( HistoryEntry | null ) [ ] {
61- try {
62- const raw = sessionStorage . getItem ( NAVIGATION_HISTORY_SESSION_KEY ) ;
63- if ( ! raw ) return [ ] ;
64- const stored : unknown = JSON . parse ( raw ) ;
65- return Array . isArray ( stored ) &&
66- stored . every ( ( item ) => item === null || isHistoryEntry ( item ) )
67- ? stored
68- : [ ] ;
69- } catch {
70- return [ ] ;
71- }
72- }
73-
74- function writeStoredEntries ( entries : ( HistoryEntry | null ) [ ] ) {
75- try {
76- sessionStorage . setItem ( NAVIGATION_HISTORY_SESSION_KEY , JSON . stringify ( entries ) ) ;
77- } catch {
78- return ;
79- }
80- }
81-
82- function resolveEntries (
83- prev : ( HistoryEntry | null ) [ ] ,
84- idx : number ,
85- pathname : string ,
86- search : string ,
87- navigationType : NavigationType ,
88- ) : ( HistoryEntry | null ) [ ] {
89- const known = prev [ idx ] ;
90- if ( navigationType === NavigationType . Pop && known ?. pathname === pathname ) return prev ;
91-
92- const routeKind = getRouteKind ( pathname , search ) ;
93- const previous = prev [ idx - 1 ] ;
94- let depth = FALLBACK_DEPTH_BY_ROUTE_KIND [ routeKind ] ;
95- if ( routeKind === 'home' ) {
96- depth = 0 ;
97- } else if ( routeKind === 'tabBar' ) {
98- depth = 1 ;
99- } else if ( navigationType === NavigationType . Push && previous ) {
100- depth = previous . depth + 1 ;
101- } else if ( navigationType === NavigationType . Replace && known ) {
102- depth = known . depth ;
103- }
104-
105- if ( navigationType === NavigationType . Push ) {
106- const base = Array . from ( { length : idx } , ( _ , i ) => prev [ i ] ?? null ) ;
107- return [ ...base , { pathname, depth } ] ;
108- }
109- const next = Array . from ( { length : Math . max ( prev . length , idx + 1 ) } , ( _ , i ) => prev [ i ] ?? null ) ;
110- next [ idx ] = { pathname, depth } ;
111- return next ;
112- }
113-
114- interface NavigationStackContextValue {
115- depth : number ;
116- goBackTo : ( path : string ) => void ;
117- }
118-
119- const NavigationStackContext = createContext < NavigationStackContextValue > ( {
120- depth : 0 ,
121- goBackTo : ( ) => { } ,
122- } ) ;
123-
12423export function NavigationStackProvider ( { children } : { children : ReactNode } ) {
12524 const location = useLocation ( ) ;
126- const navigate = useNavigate ( ) ;
12725 const navigationType = useNavigationType ( ) ;
128- const [ history , setHistory ] = useState < NavigationHistory > ( ( ) => {
129- const idx = getHistoryIndex ( ) ;
130- return {
131- idx,
132- entries : resolveEntries (
133- readStoredEntries ( ) ,
134- idx ,
135- location . pathname ,
136- location . search ,
137- NavigationType . Pop ,
138- ) ,
139- } ;
140- } ) ;
26+ const [ depth , setDepth ] = useState ( ( ) => getRootDepth ( location ) ?? 2 ) ;
27+ const lastLocationKey = useRef ( location . key ) ;
28+ const isDepthUnknown = useRef ( getRootDepth ( location ) === null ) ;
14129
14230 useEffect ( ( ) => {
143- setHistory ( ( prev ) => {
144- const idx = getHistoryIndex ( ) ;
145- return {
146- idx ,
147- entries : resolveEntries (
148- prev . entries ,
149- idx ,
150- location . pathname ,
151- location . search ,
152- navigationType ,
153- ) ,
154- } ;
155- } ) ;
31+ if ( lastLocationKey . current === location . key ) return ;
32+ lastLocationKey . current = location . key ;
33+
34+ const rootDepth = getRootDepth ( location ) ;
35+ if ( rootDepth !== null ) {
36+ isDepthUnknown . current = false ;
37+ setDepth ( rootDepth ) ;
38+ } else if ( navigationType === 'PUSH' ) {
39+ setDepth ( ( prev ) => prev + 1 ) ;
40+ } else if ( navigationType === 'POP' ) {
41+ const minDepth = isDepthUnknown . current ? 2 : 1 ;
42+ setDepth ( ( prev ) => Math . max ( minDepth , prev - 1 ) ) ;
43+ }
15644 // oxlint-disable-next-line react-hooks/exhaustive-deps
15745 } , [ location . key , navigationType ] ) ;
15846
159- useEffect ( ( ) => {
160- writeStoredEntries ( history . entries ) ;
161- } , [ history . entries ] ) ;
162-
163- const depth = history . entries [ history . idx ] ?. depth ?? 0 ;
164-
165- const goBackTo = useCallback (
166- ( path : string ) => {
167- const targetIdx = history . entries . findLastIndex (
168- ( entry , i ) => i < history . idx && entry ?. pathname === path ,
169- ) ;
170- if ( targetIdx === - 1 ) {
171- navigate ( path , { replace : true } ) ;
172- } else {
173- navigate ( targetIdx - history . idx ) ;
174- }
175- } ,
176- [ history , navigate ] ,
177- ) ;
178-
179- const value = useMemo ( ( ) => ( { depth, goBackTo } ) , [ depth , goBackTo ] ) ;
180-
18147 return (
182- < NavigationStackContext . Provider value = { value } > { children } </ NavigationStackContext . Provider >
48+ < NavigationDepthContext . Provider value = { depth } > { children } </ NavigationDepthContext . Provider >
18349 ) ;
18450}
18551
18652export function useNavigationDepth ( ) : number {
187- return useContext ( NavigationStackContext ) . depth ;
188- }
189-
190- export function useGoBackTo ( ) : ( path : string ) => void {
191- return useContext ( NavigationStackContext ) . goBackTo ;
53+ return useContext ( NavigationDepthContext ) ;
19254}
0 commit comments