@tramvai/module-autoscroll 7.20.3 → 7.21.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/README.md CHANGED
@@ -30,6 +30,8 @@ createApp({
30
30
 
31
31
  `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
32
 
33
+ Browser scroll restoration is disabled when autoscroll is enabled (`window.history.scrollRestoration = 'manual'`).
34
+
33
35
  ## How to
34
36
 
35
37
  ### Disable autoscroll for page
@@ -97,3 +99,28 @@ const providers = [
97
99
  }),
98
100
  ];
99
101
  ```
102
+
103
+ #### Local
104
+
105
+ 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:
106
+
107
+ ```tsx
108
+ import { AUTOSCROLL_SCROLL_TOP_TOKEN } from '@tramvai/module-autoscroll';
109
+ import { provide } from '@tramvai/core';
110
+
111
+ const providers = [
112
+ // ...,
113
+ provide({
114
+ provide: AUTOSCROLL_SCROLL_TOP_TOKEN,
115
+ useValue: () => {
116
+ try {
117
+ const savedScrollTop = JSON.parse(sessionStorage.get('scrollTop'));
118
+ // for example, if you save scroll position by navigation index
119
+ return savedScrollTop[history.state.index] ?? 0;
120
+ } catch (e) {
121
+ return 0;
122
+ }
123
+ },
124
+ }),
125
+ ];
126
+ ```
@@ -1,2 +1,2 @@
1
- export declare function Autoscroll(): null;
1
+ export declare const Autoscroll: import("react").MemoExoticComponent<() => null>;
2
2
  //# sourceMappingURL=Autoscroll.d.ts.map
@@ -1,13 +1,15 @@
1
- import { useRef, useEffect } from 'react';
2
- import { useRoute, useUrl } from '@tramvai/module-router';
1
+ import { memo, useMemo, useRef } from 'react';
2
+ import { useRoute, useUrl, useViewTransition } 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
- // В некоторых браузерах не поддерживается scrollTo с одним параметром
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,59 @@ const scrollToAnchor = (anchor, behavior) => {
30
28
  return false;
31
29
  }
32
30
  };
33
- function usePreviousValue(value) {
34
- const prevValueRef = useRef(value);
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 routeRef = useRef(url);
50
- const shouldScroll = useRef(!!routeRef.current.hash && isAutoScrollEnabled(route, prevRoute));
51
- // Так как отрисовка нужного нам элемента происходит после обновления route, при первом срабатывании эффекта мы обновляем shouldScroll, а при втором скроллим
52
- useEffect(() => {
53
- if (url.pathname !== routeRef.current.pathname || url.hash !== routeRef.current.hash) {
54
- routeRef.current = url;
55
- shouldScroll.current = isAutoScrollEnabled(route, prevRoute);
35
+ const { isTransitioning } = useViewTransition(url.pathname);
36
+ const scrollBehaviorFactory = useDi(optional(AUTOSCROLL_BEHAVIOR_MODE_TOKEN)) ?? DEFAULT_AUTOSCROLL_BEHAVIOR;
37
+ const scrollTopFactory = useDi(optional(AUTOSCROLL_SCROLL_TOP_TOKEN)) ?? DEFAULT_AUTOSCROLL_SCROLL_TOP;
38
+ const logger = useDi(LOGGER_TOKEN);
39
+ // eslint-disable-next-line react-hooks/exhaustive-deps
40
+ const log = useMemo(() => logger('autoscroll'), []);
41
+ const previousUrl = useRef(url);
42
+ const shouldScroll = useRef(false);
43
+ useIsomorphicLayoutEffect(() => {
44
+ if (url.pathname !== previousUrl.current.pathname || url.hash !== previousUrl.current.hash) {
45
+ shouldScroll.current = true;
56
46
  }
47
+ // skip unnecessary checks, if url has not changed and shouldScroll.current set to false - we don't need to scroll
57
48
  if (!shouldScroll.current) {
58
49
  return;
59
50
  }
60
- const scrollBehavior = route?.navigateState?.autoscrollBehavior || globalScrollBehavior;
61
- if (!url.hash) {
62
- scrollToTop(scrollBehavior, scrollTop);
51
+ // when VT is enabled, we will have a two re-renderw with `isTransitioning: true` (with current and next url),
52
+ // and only after that `isTransitioning` will be false and url will be updated to the next one.
53
+ if (isTransitioning) {
54
+ return;
55
+ }
56
+ // disable autoscroll for current page if `disableAutoscroll` is set in route history state
57
+ if (route.navigateState?.disableAutoscroll) {
58
+ previousUrl.current = url;
63
59
  shouldScroll.current = false;
60
+ log.debug('Skipping autoscroll because "disableAutoscroll" is set in route history state');
61
+ return;
64
62
  }
65
- else {
66
- shouldScroll.current = !scrollToAnchor(url.hash, scrollBehavior);
63
+ previousUrl.current = url;
64
+ shouldScroll.current = false;
65
+ const scrollBehavior = route.navigateState?.autoscrollBehavior ??
66
+ (typeof scrollBehaviorFactory === 'function'
67
+ ? scrollBehaviorFactory()
68
+ : scrollBehaviorFactory);
69
+ const scrollTop = typeof scrollTopFactory === 'function' ? scrollTopFactory() : 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
+ }
67
79
  }
68
- });
80
+ scroll();
81
+ // eslint-disable-next-line react-hooks/exhaustive-deps
82
+ }, [route, url, isTransitioning]);
69
83
  return null;
70
- }
84
+ });
71
85
 
