fix(frontend): MkPopupMenuがドロワーで子メニューの出現と同時にpopupをresolveさせるのをやめさせる (#11441)
* fix(frontend): MkPopupMenuがドロワーで子メニューの出現と同時にpopupをresolveさせるのをやめさせる * fix * noCache * ✌️ * fix * ???? * a * a * ✌️ * fix emoji picker * ????? * close * 1 * fix2 * ✌️ * fix * ✌️ * ✌️ * ✌️ * preferClick * ✌️ * fix lint * a * rm nocache
This commit is contained in:
parent
ec229dbd3b
commit
e6f3dd81ba
5 changed files with 86 additions and 34 deletions
|
@ -39,11 +39,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)" />
|
<MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)" />
|
||||||
<span :class="$style.switchText">{{ item.text }}</span>
|
<span :class="$style.switchText">{{ item.text }}</span>
|
||||||
</button>
|
</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>
|
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
|
||||||
<span>{{ item.text }}</span>
|
<span>{{ item.text }}</span>
|
||||||
<span :class="$style.caret"><i class="ti ti-chevron-right ti-fw"></i></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)">
|
<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>
|
<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"/>
|
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
|
||||||
|
@ -56,19 +56,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="childMenu">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts">
|
||||||
import { defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { Ref, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
import { focusPrev, focusNext } from '@/scripts/focus';
|
||||||
import MkSwitchButton from '@/components/MkSwitch.button.vue';
|
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 * as os from '@/os';
|
||||||
import { i18n } from '@/i18n';
|
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 XChild = defineAsyncComponent(() => import('./MkMenu.child.vue'));
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
@ -82,6 +87,7 @@ const props = defineProps<{
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(ev: 'close', actioned?: boolean): void;
|
(ev: 'close', actioned?: boolean): void;
|
||||||
|
(ev: 'hide'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let itemsEl = $shallowRef<HTMLDivElement>();
|
let itemsEl = $shallowRef<HTMLDivElement>();
|
||||||
|
@ -98,6 +104,8 @@ let keymap = $computed(() => ({
|
||||||
|
|
||||||
let childShowingItem = $ref<MenuItem | null>();
|
let childShowingItem = $ref<MenuItem | null>();
|
||||||
|
|
||||||
|
let preferClick = isTouchUsing || props.asDrawer;
|
||||||
|
|
||||||
watch(() => props.items, () => {
|
watch(() => props.items, () => {
|
||||||
const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined);
|
const items: (MenuItem | MenuPending)[] = [...props.items].filter(item => item !== undefined);
|
||||||
|
|
||||||
|
@ -117,7 +125,7 @@ watch(() => props.items, () => {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
let childMenu = ref<MenuItem[] | null>();
|
const childMenu = ref<MenuItem[] | null>();
|
||||||
let childTarget = $shallowRef<HTMLElement | null>();
|
let childTarget = $shallowRef<HTMLElement | null>();
|
||||||
|
|
||||||
function closeChild() {
|
function closeChild() {
|
||||||
|
@ -130,11 +138,11 @@ function childActioned() {
|
||||||
close(true);
|
close(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onGlobalMousedown(event: MouseEvent) {
|
const onGlobalMousedown = (event: MouseEvent) => {
|
||||||
if (childTarget && (event.target === childTarget || childTarget.contains(event.target))) return;
|
if (childTarget && (event.target === childTarget || childTarget.contains(event.target))) return;
|
||||||
if (child && child.checkHit(event)) return;
|
if (child && child.checkHit(event)) return;
|
||||||
closeChild();
|
closeChild();
|
||||||
}
|
};
|
||||||
|
|
||||||
let childCloseTimer: null | number = null;
|
let childCloseTimer: null | number = null;
|
||||||
function onItemMouseEnter(item) {
|
function onItemMouseEnter(item) {
|
||||||
|
@ -146,31 +154,30 @@ function onItemMouseLeave(item) {
|
||||||
if (childCloseTimer) window.clearTimeout(childCloseTimer);
|
if (childCloseTimer) window.clearTimeout(childCloseTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
let childrenCache = new WeakMap<MenuParent, OuterMenuItem[]>();
|
|
||||||
async function showChildren(item: MenuParent, ev: MouseEvent) {
|
async function showChildren(item: MenuParent, ev: MouseEvent) {
|
||||||
const children = ref<OuterMenuItem[]>([]);
|
const children = await (async () => {
|
||||||
if (childrenCache.has(item)) {
|
if (childrenCache.has(item)) {
|
||||||
children.value = childrenCache.get(item)!;
|
return 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);
|
|
||||||
});
|
|
||||||
} else {
|
} 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) {
|
if (props.asDrawer) {
|
||||||
os.popupMenu(children, ev.currentTarget ?? ev.target);
|
os.popupMenu(children, ev.currentTarget ?? ev.target).finally(() => {
|
||||||
close();
|
emit('close');
|
||||||
|
});
|
||||||
|
emit('hide');
|
||||||
} else {
|
} else {
|
||||||
childTarget = ev.currentTarget ?? ev.target;
|
childTarget = ev.currentTarget ?? ev.target;
|
||||||
childMenu = children;
|
// これでもリアクティビティは保たれる
|
||||||
|
childMenu.value = children;
|
||||||
childShowingItem = item;
|
childShowingItem = item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -200,7 +207,7 @@ function switchItem(item: MenuSwitch & { ref: any }) {
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.viaKeyboard) {
|
if (props.viaKeyboard) {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
focusNext(itemsEl.children[0], true, false);
|
if (itemsEl) focusNext(itemsEl.children[0], true, false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -348,6 +355,7 @@ onBeforeUnmount(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.parent {
|
&.parent {
|
||||||
|
pointer-events: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
|
@ -4,13 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkModal ref="modal" v-slot="{ type, maxHeight }" :zPriority="'high'" :src="src" :transparentBg="true" @click="modal.close()" @close="emit('closing')" @closed="emit('closed')">
|
<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="modal.close()"/>
|
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :class="{ [$style.drawer]: type === 'drawer' }" @close="onMenuClose" @hide="hide"/>
|
||||||
</MkModal>
|
</MkModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { } from 'vue';
|
import { ref } from 'vue';
|
||||||
import MkModal from './MkModal.vue';
|
import MkModal from './MkModal.vue';
|
||||||
import MkMenu from './MkMenu.vue';
|
import MkMenu from './MkMenu.vue';
|
||||||
import { MenuItem } from '@/types/menu';
|
import { MenuItem } from '@/types/menu';
|
||||||
|
@ -29,6 +29,46 @@ const emit = defineEmits<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let modal = $shallowRef<InstanceType<typeof MkModal>>();
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
|
|
|
@ -418,7 +418,9 @@ export function getNoteMenu(props: {
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
if (_DEV_) console.log('note menu cleanup', cleanups);
|
if (_DEV_) console.log('note menu cleanup', cleanups);
|
||||||
cleanups.forEach(cleanup => cleanup());
|
for (const cl of cleanups) {
|
||||||
|
cl();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -330,7 +330,9 @@ export function getUserMenu(user: misskey.entities.UserDetailed, router: Router
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
if (_DEV_) console.log('user menu cleanup', cleanups);
|
if (_DEV_) console.log('user menu cleanup', cleanups);
|
||||||
cleanups.forEach(cleanup => cleanup());
|
for (const cl of cleanups) {
|
||||||
|
cl();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -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 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 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 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' };
|
export type MenuPending = { type: 'pending' };
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue