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

feat(QScrollArea): add scroll viewport to create overscrolling effect #17208

Closed
wants to merge 11 commits into from
Closed
66 changes: 59 additions & 7 deletions ui/dev/src/pages/components/scroll-area.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,19 @@
<q-toggle v-model="darkVariant" toggle-indeterminate label="Dark variant" />
<q-toggle v-model="focusable" label="Focusable" />

<div style="height: 100px;" />
<div class="row items-center">
<div class="q-mr-md">
<div>Top offset</div>
<q-slider v-model="topOffset" :min="0" :max="100" label-always switch-label-side style="width: 200px"/>
</div>

<div>
<div>Bottom offset</div>
<q-slider v-model="bottomOffset" :min="0" :max="100" label-always switch-label-side style="width: 200px"/>
</div>
</div>

<div style="height: 50px;" />

<keep-alive>
<q-scroll-area
Expand All @@ -15,6 +27,7 @@
ref="scroll"
style="width: 400px; height: 500px;"
class="bg-yellow"
:vertical-offset="[topOffset, bottomOffset]"
:visible="alwaysVisible"
:bar-style="customBarStyle"
:vertical-bar-style="customVBarStyle"
Expand All @@ -24,13 +37,19 @@
:horizontal-thumb-style="customHThumbStyle"
:tabindex="focusable === true ? 0 : void 0"
>
<div :class="{ 'flex no-wrap' : horizontal }">
<div style="margin-top: 150px" />
<div style="margin-bottom: 25px" :style="horizontal ? 'width: 160px' : ''" v-for="n in number" :key="n">
<div v-if="topOffset" :style="`height: ${topOffset}px`" class="flex flex-center text-white fixed-top" style="backdrop-filter: blur(8px);background: #0008;z-index: 1;">User-Defined Header</div>

<div :class="{ 'flex no-wrap' : horizontal }" :style="{
'padding-top': topOffset ? ` ${topOffset}px` : void 0,
'padding-bottom': bottomOffset ? ` ${bottomOffset}px` : void 0,
}">
<div style="margin-block: 12px" :style="horizontal ? 'width: 160px' : ''" v-for="n in number" :key="n">
{{ n }} Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
<q-btn label="Click" color="primary" />
</div>
</div>

<div v-if="bottomOffset" :style="`height: ${bottomOffset}px`" class="flex flex-center text-white fixed-bottom" style="backdrop-filter: blur(8px);background: #0008;z-index: 1;">User-Defined Footer</div>
</q-scroll-area>

<q-scroll-area
Expand Down Expand Up @@ -77,7 +96,38 @@
<pre class="inline-block" dir="ltr">{{ scrollDetails }}</pre>
<q-separator spaced />

<div style="height: 250px" />
<div style="height: 100px" />

<div class="row q-gutter-md">
<q-scroll-area
class="bg-yellow"
style="width: 800px; height: 300px;"
:visible="alwaysVisible"
:bar-style="customBarStyle"
:vertical-bar-style="customVBarStyle"
:horizontal-bar-style="customHBarStyle"
:thumbStyle="customThumbStyle"
:vertical-thumb-style="customVThumbStyle"
:horizontal-thumb-style="customHThumbStyle"
:horizontal-offset="[topOffset, bottomOffset]"
>
<div v-if="topOffset" :style="`width: ${topOffset}px`" class="flex flex-center text-center text-white fixed-left" style="backdrop-filter: blur(8px);background: #0008;z-index: 1;">User-Defined Panel</div>

<div class="flex no-wrap" :style="{
'padding-left': topOffset ? ` ${topOffset}px` : void 0,
'padding-right': bottomOffset ? ` ${bottomOffset}px` : void 0,
}">
<div style="margin: 12px; width: 300px" v-for="n in number" :key="n">
{{ n }} Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
<q-btn label="Click" color="primary" />
</div>
</div>

<div v-if="bottomOffset" :style="`width: ${bottomOffset}px`" class="flex flex-center text-center text-white fixed-right" style="backdrop-filter: blur(8px);background: #0008;z-index: 1;">User-Defined Panel</div>
</q-scroll-area>
</div>

<div style="height: 100px" />

