From e6f3dd81ba4ad492158e278ecb0f9d9893bf2958 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Wed, 9 Aug 2023 09:08:47 +0900
Subject: [PATCH] =?UTF-8?q?fix(frontend):=20MkPopupMenu=E3=81=8C=E3=83=89?=
 =?UTF-8?q?=E3=83=AD=E3=83=AF=E3=83=BC=E3=81=A7=E5=AD=90=E3=83=A1=E3=83=8B?=
 =?UTF-8?q?=E3=83=A5=E3=83=BC=E3=81=AE=E5=87=BA=E7=8F=BE=E3=81=A8=E5=90=8C?=
 =?UTF-8?q?=E6=99=82=E3=81=ABpopup=E3=82=92resolve=E3=81=95=E3=81=9B?=
 =?UTF-8?q?=E3=82=8B=E3=81=AE=E3=82=92=E3=82=84=E3=82=81=E3=81=95=E3=81=9B?=
 =?UTF-8?q?=E3=82=8B=20(#11441)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix(frontend): MkPopupMenuがドロワーで子メニューの出現と同時にpopupをresolveさせるのをやめさせる

* fix

* noCache

* :v:

* fix

* ????

* a

* a

* :v:

* fix emoji picker

* ?????

* close

* 1

* fix2

* :v:

* fix

* :v:

* :v:

* :v:

* preferClick

* :v:

* fix lint

* a

* rm nocache
---
 packages/frontend/src/components/MkMenu.vue   | 64 +++++++++++--------
 .../frontend/src/components/MkPopupMenu.vue   | 46 ++++++++++++-
 .../frontend/src/scripts/get-note-menu.ts     |  4 +-
 .../frontend/src/scripts/get-user-menu.ts     |  4 +-
 packages/frontend/src/types/menu.ts           |  2 +-
 5 files changed, 86 insertions(+), 34 deletions(-)

diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 3d4e45b1f4..3b20856e12 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -39,11 +39,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)" />
 				<span :class="$style.switchText">{{ item.text }}</span>
 			</button>
-			<button v-else-if="item.type === 'parent'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="showChildren(item, $event)">
+			<div v-else-if="item.type === 'parent'" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)">
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 				<span>{{ item.text }}</span>
 				<span :class="$style.caret"><i class="ti ti-chevron-right ti-fw"></i></span>
-			</button>
+			</div>
 			<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
 				<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
 				<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
@@ -56,19 +56,24 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</span>
 	</div>
 	<div v-if="childMenu">
-		<XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned"/>
+		<XChild ref="child" :items="childMenu" :targetElement="childTarget" :rootElement="itemsEl" showing @actioned="childActioned" @close="close(false)"/>
 	</div>
 </div>
 </template>
 
