@equinor/fusion-framework-module-navigation 7.0.9-next.0 → 7.0.10

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.
Files changed (55) hide show
  1. package/dist/esm/version.js +1 -1
  2. package/dist/esm/version.js.map +1 -1
  3. package/dist/tsconfig.tsbuildinfo +1 -1
  4. package/dist/types/version.d.ts +1 -1
  5. package/package.json +10 -7
  6. package/CHANGELOG.md +0 -600
  7. package/src/NavigateEvent.ts +0 -47
  8. package/src/NavigatedEvent.ts +0 -35
  9. package/src/NavigationConfigurator.interface.ts +0 -41
  10. package/src/NavigationConfigurator.ts +0 -226
  11. package/src/NavigationProvider.interface.ts +0 -99
  12. package/src/NavigationProvider.ts +0 -436
  13. package/src/__tests__/BrowserHistory.test.ts +0 -151
  14. package/src/__tests__/HashHistory.test.ts +0 -121
  15. package/src/__tests__/MemoryHistory.test.ts +0 -185
  16. package/src/__tests__/NavigationProvider.test.ts +0 -302
  17. package/src/__tests__/ProxyHistory.test.ts +0 -149
  18. package/src/__tests__/setup.ts +0 -8
  19. package/src/enable-navigation.ts +0 -60
  20. package/src/index.ts +0 -51
  21. package/src/lib/BaseHistory.ts +0 -255
  22. package/src/lib/BrowserHistory.ts +0 -124
  23. package/src/lib/BrowserHistoryHashStack.ts +0 -57
  24. package/src/lib/BrowserHistoryStack.ts +0 -96
  25. package/src/lib/MemoryHistory.ts +0 -76
  26. package/src/lib/MemoryHistoryStack.ts +0 -118
  27. package/src/lib/ProxyHistory.ts +0 -145
  28. package/src/lib/create-history.ts +0 -58
  29. package/src/lib/index.ts +0 -33
  30. package/src/lib/state/actions.ts +0 -108
  31. package/src/lib/state/check-blockers.ts +0 -52
  32. package/src/lib/state/create-flow.ts +0 -36
  33. package/src/lib/state/create-history-reducer.ts +0 -77
  34. package/src/lib/state/create-store.ts +0 -39
  35. package/src/lib/state/flow-creators.ts +0 -7
  36. package/src/lib/state/go.ts +0 -20
  37. package/src/lib/state/history.state.ts +0 -14
  38. package/src/lib/state/index.ts +0 -4
  39. package/src/lib/state/navigate.ts +0 -58
  40. package/src/lib/state/pop.ts +0 -24
  41. package/src/lib/state/validate-current-location.ts +0 -33
  42. package/src/lib/types.ts +0 -190
  43. package/src/lib/utils/encode-trailing-whitespace.ts +0 -18
  44. package/src/lib/utils/has-protocol.ts +0 -21
  45. package/src/lib/utils/index.ts +0 -5
  46. package/src/lib/utils/path-to-string.ts +0 -24
  47. package/src/lib/utils/path-to-url.ts +0 -52
  48. package/src/lib/utils/resolve-browser-location.ts +0 -1
  49. package/src/lib/utils/resolve-hash-location.ts +0 -26
  50. package/src/lib/utils/resolve-path.ts +0 -22
  51. package/src/lib/utils/resolve-window-location.ts +0 -24
  52. package/src/module.ts +0 -80
  53. package/src/version.ts +0 -2
  54. package/tsconfig.json +0 -24
  55. package/vitest.config.ts +0 -14
