@player-ui/auto-scroll-manager-plugin-react 0.0.1-next.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/dist/index.cjs.js +142 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.esm.js +130 -0
- package/package.json +18 -0
- package/src/hooks.tsx +83 -0
- package/src/index.tsx +2 -0
- package/src/plugin.tsx +153 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var React = require('react');
|
|
6
|
+
var scrollIntoView = require('smooth-scroll-into-view-if-needed');
|
|
7
|
+
|
|
8
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
9
|
+
|
|
10
|
+
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
|
|
11
|
+
var scrollIntoView__default = /*#__PURE__*/_interopDefaultLegacy(scrollIntoView);
|
|
12
|
+
|
|
13
|
+
const AutoScrollManagerContext = React__default["default"].createContext({ register: () => {
|
|
14
|
+
} });
|
|
15
|
+
const useRegisterAsScrollable = () => {
|
|
16
|
+
const { register } = React__default["default"].useContext(AutoScrollManagerContext);
|
|
17
|
+
return register;
|
|
18
|
+
};
|
|
19
|
+
const AutoScrollProvider = ({
|
|
20
|
+
getElementToScrollTo,
|
|
21
|
+
children
|
|
22
|
+
}) => {
|
|
23
|
+
const [scrollableMap, setScrollableMap] = React.useState(new Map());
|
|
24
|
+
const updateScrollableMap = (key, value) => {
|
|
25
|
+
setScrollableMap((prev) => {
|
|
26
|
+
var _a;
|
|
27
|
+
const nm = new Map(prev);
|
|
28
|
+
if (!nm.get(key)) {
|
|
29
|
+
nm.set(key, new Set());
|
|
30
|
+
}
|
|
31
|
+
(_a = nm.get(key)) == null ? void 0 : _a.add(value);
|
|
32
|
+
return nm;
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
const register = (data) => {
|
|
36
|
+
updateScrollableMap(data.type, data.ref);
|
|
37
|
+
};
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
const node = document.getElementById(getElementToScrollTo(scrollableMap));
|
|
40
|
+
if (node) {
|
|
41
|
+
scrollIntoView__default["default"](node, {
|
|
42
|
+
block: "nearest",
|
|
43
|
+
inline: "nearest"
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return /* @__PURE__ */ React__default["default"].createElement(AutoScrollManagerContext.Provider, {
|
|
48
|
+
value: { register }
|
|
49
|
+
}, children);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
exports.ScrollType = void 0;
|
|
53
|
+
(function(ScrollType2) {
|
|
54
|
+
ScrollType2[ScrollType2["ValidationError"] = 0] = "ValidationError";
|
|
55
|
+
ScrollType2[ScrollType2["FirstAppearance"] = 1] = "FirstAppearance";
|
|
56
|
+
ScrollType2[ScrollType2["Unknown"] = 2] = "Unknown";
|
|
57
|
+
})(exports.ScrollType || (exports.ScrollType = {}));
|
|
58
|
+
class AutoScrollManagerPlugin {
|
|
59
|
+
constructor(config) {
|
|
60
|
+
this.name = "auto-scroll-manager";
|
|
61
|
+
var _a, _b;
|
|
62
|
+
this.autoScrollOnLoad = (_a = config.autoScrollOnLoad) != null ? _a : false;
|
|
63
|
+
this.autoFocusOnErrorField = (_b = config.autoFocusOnErrorField) != null ? _b : false;
|
|
64
|
+
this.initialRender = false;
|
|
65
|
+
this.failedNavigation = false;
|
|
66
|
+
this.alreadyScrolledTo = [];
|
|
67
|
+
this.scrollFn = this.calculateScroll.bind(this);
|
|
68
|
+
}
|
|
69
|
+
getFirstScrollableElement(idList, type) {
|
|
70
|
+
const highestElement = { id: "", ypos: 0 };
|
|
71
|
+
const ypos = window.scrollY;
|
|
72
|
+
idList.forEach((id) => {
|
|
73
|
+
const element = document.getElementById(id);
|
|
74
|
+
if (type === 0 && (element == null ? void 0 : element.getAttribute("aria-invalid")) === "false") {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (type === 1) {
|
|
78
|
+
if (this.alreadyScrolledTo.indexOf(id) !== -1) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
this.alreadyScrolledTo.push(id);
|
|
82
|
+
}
|
|
83
|
+
const epos = element == null ? void 0 : element.getBoundingClientRect().top;
|
|
84
|
+
if (epos && (epos + ypos > highestElement.ypos || highestElement.ypos === 0)) {
|
|
85
|
+
highestElement.id = id;
|
|
86
|
+
highestElement.ypos = ypos - epos;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
return highestElement.id;
|
|
90
|
+
}
|
|
91
|
+
calculateScroll(scrollableElements) {
|
|
92
|
+
let currentScroll = 1;
|
|
93
|
+
if (this.initialRender) {
|
|
94
|
+
if (this.autoScrollOnLoad) {
|
|
95
|
+
currentScroll = 0;
|
|
96
|
+
}
|
|
97
|
+
this.initialRender = false;
|
|
98
|
+
} else if (this.failedNavigation) {
|
|
99
|
+
if (this.autoFocusOnErrorField) {
|
|
100
|
+
currentScroll = 0;
|
|
101
|
+
}
|
|
102
|
+
this.failedNavigation = false;
|
|
103
|
+
}
|
|
104
|
+
const elementList = scrollableElements.get(currentScroll);
|
|
105
|
+
if (elementList) {
|
|
106
|
+
const element = this.getFirstScrollableElement(elementList, currentScroll);
|
|
107
|
+
return element != null ? element : "";
|
|
108
|
+
}
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
apply(player) {
|
|
112
|
+
player.hooks.flowController.tap(this.name, (fc) => {
|
|
113
|
+
fc.hooks.flow.tap(this.name, (flow) => {
|
|
114
|
+
flow.hooks.transition.tap(this.name, () => {
|
|
115
|
+
this.initialRender = true;
|
|
116
|
+
this.failedNavigation = false;
|
|
117
|
+
this.alreadyScrolledTo = [];
|
|
118
|
+
});
|
|
119
|
+
flow.hooks.beforeTransition.tap(this.name, (state) => {
|
|
120
|
+
this.failedNavigation = true;
|
|
121
|
+
return state;
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
applyWeb(webPlayer) {
|
|
127
|
+
webPlayer.hooks.webComponent.tap(this.name, (Comp) => {
|
|
128
|
+
return () => {
|
|
129
|
+
const { scrollFn } = this;
|
|
130
|
+
return /* @__PURE__ */ React__default["default"].createElement(AutoScrollProvider, {
|
|
131
|
+
getElementToScrollTo: scrollFn
|
|
132
|
+
}, /* @__PURE__ */ React__default["default"].createElement(Comp, null));
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
exports.AutoScrollManagerContext = AutoScrollManagerContext;
|
|
139
|
+
exports.AutoScrollManagerPlugin = AutoScrollManagerPlugin;
|
|
140
|
+
exports.AutoScrollProvider = AutoScrollProvider;
|
|
141
|
+
exports.useRegisterAsScrollable = useRegisterAsScrollable;
|
|
142
|
+
//# sourceMappingURL=index.cjs.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React, { PropsWithChildren } from 'react';
|
|
2
|
+
import { WebPlayerPlugin, WebPlayer } from '@player-ui/react';
|
|
3
|
+
import { Player } from '@player-ui/player';
|
|
4
|
+
|
|
5
|
+
interface AutoScrollProviderProps {
|
|
6
|
+
/** Return the element to scroll to based on the registered types */
|
|
7
|
+
getElementToScrollTo: (scrollableElements: Map<ScrollType, Set<string>>) => string;
|
|
8
|
+
}
|
|
9
|
+
interface RegisterData {
|
|
10
|
+
/** when to scroll to the target */
|
|
11
|
+
type: ScrollType;
|
|
12
|
+
/** the html id to scroll to */
|
|
13
|
+
ref: string;
|
|
14
|
+
}
|
|
15
|
+
declare type ScrollFunction = (registerData: RegisterData) => void;
|
|
16
|
+
declare const AutoScrollManagerContext: React.Context<{
|
|
17
|
+
/** function to register a scroll target */
|
|
18
|
+
register: ScrollFunction;
|
|
19
|
+
}>;
|
|
20
|
+
/** hook to register as a scroll target */
|
|
21
|
+
declare const useRegisterAsScrollable: () => ScrollFunction;
|
|
22
|
+
/** Component to handle scrolling */
|
|
23
|
+
declare const AutoScrollProvider: ({ getElementToScrollTo, children, }: PropsWithChildren<AutoScrollProviderProps>) => JSX.Element;
|
|
24
|
+
|
|
25
|
+
declare enum ScrollType {
|
|
26
|
+
ValidationError = 0,
|
|
27
|
+
FirstAppearance = 1,
|
|
28
|
+
Unknown = 2
|
|
29
|
+
}
|
|
30
|
+
interface AutoScrollManagerConfig {
|
|
31
|
+
/** Config to auto-scroll on load */
|
|
32
|
+
autoScrollOnLoad?: boolean;
|
|
33
|
+
/** Config to auto-focus on an error */
|
|
34
|
+
autoFocusOnErrorField?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** A plugin to manage scrolling behavior */
|
|
37
|
+
declare class AutoScrollManagerPlugin implements WebPlayerPlugin {
|
|
38
|
+
name: string;
|
|
39
|
+
/** Toggles if we should auto scroll to to the first failed validation on page load */
|
|
40
|
+
private autoScrollOnLoad;
|
|
41
|
+
/** Toggles if we should auto scroll to the first failed validation on navigation failure */
|
|
42
|
+
private autoFocusOnErrorField;
|
|
43
|
+
/** tracks if its the initial page render */
|
|
44
|
+
private initialRender;
|
|
45
|
+
/** tracks if the navigation failed */
|
|
46
|
+
private failedNavigation;
|
|
47
|
+
/** map of scroll type to set of ids that are registered under that type */
|
|
48
|
+
private alreadyScrolledTo;
|
|
49
|
+
private scrollFn;
|
|
50
|
+
constructor(config: AutoScrollManagerConfig);
|
|
51
|
+
getFirstScrollableElement(idList: Set<string>, type: ScrollType): string;
|
|
52
|
+
calculateScroll(scrollableElements: Map<ScrollType, Set<string>>): string;
|
|
53
|
+
apply(player: Player): void;
|
|
54
|
+
applyWeb(webPlayer: WebPlayer): void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { AutoScrollManagerConfig, AutoScrollManagerContext, AutoScrollManagerPlugin, AutoScrollProvider, AutoScrollProviderProps, RegisterData, ScrollFunction, ScrollType, useRegisterAsScrollable };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import scrollIntoView from 'smooth-scroll-into-view-if-needed';
|
|
3
|
+
|
|
4
|
+
const AutoScrollManagerContext = React.createContext({ register: () => {
|
|
5
|
+
} });
|
|
6
|
+
const useRegisterAsScrollable = () => {
|
|
7
|
+
const { register } = React.useContext(AutoScrollManagerContext);
|
|
8
|
+
return register;
|
|
9
|
+
};
|
|
10
|
+
const AutoScrollProvider = ({
|
|
11
|
+
getElementToScrollTo,
|
|
12
|
+
children
|
|
13
|
+
}) => {
|
|
14
|
+
const [scrollableMap, setScrollableMap] = useState(new Map());
|
|
15
|
+
const updateScrollableMap = (key, value) => {
|
|
16
|
+
setScrollableMap((prev) => {
|
|
17
|
+
var _a;
|
|
18
|
+
const nm = new Map(prev);
|
|
19
|
+
if (!nm.get(key)) {
|
|
20
|
+
nm.set(key, new Set());
|
|
21
|
+
}
|
|
22
|
+
(_a = nm.get(key)) == null ? void 0 : _a.add(value);
|
|
23
|
+
return nm;
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
const register = (data) => {
|
|
27
|
+
updateScrollableMap(data.type, data.ref);
|
|
28
|
+
};
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const node = document.getElementById(getElementToScrollTo(scrollableMap));
|
|
31
|
+
if (node) {
|
|
32
|
+
scrollIntoView(node, {
|
|
33
|
+
block: "nearest",
|
|
34
|
+
inline: "nearest"
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return /* @__PURE__ */ React.createElement(AutoScrollManagerContext.Provider, {
|
|
39
|
+
value: { register }
|
|
40
|
+
}, children);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
var ScrollType;
|
|
44
|
+
(function(ScrollType2) {
|
|
45
|
+
ScrollType2[ScrollType2["ValidationError"] = 0] = "ValidationError";
|
|
46
|
+
ScrollType2[ScrollType2["FirstAppearance"] = 1] = "FirstAppearance";
|
|
47
|
+
ScrollType2[ScrollType2["Unknown"] = 2] = "Unknown";
|
|
48
|
+
})(ScrollType || (ScrollType = {}));
|
|
49
|
+
class AutoScrollManagerPlugin {
|
|
50
|
+
constructor(config) {
|
|
51
|
+
this.name = "auto-scroll-manager";
|
|
52
|
+
var _a, _b;
|
|
53
|
+
this.autoScrollOnLoad = (_a = config.autoScrollOnLoad) != null ? _a : false;
|
|
54
|
+
this.autoFocusOnErrorField = (_b = config.autoFocusOnErrorField) != null ? _b : false;
|
|
55
|
+
this.initialRender = false;
|
|
56
|
+
this.failedNavigation = false;
|
|
57
|
+
this.alreadyScrolledTo = [];
|
|
58
|
+
this.scrollFn = this.calculateScroll.bind(this);
|
|
59
|
+
}
|
|
60
|
+
getFirstScrollableElement(idList, type) {
|
|
61
|
+
const highestElement = { id: "", ypos: 0 };
|
|
62
|
+
const ypos = window.scrollY;
|
|
63
|
+
idList.forEach((id) => {
|
|
64
|
+
const element = document.getElementById(id);
|
|
65
|
+
if (type === 0 && (element == null ? void 0 : element.getAttribute("aria-invalid")) === "false") {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (type === 1) {
|
|
69
|
+
if (this.alreadyScrolledTo.indexOf(id) !== -1) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this.alreadyScrolledTo.push(id);
|
|
73
|
+
}
|
|
74
|
+
const epos = element == null ? void 0 : element.getBoundingClientRect().top;
|
|
75
|
+
if (epos && (epos + ypos > highestElement.ypos || highestElement.ypos === 0)) {
|
|
76
|
+
highestElement.id = id;
|
|
77
|
+
highestElement.ypos = ypos - epos;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return highestElement.id;
|
|
81
|
+
}
|
|
82
|
+
calculateScroll(scrollableElements) {
|
|
83
|
+
let currentScroll = 1;
|
|
84
|
+
if (this.initialRender) {
|
|
85
|
+
if (this.autoScrollOnLoad) {
|
|
86
|
+
currentScroll = 0;
|
|
87
|
+
}
|
|
88
|
+
this.initialRender = false;
|
|
89
|
+
} else if (this.failedNavigation) {
|
|
90
|
+
if (this.autoFocusOnErrorField) {
|
|
91
|
+
currentScroll = 0;
|
|
92
|
+
}
|
|
93
|
+
this.failedNavigation = false;
|
|
94
|
+
}
|
|
95
|
+
const elementList = scrollableElements.get(currentScroll);
|
|
96
|
+
if (elementList) {
|
|
97
|
+
const element = this.getFirstScrollableElement(elementList, currentScroll);
|
|
98
|
+
return element != null ? element : "";
|
|
99
|
+
}
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
apply(player) {
|
|
103
|
+
player.hooks.flowController.tap(this.name, (fc) => {
|
|
104
|
+
fc.hooks.flow.tap(this.name, (flow) => {
|
|
105
|
+
flow.hooks.transition.tap(this.name, () => {
|
|
106
|
+
this.initialRender = true;
|
|
107
|
+
this.failedNavigation = false;
|
|
108
|
+
this.alreadyScrolledTo = [];
|
|
109
|
+
});
|
|
110
|
+
flow.hooks.beforeTransition.tap(this.name, (state) => {
|
|
111
|
+
this.failedNavigation = true;
|
|
112
|
+
return state;
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
applyWeb(webPlayer) {
|
|
118
|
+
webPlayer.hooks.webComponent.tap(this.name, (Comp) => {
|
|
119
|
+
return () => {
|
|
120
|
+
const { scrollFn } = this;
|
|
121
|
+
return /* @__PURE__ */ React.createElement(AutoScrollProvider, {
|
|
122
|
+
getElementToScrollTo: scrollFn
|
|
123
|
+
}, /* @__PURE__ */ React.createElement(Comp, null));
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export { AutoScrollManagerContext, AutoScrollManagerPlugin, AutoScrollProvider, ScrollType, useRegisterAsScrollable };
|
|
130
|
+
//# sourceMappingURL=index.esm.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@player-ui/auto-scroll-manager-plugin-react",
|
|
3
|
+
"version": "0.0.1-next.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"registry": "https://registry.npmjs.org"
|
|
7
|
+
},
|
|
8
|
+
"peerDependencies": {
|
|
9
|
+
"@player-ui/binding-grammar": "0.0.1-next.1"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"smooth-scroll-into-view-if-needed": "1.1.32",
|
|
13
|
+
"@babel/runtime": "7.15.4"
|
|
14
|
+
},
|
|
15
|
+
"main": "dist/index.cjs.js",
|
|
16
|
+
"module": "dist/index.esm.js",
|
|
17
|
+
"typings": "dist/index.d.ts"
|
|
18
|
+
}
|
package/src/hooks.tsx
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { PropsWithChildren } from 'react';
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import scrollIntoView from 'smooth-scroll-into-view-if-needed';
|
|
4
|
+
import type { ScrollType } from './index';
|
|
5
|
+
|
|
6
|
+
export interface AutoScrollProviderProps {
|
|
7
|
+
/** Return the element to scroll to based on the registered types */
|
|
8
|
+
getElementToScrollTo: (
|
|
9
|
+
scrollableElements: Map<ScrollType, Set<string>>
|
|
10
|
+
) => string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface RegisterData {
|
|
14
|
+
/** when to scroll to the target */
|
|
15
|
+
type: ScrollType;
|
|
16
|
+
|
|
17
|
+
/** the html id to scroll to */
|
|
18
|
+
ref: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ScrollFunction = (registerData: RegisterData) => void;
|
|
22
|
+
|
|
23
|
+
export const AutoScrollManagerContext = React.createContext<{
|
|
24
|
+
/** function to register a scroll target */
|
|
25
|
+
register: ScrollFunction;
|
|
26
|
+
}>({ register: () => {} });
|
|
27
|
+
|
|
28
|
+
/** hook to register as a scroll target */
|
|
29
|
+
export const useRegisterAsScrollable = (): ScrollFunction => {
|
|
30
|
+
const { register } = React.useContext(AutoScrollManagerContext);
|
|
31
|
+
|
|
32
|
+
return register;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** Component to handle scrolling */
|
|
36
|
+
export const AutoScrollProvider = ({
|
|
37
|
+
getElementToScrollTo,
|
|
38
|
+
children,
|
|
39
|
+
}: PropsWithChildren<AutoScrollProviderProps>) => {
|
|
40
|
+
// Tracker for what elements are registered to be scroll targets
|
|
41
|
+
// Key is the type (initial, validation, appear)
|
|
42
|
+
// Value is a set of target ids
|
|
43
|
+
const [scrollableMap, setScrollableMap] = useState<
|
|
44
|
+
Map<ScrollType, Set<string>>
|
|
45
|
+
>(new Map());
|
|
46
|
+
|
|
47
|
+
/** Add a new entry as a scroll target */
|
|
48
|
+
const updateScrollableMap = (key: ScrollType, value: string) => {
|
|
49
|
+
setScrollableMap((prev) => {
|
|
50
|
+
const nm = new Map(prev);
|
|
51
|
+
|
|
52
|
+
if (!nm.get(key)) {
|
|
53
|
+
nm.set(key, new Set());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
nm.get(key)?.add(value);
|
|
57
|
+
|
|
58
|
+
return nm;
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** register a new scroll target */
|
|
63
|
+
const register: ScrollFunction = (data) => {
|
|
64
|
+
updateScrollableMap(data.type, data.ref);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const node = document.getElementById(getElementToScrollTo(scrollableMap));
|
|
69
|
+
|
|
70
|
+
if (node) {
|
|
71
|
+
scrollIntoView(node, {
|
|
72
|
+
block: 'nearest',
|
|
73
|
+
inline: 'nearest',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<AutoScrollManagerContext.Provider value={{ register }}>
|
|
80
|
+
{children}
|
|
81
|
+
</AutoScrollManagerContext.Provider>
|
|
82
|
+
);
|
|
83
|
+
};
|
package/src/index.tsx
ADDED
package/src/plugin.tsx
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { WebPlayer, WebPlayerPlugin } from '@player-ui/react';
|
|
2
|
+
import type { Player } from '@player-ui/player';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { AutoScrollProvider } from './hooks';
|
|
5
|
+
|
|
6
|
+
export enum ScrollType {
|
|
7
|
+
ValidationError,
|
|
8
|
+
FirstAppearance,
|
|
9
|
+
Unknown,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AutoScrollManagerConfig {
|
|
13
|
+
/** Config to auto-scroll on load */
|
|
14
|
+
autoScrollOnLoad?: boolean;
|
|
15
|
+
/** Config to auto-focus on an error */
|
|
16
|
+
autoFocusOnErrorField?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A plugin to manage scrolling behavior */
|
|
20
|
+
export class AutoScrollManagerPlugin implements WebPlayerPlugin {
|
|
21
|
+
name = 'auto-scroll-manager';
|
|
22
|
+
|
|
23
|
+
/** Toggles if we should auto scroll to to the first failed validation on page load */
|
|
24
|
+
private autoScrollOnLoad: boolean;
|
|
25
|
+
|
|
26
|
+
/** Toggles if we should auto scroll to the first failed validation on navigation failure */
|
|
27
|
+
private autoFocusOnErrorField: boolean;
|
|
28
|
+
|
|
29
|
+
/** tracks if its the initial page render */
|
|
30
|
+
private initialRender: boolean;
|
|
31
|
+
|
|
32
|
+
/** tracks if the navigation failed */
|
|
33
|
+
private failedNavigation: boolean;
|
|
34
|
+
|
|
35
|
+
/** map of scroll type to set of ids that are registered under that type */
|
|
36
|
+
private alreadyScrolledTo: Array<string>;
|
|
37
|
+
private scrollFn: (
|
|
38
|
+
scrollableElements: Map<ScrollType, Set<string>>
|
|
39
|
+
) => string;
|
|
40
|
+
|
|
41
|
+
constructor(config: AutoScrollManagerConfig) {
|
|
42
|
+
this.autoScrollOnLoad = config.autoScrollOnLoad ?? false;
|
|
43
|
+
this.autoFocusOnErrorField = config.autoFocusOnErrorField ?? false;
|
|
44
|
+
this.initialRender = false;
|
|
45
|
+
this.failedNavigation = false;
|
|
46
|
+
this.alreadyScrolledTo = [];
|
|
47
|
+
this.scrollFn = this.calculateScroll.bind(this);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getFirstScrollableElement(idList: Set<string>, type: ScrollType) {
|
|
51
|
+
const highestElement = { id: '', ypos: 0 };
|
|
52
|
+
const ypos = window.scrollY;
|
|
53
|
+
idList.forEach((id) => {
|
|
54
|
+
const element = document.getElementById(id);
|
|
55
|
+
|
|
56
|
+
// if we are looking at validation errors, make sure the element is invalid
|
|
57
|
+
if (
|
|
58
|
+
type === ScrollType.ValidationError &&
|
|
59
|
+
element?.getAttribute('aria-invalid') === 'false'
|
|
60
|
+
) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// if we are just looking at elements that just appeared, make sure we haven't
|
|
65
|
+
// scrolled to them before
|
|
66
|
+
if (type === ScrollType.FirstAppearance) {
|
|
67
|
+
if (this.alreadyScrolledTo.indexOf(id) !== -1) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
this.alreadyScrolledTo.push(id);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const epos = element?.getBoundingClientRect().top;
|
|
75
|
+
|
|
76
|
+
if (
|
|
77
|
+
epos &&
|
|
78
|
+
(epos + ypos > highestElement.ypos || highestElement.ypos === 0)
|
|
79
|
+
) {
|
|
80
|
+
highestElement.id = id;
|
|
81
|
+
highestElement.ypos = ypos - epos;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return highestElement.id;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
calculateScroll(scrollableElements: Map<ScrollType, Set<string>>) {
|
|
89
|
+
let currentScroll = ScrollType.FirstAppearance;
|
|
90
|
+
|
|
91
|
+
if (this.initialRender) {
|
|
92
|
+
if (this.autoScrollOnLoad) {
|
|
93
|
+
currentScroll = ScrollType.ValidationError;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.initialRender = false;
|
|
97
|
+
} else if (this.failedNavigation) {
|
|
98
|
+
if (this.autoFocusOnErrorField) {
|
|
99
|
+
currentScroll = ScrollType.ValidationError;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.failedNavigation = false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const elementList = scrollableElements.get(currentScroll);
|
|
106
|
+
|
|
107
|
+
if (elementList) {
|
|
108
|
+
const element = this.getFirstScrollableElement(
|
|
109
|
+
elementList,
|
|
110
|
+
currentScroll
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return element ?? '';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return '';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Hooks into player flow to determine what scroll targets need to be evaluated at specific lifecycle points
|
|
120
|
+
apply(player: Player) {
|
|
121
|
+
player.hooks.flowController.tap(this.name, (fc) => {
|
|
122
|
+
fc.hooks.flow.tap(this.name, (flow) => {
|
|
123
|
+
flow.hooks.transition.tap(this.name, () => {
|
|
124
|
+
// Reset Everything
|
|
125
|
+
this.initialRender = true;
|
|
126
|
+
this.failedNavigation = false;
|
|
127
|
+
this.alreadyScrolledTo = [];
|
|
128
|
+
});
|
|
129
|
+
flow.hooks.beforeTransition.tap(this.name, (state) => {
|
|
130
|
+
// will get reset to false if view successfully transitions
|
|
131
|
+
// otherwise stays as true when view get rerendered with errors
|
|
132
|
+
this.failedNavigation = true;
|
|
133
|
+
|
|
134
|
+
return state;
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
applyWeb(webPlayer: WebPlayer) {
|
|
141
|
+
webPlayer.hooks.webComponent.tap(this.name, (Comp) => {
|
|
142
|
+
return () => {
|
|
143
|
+
const { scrollFn } = this;
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<AutoScrollProvider getElementToScrollTo={scrollFn}>
|
|
147
|
+
<Comp />
|
|
148
|
+
</AutoScrollProvider>
|
|
149
|
+
);
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|