@equinor/fusion-framework-module-navigation 6.0.1 → 7.0.0-next.2

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 (159) hide show
  1. package/CHANGELOG.md +83 -5
  2. package/README.md +694 -0
  3. package/dist/esm/NavigationConfigurator.interface.js +2 -0
  4. package/dist/esm/NavigationConfigurator.interface.js.map +1 -0
  5. package/dist/esm/NavigationConfigurator.js +144 -0
  6. package/dist/esm/NavigationConfigurator.js.map +1 -0
  7. package/dist/esm/NavigationProvider.interface.js +2 -0
  8. package/dist/esm/NavigationProvider.interface.js.map +1 -0
  9. package/dist/esm/NavigationProvider.js +256 -0
  10. package/dist/esm/NavigationProvider.js.map +1 -0
  11. package/dist/esm/enable-navigation.js +52 -0
  12. package/dist/esm/enable-navigation.js.map +1 -0
  13. package/dist/esm/events.js +23 -0
  14. package/dist/esm/events.js.map +1 -0
  15. package/dist/esm/index.js +6 -5
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/lib/BaseHistory.js +189 -0
  18. package/dist/esm/lib/BaseHistory.js.map +1 -0
  19. package/dist/esm/lib/BrowserHistory.js +91 -0
  20. package/dist/esm/lib/BrowserHistory.js.map +1 -0
  21. package/dist/esm/lib/BrowserHistoryHashStack.js +53 -0
  22. package/dist/esm/lib/BrowserHistoryHashStack.js.map +1 -0
  23. package/dist/esm/lib/BrowserHistoryStack.js +77 -0
  24. package/dist/esm/lib/BrowserHistoryStack.js.map +1 -0
  25. package/dist/esm/lib/MemoryHistory.js +47 -0
  26. package/dist/esm/lib/MemoryHistory.js.map +1 -0
  27. package/dist/esm/lib/MemoryStack.js +98 -0
  28. package/dist/esm/lib/MemoryStack.js.map +1 -0
  29. package/dist/esm/lib/create-history.js +48 -0
  30. package/dist/esm/lib/create-history.js.map +1 -0
  31. package/dist/esm/lib/index.js +8 -1
  32. package/dist/esm/lib/index.js.map +1 -1
  33. package/dist/esm/lib/state/history.actions.js +71 -0
  34. package/dist/esm/lib/state/history.actions.js.map +1 -0
  35. package/dist/esm/lib/state/history.flows.js +212 -0
  36. package/dist/esm/lib/state/history.flows.js.map +1 -0
  37. package/dist/esm/lib/state/history.reducer.js +59 -0
  38. package/dist/esm/lib/state/history.reducer.js.map +1 -0
  39. package/dist/esm/lib/state/history.state.js +37 -0
  40. package/dist/esm/lib/state/history.state.js.map +1 -0
  41. package/dist/esm/lib/state/index.js +4 -0
  42. package/dist/esm/lib/state/index.js.map +1 -0
  43. package/dist/esm/lib/types.js +25 -0
  44. package/dist/esm/lib/types.js.map +1 -0
  45. package/dist/esm/lib/utils/encode-trailing-whitespace.js +19 -0
  46. package/dist/esm/lib/utils/encode-trailing-whitespace.js.map +1 -0
  47. package/dist/esm/lib/utils/has-protocol.js +22 -0
  48. package/dist/esm/lib/utils/has-protocol.js.map +1 -0
  49. package/dist/esm/lib/utils/index.js +6 -0
  50. package/dist/esm/lib/utils/index.js.map +1 -0
  51. package/dist/esm/lib/utils/path-to-string.js +23 -0
  52. package/dist/esm/lib/utils/path-to-string.js.map +1 -0
  53. package/dist/esm/lib/utils/path-to-url.js +47 -0
  54. package/dist/esm/lib/utils/path-to-url.js.map +1 -0
  55. package/dist/esm/lib/utils/resolve-browser-location.js +53 -0
  56. package/dist/esm/lib/utils/resolve-browser-location.js.map +1 -0
  57. package/dist/esm/lib/utils/resolve-path.js +21 -0
  58. package/dist/esm/lib/utils/resolve-path.js.map +1 -0
  59. package/dist/esm/module.js +34 -25
  60. package/dist/esm/module.js.map +1 -1
  61. package/dist/esm/version.js +1 -1
  62. package/dist/esm/version.js.map +1 -1
  63. package/dist/tsconfig.tsbuildinfo +1 -1
  64. package/dist/types/NavigationConfigurator.d.ts +60 -0
  65. package/dist/types/NavigationConfigurator.interface.d.ts +17 -0
  66. package/dist/types/NavigationProvider.d.ts +112 -0
  67. package/dist/types/NavigationProvider.interface.d.ts +75 -0
  68. package/dist/types/enable-navigation.d.ts +40 -0
  69. package/dist/types/events.d.ts +48 -0
  70. package/dist/types/index.d.ts +13 -6
  71. package/dist/types/lib/BaseHistory.d.ts +122 -0
  72. package/dist/types/lib/BrowserHistory.d.ts +53 -0
  73. package/dist/types/lib/BrowserHistoryHashStack.d.ts +41 -0
  74. package/dist/types/lib/BrowserHistoryStack.d.ts +51 -0
  75. package/dist/types/lib/MemoryHistory.d.ts +27 -0
  76. package/dist/types/lib/MemoryStack.d.ts +57 -0
  77. package/dist/types/lib/create-history.d.ts +35 -0
  78. package/dist/types/lib/index.d.ts +7 -1
  79. package/dist/types/lib/state/history.actions.d.ts +61 -0
  80. package/dist/types/lib/state/history.flows.d.ts +102 -0
  81. package/dist/types/lib/state/history.reducer.d.ts +85 -0
  82. package/dist/types/lib/state/history.state.d.ts +27 -0
  83. package/dist/types/lib/state/index.d.ts +3 -0
  84. package/dist/types/lib/types.d.ts +145 -0
  85. package/dist/types/lib/utils/encode-trailing-whitespace.d.ts +16 -0
  86. package/dist/types/lib/utils/has-protocol.d.ts +19 -0
  87. package/dist/types/lib/utils/index.d.ts +5 -0
  88. package/dist/types/lib/utils/path-to-string.d.ts +17 -0
  89. package/dist/types/lib/utils/path-to-url.d.ts +32 -0
  90. package/dist/types/lib/utils/resolve-browser-location.d.ts +42 -0
  91. package/dist/types/lib/utils/resolve-path.d.ts +17 -0
  92. package/dist/types/module.d.ts +33 -5
  93. package/dist/types/version.d.ts +1 -1
  94. package/package.json +25 -7
  95. package/src/NavigationConfigurator.interface.ts +18 -0
  96. package/src/NavigationConfigurator.ts +184 -0
  97. package/src/NavigationProvider.interface.ts +86 -0
  98. package/src/NavigationProvider.ts +313 -0
  99. package/src/__tests__/BrowserHistory.test.ts +151 -0
  100. package/src/__tests__/HashHistory.test.ts +121 -0
  101. package/src/__tests__/MemoryHistory.test.ts +185 -0
  102. package/src/__tests__/NavigationProvider.test.ts +60 -0
  103. package/src/__tests__/setup.ts +8 -0
  104. package/src/enable-navigation.ts +59 -0
  105. package/src/events.ts +65 -0
  106. package/src/index.ts +27 -6
  107. package/src/lib/BaseHistory.ts +229 -0
  108. package/src/lib/BrowserHistory.ts +123 -0
  109. package/src/lib/BrowserHistoryHashStack.ts +56 -0
  110. package/src/lib/BrowserHistoryStack.ts +82 -0
  111. package/src/lib/MemoryHistory.ts +66 -0
  112. package/src/lib/MemoryStack.ts +106 -0
  113. package/src/lib/create-history.ts +63 -0
  114. package/src/lib/index.ts +22 -1
  115. package/src/lib/state/history.actions.ts +108 -0
  116. package/src/lib/state/history.flows.ts +279 -0
  117. package/src/lib/state/history.reducer.ts +70 -0
  118. package/src/lib/state/history.state.ts +62 -0
  119. package/src/lib/state/index.ts +3 -0
  120. package/src/lib/types.ts +159 -0
  121. package/src/lib/utils/encode-trailing-whitespace.ts +18 -0
  122. package/src/lib/utils/has-protocol.ts +21 -0
  123. package/src/lib/utils/index.ts +5 -0
  124. package/src/lib/utils/path-to-string.ts +24 -0
  125. package/src/lib/utils/path-to-url.ts +52 -0
  126. package/src/lib/utils/resolve-browser-location.ts +60 -0
  127. package/src/lib/utils/resolve-path.ts +22 -0
  128. package/src/module.ts +53 -37
  129. package/src/version.ts +1 -1
  130. package/tsconfig.json +10 -1
  131. package/vitest.config.ts +14 -0
  132. package/dist/esm/configurator.js +0 -5
  133. package/dist/esm/configurator.js.map +0 -1
  134. package/dist/esm/createHistory.js +0 -18
  135. package/dist/esm/createHistory.js.map +0 -1
  136. package/dist/esm/lib/provider/INavigationProvider.js +0 -2
  137. package/dist/esm/lib/provider/INavigationProvider.js.map +0 -1
  138. package/dist/esm/lib/provider/NavigationProvider.js +0 -82
  139. package/dist/esm/lib/provider/NavigationProvider.js.map +0 -1
  140. package/dist/esm/lib/provider/index.js +0 -2
  141. package/dist/esm/lib/provider/index.js.map +0 -1
  142. package/dist/esm/navigator.js +0 -158
  143. package/dist/esm/navigator.js.map +0 -1
  144. package/dist/esm/types.js +0 -2
  145. package/dist/esm/types.js.map +0 -1
  146. package/dist/types/configurator.d.ts +0 -9
  147. package/dist/types/createHistory.d.ts +0 -4
  148. package/dist/types/lib/provider/INavigationProvider.d.ts +0 -38
  149. package/dist/types/lib/provider/NavigationProvider.d.ts +0 -22
  150. package/dist/types/lib/provider/index.d.ts +0 -2
  151. package/dist/types/navigator.d.ts +0 -105
  152. package/dist/types/types.d.ts +0 -3
  153. package/src/configurator.ts +0 -11
  154. package/src/createHistory.ts +0 -21
  155. package/src/lib/provider/INavigationProvider.ts +0 -46
  156. package/src/lib/provider/NavigationProvider.ts +0 -117
  157. package/src/lib/provider/index.ts +0 -2
  158. package/src/navigator.ts +0 -204
  159. package/src/types.ts +0 -4
