diff --git a/packages/vuetify/src/labs/VPullToRefresh/VPullToRefresh.sass b/packages/vuetify/src/labs/VPullToRefresh/VPullToRefresh.sass new file mode 100644 index 00000000000..00eda1c2919 --- /dev/null +++ b/packages/vuetify/src/labs/VPullToRefresh/VPullToRefresh.sass @@ -0,0 +1,18 @@ +.v-pull-to-refresh + overflow: hidden + &__pull-down + position: relative + display: flex + justify-content: center + align-items: flex-end + height: 64px + transition: top .3s ease-out + &--touching + transition: none + + + &__scroll-container + position: relative + transition: top .3s ease-out + &--touching + transition: none \ No newline at end of file diff --git a/packages/vuetify/src/labs/VPullToRefresh/VPullToRefresh.tsx b/packages/vuetify/src/labs/VPullToRefresh/VPullToRefresh.tsx new file mode 100644 index 00000000000..602e0e40db8 --- /dev/null +++ b/packages/vuetify/src/labs/VPullToRefresh/VPullToRefresh.tsx @@ -0,0 +1,143 @@ +// Styles +import './VPullToRefresh.sass' + +// Components +import { VIcon } from '@/components/VIcon' +import { VProgressCircular } from '@/components/VProgressCircular' + +// Composables +import { LoaderSlot } from '@/composables/loader' + +// Utilities +import { computed, onMounted, ref, shallowRef } from 'vue' +import { convertToUnit, genericComponent, useRender } from '@/util' + +const PULL_DOWN_HEIGHT_PX = 64 + +export type PullToRefreshStatus = 'ok' | 'error' + +export const VPullToRefresh = genericComponent()({ + name: 'VPullToRefresh', + + props: { + }, + + emits: { + load: (options: { done: (status: PullToRefreshStatus) => void }) => true, + }, + + setup (props, { slots, emit }) { + let touchstartY = 0 + + const touchDiff = shallowRef(0) + const scrollContainerRef = ref() + + const height = shallowRef(0) + + const canRefresh = computed(() => touchDiff.value > PULL_DOWN_HEIGHT_PX * 2 / 3) + const refreshing = shallowRef(false) + const touching = shallowRef(false) + + function onTouchstart (e: TouchEvent) { + if (refreshing.value) { + e.preventDefault() + return + } + touching.value = true + touchstartY = e.touches[0].clientY + } + + function onTouchmove (e: TouchEvent) { + if (refreshing.value) { + e.preventDefault() + return + } + const touchY = e.touches[0].clientY + if (touchDiff.value < PULL_DOWN_HEIGHT_PX && window.scrollY === 0) { + touchDiff.value = touchY - touchstartY + } + } + + function onTouchend (e: TouchEvent) { + if (refreshing.value) { + e.preventDefault() + return + } + touching.value = false + if (canRefresh.value) { + function done (status: PullToRefreshStatus) { + if (status === 'ok') { + touchDiff.value = 0 + refreshing.value = false + } + } + emit('load', { done }) + refreshing.value = true + } + } + + onMounted(() => { + height.value = scrollContainerRef.value!.offsetHeight + }) + + useRender(() => { + return ( +
+
+ { + canRefresh.value || refreshing.value ? ( + + + + ) : ( + + ) + } +
+
+ { slots.default?.() } +
+
+ ) + }) + }, +}) + +export type VPullToRefresh = InstanceType diff --git a/packages/vuetify/src/labs/VPullToRefresh/index.ts b/packages/vuetify/src/labs/VPullToRefresh/index.ts new file mode 100644 index 00000000000..343ae37299f --- /dev/null +++ b/packages/vuetify/src/labs/VPullToRefresh/index.ts @@ -0,0 +1 @@ +export { VPullToRefresh } from './VPullToRefresh' diff --git a/packages/vuetify/src/labs/components.ts b/packages/vuetify/src/labs/components.ts index 98850084f8e..68419f1de2c 100644 --- a/packages/vuetify/src/labs/components.ts +++ b/packages/vuetify/src/labs/components.ts @@ -4,6 +4,7 @@ export * from './VEmptyState' export * from './VFab' export * from './VNumberInput' export * from './VPicker' +export * from './VPullToRefresh' export * from './VSparkline' export * from './VSpeedDial' export * from './VTimePicker'