Handle scrolling of the dummy scrollbars directly

This commit is contained in:
Nathan Sobo 2017-03-21 10:10:26 -06:00 committed by Antonio Scandurra
parent 8720dbc862
commit 0999d0bf02
2 changed files with 141 additions and 39 deletions

View File

@ -38,8 +38,7 @@ describe('TextEditorComponent', () => {
expect(element.querySelectorAll('.line-number').length).toBe(9)
expect(element.querySelectorAll('.line').length).toBe(9)
component.setScrollTop(5 * component.getLineHeight())
await component.getNextUpdatePromise()
await setScrollTop(component, 5 * component.getLineHeight())
// After scrolling down beyond > 3 rows, the order of line numbers and lines
// in the DOM is a bit weird because the first tile is recycled to the bottom
@ -59,8 +58,7 @@ describe('TextEditorComponent', () => {
editor.lineTextForScreenRow(8)
])
component.setScrollTop(2.5 * component.getLineHeight())
await component.getNextUpdatePromise()
await setScrollTop(component, 2.5 * component.getLineHeight())
expect(Array.from(element.querySelectorAll('.line-number')).map(element => element.textContent.trim())).toEqual([
'1', '2', '3', '4', '5', '6', '7', '8', '9'
])
@ -95,8 +93,7 @@ describe('TextEditorComponent', () => {
await setEditorHeightInLines(component, 6)
// scroll to end
component.setScrollTop(scrollContainer.scrollHeight - scrollContainer.clientHeight)
await component.getNextUpdatePromise()
await setScrollTop(component, scrollContainer.scrollHeight - scrollContainer.clientHeight)
expect(component.getFirstVisibleRow()).toBe(editor.getScreenLineCount() - 3)
editor.update({scrollPastEnd: false})
@ -106,8 +103,7 @@ describe('TextEditorComponent', () => {
// Always allows at least 3 lines worth of overscroll if the editor is short
await setEditorHeightInLines(component, 2)
await editor.update({scrollPastEnd: true})
component.setScrollTop(scrollContainer.scrollHeight - scrollContainer.clientHeight)
await component.getNextUpdatePromise()
await setScrollTop(component, scrollContainer.scrollHeight - scrollContainer.clientHeight)
expect(component.getFirstVisibleRow()).toBe(editor.getScreenLineCount() + 1)
})
@ -125,10 +121,73 @@ describe('TextEditorComponent', () => {
expect(gutterElement.firstChild.style.contain).toBe('strict')
})
it('renders dummy vertical and horizontal scrollbars when content overflows', async () => {
const {component, element, editor} = buildComponent({height: 100, width: 100})
const verticalScrollbar = component.refs.verticalScrollbar.element
const horizontalScrollbar = component.refs.horizontalScrollbar.element
expect(verticalScrollbar.scrollHeight).toBe(component.getContentHeight())
expect(horizontalScrollbar.scrollWidth).toBe(component.getContentWidth())
expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(0)
expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(0)
expect(verticalScrollbar.style.bottom).toBe(getVerticalScrollbarWidth(component) + 'px')
expect(horizontalScrollbar.style.right).toBe(getHorizontalScrollbarHeight(component) + 'px')
expect(component.refs.scrollbarCorner).toBeDefined()
setScrollTop(component, 100)
await setScrollLeft(component, 100)
expect(verticalScrollbar.scrollTop).toBe(100)
expect(horizontalScrollbar.scrollLeft).toBe(100)
verticalScrollbar.scrollTop = 120
horizontalScrollbar.scrollLeft = 120
await component.getNextUpdatePromise()
expect(component.getScrollTop()).toBe(120)
expect(component.getScrollLeft()).toBe(120)
editor.setText('a\n'.repeat(15))
await component.getNextUpdatePromise()
expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(0)
expect(getHorizontalScrollbarHeight(component)).toBe(0)
expect(verticalScrollbar.style.bottom).toBe('0px')
expect(component.refs.scrollbarCorner).toBeUndefined()
editor.setText('a'.repeat(100))
await component.getNextUpdatePromise()
expect(getVerticalScrollbarWidth(component)).toBe(0)
expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(0)
expect(horizontalScrollbar.style.right).toBe('0px')
expect(component.refs.scrollbarCorner).toBeUndefined()
editor.setText('')
await component.getNextUpdatePromise()
expect(getVerticalScrollbarWidth(component)).toBe(0)
expect(getHorizontalScrollbarHeight(component)).toBe(0)
expect(component.refs.scrollbarCorner).toBeUndefined()
})
it('updates the bottom/right of dummy scrollbars and client height/width measurements when scrollbar styles change', async () => {
const {component, element, editor} = buildComponent({height: 100, width: 100})
expect(getHorizontalScrollbarHeight(component)).toBeGreaterThan(10)
expect(getVerticalScrollbarWidth(component)).toBeGreaterThan(10)
const style = document.createElement('style')
style.textContent = '::-webkit-scrollbar { height: 10px; width: 10px; }'
jasmine.attachToDOM(style)
TextEditor.didUpdateScrollbarStyles()
await component.getNextUpdatePromise()
expect(getHorizontalScrollbarHeight(component)).toBe(10)
expect(getVerticalScrollbarWidth(component)).toBe(10)
expect(component.refs.horizontalScrollbar.element.style.right).toBe('10px')
expect(component.refs.verticalScrollbar.element.style.bottom).toBe('10px')
expect(component.getScrollContainerClientHeight()).toBe(100 - 10)
expect(component.getScrollContainerClientWidth()).toBe(100 - component.getGutterContainerWidth() - 10)
})
it('renders cursors within the visible row range', async () => {
const {component, element, editor} = buildComponent({height: 40, rowsPerTile: 2})
component.setScrollTop(100)
await component.getNextUpdatePromise()
await setScrollTop(component, 100)
expect(component.getRenderedStartRow()).toBe(4)
expect(component.getRenderedEndRow()).toBe(10)
@ -171,9 +230,8 @@ describe('TextEditorComponent', () => {
it('places the hidden input element at the location of the last cursor if it is visible', async () => {
const {component, element, editor} = buildComponent({height: 60, width: 120, rowsPerTile: 2})
const {hiddenInput} = component.refs
component.setScrollTop(100)
component.setScrollLeft(40)
await component.getNextUpdatePromise()
setScrollTop(component, 100)
await setScrollLeft(component, 40)
expect(component.getRenderedStartRow()).toBe(4)
expect(component.getRenderedEndRow()).toBe(12)
@ -718,8 +776,7 @@ describe('TextEditorComponent', () => {
)
// Don't flash on next update if another flash wasn't requested
component.setScrollTop(100)
await component.getNextUpdatePromise()
await setScrollTop(component, 100)
expect(highlights[0].classList.contains('b')).toBe(false)
expect(highlights[1].classList.contains('b')).toBe(false)
@ -1184,9 +1241,8 @@ describe('TextEditorComponent', () => {
const maxScrollTop = component.getMaxScrollTop()
const maxScrollLeft = component.getMaxScrollLeft()
component.setScrollTop(maxScrollTop)
component.setScrollLeft(maxScrollLeft)
await component.getNextUpdatePromise()
setScrollTop(component, maxScrollTop)
await setScrollLeft(component, maxScrollLeft)
didDrag({clientX: 199, clientY: 199})
didDrag({clientX: 199, clientY: 199})
@ -1413,9 +1469,8 @@ describe('TextEditorComponent', () => {
const maxScrollTop = component.getMaxScrollTop()
const maxScrollLeft = component.getMaxScrollLeft()
component.setScrollTop(maxScrollTop)
component.setScrollLeft(maxScrollLeft)
await component.getNextUpdatePromise()
setScrollTop(component, maxScrollTop)
await setScrollLeft(component, maxScrollLeft)
didDrag({clientX: 199, clientY: 199})
didDrag({clientX: 199, clientY: 199})
@ -1518,6 +1573,28 @@ function textNodesForScreenRow (component, row) {
return component.textNodesByScreenLineId.get(screenLine.id)
}
function setScrollTop (component, scrollTop) {
component.setScrollTop(scrollTop)
component.scheduleUpdate()
return component.getNextUpdatePromise()
}
function setScrollLeft (component, scrollTop) {
component.setScrollLeft(scrollTop)
component.scheduleUpdate()
return component.getNextUpdatePromise()
}
function getHorizontalScrollbarHeight (component) {
const element = component.refs.horizontalScrollbar.element
return element.offsetHeight - element.clientHeight
}
function getVerticalScrollbarWidth (component) {
const element = component.refs.verticalScrollbar.element
return element.offsetWidth - element.clientWidth
}
function assertDocumentFocused () {
if (!document.hasFocus()) {
throw new Error('The document needs to be focused to run this test')

View File

@ -55,9 +55,12 @@ class TextEditorComponent {
this.horizontalPixelPositionsByScreenLineId = new Map() // Values are maps from column to horiontal pixel positions
this.lineNodesByScreenLineId = new Map()
this.textNodesByScreenLineId = new Map()
this.didScrollDummyScrollbar = this.didScrollDummyScrollbar.bind(this)
this.scrollbarsVisible = true
this.refreshScrollbarStyling = false
this.pendingAutoscroll = null
this.scrollTopPending = false
this.scrollLeftPending = false
this.scrollTop = 0
this.scrollLeft = 0
this.previousScrollWidth = 0
@ -134,7 +137,8 @@ class TextEditorComponent {
etch.updateSync(this)
this.currentFrameLineNumberGutterProps = null
this.scrollTopPending = false
this.scrollLeftPending = false
if (this.refreshScrollbarStyling) {
this.measureScrollbarDimensions()
this.refreshScrollbarStyling = false
@ -529,11 +533,13 @@ class TextEditorComponent {
$(DummyScrollbarComponent, {
ref: 'verticalScrollbar',
orientation: 'vertical',
didScroll: this.didScrollDummyScrollbar,
scrollHeight, scrollTop, horizontalScrollbarHeight, forceScrollbarVisible
}),
$(DummyScrollbarComponent, {
ref: 'horizontalScrollbar',
orientation: 'horizontal',
didScroll: this.didScrollDummyScrollbar,
scrollWidth, scrollLeft, verticalScrollbarWidth, forceScrollbarVisible
})
]
@ -543,6 +549,7 @@ class TextEditorComponent {
if (verticalScrollbarWidth > 0 && horizontalScrollbarHeight > 0) {
elements.push($.div(
{
ref: 'scrollbarCorner',
style: {
position: 'absolute',
height: '20px',
@ -869,6 +876,18 @@ class TextEditorComponent {
}
}
didScrollDummyScrollbar () {
let scrollTopChanged = false
let scrollLeftChanged = false
if (!this.scrollTopPending) {
scrollTopChanged = this.setScrollTop(this.refs.verticalScrollbar.element.scrollTop)
}
if (!this.scrollLeftPending) {
scrollLeftChanged = this.setScrollLeft(this.refs.horizontalScrollbar.element.scrollLeft)
}
if (scrollTopChanged || scrollLeftChanged) this.updateSync()
}
didUpdateScrollbarStyles () {
this.refreshScrollbarStyling = true
this.scheduleUpdate()
@ -1197,17 +1216,17 @@ class TextEditorComponent {
if (!options || options.reversed !== false) {
if (desiredScrollBottom > this.getScrollBottom()) {
return this.setScrollBottom(desiredScrollBottom, true)
this.setScrollBottom(desiredScrollBottom)
}
if (desiredScrollTop < this.getScrollTop()) {
return this.setScrollTop(desiredScrollTop, true)
this.setScrollTop(desiredScrollTop)
}
} else {
if (desiredScrollTop < this.getScrollTop()) {
return this.setScrollTop(desiredScrollTop, true)
this.setScrollTop(desiredScrollTop)
}
if (desiredScrollBottom > this.getScrollBottom()) {
return this.setScrollBottom(desiredScrollBottom, true)
this.setScrollBottom(desiredScrollBottom)
}
}
@ -1226,17 +1245,17 @@ class TextEditorComponent {
if (!options || options.reversed !== false) {
if (desiredScrollRight > this.getScrollRight()) {
this.setScrollRight(desiredScrollRight, true)
this.setScrollRight(desiredScrollRight)
}
if (desiredScrollLeft < this.getScrollLeft()) {
this.setScrollLeft(desiredScrollLeft, true)
this.setScrollLeft(desiredScrollLeft)
}
} else {
if (desiredScrollLeft < this.getScrollLeft()) {
this.setScrollLeft(desiredScrollLeft, true)
this.setScrollLeft(desiredScrollLeft)
}
if (desiredScrollRight > this.getScrollRight()) {
this.setScrollRight(desiredScrollRight, true)
this.setScrollRight(desiredScrollRight)
}
}
}
@ -1691,11 +1710,11 @@ class TextEditorComponent {
return this.scrollTop
}
setScrollTop (scrollTop, suppressUpdate = false) {
setScrollTop (scrollTop) {
scrollTop = Math.round(Math.max(0, Math.min(this.getMaxScrollTop(), scrollTop)))
if (scrollTop !== this.scrollTop) {
this.scrollTopPending = true
this.scrollTop = scrollTop
if (!suppressUpdate) this.scheduleUpdate()
return true
} else {
return false
@ -1710,8 +1729,8 @@ class TextEditorComponent {
return this.getScrollTop() + this.getScrollContainerClientHeight()
}
setScrollBottom (scrollBottom, suppressUpdate = false) {
return this.setScrollTop(scrollBottom - this.getScrollContainerClientHeight(), suppressUpdate)
setScrollBottom (scrollBottom) {
return this.setScrollTop(scrollBottom - this.getScrollContainerClientHeight())
}
getScrollLeft () {
@ -1719,11 +1738,11 @@ class TextEditorComponent {
return this.scrollLeft
}
setScrollLeft (scrollLeft, suppressUpdate = false) {
setScrollLeft (scrollLeft) {
scrollLeft = Math.round(Math.max(0, Math.min(this.getMaxScrollLeft(), scrollLeft)))
if (scrollLeft !== this.scrollLeft) {
this.scrollLeftPending = true
this.scrollLeft = scrollLeft
if (!suppressUpdate) this.scheduleUpdate()
return true
} else {
return false
@ -1738,8 +1757,8 @@ class TextEditorComponent {
return this.getScrollLeft() + this.getScrollContainerClientWidth()
}
setScrollRight (scrollRight, suppressUpdate = false) {
return this.setScrollLeft(scrollRight - this.getScrollContainerClientWidth(), suppressUpdate)
setScrollRight (scrollRight) {
return this.setScrollLeft(scrollRight - this.getScrollContainerClientWidth())
}
// Ensure the spatial index is populated with rows that are currently
@ -1807,7 +1826,13 @@ class DummyScrollbarComponent {
innerStyle.height = (this.props.scrollHeight || 0) + 'px'
}
return $.div({style: outerStyle, scrollTop, scrollLeft},
return $.div(
{
style: outerStyle,
scrollTop,
scrollLeft,
on: {scroll: this.props.didScroll}
},
$.div({style: innerStyle})
)
}