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.
Files changed (52) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +20 -12
  3. package/data-storage/package.json +2 -1
  4. package/lib/cjs/NavigationStack.js +17 -2
  5. package/lib/cjs/debug.js +12 -0
  6. package/lib/cjs/getLocationFromInternalLocation.js +2 -2
  7. package/lib/cjs/navigationBlockers.js +3 -0
  8. package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +13 -2
  9. package/lib/cjs/scroll-position/ScrollPositionRestoration.js +37 -13
  10. package/lib/cjs/scroll-position/ScrollPositionSaver.js +8 -2
  11. package/lib/cjs/session/Session.js +6 -0
  12. package/lib/cjs/session/navigation/operation/operations.js +4 -4
  13. package/lib/esm/NavigationStack.js +17 -2
  14. package/lib/esm/debug.js +7 -0
  15. package/lib/esm/getLocationFromInternalLocation.js +2 -2
  16. package/lib/esm/navigationBlockers.js +3 -0
  17. package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +13 -2
  18. package/lib/esm/scroll-position/ScrollPositionRestoration.js +37 -13
  19. package/lib/esm/scroll-position/ScrollPositionSaver.js +8 -2
  20. package/lib/esm/session/Session.js +6 -0
  21. package/lib/esm/session/navigation/operation/operations.js +4 -4
  22. package/lib/index.d.ts +10 -9
  23. package/lib/scroll-position/index.d.ts +11 -11
  24. package/package.json +1 -1
  25. package/redux/package.json +2 -1
  26. package/scroll-position/package.json +2 -1
  27. package/src/NavigationStack.js +18 -2
  28. package/src/debug.js +8 -0
  29. package/src/getLocationFromInternalLocation.js +2 -2
  30. package/src/navigationBlockers.js +3 -0
  31. package/src/scroll-position/ScrollPositionAutoSaver.js +19 -2
  32. package/src/scroll-position/ScrollPositionRestoration.js +100 -53
  33. package/src/scroll-position/ScrollPositionSaver.js +21 -1
  34. package/src/session/Session.js +22 -0
  35. package/src/session/navigation/operation/operations.js +4 -4
  36. package/test/NavigationStack.test.js +130 -27
  37. package/test/middlewareTestUtil.js +1 -1
  38. package/test/redux/locationReducer.test.js +1 -1
  39. package/test/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js +5 -5
  40. package/test/redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js +2 -2
  41. package/test/redux/middleware/navigationOperationMiddleware.test.js +2 -2
  42. package/test/scroll-position/ScrollPositionRestoration.test.js +78 -61
  43. package/test/scroll-position/addScrollableContainer.js +5 -2
  44. package/test/scroll-position/{addScrollableContainerWithHyperlink.js → addScrollableContainerWithAnchors.js} +8 -2
  45. package/test/scroll-position/createApp.js +28 -7
  46. package/test/scroll-position/withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration.js +72 -0
  47. package/test/session/InMemorySession.test.js +28 -28
  48. package/test/session/ServerSession.test.js +1 -1
  49. package/test/session/WebBrowserSession.test.js +17 -17
  50. package/types/index.d.ts +10 -9
  51. package/types/scroll-position/index.d.ts +11 -11
  52. 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
- _getScrollPositionForLocation:
62
- _options && _options._getPageScrollPositionForLocation,
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
- _shouldUpdateScrollPositionForLocation:
68
- _options && _options._shouldUpdatePageScrollPositionForLocation,
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
- _shouldUpdateScrollPositionForLocation:
107
- _options && _options._shouldUpdateScrollPositionForLocation,
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
- _getScrollPositionForLocation:
113
- _options && _options._getScrollPositionForLocation,
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
- for (const scrollableContainerKey of Object.keys(
348
- this._scrollableContainers,
349
- )) {
350
- const scrollableContainerEntry =
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
- // This function is only used in tests.
354
- // There seems to be no use of it in real life, hence it's not public API.
355
- // It's only used in tests.
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
- !scrollableContainerEntry._shouldUpdateScrollPositionForLocation(
359
- this._location,
360
- this._prevLocation,
361
- )
394
+ scrollableContainerEntry._shouldSetScrollPositionOnLocationChange
362
395
  ) {
363
- continue;
396
+ if (
397
+ !scrollableContainerEntry._shouldSetScrollPositionOnLocationChange(
398
+ this._location,
399
+ this._prevLocation,
400
+ )
401
+ ) {
402
+ return Promise.resolve();
403
+ }
364
404
  }
