@tramvai/module-autoscroll 7.21.0 → 7.26.8
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/README.md +90 -7
- package/lib/components/Autoscroll.d.ts +1 -1
- package/lib/components/Autoscroll.es.js +44 -34
- package/lib/components/Autoscroll.js +45 -35
- package/lib/components/ScrollRestoration.d.ts +2 -0
- package/lib/components/ScrollRestoration.es.js +127 -0
- package/lib/components/ScrollRestoration.js +131 -0
- package/lib/index.d.ts +11 -3
- package/lib/index.es.js +182 -16
- package/lib/index.js +186 -13
- package/lib/tokens.d.ts +17 -1
- package/lib/tokens.es.js +3 -1
- package/lib/tokens.js +4 -0
- package/package.json +12 -7
package/README.md
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# Autoscroll
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Module emulates native browser scroll behavior on SPA-navigations:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- scroll to top of the page if there is no hash in the URL
|
|
6
|
+
- scroll to the element with id that equals to the hash in the URL if it exists
|
|
7
|
+
- restore scroll position on back/forward navigations
|
|
8
|
+
|
|
9
|
+
The behaviour is similar to the [react-router](https://reactrouter.com/api/components/ScrollRestoration)
|
|
6
10
|
|
|
7
11
|
## Installation
|
|
8
12
|
|
|
@@ -12,15 +16,15 @@ First install `@tramvai/module-autoscroll`:
|
|
|
12
16
|
yarn add @tramvai/module-autoscroll
|
|
13
17
|
```
|
|
14
18
|
|
|
15
|
-
And add `
|
|
19
|
+
And add `ScrollRestorationModule` to the modules list:
|
|
16
20
|
|
|
17
21
|
```tsx
|
|
18
22
|
import { createApp } from '@tramvai/core';
|
|
19
|
-
import {
|
|
23
|
+
import { ScrollRestorationModule } from '@tramvai/module-autoscroll';
|
|
20
24
|
|
|
21
25
|
createApp({
|
|
22
26
|
name: 'tincoin',
|
|
23
|
-
modules: [
|
|
27
|
+
modules: [ScrollRestorationModule],
|
|
24
28
|
});
|
|
25
29
|
```
|
|
26
30
|
|
|
@@ -30,6 +34,12 @@ createApp({
|
|
|
30
34
|
|
|
31
35
|
`behavior: smooth` is not supported by every browser (e.g. doesn't work in Safari). In this case you can use polyfill `smoothscroll-polyfill` that you should add to your app.
|
|
32
36
|
|
|
37
|
+
### Scroll restoration
|
|
38
|
+
|
|
39
|
+
Native browser scroll restoration is disabled when autoscroll is enabled (`window.history.scrollRestoration = 'manual'`, depending on the `navigateState.disableAutoscroll` parameter or `AUTOSCROLL_DISABLED_TOKEN` token).
|
|
40
|
+
|
|
41
|
+
Module saves scroll position on every navigation and restores it on back/forward navigations. Scroll position is saved in `sessionStorage` by navigation index (`history.state.index`).
|
|
42
|
+
|
|
33
43
|
## How to
|
|
34
44
|
|
|
35
45
|
### Disable autoscroll for page
|
|
@@ -49,6 +59,53 @@ function Component() {
|
|
|
49
59
|
}
|
|
50
60
|
```
|
|
51
61
|
|
|
62
|
+
### Disable autoscroll for custom conditions
|
|
63
|
+
|
|
64
|
+
By default, autoscroll is disabled, when `navigateState.disableAutoscroll` is `true`.
|
|
65
|
+
|
|
66
|
+
For example, if you want to disable it for all `updateCurrentRoute` calls, you can provide `AUTOSCROLL_DISABLED_TOKEN` token:
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { AUTOSCROLL_DISABLED_TOKEN } from '@tramvai/module-autoscroll';
|
|
70
|
+
import { provide } from '@tramvai/core';
|
|
71
|
+
|
|
72
|
+
const providers = [
|
|
73
|
+
// ...,
|
|
74
|
+
provide({
|
|
75
|
+
provide: AUTOSCROLL_DISABLED_TOKEN,
|
|
76
|
+
useFactory:
|
|
77
|
+
({ router }) =>
|
|
78
|
+
() => {
|
|
79
|
+
// disable autoscroll for navigation with query `?autoscroll_disabled=true`
|
|
80
|
+
if (router.getCurrentUrl().query.autoscroll_disabled === 'true') {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
// return nothing to use default behavior
|
|
84
|
+
},
|
|
85
|
+
deps: {
|
|
86
|
+
router: ROUTER_TOKEN,
|
|
87
|
+
},
|
|
88
|
+
}),
|
|
89
|
+
];
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Use autoscroll with View Transitions
|
|
93
|
+
|
|
94
|
+
To avoid inconsistent animations when using View Transition API (e.g. X and Y axis movements in the same time), recommended to set autoscroll behavior to `instant`:
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import { AUTOSCROLL_BEHAVIOR_MODE_TOKEN } from '@tramvai/module-autoscroll';
|
|
98
|
+
import { provide } from '@tramvai/core';
|
|
99
|
+
|
|
100
|
+
const providers = [
|
|
101
|
+
// ...,
|
|
102
|
+
provide({
|
|
103
|
+
provide: AUTOSCROLL_BEHAVIOR_MODE_TOKEN,
|
|
104
|
+
useValue: (defaultBehavior) => 'instant', // default is 'smooth' for autoscroll in new pages or anchors and 'instant' for scroll restoration
|
|
105
|
+
}),
|
|
106
|
+
];
|
|
107
|
+
```
|
|
108
|
+
|
|
52
109
|
### Scroll behavior change
|
|
53
110
|
|
|
54
111
|
#### Global
|
|
@@ -61,7 +118,7 @@ const providers = [
|
|
|
61
118
|
// ...,
|
|
62
119
|
provide({
|
|
63
120
|
provide: AUTOSCROLL_BEHAVIOR_MODE_TOKEN,
|
|
64
|
-
useValue: 'auto', // default is 'smooth'
|
|
121
|
+
useValue: (defaultBehavior) => 'auto', // default is 'smooth' for autoscroll in new pages or anchors and 'instant' for scroll restoration
|
|
65
122
|
}),
|
|
66
123
|
];
|
|
67
124
|
```
|
|
@@ -81,7 +138,7 @@ function Component() {
|
|
|
81
138
|
}
|
|
82
139
|
```
|
|
83
140
|
|
|
84
|
-
###
|
|
141
|
+
### Scroll top change
|
|
85
142
|
|
|
86
143
|
#### Global
|
|
87
144
|
|
|
@@ -97,3 +154,29 @@ const providers = [
|
|
|
97
154
|
}),
|
|
98
155
|
];
|
|
99
156
|
```
|
|
157
|
+
|
|
158
|
+
#### Local
|
|
159
|
+
|
|
160
|
+
You can also provide a function that will return scroll top value, for example if you save page scroll position in sessionStorage and then want to restore it:
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
import { AUTOSCROLL_SCROLL_TOP_TOKEN } from '@tramvai/module-autoscroll';
|
|
164
|
+
import { provide } from '@tramvai/core';
|
|
165
|
+
|
|
166
|
+
const providers = [
|
|
167
|
+
// ...,
|
|
168
|
+
provide({
|
|
169
|
+
provide: AUTOSCROLL_SCROLL_TOP_TOKEN,
|
|
170
|
+
// if `isRestoredValue` is `true`, it means that `defaultScrollTop` is resolved previous position for auto scroll restoration
|
|
171
|
+
useValue: (defaultScrollTop, isRestoredValue) => {
|
|
172
|
+
try {
|
|
173
|
+
const savedScrollTop = JSON.parse(sessionStorage.get('scrollTop'));
|
|
174
|
+
// for example, if you save scroll position by navigation index
|
|
175
|
+
return savedScrollTop[history.state.index] ?? 0;
|
|
176
|
+
} catch (e) {
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
];
|
|
182
|
+
```
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare
|
|
1
|
+
export declare const Autoscroll: import("react").MemoExoticComponent<() => null>;
|
|
2
2
|
//# sourceMappingURL=Autoscroll.d.ts.map
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { memo, useMemo, useRef } from 'react';
|
|
2
2
|
import { useRoute, useUrl } from '@tramvai/module-router';
|
|
3
3
|
import { optional } from '@tinkoff/dippy';
|
|
4
4
|
import { useDi } from '@tramvai/react';
|
|
5
|
+
import { LOGGER_TOKEN } from '@tramvai/tokens-common';
|
|
6
|
+
import { useIsomorphicLayoutEffect } from '@tinkoff/react-hooks';
|
|
5
7
|
import { AUTOSCROLL_BEHAVIOR_MODE_TOKEN, AUTOSCROLL_SCROLL_TOP_TOKEN } from '../tokens.es.js';
|
|
6
8
|
|
|
7
9
|
const DEFAULT_AUTOSCROLL_BEHAVIOR = 'smooth';
|
|
8
10
|
const DEFAULT_AUTOSCROLL_SCROLL_TOP = 0;
|
|
9
11
|
const scrollToTop = (behavior, top) => {
|
|
10
|
-
//
|
|
12
|
+
// cross-browser `window.scrollTo` parameters
|
|
11
13
|
try {
|
|
12
14
|
window.scrollTo({ top, left: 0, behavior });
|
|
13
15
|
}
|
|
@@ -15,10 +17,6 @@ const scrollToTop = (behavior, top) => {
|
|
|
15
17
|
window.scrollTo(0, top);
|
|
16
18
|
}
|
|
17
19
|
};
|
|
18
|
-
const isAutoScrollEnabled = (route, prevRoute) => {
|
|
19
|
-
const isDisabled = route.navigateState?.disableAutoscroll || prevRoute.navigateState?.disableAutoscroll;
|
|
20
|
-
return !isDisabled;
|
|
21
|
-
};
|
|
22
20
|
const scrollToAnchor = (anchor, behavior) => {
|
|
23
21
|
try {
|
|
24
22
|
document.querySelector(anchor)?.scrollIntoView({
|
|
@@ -30,43 +28,55 @@ const scrollToAnchor = (anchor, behavior) => {
|
|
|
30
28
|
return false;
|
|
31
29
|
}
|
|
32
30
|
};
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
/** Сохраняем предыдущее значение */
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
prevValueRef.current = value;
|
|
38
|
-
}, [value]);
|
|
39
|
-
return prevValueRef.current;
|
|
40
|
-
}
|
|
41
|
-
// Поведение с подскроллом похоже на
|
|
42
|
-
// https://reacttraining.com/react-router/web/guides/scroll-restoration/scroll-to-top
|
|
43
|
-
function Autoscroll() {
|
|
44
|
-
const globalScrollBehavior = useDi(optional(AUTOSCROLL_BEHAVIOR_MODE_TOKEN)) || DEFAULT_AUTOSCROLL_BEHAVIOR;
|
|
45
|
-
const scrollTop = useDi(optional(AUTOSCROLL_SCROLL_TOP_TOKEN)) ?? DEFAULT_AUTOSCROLL_SCROLL_TOP;
|
|
31
|
+
// @reference https://reacttraining.com/react-router/web/guides/scroll-restoration/scroll-to-top
|
|
32
|
+
const Autoscroll = memo(() => {
|
|
46
33
|
const route = useRoute();
|
|
47
|
-
const prevRoute = usePreviousValue(route);
|
|
48
34
|
const url = useUrl();
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
35
|
+
const scrollBehaviorFactory = useDi(optional(AUTOSCROLL_BEHAVIOR_MODE_TOKEN)) ?? DEFAULT_AUTOSCROLL_BEHAVIOR;
|
|
36
|
+
const scrollTopFactory = useDi(optional(AUTOSCROLL_SCROLL_TOP_TOKEN)) ?? DEFAULT_AUTOSCROLL_SCROLL_TOP;
|
|
37
|
+
const logger = useDi(LOGGER_TOKEN);
|
|
38
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
39
|
+
const log = useMemo(() => logger('autoscroll'), []);
|
|
40
|
+
const previousUrl = useRef(url);
|
|
41
|
+
const shouldScroll = useRef(false);
|
|
42
|
+
useIsomorphicLayoutEffect(() => {
|
|
43
|
+
if (url.pathname !== previousUrl.current.pathname || url.hash !== previousUrl.current.hash) {
|
|
44
|
+
shouldScroll.current = true;
|
|
56
45
|
}
|
|
46
|
+
// skip unnecessary checks, if url has not changed and shouldScroll.current set to false - we don't need to scroll
|
|
57
47
|
if (!shouldScroll.current) {
|
|
58
48
|
return;
|
|
59
49
|
}
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
50
|
+
// disable autoscroll for current page if `disableAutoscroll` is set in route history state
|
|
51
|
+
if (route.navigateState?.disableAutoscroll) {
|
|
52
|
+
previousUrl.current = url;
|
|
63
53
|
shouldScroll.current = false;
|
|
54
|
+
log.debug('Skipping autoscroll because "disableAutoscroll" is set in route history state');
|
|
55
|
+
return;
|
|
64
56
|
}
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
previousUrl.current = url;
|
|
58
|
+
shouldScroll.current = false;
|
|
59
|
+
const scrollBehavior = route.navigateState?.autoscrollBehavior ??
|
|
60
|
+
(typeof scrollBehaviorFactory === 'function'
|
|
61
|
+
? scrollBehaviorFactory(DEFAULT_AUTOSCROLL_BEHAVIOR)
|
|
62
|
+
: scrollBehaviorFactory);
|
|
63
|
+
const scrollTop = typeof scrollTopFactory === 'function'
|
|
64
|
+
? scrollTopFactory(DEFAULT_AUTOSCROLL_SCROLL_TOP, false)
|
|
65
|
+
: scrollTopFactory;
|
|
66
|
+
function scroll() {
|
|
67
|
+
if (!url.hash) {
|
|
68
|
+
log.debug(`Scrolling to top ${scrollTop}px with behavior: ${scrollBehavior}`);
|
|
69
|
+
scrollToTop(scrollBehavior, scrollTop);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
log.debug(`Scrolling to anchor ${url.hash} with behavior: ${scrollBehavior}`);
|
|
73
|
+
scrollToAnchor(url.hash, scrollBehavior);
|
|
74
|
+
}
|
|
67
75
|
}
|
|
68
|
-
|
|
76
|
+
scroll();
|
|
77
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
78
|
+
}, [route, url]);
|
|
69
79
|
return null;
|
|
70
|
-
}
|
|
80
|
+
});
|
|
71
81
|
|
|
72
82
|
export { Autoscroll };
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
var react
|
|
5
|
+
var react = require('react');
|
|
6
6
|
var moduleRouter = require('@tramvai/module-router');
|
|
7
7
|
var dippy = require('@tinkoff/dippy');
|
|
8
|
-
var react = require('@tramvai/react');
|
|
8
|
+
var react$1 = require('@tramvai/react');
|
|
9
|
+
var tokensCommon = require('@tramvai/tokens-common');
|
|
10
|
+
var reactHooks = require('@tinkoff/react-hooks');
|
|
9
11
|
var tokens = require('../tokens.js');
|
|
10
12
|
|
|
11
13
|
const DEFAULT_AUTOSCROLL_BEHAVIOR = 'smooth';
|
|
12
14
|
const DEFAULT_AUTOSCROLL_SCROLL_TOP = 0;
|
|
13
15
|
const scrollToTop = (behavior, top) => {
|
|
14
|
-
//
|
|
16
|
+
// cross-browser `window.scrollTo` parameters
|
|
15
17
|
try {
|
|
16
18
|
window.scrollTo({ top, left: 0, behavior });
|
|
17
19
|
}
|
|
@@ -19,10 +21,6 @@ const scrollToTop = (behavior, top) => {
|
|
|
19
21
|
window.scrollTo(0, top);
|
|
20
22
|
}
|
|
21
23
|
};
|
|
22
|
-
const isAutoScrollEnabled = (route, prevRoute) => {
|
|
23
|
-
const isDisabled = route.navigateState?.disableAutoscroll || prevRoute.navigateState?.disableAutoscroll;
|
|
24
|
-
return !isDisabled;
|
|
25
|
-
};
|
|
26
24
|
const scrollToAnchor = (anchor, behavior) => {
|
|
27
25
|
try {
|
|
28
26
|
document.querySelector(anchor)?.scrollIntoView({
|
|
@@ -34,43 +32,55 @@ const scrollToAnchor = (anchor, behavior) => {
|
|
|
34
32
|
return false;
|
|
35
33
|
}
|
|
36
34
|
};
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
/** Сохраняем предыдущее значение */
|
|
40
|
-
react$1.useEffect(() => {
|
|
41
|
-
prevValueRef.current = value;
|
|
42
|
-
}, [value]);
|
|
43
|
-
return prevValueRef.current;
|
|
44
|
-
}
|
|
45
|
-
// Поведение с подскроллом похоже на
|
|
46
|
-
// https://reacttraining.com/react-router/web/guides/scroll-restoration/scroll-to-top
|
|
47
|
-
function Autoscroll() {
|
|
48
|
-
const globalScrollBehavior = react.useDi(dippy.optional(tokens.AUTOSCROLL_BEHAVIOR_MODE_TOKEN)) || DEFAULT_AUTOSCROLL_BEHAVIOR;
|
|
49
|
-
const scrollTop = react.useDi(dippy.optional(tokens.AUTOSCROLL_SCROLL_TOP_TOKEN)) ?? DEFAULT_AUTOSCROLL_SCROLL_TOP;
|
|
35
|
+
// @reference https://reacttraining.com/react-router/web/guides/scroll-restoration/scroll-to-top
|
|
36
|
+
const Autoscroll = react.memo(() => {
|
|
50
37
|
const route = moduleRouter.useRoute();
|
|
51
|
-
const prevRoute = usePreviousValue(route);
|
|
52
38
|
const url = moduleRouter.useUrl();
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
39
|
+
const scrollBehaviorFactory = react$1.useDi(dippy.optional(tokens.AUTOSCROLL_BEHAVIOR_MODE_TOKEN)) ?? DEFAULT_AUTOSCROLL_BEHAVIOR;
|
|
40
|
+
const scrollTopFactory = react$1.useDi(dippy.optional(tokens.AUTOSCROLL_SCROLL_TOP_TOKEN)) ?? DEFAULT_AUTOSCROLL_SCROLL_TOP;
|
|
41
|
+
const logger = react$1.useDi(tokensCommon.LOGGER_TOKEN);
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
const log = react.useMemo(() => logger('autoscroll'), []);
|
|
44
|
+
const previousUrl = react.useRef(url);
|
|
45
|
+
const shouldScroll = react.useRef(false);
|
|
46
|
+
reactHooks.useIsomorphicLayoutEffect(() => {
|
|
47
|
+
if (url.pathname !== previousUrl.current.pathname || url.hash !== previousUrl.current.hash) {
|
|
48
|
+
shouldScroll.current = true;
|
|
60
49
|
}
|
|
50
|
+
// skip unnecessary checks, if url has not changed and shouldScroll.current set to false - we don't need to scroll
|
|
61
51
|
if (!shouldScroll.current) {
|
|
62
52
|
return;
|
|
63
53
|
}
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
54
|
+
// disable autoscroll for current page if `disableAutoscroll` is set in route history state
|
|
55
|
+
if (route.navigateState?.disableAutoscroll) {
|
|
56
|
+
previousUrl.current = url;
|
|
67
57
|
shouldScroll.current = false;
|
|
58
|
+
log.debug('Skipping autoscroll because "disableAutoscroll" is set in route history state');
|
|
59
|
+
return;
|
|
68
60
|
}
|
|
69
|
-
|
|
70
|
-
|
|
61
|
+
previousUrl.current = url;
|
|
62
|
+
shouldScroll.current = false;
|
|
63
|
+
const scrollBehavior = route.navigateState?.autoscrollBehavior ??
|
|
64
|
+
(typeof scrollBehaviorFactory === 'function'
|
|
65
|
+
? scrollBehaviorFactory(DEFAULT_AUTOSCROLL_BEHAVIOR)
|
|
66
|
+
: scrollBehaviorFactory);
|
|
67
|
+
const scrollTop = typeof scrollTopFactory === 'function'
|
|
68
|
+
? scrollTopFactory(DEFAULT_AUTOSCROLL_SCROLL_TOP, false)
|
|
69
|
+
: scrollTopFactory;
|
|
70
|
+
function scroll() {
|
|
71
|
+
if (!url.hash) {
|
|
72
|
+
log.debug(`Scrolling to top ${scrollTop}px with behavior: ${scrollBehavior}`);
|
|
73
|
+
scrollToTop(scrollBehavior, scrollTop);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
log.debug(`Scrolling to anchor ${url.hash} with behavior: ${scrollBehavior}`);
|
|
77
|
+
scrollToAnchor(url.hash, scrollBehavior);
|
|
78
|
+
}
|
|
71
79
|
}
|
|
72
|
-
|
|
80
|
+
scroll();
|
|
81
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
82
|
+
}, [route, url]);
|
|
73
83
|
return null;
|
|
74
|
-
}
|
|
84
|
+
});
|
|
75
85
|
|
|
76
86
|
exports.Autoscroll = Autoscroll;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { memo, useMemo, useRef } from 'react';
|
|
2
|
+
import { useRouter, useRoute, useUrl } from '@tramvai/module-router';
|
|
3
|
+
import { optional } from '@tinkoff/dippy';
|
|
4
|
+
import { useDi } from '@tramvai/react';
|
|
5
|
+
import { LOGGER_TOKEN } from '@tramvai/tokens-common';
|
|
6
|
+
import { useIsomorphicLayoutEffect } from '@tinkoff/react-hooks';
|
|
7
|
+
import { AUTOSCROLL_BEHAVIOR_MODE_TOKEN, AUTOSCROLL_SCROLL_TOP_TOKEN, AUTOSCROLL_DISABLED_TOKEN, AUTOSCROLL_APPPLIED_NAVIGATIONS_TOKEN } from '../tokens.es.js';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_AUTOSCROLL_BEHAVIOR = process.env.__TRAMVAI_REACT_TRANSITIONS === 'true' ? 'instant' : 'smooth';
|
|
10
|
+
const DEFAULT_SCROLL_RESTORATION_BEHAVIOR = 'instant';
|
|
11
|
+
const DEFAULT_AUTOSCROLL_SCROLL_TOP = 0;
|
|
12
|
+
const scrollToTop = (behavior, top) => {
|
|
13
|
+
// cross-browser `window.scrollTo` parameters
|
|
14
|
+
try {
|
|
15
|
+
window.scrollTo({ top, left: 0, behavior });
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
window.scrollTo(0, top);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const scrollToAnchor = (anchor, behavior) => {
|
|
22
|
+
try {
|
|
23
|
+
document.querySelector(anchor)?.scrollIntoView({
|
|
24
|
+
behavior,
|
|
25
|
+
});
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
// @reference https://reacttraining.com/react-router/web/guides/scroll-restoration/scroll-to-top
|
|
33
|
+
const ScrollRestoration = memo(() => {
|
|
34
|
+
const router = useRouter();
|
|
35
|
+
const route = useRoute();
|
|
36
|
+
const url = useUrl();
|
|
37
|
+
const scrollBehaviorFactory = useDi(optional(AUTOSCROLL_BEHAVIOR_MODE_TOKEN));
|
|
38
|
+
const scrollTopFactory = useDi(optional(AUTOSCROLL_SCROLL_TOP_TOKEN));
|
|
39
|
+
const autoscrollDisabledFactory = useDi(optional(AUTOSCROLL_DISABLED_TOKEN));
|
|
40
|
+
const appliedNavigations = useDi(AUTOSCROLL_APPPLIED_NAVIGATIONS_TOKEN);
|
|
41
|
+
const logger = useDi(LOGGER_TOKEN);
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
const log = useMemo(() => logger('autoscroll'), []);
|
|
44
|
+
const previousUrl = useRef(url);
|
|
45
|
+
const shouldScroll = useRef(true);
|
|
46
|
+
// workaround to restore scroll position on page reload instantly
|
|
47
|
+
const initialScrollRestoration = useRef(true);
|
|
48
|
+
// eslint-disable-next-line max-statements
|
|
49
|
+
useIsomorphicLayoutEffect(() => {
|
|
50
|
+
if (url.pathname !== previousUrl.current.pathname ||
|
|
51
|
+
url.hash !== previousUrl.current.hash ||
|
|
52
|
+
url.search !== previousUrl.current.search) {
|
|
53
|
+
shouldScroll.current = true;
|
|
54
|
+
}
|
|
55
|
+
// skip unnecessary checks, if url has not changed and shouldScroll.current set to false - we don't need to scroll
|
|
56
|
+
if (!shouldScroll.current) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const autoscrollDisabled = typeof autoscrollDisabledFactory === 'function'
|
|
60
|
+
? autoscrollDisabledFactory()
|
|
61
|
+
: autoscrollDisabledFactory;
|
|
62
|
+
// disable autoscroll for current page if `disableAutoscroll` is set in route history state
|
|
63
|
+
if (autoscrollDisabled ?? route.navigateState?.disableAutoscroll) {
|
|
64
|
+
previousUrl.current = url;
|
|
65
|
+
shouldScroll.current = false;
|
|
66
|
+
if (typeof autoscrollDisabled === 'boolean') {
|
|
67
|
+
log.debug('Skipping autoscroll because "AUTOSCROLL_DISABLED_TOKEN" is return true');
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
log.debug('Skipping autoscroll because "disableAutoscroll" is set in navigation state');
|
|
71
|
+
}
|
|
72
|
+
log.debug('Enable browser scroll restoration to handle scroll position automatically');
|
|
73
|
+
window.history.scrollRestoration = 'auto';
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
log.debug('Disabling browser scroll restoration to handle scroll position manually');
|
|
77
|
+
window.history.scrollRestoration = 'manual';
|
|
78
|
+
previousUrl.current = url;
|
|
79
|
+
shouldScroll.current = false;
|
|
80
|
+
const navigationIndex = router.history.getCurrentState()?.index;
|
|
81
|
+
const scrollRestoredPosition = appliedNavigations.has(navigationIndex) &&
|
|
82
|
+
// if url was changed with replace: true navigation
|
|
83
|
+
appliedNavigations.get(navigationIndex).href === url.href &&
|
|
84
|
+
appliedNavigations.get(navigationIndex).scrollTop;
|
|
85
|
+
const isRestoredScrollPosition = typeof scrollRestoredPosition === 'number';
|
|
86
|
+
const scrollTopDefault = isRestoredScrollPosition
|
|
87
|
+
? scrollRestoredPosition
|
|
88
|
+
: DEFAULT_AUTOSCROLL_SCROLL_TOP;
|
|
89
|
+
const scrollTopFromFactory = typeof scrollTopFactory === 'function'
|
|
90
|
+
? scrollTopFactory(scrollTopDefault, isRestoredScrollPosition)
|
|
91
|
+
: scrollTopFactory;
|
|
92
|
+
const scrollTop = scrollTopFromFactory ?? scrollTopDefault;
|
|
93
|
+
const scrollBehaviorDefault = initialScrollRestoration.current || isRestoredScrollPosition
|
|
94
|
+
? DEFAULT_SCROLL_RESTORATION_BEHAVIOR
|
|
95
|
+
: DEFAULT_AUTOSCROLL_BEHAVIOR;
|
|
96
|
+
const scrollBehaviorFromFactory = typeof scrollBehaviorFactory === 'function'
|
|
97
|
+
? scrollBehaviorFactory(scrollBehaviorDefault)
|
|
98
|
+
: scrollBehaviorFactory;
|
|
99
|
+
const scrollBehavior = scrollBehaviorFromFactory ?? scrollBehaviorDefault;
|
|
100
|
+
if (isRestoredScrollPosition) {
|
|
101
|
+
if (initialScrollRestoration.current) {
|
|
102
|
+
log.debug(`Restoring scroll position ${scrollRestoredPosition}px with instant behavior due to page reload`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
log.debug(`Restoring scroll position ${scrollRestoredPosition}px for navigation index ${navigationIndex}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (initialScrollRestoration.current) {
|
|
109
|
+
initialScrollRestoration.current = false;
|
|
110
|
+
}
|
|
111
|
+
function scroll() {
|
|
112
|
+
if (!url.hash) {
|
|
113
|
+
log.debug(`Scrolling to top ${scrollTop}px with behavior: ${scrollBehavior}`);
|
|
114
|
+
scrollToTop(scrollBehavior, scrollTop);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
log.debug(`Scrolling to anchor ${url.hash} with behavior: ${scrollBehavior}`);
|
|
118
|
+
scrollToAnchor(url.hash, scrollBehavior);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
scroll();
|
|
122
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
123
|
+
}, [router, route, url]);
|
|
124
|
+
return null;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
export { ScrollRestoration };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var react = require('react');
|
|
6
|
+
var moduleRouter = require('@tramvai/module-router');
|
|
7
|
+
var dippy = require('@tinkoff/dippy');
|
|
8
|
+
var react$1 = require('@tramvai/react');
|
|
9
|
+
var tokensCommon = require('@tramvai/tokens-common');
|
|
10
|
+
var reactHooks = require('@tinkoff/react-hooks');
|
|
11
|
+
var tokens = require('../tokens.js');
|
|
12
|
+
|
|
13
|
+
const DEFAULT_AUTOSCROLL_BEHAVIOR = process.env.__TRAMVAI_REACT_TRANSITIONS === 'true' ? 'instant' : 'smooth';
|
|
14
|
+
const DEFAULT_SCROLL_RESTORATION_BEHAVIOR = 'instant';
|
|
15
|
+
const DEFAULT_AUTOSCROLL_SCROLL_TOP = 0;
|
|
16
|
+
const scrollToTop = (behavior, top) => {
|
|
17
|
+
// cross-browser `window.scrollTo` parameters
|
|
18
|
+
try {
|
|
19
|
+
window.scrollTo({ top, left: 0, behavior });
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
window.scrollTo(0, top);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const scrollToAnchor = (anchor, behavior) => {
|
|
26
|
+
try {
|
|
27
|
+
document.querySelector(anchor)?.scrollIntoView({
|
|
28
|
+
behavior,
|
|
29
|
+
});
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
// @reference https://reacttraining.com/react-router/web/guides/scroll-restoration/scroll-to-top
|
|
37
|
+
const ScrollRestoration = react.memo(() => {
|
|
38
|
+
const router = moduleRouter.useRouter();
|
|
39
|
+
const route = moduleRouter.useRoute();
|
|
40
|
+
const url = moduleRouter.useUrl();
|
|
41
|
+
const scrollBehaviorFactory = react$1.useDi(dippy.optional(tokens.AUTOSCROLL_BEHAVIOR_MODE_TOKEN));
|
|
42
|
+
const scrollTopFactory = react$1.useDi(dippy.optional(tokens.AUTOSCROLL_SCROLL_TOP_TOKEN));
|
|
43
|
+
const autoscrollDisabledFactory = react$1.useDi(dippy.optional(tokens.AUTOSCROLL_DISABLED_TOKEN));
|
|
44
|
+
const appliedNavigations = react$1.useDi(tokens.AUTOSCROLL_APPPLIED_NAVIGATIONS_TOKEN);
|
|
45
|
+
const logger = react$1.useDi(tokensCommon.LOGGER_TOKEN);
|
|
46
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
47
|
+
const log = react.useMemo(() => logger('autoscroll'), []);
|
|
48
|
+
const previousUrl = react.useRef(url);
|
|
49
|
+
const shouldScroll = react.useRef(true);
|
|
50
|
+
// workaround to restore scroll position on page reload instantly
|
|
51
|
+
const initialScrollRestoration = react.useRef(true);
|
|
52
|
+
// eslint-disable-next-line max-statements
|
|
53
|
+
reactHooks.useIsomorphicLayoutEffect(() => {
|
|
54
|
+
if (url.pathname !== previousUrl.current.pathname ||
|
|
55
|
+
url.hash !== previousUrl.current.hash ||
|
|
56
|
+
url.search !== previousUrl.current.search) {
|
|
57
|
+
shouldScroll.current = true;
|
|
58
|
+
}
|
|
59
|
+
// skip unnecessary checks, if url has not changed and shouldScroll.current set to false - we don't need to scroll
|
|
60
|
+
if (!shouldScroll.current) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const autoscrollDisabled = typeof autoscrollDisabledFactory === 'function'
|
|
64
|
+
? autoscrollDisabledFactory()
|
|
65
|
+
: autoscrollDisabledFactory;
|
|
66
|
+
// disable autoscroll for current page if `disableAutoscroll` is set in route history state
|
|
67
|
+
if (autoscrollDisabled ?? route.navigateState?.disableAutoscroll) {
|
|
68
|
+
previousUrl.current = url;
|
|
69
|
+
shouldScroll.current = false;
|
|
70
|
+
if (typeof autoscrollDisabled === 'boolean') {
|
|
71
|
+
log.debug('Skipping autoscroll because "AUTOSCROLL_DISABLED_TOKEN" is return true');
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
log.debug('Skipping autoscroll because "disableAutoscroll" is set in navigation state');
|
|
75
|
+
}
|
|
76
|
+
log.debug('Enable browser scroll restoration to handle scroll position automatically');
|
|
77
|
+
window.history.scrollRestoration = 'auto';
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
log.debug('Disabling browser scroll restoration to handle scroll position manually');
|
|
81
|
+
window.history.scrollRestoration = 'manual';
|
|
82
|
+
previousUrl.current = url;
|
|
83
|
+
shouldScroll.current = false;
|
|
84
|
+
const navigationIndex = router.history.getCurrentState()?.index;
|
|
85
|
+
const scrollRestoredPosition = appliedNavigations.has(navigationIndex) &&
|
|
86
|
+
// if url was changed with replace: true navigation
|
|
87
|
+
appliedNavigations.get(navigationIndex).href === url.href &&
|
|
88
|
+
appliedNavigations.get(navigationIndex).scrollTop;
|
|
89
|
+
const isRestoredScrollPosition = typeof scrollRestoredPosition === 'number';
|
|
90
|
+
const scrollTopDefault = isRestoredScrollPosition
|
|
91
|
+
? scrollRestoredPosition
|
|
92
|
+
: DEFAULT_AUTOSCROLL_SCROLL_TOP;
|
|
93
|
+
const scrollTopFromFactory = typeof scrollTopFactory === 'function'
|
|
94
|
+
? scrollTopFactory(scrollTopDefault, isRestoredScrollPosition)
|
|
95
|
+
: scrollTopFactory;
|
|
96
|
+
const scrollTop = scrollTopFromFactory ?? scrollTopDefault;
|
|
97
|
+
const scrollBehaviorDefault = initialScrollRestoration.current || isRestoredScrollPosition
|
|
98
|
+
? DEFAULT_SCROLL_RESTORATION_BEHAVIOR
|
|
99
|
+
: DEFAULT_AUTOSCROLL_BEHAVIOR;
|
|
100
|
+
const scrollBehaviorFromFactory = typeof scrollBehaviorFactory === 'function'
|
|
101
|
+
? scrollBehaviorFactory(scrollBehaviorDefault)
|
|
102
|
+
: scrollBehaviorFactory;
|
|
103
|
+
const scrollBehavior = scrollBehaviorFromFactory ?? scrollBehaviorDefault;
|
|
104
|
+
if (isRestoredScrollPosition) {
|
|
105
|
+
if (initialScrollRestoration.current) {
|
|
106
|
+
log.debug(`Restoring scroll position ${scrollRestoredPosition}px with instant behavior due to page reload`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
log.debug(`Restoring scroll position ${scrollRestoredPosition}px for navigation index ${navigationIndex}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (initialScrollRestoration.current) {
|
|
113
|
+
initialScrollRestoration.current = false;
|
|
114
|
+
}
|
|
115
|
+
function scroll() {
|
|
116
|
+
if (!url.hash) {
|
|
117
|
+
log.debug(`Scrolling to top ${scrollTop}px with behavior: ${scrollBehavior}`);
|
|
118
|
+
scrollToTop(scrollBehavior, scrollTop);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
log.debug(`Scrolling to anchor ${url.hash} with behavior: ${scrollBehavior}`);
|
|
122
|
+
scrollToAnchor(url.hash, scrollBehavior);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
scroll();
|
|
126
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
127
|
+
}, [router, route, url]);
|
|
128
|
+
return null;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
exports.ScrollRestoration = ScrollRestoration;
|
package/lib/index.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { Autoscroll } from './components/Autoscroll';
|
|
2
|
+
import { ScrollRestoration } from './components/ScrollRestoration';
|
|
2
3
|
export * from './tokens';
|
|
3
|
-
export { Autoscroll };
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
export { Autoscroll, ScrollRestoration };
|
|
5
|
+
/**
|
|
6
|
+
* @deprecated use ScrollRestorationModule instead, which includes autoscroll and compatible with View Transitions
|
|
7
|
+
*/
|
|
8
|
+
export declare const AutoscrollModule: import("@tinkoff/dippy/lib/modules/module.h").ModuleClass & Partial<import("@tinkoff/dippy/lib/modules/module.h").ModuleSecretParameters> & {
|
|
9
|
+
[x: string]: (...args: any[]) => import("@tramvai/core").ModuleType;
|
|
10
|
+
};
|
|
11
|
+
export declare const ScrollRestorationModule: import("@tinkoff/dippy/lib/modules/module.h").ModuleClass & Partial<import("@tinkoff/dippy/lib/modules/module.h").ModuleSecretParameters> & {
|
|
12
|
+
[x: string]: (...args: any[]) => import("@tramvai/core").ModuleType;
|
|
13
|
+
};
|
|
6
14
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.es.js
CHANGED
|
@@ -1,26 +1,192 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import noop from '@tinkoff/utils/function/noop';
|
|
2
|
+
import { createToken, declareModule, provide, optional, commandLineListTokens } from '@tramvai/core';
|
|
3
3
|
import { LAYOUT_OPTIONS } from '@tramvai/tokens-render';
|
|
4
|
+
import { ROUTER_TOKEN } from '@tramvai/module-router';
|
|
5
|
+
import { LOGGER_TOKEN } from '@tramvai/tokens-common';
|
|
4
6
|
import { Autoscroll } from './components/Autoscroll.es.js';
|
|
5
7
|
export { Autoscroll } from './components/Autoscroll.es.js';
|
|
6
|
-
|
|
8
|
+
import { ScrollRestoration } from './components/ScrollRestoration.es.js';
|
|
9
|
+
export { ScrollRestoration } from './components/ScrollRestoration.es.js';
|
|
10
|
+
import { AUTOSCROLL_APPPLIED_NAVIGATIONS_TOKEN } from './tokens.es.js';
|
|
11
|
+
export { AUTOSCROLL_APPPLIED_NAVIGATIONS_TOKEN, AUTOSCROLL_BEHAVIOR_MODE_TOKEN, AUTOSCROLL_DISABLED_TOKEN, AUTOSCROLL_SCROLL_TOP_TOKEN } from './tokens.es.js';
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const APPLIED_NAVIGATIONS_KEY = '_t_autoscroll_applied_navigations';
|
|
14
|
+
const NEW_SCROLL_RESTORATION_IS_USED = createToken('tramvai autoscroll new scroll restoration');
|
|
15
|
+
/**
|
|
16
|
+
* @deprecated use ScrollRestorationModule instead, which includes autoscroll and compatible with View Transitions
|
|
17
|
+
*/
|
|
18
|
+
const AutoscrollModule = declareModule({
|
|
19
|
+
name: 'AutoscrollModule',
|
|
20
|
+
providers: [
|
|
21
|
+
provide({
|
|
22
|
+
provide: LAYOUT_OPTIONS,
|
|
23
|
+
useFactory: ({ newScrollRestoration }) => newScrollRestoration
|
|
24
|
+
? {}
|
|
25
|
+
: {
|
|
16
26
|
components: {
|
|
17
27
|
autoscroll: Autoscroll,
|
|
18
28
|
},
|
|
19
29
|
},
|
|
20
|
-
|
|
30
|
+
multi: true,
|
|
31
|
+
deps: {
|
|
32
|
+
newScrollRestoration: optional(NEW_SCROLL_RESTORATION_IS_USED),
|
|
21
33
|
},
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
}),
|
|
35
|
+
...(typeof window !== 'undefined'
|
|
36
|
+
? [
|
|
37
|
+
provide({
|
|
38
|
+
provide: commandLineListTokens.customerStart,
|
|
39
|
+
useFactory: ({ logger, router, newScrollRestoration }) => {
|
|
40
|
+
const log = logger('autoscroll');
|
|
41
|
+
if (newScrollRestoration) {
|
|
42
|
+
return noop;
|
|
43
|
+
}
|
|
44
|
+
// disable browser scroll restoration, to avoid conflicts with autoscroll
|
|
45
|
+
const disableBrowserScrollRestoration = () => {
|
|
46
|
+
log.debug('Disabling browser scroll restoration, autoscroll will be applied for this history entry');
|
|
47
|
+
window.history.scrollRestoration = 'manual';
|
|
48
|
+
};
|
|
49
|
+
// enable browser scroll restoration, if autoscroll was disabled
|
|
50
|
+
const enableBrowserScrollRestoration = () => {
|
|
51
|
+
log.debug('Enable browser scroll restoration, autoscroll was disabled for this history entry');
|
|
52
|
+
window.history.scrollRestoration = 'auto';
|
|
53
|
+
};
|
|
54
|
+
return () => {
|
|
55
|
+
// for initial page load
|
|
56
|
+
if (!window.history.state?.navigateState?.disableAutoscroll) {
|
|
57
|
+
disableBrowserScrollRestoration();
|
|
58
|
+
}
|
|
59
|
+
// for sequent navigations
|
|
60
|
+
router.registerSyncHook('change', async (navigation) => {
|
|
61
|
+
if (!navigation.navigateState?.disableAutoscroll) {
|
|
62
|
+
disableBrowserScrollRestoration();
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
enableBrowserScrollRestoration();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
deps: {
|
|
71
|
+
logger: LOGGER_TOKEN,
|
|
72
|
+
router: ROUTER_TOKEN,
|
|
73
|
+
newScrollRestoration: optional(NEW_SCROLL_RESTORATION_IS_USED),
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
]
|
|
77
|
+
: []),
|
|
78
|
+
],
|
|
79
|
+
});
|
|
80
|
+
const ScrollRestorationModule = declareModule({
|
|
81
|
+
name: 'ScrollRestorationModule',
|
|
82
|
+
providers: [
|
|
83
|
+
provide({
|
|
84
|
+
provide: NEW_SCROLL_RESTORATION_IS_USED,
|
|
85
|
+
useValue: true,
|
|
86
|
+
}),
|
|
87
|
+
provide({
|
|
88
|
+
provide: LAYOUT_OPTIONS,
|
|
89
|
+
useValue: {
|
|
90
|
+
components: {
|
|
91
|
+
autoscroll: ScrollRestoration,
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
multi: true,
|
|
95
|
+
}),
|
|
96
|
+
provide({
|
|
97
|
+
provide: AUTOSCROLL_APPPLIED_NAVIGATIONS_TOKEN,
|
|
98
|
+
useFactory: () => new Map(),
|
|
99
|
+
}),
|
|
100
|
+
...(typeof window !== 'undefined'
|
|
101
|
+
? [
|
|
102
|
+
provide({
|
|
103
|
+
provide: commandLineListTokens.customerStart,
|
|
104
|
+
useFactory: ({ logger, router, appliedNavigations }) => {
|
|
105
|
+
const log = logger('autoscroll');
|
|
106
|
+
return () => {
|
|
107
|
+
try {
|
|
108
|
+
log.debug('Restoring applied navigations from sessionStorage');
|
|
109
|
+
const valueFromStorage = sessionStorage.getItem(APPLIED_NAVIGATIONS_KEY);
|
|
110
|
+
if (valueFromStorage !== null) {
|
|
111
|
+
const parsedValue = JSON.parse(valueFromStorage);
|
|
112
|
+
Object.entries(parsedValue).forEach(([key, value]) => {
|
|
113
|
+
appliedNavigations.set(Number.parseInt(key, 10), value);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
log.debug('No applied navigations found in sessionStorage');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (error) { }
|
|
121
|
+
window.addEventListener('pagehide', () => {
|
|
122
|
+
// to correct scroll restoration for current page in case of reload,
|
|
123
|
+
// both enable automatic scroll restoration and save scroll position
|
|
124
|
+
window.history.scrollRestoration = 'auto';
|
|
125
|
+
const state = router.history.getCurrentState();
|
|
126
|
+
appliedNavigations.set(state.index, {
|
|
127
|
+
href: router.getCurrentUrl()?.href || window.location.href,
|
|
128
|
+
scrollTop: window.scrollY,
|
|
129
|
+
});
|
|
130
|
+
try {
|
|
131
|
+
log.debug('Saving applied navigations to sessionStorage');
|
|
132
|
+
const valueToSave = {};
|
|
133
|
+
for (const [key, value] of appliedNavigations) {
|
|
134
|
+
valueToSave[key] = value;
|
|
135
|
+
}
|
|
136
|
+
sessionStorage.setItem(APPLIED_NAVIGATIONS_KEY, JSON.stringify(valueToSave));
|
|
137
|
+
}
|
|
138
|
+
catch (error) { }
|
|
139
|
+
});
|
|
140
|
+
// case for browser back/forward buttons and router.go/back/forward navigations,
|
|
141
|
+
// here `history.getCurrentState()` and `router.getCurrentUrl()` will return leaving page values,
|
|
142
|
+
// so we can save scroll position for correct page index
|
|
143
|
+
window.addEventListener('popstate', (event) => {
|
|
144
|
+
const state = router.history.getCurrentState();
|
|
145
|
+
appliedNavigations.set(state.index, {
|
|
146
|
+
href: router.getCurrentUrl()?.href || window.location.href,
|
|
147
|
+
scrollTop: window.scrollY,
|
|
148
|
+
});
|
|
149
|
+
log.debug(`Save scroll position on history change, index: ${state.index}, scrollTop: ${window.scrollY}, url: ${router.getCurrentUrl()?.href || window.location.href}`);
|
|
150
|
+
});
|
|
151
|
+
// case for router.navigate navigation
|
|
152
|
+
router.registerHook('beforeNavigate', async (navigation) => {
|
|
153
|
+
if (!navigation.history) {
|
|
154
|
+
const state = router.history.getCurrentState();
|
|
155
|
+
appliedNavigations.set(state.index, {
|
|
156
|
+
href: navigation.fromUrl?.href || window.location.href,
|
|
157
|
+
scrollTop: window.scrollY,
|
|
158
|
+
});
|
|
159
|
+
log.debug(`Save scroll position on router.navigate, index: ${state.index}, scrollTop: ${window.scrollY}, url: ${navigation.fromUrl?.href || window.location.href}`);
|
|
160
|
+
// browser invalidates forward history on pushState, prune stale entries
|
|
161
|
+
for (const key of appliedNavigations.keys()) {
|
|
162
|
+
if (key > state.index) {
|
|
163
|
+
appliedNavigations.delete(key);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
// case for router.updateCurrentRoute navigation
|
|
169
|
+
router.registerHook('beforeUpdateCurrent', async (navigation) => {
|
|
170
|
+
if (!navigation.history) {
|
|
171
|
+
const state = router.history.getCurrentState();
|
|
172
|
+
appliedNavigations.set(state.index, {
|
|
173
|
+
href: navigation.fromUrl?.href || window.location.href,
|
|
174
|
+
scrollTop: window.scrollY,
|
|
175
|
+
});
|
|
176
|
+
log.debug(`Save scroll position on router.updateCurrentRoute, index: ${state.index}, scrollTop: ${window.scrollY}, url: ${navigation.fromUrl?.href || window.location.href}`);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
},
|
|
181
|
+
deps: {
|
|
182
|
+
logger: LOGGER_TOKEN,
|
|
183
|
+
router: ROUTER_TOKEN,
|
|
184
|
+
appliedNavigations: AUTOSCROLL_APPPLIED_NAVIGATIONS_TOKEN,
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
]
|
|
188
|
+
: []),
|
|
189
|
+
],
|
|
190
|
+
});
|
|
25
191
|
|
|
26
|
-
export { AutoscrollModule };
|
|
192
|
+
export { AutoscrollModule, ScrollRestorationModule };
|
package/lib/index.js
CHANGED
|
@@ -2,30 +2,203 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
var
|
|
5
|
+
var noop = require('@tinkoff/utils/function/noop');
|
|
6
6
|
var core = require('@tramvai/core');
|
|
7
7
|
var tokensRender = require('@tramvai/tokens-render');
|
|
8
|
+
var moduleRouter = require('@tramvai/module-router');
|
|
9
|
+
var tokensCommon = require('@tramvai/tokens-common');
|
|
8
10
|
var Autoscroll = require('./components/Autoscroll.js');
|
|
11
|
+
var ScrollRestoration = require('./components/ScrollRestoration.js');
|
|
9
12
|
var tokens = require('./tokens.js');
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
15
|
+
|
|
16
|
+
var noop__default = /*#__PURE__*/_interopDefaultLegacy(noop);
|
|
17
|
+
|
|
18
|
+
const APPLIED_NAVIGATIONS_KEY = '_t_autoscroll_applied_navigations';
|
|
19
|
+
const NEW_SCROLL_RESTORATION_IS_USED = core.createToken('tramvai autoscroll new scroll restoration');
|
|
20
|
+
/**
|
|
21
|
+
* @deprecated use ScrollRestorationModule instead, which includes autoscroll and compatible with View Transitions
|
|
22
|
+
*/
|
|
23
|
+
const AutoscrollModule = core.declareModule({
|
|
24
|
+
name: 'AutoscrollModule',
|
|
25
|
+
providers: [
|
|
26
|
+
core.provide({
|
|
27
|
+
provide: tokensRender.LAYOUT_OPTIONS,
|
|
28
|
+
useFactory: ({ newScrollRestoration }) => newScrollRestoration
|
|
29
|
+
? {}
|
|
30
|
+
: {
|
|
19
31
|
components: {
|
|
20
32
|
autoscroll: Autoscroll.Autoscroll,
|
|
21
33
|
},
|
|
22
34
|
},
|
|
23
|
-
|
|
35
|
+
multi: true,
|
|
36
|
+
deps: {
|
|
37
|
+
newScrollRestoration: core.optional(NEW_SCROLL_RESTORATION_IS_USED),
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
...(typeof window !== 'undefined'
|
|
41
|
+
? [
|
|
42
|
+
core.provide({
|
|
43
|
+
provide: core.commandLineListTokens.customerStart,
|
|
44
|
+
useFactory: ({ logger, router, newScrollRestoration }) => {
|
|
45
|
+
const log = logger('autoscroll');
|
|
46
|
+
if (newScrollRestoration) {
|
|
47
|
+
return noop__default["default"];
|
|
48
|
+
}
|
|
49
|
+
// disable browser scroll restoration, to avoid conflicts with autoscroll
|
|
50
|
+
const disableBrowserScrollRestoration = () => {
|
|
51
|
+
log.debug('Disabling browser scroll restoration, autoscroll will be applied for this history entry');
|
|
52
|
+
window.history.scrollRestoration = 'manual';
|
|
53
|
+
};
|
|
54
|
+
// enable browser scroll restoration, if autoscroll was disabled
|
|
55
|
+
const enableBrowserScrollRestoration = () => {
|
|
56
|
+
log.debug('Enable browser scroll restoration, autoscroll was disabled for this history entry');
|
|
57
|
+
window.history.scrollRestoration = 'auto';
|
|
58
|
+
};
|
|
59
|
+
return () => {
|
|
60
|
+
// for initial page load
|
|
61
|
+
if (!window.history.state?.navigateState?.disableAutoscroll) {
|
|
62
|
+
disableBrowserScrollRestoration();
|
|
63
|
+
}
|
|
64
|
+
// for sequent navigations
|
|
65
|
+
router.registerSyncHook('change', async (navigation) => {
|
|
66
|
+
if (!navigation.navigateState?.disableAutoscroll) {
|
|
67
|
+
disableBrowserScrollRestoration();
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
enableBrowserScrollRestoration();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
deps: {
|
|
76
|
+
logger: tokensCommon.LOGGER_TOKEN,
|
|
77
|
+
router: moduleRouter.ROUTER_TOKEN,
|
|
78
|
+
newScrollRestoration: core.optional(NEW_SCROLL_RESTORATION_IS_USED),
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
]
|
|
82
|
+
: []),
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
const ScrollRestorationModule = core.declareModule({
|
|
86
|
+
name: 'ScrollRestorationModule',
|
|
87
|
+
providers: [
|
|
88
|
+
core.provide({
|
|
89
|
+
provide: NEW_SCROLL_RESTORATION_IS_USED,
|
|
90
|
+
useValue: true,
|
|
91
|
+
}),
|
|
92
|
+
core.provide({
|
|
93
|
+
provide: tokensRender.LAYOUT_OPTIONS,
|
|
94
|
+
useValue: {
|
|
95
|
+
components: {
|
|
96
|
+
autoscroll: ScrollRestoration.ScrollRestoration,
|
|
97
|
+
},
|
|
24
98
|
},
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
99
|
+
multi: true,
|
|
100
|
+
}),
|
|
101
|
+
core.provide({
|
|
102
|
+
provide: tokens.AUTOSCROLL_APPPLIED_NAVIGATIONS_TOKEN,
|
|
103
|
+
useFactory: () => new Map(),
|
|
104
|
+
}),
|
|
105
|
+
...(typeof window !== 'undefined'
|
|
106
|
+
? [
|
|
107
|
+
core.provide({
|
|
108
|
+
provide: core.commandLineListTokens.customerStart,
|
|
109
|
+
useFactory: ({ logger, router, appliedNavigations }) => {
|
|
110
|
+
const log = logger('autoscroll');
|
|
111
|
+
return () => {
|
|
112
|
+
try {
|
|
113
|
+
log.debug('Restoring applied navigations from sessionStorage');
|
|
114
|
+
const valueFromStorage = sessionStorage.getItem(APPLIED_NAVIGATIONS_KEY);
|
|
115
|
+
if (valueFromStorage !== null) {
|
|
116
|
+
const parsedValue = JSON.parse(valueFromStorage);
|
|
117
|
+
Object.entries(parsedValue).forEach(([key, value]) => {
|
|
118
|
+
appliedNavigations.set(Number.parseInt(key, 10), value);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
log.debug('No applied navigations found in sessionStorage');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (error) { }
|
|
126
|
+
window.addEventListener('pagehide', () => {
|
|
127
|
+
// to correct scroll restoration for current page in case of reload,
|
|
128
|
+
// both enable automatic scroll restoration and save scroll position
|
|
129
|
+
window.history.scrollRestoration = 'auto';
|
|
130
|
+
const state = router.history.getCurrentState();
|
|
131
|
+
appliedNavigations.set(state.index, {
|
|
132
|
+
href: router.getCurrentUrl()?.href || window.location.href,
|
|
133
|
+
scrollTop: window.scrollY,
|
|
134
|
+
});
|
|
135
|
+
try {
|
|
136
|
+
log.debug('Saving applied navigations to sessionStorage');
|
|
137
|
+
const valueToSave = {};
|
|
138
|
+
for (const [key, value] of appliedNavigations) {
|
|
139
|
+
valueToSave[key] = value;
|
|
140
|
+
}
|
|
141
|
+
sessionStorage.setItem(APPLIED_NAVIGATIONS_KEY, JSON.stringify(valueToSave));
|
|
142
|
+
}
|
|
143
|
+
catch (error) { }
|
|
144
|
+
});
|
|
145
|
+
// case for browser back/forward buttons and router.go/back/forward navigations,
|
|
146
|
+
// here `history.getCurrentState()` and `router.getCurrentUrl()` will return leaving page values,
|
|
147
|
+
// so we can save scroll position for correct page index
|
|
148
|
+
window.addEventListener('popstate', (event) => {
|
|
149
|
+
const state = router.history.getCurrentState();
|
|
150
|
+
appliedNavigations.set(state.index, {
|
|
151
|
+
href: router.getCurrentUrl()?.href || window.location.href,
|
|
152
|
+
scrollTop: window.scrollY,
|
|
153
|
+
});
|
|
154
|
+
log.debug(`Save scroll position on history change, index: ${state.index}, scrollTop: ${window.scrollY}, url: ${router.getCurrentUrl()?.href || window.location.href}`);
|
|
155
|
+
});
|
|
156
|
+
// case for router.navigate navigation
|
|
157
|
+
router.registerHook('beforeNavigate', async (navigation) => {
|
|
158
|
+
if (!navigation.history) {
|
|
159
|
+
const state = router.history.getCurrentState();
|
|
160
|
+
appliedNavigations.set(state.index, {
|
|
161
|
+
href: navigation.fromUrl?.href || window.location.href,
|
|
162
|
+
scrollTop: window.scrollY,
|
|
163
|
+
});
|
|
164
|
+
log.debug(`Save scroll position on router.navigate, index: ${state.index}, scrollTop: ${window.scrollY}, url: ${navigation.fromUrl?.href || window.location.href}`);
|
|
165
|
+
// browser invalidates forward history on pushState, prune stale entries
|
|
166
|
+
for (const key of appliedNavigations.keys()) {
|
|
167
|
+
if (key > state.index) {
|
|
168
|
+
appliedNavigations.delete(key);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
// case for router.updateCurrentRoute navigation
|
|
174
|
+
router.registerHook('beforeUpdateCurrent', async (navigation) => {
|
|
175
|
+
if (!navigation.history) {
|
|
176
|
+
const state = router.history.getCurrentState();
|
|
177
|
+
appliedNavigations.set(state.index, {
|
|
178
|
+
href: navigation.fromUrl?.href || window.location.href,
|
|
179
|
+
scrollTop: window.scrollY,
|
|
180
|
+
});
|
|
181
|
+
log.debug(`Save scroll position on router.updateCurrentRoute, index: ${state.index}, scrollTop: ${window.scrollY}, url: ${navigation.fromUrl?.href || window.location.href}`);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
deps: {
|
|
187
|
+
logger: tokensCommon.LOGGER_TOKEN,
|
|
188
|
+
router: moduleRouter.ROUTER_TOKEN,
|
|
189
|
+
appliedNavigations: tokens.AUTOSCROLL_APPPLIED_NAVIGATIONS_TOKEN,
|
|
190
|
+
},
|
|
191
|
+
}),
|
|
192
|
+
]
|
|
193
|
+
: []),
|
|
194
|
+
],
|
|
195
|
+
});
|
|
28
196
|
|
|
29
197
|
exports.Autoscroll = Autoscroll.Autoscroll;
|
|
198
|
+
exports.ScrollRestoration = ScrollRestoration.ScrollRestoration;
|
|
199
|
+
exports.AUTOSCROLL_APPPLIED_NAVIGATIONS_TOKEN = tokens.AUTOSCROLL_APPPLIED_NAVIGATIONS_TOKEN;
|
|
30
200
|
exports.AUTOSCROLL_BEHAVIOR_MODE_TOKEN = tokens.AUTOSCROLL_BEHAVIOR_MODE_TOKEN;
|
|
201
|
+
exports.AUTOSCROLL_DISABLED_TOKEN = tokens.AUTOSCROLL_DISABLED_TOKEN;
|
|
31
202
|
exports.AUTOSCROLL_SCROLL_TOP_TOKEN = tokens.AUTOSCROLL_SCROLL_TOP_TOKEN;
|
|
203
|
+
exports.AutoscrollModule = AutoscrollModule;
|
|
204
|
+
exports.ScrollRestorationModule = ScrollRestorationModule;
|
package/lib/tokens.d.ts
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
|
+
export type AutoscrollBehavior = 'smooth' | 'instant' | 'auto';
|
|
1
2
|
export declare const AUTOSCROLL_BEHAVIOR_MODE_TOKEN: ("smooth" & {
|
|
2
3
|
__type?: "base token" | undefined;
|
|
4
|
+
}) | ("instant" & {
|
|
5
|
+
__type?: "base token" | undefined;
|
|
3
6
|
}) | ("auto" & {
|
|
4
7
|
__type?: "base token" | undefined;
|
|
8
|
+
}) | (((defaultBehavior: AutoscrollBehavior) => AutoscrollBehavior) & {
|
|
9
|
+
__type?: "base token" | undefined;
|
|
5
10
|
});
|
|
6
|
-
export declare const AUTOSCROLL_SCROLL_TOP_TOKEN: number & {
|
|
11
|
+
export declare const AUTOSCROLL_SCROLL_TOP_TOKEN: (number & {
|
|
12
|
+
__type?: "base token" | undefined;
|
|
13
|
+
}) | (((defaultScrollTop: number, isRestoredValue: boolean) => number) & {
|
|
14
|
+
__type?: "base token" | undefined;
|
|
15
|
+
});
|
|
16
|
+
export declare const AUTOSCROLL_DISABLED_TOKEN: (() => boolean | undefined) & {
|
|
17
|
+
__type?: "base token" | undefined;
|
|
18
|
+
};
|
|
19
|
+
export declare const AUTOSCROLL_APPPLIED_NAVIGATIONS_TOKEN: Map<number, {
|
|
20
|
+
href: string;
|
|
21
|
+
scrollTop: number;
|
|
22
|
+
}> & {
|
|
7
23
|
__type?: "base token" | undefined;
|
|
8
24
|
};
|
|
9
25
|
//# sourceMappingURL=tokens.d.ts.map
|
package/lib/tokens.es.js
CHANGED
|
@@ -2,5 +2,7 @@ import { createToken } from '@tinkoff/dippy';
|
|
|
2
2
|
|
|
3
3
|
const AUTOSCROLL_BEHAVIOR_MODE_TOKEN = createToken('autoscroll behavior');
|
|
4
4
|
const AUTOSCROLL_SCROLL_TOP_TOKEN = createToken('autoscroll scroll top');
|
|
5
|
+
const AUTOSCROLL_DISABLED_TOKEN = createToken('autoscroll disabled');
|
|
6
|
+
const AUTOSCROLL_APPPLIED_NAVIGATIONS_TOKEN = createToken('autoscroll applied navigations');
|
|
5
7
|
|
|
6
|
-
export { AUTOSCROLL_BEHAVIOR_MODE_TOKEN, AUTOSCROLL_SCROLL_TOP_TOKEN };
|
|
8
|
+
export { AUTOSCROLL_APPPLIED_NAVIGATIONS_TOKEN, AUTOSCROLL_BEHAVIOR_MODE_TOKEN, AUTOSCROLL_DISABLED_TOKEN, AUTOSCROLL_SCROLL_TOP_TOKEN };
|
package/lib/tokens.js
CHANGED
|
@@ -6,6 +6,10 @@ var dippy = require('@tinkoff/dippy');
|
|
|
6
6
|
|
|
7
7
|
const AUTOSCROLL_BEHAVIOR_MODE_TOKEN = dippy.createToken('autoscroll behavior');
|
|
8
8
|
const AUTOSCROLL_SCROLL_TOP_TOKEN = dippy.createToken('autoscroll scroll top');
|
|
9
|
+
const AUTOSCROLL_DISABLED_TOKEN = dippy.createToken('autoscroll disabled');
|
|
10
|
+
const AUTOSCROLL_APPPLIED_NAVIGATIONS_TOKEN = dippy.createToken('autoscroll applied navigations');
|
|
9
11
|
|
|
12
|
+
exports.AUTOSCROLL_APPPLIED_NAVIGATIONS_TOKEN = AUTOSCROLL_APPPLIED_NAVIGATIONS_TOKEN;
|
|
10
13
|
exports.AUTOSCROLL_BEHAVIOR_MODE_TOKEN = AUTOSCROLL_BEHAVIOR_MODE_TOKEN;
|
|
14
|
+
exports.AUTOSCROLL_DISABLED_TOKEN = AUTOSCROLL_DISABLED_TOKEN;
|
|
11
15
|
exports.AUTOSCROLL_SCROLL_TOP_TOKEN = AUTOSCROLL_SCROLL_TOP_TOKEN;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tramvai/module-autoscroll",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.26.8",
|
|
4
4
|
"description": "Компонент с автопрокруткой к началу страницы",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
@@ -16,14 +16,19 @@
|
|
|
16
16
|
"build": "tramvai-build --forPublish --preserveModules",
|
|
17
17
|
"watch": "tsc -w"
|
|
18
18
|
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@tinkoff/utils": "^2.1.3",
|
|
21
|
+
"tslib": "^2.4.0"
|
|
22
|
+
},
|
|
19
23
|
"peerDependencies": {
|
|
20
24
|
"@tinkoff/dippy": "^1.0.0",
|
|
21
|
-
"@
|
|
22
|
-
"@tramvai/
|
|
23
|
-
"@tramvai/
|
|
24
|
-
"@tramvai/
|
|
25
|
-
"
|
|
26
|
-
"
|
|
25
|
+
"@tinkoff/react-hooks": "0.6.1",
|
|
26
|
+
"@tramvai/core": "7.26.8",
|
|
27
|
+
"@tramvai/module-router": "7.26.8",
|
|
28
|
+
"@tramvai/react": "7.26.8",
|
|
29
|
+
"@tramvai/tokens-common": "7.26.8",
|
|
30
|
+
"@tramvai/tokens-render": "7.26.8",
|
|
31
|
+
"react": ">=16.14.0"
|
|
27
32
|
},
|
|
28
33
|
"module": "lib/index.es.js",
|
|
29
34
|
"license": "Apache-2.0"
|