From e804a299e068afcbfb2fe36cc429b9a502a7b2d9 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Sun, 5 Dec 2021 20:01:52 +0900
Subject: [PATCH] fix(client): better hover detection

---
 .../client/src/components/notification.vue    |  8 +--
 .../components/reactions-viewer.reaction.vue  |  8 +--
 .../client/src/components/renote-button.vue   |  8 +--
 packages/client/src/scripts/use-tooltip.ts    | 49 ++++++++++++++-----
 4 files changed, 41 insertions(+), 32 deletions(-)

diff --git a/packages/client/src/components/notification.vue b/packages/client/src/components/notification.vue
index 15d36f5a6..37a88edc6 100644
--- a/packages/client/src/components/notification.vue
+++ b/packages/client/src/components/notification.vue
@@ -19,10 +19,6 @@
 				:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
 				:custom-emojis="notification.note.emojis"
 				:no-style="true"
-				@touchstart.passive="onReactionMouseover"
-				@mouseover="onReactionMouseover"
-				@mouseleave="onReactionMouseleave"
-				@touchend="onReactionMouseleave"
 			/>
 		</div>
 	</div>
@@ -151,7 +147,7 @@ export default defineComponent({
 			os.api('users/groups/invitations/reject', { invitationId: props.notification.invitation.id });
 		};
 
-		const { onMouseover: onReactionMouseover, onMouseleave: onReactionMouseleave } = useTooltip((showing) => {
+		useTooltip(reactionRef, (showing) => {
 			os.popup(XReactionTooltip, {
 				showing,
 				reaction: props.notification.reaction ? props.notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : props.notification.reaction,
@@ -170,8 +166,6 @@ export default defineComponent({
 			rejectFollowRequest,
 			acceptGroupInvitation,
 			rejectGroupInvitation,
-			onReactionMouseover,
-			onReactionMouseleave,
 			elRef,
 			reactionRef,
 		};
diff --git a/packages/client/src/components/reactions-viewer.reaction.vue b/packages/client/src/components/reactions-viewer.reaction.vue
index 44c6e9809..a1de99f01 100644
--- a/packages/client/src/components/reactions-viewer.reaction.vue
+++ b/packages/client/src/components/reactions-viewer.reaction.vue
@@ -6,10 +6,6 @@
 	class="hkzvhatu _button"
 	:class="{ reacted: note.myReaction == reaction, canToggle }"
 	@click="toggleReaction()"
-	@touchstart.passive="onMouseover"
-	@mouseover="onMouseover"
-	@mouseleave="onMouseleave"
-	@touchend="onMouseleave"
 >
 	<XReactionIcon :reaction="reaction" :custom-emojis="note.emojis"/>
 	<span>{{ count }}</span>
@@ -90,7 +86,7 @@ export default defineComponent({
 			if (!props.isInitial) anime();
 		});
 
-		const { onMouseover, onMouseleave } = useTooltip(async (showing) => {
+		useTooltip(buttonRef, async (showing) => {
 			const reactions = await os.api('notes/reactions', {
 				noteId: props.note.id,
 				type: props.reaction,
@@ -113,8 +109,6 @@ export default defineComponent({
 			buttonRef,
 			canToggle,
 			toggleReaction,
-			onMouseover,
-			onMouseleave,
 		};
 	},
 });
diff --git a/packages/client/src/components/renote-button.vue b/packages/client/src/components/renote-button.vue
index 280283ec6..446686de1 100644
--- a/packages/client/src/components/renote-button.vue
+++ b/packages/client/src/components/renote-button.vue
@@ -3,10 +3,6 @@
 	ref="buttonRef"
 	class="eddddedb _button canRenote"
 	@click="renote()"
-	@touchstart.passive="onMouseover"
-	@mouseover="onMouseover"
-	@mouseleave="onMouseleave"
-	@touchend="onMouseleave"
 >
 	<i class="fas fa-retweet"></i>
 	<p v-if="count > 0" class="count">{{ count }}</p>
@@ -42,7 +38,7 @@ export default defineComponent({
 
 		const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
 
-		const { onMouseover, onMouseleave } = useTooltip(async (showing) => {
+		useTooltip(buttonRef, async (showing) => {
 			const renotes = await os.api('notes/renotes', {
 				noteId: props.note.id,
 				limit: 11
@@ -87,8 +83,6 @@ export default defineComponent({
 			buttonRef,
 			canRenote,
 			renote,
-			onMouseover,
-			onMouseleave,
 		};
 	},
 });
diff --git a/packages/client/src/scripts/use-tooltip.ts b/packages/client/src/scripts/use-tooltip.ts
index f72dcb162..0df4baca7 100644
--- a/packages/client/src/scripts/use-tooltip.ts
+++ b/packages/client/src/scripts/use-tooltip.ts
@@ -1,8 +1,16 @@
-import { Ref, ref } from 'vue';
-import { isScreenTouching, isTouchUsing } from './touch';
+import { Ref, ref, watch } from 'vue';
 
-export function useTooltip(onShow: (showing: Ref<boolean>) => void) {
+export function useTooltip(
+	elRef: Ref<HTMLElement | { $el: HTMLElement } | null | undefined>,
+	onShow: (showing: Ref<boolean>) => void,
+): void {
 	let isHovering = false;
+
+	// iOS(Androidも?)では、要素をタップした直後に(おせっかいで)mouseoverイベントを発火させたりするため、それを無視するためのフラグ
+	// 無視しないと、画面に触れてないのにツールチップが出たりし、ユーザビリティが損なわれる
+	// TODO: 一度でもタップすると二度とマウスでツールチップ出せなくなるのをどうにかする 定期的にfalseに戻すとか...?
+	let shouldIgnoreMouseover = false;
+
 	let timeoutId: number;
 
 	let changeShowingState: (() => void) | null;
@@ -11,11 +19,6 @@ export function useTooltip(onShow: (showing: Ref<boolean>) => void) {
 		close();
 		if (!isHovering) return;
 
-		// iOS(Androidも?)では、要素をタップした直後に(おせっかいで)mouseoverイベントを発火させたりするため、その対策
-		// これが無いと、画面に触れてないのにツールチップが出たりしてしまう
-		// TODO: タッチとマウス両方使っている環境では、マウス操作でツールチップ出せなくなるのをどうにかする
-		if (isTouchUsing && !isScreenTouching) return;
-
 		const showing = ref(true);
 		onShow(showing);
 		changeShowingState = () => {
@@ -32,6 +35,7 @@ export function useTooltip(onShow: (showing: Ref<boolean>) => void) {
 
 	const onMouseover = () => {
 		if (isHovering) return;
+		if (shouldIgnoreMouseover) return;
 		isHovering = true;
 		timeoutId = window.setTimeout(open, 300);
 	};
@@ -43,8 +47,31 @@ export function useTooltip(onShow: (showing: Ref<boolean>) => void) {
 		close();
 	};
 
-	return {
-		onMouseover,
-		onMouseleave,
+	const onTouchstart = () => {
+		shouldIgnoreMouseover = true;
+		if (isHovering) return;
+		isHovering = true;
+		timeoutId = window.setTimeout(open, 300);
 	};
+
+	const onTouchend = () => {
+		if (!isHovering) return;
+		isHovering = false;
+		window.clearTimeout(timeoutId);
+		close();
+	};
+
+	const stop = watch(elRef, () => {
+		if (elRef.value) {
+			stop();
+			const el = elRef.value instanceof Element ? elRef.value : elRef.value.$el;
+			el.addEventListener('mouseover', onMouseover, { passive: true });
+			el.addEventListener('mouseleave', onMouseleave, { passive: true });
+			el.addEventListener('touchstart', onTouchstart, { passive: true });
+			el.addEventListener('touchend', onTouchend, { passive: true });
+		}
+	}, {
+		immediate: true,
+		flush: 'post',
+	});
 }