365
- }
366
405
 
367
- // Scroll position (or anchor) to set.
368
- let scrollPositionOrAnchorToSet;
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
- // This function is only used in tests.
371
- // There seems to be no use of it in real life, hence it's not public API.
372
- // It's only used in tests.
373
- if (scrollableContainerEntry._getScrollPositionForLocation) {
374
- scrollPositionOrAnchorToSet =
375
- scrollableContainerEntry._getScrollPositionForLocation(
376
- this._location,
377
- this._prevLocation,
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
- // Get scroll position (or anchor) to set.
382
- if (!scrollPositionOrAnchorToSet) {
383
- scrollPositionOrAnchorToSet =
384
- scrollableContainerKey === PAGE_SCROLLABLE_CONTAINER_KEY
385
- ? this._getPageScrollPositionOrAnchorToSet(this._location)
386
- : this._getScrollableContainerScrollPositionToSet(
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
- // Set scroll position of scrollable container.
393
- scrollableContainerEntry.scrollPositionSetter.set(
394
- scrollableContainerEntry.scrollableContainer,
395
- scrollPositionOrAnchorToSet,
396
- this._scrollPosition,
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.
@@ -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);
@@ -1,6 +1,6 @@
1
1
  export default {
2
- INIT: 'INIT',
3
- PUSH: 'PUSH',
4
- REPLACE: 'REPLACE',
5
- SHIFT: 'SHIFT',
2
+ INIT: 'init',
3
+ PUSH: 'push',
4
+ REPLACE: 'replace',
5
+ SHIFT: 'shift',
6
6
  };
@@ -23,19 +23,19 @@ describe('NavigationStack', () => {
23
23
  navigationStack.push('/new');
24
24
  expect(navigationStack.current()).to.include({
25
25
  pathname: '/new',
26
- // index: 1,
26
+ index: 1,
27
27
  });
28
28
 
29
29
  navigationStack.shift(-1);
30
30
  expect(navigationStack.current()).to.include({
31
31
  pathname: '/initial',
32
- // index: 0,
32
+ index: 0,
33
33
  });
34
34
 
35
35
  navigationStack.shift(+1);
36
36
  expect(navigationStack.current()).to.include({
37
37
  pathname: '/new',
38
- // index: 1,
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
- // index: 0,
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
- // index: 1,
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
- // index: 0,
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
- // index: 1,
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
- // index: 0,
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: 'INIT',
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: 'PUSH',
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: 'REPLACE',
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: 'SHIFT',
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: 'INIT',
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
- // index: 1,
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
- navigationStack.locationRendered();
284
+ // "/initial" page rendered.
285
+ // Restore scroll position (no saved scroll position to restore).
286
+ await navigationStack.locationRendered();
267
287
 
268
- navigationStack.push('/new');
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
- navigationStack.locationRendered();
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
- const removeScrollableContainer = navigationStack.addScrollableContainer(
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
- // `PageScrollPositionSetter` works in an asynchronous fashion,
281
- // so this delay lets it finish setting page scroll position
282
- // before proceeding to next location.
283
- await delay(20);
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
- removeScrollableContainer();
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
- navigationStack.locationRendered();
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
- // `PageScrollPositionSetter` works in an asynchronous fashion,
292
- // so this delay lets it finish setting page scroll position
293
- // before proceeding to next location.
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
  });
@@ -17,7 +17,7 @@ export function transformInputLocationUsingMiddleware(middleware, location) {
17
17
  return invokeLocationMiddleware(middleware, {
18
18
  type: ActionTypes.NAVIGATE,
19
19
  payload: {
20
- operation: 'PUSH',
20
+ operation: 'push',
21
21
  location,
22
22
  },
23
23
  }).payload.location;