@esmx/router-react 3.0.0-rc.105
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +515 -0
- package/dist/context.d.ts +85 -0
- package/dist/context.mjs +23 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.mjs +11 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.mjs +169 -0
- package/dist/router-link.d.ts +35 -0
- package/dist/router-link.mjs +85 -0
- package/dist/router-provider.d.ts +61 -0
- package/dist/router-provider.mjs +34 -0
- package/dist/router-view.d.ts +63 -0
- package/dist/router-view.mjs +40 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.mjs +0 -0
- package/dist/use-link.d.ts +97 -0
- package/dist/use-link.mjs +40 -0
- package/dist/util.d.ts +13 -0
- package/dist/util.mjs +15 -0
- package/package.json +85 -0
- package/src/context.ts +109 -0
- package/src/index.test.ts +215 -0
- package/src/index.ts +21 -0
- package/src/router-link.ts +222 -0
- package/src/router-provider.ts +104 -0
- package/src/router-view.ts +113 -0
- package/src/types.ts +30 -0
- package/src/use-link.ts +138 -0
- package/src/util.ts +38 -0
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@esmx/router-react",
|
|
3
|
+
"description": "React integration for @esmx/router - A powerful router with React 18+ support using modern hooks and context patterns",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"react",
|
|
6
|
+
"router",
|
|
7
|
+
"routing",
|
|
8
|
+
"react-router",
|
|
9
|
+
"hooks",
|
|
10
|
+
"typescript",
|
|
11
|
+
"navigation",
|
|
12
|
+
"esmx",
|
|
13
|
+
"esm",
|
|
14
|
+
"single-page-application",
|
|
15
|
+
"spa",
|
|
16
|
+
"framework",
|
|
17
|
+
"frontend"
|
|
18
|
+
],
|
|
19
|
+
"template": "library",
|
|
20
|
+
"scripts": {
|
|
21
|
+
"lint:js": "biome check --write --no-errors-on-unmatched",
|
|
22
|
+
"lint:css": "pnpm run lint:js",
|
|
23
|
+
"lint:type": "tsc --noEmit",
|
|
24
|
+
"test": "vitest run --pass-with-no-tests",
|
|
25
|
+
"coverage": "vitest run --coverage --pass-with-no-tests",
|
|
26
|
+
"build": "unbuild"
|
|
27
|
+
},
|
|
28
|
+
"contributors": [
|
|
29
|
+
{
|
|
30
|
+
"name": "lzxb",
|
|
31
|
+
"url": "https://github.com/lzxb"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "RockShi1994",
|
|
35
|
+
"url": "https://github.com/RockShi1994"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "jerrychan7",
|
|
39
|
+
"url": "https://github.com/jerrychan7"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "wesloong",
|
|
43
|
+
"url": "https://github.com/wesloong"
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@esmx/router": "3.0.0-rc.105"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@biomejs/biome": "2.3.7",
|
|
54
|
+
"@types/node": "^24.0.0",
|
|
55
|
+
"@types/react": "^19.1.8",
|
|
56
|
+
"@types/react-dom": "^19.1.6",
|
|
57
|
+
"@vitest/coverage-v8": "3.2.4",
|
|
58
|
+
"happy-dom": "^20.0.10",
|
|
59
|
+
"react": "^19.1.0",
|
|
60
|
+
"react-dom": "^19.1.0",
|
|
61
|
+
"typescript": "5.9.3",
|
|
62
|
+
"unbuild": "3.6.1",
|
|
63
|
+
"vitest": "3.2.4"
|
|
64
|
+
},
|
|
65
|
+
"version": "3.0.0-rc.105",
|
|
66
|
+
"type": "module",
|
|
67
|
+
"private": false,
|
|
68
|
+
"exports": {
|
|
69
|
+
".": {
|
|
70
|
+
"import": "./dist/index.mjs",
|
|
71
|
+
"types": "./dist/index.d.ts"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
"module": "dist/index.mjs",
|
|
75
|
+
"types": "./dist/index.d.ts",
|
|
76
|
+
"files": [
|
|
77
|
+
"lib",
|
|
78
|
+
"src",
|
|
79
|
+
"dist",
|
|
80
|
+
"*.mjs",
|
|
81
|
+
"template",
|
|
82
|
+
"public"
|
|
83
|
+
],
|
|
84
|
+
"gitHead": "db0f4a2a8a54a932f900d0b4f09ef0bf1ca9243f"
|
|
85
|
+
}
|
package/src/context.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { Route, Router } from '@esmx/router';
|
|
2
|
+
import { createContext, useContext } from 'react';
|
|
3
|
+
import type { RouterContextValue } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* React Context for router state.
|
|
7
|
+
* Contains the router instance and current route.
|
|
8
|
+
* Using null as default to detect missing provider.
|
|
9
|
+
*/
|
|
10
|
+
export const RouterContext = createContext<RouterContextValue | null>(null);
|
|
11
|
+
RouterContext.displayName = 'RouterContext';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* React Context for RouterView depth tracking.
|
|
15
|
+
* Used for nested routing to determine which matched route to render.
|
|
16
|
+
*/
|
|
17
|
+
export const RouterViewDepthContext = createContext<number>(0);
|
|
18
|
+
RouterViewDepthContext.displayName = 'RouterViewDepthContext';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get the router context value.
|
|
22
|
+
* @throws {Error} If used outside of RouterProvider
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export function useRouterContext(): RouterContextValue {
|
|
26
|
+
const context = useContext(RouterContext);
|
|
27
|
+
if (!context) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
'[@esmx/router-react] Router context not found. ' +
|
|
30
|
+
'Please ensure your component is wrapped in a RouterProvider.'
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return context;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the router instance for navigation.
|
|
38
|
+
* Must be used within a RouterProvider.
|
|
39
|
+
*
|
|
40
|
+
* @returns Router instance with navigation methods
|
|
41
|
+
* @throws {Error} If used outside of RouterProvider
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* import { useRouter } from '@esmx/router-react';
|
|
46
|
+
*
|
|
47
|
+
* function NavigationButton() {
|
|
48
|
+
* const router = useRouter();
|
|
49
|
+
*
|
|
50
|
+
* const handleClick = () => {
|
|
51
|
+
* router.push('/dashboard');
|
|
52
|
+
* };
|
|
53
|
+
*
|
|
54
|
+
* return <button onClick={handleClick}>Go to Dashboard</button>;
|
|
55
|
+
* }
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export function useRouter(): Router {
|
|
59
|
+
return useRouterContext().router;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the current route information.
|
|
64
|
+
* Returns a reactive route object that updates when navigation occurs.
|
|
65
|
+
* Must be used within a RouterProvider.
|
|
66
|
+
*
|
|
67
|
+
* @returns Current route object with path, params, query, etc.
|
|
68
|
+
* @throws {Error} If used outside of RouterProvider
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```tsx
|
|
72
|
+
* import { useRoute } from '@esmx/router-react';
|
|
73
|
+
*
|
|
74
|
+
* function CurrentPath() {
|
|
75
|
+
* const route = useRoute();
|
|
76
|
+
*
|
|
77
|
+
* return (
|
|
78
|
+
* <div>
|
|
79
|
+
* <p>Path: {route.path}</p>
|
|
80
|
+
* <p>Params: {JSON.stringify(route.params)}</p>
|
|
81
|
+
* <p>Query: {JSON.stringify(route.query)}</p>
|
|
82
|
+
* </div>
|
|
83
|
+
* );
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function useRoute(): Route {
|
|
88
|
+
return useRouterContext().route;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get the current RouterView depth.
|
|
93
|
+
* Used internally by RouterView for nested routing.
|
|
94
|
+
*
|
|
95
|
+
* @returns Current depth level (0 for root, 1 for first nested, etc.)
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```tsx
|
|
99
|
+
* import { useRouterViewDepth } from '@esmx/router-react';
|
|
100
|
+
*
|
|
101
|
+
* function DebugView() {
|
|
102
|
+
* const depth = useRouterViewDepth();
|
|
103
|
+
* return <div>Current depth: {depth}</div>;
|
|
104
|
+
* }
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export function useRouterViewDepth(): number {
|
|
108
|
+
return useContext(RouterViewDepthContext);
|
|
109
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
import * as RouterReactModule from './index';
|
|
6
|
+
|
|
7
|
+
describe('index.ts - Package Entry Point', () => {
|
|
8
|
+
describe('Hook Exports', () => {
|
|
9
|
+
it('should export useRouter hook', () => {
|
|
10
|
+
expect(RouterReactModule.useRouter).toBeDefined();
|
|
11
|
+
expect(typeof RouterReactModule.useRouter).toBe('function');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should export useRoute hook', () => {
|
|
15
|
+
expect(RouterReactModule.useRoute).toBeDefined();
|
|
16
|
+
expect(typeof RouterReactModule.useRoute).toBe('function');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should export useLink hook', () => {
|
|
20
|
+
expect(RouterReactModule.useLink).toBeDefined();
|
|
21
|
+
expect(typeof RouterReactModule.useLink).toBe('function');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should export useRouterViewDepth hook', () => {
|
|
25
|
+
expect(RouterReactModule.useRouterViewDepth).toBeDefined();
|
|
26
|
+
expect(typeof RouterReactModule.useRouterViewDepth).toBe(
|
|
27
|
+
'function'
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('Component Exports', () => {
|
|
33
|
+
it('should export RouterProvider component', () => {
|
|
34
|
+
expect(RouterReactModule.RouterProvider).toBeDefined();
|
|
35
|
+
expect(typeof RouterReactModule.RouterProvider).toBe('function');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should export RouterLink component', () => {
|
|
39
|
+
expect(RouterReactModule.RouterLink).toBeDefined();
|
|
40
|
+
expect(typeof RouterReactModule.RouterLink).toBe('object'); // forwardRef returns object
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should export RouterView component', () => {
|
|
44
|
+
expect(RouterReactModule.RouterView).toBeDefined();
|
|
45
|
+
expect(typeof RouterReactModule.RouterView).toBe('function');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('Context Exports', () => {
|
|
50
|
+
it('should export RouterContext', () => {
|
|
51
|
+
expect(RouterReactModule.RouterContext).toBeDefined();
|
|
52
|
+
expect(RouterReactModule.RouterContext.displayName).toBe(
|
|
53
|
+
'RouterContext'
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should export RouterViewDepthContext', () => {
|
|
58
|
+
expect(RouterReactModule.RouterViewDepthContext).toBeDefined();
|
|
59
|
+
expect(RouterReactModule.RouterViewDepthContext.displayName).toBe(
|
|
60
|
+
'RouterViewDepthContext'
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('Export Completeness', () => {
|
|
66
|
+
it('should export all expected functions and components', () => {
|
|
67
|
+
const expectedExports = [
|
|
68
|
+
// Hooks
|
|
69
|
+
'useRouter',
|
|
70
|
+
'useRoute',
|
|
71
|
+
'useLink',
|
|
72
|
+
'useRouterViewDepth',
|
|
73
|
+
// Components
|
|
74
|
+
'RouterProvider',
|
|
75
|
+
'RouterLink',
|
|
76
|
+
'RouterView',
|
|
77
|
+
// Context
|
|
78
|
+
'RouterContext',
|
|
79
|
+
'RouterViewDepthContext'
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
expectedExports.forEach((exportName) => {
|
|
83
|
+
expect(RouterReactModule).toHaveProperty(exportName);
|
|
84
|
+
expect(Object.hasOwn(RouterReactModule, exportName)).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should not export unexpected items', () => {
|
|
89
|
+
const actualExports = Object.keys(RouterReactModule);
|
|
90
|
+
const expectedExports = [
|
|
91
|
+
'useRouter',
|
|
92
|
+
'useRoute',
|
|
93
|
+
'useLink',
|
|
94
|
+
'useRouterViewDepth',
|
|
95
|
+
'RouterProvider',
|
|
96
|
+
'RouterLink',
|
|
97
|
+
'RouterView',
|
|
98
|
+
'RouterContext',
|
|
99
|
+
'RouterViewDepthContext'
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
// Check that we don't have unexpected exports
|
|
103
|
+
const unexpectedExports = actualExports.filter(
|
|
104
|
+
(exportName) => !expectedExports.includes(exportName)
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(unexpectedExports).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('Hook Error Handling', () => {
|
|
112
|
+
it('hooks should throw when used incorrectly', () => {
|
|
113
|
+
// React hooks cannot be called outside of components
|
|
114
|
+
// This is expected React behavior - hooks depend on internal React state
|
|
115
|
+
// The error message varies depending on the React version and environment
|
|
116
|
+
expect(() => {
|
|
117
|
+
RouterReactModule.useRouter();
|
|
118
|
+
}).toThrow(); // Will throw "Invalid hook call" or similar
|
|
119
|
+
|
|
120
|
+
expect(() => {
|
|
121
|
+
RouterReactModule.useRoute();
|
|
122
|
+
}).toThrow(); // Will throw "Invalid hook call" or similar
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('Component Properties', () => {
|
|
127
|
+
it('should have RouterProvider with displayName', () => {
|
|
128
|
+
expect(RouterReactModule.RouterProvider.displayName).toBe(
|
|
129
|
+
'RouterProvider'
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should have RouterView with displayName', () => {
|
|
134
|
+
expect(RouterReactModule.RouterView.displayName).toBe('RouterView');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should have RouterLink with displayName', () => {
|
|
138
|
+
expect(RouterReactModule.RouterLink.displayName).toBe('RouterLink');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('Module Structure', () => {
|
|
143
|
+
it('should be a proper ES module', () => {
|
|
144
|
+
expect(typeof RouterReactModule).toBe('object');
|
|
145
|
+
expect(RouterReactModule).not.toBeNull();
|
|
146
|
+
|
|
147
|
+
// Verify it's not a default export
|
|
148
|
+
expect('default' in RouterReactModule).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should have consistent export naming', () => {
|
|
152
|
+
// All hooks should start with 'use'
|
|
153
|
+
const hookExports = [
|
|
154
|
+
'useRouter',
|
|
155
|
+
'useRoute',
|
|
156
|
+
'useLink',
|
|
157
|
+
'useRouterViewDepth'
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
hookExports.forEach((exportName) => {
|
|
161
|
+
expect(exportName).toMatch(/^use[A-Z][a-zA-Z]*$/);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Component exports should be PascalCase
|
|
165
|
+
const componentExports = [
|
|
166
|
+
'RouterProvider',
|
|
167
|
+
'RouterLink',
|
|
168
|
+
'RouterView',
|
|
169
|
+
'RouterContext',
|
|
170
|
+
'RouterViewDepthContext'
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
componentExports.forEach((exportName) => {
|
|
174
|
+
expect(exportName).toMatch(/^[A-Z][a-zA-Z]*$/);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('TypeScript Types', () => {
|
|
180
|
+
it('should export RouterContextValue type (via typeof)', () => {
|
|
181
|
+
// Types are compile-time only, but we can verify the runtime shape
|
|
182
|
+
const context = RouterReactModule.RouterContext;
|
|
183
|
+
expect(context).toBeDefined();
|
|
184
|
+
expect(context.Provider).toBeDefined();
|
|
185
|
+
expect(context.Consumer).toBeDefined();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should have RouterViewDepthContext with correct default value', () => {
|
|
189
|
+
// RouterViewDepthContext should default to 0
|
|
190
|
+
const context = RouterReactModule.RouterViewDepthContext;
|
|
191
|
+
expect(context).toBeDefined();
|
|
192
|
+
expect(context.Provider).toBeDefined();
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('React Best Practices', () => {
|
|
197
|
+
it('RouterLink should be a forwardRef component', () => {
|
|
198
|
+
// forwardRef components have $$typeof and render properties
|
|
199
|
+
const link = RouterReactModule.RouterLink as any;
|
|
200
|
+
expect(link.$$typeof).toBeDefined();
|
|
201
|
+
// The render property exists on forwardRef components
|
|
202
|
+
expect(link.render || link.type).toBeDefined();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('RouterProvider should accept router and children props', () => {
|
|
206
|
+
// Function components show their parameter count
|
|
207
|
+
// RouterProvider should be a regular function component
|
|
208
|
+
expect(RouterReactModule.RouterProvider).toBeDefined();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('RouterView should accept fallback prop', () => {
|
|
212
|
+
expect(RouterReactModule.RouterView).toBeDefined();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Components
|
|
2
|
+
|
|
3
|
+
// Hooks
|
|
4
|
+
export {
|
|
5
|
+
RouterContext,
|
|
6
|
+
RouterViewDepthContext,
|
|
7
|
+
useRoute,
|
|
8
|
+
useRouter,
|
|
9
|
+
useRouterViewDepth
|
|
10
|
+
} from './context';
|
|
11
|
+
export type { RouterLinkComponentProps } from './router-link';
|
|
12
|
+
export { RouterLink } from './router-link';
|
|
13
|
+
export { RouterProvider } from './router-provider';
|
|
14
|
+
export { RouterView } from './router-view';
|
|
15
|
+
// Types
|
|
16
|
+
export type {
|
|
17
|
+
RouterContextValue,
|
|
18
|
+
RouterProviderProps,
|
|
19
|
+
RouterViewProps
|
|
20
|
+
} from './types';
|
|
21
|
+
export { useLink } from './use-link';
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RouteLayerOptions,
|
|
3
|
+
RouteLocationInput,
|
|
4
|
+
RouteMatchType,
|
|
5
|
+
RouterLinkResolved,
|
|
6
|
+
RouterLinkType
|
|
7
|
+
} from '@esmx/router';
|
|
8
|
+
import {
|
|
9
|
+
type CSSProperties,
|
|
10
|
+
createElement,
|
|
11
|
+
type ElementType,
|
|
12
|
+
forwardRef,
|
|
13
|
+
type MouseEvent,
|
|
14
|
+
type ReactElement,
|
|
15
|
+
type ReactNode,
|
|
16
|
+
type Ref,
|
|
17
|
+
useCallback,
|
|
18
|
+
useMemo
|
|
19
|
+
} from 'react';
|
|
20
|
+
import { useRouter } from './context';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Props for RouterLink component.
|
|
24
|
+
* Similar to RouterLinkProps from @esmx/router with React-specific additions.
|
|
25
|
+
*/
|
|
26
|
+
export interface RouterLinkComponentProps {
|
|
27
|
+
/** Target route location (required) */
|
|
28
|
+
to: RouteLocationInput;
|
|
29
|
+
/** Navigation type */
|
|
30
|
+
type?: RouterLinkType;
|
|
31
|
+
/** @deprecated Use type='replace' instead */
|
|
32
|
+
replace?: boolean;
|
|
33
|
+
/** Active matching mode */
|
|
34
|
+
exact?: RouteMatchType;
|
|
35
|
+
/** CSS class for active state */
|
|
36
|
+
activeClass?: string;
|
|
37
|
+
/** Event(s) that trigger navigation */
|
|
38
|
+
event?: string | string[];
|
|
39
|
+
/** HTML tag to render */
|
|
40
|
+
tag?: string;
|
|
41
|
+
/** Layer navigation options */
|
|
42
|
+
layerOptions?: RouteLayerOptions;
|
|
43
|
+
/** Hook function called before navigation */
|
|
44
|
+
beforeNavigate?: (event: Event, eventName: string) => void;
|
|
45
|
+
/** Link content */
|
|
46
|
+
children?: ReactNode;
|
|
47
|
+
/** Additional CSS class name */
|
|
48
|
+
className?: string;
|
|
49
|
+
/** Inline styles */
|
|
50
|
+
style?: CSSProperties;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* RouterLink component for declarative navigation.
|
|
55
|
+
* Renders an anchor tag with proper navigation behavior and active state management.
|
|
56
|
+
* Supports all navigation types: push, replace, pushWindow, replaceWindow, pushLayer.
|
|
57
|
+
*
|
|
58
|
+
* @param props - Component props
|
|
59
|
+
* @param props.to - Target route location
|
|
60
|
+
* @param props.type - Navigation type ('push' | 'replace' | 'pushWindow' | 'replaceWindow' | 'pushLayer')
|
|
61
|
+
* @param props.exact - Active matching mode ('include' | 'exact' | 'route')
|
|
62
|
+
* @param props.activeClass - CSS class for active state
|
|
63
|
+
* @param props.event - Event(s) that trigger navigation
|
|
64
|
+
* @param props.tag - HTML tag to render (default: 'a')
|
|
65
|
+
* @param props.layerOptions - Options for layer navigation
|
|
66
|
+
* @param props.beforeNavigate - Callback before navigation
|
|
67
|
+
* @param props.children - Link content
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```tsx
|
|
71
|
+
* // Basic navigation
|
|
72
|
+
* <RouterLink to="/home">Home</RouterLink>
|
|
73
|
+
* <RouterLink to="/about">About</RouterLink>
|
|
74
|
+
*
|
|
75
|
+
* // With object location
|
|
76
|
+
* <RouterLink to={{ path: '/users', query: { page: '1' } }}>
|
|
77
|
+
* Users
|
|
78
|
+
* </RouterLink>
|
|
79
|
+
*
|
|
80
|
+
* // Replace navigation (no history entry)
|
|
81
|
+
* <RouterLink to="/login" type="replace">Login</RouterLink>
|
|
82
|
+
*
|
|
83
|
+
* // Open in new window
|
|
84
|
+
* <RouterLink to="/external" type="pushWindow">External</RouterLink>
|
|
85
|
+
*
|
|
86
|
+
* // Custom active class
|
|
87
|
+
* <RouterLink
|
|
88
|
+
* to="/dashboard"
|
|
89
|
+
* activeClass="nav-active"
|
|
90
|
+
* exact="exact"
|
|
91
|
+
* >
|
|
92
|
+
* Dashboard
|
|
93
|
+
* </RouterLink>
|
|
94
|
+
*
|
|
95
|
+
* // Custom tag (button)
|
|
96
|
+
* <RouterLink to="/submit" tag="button" className="btn">
|
|
97
|
+
* Submit
|
|
98
|
+
* </RouterLink>
|
|
99
|
+
*
|
|
100
|
+
* // With beforeNavigate callback
|
|
101
|
+
* <RouterLink
|
|
102
|
+
* to="/protected"
|
|
103
|
+
* beforeNavigate={(e, eventName) => {
|
|
104
|
+
* if (!isAuthenticated) {
|
|
105
|
+
* e.preventDefault();
|
|
106
|
+
* showLoginModal();
|
|
107
|
+
* }
|
|
108
|
+
* }}
|
|
109
|
+
* >
|
|
110
|
+
* Protected Page
|
|
111
|
+
* </RouterLink>
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
function RouterLinkInner(
|
|
115
|
+
props: RouterLinkComponentProps & { [key: string]: any },
|
|
116
|
+
ref: Ref<HTMLAnchorElement>
|
|
117
|
+
): ReactElement {
|
|
118
|
+
const {
|
|
119
|
+
to,
|
|
120
|
+
type = 'push',
|
|
121
|
+
replace,
|
|
122
|
+
exact = 'include',
|
|
123
|
+
activeClass,
|
|
124
|
+
event = 'click',
|
|
125
|
+
tag = 'a',
|
|
126
|
+
layerOptions,
|
|
127
|
+
beforeNavigate,
|
|
128
|
+
children,
|
|
129
|
+
className,
|
|
130
|
+
style,
|
|
131
|
+
...rest
|
|
132
|
+
} = props;
|
|
133
|
+
|
|
134
|
+
const router = useRouter();
|
|
135
|
+
|
|
136
|
+
// Resolve the link using router's built-in resolver
|
|
137
|
+
const linkResolved: RouterLinkResolved = useMemo(() => {
|
|
138
|
+
return router.resolveLink({
|
|
139
|
+
to,
|
|
140
|
+
type,
|
|
141
|
+
replace,
|
|
142
|
+
exact,
|
|
143
|
+
activeClass,
|
|
144
|
+
event,
|
|
145
|
+
tag,
|
|
146
|
+
layerOptions,
|
|
147
|
+
beforeNavigate
|
|
148
|
+
});
|
|
149
|
+
}, [
|
|
150
|
+
router,
|
|
151
|
+
to,
|
|
152
|
+
type,
|
|
153
|
+
replace,
|
|
154
|
+
exact,
|
|
155
|
+
activeClass,
|
|
156
|
+
event,
|
|
157
|
+
tag,
|
|
158
|
+
layerOptions,
|
|
159
|
+
beforeNavigate
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
// Handle click event
|
|
163
|
+
const handleClick = useCallback(
|
|
164
|
+
async (e: MouseEvent<HTMLElement>) => {
|
|
165
|
+
// Call beforeNavigate callback if provided
|
|
166
|
+
beforeNavigate?.(e as unknown as Event, 'click');
|
|
167
|
+
|
|
168
|
+
// If default was prevented by beforeNavigate or by modifier keys, skip navigation
|
|
169
|
+
if (e.defaultPrevented) return;
|
|
170
|
+
|
|
171
|
+
// Check for modifier keys - let browser handle these
|
|
172
|
+
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
|
|
173
|
+
if (e.button !== 0) return;
|
|
174
|
+
|
|
175
|
+
// Prevent default browser navigation
|
|
176
|
+
e.preventDefault();
|
|
177
|
+
|
|
178
|
+
// Navigate using the resolved link's navigate function
|
|
179
|
+
await linkResolved.navigate(e as unknown as Event);
|
|
180
|
+
},
|
|
181
|
+
[linkResolved, beforeNavigate]
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// Build class names
|
|
185
|
+
const computedClassName = useMemo(() => {
|
|
186
|
+
const classes: string[] = [];
|
|
187
|
+
if (linkResolved.attributes.class) {
|
|
188
|
+
classes.push(linkResolved.attributes.class);
|
|
189
|
+
}
|
|
190
|
+
if (className) {
|
|
191
|
+
classes.push(className);
|
|
192
|
+
}
|
|
193
|
+
return classes.join(' ') || undefined;
|
|
194
|
+
}, [linkResolved.attributes.class, className]);
|
|
195
|
+
|
|
196
|
+
// Build props for the element
|
|
197
|
+
const elementProps = {
|
|
198
|
+
ref,
|
|
199
|
+
href: linkResolved.attributes.href,
|
|
200
|
+
target: linkResolved.attributes.target,
|
|
201
|
+
rel: linkResolved.attributes.rel,
|
|
202
|
+
className: computedClassName,
|
|
203
|
+
style,
|
|
204
|
+
onClick: handleClick,
|
|
205
|
+
...rest
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Render the element with the appropriate tag using createElement
|
|
209
|
+
return createElement(tag as ElementType, elementProps, children);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Cast needed to fix TypeScript forwardRef type inference issue
|
|
213
|
+
// This is a common pattern when index signatures conflict with forwardRef's prop handling
|
|
214
|
+
export const RouterLink = forwardRef(
|
|
215
|
+
RouterLinkInner as any
|
|
216
|
+
) as React.ForwardRefExoticComponent<
|
|
217
|
+
RouterLinkComponentProps & {
|
|
218
|
+
[key: string]: any;
|
|
219
|
+
} & React.RefAttributes<HTMLAnchorElement>
|
|
220
|
+
>;
|
|
221
|
+
|
|
222
|
+
RouterLink.displayName = 'RouterLink';
|