-<script lang="ts" setup>
-import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
+<script lang="ts">
+import { Ref, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
 import { focusPrev, focusNext } from '@/scripts/focus';
 import MkSwitchButton from '@/components/MkSwitch.button.vue';
-import { MenuItem, InnerMenuItem, OuterMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu';
+import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
+import { isTouchUsing } from '@/scripts/touch';
 
+const childrenCache = new WeakMap<MenuParent, MenuItem[]>();
+</script>
+
+<script lang="ts" setup>
 const XChild = defineAsyncComponent(() => import('./MkMenu.child.vue'));
 
 const props = defineProps<{
@@ -82,6 +87,7 @@ const props = defineProps<{
 
 const emit = defineEmits<{
 	(ev: 'close', actioned?: boolean): void;
+	(ev: 'hide'): void;
 }>();
 
 let itemsEl = $shallowRef<HTMLDivElement>();
@@ -98,6 +104,8 @@ let keymap = $computed(() => ({
 
 let childShowingItem = $ref<MenuItem | null>();
 
+let preferClick = isTouchUsing || props.asDrawer;
+
 watch(() => props.items, () => {
 	const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined);
 
@@ -117,7 +125,7 @@ watch(() => props.items, () => {
 	immediate: true,
 });
 
-let childMenu = ref<MenuItem[] | null>();
+const childMenu = ref<MenuItem[] | null>();
 let childTarget = $shallowRef<HTMLElement | null>();
 
 function closeChild() {
@@ -130,11 +138,11 @@ function childActioned() {
 	close(true);
 }
 
-function onGlobalMousedown(event: MouseEvent) {
+const onGlobalMousedown = (event: MouseEvent) => {
 	if (childTarget && (event.target === childTarget || childTarget.contains(event.target))) return;
 	if (child && child.checkHit(event)) return;
 	closeChild();
-}
+};
 
 let childCloseTimer: null | number = null;
 function onItemMouseEnter(item) {
@@ -146,31 +154,30 @@ function onItemMouseLeave(item) {
 	if (childCloseTimer) window.clearTimeout(childCloseTimer);
 }
 
-let childrenCache = new WeakMap<MenuParent, OuterMenuItem[]>();
 async function showChildren(item: MenuParent, ev: MouseEvent) {
-	const children = ref<OuterMenuItem[]>([]);
-	if (childrenCache.has(item)) {
-		children.value = childrenCache.get(item)!;
-	} else {
-		if (typeof item.children === 'function') {
-			children.value = [{
-				type: 'pending',
-			}];
-			Promise.resolve(item.children()).then(x => {
-				children.value = x;
-				childrenCache.set(item, x);
-			});
+	const children = await (async () => {
+		if (childrenCache.has(item)) {
+			return childrenCache.get(item)!;
 		} else {
-			children.value = item.children;
+			if (typeof item.children === 'function') {
+				return Promise.resolve(item.children());
+			} else {
+				return item.children;
+			}
 		}
-	}
+	})();
+
+	childrenCache.set(item, children);
 
 	if (props.asDrawer) {
-		os.popupMenu(children, ev.currentTarget ?? ev.target);
-		close();
+		os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
+			emit('close');
+		});
+		emit('hide');
 	} else {
 		childTarget = ev.currentTarget ?? ev.target;
-		childMenu = children;
+		// これでもリアクティビティは保たれる
+		childMenu.value = children;
 		childShowingItem = item;
 	}
 }
@@ -200,7 +207,7 @@ function switchItem(item: MenuSwitch & { ref: any }) {
 onMounted(() => {
 	if (props.viaKeyboard) {
 		nextTick(() => {
-			focusNext(itemsEl.children[0], true, false);
+			if (itemsEl) focusNext(itemsEl.children[0], true, false);
 		});
 	}
 
@@ -348,6 +355,7 @@ onBeforeUnmount(() => {
 	}
 
 	&.parent {
+		pointer-events: auto;
 		display: flex;
 		align-items: center;
 		cursor: default;
diff --git a/packages/frontend/src/components/MkPopupMenu.vue b/packages/frontend/src/components/MkPopupMenu.vue
index 6e7656f745..ee7dbecf05 100644
--- a/packages/frontend/src/components/MkPopupMenu.vue
+++ b/packages/frontend/src/components/MkPopupMenu.vue
@@ -4,13 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkModal ref="modal" v-slot="{ type, maxHeight }" :zPriority="'high'" :src="src" :transparentBg="true" @click="modal.close()" @close="emit('closing')" @closed="emit('closed')">
-	<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="modal.close()"/>
+<MkModal ref="modal" v-slot="{ type, maxHeight }" :manualShowing="manualShowing" :zPriority="'high'" :src="src" :transparentBg="true" @click="click" @close="onModalClose" @closed="onModalClosed">
+	<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="onMenuClose" @hide="hide"/>
 </MkModal>
 </template>
 
 <script lang="ts" setup>
-import { } from 'vue';
+import { ref } from 'vue';
 import MkModal from './MkModal.vue';
 import MkMenu from './MkMenu.vue';
 import { MenuItem } from '@/types/menu';
@@ -29,6 +29,46 @@ const emit = defineEmits<{
 }>();
 
 let modal = $shallowRef<InstanceType<typeof MkModal>>();
+const manualShowing = ref(true);
+const hiding = ref(false);
+
+function click() {
+	close();
+}
+
+function onModalClose() {
+	emit('closing');
+}
+
+function onMenuClose() {
+	close();
+	if (hiding.value) {
+		// hidingであればclosedを発火
+		emit('closed');
+	}
+}
+
+function onModalClosed() {
+	if (!hiding.value) {
+		// hidingでなければclosedを発火
+		emit('closed');
+	}
+}
+
+function hide() {
+	manualShowing.value = false;
+	hiding.value = true;
+
+	// closeは呼ぶ必要がある
+	modal?.close();
+}
+
+function close() {
+	manualShowing.value = false;
+
+	// closeは呼ぶ必要がある
+	modal?.close();
+}
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index 20cea45ee3..8e29fc0c9b 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -418,7 +418,9 @@ export function getNoteMenu(props: {
 
 	const cleanup = () => {
 		if (_DEV_) console.log('note menu cleanup', cleanups);
-		cleanups.forEach(cleanup => cleanup());
+		for (const cl of cleanups) {
+			cl();
+		}
 	};
 
 	return {
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index 445560b0c3..69a6f75c12 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -330,7 +330,9 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
 
 	const cleanup = () => {
 		if (_DEV_) console.log('user menu cleanup', cleanups);
-		cleanups.forEach(cleanup => cleanup());
+		for (const cl of cleanups) {
+			cl();
+		}
 	};
 
 	return {
diff --git a/packages/frontend/src/types/menu.ts b/packages/frontend/src/types/menu.ts
index b2ba6290c4..66061fcd70 100644
--- a/packages/frontend/src/types/menu.ts
+++ b/packages/frontend/src/types/menu.ts
@@ -16,7 +16,7 @@ export type MenuA = { type: 'a', href: string, target?: string, download?: strin
 export type MenuUser = { type: 'user', user: Misskey.entities.User, active?: boolean, indicate?: boolean, action: MenuAction };
 export type MenuSwitch = { type: 'switch', ref: Ref<boolean>, text: string, disabled?: boolean };
 export type MenuButton = { type?: 'button', text: string, icon?: string, indicate?: boolean, danger?: boolean, active?: boolean, avatar?: Misskey.entities.User; action: MenuAction };
-export type MenuParent = { type: 'parent', text: string, icon?: string, children: OuterMenuItem[] | (() => Promise<OuterMenuItem[]> | OuterMenuItem[]) };
+export type MenuParent = { type: 'parent', text: string, icon?: string, children: MenuItem[] | (() => Promise<MenuItem[]> | MenuItem[]) };
 
 export type MenuPending = { type: 'pending' };