fix(client): pull to refresh activates when scrolling down mid-way in the page
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp> Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
This commit is contained in:
parent
54dfe1c222
commit
0666a78dcf
6 changed files with 83 additions and 59 deletions
|
@ -9,6 +9,16 @@
|
|||
}px;`"
|
||||
>
|
||||
<div :class="$style.frameContent">
|
||||
<MkLoading
|
||||
v-if="isRefreshing"
|
||||
:class="$style.loader"
|
||||
:em="true"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
class="ti ti-arrow-bar-to-down"
|
||||
:class="[$style.icon, { [$style.refresh]: pullEnded }]"
|
||||
></i>
|
||||
<div :class="$style.text">
|
||||
<template v-if="pullEnded">{{
|
||||
i18n.ts.releaseToReload
|
||||
|
@ -28,16 +38,16 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref, shallowRef } from "vue";
|
||||
// import { deviceKind } from "@/scripts/device-kind";
|
||||
import { i18n } from "@/i18n";
|
||||
import { defaultStore } from "@/store";
|
||||
import { getScrollContainer } from "@/scripts/scroll";
|
||||
import { isDuringHorizontalSwipe } from "@/scripts/touch";
|
||||
|
||||
const SCROLL_STOP = 10;
|
||||
const MAX_PULL_DISTANCE = Infinity;
|
||||
const FIRE_THRESHOLD = defaultStore.state.pullToRefreshThreshold;
|
||||
const RELEASE_TRANSITION_DURATION = 120;
|
||||
const FIRE_THRESHOLD = 230;
|
||||
const RELEASE_TRANSITION_DURATION = 200;
|
||||
const PULL_BRAKE_BASE = 1.5;
|
||||
const PULL_BRAKE_FACTOR = 100;
|
||||
const PULL_BRAKE_FACTOR = 170;
|
||||
|
||||
const pullStarted = ref(false);
|
||||
const pullEnded = ref(false);
|
||||
|
@ -64,13 +74,6 @@ const emits = defineEmits<{
|
|||
(ev: "refresh"): void;
|
||||
}>();
|
||||
|
||||
function getScrollableParentElement(node) {
|
||||
if (node == null) return null;
|
||||
if (node.scrollHeight > node.clientHeight) return node;
|
||||
|
||||
return getScrollableParentElement(node.parentNode);
|
||||
}
|
||||
|
||||
function getScreenY(event) {
|
||||
if (supportPointerDesktop) return event.screenY;
|
||||
return event.touches[0].screenY;
|
||||
|
@ -135,12 +138,14 @@ function moveEnd() {
|
|||
}
|
||||
}
|
||||
|
||||
function moving(event) {
|
||||
function moving(event: TouchEvent | PointerEvent) {
|
||||
if (!pullStarted.value || isRefreshing.value || disabled) return;
|
||||
if (scrollEl == null) scrollEl = getScrollableParentElement(rootEl);
|
||||
if (
|
||||
(scrollEl?.scrollTop ?? 0) >
|
||||
(supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance.value)
|
||||
(supportPointerDesktop
|
||||
? SCROLL_STOP
|
||||
: SCROLL_STOP + pullDistance.value) ||
|
||||
isDuringHorizontalSwipe.value
|
||||
) {
|
||||
pullDistance.value = 0;
|
||||
pullEnded.value = false;
|
||||
|
@ -153,6 +158,15 @@ function moving(event) {
|
|||
const moveScreenY = getScreenY(event);
|
||||
const moveHeight = moveScreenY - startScreenY!;
|
||||
pullDistance.value = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
|
||||
|
||||
if (pullDistance.value > 0) {
|
||||
if (event.cancelable) event.preventDefault();
|
||||
}
|
||||
|
||||
if (pullDistance.value > SCROLL_STOP) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
pullEnded.value = pullDistance.value >= FIRE_THRESHOLD;
|
||||
}
|
||||
|
||||
|
@ -167,25 +181,50 @@ function setDisabled(value) {
|
|||
disabled = value;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// supportPointerDesktop = !!window.PointerEvent && deviceKind === "desktop";
|
||||
function onScrollContainerScroll() {
|
||||
const scrollPos = scrollEl!.scrollTop;
|
||||
|
||||
if (supportPointerDesktop) {
|
||||
rootEl.value?.addEventListener("pointerdown", moveStart);
|
||||
// "up" event won't be emmitted by mouse pointer on desktop
|
||||
window.addEventListener("pointerup", moveEnd);
|
||||
rootEl.value?.addEventListener("pointermove", moving, {
|
||||
passive: true,
|
||||
});
|
||||
// When at the top of the page, disable vertical overscroll so passive touch listeners can take over.
|
||||
if (scrollPos === 0) {
|
||||
scrollEl!.style.touchAction = "pan-x pan-down pinch-zoom";
|
||||
registerEventListenersForReadyToPull();
|
||||
} else {
|
||||
rootEl.value?.addEventListener("touchstart", moveStart);
|
||||
rootEl.value?.addEventListener("touchend", moveEnd);
|
||||
rootEl.value?.addEventListener("touchmove", moving, { passive: true });
|
||||
scrollEl!.style.touchAction = "auto";
|
||||
unregisterEventListenersForReadyToPull();
|
||||
}
|
||||
}
|
||||
|
||||
function registerEventListenersForReadyToPull() {
|
||||
if (rootEl.value == null) return;
|
||||
rootEl.value.addEventListener("touchstart", moveStart, { passive: true });
|
||||
rootEl.value.addEventListener("touchmove", moving, { passive: false }); // passive: falseにしないとpreventDefaultが使えない
|
||||
}
|
||||
|
||||
function unregisterEventListenersForReadyToPull() {
|
||||
if (rootEl.value == null) return;
|
||||
rootEl.value.removeEventListener("touchstart", moveStart);
|
||||
rootEl.value.removeEventListener("touchmove", moving);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (rootEl.value == null) return;
|
||||
|
||||
scrollEl = getScrollContainer(rootEl.value);
|
||||
if (scrollEl == null) return;
|
||||
|
||||
scrollEl.addEventListener("scroll", onScrollContainerScroll, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
rootEl.value.addEventListener("touchend", moveEnd, { passive: true });
|
||||
|
||||
registerEventListenersForReadyToPull();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (supportPointerDesktop) window.removeEventListener("pointerup", moveEnd);
|
||||
if (scrollEl) scrollEl.removeEventListener("scroll", onScrollContainerScroll);
|
||||
|
||||
unregisterEventListenersForReadyToPull();
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
<div
|
||||
ref="rootEl"
|
||||
v-hotkey.global="keymap"
|
||||
v-size="{ min: [800] }"
|
||||
class="tqmomfks"
|
||||
>
|
||||
<div class="tl _block">
|
||||
|
@ -104,15 +103,12 @@ definePageMetadata(
|
|||
<style lang="scss" scoped>
|
||||
.tqmomfks {
|
||||
padding: var(--margin);
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
|
||||
> .tl {
|
||||
background: none;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
&.min-width_800px {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<template #header
|
||||
><MkPageHeader :actions="headerActions" :tabs="headerTabs"
|
||||
/></template>
|
||||
<div ref="rootEl" v-size="{ min: [800] }" class="eqqrhokj">
|
||||
<div ref="rootEl" class="eqqrhokj">
|
||||
<div class="tl _block">
|
||||
<XTimeline
|
||||
ref="tlEl"
|
||||
|
@ -94,14 +94,11 @@ definePageMetadata(
|
|||
<style lang="scss" scoped>
|
||||
.eqqrhokj {
|
||||
padding: var(--margin);
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
> .tl {
|
||||
background: none;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
&.min-width_800px {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
type ScrollBehavior = "auto" | "smooth" | "instant";
|
||||
|
||||
export function getScrollContainer(el: HTMLElement | null): HTMLElement | null {
|
||||
if (el == null || el.tagName === "HTML") return null;
|
||||
if (el == null) return null;
|
||||
const overflow = window.getComputedStyle(el).getPropertyValue("overflow-y");
|
||||
if (overflow === "scroll" || overflow === "auto") {
|
||||
return el;
|
||||
|
|
|
@ -1,32 +1,23 @@
|
|||
import { ref } from "vue";
|
||||
import { deviceKind } from "@/scripts/device-kind.js";
|
||||
|
||||
const isTouchSupported =
|
||||
"maxTouchPoints" in navigator && navigator.maxTouchPoints > 0;
|
||||
|
||||
export let isTouchUsing = false;
|
||||
export let isTouchUsing =
|
||||
deviceKind === "tablet" || deviceKind === "smartphone";
|
||||
|
||||
export let isScreenTouching = false;
|
||||
|
||||
if (isTouchSupported) {
|
||||
if (isTouchSupported && !isTouchUsing) {
|
||||
window.addEventListener(
|
||||
"touchstart",
|
||||
() => {
|
||||
// maxTouchPointsなどでの判定だけだと、「タッチ機能付きディスプレイを使っているがマウスでしか操作しない」場合にも
|
||||
// タッチで使っていると判定されてしまうため、実際に一度でもタッチされたらtrueにする
|
||||
isTouchUsing = true;
|
||||
|
||||
isScreenTouching = true;
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
|
||||
window.addEventListener(
|
||||
"touchend",
|
||||
() => {
|
||||
// 子要素のtouchstartイベントでstopPropagation()が呼ばれると親要素に伝搬されずタッチされたと判定されないため、
|
||||
// touchendイベントでもtouchstartイベントと同様にtrueにする
|
||||
isTouchUsing = true;
|
||||
|
||||
isScreenTouching = false;
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
}
|
||||
|
||||
/** (MkHorizontalSwipe) 横スワイプ中か? */
|
||||
export const isDuringHorizontalSwipe = ref(false);
|
||||
|
|
|
@ -84,6 +84,7 @@ html {
|
|||
tab-size: 2;
|
||||
scroll-padding: 60px;
|
||||
overflow-x: clip;
|
||||
overflow-y: auto;
|
||||
text-size-adjust: none;
|
||||
-webkit-text-size-adjust: none;
|
||||
|
||||
|
|
Loading…
Reference in a new issue