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:
eana 2024-03-31 20:13:00 +00:00
parent 54dfe1c222
commit 0666a78dcf
6 changed files with 83 additions and 59 deletions

View File

@ -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: falsepreventDefault使
}
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({

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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);

View File

@ -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;