diff --git a/packages/docs/src/data/nav.json b/packages/docs/src/data/nav.json index 9666b28d020..26e804d420d 100644 --- a/packages/docs/src/data/nav.json +++ b/packages/docs/src/data/nav.json @@ -247,6 +247,10 @@ "title": "number-inputs", "subfolder": "components" }, + { + "title": "pull-to-refresh", + "subfolder": "components" + }, { "title": "snackbar-queue", "subfolder": "components" diff --git a/packages/docs/src/examples/v-pull-to-refresh/usage.vue b/packages/docs/src/examples/v-pull-to-refresh/usage.vue new file mode 100644 index 00000000000..9ac2ee24d71 --- /dev/null +++ b/packages/docs/src/examples/v-pull-to-refresh/usage.vue @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + diff --git a/packages/docs/src/pages/en/components/pull-to-refresh.md b/packages/docs/src/pages/en/components/pull-to-refresh.md new file mode 100644 index 00000000000..1b762526753 --- /dev/null +++ b/packages/docs/src/pages/en/components/pull-to-refresh.md @@ -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. + + + +::: 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. + + + +::: tip + +Pull down functionality is available as soon as its immediate scrollable parent has scrolled to the top. + +::: + + + +## API + +| Component | Description | +| - | - | +| [v-pull-to-refresh](/api/v-pull-to-refresh/) | Primary Component | + + diff --git a/packages/vuetify/src/labs/VPullToRefresh/VPullToRefresh.sass b/packages/vuetify/src/labs/VPullToRefresh/VPullToRefresh.sass new file mode 100644 index 00000000000..7b3d28886ab --- /dev/null +++ b/packages/vuetify/src/labs/VPullToRefresh/VPullToRefresh.sass @@ -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 diff --git a/packages/vuetify/src/labs/VPullToRefresh/VPullToRefresh.tsx b/packages/vuetify/src/labs/VPullToRefresh/VPullToRefresh.tsx new file mode 100644 index 00000000000..0d85b0a8908 --- /dev/null +++ b/packages/vuetify/src/labs/VPullToRefresh/VPullToRefresh.tsx @@ -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()({ + 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() + + 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 ( + + + { slots.pullDownPanel + ? slots.pullDownPanel({ + canRefresh: canRefresh.value, + goingUp: goingUp.value, + refreshing: refreshing.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 41c5a37956e..fc82c5954c9 100644 --- a/packages/vuetify/src/labs/components.ts +++ b/packages/vuetify/src/labs/components.ts @@ -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'