<div class="row q-gutter-md">
<q-scroll-area
Expand Down Expand Up @@ -142,12 +192,14 @@ export default {
data () {
return {
darkVariant: false,
number: 10,
number: 5,
horizontal: false,
alwaysVisible: true,
customStyle: false,
focusable: true,
scrollDetails: null
scrollDetails: null,
topOffset: 100,
bottomOffset: 100
}
},

Expand Down
185 changes: 56 additions & 129 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.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.js'
import { between } from '../../utils/format.js'
Expand All @@ -14,16 +14,6 @@ import { hMergeSlot } from '../../utils/private/render.js'
import debounce from '../../utils/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 All @@ -40,6 +30,15 @@ export default createComponent({
verticalBarStyle: [ Array, String, Object ],
horizontalBarStyle: [ Array, String, Object ],

verticalOffset: {
type: Array,
default: [ 0, 0 ]
},
horizontalOffset: {
type: Array,
default: [ 0, 0 ]
},

contentStyle: [ Array, String, Object ],
contentActiveStyle: [ Array, String, Object ],

Expand Down Expand Up @@ -67,7 +66,13 @@ export default createComponent({
// other...
const container = {
vertical: ref(0),
horizontal: ref(0)
verticalNet: computed(() =>
container.vertical.value - props.verticalOffset[ 0 ] - props.verticalOffset[ 1 ]
),
horizontal: ref(0),
horizontalNet: computed(() =>
container.horizontal.value - props.horizontalOffset[ 0 ] - props.horizontalOffset[ 1 ]
)
}

const scroll = {
Expand All @@ -88,7 +93,7 @@ export default createComponent({

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

let timer = null, panRefPos
let timer = null

const targetRef = ref(null)

Expand All @@ -110,15 +115,15 @@ export default createComponent({
&& panning.value === false
) || scroll.vertical.size.value <= container.vertical.value + 1
)
scroll.vertical.thumbStart = computed(() =>
scroll.vertical.percentage.value * (container.vertical.value - scroll.vertical.thumbSize.value)
)
scroll.vertical.thumbStart = computed(() => {
return props.verticalOffset[ 0 ] + scroll.vertical.percentage.value * (container.verticalNet.value - scroll.vertical.thumbSize.value)
})
scroll.vertical.thumbSize = computed(() =>
Math.round(
between(
container.vertical.value * container.vertical.value / scroll.vertical.size.value,
getMinThumbSize(container.vertical.value),
container.vertical.value
container.verticalNet.value * container.verticalNet.value / scroll.vertical.size.value,
getMinThumbSize(container.verticalNet.value),
container.verticalNet.value
)
)
)
Expand All @@ -127,7 +132,8 @@ export default createComponent({
...props.thumbStyle,
...props.verticalThumbStyle,
top: `${ scroll.vertical.thumbStart.value }px`,
height: `${ scroll.vertical.thumbSize.value }px`
height: `${ scroll.vertical.thumbSize.value }px`,
right: `${ props.horizontalOffset[ 1 ] }px`
}
})
scroll.vertical.thumbClass = computed(() =>
Expand All @@ -152,15 +158,15 @@ export default createComponent({
&& panning.value === false
) || scroll.horizontal.size.value <= container.horizontal.value + 1
)
scroll.horizontal.thumbStart = computed(() =>
scroll.horizontal.percentage.value * (container.horizontal.value - scroll.horizontal.thumbSize.value)
)
scroll.horizontal.thumbStart = computed(() => {
return props.horizontalOffset[ 0 ] + scroll.horizontal.percentage.value * (container.horizontalNet.value - scroll.horizontal.thumbSize.value)
})
scroll.horizontal.thumbSize = computed(() =>
Math.round(
between(
container.horizontal.value * container.horizontal.value / scroll.horizontal.size.value,
getMinThumbSize(container.horizontal.value),
container.horizontal.value
container.horizontalNet.value * container.horizontalNet.value / scroll.horizontal.size.value,
getMinThumbSize(container.horizontalNet.value),
container.horizontalNet.value
)
)
)
Expand All @@ -169,7 +175,8 @@ export default createComponent({
...props.thumbStyle,
...props.horizontalThumbStyle,
[ proxy.$q.lang.rtl === true ? 'right' : 'left' ]: `${ scroll.horizontal.thumbStart.value }px`,
width: `${ scroll.horizontal.thumbSize.value }px`
width: `${ scroll.horizontal.thumbSize.value }px`,
bottom: `${ props.verticalOffset[ 1 ] }px`
}
})
scroll.horizontal.thumbClass = computed(() =>
Expand All @@ -182,25 +189,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 +277,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 +403,25 @@ 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
)
verticalOffset: props.verticalOffset,
horizontalOffset: props.horizontalOffset,

visible: props.visible,

scroll,
container,

onSetScroll: setScroll
})
])
}
}
Expand Down
Loading