@equinor/fusion-framework-module-navigation 7.0.0-next.2 → 7.0.1
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/CHANGELOG.md +42 -19
- package/README.md +97 -614
- package/dist/esm/NavigationConfigurator.js +29 -6
- package/dist/esm/NavigationConfigurator.js.map +1 -1
- package/dist/esm/NavigationProvider.js +57 -23
- package/dist/esm/NavigationProvider.js.map +1 -1
- package/dist/esm/events.js +20 -3
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/index.js +23 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/BaseHistory.js +23 -6
- package/dist/esm/lib/BaseHistory.js.map +1 -1
- package/dist/esm/lib/BrowserHistoryStack.js +5 -6
- package/dist/esm/lib/BrowserHistoryStack.js.map +1 -1
- package/dist/esm/lib/MemoryHistory.js +13 -6
- package/dist/esm/lib/MemoryHistory.js.map +1 -1
- package/dist/esm/lib/MemoryStack.js +1 -0
- package/dist/esm/lib/MemoryStack.js.map +1 -1
- package/dist/esm/lib/ProxyHistory.js +114 -0
- package/dist/esm/lib/ProxyHistory.js.map +1 -0
- package/dist/esm/lib/create-history.js +7 -13
- package/dist/esm/lib/create-history.js.map +1 -1
- package/dist/esm/lib/index.js +10 -0
- package/dist/esm/lib/index.js.map +1 -1
- package/dist/esm/lib/state/history.flows.js +7 -12
- package/dist/esm/lib/state/history.flows.js.map +1 -1
- package/dist/esm/lib/state/history.reducer.js +8 -4
- package/dist/esm/lib/state/history.reducer.js.map +1 -1
- package/dist/esm/lib/state/history.state.js +9 -3
- package/dist/esm/lib/state/history.state.js.map +1 -1
- package/dist/esm/lib/utils/resolve-browser-location.js +2 -2
- package/dist/esm/lib/utils/resolve-browser-location.js.map +1 -1
- package/dist/esm/module.js +4 -4
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/NavigationConfigurator.d.ts +10 -1
- package/dist/types/NavigationConfigurator.interface.d.ts +25 -5
- package/dist/types/NavigationProvider.d.ts +57 -23
- package/dist/types/NavigationProvider.interface.d.ts +20 -7
- package/dist/types/events.d.ts +20 -3
- package/dist/types/index.d.ts +24 -1
- package/dist/types/lib/BaseHistory.d.ts +23 -6
- package/dist/types/lib/BrowserHistoryStack.d.ts +5 -6
- package/dist/types/lib/MemoryHistory.d.ts +5 -5
- package/dist/types/lib/ProxyHistory.d.ts +68 -0
- package/dist/types/lib/create-history.d.ts +7 -13
- package/dist/types/lib/index.d.ts +10 -0
- package/dist/types/lib/state/history.flows.d.ts +6 -11
- package/dist/types/lib/state/history.reducer.d.ts +8 -4
- package/dist/types/lib/state/history.state.d.ts +9 -3
- package/dist/types/lib/types.d.ts +39 -8
- package/dist/types/version.d.ts +1 -1
- package/package.json +12 -13
- package/src/NavigationConfigurator.interface.ts +28 -5
- package/src/NavigationConfigurator.ts +47 -15
- package/src/NavigationProvider.interface.ts +20 -7
- package/src/NavigationProvider.ts +57 -23
- package/src/__tests__/ProxyHistory.test.ts +149 -0
- package/src/events.ts +20 -3
- package/src/index.ts +25 -1
- package/src/lib/BaseHistory.ts +23 -6
- package/src/lib/BrowserHistoryStack.ts +5 -6
- package/src/lib/MemoryHistory.ts +13 -6
- package/src/lib/MemoryStack.ts +1 -0
- package/src/lib/ProxyHistory.ts +144 -0
- package/src/lib/create-history.ts +7 -13
- package/src/lib/index.ts +11 -0
- package/src/lib/state/history.flows.ts +7 -12
- package/src/lib/state/history.reducer.ts +8 -4
- package/src/lib/state/history.state.ts +9 -3
- package/src/lib/types.ts +39 -8
- package/src/lib/utils/resolve-browser-location.ts +2 -2
- package/src/module.ts +4 -4
- package/src/version.ts +1 -1
|
@@ -38,11 +38,15 @@ export type Path = {
|
|
|
38
38
|
};
|
|
39
39
|
/**
|
|
40
40
|
* Location object representing a navigation entry.
|
|
41
|
-
* Extends Path with state and a unique key.
|
|
41
|
+
* Extends {@link Path} with arbitrary state data and a unique key.
|
|
42
|
+
*
|
|
43
|
+
* @template T - The type of the state payload stored in this location entry
|
|
42
44
|
*/
|
|
43
45
|
export type Location<T = any> = Path & {
|
|
44
46
|
state: T;
|
|
45
47
|
key: string;
|
|
48
|
+
/** Masked path used by react-router 7.13+ for unstable view-transition masking. */
|
|
49
|
+
unstable_mask: Path | undefined;
|
|
46
50
|
};
|
|
47
51
|
/**
|
|
48
52
|
* Internal state for history management.
|
|
@@ -57,7 +61,10 @@ export type LocationState = {
|
|
|
57
61
|
*/
|
|
58
62
|
export type NavigationListener = (update: Readonly<NavigationUpdate>) => void;
|
|
59
63
|
/**
|
|
60
|
-
* Navigation update event containing action and
|
|
64
|
+
* Navigation update event containing the action, location, and stack delta.
|
|
65
|
+
*
|
|
66
|
+
* @template A - The type of navigation action (defaults to {@link Action})
|
|
67
|
+
* @template T - The type of the state payload in the location
|
|
61
68
|
*/
|
|
62
69
|
export type NavigationUpdate<A extends string = Action, T = unknown> = {
|
|
63
70
|
delta: number;
|
|
@@ -96,7 +103,16 @@ export interface NavigateOptions {
|
|
|
96
103
|
}
|
|
97
104
|
/**
|
|
98
105
|
* History interface for managing navigation state.
|
|
99
|
-
*
|
|
106
|
+
*
|
|
107
|
+
* Compatible with Remix / React Router and provides observable
|
|
108
|
+
* state management via RxJS.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* const history = createHistory('browser');
|
|
113
|
+
* history.push('/dashboard');
|
|
114
|
+
* history.state$.subscribe(update => console.log(update.location.pathname));
|
|
115
|
+
* ```
|
|
100
116
|
*/
|
|
101
117
|
export interface History extends Disposable {
|
|
102
118
|
/** Observable stream of navigation state updates. */
|
|
@@ -107,11 +123,20 @@ export interface History extends Disposable {
|
|
|
107
123
|
readonly action: Action;
|
|
108
124
|
/** Current location in the history stack. */
|
|
109
125
|
readonly location: Location;
|
|
110
|
-
/** Creates a valid href string for a given path.
|
|
126
|
+
/** Creates a valid href string for a given path.
|
|
127
|
+
* @param to - Target path or partial path object
|
|
128
|
+
* @returns Fully-qualified href string
|
|
129
|
+
*/
|
|
111
130
|
createHref(to: To): string;
|
|
112
|
-
/** Creates a URL object for a given path.
|
|
131
|
+
/** Creates a {@link URL} object for a given path.
|
|
132
|
+
* @param to - Target path or partial path object
|
|
133
|
+
* @returns Resolved {@link URL} instance
|
|
134
|
+
*/
|
|
113
135
|
createURL(to: To): URL;
|
|
114
|
-
/** Encodes a location by properly URL-encoding the pathname.
|
|
136
|
+
/** Encodes a location by properly URL-encoding the pathname.
|
|
137
|
+
* @param to - Target path or partial path object
|
|
138
|
+
* @returns A {@link Path} with URL-encoded components
|
|
139
|
+
*/
|
|
115
140
|
encodeLocation(to: To): Path;
|
|
116
141
|
/** Pushes a new navigation entry onto the history stack. */
|
|
117
142
|
push(to: To, state?: unknown): void;
|
|
@@ -121,9 +146,15 @@ export interface History extends Disposable {
|
|
|
121
146
|
navigate(to: To, options?: NavigateOptions): void;
|
|
122
147
|
/** Navigates backward or forward in the history stack. */
|
|
123
148
|
go(delta: number): void;
|
|
124
|
-
/** Sets up a listener for navigation changes.
|
|
149
|
+
/** Sets up a listener for navigation changes.
|
|
150
|
+
* @param listener - Callback invoked on POP actions (browser back/forward)
|
|
151
|
+
* @returns A function that unsubscribes the listener when called
|
|
152
|
+
*/
|
|
125
153
|
listen(listener: NavigationListener): () => void;
|
|
126
|
-
/** Registers a blocker to intercept navigation attempts.
|
|
154
|
+
/** Registers a blocker to intercept navigation attempts.
|
|
155
|
+
* @param blocker - Callback invoked before each navigation to allow or prevent it
|
|
156
|
+
* @returns A function that removes the blocker when called
|
|
157
|
+
*/
|
|
127
158
|
block(blocker: NavigationBlocker): VoidFunction;
|
|
128
159
|
}
|
|
129
160
|
/**
|
package/dist/types/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "7.0.
|
|
1
|
+
export declare const version = "7.0.1";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@equinor/fusion-framework-module-navigation",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.1",
|
|
4
4
|
"description": "Navigation module for Fusion Framework providing routing and navigation capabilities using React Router 7",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "dist/esm/index.js",
|
|
@@ -33,24 +33,23 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@remix-run/router": "^1.23.0",
|
|
35
35
|
"uuid": "^13.0.0",
|
|
36
|
-
"zod": "^4.
|
|
36
|
+
"zod": "^4.3.6"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"
|
|
40
|
-
"jsdom": "^27.1.0",
|
|
39
|
+
"jsdom": "^29.0.2",
|
|
41
40
|
"rxjs": "^7.8.1",
|
|
42
|
-
"typescript": "^5.
|
|
43
|
-
"vitest": "^
|
|
44
|
-
"@equinor/fusion-framework-module": "^
|
|
45
|
-
"@equinor/fusion-framework-module-
|
|
46
|
-
"@equinor/fusion-framework-module
|
|
47
|
-
"@equinor/fusion-observable": "^
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vitest": "^4.1.0",
|
|
43
|
+
"@equinor/fusion-framework-module-event": "^6.0.0",
|
|
44
|
+
"@equinor/fusion-framework-module-telemetry": "^5.0.0",
|
|
45
|
+
"@equinor/fusion-framework-module": "^6.0.0",
|
|
46
|
+
"@equinor/fusion-observable": "^9.0.0"
|
|
48
47
|
},
|
|
49
48
|
"peerDependencies": {
|
|
50
|
-
"@remix-run/router": "^1.
|
|
49
|
+
"@remix-run/router": "^1.0.0",
|
|
51
50
|
"rxjs": "^7.0.0",
|
|
52
|
-
"@equinor/fusion-
|
|
53
|
-
"@equinor/fusion-
|
|
51
|
+
"@equinor/fusion-observable": "^9.0.0",
|
|
52
|
+
"@equinor/fusion-framework-module": "^6.0.0"
|
|
54
53
|
},
|
|
55
54
|
"scripts": {
|
|
56
55
|
"build": "tsc -b",
|
|
@@ -4,15 +4,38 @@ import type { IEventModuleProvider } from '@equinor/fusion-framework-module-even
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Configuration object for the navigation module.
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
|
+
* Provides options for customizing the history implementation, basename prefix,
|
|
9
|
+
* telemetry tracking, and event dispatching used by the {@link NavigationProvider}.
|
|
8
10
|
*/
|
|
9
11
|
export interface INavigationConfigurator {
|
|
10
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Base pathname prefix for the application (e.g. `"/apps/my-app"`).
|
|
14
|
+
*
|
|
15
|
+
* When set, the navigation provider automatically prepends this prefix to
|
|
16
|
+
* outgoing paths and strips it from incoming paths, so consumer code
|
|
17
|
+
* operates on clean, basename-free paths.
|
|
18
|
+
*/
|
|
11
19
|
basename?: string;
|
|
12
|
-
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Custom {@link History} instance for navigation.
|
|
23
|
+
*
|
|
24
|
+
* If not provided, defaults to browser history in browser environments
|
|
25
|
+
* or memory history in Node.js environments. Create instances with
|
|
26
|
+
* {@link createHistory}.
|
|
27
|
+
*/
|
|
13
28
|
history?: History;
|
|
14
|
-
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Telemetry provider for tracking navigation events, location changes,
|
|
32
|
+
* and errors for monitoring and debugging.
|
|
33
|
+
*/
|
|
15
34
|
telemetry?: ITelemetryProvider;
|
|
16
|
-
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Event provider for dispatching {@link NavigateEvent} and {@link NavigatedEvent}.
|
|
38
|
+
* Allows other modules to listen for and react to navigation changes.
|
|
39
|
+
*/
|
|
17
40
|
eventProvider?: IEventModuleProvider;
|
|
18
41
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { of, type ObservableInput } from 'rxjs';
|
|
2
|
+
import { of, from, type ObservableInput } from 'rxjs';
|
|
3
|
+
import { map } from 'rxjs/operators';
|
|
3
4
|
import {
|
|
4
5
|
BaseConfigBuilder,
|
|
5
6
|
type ModulesInstance,
|
|
@@ -12,6 +13,7 @@ import type { INavigationConfigurator } from './NavigationConfigurator.interface
|
|
|
12
13
|
|
|
13
14
|
import type { History } from './lib/types';
|
|
14
15
|
import { createHistory } from './lib/create-history';
|
|
16
|
+
import { ProxyHistory } from './lib/ProxyHistory';
|
|
15
17
|
import type { NavigationModule } from './module';
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -72,16 +74,21 @@ export class NavigationConfigurator extends BaseConfigBuilder<INavigationConfigu
|
|
|
72
74
|
return await args.requireInstance('event');
|
|
73
75
|
}
|
|
74
76
|
});
|
|
75
|
-
this.setHistory(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
77
|
+
this.setHistory(
|
|
78
|
+
async (args) => {
|
|
79
|
+
const history = (args.ref as ModulesInstance<[NavigationModule]>)?.navigation?.history;
|
|
80
|
+
if (history) {
|
|
81
|
+
// Wrap the provided history in a ProxyHistory to ensure the module can manage its own teardowns without affecting the original instance.
|
|
82
|
+
return new ProxyHistory(history);
|
|
83
|
+
}
|
|
84
|
+
if (typeof window !== 'undefined') {
|
|
85
|
+
return createHistory('browser');
|
|
86
|
+
}
|
|
87
|
+
return createHistory('memory');
|
|
88
|
+
},
|
|
89
|
+
// Don't wrap the default history in a ProxyHistory since it's already owned by the module and will be properly disposed.
|
|
90
|
+
{ proxy: false },
|
|
91
|
+
);
|
|
85
92
|
}
|
|
86
93
|
/**
|
|
87
94
|
* @deprecated Use `setBasename()` method instead
|
|
@@ -121,13 +128,38 @@ export class NavigationConfigurator extends BaseConfigBuilder<INavigationConfigu
|
|
|
121
128
|
/**
|
|
122
129
|
* Sets a custom history instance for the navigation module.
|
|
123
130
|
*
|
|
131
|
+
* By default the resolved history is wrapped in a {@link ProxyHistory} so the
|
|
132
|
+
* module gets its own disposable handle without owning (or accidentally
|
|
133
|
+
* disposing) the original instance. Set `proxy` to `false` to use the
|
|
134
|
+
* history as-is.
|
|
135
|
+
*
|
|
124
136
|
* @param historyOrCallback - History instance or configuration callback
|
|
137
|
+
* @param options - Optional settings for history wrapping
|
|
138
|
+
* @param options.proxy - Wrap the history in a {@link ProxyHistory} (default: `true`)
|
|
125
139
|
* @returns The configurator instance for method chaining
|
|
126
140
|
*/
|
|
127
|
-
public setHistory(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
public setHistory(
|
|
142
|
+
historyOrCallback?: History | ConfigBuilderCallback<History>,
|
|
143
|
+
options?: { proxy?: boolean },
|
|
144
|
+
): this {
|
|
145
|
+
const { proxy = true } = options ?? {};
|
|
146
|
+
const resolve =
|
|
147
|
+
typeof historyOrCallback === 'function'
|
|
148
|
+
? historyOrCallback
|
|
149
|
+
: // Normalize a direct instance to a callback for consistent handling.
|
|
150
|
+
async () => historyOrCallback;
|
|
151
|
+
|
|
152
|
+
if (proxy) {
|
|
153
|
+
// Wrap each emitted history in a ProxyHistory so dispose only tears down
|
|
154
|
+
// proxy-owned listeners/blockers, never the underlying history itself.
|
|
155
|
+
this._set('history', (args) =>
|
|
156
|
+
from(resolve(args) as ObservableInput<History | undefined>).pipe(
|
|
157
|
+
map((history) => (history ? new ProxyHistory(history) : undefined)),
|
|
158
|
+
),
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
this._set('history', resolve);
|
|
162
|
+
}
|
|
131
163
|
return this;
|
|
132
164
|
}
|
|
133
165
|
|
|
@@ -9,7 +9,17 @@ import type { IModuleProvider } from '@equinor/fusion-framework-module';
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Navigation provider interface.
|
|
12
|
-
*
|
|
12
|
+
*
|
|
13
|
+
* Provides routing and navigation capabilities with automatic basename
|
|
14
|
+
* localization. Consumers work with clean paths (e.g. `/users`) while the
|
|
15
|
+
* underlying history operates on full paths (e.g. `/apps/my-app/users`).
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const nav: INavigationProvider = framework.modules.navigation;
|
|
20
|
+
* nav.push('/users');
|
|
21
|
+
* console.log(nav.path.pathname); // '/users'
|
|
22
|
+
* ```
|
|
13
23
|
*/
|
|
14
24
|
export interface INavigationProvider extends IModuleProvider {
|
|
15
25
|
/**
|
|
@@ -41,22 +51,25 @@ export interface INavigationProvider extends IModuleProvider {
|
|
|
41
51
|
/**
|
|
42
52
|
* Creates a router instance from route configuration.
|
|
43
53
|
*
|
|
44
|
-
* @
|
|
45
|
-
* @
|
|
54
|
+
* @deprecated Use `@equinor/fusion-framework-react-router` instead.
|
|
55
|
+
* @param routes - Route configuration objects compatible with Remix/React Router
|
|
56
|
+
* @returns A configured and initialized {@link Router} instance with basename applied
|
|
46
57
|
*/
|
|
47
58
|
createRouter(routes: AgnosticRouteObject[]): Router;
|
|
48
59
|
|
|
49
60
|
/**
|
|
50
|
-
* Creates a localized href string
|
|
61
|
+
* Creates a localized href string including the basename prefix.
|
|
51
62
|
*
|
|
52
|
-
* @param to -
|
|
63
|
+
* @param to - Path or location to resolve (defaults to current path)
|
|
64
|
+
* @returns Fully-qualified href string with basename included
|
|
53
65
|
*/
|
|
54
66
|
createHref(to?: To): string;
|
|
55
67
|
|
|
56
68
|
/**
|
|
57
|
-
* Creates a full URL object
|
|
69
|
+
* Creates a full {@link URL} object including the basename prefix.
|
|
58
70
|
*
|
|
59
|
-
* @param to -
|
|
71
|
+
* @param to - Path or location to resolve (defaults to current path)
|
|
72
|
+
* @returns A {@link URL} instance representing the resolved navigation target
|
|
60
73
|
*/
|
|
61
74
|
createURL(to?: To): URL;
|
|
62
75
|
|
|
@@ -37,17 +37,23 @@ const normalizePathname = (path: string) => path.replace(/\/+/g, '/').replace(/\
|
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Navigation provider implementation.
|
|
40
|
-
*
|
|
40
|
+
*
|
|
41
|
+
* Manages routing and navigation state with automatic basename localization.
|
|
42
|
+
* Wraps a {@link History} instance to expose observable state, path localization,
|
|
43
|
+
* and router creation.
|
|
41
44
|
*
|
|
42
45
|
* @remarks
|
|
43
|
-
*
|
|
44
|
-
* -
|
|
45
|
-
* -
|
|
46
|
-
* -
|
|
47
|
-
* - Handles navigation actions (push, replace, createHref, etc.)
|
|
46
|
+
* - Emits localized paths (basename removed) to consumers via {@link NavigationProvider.state$ | state$}
|
|
47
|
+
* - Internally prefixes paths with the basename before forwarding to the history stack
|
|
48
|
+
* - Creates routers compatible with Remix / React Router via {@link NavigationProvider.createRouter | createRouter}
|
|
49
|
+
* - Dispatches {@link NavigatedEvent} and telemetry on navigation changes
|
|
48
50
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* const provider = new NavigationProvider({ version, config });
|
|
54
|
+
* provider.push('/users');
|
|
55
|
+
* console.log(provider.path.pathname); // '/users'
|
|
56
|
+
* ```
|
|
51
57
|
*/
|
|
52
58
|
export class NavigationProvider
|
|
53
59
|
extends BaseModuleProvider<INavigationConfigurator>
|
|
@@ -62,14 +68,19 @@ export class NavigationProvider
|
|
|
62
68
|
|
|
63
69
|
/**
|
|
64
70
|
* Observable stream of navigation state updates.
|
|
65
|
-
*
|
|
71
|
+
*
|
|
72
|
+
* Emits localized paths (with basename removed) and filters to only
|
|
73
|
+
* paths within the basename scope. Late subscribers receive the last
|
|
74
|
+
* emitted value immediately.
|
|
66
75
|
*/
|
|
67
76
|
public get state$(): Observable<NavigationUpdate> {
|
|
68
77
|
return this.#state$;
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
/**
|
|
72
|
-
* Gets the basename.
|
|
81
|
+
* Gets the basename prefix configured for this provider.
|
|
82
|
+
*
|
|
83
|
+
* @returns The basename string, or an empty string if none is configured
|
|
73
84
|
*/
|
|
74
85
|
public get basename(): string {
|
|
75
86
|
return this.#basename ?? '';
|
|
@@ -89,22 +100,28 @@ export class NavigationProvider
|
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
/**
|
|
92
|
-
* Gets the history instance.
|
|
103
|
+
* Gets the underlying history instance.
|
|
104
|
+
*
|
|
105
|
+
* @returns The {@link History} instance used for navigation
|
|
93
106
|
*/
|
|
94
107
|
public get history(): History {
|
|
95
108
|
return this.#history;
|
|
96
109
|
}
|
|
97
110
|
|
|
98
111
|
/**
|
|
99
|
-
* Gets the current localized path
|
|
112
|
+
* Gets the current localized path with the basename prefix removed.
|
|
113
|
+
*
|
|
114
|
+
* @returns A {@link Path} object representing the current location without basename
|
|
100
115
|
*/
|
|
101
116
|
public get path(): Path {
|
|
102
117
|
return this._localizePath(this.#history.location);
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
/**
|
|
121
|
+
* Creates a new {@link NavigationProvider}.
|
|
122
|
+
*
|
|
106
123
|
* @param args - Configuration arguments containing module config
|
|
107
|
-
* @throws {Error} If no history is provided in the configuration
|
|
124
|
+
* @throws {Error} If no history instance is provided in the configuration
|
|
108
125
|
*/
|
|
109
126
|
constructor(args: BaseModuleProviderCtorArgs<INavigationConfigurator>) {
|
|
110
127
|
super(args);
|
|
@@ -201,10 +218,10 @@ export class NavigationProvider
|
|
|
201
218
|
/**
|
|
202
219
|
* Creates a router instance from route configuration.
|
|
203
220
|
*
|
|
204
|
-
* @deprecated Use `@equinor/fusion-framework-react-router` instead
|
|
221
|
+
* @deprecated Use `@equinor/fusion-framework-react-router` instead.
|
|
205
222
|
*
|
|
206
|
-
* @param routes - Route configuration objects compatible with
|
|
207
|
-
* @returns A configured and initialized
|
|
223
|
+
* @param routes - Route configuration objects compatible with Remix/React Router
|
|
224
|
+
* @returns A configured and initialized {@link Router} instance with basename applied
|
|
208
225
|
*/
|
|
209
226
|
public createRouter(routes: AgnosticRouteObject[]) {
|
|
210
227
|
this.#telemetry?.trackEvent({
|
|
@@ -225,18 +242,26 @@ export class NavigationProvider
|
|
|
225
242
|
}
|
|
226
243
|
|
|
227
244
|
/**
|
|
228
|
-
* Creates a localized href string
|
|
245
|
+
* Creates a localized href string including the basename prefix.
|
|
229
246
|
*
|
|
230
|
-
* @param to -
|
|
247
|
+
* @param to - Path or location to resolve (defaults to current path)
|
|
248
|
+
* @returns Fully-qualified href string with basename included
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```ts
|
|
252
|
+
* // basename = '/apps/my-app'
|
|
253
|
+
* provider.createHref('/users'); // '/apps/my-app/users'
|
|
254
|
+
* ```
|
|
231
255
|
*/
|
|
232
256
|
public createHref(to?: To): string {
|
|
233
257
|
return this.#history.createHref(this._createToPath(to ?? this.path));
|
|
234
258
|
}
|
|
235
259
|
|
|
236
260
|
/**
|
|
237
|
-
* Creates a full URL object
|
|
261
|
+
* Creates a full {@link URL} object including the basename prefix.
|
|
238
262
|
*
|
|
239
|
-
* @param to -
|
|
263
|
+
* @param to - Path or location to resolve (defaults to current path)
|
|
264
|
+
* @returns A {@link URL} instance representing the resolved navigation target
|
|
240
265
|
*/
|
|
241
266
|
public createURL(to?: To): URL {
|
|
242
267
|
return this.#history.createURL(this._createToPath(to ?? this.path));
|
|
@@ -277,14 +302,20 @@ export class NavigationProvider
|
|
|
277
302
|
(this.#history as BaseHistory).pop();
|
|
278
303
|
}
|
|
279
304
|
/**
|
|
280
|
-
* Checks
|
|
305
|
+
* Checks whether a pathname falls within the configured basename scope.
|
|
306
|
+
*
|
|
307
|
+
* @param pathname - The pathname to check
|
|
308
|
+
* @returns `true` if the pathname starts with the basename (or no basename is set)
|
|
281
309
|
*/
|
|
282
310
|
protected _isWithinBasenameScope(pathname: string): boolean {
|
|
283
311
|
return this.#basename ? pathname.startsWith(this.#basename) : true;
|
|
284
312
|
}
|
|
285
313
|
|
|
286
314
|
/**
|
|
287
|
-
* Localizes a path by
|
|
315
|
+
* Localizes a path by stripping the basename prefix from the pathname.
|
|
316
|
+
*
|
|
317
|
+
* @param location - The full path to localize
|
|
318
|
+
* @returns A new {@link Path} with the basename removed from the pathname
|
|
288
319
|
*/
|
|
289
320
|
protected _localizePath(location: Path): Path {
|
|
290
321
|
const { pathname, search, hash } = location;
|
|
@@ -296,7 +327,10 @@ export class NavigationProvider
|
|
|
296
327
|
}
|
|
297
328
|
|
|
298
329
|
/**
|
|
299
|
-
* Creates a full path object from a target location,
|
|
330
|
+
* Creates a full path object from a target location, prepending the basename prefix.
|
|
331
|
+
*
|
|
332
|
+
* @param to - The target location (string path or partial {@link Path} object)
|
|
333
|
+
* @returns A partial {@link Path} with basename prepended to the pathname
|
|
300
334
|
*/
|
|
301
335
|
protected _createToPath(to: To): Partial<Path> {
|
|
302
336
|
// Parse the 'to' parameter into path components
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { firstValueFrom, skip } from 'rxjs';
|
|
3
|
+
import { MemoryHistory } from '../lib/MemoryHistory';
|
|
4
|
+
import { ProxyHistory } from '../lib/ProxyHistory';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Awaits the next state emission after performing a navigation action.
|
|
8
|
+
* MemoryHistory processes state updates asynchronously through the subject.
|
|
9
|
+
*/
|
|
10
|
+
const awaitNavigation = async (
|
|
11
|
+
history: MemoryHistory | ProxyHistory,
|
|
12
|
+
action: () => void,
|
|
13
|
+
): Promise<void> => {
|
|
14
|
+
const next = firstValueFrom(history.state$.pipe(skip(1)));
|
|
15
|
+
action();
|
|
16
|
+
await next;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe('ProxyHistory', () => {
|
|
20
|
+
let target: MemoryHistory;
|
|
21
|
+
let proxy: ProxyHistory;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
target = new MemoryHistory();
|
|
25
|
+
proxy = new ProxyHistory(target);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
proxy[Symbol.dispose]();
|
|
30
|
+
target[Symbol.dispose]();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('delegation', () => {
|
|
34
|
+
it('should delegate location to the target', () => {
|
|
35
|
+
expect(proxy.location).toBe(target.location);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should delegate action to the target', () => {
|
|
39
|
+
expect(proxy.action).toBe(target.action);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should delegate push to the target', async () => {
|
|
43
|
+
await awaitNavigation(proxy, () => proxy.push('/test'));
|
|
44
|
+
|
|
45
|
+
expect(target.location.pathname).toBe('/test');
|
|
46
|
+
expect(proxy.location.pathname).toBe('/test');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should delegate replace to the target', async () => {
|
|
50
|
+
await awaitNavigation(proxy, () => proxy.replace('/replaced'));
|
|
51
|
+
|
|
52
|
+
expect(target.location.pathname).toBe('/replaced');
|
|
53
|
+
expect(proxy.location.pathname).toBe('/replaced');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should delegate navigate to the target', async () => {
|
|
57
|
+
await awaitNavigation(proxy, () =>
|
|
58
|
+
proxy.navigate('/nav', { replace: true, state: { foo: 1 } }),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(target.location.pathname).toBe('/nav');
|
|
62
|
+
expect(target.location.state).toEqual({ foo: 1 });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should delegate createHref to the target', () => {
|
|
66
|
+
expect(proxy.createHref('/path')).toBe(target.createHref('/path'));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should delegate createURL to the target', () => {
|
|
70
|
+
expect(proxy.createURL('/path').href).toBe(target.createURL('/path').href);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should delegate encodeLocation to the target', () => {
|
|
74
|
+
const encoded = proxy.encodeLocation('/path');
|
|
75
|
+
expect(encoded).toEqual(target.encodeLocation('/path'));
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('pop()', () => {
|
|
80
|
+
it('should delegate pop to the target when target supports it', () => {
|
|
81
|
+
const popSpy = vi.spyOn(target, 'pop');
|
|
82
|
+
proxy.pop();
|
|
83
|
+
expect(popSpy).toHaveBeenCalledOnce();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('teardown isolation', () => {
|
|
88
|
+
it('should not dispose the target when proxy is disposed', async () => {
|
|
89
|
+
// Dispose the proxy
|
|
90
|
+
proxy[Symbol.dispose]();
|
|
91
|
+
|
|
92
|
+
// Target should still be functional — push should work
|
|
93
|
+
await awaitNavigation(target, () => target.push('/after-proxy-dispose'));
|
|
94
|
+
expect(target.location.pathname).toBe('/after-proxy-dispose');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should clean up proxy-owned listeners on dispose', async () => {
|
|
98
|
+
const proxyListener = vi.fn();
|
|
99
|
+
proxy.listen(proxyListener);
|
|
100
|
+
|
|
101
|
+
// Dispose the proxy — listener teardown should be called on the target
|
|
102
|
+
proxy[Symbol.dispose]();
|
|
103
|
+
|
|
104
|
+
// Target is still alive, push something — the disposed proxy listener
|
|
105
|
+
// should not fire (listen only triggers on POP, but the underlying
|
|
106
|
+
// subscription should be removed entirely)
|
|
107
|
+
await awaitNavigation(target, () => target.push('/after-dispose'));
|
|
108
|
+
expect(target.location.pathname).toBe('/after-dispose');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should not affect target listeners when proxy is disposed', async () => {
|
|
112
|
+
// Register a listener directly on the target
|
|
113
|
+
const targetListener = vi.fn();
|
|
114
|
+
target.listen(targetListener);
|
|
115
|
+
|
|
116
|
+
// Register a listener through the proxy
|
|
117
|
+
proxy.listen(vi.fn());
|
|
118
|
+
|
|
119
|
+
// Dispose proxy — only proxy listener should be removed
|
|
120
|
+
proxy[Symbol.dispose]();
|
|
121
|
+
|
|
122
|
+
// Target should still be functional
|
|
123
|
+
await awaitNavigation(target, () => target.push('/still-works'));
|
|
124
|
+
expect(target.location.pathname).toBe('/still-works');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('listen/block unsubscribe', () => {
|
|
129
|
+
it('should allow manual unsubscribe of a listener', () => {
|
|
130
|
+
const listener = vi.fn();
|
|
131
|
+
const unlisten = proxy.listen(listener);
|
|
132
|
+
|
|
133
|
+
unlisten();
|
|
134
|
+
|
|
135
|
+
// No error on dispose — the listener was already removed
|
|
136
|
+
proxy[Symbol.dispose]();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should allow manual unsubscribe of a blocker', () => {
|
|
140
|
+
const blocker = vi.fn();
|
|
141
|
+
const unblock = proxy.block(blocker);
|
|
142
|
+
|
|
143
|
+
unblock();
|
|
144
|
+
|
|
145
|
+
// No error on dispose — the blocker was already removed
|
|
146
|
+
proxy[Symbol.dispose]();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
package/src/events.ts
CHANGED
|
@@ -15,7 +15,16 @@ export interface NavigateEventDetail {
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Event emitted before navigation occurs.
|
|
18
|
-
* Can be canceled by calling `preventDefault()
|
|
18
|
+
* Can be canceled by calling `preventDefault()` to block the navigation.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* eventProvider.addEventListener('onNavigate', (event) => {
|
|
23
|
+
* if (hasUnsavedChanges) {
|
|
24
|
+
* event.preventDefault();
|
|
25
|
+
* }
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
19
28
|
*/
|
|
20
29
|
export class NavigateEvent extends FrameworkEvent<
|
|
21
30
|
FrameworkEventInit<NavigateEventDetail, INavigationProvider>
|
|
@@ -41,8 +50,16 @@ export interface NavigatedEventDetail {
|
|
|
41
50
|
}
|
|
42
51
|
|
|
43
52
|
/**
|
|
44
|
-
* Event emitted after navigation
|
|
45
|
-
* Contains the navigation action and
|
|
53
|
+
* Event emitted after navigation completes.
|
|
54
|
+
* Contains the navigation action type and both current and previous locations.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* eventProvider.addEventListener('onNavigated', (event) => {
|
|
59
|
+
* const { action, current, previous } = event.detail;
|
|
60
|
+
* console.log(`${action}: ${previous.location.pathname} → ${current.location.pathname}`);
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
46
63
|
*/
|
|
47
64
|
export class NavigatedEvent extends FrameworkEvent<
|
|
48
65
|
FrameworkEventInit<NavigatedEventDetail, INavigationProvider>
|