Skip to content

Commit

Permalink
feat(VPullToRefresh): add new component (#19666)
Browse files Browse the repository at this point in the history
Co-authored-by: John Leider <john@vuetifyjs.com>
  • Loading branch information
yuwu9145 and johnleider committed Apr 29, 2024
1 parent 66880ce commit 1816775
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 0 deletions.
4 changes: 4 additions & 0 deletions packages/docs/src/data/nav.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@
"title": "number-inputs",
"subfolder": "components"
},
{
"title": "pull-to-refresh",
"subfolder": "components"
},
{
"title": "snackbar-queue",
"subfolder": "components"
Expand Down
92 changes: 92 additions & 0 deletions packages/docs/src/examples/v-pull-to-refresh/usage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<template>
<div class="scrollable-container bg-surface-light">
<v-pull-to-refresh
:pull-down-threshold="pullDownThreshold"
@load="load"
>
<v-list>
<v-list-item
v-for="item in items"
:key="item.value"
:title="item.title"
></v-list-item>
</v-list>
</v-pull-to-refresh>
</div>
</template>

<script setup>
const pullDownThreshold = ref(64)
let items = [
{
title: '1',
value: 1,
},
{
title: '2',
value: 2,
},
{
title: '3',
value: 3,
},
]
let count = 2
async function load ({ done }) {
console.log('loading')
await new Promise(resolve => setTimeout(() => resolve(), 2000))
items = Array.from({ length: count * 3 }, (k, v) => ({
title: `${v + 1}`,
value: v + 1,
}))
console.log('load finish')
count++
done('ok')
}
</script>

<script>
export default {
data: () => ({
pullDownThreshold: 64,
items: [
{
title: '1',
value: 1,
},
{
title: '2',
value: 2,
},
{
title: '3',
value: 3,
},
],
}),
methods: {
async load ({ done }) {
// Perform API call
console.log('loading')
await new Promise(resolve => setTimeout(() => resolve(), 2000))
this.items = Array.from({ length: 3 }, (k, v) => ({
title: `${v + 1}`,
value: v + 1,
}))
console.log('load finish')
done('ok')
},
},
}
</script>

<style>
.scrollable-container {
max-height: 300px;
overflow-y: scroll;
}
</style>
59 changes: 59 additions & 0 deletions packages/docs/src/pages/en/components/pull-to-refresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
---
emphasized: true
meta:
title: Pull To Refresh
description: The PullToRefresh allows users to update content with a simple downward swipe on their screen.
keywords: Pull to refresh, vuetify Pull to refresh component, vue pull to refresh component
features:
label: 'C: VPullToRefresh'
github: /components/VPullToRefresh/
report: true
---

# Pull To Refresh

The PullToRefresh allows users to update content with a simple downward swipe on their screen. Works for Mobile and Desktop.

<PageFeatures />

::: warning

This feature requires [v3.6.0](/getting-started/release-notes/?version=v3.6.0)

:::

## Installation

Labs components require a manual import and installation of the component.

```js { resource="src/plugins/vuetify.js" }
import { VPullToRefresh } from 'vuetify/labs/VPullToRefresh'

export default createVuetify({
components: {
VPullToRefresh,
},
})
```

## Usage

Drag the list downward to activate the pull-to-refresh feature.

<ExamplesExample file="v-pull-to-refresh/usage" />

::: tip

Pull down functionality is available as soon as its immediate scrollable parent has scrolled to the top.

:::

<PromotedEntry />

## API

| Component | Description |
| - | - |
| [v-pull-to-refresh](/api/v-pull-to-refresh/) | Primary Component |

<ApiInline hide-links />
23 changes: 23 additions & 0 deletions packages/vuetify/src/labs/VPullToRefresh/VPullToRefresh.sass
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.v-pull-to-refresh
overflow: hidden
position: relative
&__pull-down
position: absolute
width: 100%
transition: top .3s ease-out
&--touching
transition: none

&__pull-down-default
display: flex
width: 100%
height: 100%
justify-content: center
align-items: flex-end
padding-bottom: 10px

&__scroll-container
position: relative
transition: top .3s ease-out
&--touching
transition: none
167 changes: 167 additions & 0 deletions packages/vuetify/src/labs/VPullToRefresh/VPullToRefresh.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Styles
import './VPullToRefresh.sass'

// Components
import { VIcon } from '@/components/VIcon'
import { VProgressCircular } from '@/components/VProgressCircular'

// Utilities
import { computed, onMounted, ref, shallowRef, watch } from 'vue'
import { clamp, convertToUnit, genericComponent, getScrollParents, useRender } from '@/util'

export type VPullToRefreshSlots = {
default: never
pullDownPanel: {
canRefresh: boolean
goingUp: boolean
refreshing: boolean
}
}

export const VPullToRefresh = genericComponent<VPullToRefreshSlots>()({
name: 'VPullToRefresh',

props: {
pullDownThreshold: {
type: Number,
default: 64,
},
},

emits: {
load: (options: { done: () => void }) => true,
},

setup (props, { slots, emit }) {
let touchstartY = 0
let scrollParents: HTMLElement[] = []

const touchDiff = shallowRef(0)
const containerRef = ref<HTMLElement>()

const refreshing = shallowRef(false)
const goingUp = shallowRef(false)
const touching = shallowRef(false)

const canRefresh = computed(() => touchDiff.value >= props.pullDownThreshold && !refreshing.value)
const topOffset = computed(() => clamp(touchDiff.value, 0, props.pullDownThreshold))

function onTouchstart (e: TouchEvent | MouseEvent) {
if (refreshing.value) return
touching.value = true
touchstartY = 'clientY' in e ? e.clientY : e.touches[0].clientY
}

function onTouchmove (e: TouchEvent | MouseEvent) {
if (refreshing.value || !touching.value) return

const touchY = 'clientY' in e ? e.clientY : e.touches[0].clientY

if (scrollParents.length && !scrollParents[0].scrollTop) {
touchDiff.value = touchY - touchstartY
}
}

function onTouchend (e: TouchEvent | MouseEvent) {
if (refreshing.value) return
touching.value = false
if (canRefresh.value) {
function done () {
if (!refreshing.value) return
touchDiff.value = 0
refreshing.value = false
}
emit('load', { done })
refreshing.value = true
} else {
touchDiff.value = 0
}
}

onMounted(() => {
scrollParents = getScrollParents(containerRef.value)
})

watch([topOffset, refreshing], () => {
if (scrollParents.length) {
const stopScrolling = topOffset.value && !refreshing.value
scrollParents.forEach(p => p.style.overflow = stopScrolling ? 'hidden' : 'auto')
}
})

watch(topOffset, (newVal, oldVal) => {
goingUp.value = newVal < oldVal
})

useRender(() => {
return (
<div
class={[
'v-pull-to-refresh',
]}
onTouchstart={ onTouchstart }
onTouchmove={ onTouchmove }
onTouchend={ onTouchend }
onMousedown={ onTouchstart }
onMouseup={ onTouchend }
onMouseleave={ onTouchend }
onMousemove={ onTouchmove }
ref={ containerRef }
>
<div
class={[
'v-pull-to-refresh__pull-down',
{
'v-pull-to-refresh__pull-down--touching': touching.value,
},
]}
style={{
top: convertToUnit(-1 * props.pullDownThreshold + topOffset.value),
height: convertToUnit(props.pullDownThreshold),
}}
>
{ slots.pullDownPanel
? slots.pullDownPanel({
canRefresh: canRefresh.value,
goingUp: goingUp.value,
refreshing: refreshing.value,
}) : (
<div
class={[
'v-pull-to-refresh__pull-down-default',
]}
>
{
refreshing.value ? (
<VProgressCircular
indeterminate
active={ false }
/>
) : (
<VIcon
icon={ canRefresh.value || goingUp.value ? '$sortAsc' : '$sortDesc' }
/>
)
}
</div>
)
}
</div>
<div
class={[
'v-pull-to-refresh__scroll-container',
{
'v-pull-to-refresh__scroll-container--touching': touching.value,
},
]}
style={{ top: convertToUnit(topOffset.value) }}
>
{ slots.default?.() }
</div>
</div>
)
})
},
})

export type VPullToRefresh = InstanceType<typeof VPullToRefresh>
1 change: 1 addition & 0 deletions packages/vuetify/src/labs/VPullToRefresh/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { VPullToRefresh } from './VPullToRefresh'
1 change: 1 addition & 0 deletions packages/vuetify/src/labs/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export * from './VEmptyState'
export * from './VFab'
export * from './VNumberInput'
export * from './VPicker'
export * from './VPullToRefresh'
export * from './VSnackbarQueue'
export * from './VSparkline'
export * from './VSpeedDial'
Expand Down

0 comments on commit 1816775

Please sign in to comment.