navigation-stack 0.4.0 → 0.5.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 +12 -0
- package/README.md +20 -12
- package/data-storage/package.json +2 -1
- package/lib/cjs/NavigationStack.js +17 -2
- package/lib/cjs/debug.js +12 -0
- package/lib/cjs/getLocationFromInternalLocation.js +2 -2
- package/lib/cjs/navigationBlockers.js +3 -0
- package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +13 -2
- package/lib/cjs/scroll-position/ScrollPositionRestoration.js +37 -13
- package/lib/cjs/scroll-position/ScrollPositionSaver.js +8 -2
- package/lib/cjs/session/Session.js +6 -0
- package/lib/cjs/session/navigation/operation/operations.js +4 -4
- package/lib/esm/NavigationStack.js +17 -2
- package/lib/esm/debug.js +7 -0
- package/lib/esm/getLocationFromInternalLocation.js +2 -2
- package/lib/esm/navigationBlockers.js +3 -0
- package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +13 -2
- package/lib/esm/scroll-position/ScrollPositionRestoration.js +37 -13
- package/lib/esm/scroll-position/ScrollPositionSaver.js +8 -2
- package/lib/esm/session/Session.js +6 -0
- package/lib/esm/session/navigation/operation/operations.js +4 -4
- package/lib/index.d.ts +10 -9
- package/lib/scroll-position/index.d.ts +11 -11
- package/package.json +1 -1
- package/redux/package.json +2 -1
- package/scroll-position/package.json +2 -1
- package/src/NavigationStack.js +18 -2
- package/src/debug.js +8 -0
- package/src/getLocationFromInternalLocation.js +2 -2
- package/src/navigationBlockers.js +3 -0
- package/src/scroll-position/ScrollPositionAutoSaver.js +19 -2
- package/src/scroll-position/ScrollPositionRestoration.js +100 -53
- package/src/scroll-position/ScrollPositionSaver.js +21 -1
- package/src/session/Session.js +22 -0
- package/src/session/navigation/operation/operations.js +4 -4
- package/test/NavigationStack.test.js +130 -27
- package/test/middlewareTestUtil.js +1 -1
- package/test/redux/locationReducer.test.js +1 -1
- package/test/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js +5 -5
- package/test/redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js +2 -2
- package/test/redux/middleware/navigationOperationMiddleware.test.js +2 -2
- package/test/scroll-position/ScrollPositionRestoration.test.js +78 -61
- package/test/scroll-position/addScrollableContainer.js +5 -2
- package/test/scroll-position/{addScrollableContainerWithHyperlink.js → addScrollableContainerWithAnchors.js} +8 -2
- package/test/scroll-position/createApp.js +28 -7
- package/test/scroll-position/withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration.js +72 -0
- package/test/session/InMemorySession.test.js +28 -28
- package/test/session/ServerSession.test.js +1 -1
- package/test/session/WebBrowserSession.test.js +17 -17
- package/types/index.d.ts +10 -9
- package/types/scroll-position/index.d.ts +11 -11
- package/test/scroll-position/withScrollableContainerAtIndexPage.js +0 -62
|
@@ -5,6 +5,7 @@ import ScrollPositionSaver from './ScrollPositionSaver';
|
|
|
5
5
|
import ScrollPositionSetter from './ScrollPositionSetter';
|
|
6
6
|
import { PAGE_SCROLLABLE_CONTAINER_KEY } from './constants';
|
|
7
7
|
import LocationDataStorage from '../data-storage/LocationDataStorage';
|
|
8
|
+
import debug from '../debug';
|
|
8
9
|
|
|
9
10
|
function areEqualScrollPositions(scrollPosition1, scrollPosition2) {
|
|
10
11
|
let i = 0;
|
|
@@ -58,14 +59,14 @@ export default class ScrollPositionRestoration {
|
|
|
58
59
|
// This function is only used in tests.
|
|
59
60
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
60
61
|
// It's only used in tests.
|
|
61
|
-
|
|
62
|
-
_options && _options.
|
|
62
|
+
_getSavedScrollPositionOnLocationChange:
|
|
63
|
+
_options && _options._getSavedPageScrollPositionOnLocationChange,
|
|
63
64
|
|
|
64
65
|
// This function is only used in tests.
|
|
65
66
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
66
67
|
// It's only used in tests.
|
|
67
|
-
|
|
68
|
-
_options && _options.
|
|
68
|
+
_shouldSetScrollPositionOnLocationChange:
|
|
69
|
+
_options && _options._shouldSetPageScrollPositionOnLocationChange,
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
|
|
@@ -81,12 +82,22 @@ export default class ScrollPositionRestoration {
|
|
|
81
82
|
// this._scrollableContainerKeyCounter++;
|
|
82
83
|
// const scrollableContainerKey = String(this._scrollableContainerKeyCounter);
|
|
83
84
|
|
|
85
|
+
// Validate `scrollableContainerKey`.
|
|
84
86
|
if (scrollableContainerKey === PAGE_SCROLLABLE_CONTAINER_KEY) {
|
|
85
87
|
throw new Error(
|
|
86
88
|
`Scrollable container key "${scrollableContainerKey}" is not allowed`,
|
|
87
89
|
);
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
// Check that it hasn't already been added.
|
|
93
|
+
if (this._scrollableContainers[scrollableContainerKey]) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Scrollable container key "${scrollableContainerKey}" is already added`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
debug('add scrollable container', scrollableContainerKey);
|
|
100
|
+
|
|
90
101
|
// Add scrollable container entry.
|
|
91
102
|
this._scrollableContainers[scrollableContainerKey] = {
|
|
92
103
|
// Scrollable container element.
|
|
@@ -103,14 +114,14 @@ export default class ScrollPositionRestoration {
|
|
|
103
114
|
// This function is only used in tests.
|
|
104
115
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
105
116
|
// It's only used in tests.
|
|
106
|
-
|
|
107
|
-
_options && _options.
|
|
117
|
+
_shouldSetScrollPositionOnLocationChange:
|
|
118
|
+
_options && _options._shouldSetScrollPositionOnLocationChange,
|
|
108
119
|
|
|
109
120
|
// This function is only used in tests.
|
|
110
121
|
// There seems to be no use of it in real life, hence it's not public API.
|
|
111
122
|
// It's only used in tests.
|
|
112
|
-
|
|
113
|
-
_options && _options.
|
|
123
|
+
_getSavedScrollPositionOnLocationChange:
|
|
124
|
+
_options && _options._getSavedScrollPositionOnLocationChange,
|
|
114
125
|
};
|
|
115
126
|
|
|
116
127
|
// Scrollable containers could be added at any time, including page mount.
|
|
@@ -126,11 +137,22 @@ export default class ScrollPositionRestoration {
|
|
|
126
137
|
scrollableContainerKey,
|
|
127
138
|
);
|
|
128
139
|
if (previouslySavedScrollPosition) {
|
|
140
|
+
debug(
|
|
141
|
+
'restore scroll position on add scrollable container',
|
|
142
|
+
this._location.pathname,
|
|
143
|
+
scrollableContainerKey,
|
|
144
|
+
previouslySavedScrollPosition,
|
|
145
|
+
);
|
|
129
146
|
this._scrollPosition.setScrollableContainerScrollPosition(
|
|
130
147
|
scrollableContainer,
|
|
131
148
|
previouslySavedScrollPosition,
|
|
132
149
|
);
|
|
133
150
|
} else {
|
|
151
|
+
debug(
|
|
152
|
+
'save scroll position on add scrollable container',
|
|
153
|
+
this._location.pathname,
|
|
154
|
+
scrollableContainerKey,
|
|
155
|
+
);
|
|
134
156
|
this._scrollPositionSaver.saveScrollableContainerScrollPosition(
|
|
135
157
|
scrollableContainerKey,
|
|
136
158
|
scrollableContainer,
|
|
@@ -146,12 +168,16 @@ export default class ScrollPositionRestoration {
|
|
|
146
168
|
|
|
147
169
|
// Removes the scrollable container.
|
|
148
170
|
return () => {
|
|
171
|
+
debug('remove scrollable container', scrollableContainerKey);
|
|
172
|
+
|
|
149
173
|
this._scrollPositionSaver._scrollPositionAutoSaver.cancelSaveScrollableContainerScrollPosition(
|
|
150
174
|
scrollableContainerKey,
|
|
151
175
|
);
|
|
176
|
+
|
|
152
177
|
this._scrollPositionSaver._scrollPositionAutoSaver.removeScrollableContainerScrollListener(
|
|
153
178
|
scrollableContainerKey,
|
|
154
179
|
);
|
|
180
|
+
|
|
155
181
|
delete this._scrollableContainers[scrollableContainerKey];
|
|
156
182
|
};
|
|
157
183
|
}
|
|
@@ -231,8 +257,10 @@ export default class ScrollPositionRestoration {
|
|
|
231
257
|
//
|
|
232
258
|
_sessionExecutionStatusListener = ({ running }) => {
|
|
233
259
|
if (running) {
|
|
260
|
+
debug('▶ running');
|
|
234
261
|
this._disableAutomaticScrollRestoration();
|
|
235
262
|
} else {
|
|
263
|
+
debug('⏹ not running');
|
|
236
264
|
this._enableAutomaticScrollRestoration();
|
|
237
265
|
|
|
238
266
|
// There might be previous scroll position already saved in the data storage.
|
|
@@ -266,15 +294,22 @@ export default class ScrollPositionRestoration {
|
|
|
266
294
|
//
|
|
267
295
|
// // Save the current scroll position on the current page while it's still rendered.
|
|
268
296
|
// // This saved scroll position could later be restored in case of returing to this page.
|
|
297
|
+
// // Even if the current scroll position is a default one (scrolled to top), it should still
|
|
298
|
+
// // be saved in order to overwrite any potential previously-saved non-default scroll position.
|
|
269
299
|
// this._scrollPositionSaver.saveScrollPosition();
|
|
270
300
|
// };
|
|
271
301
|
|
|
302
|
+
// Should be called whenever a different location has been rendered (i.e. immediately after).
|
|
303
|
+
// Returns a Promise that resolves when finished restoring scroll position.
|
|
304
|
+
// There's no need to await for that Promise. It's just there because it exists.
|
|
272
305
|
locationRendered(location) {
|
|
273
306
|
// Validate that `location` has a `key`.
|
|
274
307
|
if (!location.key) {
|
|
275
308
|
throw new Error('`location` must have a `key`');
|
|
276
309
|
}
|
|
277
310
|
|
|
311
|
+
debug('rendered location', location.pathname);
|
|
312
|
+
|
|
278
313
|
this._prevLocation = this._location;
|
|
279
314
|
this._location = location;
|
|
280
315
|
|
|
@@ -307,7 +342,7 @@ export default class ScrollPositionRestoration {
|
|
|
307
342
|
|
|
308
343
|
// Set the scroll position for the new page:
|
|
309
344
|
// either restore a previously-saved one or set it to a default scroll position.
|
|
310
|
-
this._setScrollPosition();
|
|
345
|
+
return this._setScrollPosition();
|
|
311
346
|
}
|
|
312
347
|
|
|
313
348
|
// Tells if the current scroll position is the default one.
|
|
@@ -343,59 +378,71 @@ export default class ScrollPositionRestoration {
|
|
|
343
378
|
return true;
|
|
344
379
|
}
|
|
345
380
|
|
|
381
|
+
// Restores scroll position.
|
|
382
|
+
// Returns a Promise that resolves when finished setting scroll position.
|
|
383
|
+
// There's no need to await for this Promise. It just exists.
|
|
346
384
|
_setScrollPosition() {
|
|
347
|
-
|
|
348
|
-
this._scrollableContainers
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
this._scrollableContainers[scrollableContainerKey];
|
|
385
|
+
return Promise.all(
|
|
386
|
+
Object.keys(this._scrollableContainers).map((scrollableContainerKey) => {
|
|
387
|
+
const scrollableContainerEntry =
|
|
388
|
+
this._scrollableContainers[scrollableContainerKey];
|
|
352
389
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (scrollableContainerEntry._shouldUpdateScrollPositionForLocation) {
|
|
390
|
+
// This function is only used in tests.
|
|
391
|
+
// There seems to be no use of it in real life, hence it's not public API.
|
|
392
|
+
// It's only used in tests.
|
|
357
393
|
if (
|
|
358
|
-
|
|
359
|
-
this._location,
|
|
360
|
-
this._prevLocation,
|
|
361
|
-
)
|
|
394
|
+
scrollableContainerEntry._shouldSetScrollPositionOnLocationChange
|
|
362
395
|
) {
|
|
363
|
-
|
|
396
|
+
if (
|
|
397
|
+
!scrollableContainerEntry._shouldSetScrollPositionOnLocationChange(
|
|
398
|
+
this._location,
|
|
399
|
+
this._prevLocation,
|
|
400
|
+
)
|
|
401
|
+
) {
|
|
402
|
+
return Promise.resolve();
|
|
403
|
+
}
|
|
364
404
|
}
|
|
365
|
-
}
|
|
366
405
|
|
|
367
|
-
|
|
368
|
-
|
|
406
|
+
// Scroll position (or anchor) to set.
|
|
407
|
+
let scrollPositionOrAnchorToSet;
|
|
408
|
+
|
|
409
|
+
// This function is only used in tests.
|
|
410
|
+
// There seems to be no use of it in real life, hence it's not public API.
|
|
411
|
+
// It's only used in tests.
|
|
412
|
+
if (scrollableContainerEntry._getSavedScrollPositionOnLocationChange) {
|
|
413
|
+
scrollPositionOrAnchorToSet =
|
|
414
|
+
scrollableContainerEntry._getSavedScrollPositionOnLocationChange(
|
|
415
|
+
this._location,
|
|
416
|
+
this._prevLocation,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
369
419
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
420
|
+
// Get scroll position (or anchor) to set.
|
|
421
|
+
if (!scrollPositionOrAnchorToSet) {
|
|
422
|
+
scrollPositionOrAnchorToSet =
|
|
423
|
+
scrollableContainerKey === PAGE_SCROLLABLE_CONTAINER_KEY
|
|
424
|
+
? this._getPageScrollPositionOrAnchorToSet(this._location)
|
|
425
|
+
: this._getScrollableContainerScrollPositionToSet(
|
|
426
|
+
this._location,
|
|
427
|
+
scrollableContainerKey,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
380
430
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
scrollableContainerKey
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
this._location,
|
|
388
|
-
scrollableContainerKey,
|
|
389
|
-
);
|
|
390
|
-
}
|
|
431
|
+
debug(
|
|
432
|
+
'restore scroll position',
|
|
433
|
+
this._location.pathname,
|
|
434
|
+
scrollableContainerKey,
|
|
435
|
+
scrollPositionOrAnchorToSet,
|
|
436
|
+
);
|
|
391
437
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
438
|
+
// Set scroll position of scrollable container.
|
|
439
|
+
return scrollableContainerEntry.scrollPositionSetter.set(
|
|
440
|
+
scrollableContainerEntry.scrollableContainer,
|
|
441
|
+
scrollPositionOrAnchorToSet,
|
|
442
|
+
this._scrollPosition,
|
|
443
|
+
);
|
|
444
|
+
}),
|
|
445
|
+
);
|
|
399
446
|
}
|
|
400
447
|
|
|
401
448
|
// Overrides the default `window.history.scrollRestoration` value.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import ScrollPositionAutoSaver from './ScrollPositionAutoSaver';
|
|
4
4
|
import { PAGE_SCROLLABLE_CONTAINER_KEY } from './constants';
|
|
5
|
+
import debug from '../debug';
|
|
5
6
|
|
|
6
7
|
export default class ScrollPositionSaver {
|
|
7
8
|
constructor({
|
|
@@ -43,6 +44,8 @@ export default class ScrollPositionSaver {
|
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
debug('save scroll position', this._getLocation().pathname);
|
|
48
|
+
|
|
46
49
|
// Get scrollable containers.
|
|
47
50
|
const scrollableContainers = this._getScrollableContainers();
|
|
48
51
|
|
|
@@ -60,12 +63,19 @@ export default class ScrollPositionSaver {
|
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
savePageScrollPosition() {
|
|
66
|
+
debug(
|
|
67
|
+
'save scroll position',
|
|
68
|
+
this._getLocation().pathname,
|
|
69
|
+
PAGE_SCROLLABLE_CONTAINER_KEY,
|
|
70
|
+
this._scrollPosition.getPageScrollPosition(),
|
|
71
|
+
);
|
|
72
|
+
|
|
63
73
|
// * If this is not a scheduled "auto-save" of scroll position
|
|
64
74
|
// and there already exists any scheduled "auto-save" of scroll position,
|
|
65
75
|
// cancel it and save scroll position right now instead.
|
|
66
76
|
// * If this is a scheduled "auto-save" of scroll position,
|
|
67
77
|
// clear the "cancel" function because it's no longer of use.
|
|
68
|
-
this._scrollPositionAutoSaver.cancelSavePageScrollPosition();
|
|
78
|
+
this._scrollPositionAutoSaver.cancelSavePageScrollPosition(true);
|
|
69
79
|
|
|
70
80
|
// Save scroll position.
|
|
71
81
|
this._saveScrollPositionForLocation(
|
|
@@ -79,6 +89,15 @@ export default class ScrollPositionSaver {
|
|
|
79
89
|
scrollableContainerKey,
|
|
80
90
|
scrollableContainer,
|
|
81
91
|
) {
|
|
92
|
+
debug(
|
|
93
|
+
'save scroll position',
|
|
94
|
+
this._getLocation().pathname,
|
|
95
|
+
scrollableContainerKey,
|
|
96
|
+
this._scrollPosition.getScrollableContainerScrollPosition(
|
|
97
|
+
scrollableContainer,
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
|
|
82
101
|
// * If this is not a scheduled "auto-save" of scroll position
|
|
83
102
|
// and there already exists any scheduled "auto-save" of scroll position,
|
|
84
103
|
// cancel it and save scroll position right now instead.
|
|
@@ -86,6 +105,7 @@ export default class ScrollPositionSaver {
|
|
|
86
105
|
// clear the "cancel" function because it's no longer of use.
|
|
87
106
|
this._scrollPositionAutoSaver.cancelSaveScrollableContainerScrollPosition(
|
|
88
107
|
scrollableContainerKey,
|
|
108
|
+
true,
|
|
89
109
|
);
|
|
90
110
|
|
|
91
111
|
// Save scroll position.
|
package/src/session/Session.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import debug from '../debug';
|
|
1
2
|
import parseInputLocation from '../parseInputLocation';
|
|
2
3
|
import createSessionKey from './key/createSessionKey';
|
|
3
4
|
import NavigationOutOfBoundsError from './navigation/error/NavigationOutOfBoundsError';
|
|
@@ -48,6 +49,13 @@ export default class Session {
|
|
|
48
49
|
// but if it was possible, this call would be required. It would also be required
|
|
49
50
|
// by `navigation` to call `session.getNextKey()` function to increment `locationKeyIndex`.
|
|
50
51
|
this._updateTerminalLocationIndex(location);
|
|
52
|
+
|
|
53
|
+
debug(
|
|
54
|
+
'current location',
|
|
55
|
+
location.pathname,
|
|
56
|
+
'index',
|
|
57
|
+
this._currentLocationIndex,
|
|
58
|
+
);
|
|
51
59
|
});
|
|
52
60
|
}
|
|
53
61
|
|
|
@@ -94,6 +102,8 @@ export default class Session {
|
|
|
94
102
|
throw new Error('Already started');
|
|
95
103
|
}
|
|
96
104
|
|
|
105
|
+
debug('▶ start session', initialLocation.pathname);
|
|
106
|
+
|
|
97
107
|
this._started = true;
|
|
98
108
|
|
|
99
109
|
const key = this._getNextLocationKey();
|
|
@@ -117,6 +127,8 @@ export default class Session {
|
|
|
117
127
|
throw Error('Already stopped');
|
|
118
128
|
}
|
|
119
129
|
|
|
130
|
+
debug('⏹ stop session');
|
|
131
|
+
|
|
120
132
|
// Once stopped, it won't be able to be restarted.
|
|
121
133
|
this._stopped = true;
|
|
122
134
|
|
|
@@ -148,6 +160,14 @@ export default class Session {
|
|
|
148
160
|
const key = this._getNextLocationKey();
|
|
149
161
|
const index = this._currentLocationIndex + delta;
|
|
150
162
|
|
|
163
|
+
debug(
|
|
164
|
+
operation === NavigationOperations.PUSH ? '↓' : '⇅',
|
|
165
|
+
operation,
|
|
166
|
+
location.pathname,
|
|
167
|
+
'index',
|
|
168
|
+
index,
|
|
169
|
+
);
|
|
170
|
+
|
|
151
171
|
// Navigate to the location.
|
|
152
172
|
const locationResult = this._navigation.navigate(location, {
|
|
153
173
|
operation,
|
|
@@ -173,6 +193,8 @@ export default class Session {
|
|
|
173
193
|
|
|
174
194
|
const index = this._currentLocationIndex + delta;
|
|
175
195
|
|
|
196
|
+
debug(delta > 0 ? '→' : '←', 'shift', delta, 'index', index);
|
|
197
|
+
|
|
176
198
|
// Validate that the new `index` is not out of bounds.
|
|
177
199
|
if (index < 0 || index > this._terminalLocationIndex) {
|
|
178
200
|
throw new NavigationOutOfBoundsError(index);
|
|
@@ -23,19 +23,19 @@ describe('NavigationStack', () => {
|
|
|
23
23
|
navigationStack.push('/new');
|
|
24
24
|
expect(navigationStack.current()).to.include({
|
|
25
25
|
pathname: '/new',
|
|
26
|
-
|
|
26
|
+
index: 1,
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
navigationStack.shift(-1);
|
|
30
30
|
expect(navigationStack.current()).to.include({
|
|
31
31
|
pathname: '/initial',
|
|
32
|
-
|
|
32
|
+
index: 0,
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
navigationStack.shift(+1);
|
|
36
36
|
expect(navigationStack.current()).to.include({
|
|
37
37
|
pathname: '/new',
|
|
38
|
-
|
|
38
|
+
index: 1,
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
41
|
|
|
@@ -43,7 +43,7 @@ describe('NavigationStack', () => {
|
|
|
43
43
|
navigationStack.replace('/new');
|
|
44
44
|
expect(navigationStack.current()).to.include({
|
|
45
45
|
pathname: '/new',
|
|
46
|
-
|
|
46
|
+
index: 0,
|
|
47
47
|
});
|
|
48
48
|
});
|
|
49
49
|
});
|
|
@@ -71,21 +71,21 @@ describe('NavigationStack (WebBrowserSession)', () => {
|
|
|
71
71
|
await delay(20);
|
|
72
72
|
expect(navigationStack.current()).to.include({
|
|
73
73
|
pathname: '/new',
|
|
74
|
-
|
|
74
|
+
index: 1,
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
navigationStack.shift(-1);
|
|
78
78
|
await delay(20);
|
|
79
79
|
expect(navigationStack.current()).to.include({
|
|
80
80
|
pathname: '/initial',
|
|
81
|
-
|
|
81
|
+
index: 0,
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
navigationStack.shift(+1);
|
|
85
85
|
await delay(20);
|
|
86
86
|
expect(navigationStack.current()).to.include({
|
|
87
87
|
pathname: '/new',
|
|
88
|
-
|
|
88
|
+
index: 1,
|
|
89
89
|
});
|
|
90
90
|
});
|
|
91
91
|
|
|
@@ -94,7 +94,7 @@ describe('NavigationStack (WebBrowserSession)', () => {
|
|
|
94
94
|
await delay(20);
|
|
95
95
|
expect(navigationStack.current()).to.include({
|
|
96
96
|
pathname: '/new',
|
|
97
|
-
|
|
97
|
+
index: 0,
|
|
98
98
|
});
|
|
99
99
|
});
|
|
100
100
|
});
|
|
@@ -121,7 +121,7 @@ describe('NavigationStack.subscribe', () => {
|
|
|
121
121
|
// `.init()` calls subscription listeners.
|
|
122
122
|
expect(listener).to.have.been.calledOnce();
|
|
123
123
|
expect(listener.lastCall.args[0]).to.include({
|
|
124
|
-
// operation: '
|
|
124
|
+
// operation: 'init',
|
|
125
125
|
pathname: '/initial',
|
|
126
126
|
});
|
|
127
127
|
listener.resetHistory();
|
|
@@ -130,7 +130,7 @@ describe('NavigationStack.subscribe', () => {
|
|
|
130
130
|
|
|
131
131
|
expect(listener).to.have.been.calledOnce();
|
|
132
132
|
expect(listener.lastCall.args[0]).to.include({
|
|
133
|
-
// operation: '
|
|
133
|
+
// operation: 'push',
|
|
134
134
|
pathname: '/new',
|
|
135
135
|
});
|
|
136
136
|
listener.resetHistory();
|
|
@@ -139,7 +139,7 @@ describe('NavigationStack.subscribe', () => {
|
|
|
139
139
|
|
|
140
140
|
expect(listener).to.have.been.calledOnce();
|
|
141
141
|
expect(listener.lastCall.args[0]).to.include({
|
|
142
|
-
// operation: '
|
|
142
|
+
// operation: 'replace',
|
|
143
143
|
pathname: '/new-2',
|
|
144
144
|
});
|
|
145
145
|
listener.resetHistory();
|
|
@@ -148,7 +148,7 @@ describe('NavigationStack.subscribe', () => {
|
|
|
148
148
|
|
|
149
149
|
expect(listener).to.have.been.calledOnce();
|
|
150
150
|
expect(listener.lastCall.args[0]).to.include({
|
|
151
|
-
// operation: '
|
|
151
|
+
// operation: 'shift',
|
|
152
152
|
// delta: -1,
|
|
153
153
|
pathname: '/initial',
|
|
154
154
|
});
|
|
@@ -176,7 +176,7 @@ describe('NavigationStack.subscribe', () => {
|
|
|
176
176
|
// `.init()` calls subscription listeners.
|
|
177
177
|
expect(listener).to.have.been.calledOnce();
|
|
178
178
|
expect(listener.lastCall.args[0]).to.include({
|
|
179
|
-
// operation: '
|
|
179
|
+
// operation: 'init',
|
|
180
180
|
pathname: '/initial',
|
|
181
181
|
});
|
|
182
182
|
listener.resetHistory();
|
|
@@ -252,45 +252,148 @@ describe('NavigationStack', () => {
|
|
|
252
252
|
|
|
253
253
|
expect(navigationStack.current()).to.include({
|
|
254
254
|
pathname: '/new',
|
|
255
|
-
|
|
255
|
+
index: 1,
|
|
256
256
|
});
|
|
257
257
|
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('NavigationStack (maintainScrollPosition: true)', () => {
|
|
261
|
+
let navigationStack;
|
|
262
|
+
|
|
263
|
+
afterEach(() => {
|
|
264
|
+
// Even if a test errors, the `NavigationStack` should still be stopped
|
|
265
|
+
// in order to remove the potential "popstate" listener so that it doesn't
|
|
266
|
+
// interfere with other tests.
|
|
267
|
+
//
|
|
268
|
+
// `navigationStack.stop()` method is "idempotent", i.e. it can be called multiple times.
|
|
269
|
+
//
|
|
270
|
+
navigationStack.stop();
|
|
271
|
+
});
|
|
258
272
|
|
|
259
273
|
it('should support `maintainScrollPosition: true` option', async () => {
|
|
260
274
|
navigationStack = new NavigationStack(new WebBrowserSession(), {
|
|
261
275
|
maintainScrollPosition: true,
|
|
262
276
|
});
|
|
263
277
|
|
|
278
|
+
// Start with the "/initial" page.
|
|
279
|
+
window.history.replaceState(null, null, '/initial');
|
|
280
|
+
|
|
281
|
+
// Initialize `NavigationStack`.
|
|
264
282
|
navigationStack.init();
|
|
265
283
|
|
|
266
|
-
|
|
284
|
+
// "/initial" page rendered.
|
|
285
|
+
// Restore scroll position (no saved scroll position to restore).
|
|
286
|
+
await navigationStack.locationRendered();
|
|
267
287
|
|
|
268
|
-
|
|
288
|
+
// Create a content <div/> that "overflows" the window so that it becomes scrollable.
|
|
289
|
+
const content = document.createElement('div');
|
|
290
|
+
content.style.height = '10000px';
|
|
291
|
+
content.style.width = '10000px';
|
|
292
|
+
document.body.appendChild(content);
|
|
269
293
|
|
|
270
|
-
|
|
294
|
+
// Scroll the page to some position.
|
|
295
|
+
// The scroll position will be saved.
|
|
296
|
+
window.scrollTo(0, 1000);
|
|
297
|
+
|
|
298
|
+
// Check that it has scrolled to that position.
|
|
299
|
+
expect(window.pageYOffset).to.be.closeTo(1000, 0.5);
|
|
300
|
+
|
|
301
|
+
// Wait a bit for `ScrollPositionRestoration` to save the scroll position
|
|
302
|
+
// because it does that "asynchronously", i.e. in an "immediate" timeout
|
|
303
|
+
// as a way of "throttling" scroll events.
|
|
304
|
+
await delay(20);
|
|
305
|
+
|
|
306
|
+
// Go to "/new" page.
|
|
307
|
+
navigationStack.push('/new');
|
|
271
308
|
|
|
309
|
+
// Create a scrollable container to test its scroll position restoration later.
|
|
272
310
|
const scrollableContainer = document.createElement('div');
|
|
311
|
+
scrollableContainer.style.height = '100px';
|
|
312
|
+
scrollableContainer.style.width = '100px';
|
|
313
|
+
// With default `overflow` value, the scrollable container won't become scrollable
|
|
314
|
+
// and will simply stretch vertically according to the child content.
|
|
315
|
+
scrollableContainer.style.overflow = 'auto';
|
|
316
|
+
// "Overflow" the scrollable container with nested content so that it becomes scrollable.
|
|
317
|
+
scrollableContainer.innerHTML = '<div style="height: 10000px"></div>';
|
|
273
318
|
document.body.appendChild(scrollableContainer);
|
|
274
319
|
|
|
275
|
-
|
|
320
|
+
// "/new" page rendered.
|
|
321
|
+
// Restore scroll position (no saved scroll position to restore).
|
|
322
|
+
await navigationStack.locationRendered();
|
|
323
|
+
|
|
324
|
+
// It should've reset page scroll position.
|
|
325
|
+
expect(window.pageYOffset).to.equal(0);
|
|
326
|
+
|
|
327
|
+
// Scroll the page to some position.
|
|
328
|
+
// The scroll position will be saved.
|
|
329
|
+
window.scrollTo(0, 500);
|
|
330
|
+
|
|
331
|
+
// Check that it has scrolled to that position.
|
|
332
|
+
expect(window.pageYOffset).to.be.closeTo(500, 0.5);
|
|
333
|
+
|
|
334
|
+
// Scroll inside the scrollable container to see if the scroll position
|
|
335
|
+
// is restored later when revisiting this page.
|
|
336
|
+
scrollableContainer.scrollTo(0, 1000);
|
|
337
|
+
|
|
338
|
+
// Check that it has scrolled to that position.
|
|
339
|
+
expect(scrollableContainer.scrollTop).to.be.closeTo(1000, 0.5);
|
|
340
|
+
|
|
341
|
+
// Wait a bit for `ScrollPositionRestoration` to save the scroll position
|
|
342
|
+
// because it does that "asynchronously", i.e. in an "immediate" timeout
|
|
343
|
+
// as a way of "throttling" scroll events.
|
|
344
|
+
await delay(20);
|
|
345
|
+
|
|
346
|
+
// Register the scrollable container on the "/new" page.
|
|
347
|
+
const untrackScrollableContainer = navigationStack.addScrollableContainer(
|
|
276
348
|
'container',
|
|
277
349
|
scrollableContainer,
|
|
278
350
|
);
|
|
279
351
|
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
352
|
+
// Go to "/new-2" page to check that it resets the scroll position inside the scrollable container.
|
|
353
|
+
navigationStack.push('/new-2');
|
|
354
|
+
|
|
355
|
+
// "/new-2" page rendered.
|
|
356
|
+
// Restore scroll position (no saved scroll position to restore).
|
|
357
|
+
await navigationStack.locationRendered();
|
|
284
358
|
|
|
285
|
-
|
|
359
|
+
// Check that it has reset the scroll position inside the scrollable container.
|
|
360
|
+
expect(scrollableContainer.scrollTop).to.equal(0);
|
|
286
361
|
|
|
362
|
+
// It should also reset page scroll position on any "push" navigation.
|
|
363
|
+
expect(window.pageYOffset).to.equal(0);
|
|
364
|
+
|
|
365
|
+
// Return to the "/new" page to check if it restores scroll position inside the scrollable container.
|
|
287
366
|
navigationStack.shift(-1);
|
|
288
367
|
|
|
289
|
-
|
|
368
|
+
// Wait for the web browser to emit a "popstate" event from `window.history.go(-1)` navigation.
|
|
369
|
+
await delay(20);
|
|
370
|
+
|
|
371
|
+
// "/new" page rendered.
|
|
372
|
+
// Restore scroll position.
|
|
373
|
+
await navigationStack.locationRendered();
|
|
374
|
+
|
|
375
|
+
// Check that it has restored the scroll position inside the scrollable container.
|
|
376
|
+
expect(scrollableContainer.scrollTop).to.be.closeTo(1000, 0.5);
|
|
377
|
+
|
|
378
|
+
// Check that it has restored page scroll position.
|
|
379
|
+
expect(window.pageYOffset).to.be.closeTo(500, 0.5);
|
|
290
380
|
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
381
|
+
// Return to the "/initial" page to check if it restores scroll position.
|
|
382
|
+
navigationStack.shift(-1);
|
|
383
|
+
|
|
384
|
+
// Wait for the web browser to emit a "popstate" event from `window.history.go(-1)` navigation.
|
|
294
385
|
await delay(20);
|
|
386
|
+
|
|
387
|
+
// The scrollable container is only present at the "/new" or "/new-2" pages so remove it now.
|
|
388
|
+
document.body.removeChild(scrollableContainer);
|
|
389
|
+
// The scrollable container is only present at the "/new" or "/new-2" pages so untrack it now.
|
|
390
|
+
untrackScrollableContainer();
|
|
391
|
+
|
|
392
|
+
// "/initial" page rendered.
|
|
393
|
+
// Restore scroll position.
|
|
394
|
+
await navigationStack.locationRendered();
|
|
395
|
+
|
|
396
|
+
// Check that it has restored page scroll position.
|
|
397
|
+
expect(window.pageYOffset).to.be.closeTo(1000, 0.5);
|
|
295
398
|
});
|
|
296
399
|
});
|