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(useScroll): support setting scroll position and toggling smooth scrolling #1996

Merged
merged 2 commits into from Sep 26, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
39 changes: 35 additions & 4 deletions packages/core/useScroll/demo.vue
@@ -1,11 +1,32 @@
<script setup lang="ts">
import { ref, toRefs } from 'vue'
import { computed, ref, toRefs } from 'vue'
import { useScroll } from '@vueuse/core'

const el = ref<HTMLElement | null>(null)
const { x, y, isScrolling, arrivedState, directions } = useScroll(el)
const smooth = ref(false)
const behavior = computed(() => smooth.value ? 'smooth' : 'auto')
const { x, y, isScrolling, arrivedState, directions } = useScroll(el, { behavior })
const { left, right, top, bottom } = toRefs(arrivedState)
const { left: toLeft, right: toRight, top: toTop, bottom: toBottom } = toRefs(directions)

// Format the numbers with toFixed() to make them
// nicer to displayœ
const displayX = computed({
get() {
return x.value.toFixed(1)
},
set(val) {
x.value = parseFloat(val)
},
})
const displayY = computed({
get() {
return y.value.toFixed(1)
},
set(val) {
y.value = parseFloat(val)
},
})
</script>

<template>
Expand All @@ -31,10 +52,20 @@ const { left: toLeft, right: toRight, top: toTop, bottom: toBottom } = toRefs(di
</div>
<div class="m-auto w-280px pl-4">
<div class="px-6 py-4 rounded grid grid-cols-[120px_auto] gap-2 bg-gray-500/5">
<span text="right" opacity="75">Position</span>
<span text="right" opacity="75" class="py-4">X Position</span>
<div class="text-primary">
<div>
<input v-model="displayX" type="number" min="0" max="200" step="10" class="w-full !min-w-0">
</div>
</div>
<span text="right" opacity="75" class="py-4">Y Position</span>
<div class="text-primary">
{{ x.toFixed(1) }}, {{ y.toFixed(1) }}
<div>
<input v-model="displayY" type="number" min="0" max="100" step="10" class="w-full !min-w-0">
</div>
</div>
<label for="smooth-scrolling-option" text="right" opacity="75">Smooth scrolling</label>
<span><input id="smooth-scrolling-option" v-model="smooth" type="checkbox"></span>
<span text="right" opacity="75">isScrolling</span>
<BooleanDisplay :value="isScrolling" />
<div text="right" opacity="75">
Expand Down
41 changes: 39 additions & 2 deletions packages/core/useScroll/index.md
Expand Up @@ -4,7 +4,7 @@ category: Sensors

# useScroll

Reactive scroll position and state
Reactive scroll position and state.

## Usage

Expand All @@ -22,13 +22,50 @@ const { x, y, isScrolling, arrivedState, directions } = useScroll(el)
</template>
```

### With offsets
```js
// With offsets
const { x, y, isScrolling, arrivedState, directions } = useScroll(el, {
offset: { top: 30, bottom: 30, right: 30, left: 30 },
})
```

### Setting scroll position

Set the `x` and `y` values to make the element scroll to that position.
```vue
<script setup lang="ts">
import { useScroll } from '@vueuse/core'

const el = ref<HTMLElement | null>(null)
const { x, y } = useScroll(el)
</script>

<template>
<div ref="el" />
<button @click="x += 10">
Scroll right 10px
</button>
<button @click="y += 10">
Scroll down 10px
</button>
</template>
```

### Smooth scrolling

Set `behavior: smooth` to enable smooth scrolling. The `behavior` option defaults to `auto`, which means no smooth scrolling. See the `behavior` option on [`window.scrollTo()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo) for more information.
```ts
import { useWindowScroll } from '@vueuse/core'

const el = ref<HTMLElement | null>(null)
const { x, y } = useScroll(el, { behavior: 'smooth' })

// Or as a `ref`:
const smooth = ref(false)
const behavior = computed(() => smooth.value ? 'smooth' : 'auto')
const { x, y } = useScroll(el, { behavior })
```

## Directive Usage

```html
Expand Down
67 changes: 55 additions & 12 deletions packages/core/useScroll/index.ts
@@ -1,6 +1,6 @@
import { reactive, ref } from 'vue-demi'
import { computed, reactive, ref } from 'vue-demi'
import type { MaybeComputedRef } from '@vueuse/shared'
import { noop, useDebounceFn, useThrottleFn } from '@vueuse/shared'
import { noop, resolveUnref, useDebounceFn, useThrottleFn } from '@vueuse/shared'
import { useEventListener } from '../useEventListener'

export interface UseScrollOptions {
Expand Down Expand Up @@ -48,6 +48,14 @@ export interface UseScrollOptions {
* @default {capture: false, passive: true}
*/
eventListenerOptions?: boolean | AddEventListenerOptions

/**
* Optionally specify a scroll behavior of `auto` (default, not smooth scrolling) or
* `smooth` (for smooth scrolling) which takes effect when changing the `x` or `y` refs.
*
* @default 'auto'
*/
behavior?: MaybeComputedRef<ScrollBehavior>
}

/**
Expand Down Expand Up @@ -77,10 +85,45 @@ export function useScroll(
capture: false,
passive: true,
},
behavior = 'auto',
} = options

const x = ref(0)
const y = ref(0)
const internalX = ref(0)
const internalY = ref(0)

// Use a computed for x and y because we want to write the value to the refs
// during a `scrollTo()` without firing additional `scrollTo()`s in the process.
const x = computed({
get() {
return internalX.value
},
set(x) {
scrollTo(x, undefined)
},
})

const y = computed({
get() {
return internalY.value
},
set(y) {
scrollTo(undefined, y)
},
})

function scrollTo(_x: number | undefined, _y: number | undefined) {
const _element = resolveUnref(element)

if (!_element)
return

(_element instanceof Document ? document.body : _element)?.scrollTo({
top: resolveUnref(_y) ?? y.value,
left: resolveUnref(_x) ?? x.value,
behavior: resolveUnref(behavior),
})
}

const isScrolling = ref(false)
const arrivedState = reactive({
left: true,
Expand Down Expand Up @@ -110,25 +153,25 @@ export function useScroll(
) as HTMLElement

const scrollLeft = eventTarget.scrollLeft
directions.left = scrollLeft < x.value
directions.right = scrollLeft > x.value
directions.left = scrollLeft < internalX.value
directions.right = scrollLeft > internalY.value
arrivedState.left = scrollLeft <= 0 + (offset.left || 0)
arrivedState.right
= scrollLeft + eventTarget.clientWidth >= eventTarget.scrollWidth - (offset.right || 0)
x.value = scrollLeft
= scrollLeft + eventTarget.clientWidth >= eventTarget.scrollWidth - (offset.right || 0)
internalX.value = scrollLeft

let scrollTop = eventTarget.scrollTop

// patch for mobile compatible
if (e.target === document && !scrollTop)
scrollTop = document.body.scrollTop

directions.top = scrollTop < y.value
directions.bottom = scrollTop > y.value
directions.top = scrollTop < internalY.value
directions.bottom = scrollTop > internalY.value
arrivedState.top = scrollTop <= 0 + (offset.top || 0)
arrivedState.bottom
= scrollTop + eventTarget.clientHeight >= eventTarget.scrollHeight - (offset.bottom || 0)
y.value = scrollTop
= scrollTop + eventTarget.clientHeight >= eventTarget.scrollHeight - (offset.bottom || 0)
internalY.value = scrollTop

isScrolling.value = true
onScrollEnd(e)
Expand Down