@@ -0,0 +1,185 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { firstValueFrom, skip } from 'rxjs';
3
+ import { MemoryHistory } from '../lib/MemoryHistory';
4
+
5
+ describe('MemoryHistory', () => {
6
+ let history: MemoryHistory;
7
+
8
+ beforeEach(() => {
9
+ history = new MemoryHistory();
10
+ });
11
+
12
+ afterEach(() => {
13
+ history[Symbol.dispose]();
14
+ });
15
+
16
+ describe('initialization', () => {
17
+ it('should initialize with default location', () => {
18
+ expect(history.location.pathname).toBe('/');
19
+ expect(history.action).toBe('POP');
20
+ });
21
+
22
+ it('should initialize with custom initial location', () => {
23
+ const customHistory = new MemoryHistory({
24
+ initialLocation: {
25
+ action: 'POP',
26
+ location: {
27
+ pathname: '/custom',
28
+ search: '',
29
+ hash: '',
30
+ key: 'custom-key',
31
+ state: { test: 'value' },
32
+ },
33
+ },
34
+ });
35
+
36
+ expect(customHistory.location.pathname).toBe('/custom');
37
+ expect(customHistory.location.state).toEqual({ test: 'value' });
38
+ expect(customHistory.action).toBe('POP');
39
+
40
+ customHistory[Symbol.dispose]();
41
+ });
42
+ });
43
+
44
+ describe('in-memory navigation', () => {
45
+ it('should push without affecting browser', async () => {
46
+ const initialPathname = window.location.pathname;
47
+ const initialHash = window.location.hash;
48
+
49
+ const updatePromise = firstValueFrom(history.state$.pipe(skip(1)));
50
+ history.push('/about', { test: 'test' });
51
+ await updatePromise;
52
+
53
+ expect(history.location.pathname).toBe('/about');
54
+ expect(history.action).toBe('PUSH');
55
+ expect(history.location.state).toEqual({ test: 'test' });
56
+ // Browser should not be affected
57
+ expect(window.location.pathname).toBe(initialPathname);
58
+ expect(window.location.hash).toBe(initialHash);
59
+ });
60
+
61
+ it('should replace without affecting browser', async () => {
62
+ const initialPathname = window.location.pathname;
63
+ const initialHash = window.location.hash;
64
+
65
+ const updatePromise = firstValueFrom(history.state$.pipe(skip(1)));
66
+ history.replace('/profile', { userId: 123 });
67
+ await updatePromise;
68
+
69
+ expect(history.location.pathname).toBe('/profile');
70
+ expect(history.action).toBe('REPLACE');
71
+ expect(history.location.state).toEqual({ userId: 123 });
72
+ // Browser should not be affected
73
+ expect(window.location.pathname).toBe(initialPathname);
74
+ expect(window.location.hash).toBe(initialHash);
75
+ });
76
+
77
+ it('should maintain in-memory history stack', async () => {
78
+ // Push multiple entries
79
+ history.push('/page1', { page: 1 });
80
+ await firstValueFrom(history.state$.pipe(skip(1)));
81
+ expect(history.location.pathname).toBe('/page1');
82
+
83
+ history.push('/page2', { page: 2 });
84
+ await firstValueFrom(history.state$.pipe(skip(1)));
85
+ expect(history.location.pathname).toBe('/page2');
86
+
87
+ history.push('/page3', { page: 3 });
88
+ await firstValueFrom(history.state$.pipe(skip(1)));
89
+ expect(history.location.pathname).toBe('/page3');
90
+
91
+ // Go back
92
+ const goBackPromise1 = firstValueFrom(history.state$.pipe(skip(1)));
93
+ history.go(-1);
94
+ await goBackPromise1;
95
+ expect(history.location.pathname).toBe('/page2');
96
+ expect(history.action).toBe('POP');
97
+ expect(history.location.state).toEqual({ page: 2 });
98
+
99
+ // Go back again
100
+ const goBackPromise2 = firstValueFrom(history.state$.pipe(skip(1)));
101
+ history.go(-1);
102
+ await goBackPromise2;
103
+ expect(history.location.pathname).toBe('/page1');
104
+ expect(history.action).toBe('POP');
105
+ expect(history.location.state).toEqual({ page: 1 });
106
+
107
+ // Go forward
108
+ const goForwardPromise = firstValueFrom(history.state$.pipe(skip(1)));
109
+ history.go(1);
110
+ await goForwardPromise;
111
+ expect(history.location.pathname).toBe('/page2');
112
+ expect(history.action).toBe('POP');
113
+ expect(history.location.state).toEqual({ page: 2 });
114
+ });
115
+
116
+ it('should clamp go() to history bounds', async () => {
117
+ history.push('/page1');
118
+ await firstValueFrom(history.state$.pipe(skip(1)));
119
+ expect(history.location.pathname).toBe('/page1');
120
+
121
+ // Try to go back beyond start
122
+ const goBackPromise = firstValueFrom(history.state$.pipe(skip(1)));
123
+ history.go(-10);
124
+ await goBackPromise;
125
+ // Should clamp to first entry
126
+ expect(history.location.pathname).toBe('/');
127
+ expect(history.action).toBe('POP');
128
+
129
+ // Try to go forward beyond end
130
+ const goForwardPromise = firstValueFrom(history.state$.pipe(skip(1)));
131
+ history.go(10);
132
+ await goForwardPromise;
133
+ // Should clamp to last entry
134
+ expect(history.location.pathname).toBe('/page1');
135
+ expect(history.action).toBe('POP');
136
+ });
137
+ });
138
+
139
+ describe('createURL', () => {
140
+ it('should create URLs with memory:// protocol', () => {
141
+ const url = history.createURL('/test');
142
+ expect(url.protocol).toBe('memory:');
143
+ expect(url.pathname).toBe('/test');
144
+ expect(url.href).toContain('memory://');
145
+ });
146
+
147
+ it('should create URLs with search and hash', () => {
148
+ const url = history.createURL('/test?query=1#section');
149
+ expect(url.protocol).toBe('memory:');
150
+ expect(url.pathname).toBe('/test');
151
+ expect(url.search).toBe('?query=1');
152
+ expect(url.hash).toBe('#section');
153
+ expect(url.href).toContain('memory://');
154
+ });
155
+ });
156
+
157
+ describe('state$ observable', () => {
158
+ it('should emit state updates on navigation', async () => {
159
+ const updatePromise = firstValueFrom(history.state$.pipe(skip(1)));
160
+ history.push('/test', { data: 'test' });
161
+ const update = await updatePromise;
162
+
163
+ expect(update.action).toBe('PUSH');
164
+ expect(update.location.pathname).toBe('/test');
165
+ expect(update.location.state).toEqual({ data: 'test' });
166
+ });
167
+
168
+ it('should emit state updates on go()', async () => {
169
+ history.push('/page1');
170
+ await firstValueFrom(history.state$.pipe(skip(1)));
171
+ expect(history.location.pathname).toBe('/page1');
172
+
173
+ history.push('/page2');
174
+ await firstValueFrom(history.state$.pipe(skip(1)));
175
+ expect(history.location.pathname).toBe('/page2');
176
+
177
+ const goPromise = firstValueFrom(history.state$.pipe(skip(1)));
178
+ history.go(-1);
179
+ const update = await goPromise;
180
+
181
+ expect(update.action).toBe('POP');
182
+ expect(update.location.pathname).toBe('/page1');
183
+ });
184
+ });
185
+ });
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { NavigationProvider } from '../NavigationProvider';
3
+ import { MemoryHistory } from '../lib/MemoryHistory';
4
+ import type { INavigationConfigurator } from '../NavigationConfigurator.interface';
5
+ import type { History } from '../lib/types';
6
+
7
+ describe('NavigationProvider', () => {
8
+ let history: History;
9
+ let provider: NavigationProvider;
10
+ let config: INavigationConfigurator;
11
+
12
+ beforeEach(() => {
13
+ history = new MemoryHistory();
14
+ config = { history };
15
+ provider = new NavigationProvider({
16
+ version: '1.0.0',
17
+ config,
18
+ });
19
+ });
20
+
21
+ afterEach(() => {
22
+ provider?.dispose();
23
+ history[Symbol.dispose]();
24
+ });
25
+
26
+ it('should have version', () => {
27
+ expect(provider.version).toBeDefined();
28
+ expect(provider.version.toString()).toBe('1.0.0');
29
+ });
30
+
31
+ it('should have history property', () => {
32
+ expect(provider.history).toBe(history);
33
+ });
34
+
35
+ it('should have path property', () => {
36
+ expect(provider.path).toBeDefined();
37
+ expect(provider.path.pathname).toBeDefined();
38
+ });
39
+
40
+ it('should have state$ observable', () => {
41
+ expect(provider.state$).toBeDefined();
42
+ expect(typeof provider.state$.subscribe).toBe('function');
43
+ });
44
+
45
+ it('should throw error if no history provided', () => {
46
+ config = {};
47
+ expect(() => {
48
+ new NavigationProvider({
49
+ version: '1.0.0',
50
+ config,
51
+ });
52
+ }).toThrow('no history provided!');
53
+ });
54
+
55
+ it('should dispose correctly', () => {
56
+ const disposeSpy = vi.spyOn(history as { [Symbol.dispose]: () => void }, Symbol.dispose);
57
+ provider.dispose();
58
+ expect(disposeSpy).toHaveBeenCalled();
59
+ });
60
+ });
@@ -0,0 +1,8 @@
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
+ });
@@ -0,0 +1,59 @@
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
+ if (typeof basenameOrOptions === 'string') {
51
+ config.setBasename(basenameOrOptions);
52
+ } else if (typeof basenameOrOptions === 'object' && 'configure' in basenameOrOptions) {
53
+ basenameOrOptions.configure(config, ref);
54
+ }
55
+ },
56
+ });
57
+ };
58
+
59
+ export default enableNavigation;
package/src/events.ts ADDED
@@ -0,0 +1,65 @@
1
+ import { FrameworkEvent, type FrameworkEventInit } from '@equinor/fusion-framework-module-event';
2
+
3
+ import type { INavigationProvider } from './NavigationProvider.interface';
4
+ import type { Action, NavigationUpdate, Path } from './lib/types';
5
+
6
+ /**
7
+ * Event detail for navigation events (before navigation).
8
+ */
9
+ export interface NavigateEventDetail {
10
+ /** The target path or location to navigate to */
11
+ location: NavigationUpdate;
12
+ /** Optional current location before navigation */
13
+ currentLocation?: Path;
14
+ }
15
+
16
+ /**
17
+ * Event emitted before navigation occurs.
18
+ * Can be canceled by calling `preventDefault()`.
19
+ */
20
+ export class NavigateEvent extends FrameworkEvent<
21
+ FrameworkEventInit<NavigateEventDetail, INavigationProvider>
22
+ > {
23
+ constructor(detail: NavigateEventDetail, source: INavigationProvider) {
24
+ super('onNavigate', {
25
+ detail,
26
+ source,
27
+ });
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Event detail for navigated events (after navigation).
33
+ */
34
+ export interface NavigatedEventDetail {
35
+ /** The navigation action that occurred (PUSH, POP, REPLACE) */
36
+ action: Action;
37
+ /** The new location after navigation */
38
+ current: NavigationUpdate;
39
+ /** The previous location before navigation */
40
+ previous: NavigationUpdate;
41
+ }
42
+
43
+ /**
44
+ * Event emitted after navigation occurs.
45
+ * Contains the navigation action and location details.
46
+ */
47
+ export class NavigatedEvent extends FrameworkEvent<
48
+ FrameworkEventInit<NavigatedEventDetail, INavigationProvider>
49
+ > {
50
+ constructor(detail: NavigatedEventDetail, source: INavigationProvider) {
51
+ super('onNavigated', { detail, source });
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Declares navigation events in the FrameworkEventMap for type safety.
57
+ */
58
+ declare module '@equinor/fusion-framework-module-event' {
59
+ interface FrameworkEventMap {
60
+ /** Event emitted before navigation occurs, can be canceled to prevent navigation */
61
+ onNavigate: NavigateEvent;
62
+ /** Event emitted after navigation occurs */
63
+ onNavigated: NavigatedEvent;
64
+ }
65
+ }
package/src/index.ts CHANGED
@@ -1,10 +1,31 @@
1
- export type { Action, Path } from '@remix-run/router';
1
+ export type { INavigationConfigurator } from './NavigationConfigurator.interface';
2
+ export { NavigationConfigurator } from './NavigationConfigurator';
2
3
 
3
- export { INavigationConfigurator, NavigationConfigurator } from './configurator';
4
+ export { NavigationModule, module, moduleKey } from './module';
5
+ export { enableNavigation } from './enable-navigation';
4
6
 
5
- export { NavigationModule, enableNavigation, module, moduleKey } from './module';
7
+ export type { INavigationProvider } from './NavigationProvider.interface';
8
+ export { NavigationProvider } from './NavigationProvider';
6
9
 
7
- export { createHistory } from './createHistory';
10
+ export { createHistory } from './lib/create-history';
8
11
 
9
- export * from './lib';
10
- export * from './types';
12
+ export {
13
+ NavigateEvent,
14
+ NavigatedEvent,
15
+ type NavigateEventDetail,
16
+ type NavigatedEventDetail,
17
+ } from './events';
18
+
19
+ export type {
20
+ Path,
21
+ To,
22
+ Location,
23
+ History,
24
+ NavigationBlocker,
25
+ NavigationListener,
26
+ } from './lib/types';
27
+
28
+ /**
29
+ * @deprecated use History instead
30
+ */
31
+ export type { History as INavigator } from './lib';
@@ -0,0 +1,229 @@
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
+ * Provides common state management and navigation logic that can be shared
20
+ * across different history implementations (browser, memory, etc.).
21
+ */
22
+ export abstract class BaseHistory implements History {
23
+ // Subscriptions for cleanup
24
+ #teardowns = new Subscription();
25
+
26
+ // internal state
27
+ #state: HistoryState;
28
+
29
+ /**
30
+ * Gets the current location.
31
+ */
32
+ public get location(): History['location'] {
33
+ return this.#state.subject.value.current.location;
34
+ }
35
+
36
+ /**
37
+ * Gets the current action.
38
+ */
39
+ public get action(): History['action'] {
40
+ return this.#state.subject.value.current.action;
41
+ }
42
+
43
+ /**
44
+ * Observable stream of navigation state changes.
45
+ * Emits on all navigation events (push, replace, pop).
46
+ */
47
+ public get state$(): History['state$'] {
48
+ return this.#state.subject.select((state) => state.current);
49
+ }
50
+
51
+ /**
52
+ * Observable stream of navigation actions.
53
+ */
54
+ public get action$(): History['action$'] {
55
+ return this.#state.subject.action$;
56
+ }
57
+
58
+ /**
59
+ * Checks if there are any active navigation blockers.
60
+ */
61
+ public get hasBlockers(): boolean {
62
+ return this.#state.subject.value.blockers.length > 0;
63
+ }
64
+
65
+ protected constructor(state: HistoryState) {
66
+ this.#state = state;
67
+ }
68
+
69
+ /**
70
+ * Creates a valid href string for a given path.
71
+ */
72
+ public createHref(to: To): string {
73
+ return this.createURL(to).href;
74
+ }
75
+
76
+ /**
77
+ * Creates a URL object for a given path.
78
+ */
79
+ public createURL(to: To): URL {
80
+ return this.#state.stack.createURL(to);
81
+ }
82
+
83
+ /**
84
+ * Encodes a location by properly URL-encoding the pathname.
85
+ */
86
+ public encodeLocation(to: To): Path {
87
+ return this.#state.stack.createURL(to);
88
+ }
89
+
90
+ /**
91
+ * Navigate to a location with explicit options.
92
+ *
93
+ * @param to - The target path (string, Path object, or Location object)
94
+ * @param options - Navigation options specifying action (PUSH/REPLACE) and optional state
95
+ */
96
+ public navigate(to: To, options: NavigateOptions): void {
97
+ const action = this.#state.actions.navigate(to, options);
98
+ this.#state.subject.next(action);
99
+ }
100
+
101
+ /**
102
+ * Pushes a new location onto the history stack.
103
+ */
104
+ public push(to: To, state?: unknown): void {
105
+ this.navigate(to, { state });
106
+ }
107
+
108
+ /**
109
+ * Replaces the current location in the history stack.
110
+ */
111
+ public replace(to: To, state?: unknown): void {
112
+ this.navigate(to, { replace: true, state });
113
+ }
114
+
115
+ /**
116
+ * Navigates backward or forward in the history stack.
117
+ *
118
+ * @param delta - The number of steps to move (negative for backward, positive for forward)
119
+ */
120
+ public go(delta: number): void {
121
+ const action = this.#state.actions.go(delta);
122
+ this.#state.subject.next(action);
123
+ }
124
+
125
+ /**
126
+ * Sets up a listener for navigation changes.
127
+ *
128
+ * Only listens for POP actions (browser back/forward navigation). PUSH/REPLACE
129
+ * actions are programmatic and synchronous - the caller already knows about them.
130
+ * POP actions come from browser events and are asynchronous, so we need to listen.
131
+ * This matches industry-standard router behavior (Remix/React Router).
132
+ *
133
+ * @param listener - Function to call on navigation changes
134
+ * @returns Function to unsubscribe the listener
135
+ */
136
+ public listen(listener: NavigationListener): () => void {
137
+ // Filter for POP actions only - PUSH/REPLACE are handled synchronously
138
+ const subscription = this.#state.subject
139
+ .select((state) => state.current)
140
+ .pipe(
141
+ filter((update): update is NavigationUpdate<Action.Pop> => update.action === Action.Pop),
142
+ )
143
+ .subscribe((update) => {
144
+ listener(update);
145
+ });
146
+
147
+ // Register subscription for cleanup on dispose
148
+ this._addTeardown(subscription);
149
+
150
+ // Return unsubscribe function
151
+ return () => {
152
+ subscription.unsubscribe();
153
+ this._removeTeardown(subscription);
154
+ };
155
+ }
156
+
157
+ /**
158
+ * Registers a blocker to intercept navigation attempts.
159
+ *
160
+ * @param blocker - Navigation blocker function to register
161
+ * @returns Function to unsubscribe the blocker
162
+ */
163
+ public block(blocker: NavigationBlocker): VoidFunction {
164
+ this.#state.subject.next(this.#state.actions.addBlocker(blocker));
165
+ const removeBlocker = () => {
166
+ this.#state.subject.next(this.#state.actions.removeBlocker(blocker));
167
+ };
168
+ return this._addTeardown(removeBlocker, { executeOnRemove: true });
169
+ }
170
+
171
+ public pop(): void {
172
+ this.#state.subject.next(this.#state.actions.pop());
173
+ }
174
+
175
+ /**
176
+ * Disposes of the history instance and cleans up all subscriptions.
177
+ */
178
+ public [Symbol.dispose](): void {
179
+ this.#teardowns.unsubscribe();
180
+ }
181
+
182
+ /**
183
+ * Dispatches an action to trigger state updates.
184
+ *
185
+ * Use this to dispatch actions when external events occur (e.g., browser popstate).
186
+ * The action will be processed by flows and update the history state.
187
+ *
188
+ * @param action - The action to dispatch
189
+ */
190
+ protected _dispatch(action: Actions): void {
191
+ this.#state.subject.next(action);
192
+ }
193
+
194
+ /**
195
+ * Registers a cleanup function or subscription for automatic disposal.
196
+ *
197
+ * All teardowns are automatically cleaned up when the history instance is disposed.
198
+ * Use `executeOnRemove: true` if the teardown should run when manually removed.
199
+ *
200
+ * @param teardown - Function or subscription to clean up
201
+ * @param options - Optional configuration
202
+ * @param options.executeOnRemove - Execute teardown when removed (default: false)
203
+ * @returns Function to manually remove the teardown
204
+ */
205
+ protected _addTeardown(
206
+ teardown: VoidFunction | Subscription,
207
+ options?: { executeOnRemove: boolean },
208
+ ): VoidFunction {
209
+ this.#teardowns.add(teardown);
210
+ return () => {
211
+ if (options?.executeOnRemove) {
212
+ typeof teardown === 'function' ? teardown() : teardown.unsubscribe();
213
+ }
214
+ return this._removeTeardown(teardown);
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Removes a teardown from the cleanup collection.
220
+ *
221
+ * Typically called automatically by the function returned from `_addTeardown`.
222
+ * Only call directly if you need to remove a teardown without executing it.
223
+ *
224
+ * @param teardown - The teardown to remove
225
+ */
226
+ protected _removeTeardown(teardown: VoidFunction | Subscription) {
227
+ this.#teardowns.remove(teardown);
228
+ }
229
+ }