-
Notifications
You must be signed in to change notification settings - Fork 3.2k
/
scroller.ts
149 lines (116 loc) · 4.01 KB
/
scroller.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
/**
container.clientHeight:
- container visible area height ("viewport")
- includes padding, but not margin or border
container.scrollTop:
- container scroll position:
container.scrollHeight:
- total container height (visible + not visible)
element.clientHeight:
- element height
- includes padding, but not margin or border
element.offsetTop:
- element distance from top of container
*/
export type UserScrollCallback = () => void
const PADDING = 100
const SCROLL_THRESHOLD_MS = 50
export class Scroller {
private _container: Element | null = null
private _userScrollCount = 0
private _userScroll = true
private _countUserScrollsTimeout?: number
private _userScrollThresholdMs = SCROLL_THRESHOLD_MS
setContainer (container: Element, onUserScroll?: UserScrollCallback) {
this._container = container
this._userScroll = true
this._userScrollCount = 0
this._listenToScrolls(onUserScroll)
}
_listenToScrolls (onUserScroll?: UserScrollCallback) {
if (!this._container) return
this._container.addEventListener('scroll', () => {
if (!this._userScroll) {
// programmatic scroll
this._userScroll = true
return
}
// there can be false positives for user scrolls, so make sure we get 3
// or more scroll events within 50ms to count it as a user intending to scroll
this._userScrollCount++
if (this._userScrollCount >= 3) {
if (onUserScroll) {
onUserScroll()
}
clearTimeout(this._countUserScrollsTimeout)
this._countUserScrollsTimeout = undefined
this._userScrollCount = 0
return
}
if (this._countUserScrollsTimeout) return
this._countUserScrollsTimeout = window.setTimeout(() => {
this._countUserScrollsTimeout = undefined
this._userScrollCount = 0
}, this._userScrollThresholdMs)
})
}
scrollIntoView (element: HTMLElement) {
if (!this._container) {
throw new Error('A container must be set on the scroller with `scroller.setContainer(container)` before trying to scroll an element into view')
}
if (this._isFullyVisible(element)) {
return
}
// aim to scroll just into view, so that the bottom of the element
// is just above the bottom of the container
let scrollTopGoal = this._aboveBottom(element)
// can't have a negative scroll, so put it to the top
if (scrollTopGoal < 0) {
scrollTopGoal = 0
}
this._userScroll = false
this._container.scrollTop = scrollTopGoal
}
_isFullyVisible (element: HTMLElement) {
if (!this._container) return false
return element.offsetTop - this._container.scrollTop > 0
&& this._container.scrollTop > this._aboveBottom(element)
}
_aboveBottom (element: HTMLElement) {
// add padding, since commands expanding and collapsing can mess with
// the offset, causing the running command to be half cut off
// https://github.com/cypress-io/cypress/issues/228
const containerHeight = this._container ? this._container.clientHeight : 0
return element.offsetTop + element.clientHeight - containerHeight + PADDING
}
getScrollTop () {
return this._container ? this._container.scrollTop : 0
}
setScrollTop (scrollTop?: number | null) {
if (this._container && scrollTop != null) {
this._container.scrollTop = scrollTop
}
}
scrollToEnd () {
if (!this._container) return
this.setScrollTop(this._container.scrollHeight - this._container.clientHeight)
}
// for testing purposes
__reset () {
this._container = null
this._userScroll = true
this._userScrollCount = 0
clearTimeout(this._countUserScrollsTimeout)
this._countUserScrollsTimeout = undefined
this._userScrollThresholdMs = SCROLL_THRESHOLD_MS
}
__setScrollThresholdMs (ms: number) {
const isCypressInCypress = document.defaultView !== top
// only allow this to be set in testing
if (!isCypressInCypress) {
return
}
this._userScrollThresholdMs = ms
}
}
export default new Scroller()