72
86
  export { Autoscroll };
@@ -2,16 +2,18 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var react$1 = require('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
- // В некоторых браузерах не поддерживается scrollTo с одним параметром
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,59 @@ const scrollToAnchor = (anchor, behavior) => {
34
32
  return false;
35
33
  }
36
34
  };
37
- function usePreviousValue(value) {
38
- const prevValueRef = react$1.useRef(value);
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 routeRef = react$1.useRef(url);
54
- const shouldScroll = react$1.useRef(!!routeRef.current.hash && isAutoScrollEnabled(route, prevRoute));
55
- // Так как отрисовка нужного нам элемента происходит после обновления route, при первом срабатывании эффекта мы обновляем shouldScroll, а при втором скроллим
56
- react$1.useEffect(() => {
57
- if (url.pathname !== routeRef.current.pathname || url.hash !== routeRef.current.hash) {
58
- routeRef.current = url;
59
- shouldScroll.current = isAutoScrollEnabled(route, prevRoute);
39
+ const { isTransitioning } = moduleRouter.useViewTransition(url.pathname);
40
+ const scrollBehaviorFactory = react$1.useDi(dippy.optional(tokens.AUTOSCROLL_BEHAVIOR_MODE_TOKEN)) ?? DEFAULT_AUTOSCROLL_BEHAVIOR;
41
+ const scrollTopFactory = react$1.useDi(dippy.optional(tokens.AUTOSCROLL_SCROLL_TOP_TOKEN)) ?? DEFAULT_AUTOSCROLL_SCROLL_TOP;
42
+ const logger = react$1.useDi(tokensCommon.LOGGER_TOKEN);
43
+ // eslint-disable-next-line react-hooks/exhaustive-deps
44
+ const log = react.useMemo(() => logger('autoscroll'), []);
45
+ const previousUrl = react.useRef(url);
46
+ const shouldScroll = react.useRef(false);
47
+ reactHooks.useIsomorphicLayoutEffect(() => {
48
+ if (url.pathname !== previousUrl.current.pathname || url.hash !== previousUrl.current.hash) {
49
+ shouldScroll.current = true;
60
50
  }
51
+ // skip unnecessary checks, if url has not changed and shouldScroll.current set to false - we don't need to scroll
61
52
  if (!shouldScroll.current) {
62
53
  return;
63
54
  }
64
- const scrollBehavior = route?.navigateState?.autoscrollBehavior || globalScrollBehavior;
65
- if (!url.hash) {
66
- scrollToTop(scrollBehavior, scrollTop);
55
+ // when VT is enabled, we will have a two re-renderw with `isTransitioning: true` (with current and next url),
56
+ // and only after that `isTransitioning` will be false and url will be updated to the next one.
57
+ if (isTransitioning) {
58
+ return;
59
+ }
60
+ // disable autoscroll for current page if `disableAutoscroll` is set in route history state
61
+ if (route.navigateState?.disableAutoscroll) {
62
+ previousUrl.current = url;
67
63
  shouldScroll.current = false;
64
+ log.debug('Skipping autoscroll because "disableAutoscroll" is set in route history state');
65
+ return;
68
66
  }
69
- else {
70
- shouldScroll.current = !scrollToAnchor(url.hash, scrollBehavior);
67
+ previousUrl.current = url;
68
+ shouldScroll.current = false;
69
+ const scrollBehavior = route.navigateState?.autoscrollBehavior ??
70
+ (typeof scrollBehaviorFactory === 'function'
71
+ ? scrollBehaviorFactory()
72
+ : scrollBehaviorFactory);
73
+ const scrollTop = typeof scrollTopFactory === 'function' ? scrollTopFactory() : scrollTopFactory;
74
+ function scroll() {
75
+ if (!url.hash) {
76
+ log.debug(`Scrolling to top ${scrollTop}px with behavior: ${scrollBehavior}`);
77
+ scrollToTop(scrollBehavior, scrollTop);
78
+ }
79
+ else {
80
+ log.debug(`Scrolling to anchor ${url.hash} with behavior: ${scrollBehavior}`);
81
+ scrollToAnchor(url.hash, scrollBehavior);
82
+ }
71
83
  }
72
- });
84
+ scroll();
85
+ // eslint-disable-next-line react-hooks/exhaustive-deps
86
+ }, [route, url, isTransitioning]);
73
87
  return null;
74
- }
88
+ });
75
89
 
76
90
  exports.Autoscroll = Autoscroll;
package/lib/index.es.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { __decorate } from 'tslib';
2
- import { Module } from '@tramvai/core';
2
+ import { Module, provide, 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
  export { AUTOSCROLL_BEHAVIOR_MODE_TOKEN, AUTOSCROLL_SCROLL_TOP_TOKEN } from './tokens.es.js';
@@ -10,7 +12,7 @@ let AutoscrollModule = class AutoscrollModule {
10
12
  AutoscrollModule = __decorate([
11
13
  Module({
12
14
  providers: [
13
- {
15
+ provide({
14
16
  provide: LAYOUT_OPTIONS,
15
17
  useValue: {
16
18
  components: {
@@ -18,7 +20,46 @@ AutoscrollModule = __decorate([
18
20
  },
19
21
  },
20
22
  multi: true,
21
- },
23
+ }),
24
+ ...(typeof window !== 'undefined'
25
+ ? [
26
+ provide({
27
+ provide: commandLineListTokens.customerStart,
28
+ useFactory: ({ logger, router }) => {
29
+ const log = logger('autoscroll');
30
+ // disable browser scroll restoration, to avoid conflicts with autoscroll
31
+ const disableBrowserScrollRestoration = () => {
32
+ log.debug('Disabling browser scroll restoration, autoscroll will be applied for this history entry');
33
+ window.history.scrollRestoration = 'manual';
34
+ };
35
+ // enable browser scroll restoration, if autoscroll was disabled
36
+ const enableBrowserScrollRestoration = () => {
37
+ log.debug('Enable browser scroll restoration, autoscroll was disabled for this history entry');
38
+ window.history.scrollRestoration = 'auto';
39
+ };
40
+ return () => {
41
+ // for initial page load
42
+ if (!window.history.state?.navigateState?.disableAutoscroll) {
43
+ disableBrowserScrollRestoration();
44
+ }
45
+ // for sequent navigations
46
+ router.registerSyncHook('change', async (navigation) => {
47
+ if (!navigation.navigateState?.disableAutoscroll) {
48
+ disableBrowserScrollRestoration();
49
+ }
50
+ else {
51
+ enableBrowserScrollRestoration();
52
+ }
53
+ });
54
+ };
55
+ },
56
+ deps: {
57
+ logger: LOGGER_TOKEN,
58
+ router: ROUTER_TOKEN,
59
+ },
60
+ }),
61
+ ]
62
+ : []),
22
63
  ],
23
64
  })
24
65
  ], AutoscrollModule);
package/lib/index.js CHANGED
@@ -5,6 +5,8 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var tslib = require('tslib');
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');
9
11
  var tokens = require('./tokens.js');
10
12
 
@@ -13,7 +15,7 @@ exports.AutoscrollModule = class AutoscrollModule {
13
15
  exports.AutoscrollModule = tslib.__decorate([
14
16
  core.Module({
15
17
  providers: [
16
- {
18
+ core.provide({
17
19
  provide: tokensRender.LAYOUT_OPTIONS,
18
20
  useValue: {
19
21
  components: {
@@ -21,7 +23,46 @@ exports.AutoscrollModule = tslib.__decorate([
21
23
  },
22
24
  },
23
25
  multi: true,
24
- },
26
+ }),
27
+ ...(typeof window !== 'undefined'
28
+ ? [
29
+ core.provide({
30
+ provide: core.commandLineListTokens.customerStart,
31
+ useFactory: ({ logger, router }) => {
32
+ const log = logger('autoscroll');
33
+ // disable browser scroll restoration, to avoid conflicts with autoscroll
34
+ const disableBrowserScrollRestoration = () => {
35
+ log.debug('Disabling browser scroll restoration, autoscroll will be applied for this history entry');
36
+ window.history.scrollRestoration = 'manual';
37
+ };
38
+ // enable browser scroll restoration, if autoscroll was disabled
39
+ const enableBrowserScrollRestoration = () => {
40
+ log.debug('Enable browser scroll restoration, autoscroll was disabled for this history entry');
41
+ window.history.scrollRestoration = 'auto';
42
+ };
43
+ return () => {
44
+ // for initial page load
45
+ if (!window.history.state?.navigateState?.disableAutoscroll) {
46
+ disableBrowserScrollRestoration();
47
+ }
48
+ // for sequent navigations
49
+ router.registerSyncHook('change', async (navigation) => {
50
+ if (!navigation.navigateState?.disableAutoscroll) {
51
+ disableBrowserScrollRestoration();
52
+ }
53
+ else {
54
+ enableBrowserScrollRestoration();
55
+ }
56
+ });
57
+ };
58
+ },
59
+ deps: {
60
+ logger: tokensCommon.LOGGER_TOKEN,
61
+ router: moduleRouter.ROUTER_TOKEN,
62
+ },
63
+ }),
64
+ ]
65
+ : []),
25
66
  ],
26
67
  })
27
68
  ], exports.AutoscrollModule);
package/lib/tokens.d.ts CHANGED
@@ -1,9 +1,14 @@
1
+ export type AutoscrollBehavior = 'smooth' | 'auto';
1
2
  export declare const AUTOSCROLL_BEHAVIOR_MODE_TOKEN: ("smooth" & {
2
3
  __type?: "base token" | undefined;
3
4
  }) | ("auto" & {
4
5
  __type?: "base token" | undefined;
6
+ }) | ((() => AutoscrollBehavior) & {
7
+ __type?: "base token" | undefined;
5
8
  });
6
- export declare const AUTOSCROLL_SCROLL_TOP_TOKEN: number & {
9
+ export declare const AUTOSCROLL_SCROLL_TOP_TOKEN: (number & {
10
+ __type?: "base token" | undefined;
11
+ }) | ((() => number) & {
7
12
  __type?: "base token" | undefined;
8
- };
13
+ });
9
14
  //# sourceMappingURL=tokens.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tramvai/module-autoscroll",
3
- "version": "7.20.3",
3
+ "version": "7.21.1",
4
4
  "description": "Компонент с автопрокруткой к началу страницы",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
@@ -16,14 +16,18 @@
16
16
  "build": "tramvai-build --forPublish --preserveModules",
17
17
  "watch": "tsc -w"
18
18
  },
19
+ "dependencies": {
20
+ "tslib": "^2.4.0"
21
+ },
19
22
  "peerDependencies": {
20
23
  "@tinkoff/dippy": "^1.0.0",
21
- "@tramvai/core": "7.20.3",
22
- "@tramvai/module-router": "7.20.3",
23
- "@tramvai/react": "7.20.3",
24
- "@tramvai/tokens-render": "7.20.3",
25
- "react": ">=16.14.0",
26
- "tslib": "^2.4.0"
24
+ "@tinkoff/react-hooks": "0.6.1",
25
+ "@tramvai/core": "7.21.1",
26
+ "@tramvai/module-router": "7.21.1",
27
+ "@tramvai/react": "7.21.1",
28
+ "@tramvai/tokens-common": "7.21.1",
29
+ "@tramvai/tokens-render": "7.21.1",
30
+ "react": ">=16.14.0"
27
31
  },
28
32
  "module": "lib/index.es.js",
29
33
  "license": "Apache-2.0"