@player-ui/auto-scroll-manager-plugin-react 0.15.1-next.2 → 0.15.1-next.4
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/cjs/index.cjs +15 -2
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.legacy-esm.js +15 -2
- package/dist/index.mjs +15 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/plugin.test.tsx +125 -18
- package/src/hooks.tsx +8 -1
- package/src/plugin.tsx +15 -4
- package/types/hooks.d.ts +3 -1
- package/types/plugin.d.ts +2 -0
package/dist/cjs/index.cjs
CHANGED
|
@@ -61,9 +61,13 @@ var AutoScrollProvider = ({
|
|
|
61
61
|
getElementToScrollTo,
|
|
62
62
|
getBaseElement,
|
|
63
63
|
offset,
|
|
64
|
+
onMount,
|
|
64
65
|
children
|
|
65
66
|
}) => {
|
|
66
67
|
const [scrollableMap, setScrollableMap] = (0, import_react.useState)(/* @__PURE__ */ new Map());
|
|
68
|
+
(0, import_react.useEffect)(() => {
|
|
69
|
+
onMount?.(() => setScrollableMap(/* @__PURE__ */ new Map()));
|
|
70
|
+
}, []);
|
|
67
71
|
const updateScrollableMap = (key, value) => {
|
|
68
72
|
setScrollableMap((prev) => {
|
|
69
73
|
const nm = new Map(prev);
|
|
@@ -82,7 +86,7 @@ var AutoScrollProvider = ({
|
|
|
82
86
|
if (node) {
|
|
83
87
|
scrollIntoViewWithOffset(node, getBaseElement() || document.body, offset);
|
|
84
88
|
}
|
|
85
|
-
});
|
|
89
|
+
}, [scrollableMap]);
|
|
86
90
|
return /* @__PURE__ */ import_react.default.createElement(AutoScrollManagerContext.Provider, { value: { register } }, children);
|
|
87
91
|
};
|
|
88
92
|
|
|
@@ -104,6 +108,8 @@ var AutoScrollManagerPlugin = class {
|
|
|
104
108
|
this.initialRender = false;
|
|
105
109
|
this.failedNavigation = false;
|
|
106
110
|
this.alreadyScrolledTo = [];
|
|
111
|
+
this.clearScrollableMap = () => {
|
|
112
|
+
};
|
|
107
113
|
this.scrollFn = this.calculateScroll.bind(this);
|
|
108
114
|
}
|
|
109
115
|
getFirstScrollableElement(idList, type) {
|
|
@@ -162,6 +168,7 @@ var AutoScrollManagerPlugin = class {
|
|
|
162
168
|
this.initialRender = true;
|
|
163
169
|
this.failedNavigation = false;
|
|
164
170
|
this.alreadyScrolledTo = [];
|
|
171
|
+
this.clearScrollableMap();
|
|
165
172
|
window.scroll(0, 0);
|
|
166
173
|
});
|
|
167
174
|
flow.hooks.skipTransition.intercept({
|
|
@@ -175,13 +182,19 @@ var AutoScrollManagerPlugin = class {
|
|
|
175
182
|
applyReact(reactPlayer) {
|
|
176
183
|
reactPlayer.hooks.webComponent.tap(this.name, (Comp) => {
|
|
177
184
|
const { scrollFn, getBaseElement, offset } = this;
|
|
185
|
+
const setScrollableMapClearer = (clear) => {
|
|
186
|
+
this.clearScrollableMap = clear;
|
|
187
|
+
};
|
|
178
188
|
function AutoScrollManagerComponent() {
|
|
179
189
|
return /* @__PURE__ */ import_react2.default.createElement(
|
|
180
190
|
AutoScrollProvider,
|
|
181
191
|
{
|
|
182
192
|
getElementToScrollTo: scrollFn,
|
|
183
193
|
getBaseElement,
|
|
184
|
-
offset
|
|
194
|
+
offset,
|
|
195
|
+
onMount: (clear) => {
|
|
196
|
+
setScrollableMapClearer(clear);
|
|
197
|
+
}
|
|
185
198
|
},
|
|
186
199
|
/* @__PURE__ */ import_react2.default.createElement(Comp, null)
|
|
187
200
|
);
|
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/plugins/auto-scroll/react/src/index.tsx","../../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/plugins/auto-scroll/react/src/hooks.tsx","../../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/plugins/auto-scroll/react/src/scrollIntoViewWithOffset.ts","../../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/plugins/auto-scroll/react/src/plugin.tsx"],"sourcesContent":["export * from \"./hooks\";\nexport * from \"./plugin\";\n","import type { PropsWithChildren } from \"react\";\nimport React, { useEffect, useState } from \"react\";\nimport { scrollIntoViewWithOffset } from \"./scrollIntoViewWithOffset\";\nimport type { ScrollType } from \"./index\";\n\nexport interface AutoScrollProviderProps {\n /** Return the element to scroll to based on the registered types */\n getElementToScrollTo: (\n scrollableElements: Map<ScrollType, Set<string>>,\n ) => string;\n /** Optional function to get container element, which is used for calculating offset (default: document.body) */\n getBaseElement: () => HTMLElement | undefined | null;\n /** Additional offset to be used (default: 0) */\n offset: number;\n}\n\nexport interface RegisterData {\n /** when to scroll to the target */\n type: ScrollType;\n\n /** the html id to scroll to */\n ref: string;\n}\n\nexport type ScrollFunction = (registerData: RegisterData) => void;\n\nexport const AutoScrollManagerContext = React.createContext<{\n /** function to register a scroll target */\n register: ScrollFunction;\n}>({ register: () => {} });\n\n/** hook to register as a scroll target */\nexport const useRegisterAsScrollable = (): ScrollFunction => {\n const { register } = React.useContext(AutoScrollManagerContext);\n\n return register;\n};\n\n/** Component to handle scrolling */\nexport const AutoScrollProvider = ({\n getElementToScrollTo,\n getBaseElement,\n offset,\n children,\n}: PropsWithChildren<AutoScrollProviderProps>) => {\n // Tracker for what elements are registered to be scroll targets\n // Key is the type (initial, validation, appear)\n // Value is a set of target ids\n const [scrollableMap, setScrollableMap] = useState<\n Map<ScrollType, Set<string>>\n >(new Map());\n\n /** Add a new entry as a scroll target */\n const updateScrollableMap = (key: ScrollType, value: string) => {\n setScrollableMap((prev) => {\n const nm = new Map(prev);\n\n if (!nm.get(key)) {\n nm.set(key, new Set());\n }\n\n nm.get(key)?.add(value);\n\n return nm;\n });\n };\n\n /** register a new scroll target */\n const register: ScrollFunction = (data) => {\n updateScrollableMap(data.type, data.ref);\n };\n\n useEffect(() => {\n const node = document.getElementById(getElementToScrollTo(scrollableMap));\n\n if (node) {\n scrollIntoViewWithOffset(node, getBaseElement() || document.body, offset);\n }\n });\n\n return (\n <AutoScrollManagerContext.Provider value={{ register }}>\n {children}\n </AutoScrollManagerContext.Provider>\n );\n};\n","import { scrollTo } from \"seamless-scroll-polyfill\";\n\n/**\n * Scroll to the given element with an offset\n * @param node Element to scroll to\n * @param baseElement Container element used to calculate offset\n * @param offset Additional offset\n */\nexport function scrollIntoViewWithOffset(\n node: HTMLElement,\n baseElement: HTMLElement,\n offset: number,\n) {\n scrollTo(window, {\n behavior: \"smooth\",\n top:\n node.getBoundingClientRect().top -\n baseElement.getBoundingClientRect().top -\n offset,\n });\n}\n","import type { ReactPlayer, ReactPlayerPlugin } from \"@player-ui/react\";\nimport type { Player } from \"@player-ui/react\";\nimport React from \"react\";\nimport { AutoScrollProvider } from \"./hooks\";\n\nexport enum ScrollType {\n ValidationError,\n FirstAppearance,\n Unknown,\n}\n\nexport interface AutoScrollManagerConfig {\n /** Config to auto-scroll on load */\n autoScrollOnLoad?: boolean;\n /** Config to auto-focus on an error */\n autoFocusOnErrorField?: boolean;\n /** Optional function to get container element, which is used for calculating offset (default: document.body) */\n getBaseElement?: () => HTMLElement | undefined | null;\n /** Additional offset to be used (default: 0) */\n offset?: number;\n}\n\n/** A plugin to manage scrolling behavior */\nexport class AutoScrollManagerPlugin implements ReactPlayerPlugin {\n name = \"auto-scroll-manager\";\n\n /** Toggles if we should auto scroll to to the first failed validation on page load */\n private autoScrollOnLoad: boolean;\n\n /** Toggles if we should auto scroll to the first failed validation on navigation failure */\n private autoFocusOnErrorField: boolean;\n\n /** tracks if its the initial page render */\n private initialRender: boolean;\n\n /** tracks if the navigation failed */\n private failedNavigation: boolean;\n\n /** function to return the base of the scrollable area */\n private getBaseElement: () => HTMLElement | undefined | null;\n\n /** static offset */\n private offset: number;\n\n /** map of scroll type to set of ids that are registered under that type */\n private alreadyScrolledTo: Array<string>;\n private scrollFn: (\n scrollableElements: Map<ScrollType, Set<string>>,\n ) => string;\n\n constructor(config: AutoScrollManagerConfig) {\n this.autoScrollOnLoad = config.autoScrollOnLoad ?? false;\n this.autoFocusOnErrorField = config.autoFocusOnErrorField ?? false;\n this.getBaseElement = config.getBaseElement ?? (() => null);\n this.offset = config.offset ?? 0;\n this.initialRender = false;\n this.failedNavigation = false;\n this.alreadyScrolledTo = [];\n this.scrollFn = this.calculateScroll.bind(this);\n }\n\n getFirstScrollableElement(idList: Set<string>, type: ScrollType) {\n const highestElement = {\n id: \"\",\n ypos: 0,\n };\n const ypos = window.scrollY;\n idList.forEach((id) => {\n const element = document.getElementById(id);\n\n // if we are looking at validation errors, make sure the element is invalid\n if (\n type === ScrollType.ValidationError &&\n element?.getAttribute(\"aria-invalid\") === \"false\"\n ) {\n return;\n }\n\n // if we are just looking at elements that just appeared, make sure we haven't\n // scrolled to them before\n if (type === ScrollType.FirstAppearance) {\n if (this.alreadyScrolledTo.indexOf(id) !== -1) {\n return;\n }\n\n this.alreadyScrolledTo.push(id);\n }\n\n const epos = element?.getBoundingClientRect().top;\n if (\n epos !== undefined &&\n (epos + ypos < highestElement.ypos || highestElement.id === \"\")\n ) {\n highestElement.id = id;\n highestElement.ypos = ypos + epos;\n }\n });\n return highestElement.id;\n }\n\n calculateScroll(scrollableElements: Map<ScrollType, Set<string>>) {\n let currentScroll = ScrollType.FirstAppearance;\n if (this.initialRender) {\n if (this.autoScrollOnLoad) {\n currentScroll = ScrollType.ValidationError;\n }\n\n this.initialRender = false;\n } else if (this.failedNavigation) {\n if (this.autoFocusOnErrorField) {\n currentScroll = ScrollType.ValidationError;\n }\n\n this.failedNavigation = false;\n }\n\n const elementList = scrollableElements.get(currentScroll);\n if (elementList) {\n const element = this.getFirstScrollableElement(\n elementList,\n currentScroll,\n );\n return element ?? \"\";\n }\n\n return \"\";\n }\n\n // Hooks into player flow to determine what scroll targets need to be evaluated at specific lifecycle points\n apply(player: Player) {\n player.hooks.flowController.tap(this.name, (fc) => {\n fc.hooks.flow.tap(this.name, (flow) => {\n flow.hooks.transition.tap(this.name, () => {\n // Reset Everything\n this.initialRender = true;\n this.failedNavigation = false;\n this.alreadyScrolledTo = [];\n // Reset scroll position for new view\n window.scroll(0, 0);\n });\n flow.hooks.skipTransition.intercept({\n call: () => {\n this.failedNavigation = true;\n },\n });\n });\n });\n }\n\n applyReact(reactPlayer: ReactPlayer) {\n reactPlayer.hooks.webComponent.tap(this.name, (Comp) => {\n const { scrollFn, getBaseElement, offset } = this;\n\n function AutoScrollManagerComponent() {\n return (\n <AutoScrollProvider\n getElementToScrollTo={scrollFn}\n getBaseElement={getBaseElement}\n offset={offset}\n >\n <Comp />\n </AutoScrollProvider>\n );\n }\n\n return AutoScrollManagerComponent;\n });\n }\n}\n"],"mappings":"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","names":["React","import_react","ScrollType","React"]}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/plugins/auto-scroll/react/src/index.tsx","../../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/plugins/auto-scroll/react/src/hooks.tsx","../../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/plugins/auto-scroll/react/src/scrollIntoViewWithOffset.ts","../../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/plugins/auto-scroll/react/src/plugin.tsx"],"sourcesContent":["export * from \"./hooks\";\nexport * from \"./plugin\";\n","import type { PropsWithChildren } from \"react\";\nimport React, { useEffect, useState } from \"react\";\nimport { scrollIntoViewWithOffset } from \"./scrollIntoViewWithOffset\";\nimport type { ScrollType } from \"./index\";\n\nexport interface AutoScrollProviderProps {\n /** Return the element to scroll to based on the registered types */\n getElementToScrollTo: (\n scrollableElements: Map<ScrollType, Set<string>>,\n ) => string;\n /** Optional function to get container element, which is used for calculating offset (default: document.body) */\n getBaseElement: () => HTMLElement | undefined | null;\n /** Additional offset to be used (default: 0) */\n offset: number;\n /** Called on mount with a function that clears all registered scroll targets */\n onMount?: (clearScrollableMap: () => void) => void;\n}\n\nexport interface RegisterData {\n /** when to scroll to the target */\n type: ScrollType;\n\n /** the html id to scroll to */\n ref: string;\n}\n\nexport type ScrollFunction = (registerData: RegisterData) => void;\n\nexport const AutoScrollManagerContext = React.createContext<{\n /** function to register a scroll target */\n register: ScrollFunction;\n}>({ register: () => {} });\n\n/** hook to register as a scroll target */\nexport const useRegisterAsScrollable = (): ScrollFunction => {\n const { register } = React.useContext(AutoScrollManagerContext);\n\n return register;\n};\n\n/** Component to handle scrolling */\nexport const AutoScrollProvider = ({\n getElementToScrollTo,\n getBaseElement,\n offset,\n onMount,\n children,\n}: PropsWithChildren<AutoScrollProviderProps>) => {\n // Tracker for what elements are registered to be scroll targets\n // Key is the type (initial, validation, appear)\n // Value is a set of target ids\n const [scrollableMap, setScrollableMap] = useState<\n Map<ScrollType, Set<string>>\n >(new Map());\n\n useEffect(() => {\n onMount?.(() => setScrollableMap(new Map()));\n }, []);\n\n /** Add a new entry as a scroll target */\n const updateScrollableMap = (key: ScrollType, value: string) => {\n setScrollableMap((prev) => {\n const nm = new Map(prev);\n\n if (!nm.get(key)) {\n nm.set(key, new Set());\n }\n\n nm.get(key)?.add(value);\n\n return nm;\n });\n };\n\n /** register a new scroll target */\n const register: ScrollFunction = (data) => {\n updateScrollableMap(data.type, data.ref);\n };\n\n useEffect(() => {\n const node = document.getElementById(getElementToScrollTo(scrollableMap));\n\n if (node) {\n scrollIntoViewWithOffset(node, getBaseElement() || document.body, offset);\n }\n }, [scrollableMap]);\n\n return (\n <AutoScrollManagerContext.Provider value={{ register }}>\n {children}\n </AutoScrollManagerContext.Provider>\n );\n};\n","import { scrollTo } from \"seamless-scroll-polyfill\";\n\n/**\n * Scroll to the given element with an offset\n * @param node Element to scroll to\n * @param baseElement Container element used to calculate offset\n * @param offset Additional offset\n */\nexport function scrollIntoViewWithOffset(\n node: HTMLElement,\n baseElement: HTMLElement,\n offset: number,\n) {\n scrollTo(window, {\n behavior: \"smooth\",\n top:\n node.getBoundingClientRect().top -\n baseElement.getBoundingClientRect().top -\n offset,\n });\n}\n","import type { ReactPlayer, ReactPlayerPlugin } from \"@player-ui/react\";\nimport type { Player } from \"@player-ui/react\";\nimport React from \"react\";\nimport { AutoScrollProvider } from \"./hooks\";\n\nexport enum ScrollType {\n ValidationError,\n FirstAppearance,\n Unknown,\n}\n\nexport interface AutoScrollManagerConfig {\n /** Config to auto-scroll on load */\n autoScrollOnLoad?: boolean;\n /** Config to auto-focus on an error */\n autoFocusOnErrorField?: boolean;\n /** Optional function to get container element, which is used for calculating offset (default: document.body) */\n getBaseElement?: () => HTMLElement | undefined | null;\n /** Additional offset to be used (default: 0) */\n offset?: number;\n}\n\n/** A plugin to manage scrolling behavior */\nexport class AutoScrollManagerPlugin implements ReactPlayerPlugin {\n name = \"auto-scroll-manager\";\n\n /** Toggles if we should auto scroll to to the first failed validation on page load */\n private autoScrollOnLoad: boolean;\n\n /** Toggles if we should auto scroll to the first failed validation on navigation failure */\n private autoFocusOnErrorField: boolean;\n\n /** tracks if its the initial page render */\n private initialRender: boolean;\n\n /** tracks if the navigation failed */\n private failedNavigation: boolean;\n\n /** function to return the base of the scrollable area */\n private getBaseElement: () => HTMLElement | undefined | null;\n\n /** static offset */\n private offset: number;\n\n /** clears scrollableMap in AutoScrollProvider — set when the provider mounts */\n private clearScrollableMap: () => void;\n\n /** map of scroll type to set of ids that are registered under that type */\n private alreadyScrolledTo: Array<string>;\n private scrollFn: (\n scrollableElements: Map<ScrollType, Set<string>>,\n ) => string;\n\n constructor(config: AutoScrollManagerConfig) {\n this.autoScrollOnLoad = config.autoScrollOnLoad ?? false;\n this.autoFocusOnErrorField = config.autoFocusOnErrorField ?? false;\n this.getBaseElement = config.getBaseElement ?? (() => null);\n this.offset = config.offset ?? 0;\n this.initialRender = false;\n this.failedNavigation = false;\n this.alreadyScrolledTo = [];\n this.clearScrollableMap = () => {};\n this.scrollFn = this.calculateScroll.bind(this);\n }\n\n getFirstScrollableElement(idList: Set<string>, type: ScrollType): string {\n const highestElement = {\n id: \"\",\n ypos: 0,\n };\n const ypos = window.scrollY;\n idList.forEach((id) => {\n const element = document.getElementById(id);\n\n // if we are looking at validation errors, make sure the element is invalid\n if (\n type === ScrollType.ValidationError &&\n element?.getAttribute(\"aria-invalid\") === \"false\"\n ) {\n return;\n }\n\n // if we are just looking at elements that just appeared, make sure we haven't\n // scrolled to them before\n if (type === ScrollType.FirstAppearance) {\n if (this.alreadyScrolledTo.indexOf(id) !== -1) {\n return;\n }\n\n this.alreadyScrolledTo.push(id);\n }\n\n const epos = element?.getBoundingClientRect().top;\n if (\n epos !== undefined &&\n (epos + ypos < highestElement.ypos || highestElement.id === \"\")\n ) {\n highestElement.id = id;\n highestElement.ypos = ypos + epos;\n }\n });\n return highestElement.id;\n }\n\n calculateScroll(scrollableElements: Map<ScrollType, Set<string>>): string {\n let currentScroll = ScrollType.FirstAppearance;\n if (this.initialRender) {\n if (this.autoScrollOnLoad) {\n currentScroll = ScrollType.ValidationError;\n }\n\n this.initialRender = false;\n } else if (this.failedNavigation) {\n if (this.autoFocusOnErrorField) {\n currentScroll = ScrollType.ValidationError;\n }\n\n this.failedNavigation = false;\n }\n\n const elementList = scrollableElements.get(currentScroll);\n if (elementList) {\n const element = this.getFirstScrollableElement(\n elementList,\n currentScroll,\n );\n return element ?? \"\";\n }\n\n return \"\";\n }\n\n // Hooks into player flow to determine what scroll targets need to be evaluated at specific lifecycle points\n apply(player: Player): void {\n player.hooks.flowController.tap(this.name, (fc) => {\n fc.hooks.flow.tap(this.name, (flow) => {\n flow.hooks.transition.tap(this.name, () => {\n // Reset Everything\n this.initialRender = true;\n this.failedNavigation = false;\n this.alreadyScrolledTo = [];\n this.clearScrollableMap();\n // Reset scroll position for new view\n window.scroll(0, 0);\n });\n flow.hooks.skipTransition.intercept({\n call: () => {\n this.failedNavigation = true;\n },\n });\n });\n });\n }\n\n applyReact(reactPlayer: ReactPlayer): void {\n reactPlayer.hooks.webComponent.tap(this.name, (Comp) => {\n const { scrollFn, getBaseElement, offset } = this;\n const setScrollableMapClearer = (clear: () => void) => {\n this.clearScrollableMap = clear;\n };\n\n function AutoScrollManagerComponent() {\n return (\n <AutoScrollProvider\n getElementToScrollTo={scrollFn}\n getBaseElement={getBaseElement}\n offset={offset}\n onMount={(clear) => {\n setScrollableMapClearer(clear);\n }}\n >\n <Comp />\n </AutoScrollProvider>\n );\n }\n\n return AutoScrollManagerComponent;\n });\n }\n}\n"],"mappings":"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","names":["React","import_react","ScrollType","React"]}
|
package/dist/index.legacy-esm.js
CHANGED
|
@@ -21,9 +21,13 @@ var AutoScrollProvider = ({
|
|
|
21
21
|
getElementToScrollTo,
|
|
22
22
|
getBaseElement,
|
|
23
23
|
offset,
|
|
24
|
+
onMount,
|
|
24
25
|
children
|
|
25
26
|
}) => {
|
|
26
27
|
const [scrollableMap, setScrollableMap] = useState(/* @__PURE__ */ new Map());
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
onMount?.(() => setScrollableMap(/* @__PURE__ */ new Map()));
|
|
30
|
+
}, []);
|
|
27
31
|
const updateScrollableMap = (key, value) => {
|
|
28
32
|
setScrollableMap((prev) => {
|
|
29
33
|
const nm = new Map(prev);
|
|
@@ -42,7 +46,7 @@ var AutoScrollProvider = ({
|
|
|
42
46
|
if (node) {
|
|
43
47
|
scrollIntoViewWithOffset(node, getBaseElement() || document.body, offset);
|
|
44
48
|
}
|
|
45
|
-
});
|
|
49
|
+
}, [scrollableMap]);
|
|
46
50
|
return /* @__PURE__ */ React.createElement(AutoScrollManagerContext.Provider, { value: { register } }, children);
|
|
47
51
|
};
|
|
48
52
|
|
|
@@ -64,6 +68,8 @@ var AutoScrollManagerPlugin = class {
|
|
|
64
68
|
this.initialRender = false;
|
|
65
69
|
this.failedNavigation = false;
|
|
66
70
|
this.alreadyScrolledTo = [];
|
|
71
|
+
this.clearScrollableMap = () => {
|
|
72
|
+
};
|
|
67
73
|
this.scrollFn = this.calculateScroll.bind(this);
|
|
68
74
|
}
|
|
69
75
|
getFirstScrollableElement(idList, type) {
|
|
@@ -122,6 +128,7 @@ var AutoScrollManagerPlugin = class {
|
|
|
122
128
|
this.initialRender = true;
|
|
123
129
|
this.failedNavigation = false;
|
|
124
130
|
this.alreadyScrolledTo = [];
|
|
131
|
+
this.clearScrollableMap();
|
|
125
132
|
window.scroll(0, 0);
|
|
126
133
|
});
|
|
127
134
|
flow.hooks.skipTransition.intercept({
|
|
@@ -135,13 +142,19 @@ var AutoScrollManagerPlugin = class {
|
|
|
135
142
|
applyReact(reactPlayer) {
|
|
136
143
|
reactPlayer.hooks.webComponent.tap(this.name, (Comp) => {
|
|
137
144
|
const { scrollFn, getBaseElement, offset } = this;
|
|
145
|
+
const setScrollableMapClearer = (clear) => {
|
|
146
|
+
this.clearScrollableMap = clear;
|
|
147
|
+
};
|
|
138
148
|
function AutoScrollManagerComponent() {
|
|
139
149
|
return /* @__PURE__ */ React2.createElement(
|
|
140
150
|
AutoScrollProvider,
|
|
141
151
|
{
|
|
142
152
|
getElementToScrollTo: scrollFn,
|
|
143
153
|
getBaseElement,
|
|
144
|
-
offset
|
|
154
|
+
offset,
|
|
155
|
+
onMount: (clear) => {
|
|
156
|
+
setScrollableMapClearer(clear);
|
|
157
|
+
}
|
|
145
158
|
},
|
|
146
159
|
/* @__PURE__ */ React2.createElement(Comp, null)
|
|
147
160
|
);
|
package/dist/index.mjs
CHANGED
|
@@ -21,9 +21,13 @@ var AutoScrollProvider = ({
|
|
|
21
21
|
getElementToScrollTo,
|
|
22
22
|
getBaseElement,
|
|
23
23
|
offset,
|
|
24
|
+
onMount,
|
|
24
25
|
children
|
|
25
26
|
}) => {
|
|
26
27
|
const [scrollableMap, setScrollableMap] = useState(/* @__PURE__ */ new Map());
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
onMount?.(() => setScrollableMap(/* @__PURE__ */ new Map()));
|
|
30
|
+
}, []);
|
|
27
31
|
const updateScrollableMap = (key, value) => {
|
|
28
32
|
setScrollableMap((prev) => {
|
|
29
33
|
const nm = new Map(prev);
|
|
@@ -42,7 +46,7 @@ var AutoScrollProvider = ({
|
|
|
42
46
|
if (node) {
|
|
43
47
|
scrollIntoViewWithOffset(node, getBaseElement() || document.body, offset);
|
|
44
48
|
}
|
|
45
|
-
});
|
|
49
|
+
}, [scrollableMap]);
|
|
46
50
|
return /* @__PURE__ */ React.createElement(AutoScrollManagerContext.Provider, { value: { register } }, children);
|
|
47
51
|
};
|
|
48
52
|
|
|
@@ -64,6 +68,8 @@ var AutoScrollManagerPlugin = class {
|
|
|
64
68
|
this.initialRender = false;
|
|
65
69
|
this.failedNavigation = false;
|
|
66
70
|
this.alreadyScrolledTo = [];
|
|
71
|
+
this.clearScrollableMap = () => {
|
|
72
|
+
};
|
|
67
73
|
this.scrollFn = this.calculateScroll.bind(this);
|
|
68
74
|
}
|
|
69
75
|
getFirstScrollableElement(idList, type) {
|
|
@@ -122,6 +128,7 @@ var AutoScrollManagerPlugin = class {
|
|
|
122
128
|
this.initialRender = true;
|
|
123
129
|
this.failedNavigation = false;
|
|
124
130
|
this.alreadyScrolledTo = [];
|
|
131
|
+
this.clearScrollableMap();
|
|
125
132
|
window.scroll(0, 0);
|
|
126
133
|
});
|
|
127
134
|
flow.hooks.skipTransition.intercept({
|
|
@@ -135,13 +142,19 @@ var AutoScrollManagerPlugin = class {
|
|
|
135
142
|
applyReact(reactPlayer) {
|
|
136
143
|
reactPlayer.hooks.webComponent.tap(this.name, (Comp) => {
|
|
137
144
|
const { scrollFn, getBaseElement, offset } = this;
|
|
145
|
+
const setScrollableMapClearer = (clear) => {
|
|
146
|
+
this.clearScrollableMap = clear;
|
|
147
|
+
};
|
|
138
148
|
function AutoScrollManagerComponent() {
|
|
139
149
|
return /* @__PURE__ */ React2.createElement(
|
|
140
150
|
AutoScrollProvider,
|
|
141
151
|
{
|
|
142
152
|
getElementToScrollTo: scrollFn,
|
|
143
153
|
getBaseElement,
|
|
144
|
-
offset
|
|
154
|
+
offset,
|
|
155
|
+
onMount: (clear) => {
|
|
156
|
+
setScrollableMapClearer(clear);
|
|
157
|
+
}
|
|
145
158
|
},
|
|
146
159
|
/* @__PURE__ */ React2.createElement(Comp, null)
|
|
147
160
|
);
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/plugins/auto-scroll/react/src/hooks.tsx","../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/plugins/auto-scroll/react/src/scrollIntoViewWithOffset.ts","../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/plugins/auto-scroll/react/src/plugin.tsx"],"sourcesContent":["import type { PropsWithChildren } from \"react\";\nimport React, { useEffect, useState } from \"react\";\nimport { scrollIntoViewWithOffset } from \"./scrollIntoViewWithOffset\";\nimport type { ScrollType } from \"./index\";\n\nexport interface AutoScrollProviderProps {\n /** Return the element to scroll to based on the registered types */\n getElementToScrollTo: (\n scrollableElements: Map<ScrollType, Set<string>>,\n ) => string;\n /** Optional function to get container element, which is used for calculating offset (default: document.body) */\n getBaseElement: () => HTMLElement | undefined | null;\n /** Additional offset to be used (default: 0) */\n offset: number;\n}\n\nexport interface RegisterData {\n /** when to scroll to the target */\n type: ScrollType;\n\n /** the html id to scroll to */\n ref: string;\n}\n\nexport type ScrollFunction = (registerData: RegisterData) => void;\n\nexport const AutoScrollManagerContext = React.createContext<{\n /** function to register a scroll target */\n register: ScrollFunction;\n}>({ register: () => {} });\n\n/** hook to register as a scroll target */\nexport const useRegisterAsScrollable = (): ScrollFunction => {\n const { register } = React.useContext(AutoScrollManagerContext);\n\n return register;\n};\n\n/** Component to handle scrolling */\nexport const AutoScrollProvider = ({\n getElementToScrollTo,\n getBaseElement,\n offset,\n children,\n}: PropsWithChildren<AutoScrollProviderProps>) => {\n // Tracker for what elements are registered to be scroll targets\n // Key is the type (initial, validation, appear)\n // Value is a set of target ids\n const [scrollableMap, setScrollableMap] = useState<\n Map<ScrollType, Set<string>>\n >(new Map());\n\n /** Add a new entry as a scroll target */\n const updateScrollableMap = (key: ScrollType, value: string) => {\n setScrollableMap((prev) => {\n const nm = new Map(prev);\n\n if (!nm.get(key)) {\n nm.set(key, new Set());\n }\n\n nm.get(key)?.add(value);\n\n return nm;\n });\n };\n\n /** register a new scroll target */\n const register: ScrollFunction = (data) => {\n updateScrollableMap(data.type, data.ref);\n };\n\n useEffect(() => {\n const node = document.getElementById(getElementToScrollTo(scrollableMap));\n\n if (node) {\n scrollIntoViewWithOffset(node, getBaseElement() || document.body, offset);\n }\n });\n\n return (\n <AutoScrollManagerContext.Provider value={{ register }}>\n {children}\n </AutoScrollManagerContext.Provider>\n );\n};\n","import { scrollTo } from \"seamless-scroll-polyfill\";\n\n/**\n * Scroll to the given element with an offset\n * @param node Element to scroll to\n * @param baseElement Container element used to calculate offset\n * @param offset Additional offset\n */\nexport function scrollIntoViewWithOffset(\n node: HTMLElement,\n baseElement: HTMLElement,\n offset: number,\n) {\n scrollTo(window, {\n behavior: \"smooth\",\n top:\n node.getBoundingClientRect().top -\n baseElement.getBoundingClientRect().top -\n offset,\n });\n}\n","import type { ReactPlayer, ReactPlayerPlugin } from \"@player-ui/react\";\nimport type { Player } from \"@player-ui/react\";\nimport React from \"react\";\nimport { AutoScrollProvider } from \"./hooks\";\n\nexport enum ScrollType {\n ValidationError,\n FirstAppearance,\n Unknown,\n}\n\nexport interface AutoScrollManagerConfig {\n /** Config to auto-scroll on load */\n autoScrollOnLoad?: boolean;\n /** Config to auto-focus on an error */\n autoFocusOnErrorField?: boolean;\n /** Optional function to get container element, which is used for calculating offset (default: document.body) */\n getBaseElement?: () => HTMLElement | undefined | null;\n /** Additional offset to be used (default: 0) */\n offset?: number;\n}\n\n/** A plugin to manage scrolling behavior */\nexport class AutoScrollManagerPlugin implements ReactPlayerPlugin {\n name = \"auto-scroll-manager\";\n\n /** Toggles if we should auto scroll to to the first failed validation on page load */\n private autoScrollOnLoad: boolean;\n\n /** Toggles if we should auto scroll to the first failed validation on navigation failure */\n private autoFocusOnErrorField: boolean;\n\n /** tracks if its the initial page render */\n private initialRender: boolean;\n\n /** tracks if the navigation failed */\n private failedNavigation: boolean;\n\n /** function to return the base of the scrollable area */\n private getBaseElement: () => HTMLElement | undefined | null;\n\n /** static offset */\n private offset: number;\n\n /** map of scroll type to set of ids that are registered under that type */\n private alreadyScrolledTo: Array<string>;\n private scrollFn: (\n scrollableElements: Map<ScrollType, Set<string>>,\n ) => string;\n\n constructor(config: AutoScrollManagerConfig) {\n this.autoScrollOnLoad = config.autoScrollOnLoad ?? false;\n this.autoFocusOnErrorField = config.autoFocusOnErrorField ?? false;\n this.getBaseElement = config.getBaseElement ?? (() => null);\n this.offset = config.offset ?? 0;\n this.initialRender = false;\n this.failedNavigation = false;\n this.alreadyScrolledTo = [];\n this.scrollFn = this.calculateScroll.bind(this);\n }\n\n getFirstScrollableElement(idList: Set<string>, type: ScrollType) {\n const highestElement = {\n id: \"\",\n ypos: 0,\n };\n const ypos = window.scrollY;\n idList.forEach((id) => {\n const element = document.getElementById(id);\n\n // if we are looking at validation errors, make sure the element is invalid\n if (\n type === ScrollType.ValidationError &&\n element?.getAttribute(\"aria-invalid\") === \"false\"\n ) {\n return;\n }\n\n // if we are just looking at elements that just appeared, make sure we haven't\n // scrolled to them before\n if (type === ScrollType.FirstAppearance) {\n if (this.alreadyScrolledTo.indexOf(id) !== -1) {\n return;\n }\n\n this.alreadyScrolledTo.push(id);\n }\n\n const epos = element?.getBoundingClientRect().top;\n if (\n epos !== undefined &&\n (epos + ypos < highestElement.ypos || highestElement.id === \"\")\n ) {\n highestElement.id = id;\n highestElement.ypos = ypos + epos;\n }\n });\n return highestElement.id;\n }\n\n calculateScroll(scrollableElements: Map<ScrollType, Set<string>>) {\n let currentScroll = ScrollType.FirstAppearance;\n if (this.initialRender) {\n if (this.autoScrollOnLoad) {\n currentScroll = ScrollType.ValidationError;\n }\n\n this.initialRender = false;\n } else if (this.failedNavigation) {\n if (this.autoFocusOnErrorField) {\n currentScroll = ScrollType.ValidationError;\n }\n\n this.failedNavigation = false;\n }\n\n const elementList = scrollableElements.get(currentScroll);\n if (elementList) {\n const element = this.getFirstScrollableElement(\n elementList,\n currentScroll,\n );\n return element ?? \"\";\n }\n\n return \"\";\n }\n\n // Hooks into player flow to determine what scroll targets need to be evaluated at specific lifecycle points\n apply(player: Player) {\n player.hooks.flowController.tap(this.name, (fc) => {\n fc.hooks.flow.tap(this.name, (flow) => {\n flow.hooks.transition.tap(this.name, () => {\n // Reset Everything\n this.initialRender = true;\n this.failedNavigation = false;\n this.alreadyScrolledTo = [];\n // Reset scroll position for new view\n window.scroll(0, 0);\n });\n flow.hooks.skipTransition.intercept({\n call: () => {\n this.failedNavigation = true;\n },\n });\n });\n });\n }\n\n applyReact(reactPlayer: ReactPlayer) {\n reactPlayer.hooks.webComponent.tap(this.name, (Comp) => {\n const { scrollFn, getBaseElement, offset } = this;\n\n function AutoScrollManagerComponent() {\n return (\n <AutoScrollProvider\n getElementToScrollTo={scrollFn}\n getBaseElement={getBaseElement}\n offset={offset}\n >\n <Comp />\n </AutoScrollProvider>\n );\n }\n\n return AutoScrollManagerComponent;\n });\n }\n}\n"],"mappings":"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","names":["React","ScrollType","React"]}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/plugins/auto-scroll/react/src/hooks.tsx","../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/plugins/auto-scroll/react/src/scrollIntoViewWithOffset.ts","../../../../../../../../../../../../execroot/_main/bazel-out/k8-fastbuild/bin/plugins/auto-scroll/react/src/plugin.tsx"],"sourcesContent":["import type { PropsWithChildren } from \"react\";\nimport React, { useEffect, useState } from \"react\";\nimport { scrollIntoViewWithOffset } from \"./scrollIntoViewWithOffset\";\nimport type { ScrollType } from \"./index\";\n\nexport interface AutoScrollProviderProps {\n /** Return the element to scroll to based on the registered types */\n getElementToScrollTo: (\n scrollableElements: Map<ScrollType, Set<string>>,\n ) => string;\n /** Optional function to get container element, which is used for calculating offset (default: document.body) */\n getBaseElement: () => HTMLElement | undefined | null;\n /** Additional offset to be used (default: 0) */\n offset: number;\n /** Called on mount with a function that clears all registered scroll targets */\n onMount?: (clearScrollableMap: () => void) => void;\n}\n\nexport interface RegisterData {\n /** when to scroll to the target */\n type: ScrollType;\n\n /** the html id to scroll to */\n ref: string;\n}\n\nexport type ScrollFunction = (registerData: RegisterData) => void;\n\nexport const AutoScrollManagerContext = React.createContext<{\n /** function to register a scroll target */\n register: ScrollFunction;\n}>({ register: () => {} });\n\n/** hook to register as a scroll target */\nexport const useRegisterAsScrollable = (): ScrollFunction => {\n const { register } = React.useContext(AutoScrollManagerContext);\n\n return register;\n};\n\n/** Component to handle scrolling */\nexport const AutoScrollProvider = ({\n getElementToScrollTo,\n getBaseElement,\n offset,\n onMount,\n children,\n}: PropsWithChildren<AutoScrollProviderProps>) => {\n // Tracker for what elements are registered to be scroll targets\n // Key is the type (initial, validation, appear)\n // Value is a set of target ids\n const [scrollableMap, setScrollableMap] = useState<\n Map<ScrollType, Set<string>>\n >(new Map());\n\n useEffect(() => {\n onMount?.(() => setScrollableMap(new Map()));\n }, []);\n\n /** Add a new entry as a scroll target */\n const updateScrollableMap = (key: ScrollType, value: string) => {\n setScrollableMap((prev) => {\n const nm = new Map(prev);\n\n if (!nm.get(key)) {\n nm.set(key, new Set());\n }\n\n nm.get(key)?.add(value);\n\n return nm;\n });\n };\n\n /** register a new scroll target */\n const register: ScrollFunction = (data) => {\n updateScrollableMap(data.type, data.ref);\n };\n\n useEffect(() => {\n const node = document.getElementById(getElementToScrollTo(scrollableMap));\n\n if (node) {\n scrollIntoViewWithOffset(node, getBaseElement() || document.body, offset);\n }\n }, [scrollableMap]);\n\n return (\n <AutoScrollManagerContext.Provider value={{ register }}>\n {children}\n </AutoScrollManagerContext.Provider>\n );\n};\n","import { scrollTo } from \"seamless-scroll-polyfill\";\n\n/**\n * Scroll to the given element with an offset\n * @param node Element to scroll to\n * @param baseElement Container element used to calculate offset\n * @param offset Additional offset\n */\nexport function scrollIntoViewWithOffset(\n node: HTMLElement,\n baseElement: HTMLElement,\n offset: number,\n) {\n scrollTo(window, {\n behavior: \"smooth\",\n top:\n node.getBoundingClientRect().top -\n baseElement.getBoundingClientRect().top -\n offset,\n });\n}\n","import type { ReactPlayer, ReactPlayerPlugin } from \"@player-ui/react\";\nimport type { Player } from \"@player-ui/react\";\nimport React from \"react\";\nimport { AutoScrollProvider } from \"./hooks\";\n\nexport enum ScrollType {\n ValidationError,\n FirstAppearance,\n Unknown,\n}\n\nexport interface AutoScrollManagerConfig {\n /** Config to auto-scroll on load */\n autoScrollOnLoad?: boolean;\n /** Config to auto-focus on an error */\n autoFocusOnErrorField?: boolean;\n /** Optional function to get container element, which is used for calculating offset (default: document.body) */\n getBaseElement?: () => HTMLElement | undefined | null;\n /** Additional offset to be used (default: 0) */\n offset?: number;\n}\n\n/** A plugin to manage scrolling behavior */\nexport class AutoScrollManagerPlugin implements ReactPlayerPlugin {\n name = \"auto-scroll-manager\";\n\n /** Toggles if we should auto scroll to to the first failed validation on page load */\n private autoScrollOnLoad: boolean;\n\n /** Toggles if we should auto scroll to the first failed validation on navigation failure */\n private autoFocusOnErrorField: boolean;\n\n /** tracks if its the initial page render */\n private initialRender: boolean;\n\n /** tracks if the navigation failed */\n private failedNavigation: boolean;\n\n /** function to return the base of the scrollable area */\n private getBaseElement: () => HTMLElement | undefined | null;\n\n /** static offset */\n private offset: number;\n\n /** clears scrollableMap in AutoScrollProvider — set when the provider mounts */\n private clearScrollableMap: () => void;\n\n /** map of scroll type to set of ids that are registered under that type */\n private alreadyScrolledTo: Array<string>;\n private scrollFn: (\n scrollableElements: Map<ScrollType, Set<string>>,\n ) => string;\n\n constructor(config: AutoScrollManagerConfig) {\n this.autoScrollOnLoad = config.autoScrollOnLoad ?? false;\n this.autoFocusOnErrorField = config.autoFocusOnErrorField ?? false;\n this.getBaseElement = config.getBaseElement ?? (() => null);\n this.offset = config.offset ?? 0;\n this.initialRender = false;\n this.failedNavigation = false;\n this.alreadyScrolledTo = [];\n this.clearScrollableMap = () => {};\n this.scrollFn = this.calculateScroll.bind(this);\n }\n\n getFirstScrollableElement(idList: Set<string>, type: ScrollType): string {\n const highestElement = {\n id: \"\",\n ypos: 0,\n };\n const ypos = window.scrollY;\n idList.forEach((id) => {\n const element = document.getElementById(id);\n\n // if we are looking at validation errors, make sure the element is invalid\n if (\n type === ScrollType.ValidationError &&\n element?.getAttribute(\"aria-invalid\") === \"false\"\n ) {\n return;\n }\n\n // if we are just looking at elements that just appeared, make sure we haven't\n // scrolled to them before\n if (type === ScrollType.FirstAppearance) {\n if (this.alreadyScrolledTo.indexOf(id) !== -1) {\n return;\n }\n\n this.alreadyScrolledTo.push(id);\n }\n\n const epos = element?.getBoundingClientRect().top;\n if (\n epos !== undefined &&\n (epos + ypos < highestElement.ypos || highestElement.id === \"\")\n ) {\n highestElement.id = id;\n highestElement.ypos = ypos + epos;\n }\n });\n return highestElement.id;\n }\n\n calculateScroll(scrollableElements: Map<ScrollType, Set<string>>): string {\n let currentScroll = ScrollType.FirstAppearance;\n if (this.initialRender) {\n if (this.autoScrollOnLoad) {\n currentScroll = ScrollType.ValidationError;\n }\n\n this.initialRender = false;\n } else if (this.failedNavigation) {\n if (this.autoFocusOnErrorField) {\n currentScroll = ScrollType.ValidationError;\n }\n\n this.failedNavigation = false;\n }\n\n const elementList = scrollableElements.get(currentScroll);\n if (elementList) {\n const element = this.getFirstScrollableElement(\n elementList,\n currentScroll,\n );\n return element ?? \"\";\n }\n\n return \"\";\n }\n\n // Hooks into player flow to determine what scroll targets need to be evaluated at specific lifecycle points\n apply(player: Player): void {\n player.hooks.flowController.tap(this.name, (fc) => {\n fc.hooks.flow.tap(this.name, (flow) => {\n flow.hooks.transition.tap(this.name, () => {\n // Reset Everything\n this.initialRender = true;\n this.failedNavigation = false;\n this.alreadyScrolledTo = [];\n this.clearScrollableMap();\n // Reset scroll position for new view\n window.scroll(0, 0);\n });\n flow.hooks.skipTransition.intercept({\n call: () => {\n this.failedNavigation = true;\n },\n });\n });\n });\n }\n\n applyReact(reactPlayer: ReactPlayer): void {\n reactPlayer.hooks.webComponent.tap(this.name, (Comp) => {\n const { scrollFn, getBaseElement, offset } = this;\n const setScrollableMapClearer = (clear: () => void) => {\n this.clearScrollableMap = clear;\n };\n\n function AutoScrollManagerComponent() {\n return (\n <AutoScrollProvider\n getElementToScrollTo={scrollFn}\n getBaseElement={getBaseElement}\n offset={offset}\n onMount={(clear) => {\n setScrollableMapClearer(clear);\n }}\n >\n <Comp />\n </AutoScrollProvider>\n );\n }\n\n return AutoScrollManagerComponent;\n });\n }\n}\n"],"mappings":"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","names":["React","ScrollType","React"]}
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"types"
|
|
7
7
|
],
|
|
8
8
|
"name": "@player-ui/auto-scroll-manager-plugin-react",
|
|
9
|
-
"version": "0.15.1-next.
|
|
9
|
+
"version": "0.15.1-next.4",
|
|
10
10
|
"main": "dist/cjs/index.cjs",
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@player-ui/types": "workspace:*",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"@player-ui/reference-assets-plugin": "workspace:*"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
|
-
"@player-ui/react": "0.15.1-next.
|
|
20
|
+
"@player-ui/react": "0.15.1-next.4",
|
|
21
21
|
"react": "^18.2.0",
|
|
22
22
|
"@types/react": "^18.2.39"
|
|
23
23
|
},
|
|
@@ -177,24 +177,24 @@ describe("auto-scroll plugin", () => {
|
|
|
177
177
|
|
|
178
178
|
const action = await findByRole(container, "button");
|
|
179
179
|
act(() => action.click());
|
|
180
|
-
await
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
180
|
+
await waitFor(() =>
|
|
181
|
+
expect(scrollIntoViewWithOffset).toBeCalledWith(
|
|
182
|
+
expect.anything(),
|
|
183
|
+
expect.objectContaining({ id: "view" }),
|
|
184
|
+
40,
|
|
185
|
+
),
|
|
186
186
|
);
|
|
187
187
|
|
|
188
188
|
// Mock the case where the base element can't be found, so document.body is used as a fallback
|
|
189
189
|
getBaseElementMock.mockReturnValue(null);
|
|
190
190
|
|
|
191
191
|
act(() => action.click());
|
|
192
|
-
await
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
192
|
+
await waitFor(() =>
|
|
193
|
+
expect(scrollIntoViewWithOffset).toHaveBeenLastCalledWith(
|
|
194
|
+
expect.anything(),
|
|
195
|
+
document.body,
|
|
196
|
+
40,
|
|
197
|
+
),
|
|
198
198
|
);
|
|
199
199
|
});
|
|
200
200
|
|
|
@@ -233,12 +233,12 @@ describe("auto-scroll plugin", () => {
|
|
|
233
233
|
|
|
234
234
|
const action = await findByRole(container, "button");
|
|
235
235
|
act(() => action.click());
|
|
236
|
-
await
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
236
|
+
await waitFor(() =>
|
|
237
|
+
expect(scrollIntoViewWithOffset).toBeCalledWith(
|
|
238
|
+
expect.anything(),
|
|
239
|
+
document.body,
|
|
240
|
+
0,
|
|
241
|
+
),
|
|
242
242
|
);
|
|
243
243
|
});
|
|
244
244
|
|
|
@@ -280,6 +280,113 @@ describe("auto-scroll plugin", () => {
|
|
|
280
280
|
});
|
|
281
281
|
});
|
|
282
282
|
|
|
283
|
+
const twoViewFlow = {
|
|
284
|
+
id: "two-view-flow",
|
|
285
|
+
views: [
|
|
286
|
+
{
|
|
287
|
+
id: "view-1",
|
|
288
|
+
type: "info",
|
|
289
|
+
primaryInfo: {
|
|
290
|
+
asset: { id: "info-1", type: "input", binding: "person.name" },
|
|
291
|
+
},
|
|
292
|
+
actions: [
|
|
293
|
+
{ asset: { id: "next-action", type: "action", value: "Next" } },
|
|
294
|
+
],
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: "view-2",
|
|
298
|
+
type: "info",
|
|
299
|
+
primaryInfo: {
|
|
300
|
+
asset: { id: "info-2", type: "input", binding: "person.age" },
|
|
301
|
+
},
|
|
302
|
+
actions: [],
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
schema: {},
|
|
306
|
+
data: { person: { name: "sam", age: "30" } },
|
|
307
|
+
navigation: {
|
|
308
|
+
BEGIN: "FLOW_Start",
|
|
309
|
+
FLOW_Start: {
|
|
310
|
+
startState: "VIEW_1",
|
|
311
|
+
VIEW_1: {
|
|
312
|
+
state_type: "VIEW",
|
|
313
|
+
ref: "view-1",
|
|
314
|
+
transitions: { "*": "VIEW_2" },
|
|
315
|
+
},
|
|
316
|
+
VIEW_2: {
|
|
317
|
+
state_type: "VIEW",
|
|
318
|
+
ref: "view-2",
|
|
319
|
+
transitions: { "*": "END_Done" },
|
|
320
|
+
},
|
|
321
|
+
END_Done: { state_type: "END", outcome: "done" },
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
describe("scroll reset on view transition", () => {
|
|
327
|
+
test("does not scroll to page 1 elements after view transition (stale scrollableMap)", async () => {
|
|
328
|
+
vitest.clearAllMocks();
|
|
329
|
+
|
|
330
|
+
const withFirstAppearance = (Component: ComponentType<any>) => {
|
|
331
|
+
const Scrollable = (props: any) => {
|
|
332
|
+
const register = useRegisterAsScrollable();
|
|
333
|
+
useLayoutEffect(() => {
|
|
334
|
+
register({ type: ScrollType.FirstAppearance, ref: props.id });
|
|
335
|
+
}, []);
|
|
336
|
+
return <Component {...props} />;
|
|
337
|
+
};
|
|
338
|
+
return Scrollable;
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// only "info-1" (page 1) is findable. if the map isn't cleared on transition,
|
|
342
|
+
// "info-1" remains a scroll target and scrollIntoViewWithOffset gets called.
|
|
343
|
+
vitest.spyOn(document, "getElementById").mockImplementation((id) => {
|
|
344
|
+
if (id === "info-1") {
|
|
345
|
+
return { getBoundingClientRect: () => ({ top: 50 }) } as any;
|
|
346
|
+
}
|
|
347
|
+
return null;
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
const wp = new ReactPlayer({
|
|
351
|
+
plugins: [
|
|
352
|
+
new AssetTransformPlugin([
|
|
353
|
+
[{ type: "action" }, actionTransform],
|
|
354
|
+
[{ type: "input" }, inputTransform],
|
|
355
|
+
[{ type: "info" }, infoTransform],
|
|
356
|
+
]),
|
|
357
|
+
new CommonTypesPlugin(),
|
|
358
|
+
new AutoScrollManagerPlugin({}),
|
|
359
|
+
],
|
|
360
|
+
});
|
|
361
|
+
wp.assetRegistry.set({ type: "info" }, Info);
|
|
362
|
+
wp.assetRegistry.set({ type: "action" }, Action);
|
|
363
|
+
wp.assetRegistry.set({ type: "input" }, withFirstAppearance(Input));
|
|
364
|
+
|
|
365
|
+
wp.start(twoViewFlow as any);
|
|
366
|
+
|
|
367
|
+
const { container } = render(
|
|
368
|
+
<div>
|
|
369
|
+
<React.Suspense fallback="loading...">
|
|
370
|
+
<wp.Component />
|
|
371
|
+
</React.Suspense>
|
|
372
|
+
</div>,
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// wait for page 1 to settle, then reset scroll call history so we only assert on page 2
|
|
376
|
+
await act(() => waitFor(() => {}));
|
|
377
|
+
(scrollIntoViewWithOffset as ReturnType<typeof vitest.fn>).mockClear();
|
|
378
|
+
|
|
379
|
+
// navigate to page 2
|
|
380
|
+
const action = await findByRole(container, "button");
|
|
381
|
+
act(() => action.click());
|
|
382
|
+
await act(() => waitFor(() => {}));
|
|
383
|
+
|
|
384
|
+
// map was cleared on transition — "info-1" is no longer a scroll target
|
|
385
|
+
expect(scrollIntoViewWithOffset).not.toHaveBeenCalled();
|
|
386
|
+
vitest.restoreAllMocks();
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
283
390
|
describe("getFirstScrollableElement unit tests", () => {
|
|
284
391
|
const defineGetElementId = (bcrValues: any[]) => {
|
|
285
392
|
return (idIn: string): HTMLElement | null => {
|
package/src/hooks.tsx
CHANGED
|
@@ -12,6 +12,8 @@ export interface AutoScrollProviderProps {
|
|
|
12
12
|
getBaseElement: () => HTMLElement | undefined | null;
|
|
13
13
|
/** Additional offset to be used (default: 0) */
|
|
14
14
|
offset: number;
|
|
15
|
+
/** Called on mount with a function that clears all registered scroll targets */
|
|
16
|
+
onMount?: (clearScrollableMap: () => void) => void;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export interface RegisterData {
|
|
@@ -41,6 +43,7 @@ export const AutoScrollProvider = ({
|
|
|
41
43
|
getElementToScrollTo,
|
|
42
44
|
getBaseElement,
|
|
43
45
|
offset,
|
|
46
|
+
onMount,
|
|
44
47
|
children,
|
|
45
48
|
}: PropsWithChildren<AutoScrollProviderProps>) => {
|
|
46
49
|
// Tracker for what elements are registered to be scroll targets
|
|
@@ -50,6 +53,10 @@ export const AutoScrollProvider = ({
|
|
|
50
53
|
Map<ScrollType, Set<string>>
|
|
51
54
|
>(new Map());
|
|
52
55
|
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
onMount?.(() => setScrollableMap(new Map()));
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
53
60
|
/** Add a new entry as a scroll target */
|
|
54
61
|
const updateScrollableMap = (key: ScrollType, value: string) => {
|
|
55
62
|
setScrollableMap((prev) => {
|
|
@@ -76,7 +83,7 @@ export const AutoScrollProvider = ({
|
|
|
76
83
|
if (node) {
|
|
77
84
|
scrollIntoViewWithOffset(node, getBaseElement() || document.body, offset);
|
|
78
85
|
}
|
|
79
|
-
});
|
|
86
|
+
}, [scrollableMap]);
|
|
80
87
|
|
|
81
88
|
return (
|
|
82
89
|
<AutoScrollManagerContext.Provider value={{ register }}>
|
package/src/plugin.tsx
CHANGED
|
@@ -42,6 +42,9 @@ export class AutoScrollManagerPlugin implements ReactPlayerPlugin {
|
|
|
42
42
|
/** static offset */
|
|
43
43
|
private offset: number;
|
|
44
44
|
|
|
45
|
+
/** clears scrollableMap in AutoScrollProvider — set when the provider mounts */
|
|
46
|
+
private clearScrollableMap: () => void;
|
|
47
|
+
|
|
45
48
|
/** map of scroll type to set of ids that are registered under that type */
|
|
46
49
|
private alreadyScrolledTo: Array<string>;
|
|
47
50
|
private scrollFn: (
|
|
@@ -56,10 +59,11 @@ export class AutoScrollManagerPlugin implements ReactPlayerPlugin {
|
|
|
56
59
|
this.initialRender = false;
|
|
57
60
|
this.failedNavigation = false;
|
|
58
61
|
this.alreadyScrolledTo = [];
|
|
62
|
+
this.clearScrollableMap = () => {};
|
|
59
63
|
this.scrollFn = this.calculateScroll.bind(this);
|
|
60
64
|
}
|
|
61
65
|
|
|
62
|
-
getFirstScrollableElement(idList: Set<string>, type: ScrollType) {
|
|
66
|
+
getFirstScrollableElement(idList: Set<string>, type: ScrollType): string {
|
|
63
67
|
const highestElement = {
|
|
64
68
|
id: "",
|
|
65
69
|
ypos: 0,
|
|
@@ -98,7 +102,7 @@ export class AutoScrollManagerPlugin implements ReactPlayerPlugin {
|
|
|
98
102
|
return highestElement.id;
|
|
99
103
|
}
|
|
100
104
|
|
|
101
|
-
calculateScroll(scrollableElements: Map<ScrollType, Set<string>>) {
|
|
105
|
+
calculateScroll(scrollableElements: Map<ScrollType, Set<string>>): string {
|
|
102
106
|
let currentScroll = ScrollType.FirstAppearance;
|
|
103
107
|
if (this.initialRender) {
|
|
104
108
|
if (this.autoScrollOnLoad) {
|
|
@@ -127,7 +131,7 @@ export class AutoScrollManagerPlugin implements ReactPlayerPlugin {
|
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
// Hooks into player flow to determine what scroll targets need to be evaluated at specific lifecycle points
|
|
130
|
-
apply(player: Player) {
|
|
134
|
+
apply(player: Player): void {
|
|
131
135
|
player.hooks.flowController.tap(this.name, (fc) => {
|
|
132
136
|
fc.hooks.flow.tap(this.name, (flow) => {
|
|
133
137
|
flow.hooks.transition.tap(this.name, () => {
|
|
@@ -135,6 +139,7 @@ export class AutoScrollManagerPlugin implements ReactPlayerPlugin {
|
|
|
135
139
|
this.initialRender = true;
|
|
136
140
|
this.failedNavigation = false;
|
|
137
141
|
this.alreadyScrolledTo = [];
|
|
142
|
+
this.clearScrollableMap();
|
|
138
143
|
// Reset scroll position for new view
|
|
139
144
|
window.scroll(0, 0);
|
|
140
145
|
});
|
|
@@ -147,9 +152,12 @@ export class AutoScrollManagerPlugin implements ReactPlayerPlugin {
|
|
|
147
152
|
});
|
|
148
153
|
}
|
|
149
154
|
|
|
150
|
-
applyReact(reactPlayer: ReactPlayer) {
|
|
155
|
+
applyReact(reactPlayer: ReactPlayer): void {
|
|
151
156
|
reactPlayer.hooks.webComponent.tap(this.name, (Comp) => {
|
|
152
157
|
const { scrollFn, getBaseElement, offset } = this;
|
|
158
|
+
const setScrollableMapClearer = (clear: () => void) => {
|
|
159
|
+
this.clearScrollableMap = clear;
|
|
160
|
+
};
|
|
153
161
|
|
|
154
162
|
function AutoScrollManagerComponent() {
|
|
155
163
|
return (
|
|
@@ -157,6 +165,9 @@ export class AutoScrollManagerPlugin implements ReactPlayerPlugin {
|
|
|
157
165
|
getElementToScrollTo={scrollFn}
|
|
158
166
|
getBaseElement={getBaseElement}
|
|
159
167
|
offset={offset}
|
|
168
|
+
onMount={(clear) => {
|
|
169
|
+
setScrollableMapClearer(clear);
|
|
170
|
+
}}
|
|
160
171
|
>
|
|
161
172
|
<Comp />
|
|
162
173
|
</AutoScrollProvider>
|
package/types/hooks.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export interface AutoScrollProviderProps {
|
|
|
8
8
|
getBaseElement: () => HTMLElement | undefined | null;
|
|
9
9
|
/** Additional offset to be used (default: 0) */
|
|
10
10
|
offset: number;
|
|
11
|
+
/** Called on mount with a function that clears all registered scroll targets */
|
|
12
|
+
onMount?: (clearScrollableMap: () => void) => void;
|
|
11
13
|
}
|
|
12
14
|
export interface RegisterData {
|
|
13
15
|
/** when to scroll to the target */
|
|
@@ -23,5 +25,5 @@ export declare const AutoScrollManagerContext: React.Context<{
|
|
|
23
25
|
/** hook to register as a scroll target */
|
|
24
26
|
export declare const useRegisterAsScrollable: () => ScrollFunction;
|
|
25
27
|
/** Component to handle scrolling */
|
|
26
|
-
export declare const AutoScrollProvider: ({ getElementToScrollTo, getBaseElement, offset, children, }: PropsWithChildren<AutoScrollProviderProps>) => React.JSX.Element;
|
|
28
|
+
export declare const AutoScrollProvider: ({ getElementToScrollTo, getBaseElement, offset, onMount, children, }: PropsWithChildren<AutoScrollProviderProps>) => React.JSX.Element;
|
|
27
29
|
//# sourceMappingURL=hooks.d.ts.map
|
package/types/plugin.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export declare class AutoScrollManagerPlugin implements ReactPlayerPlugin {
|
|
|
30
30
|
private getBaseElement;
|
|
31
31
|
/** static offset */
|
|
32
32
|
private offset;
|
|
33
|
+
/** clears scrollableMap in AutoScrollProvider — set when the provider mounts */
|
|
34
|
+
private clearScrollableMap;
|
|
33
35
|
/** map of scroll type to set of ids that are registered under that type */
|
|
34
36
|
private alreadyScrolledTo;
|
|
35
37
|
private scrollFn;
|