@@ -1,149 +0,0 @@
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
- });
@@ -1,8 +0,0 @@
1
- import { beforeEach } from 'vitest';
2
-
3
- // Mock history and location
4
- beforeEach(() => {
5
- // Reset history state
6
- window.history.pushState({}, '', '/');
7
- window.history.replaceState({}, '', '/');
8
- });
@@ -1,60 +0,0 @@
1
- import type { IModulesConfigurator, ModuleConfigType } from '@equinor/fusion-framework-module';
2
- import { module, type NavigationModule } from './module';
3
-
4
- /**
5
- * Helper function to enable the navigation module.
6
- *
7
- * This is the main entry point for consumers to add navigation capabilities to their
8
- * framework configuration. It registers the navigation module and allows configuration
9
- * of basename and other navigation settings.
10
- *
11
- * The navigation module provides routing and navigation capabilities compatible with
12
- * industry-standard routers (Remix/React Router), with support for browser history,
13
- * hash routing, and memory history.
14
- *
15
- * @param configurator - The modules configurator to add navigation to
16
- * @param basenameOrOptions - Optional basename string or configuration object
17
- * @param basenameOrOptions.configure - Configuration callback for advanced setup
18
- * @typeParam TRef - Reference type for module composition
19
- *
20
- * @example
21
- * ```ts
22
- * // Simple usage with basename
23
- * enableNavigation(configurator, '/app');
24
- *
25
- * // Advanced configuration
26
- * enableNavigation(configurator, {
27
- * configure(config, ref) {
28
- * config.setBasename('/app');
29
- * config.setHistory(createHistory('browser'));
30
- * }
31
- * });
32
- * ```
33
- *
34
- * @see {@link NavigationModule} - The navigation module type
35
- * @see {@link INavigationConfigurator} - Configuration interface
36
- * @see {@link INavigationProvider} - Provider interface for accessing navigation
37
- */
38
- export const enableNavigation = <TRef = unknown>(
39
- // biome-ignore lint/suspicious/noExplicitAny: must be any to support all module types
40
- configurator: IModulesConfigurator<any, any>,
41
- basenameOrOptions?:
42
- | string
43
- | {
44
- configure: (config: ModuleConfigType<NavigationModule>, ref: TRef) => void;
45
- },
46
- ): void => {
47
- configurator.addConfig({
48
- module,
49
- configure(config, ref) {
50
- // A string shortcut sets the basename directly; otherwise defer to the caller's configure callback.
51
- if (typeof basenameOrOptions === 'string') {
52
- config.setBasename(basenameOrOptions);
53
- } else if (typeof basenameOrOptions === 'object' && 'configure' in basenameOrOptions) {
54
- basenameOrOptions.configure(config, ref);
55
- }
56
- },
57
- });
58
- };
59
-
60
- export default enableNavigation;
package/src/index.ts DELETED
@@ -1,51 +0,0 @@
1
- /**
2
- * @module @equinor/fusion-framework-module-navigation
3
- *
4
- * Navigation module for Fusion Framework providing routing and navigation capabilities.
5
- *
6
- * Manages observable navigation state with automatic basename localization,
7
- * so consumers work with clean paths while the underlying history receives
8
- * full paths including the basename prefix.
9
- *
10
- * @remarks
11
- * Supports browser, hash, and memory history types. Integrates with
12
- * `@remix-run/router` for router creation and is compatible with
13
- * industry-standard routers (Remix / React Router).
14
- *
15
- * @example
16
- * ```ts
17
- * import { enableNavigation, createHistory } from '@equinor/fusion-framework-module-navigation';
18
- *
19
- * enableNavigation(configurator, '/apps/my-app');
20
- * ```
21
- *
22
- * @packageDocumentation
23
- */
24
-
25
- export type { INavigationConfigurator } from './NavigationConfigurator.interface';
26
- export { NavigationConfigurator } from './NavigationConfigurator';
27
-
28
- export { NavigationModule, module, moduleKey } from './module';
29
- export { enableNavigation } from './enable-navigation';
30
-
31
- export type { INavigationProvider } from './NavigationProvider.interface';
32
- export { NavigationProvider } from './NavigationProvider';
33
-
34
- export { createHistory } from './lib/create-history';
35
-
36
- export { NavigateEvent, type NavigateEventDetail } from './NavigateEvent';
37
- export { NavigatedEvent, type NavigatedEventDetail } from './NavigatedEvent';
38
-
39
- export type {
40
- Path,
41
- To,
42
- Location,
43
- History,
44
- NavigationBlocker,
45
- NavigationListener,
46
- } from './lib/types';
47
-
48
- /**
49
- * @deprecated Use {@link History} instead.
50
- */
51
- export type { History as INavigator } from './lib';
@@ -1,255 +0,0 @@
1
- import { Subscription } from 'rxjs';
2
- import { filter } from 'rxjs/operators';
3
-
4
- import type { Actions, HistoryState } from './state';
5
-
6
- import { Action } from './types';
7
- import type {
8
- NavigateOptions,
9
- NavigationListener,
10
- NavigationUpdate,
11
- Path,
12
- To,
13
- History,
14
- NavigationBlocker,
15
- } from './types';
16
-
17
- /**
18
- * Abstract base class for history implementations.
19
- *
20
- * Provides common state management, navigation logic, and subscription
21
- * lifecycle that is shared across different history backends (browser,
22
- * hash, memory).
23
- */
24
- export abstract class BaseHistory implements History {
25
- // Subscriptions for cleanup
26
- #teardowns = new Subscription();
27
-
28
- // internal state
29
- #state: HistoryState;
30
-
31
- /**
32
- * Gets the current location in the history stack.
33
- *
34
- * @returns The current {@link Location} including pathname, search, hash, state, and key
35
- */
36
- public get location(): History['location'] {
37
- return this.#state.subject.value.current.location;
38
- }
39
-
40
- /**
41
- * Gets the current navigation action type.
42
- *
43
- * @returns The most recent {@link Action} (`Pop`, `Push`, or `Replace`)
44
- */
45
- public get action(): History['action'] {
46
- return this.#state.subject.value.current.action;
47
- }
48
-
49
- /**
50
- * Observable stream of navigation state changes.
51
- * Emits on all navigation events (push, replace, pop).
52
- * @returns Observable of the current navigation update.
53
- */
54
- public get state$(): History['state$'] {
55
- return this.#state.subject.select((state) => state.current);
56
- }
57
-
58
- /**
59
- * Observable stream of navigation actions.
60
- * @returns Observable of navigation actions.
61
- */
62
- public get action$(): History['action$'] {
63
- return this.#state.subject.action$;
64
- }
65
-
66
- /**
67
- * Checks whether there are any active navigation blockers.
68
- *
69
- * @returns `true` if one or more blockers are registered
70
- */
71
- public get hasBlockers(): boolean {
72
- return this.#state.subject.value.blockers.length > 0;
73
- }
74
-
75
- /** @param state - Reactive history state backing this implementation. */
76
- protected constructor(state: HistoryState) {
77
- this.#state = state;
78
- }
79
-
80
- /**
81
- * Creates a valid href string for a given path.
82
- *
83
- * @param to - Target path or partial path object
84
- * @returns Fully-qualified href string
85
- */
86
- public createHref(to: To): string {
87
- return this.createURL(to).href;
88
- }
89
-
90
- /**
91
- * Creates a {@link URL} object for a given path.
92
- *
93
- * @param to - Target path or partial path object
94
- * @returns Resolved {@link URL} instance
95
- */
96
- public createURL(to: To): URL {
97
- return this.#state.stack.createURL(to);
98
- }
99
-
100
- /**
101
- * Encodes a location by properly URL-encoding the pathname.
102
- *
103
- * @param to - Target path or partial path object
104
- * @returns A {@link Path} with URL-encoded components
105
- */
106
- public encodeLocation(to: To): Path {
107
- return this.#state.stack.createURL(to);
108
- }
109
-
110
- /**
111
- * Navigate to a location with explicit options.
112
- *
113
- * @param to - The target path (string, Path object, or Location object)
114
- * @param options - Navigation options specifying action (PUSH/REPLACE) and optional state
115
- */
116
- public navigate(to: To, options: NavigateOptions): void {
117
- const action = this.#state.actions.navigate(to, options);
118
- this.#state.subject.next(action);
119
- }
120
-
121
- /**
122
- * Pushes a new location onto the history stack.
123
- * @param to - Target path or partial path object.
124
- * @param state - Optional state associated with the new location.
125
- */
126
- public push(to: To, state?: unknown): void {
127
- this.navigate(to, { state });
128
- }
129
-
130
- /**
131
- * Replaces the current location in the history stack.
132
- * @param to - Target path or partial path object.
133
- * @param state - Optional state associated with the replacement location.
134
- */
135
- public replace(to: To, state?: unknown): void {
136
- this.navigate(to, { replace: true, state });
137
- }
138
-
139
- /**
140
- * Navigates backward or forward in the history stack.
141
- *
142
- * @param delta - The number of steps to move (negative for backward, positive for forward)
143
- */
144
- public go(delta: number): void {
145
- const action = this.#state.actions.go(delta);
146
- this.#state.subject.next(action);
147
- }
148
-
149
- /**
150
- * Sets up a listener for navigation changes.
151
- *
152
- * Only listens for POP actions (browser back/forward navigation). PUSH/REPLACE
153
- * actions are programmatic and synchronous - the caller already knows about them.
154
- * POP actions come from browser events and are asynchronous, so we need to listen.
155
- * This matches industry-standard router behavior (Remix/React Router).
156
- *
157
- * @param listener - Function to call on navigation changes
158
- * @returns Function to unsubscribe the listener
159
- */
160
- public listen(listener: NavigationListener): () => void {
161
- // Filter for POP actions only - PUSH/REPLACE are handled synchronously
162
- const subscription = this.#state.subject
163
- .select((state) => state.current)
164
- .pipe(
165
- filter((update): update is NavigationUpdate<Action.Pop> => update.action === Action.Pop),
166
- )
167
- .subscribe((update) => {
168
- listener(update);
169
- });
170
-
171
- // Register subscription for cleanup on dispose
172
- this._addTeardown(subscription);
173
-
174
- // Return unsubscribe function
175
- return () => {
176
- subscription.unsubscribe();
177
- this._removeTeardown(subscription);
178
- };
179
- }
180
-
181
- /**
182
- * Registers a blocker to intercept navigation attempts.
183
- *
184
- * @param blocker - Navigation blocker function to register
185
- * @returns Function to unsubscribe the blocker
186
- */
187
- public block(blocker: NavigationBlocker): VoidFunction {
188
- this.#state.subject.next(this.#state.actions.addBlocker(blocker));
189
- const removeBlocker = () => {
190
- this.#state.subject.next(this.#state.actions.removeBlocker(blocker));
191
- };
192
- return this._addTeardown(removeBlocker, { executeOnRemove: true });
193
- }
194
-
195
- /** Triggers a POP action for the current history location. */
196
- public pop(): void {
197
- this.#state.subject.next(this.#state.actions.pop());
198
- }
199
-
200
- /**
201
- * Disposes of the history instance and cleans up all subscriptions.
202
- */
203
- public [Symbol.dispose](): void {
204
- this.#teardowns.unsubscribe();
205
- }
206
-
207
- /**
208
- * Dispatches an action to trigger state updates.
209
- *
210
- * Use this to dispatch actions when external events occur (e.g., browser popstate).
211
- * The action will be processed by flows and update the history state.
212
- *
213
- * @param action - The action to dispatch
214
- */
215
- protected _dispatch(action: Actions): void {
216
- this.#state.subject.next(action);
217
- }
218
-
219
- /**
220
- * Registers a cleanup function or subscription for automatic disposal.
221
- *
222
- * All teardowns are automatically cleaned up when the history instance is disposed.
223
- * Use `executeOnRemove: true` if the teardown should run when manually removed.
224
- *
225
- * @param teardown - Function or subscription to clean up
226
- * @param options - Optional configuration
227
- * @param options.executeOnRemove - Execute teardown when removed (default: false)
228
- * @returns Function to manually remove the teardown
229
- */
230
- protected _addTeardown(
231
- teardown: VoidFunction | Subscription,
232
- options?: { executeOnRemove: boolean },
233
- ): VoidFunction {
234
- this.#teardowns.add(teardown);
235
- return () => {
236
- // Execute teardown callbacks immediately when callers explicitly request removal side effects.
237
- if (options?.executeOnRemove) {
238
- typeof teardown === 'function' ? teardown() : teardown.unsubscribe();
239
- }
240
- return this._removeTeardown(teardown);
241
- };
242
- }
243
-
244
- /**
245
- * Removes a teardown from the cleanup collection.
246
- *
247
- * Typically called automatically by the function returned from `_addTeardown`.
248
- * Only call directly if you need to remove a teardown without executing it.
249
- *
250
- * @param teardown - The teardown to remove
251
- */
252
- protected _removeTeardown(teardown: VoidFunction | Subscription) {
253
- this.#teardowns.remove(teardown);
254
- }
255
- }
@@ -1,124 +0,0 @@
1
- import { fromEvent } from 'rxjs';
2
- import { map } from 'rxjs/operators';
3
- import { BaseHistory } from './BaseHistory';
4
- import { BrowserHistoryStack } from './BrowserHistoryStack';
5
- import { BrowserHistoryHashStack } from './BrowserHistoryHashStack';
6
- import { resolveWindowLocation } from './utils';
7
- import { Action, type HistoryStack, type NavigationBlocker } from './types';
8
- import { createStore, createHistoryReducer, actions } from './state';
9
-
10
- /**
11
- * Handler for beforeunload events.
12
- * Prevents page unload when navigation blockers are active.
13
- */
14
- const onBeforeUnload = (event: BeforeUnloadEvent) => {
15
- event.preventDefault();
16
- event.returnValue = '';
17
- };
18
- /**
19
- * Constructor for a HistoryStack implementation.
20
- *
21
- * @param window - The window object to use for history operations
22
- * @returns A new instance of the HistoryStack implementation
23
- */
24
- export interface StackConstructor {
25
- new (window: Window): HistoryStack;
26
- }
27
-
28
- /**
29
- * Options for configuring a BrowserHistory instance.
30
- */
31
- export type BrowserHistoryOptions = {
32
- /** Optional window object (defaults to global window) */
33
- window?: Window;
34
- /** Optional stack constructor (defaults to BrowserHistoryStack, use BrowserHistoryHashStack for hash routing) */
35
- stack?: StackConstructor;
36
- };
37
-
38
- /**
39
- * Browser history implementation using native browser APIs.
40
- *
41
- * Uses the browser's History API (pushState/replaceState) for navigation.
42
- * Automatically listens for popstate/hashchange events to detect browser back/forward navigation.
43
- * Compatible with industry-standard routers (Remix/React Router).
44
- */
45
- export class BrowserHistory extends BaseHistory {
46
- #window: Window;
47
- /**
48
- * Creates a browser history instance.
49
- *
50
- * Initializes with the current window location and sets up listeners for
51
- * browser navigation events (popstate for regular routing, hashchange for hash routing).
52
- *
53
- * @param options - Configuration options
54
- * @param options.window - Window object to use (defaults to global window)
55
- * @param options.stack - Stack implementation (defaults to BrowserHistoryStack, use BrowserHistoryHashStack for hash routing)
56
- * @throws {Error} If window is not available
57
- */
58
- constructor(options: BrowserHistoryOptions = {}) {
59
- // Use provided stack or default to BrowserHistoryStack
60
- const Stack: StackConstructor = options.stack ?? BrowserHistoryStack;
61
- const browserWindow = options.window ?? document.defaultView;
62
- // Fail fast without a window since the browser stack depends on the DOM history API.
63
- if (!browserWindow) {
64
- throw new Error('Window is required');
65
- }
66
-
67
- // Initialize state with current window location
68
- const state = createStore(
69
- new Stack(browserWindow),
70
- createHistoryReducer({
71
- delta: 0,
72
- action: Action.Pop,
73
- location: resolveWindowLocation(browserWindow, browserWindow.history),
74
- }),
75
- );
76
- super(state);
77
-
78
- this.#window = browserWindow;
79
-
80
- // Determine event type based on stack implementation
81
- // Hash routing uses 'hashchange', regular routing uses 'popstate'
82
- const isHashHistory = state.stack instanceof BrowserHistoryHashStack;
83
- const eventName = isHashHistory ? 'hashchange' : 'popstate';
84
-
85
- // Listen for browser navigation events and dispatch POP actions
86
- // This handles browser back/forward button clicks
87
- this._addTeardown(
88
- fromEvent(browserWindow, eventName)
89
- .pipe(
90
- map(() => {
91
- const location = state.stack.current;
92
- return actions.pop({ delta: 0, action: Action.Pop, location });
93
- }),
94
- )
95
- .subscribe(this._dispatch.bind(this)),
96
- );
97
- }
98
-
99
- /**
100
- * Registers a blocker to intercept navigation attempts.
101
- *
102
- * Adds a beforeunload event listener when blockers are active to prevent
103
- * page unload (e.g., when user tries to close the tab). This provides
104
- * browser-level protection in addition to in-app navigation blocking.
105
- *
106
- * @param blocker - Navigation blocker function to register
107
- * @returns Function to unsubscribe the blocker
108
- */
109
- public override block(blocker: NavigationBlocker): VoidFunction {
110
- const unblock = super.block(blocker);
111
- // Add beforeunload listener when blockers are active
112
- // This shows browser's "Leave site?" dialog on page unload
113
- if (this.hasBlockers) {
114
- window.addEventListener('beforeunload', onBeforeUnload);
115
- }
116
- return () => {
117
- unblock();
118
- // Remove beforeunload listener if no blockers remain
119
- if (this.hasBlockers) {
120
- window.removeEventListener('beforeunload', onBeforeUnload);
121
- }
122
- };
123
- }
124
- }