Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(QScrollArea): prevent content re-rendering on scroll or mousemove (fix #16579) #17041

Open
wants to merge 12 commits into
base: dev
Choose a base branch
from
Open
135 changes: 21 additions & 114 deletions ui/src/components/scroll-area/QScrollArea.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { h, ref, computed, watch, withDirectives, onActivated, onDeactivated, onBeforeUnmount, getCurrentInstance } from 'vue'
import { h, ref, computed, watch, onActivated, onDeactivated, onBeforeUnmount, getCurrentInstance } from 'vue'

import useDark, { useDarkProps } from '../../composables/private.use-dark/use-dark.js'
import { dirProps } from './use-scroll-area.js'

import QResizeObserver from '../resize-observer/QResizeObserver.js'
import QScrollObserver from '../scroll-observer/QScrollObserver.js'

import TouchPan from '../../directives/touch-pan/TouchPan.js'
import QScrollAreaControls from './QScrollAreaControls.js'

import { createComponent } from '../../utils/private.create/create.js'
import { between } from '../../utils/format/format.js'
Expand All @@ -14,16 +14,6 @@ import { hMergeSlot } from '../../utils/private.render/render.js'
import debounce from '../../utils/debounce/debounce.js'

const axisList = [ 'vertical', 'horizontal' ]
const dirProps = {
vertical: { offset: 'offsetY', scroll: 'scrollTop', dir: 'down', dist: 'y' },
horizontal: { offset: 'offsetX', scroll: 'scrollLeft', dir: 'right', dist: 'x' }
}
const panOpts = {
prevent: true,
mouse: true,
mouseAllDir: true
}

const getMinThumbSize = size => (size >= 250 ? 50 : Math.ceil(size / 5))

export default createComponent({
Expand Down Expand Up @@ -88,7 +78,7 @@ export default createComponent({

const isDark = useDark(props, proxy.$q)

let timer = null, panRefPos
let timer = null

const targetRef = ref(null)

Expand Down Expand Up @@ -182,25 +172,13 @@ export default createComponent({
)

const mainStyle = computed(() => (
scroll.vertical.thumbHidden.value === true && scroll.horizontal.thumbHidden.value === true
(props.contentStyle || props.contentActiveStyle)
&& scroll.vertical.thumbHidden.value === true
&& scroll.horizontal.thumbHidden.value === true
? props.contentStyle
: props.contentActiveStyle
))

const thumbVertDir = [ [
TouchPan,
e => { onPanThumb(e, 'vertical') },
void 0,
{ vertical: true, ...panOpts }
] ]

const thumbHorizDir = [ [
TouchPan,
e => { onPanThumb(e, 'horizontal') },
void 0,
{ horizontal: true, ...panOpts }
] ]

function getScroll () {
const info = {}

Expand Down Expand Up @@ -282,60 +260,6 @@ export default createComponent({
}
}

function onPanThumb (e, axis) {
const data = scroll[ axis ]

if (e.isFirst === true) {
if (data.thumbHidden.value === true) {
return
}

panRefPos = data.position.value
panning.value = true
}
else if (panning.value !== true) {
return
}

if (e.isFinal === true) {
panning.value = false
}

const dProp = dirProps[ axis ]
const containerSize = container[ axis ].value

const multiplier = (data.size.value - containerSize) / (containerSize - data.thumbSize.value)
const distance = e.distance[ dProp.dist ]
const pos = panRefPos + (e.direction === dProp.dir ? 1 : -1) * distance * multiplier

setScroll(pos, axis)
}

function onMousedown (evt, axis) {
const data = scroll[ axis ]

if (data.thumbHidden.value !== true) {
const offset = evt[ dirProps[ axis ].offset ]
if (offset < data.thumbStart.value || offset > data.thumbStart.value + data.thumbSize.value) {
const pos = offset - data.thumbSize.value / 2
setScroll(pos / container[ axis ].value * data.size.value, axis)
}

// activate thumb pan
if (data.ref.value !== null) {
data.ref.value.dispatchEvent(new MouseEvent(evt.type, evt))
}
}
}

function onVerticalMousedown (evt) {
onMousedown(evt, 'vertical')
}

function onHorizontalMousedown (evt) {
onMousedown(evt, 'horizontal')
}

function startTimer () {
tempShowing.value = true

Expand Down Expand Up @@ -462,39 +386,22 @@ export default createComponent({
onResize: updateContainer
}),

h('div', {
class: scroll.vertical.barClass.value,
style: [ props.barStyle, props.verticalBarStyle ],
'aria-hidden': 'true',
onMousedown: onVerticalMousedown
}),
h(QScrollAreaControls, {
thumbStyle: props.thumbStyle,
verticalThumbStyle: props.verticalThumbStyle,
horizontalThumbStyle: props.horizontalThumbStyle,

h('div', {
class: scroll.horizontal.barClass.value,
style: [ props.barStyle, props.horizontalBarStyle ],
'aria-hidden': 'true',
onMousedown: onHorizontalMousedown
}),
barStyle: props.barStyle,
verticalBarStyle: props.verticalBarStyle,
horizontalBarStyle: props.horizontalBarStyle,

withDirectives(
h('div', {
ref: scroll.vertical.ref,
class: scroll.vertical.thumbClass.value,
style: scroll.vertical.style.value,
'aria-hidden': 'true'
}),
thumbVertDir
),

withDirectives(
h('div', {
ref: scroll.horizontal.ref,
class: scroll.horizontal.thumbClass.value,
style: scroll.horizontal.style.value,
'aria-hidden': 'true'
}),
thumbHorizDir
)
visible: props.visible,

scroll,
container,

onSetScroll: setScroll
})
])
}
}
Expand Down
154 changes: 154 additions & 0 deletions ui/src/components/scroll-area/QScrollAreaControls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { h, ref, withDirectives } from 'vue'

import { dirProps } from './use-scroll-area.js'

import TouchPan from '../../directives/touch-pan/TouchPan.js'

import { createComponent } from '../../utils/private.create/create.js'
import { between } from '../../utils/format.js'

const panOpts = {
prevent: true,
mouse: true,
mouseAllDir: true
}

export default createComponent({
name: 'QScrollAreaControls',

emits: [ 'set-scroll' ],

props: {
thumbStyle: Object,
verticalThumbStyle: Object,
horizontalThumbStyle: Object,

barStyle: [ Array, String, Object ],
verticalBarStyle: [ Array, String, Object ],
horizontalBarStyle: [ Array, String, Object ],

visible: {
type: Boolean,
default: null
},

container: Object,
scroll: Object
},

setup (props, { emit }) {
// state management
const panning = ref(false)

let panRefPos

const thumbVertDir = [ [
TouchPan,
e => { onPanThumb(e, 'vertical') },
void 0,
{ vertical: true, ...panOpts }
] ]

const thumbHorizDir = [ [
TouchPan,
e => { onPanThumb(e, 'horizontal') },
void 0,
{ horizontal: true, ...panOpts }
] ]

function onPanThumb (e, axis) {
const data = props.scroll[ axis ]

if (e.isFirst === true) {
if (data.thumbHidden.value === true) {
return
}

panRefPos = data.position.value
panning.value = true
}
else if (panning.value !== true) {
return
}

if (e.isFinal === true) {
panning.value = false
}

const dProp = dirProps[ axis ]
const containerSize = props.container[ axis ].value

const multiplier = (data.size.value - containerSize) / (containerSize - data.thumbSize.value)
const distance = e.distance[ dProp.dist ]
const pos = panRefPos + (e.direction === dProp.dir ? 1 : -1) * distance * multiplier

setScroll(pos, axis)
}

function onMousedown (evt, axis) {
const data = props.scroll[ axis ]

if (data.thumbHidden.value !== true) {
const offset = evt[ dirProps[ axis ].offset ]
if (offset < data.thumbStart.value || offset > data.thumbStart.value + data.thumbSize.value) {
const targetThumbStart = offset - data.thumbSize.value / 2
const percentage = between(targetThumbStart / (props.container[ axis ].value - data.thumbSize.value), 0, 1)
setScroll(percentage * Math.max(0, data.size.value - props.container[ axis ].value), axis)
}

// activate thumb pan
if (data.ref.value !== null) {
data.ref.value.dispatchEvent(new MouseEvent(evt.type, evt))
}
}
}

function onVerticalMousedown (evt) {
onMousedown(evt, 'vertical')
}

function onHorizontalMousedown (evt) {
onMousedown(evt, 'horizontal')
}

function setScroll (offset, axis) {
emit('set-scroll', offset, axis)
}

return () => [
h('div', {
class: props.scroll.vertical.barClass.value,
style: [ props.barStyle, props.verticalBarStyle ],
'aria-hidden': 'true',
onMousedown: onVerticalMousedown
}),

h('div', {
class: props.scroll.horizontal.barClass.value,
style: [ props.barStyle, props.horizontalBarStyle ],
'aria-hidden': 'true',
onMousedown: onHorizontalMousedown
}),

withDirectives(
h('div', {
ref: props.scroll.vertical.ref,
class: props.scroll.vertical.thumbClass.value,
style: props.scroll.vertical.style.value,
'aria-hidden': 'true'
}),
thumbVertDir
),

withDirectives(
h('div', {
ref: props.scroll.horizontal.ref,
class: props.scroll.horizontal.thumbClass.value,
style: props.scroll.horizontal.style.value,
'aria-hidden': 'true'
}),
thumbHorizDir
)
]
}
})
4 changes: 4 additions & 0 deletions ui/src/components/scroll-area/use-scroll-area.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const dirProps = {
vertical: { offset: 'offsetY', scroll: 'scrollTop', dir: 'down', dist: 'y' },
horizontal: { offset: 'offsetX', scroll: 'scrollLeft', dir: 'right', dist: 'x' }
}