1
- import { DataRouteMatch , matchRoutes } from "react-router-dom" ;
1
+ import { DataRouteMatch , Location , matchRoutes } from "react-router-dom" ;
2
2
import { useCallback , useEffect } from "react" ;
3
3
import { RouterState } from "@remix-run/router" ;
4
- import { last , replaceAt , insertAt } from "src/utils/array-utils.ts" ;
4
+ import { replaceAt , insertAt } from "src/utils/array-utils.ts" ;
5
5
import { Router } from "@remix-run/router" ;
6
- import { getUrl } from "src/lib/tabs/getUrl.ts" ;
7
6
import { normalizePathname } from "src/lib/tabs/normalizePathname.ts" ;
8
7
9
8
type ValidUiState = Record < string , any > ;
10
9
11
- export type TabConfig < UiState extends ValidUiState = ValidUiState > = {
10
+ export type TabDefinition < UiState extends ValidUiState = ValidUiState > = {
12
11
shouldOpen : ( match : DataRouteMatch ) => boolean ;
13
12
insertAt : ( tabs : RouterTabPath [ ] ) => number ;
14
13
mapToUiState : ( match : DataRouteMatch , path : RouterTabPath ) => UiState ;
@@ -20,10 +19,15 @@ type PathsChangeCallback = (
20
19
paths : RouterTabPath [ ] | { ( prevPaths : RouterTabPath [ ] ) : RouterTabPath [ ] } ,
21
20
) => void ;
22
21
22
+ type MatchRouterTabResult = {
23
+ definition : TabDefinition ;
24
+ match : DataRouteMatch ;
25
+ } ;
26
+
23
27
export const matchRouterTab = (
24
28
matches : DataRouteMatch [ ] ,
25
- config : TabConfig [ ] ,
26
- ) => {
29
+ config : TabDefinition [ ] ,
30
+ ) : MatchRouterTabResult | undefined => {
27
31
for ( let i = matches . length - 1 ; i > - 1 ; i -- ) {
28
32
const match = matches [ i ] ;
29
33
const definition = config . find ( ( def ) => def . shouldOpen ( match ) ) ;
@@ -41,12 +45,29 @@ export const useRouterTabs = <
41
45
UiState extends ValidUiState = ValidUiState ,
42
46
> ( options : {
43
47
router : Router ;
44
- config : TabConfig < UiState > [ ] ;
48
+ config : TabDefinition < UiState > [ ] ;
45
49
onPathsChange ?: PathsChangeCallback ;
46
50
paths : RouterTabPath [ ] ;
47
51
} ) => {
48
52
const { onPathsChange, paths, config, router } = options ;
49
53
54
+ const isOpenFor = useCallback (
55
+ ( match : DataRouteMatch ) => ( path : string ) => {
56
+ const matches = matchRoutes ( router . routes , path ) || [ ] ;
57
+ const result = matchRouterTab ( matches , config ) ;
58
+
59
+ if ( ! result ) {
60
+ return false ;
61
+ }
62
+
63
+ return (
64
+ normalizePathname ( result . match . pathname ) ===
65
+ normalizePathname ( match . pathname )
66
+ ) ;
67
+ } ,
68
+ [ router . routes , config ] ,
69
+ ) ;
70
+
50
71
const updateTabs = useCallback (
51
72
( state : RouterState ) => {
52
73
const { matches, location, navigation } = state ;
@@ -55,39 +76,33 @@ export const useRouterTabs = <
55
76
return ;
56
77
}
57
78
58
- const result = matchRouterTab ( matches , config ) ;
79
+ const matchResult = matchRouterTab ( matches , config ) ;
59
80
60
- if ( ! result ) {
81
+ if ( ! matchResult ) {
61
82
return ;
62
83
}
63
- const { definition , match } = result ;
84
+
64
85
const getNextPaths = ( prevPaths : RouterTabPath [ ] ) => {
65
- const tab = prevPaths . find ( ( path ) => {
66
- const matches = matchRoutes ( router . routes , path ) || [ ] ;
67
- const result = matchRouterTab ( matches , config ) ;
68
- return (
69
- result &&
70
- normalizePathname ( result . match . pathname ) ===
71
- normalizePathname ( match . pathname )
72
- ) ;
73
- } ) ;
86
+ const tab = prevPaths . find ( isOpenFor ( matchResult . match ) ) ;
74
87
75
- const { pathname } = last ( matches ) ;
76
- const { search } = location ;
77
- const path = normalizePathname ( pathname ) + search ;
88
+ const path = getUrl ( location ) ;
78
89
79
90
if ( tab ) {
80
91
// update the tab path
81
92
const index = prevPaths . indexOf ( tab ) ;
82
93
83
94
return replaceAt ( prevPaths , index , path ) ;
84
95
} else {
85
- return insertAt ( prevPaths , definition . insertAt ( prevPaths ) , path ) ;
96
+ return insertAt (
97
+ prevPaths ,
98
+ matchResult . definition . insertAt ( prevPaths ) ,
99
+ path ,
100
+ ) ;
86
101
}
87
102
} ;
88
103
onPathsChange ?.( getNextPaths ) ;
89
104
} ,
90
- [ config , onPathsChange , router . routes ] ,
105
+ [ config , onPathsChange , isOpenFor ] ,
91
106
) ;
92
107
93
108
useEffect ( ( ) => {
@@ -96,23 +111,33 @@ export const useRouterTabs = <
96
111
return router . subscribe ( updateTabs ) ;
97
112
} , [ router , updateTabs ] ) ;
98
113
99
- const tabs = paths . map ( ( path ) => {
114
+ const toUiState = ( path : string ) => {
100
115
const matches = matchRoutes ( router . routes , path ) || [ ] ;
101
- const result = matchRouterTab ( matches , config ) ! ;
116
+ const result = matchRouterTab ( matches , config ) ;
102
117
103
- return result . definition . mapToUiState ( result . match , path ) as UiState ;
104
- } ) ;
118
+ if ( ! result ) {
119
+ return undefined ;
120
+ }
121
+ const { definition, match } = result ;
122
+ return definition . mapToUiState ( match , path ) ;
123
+ } ;
105
124
106
- const matches = matchRoutes ( router . routes , router . state . location ) || [ ] ;
107
- const result = matchRouterTab ( matches , config ) ;
125
+ const tabs = paths
126
+ . map ( toUiState )
127
+ . filter ( ( tab ) : tab is UiState => Boolean ( tab ) ) ;
108
128
109
- const activeTab = result ?. definition . mapToUiState (
110
- result . match ,
111
- getUrl ( router . state . location ) ,
112
- ) as UiState | undefined ;
129
+ const activeTab = toUiState ( getUrl ( router . state . location ) ) as
130
+ | UiState
131
+ | undefined ;
113
132
114
133
return {
115
134
tabs,
116
135
activeTab,
117
136
} ;
118
137
} ;
138
+
139
+ const getUrl = ( location : Location ) => {
140
+ const { pathname, search } = location ;
141
+
142
+ return normalizePathname ( pathname ) + search ;
143
+ } ;
0 commit comments