Skip to content

fix(frontend): 横スワイプの挙動改善#17101

Draft
kakkokari-gtyih wants to merge 12 commits intomisskey-dev:developfrom
kakkokari-gtyih:fix-swiper-2
Draft

fix(frontend): 横スワイプの挙動改善#17101
kakkokari-gtyih wants to merge 12 commits intomisskey-dev:developfrom
kakkokari-gtyih:fix-swiper-2

Conversation

@kakkokari-gtyih
Copy link
Contributor

@kakkokari-gtyih kakkokari-gtyih commented Jan 14, 2026

What

いくつかの修正

  • 横スワイプ開始時に、開始までにスワイプした分のpx数を無視するように(ゆっくりスワイプすると変に吸い付く感じになるのを修正)
  • 横スワイプ実施状態で常にCSSイージング効果が適用され、タッチアクションに完全には追従していないのを修正(MkPullToRefresh同様にスワイプ量をJSで測るようにした)
  • 一部の要素上に指を置いた状態からスワイプを開始できないのを修正(hasSomethingToDoWithXSwipeの条件緩和)

いくつかのリファクタ

  • MkPullToRefreshで1msごとにsetIntervalしていたのをrequestAnimationFrameに置き換えて負荷を軽減
  • MkSwiperの関数名をMkPullToRefreshに寄せて挙動の把握をやりやすく

Why

UX / Refactor

Additional info (optional)

Checklist

  • Read the contribution guide
  • Test working in a local environment
  • (If needed) Add story of storybook
  • (If needed) Update CHANGELOG.md
  • (If possible) Add tests

@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jan 14, 2026
@github-actions github-actions bot added the packages/frontend Client side specific issue/PR label Jan 14, 2026
@codecov
Copy link

codecov bot commented Jan 14, 2026

Codecov Report

❌ Patch coverage is 1.31004% with 226 lines in your changes missing coverage. Please review.
✅ Project coverage is 13.63%. Comparing base (810faa8) to head (26b4d97).

Files with missing lines Patch % Lines
packages/frontend/src/components/MkSwiper.vue 1.76% 124 Missing and 43 partials ⚠️
...ckages/frontend/src/components/MkPullToRefresh.vue 0.00% 48 Missing and 11 partials ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           develop   #17101       +/-   ##
============================================
- Coverage    63.48%   13.63%   -49.85%     
============================================
  Files         1161      241      -920     
  Lines       115968    11858   -104110     
  Branches      8351     3983     -4368     
============================================
- Hits         73619     1617    -72002     
+ Misses       40159     8040    -32119     
- Partials      2190     2201       +11     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the horizontal swipe behavior and refactors animation handling in the frontend components. The changes address issues with swipe gesture tracking, animation artifacts, and performance.

Changes:

  • Fixed horizontal swipe initiation by ignoring initial distance traveled before swipe starts
  • Replaced CSS easing with JavaScript-controlled animations for better touch tracking
  • Improved swipe detection by relaxing element checks in hasSomethingToDoWithXSwipe
  • Refactored MkPullToRefresh to use requestAnimationFrame instead of setInterval for better performance

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
packages/frontend/src/components/MkSwiper.vue Refactored horizontal swipe handling with improved tracking, effective distance calculation, and requestAnimationFrame-based animations
packages/frontend/src/components/MkPullToRefresh.vue Replaced setInterval with requestAnimationFrame for animation loop and added cancellation support
Comments suppressed due to low confidence (1)

packages/frontend/src/components/MkSwiper.vue:249

  • On line 244, when currentTabIndex.value === 0, accessing props.tabs[currentTabIndex.value - 1] (i.e., props.tabs[-1]) is undefined in JavaScript. The short-circuit evaluation prevents the error from occurring, but the logic is checking a non-existent array element. Similarly on line 247, when at the last index, props.tabs[currentTabIndex.value + 1] accesses beyond array bounds. While JavaScript returns undefined for out-of-bounds access and the conditions work correctly due to short-circuiting, this could be confusing. Consider checking bounds before accessing array elements for clarity: (currentTabIndex.value > 0 && props.tabs[currentTabIndex.value - 1]?.onClick) and (currentTabIndex.value < props.tabs.length - 1 && props.tabs[currentTabIndex.value + 1]?.onClick).
	if (currentTabIndex.value === 0 || props.tabs[currentTabIndex.value - 1].onClick) {
		distanceX = Math.min(distanceX, 0);
	}
	if (currentTabIndex.value === props.tabs.length - 1 || props.tabs[currentTabIndex.value + 1].onClick) {
		distanceX = Math.max(distanceX, 0);
	}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

packages/frontend/src/components/MkPullToRefresh.vue:293

  • The onUnmounted cleanup doesn't handle the case where mouseup or touchend event listeners might still be registered on the window. These listeners are added with once: true on lines 116 and 135, but if the component unmounts before the user releases their mouse/touch, these listeners will remain until they're triggered. Consider removing these listeners explicitly in onUnmounted to prevent potential memory leaks or unexpected behavior after unmount.
onUnmounted(() => {
	if (moveBySystemCancel != null) {
		moveBySystemCancel();
		moveBySystemCancel = null;
	}
	moveBySystemRafId = null;
	// pull中にwindowへ登録したリスナーが残るのを防ぐ
	window.removeEventListener('mousemove', onMouseMove);
	window.removeEventListener('touchmove', onTouchMove);
	unlockDownScroll();
	if (rootEl.value) rootEl.value.removeEventListener('mousedown', moveStartByMouse);
	if (rootEl.value) rootEl.value.removeEventListener('touchstart', moveStartByTouch);
	if (rootEl.value) rootEl.value.removeEventListener('touchend', toggleScrollLockOnTouchEnd);
});

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.


if (swipeAborted) return;

if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

movingByPointer calls hasSomethingToDoWithXSwipe(event.target as HTMLElement) without verifying that event.target is a non-null HTMLElement. If the pointer events are retargeted (e.g. to a Text node, SVGElement, or null), this will throw at runtime inside hasSomethingToDoWithXSwipe (uses tagName/getComputedStyle). Add the same instanceof HTMLElement guard used in moveStartByPointer (or use event.currentTarget).

Suggested change
if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
const target = event.target;
if (!(target instanceof HTMLElement)) return;
if (hasSomethingToDoWithXSwipe(target)) return;

Copilot uses AI. Check for mistakes.

if (!isSwiping.value) return;

if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moveEndByPointer also passes event.target as HTMLElement into hasSomethingToDoWithXSwipe without checking the target type/nullability. Pointerup targets can be outside the component (or non-HTMLElement), which can lead to a runtime error. Consider guarding with event.target instanceof HTMLElement (or switching to event.currentTarget).

Suggested change
if (hasSomethingToDoWithXSwipe(event.target as HTMLElement)) return;
const target = event.currentTarget ?? event.target;
if (!(target instanceof HTMLElement)) {
resetState();
return;
}
if (hasSomethingToDoWithXSwipe(target)) return;

Copilot uses AI. Check for mistakes.
Comment on lines +361 to +367
function moveCancelByPointer(event: PointerEvent) {
if (event.pointerType === 'mouse') return;
if (activePointerId != null && event.pointerId !== activePointerId) return;
resetState();
closeContent().finally(() => {
isSwipingForClass.value = false;
});
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moveCancelByPointer runs for lostpointercapture, but its guard allows execution even when activePointerId is already null (e.g. after a normal pointerup where resetState() already ran). This can trigger an extra closeContent() call that cancels/restarts the release animation unnecessarily. Tighten the guard to only handle cancel when isTracking is true and event.pointerId === activePointerId (or track the last captured id separately), and/or release pointer capture explicitly on end/cancel and drop the lostpointercapture handler if it’s redundant.

Copilot uses AI. Check for mistakes.
@kakkokari-gtyih
Copy link
Contributor Author

/preview

@kakkokari-gtyih
Copy link
Contributor Author

スワイプした時の感触が軽すぎるのでなんか入れる

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

packages/frontend Client side specific issue/PR size:L This PR changes 100-499 lines, ignoring generated files.

Projects

Development

Successfully merging this pull request may close these issues.

2 participants