From 0666a78dcfdafcdbeb21517b333f9406b7f529e1 Mon Sep 17 00:00:00 2001 From: eana <coder@apps.1a23.com> Date: Sun, 31 Mar 2024 20:13:00 +0000 Subject: [PATCH] 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> --- .../client/src/components/MkPullToRefresh.vue | 95 +++++++++++++------ .../client/src/pages/antenna-timeline.vue | 8 +- .../client/src/pages/user-list-timeline.vue | 9 +- packages/client/src/scripts/scroll.ts | 2 +- packages/client/src/scripts/touch.ts | 27 ++---- packages/client/src/style.scss | 1 + 6 files changed, 83 insertions(+), 59 deletions(-) diff --git a/packages/client/src/components/MkPullToRefresh.vue b/packages/client/src/components/MkPullToRefresh.vue index 40e6ca4640..dbd3a983c6 100644 --- a/packages/client/src/components/MkPullToRefresh.vue +++ b/packages/client/src/components/MkPullToRefresh.vue @@ -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({ diff --git a/packages/client/src/pages/antenna-timeline.vue b/packages/client/src/pages/antenna-timeline.vue index d9fbbdf84f..726987aa4a 100644 --- a/packages/client/src/pages/antenna-timeline.vue +++ b/packages/client/src/pages/antenna-timeline.vue @@ -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> diff --git a/packages/client/src/pages/user-list-timeline.vue b/packages/client/src/pages/user-list-timeline.vue index d48cdcce89..69f17c1047 100644 --- a/packages/client/src/pages/user-list-timeline.vue +++ b/packages/client/src/pages/user-list-timeline.vue @@ -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> diff --git a/packages/client/src/scripts/scroll.ts b/packages/client/src/scripts/scroll.ts index 0ac4cd7240..61b6515339 100644 --- a/packages/client/src/scripts/scroll.ts +++ b/packages/client/src/scripts/scroll.ts @@ -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; diff --git a/packages/client/src/scripts/touch.ts b/packages/client/src/scripts/touch.ts index 9150e42c4e..1c8cbe6ffb 100644 --- a/packages/client/src/scripts/touch.ts +++ b/packages/client/src/scripts/touch.ts @@ -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); diff --git a/packages/client/src/style.scss b/packages/client/src/style.scss index 8d677f7614..b671cecf7f 100644 --- a/packages/client/src/style.scss +++ b/packages/client/src/style.scss @@ -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;