fix(frontend): modalの中でtabsを使用する際にハイライトが変な位置に出る問題を修正

This commit is contained in:
kakkokari-gtyih 2025-10-04 17:00:23 +09:00
parent 3954837cfa
commit 3b0ec46990
3 changed files with 49 additions and 16 deletions

View File

@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.transition_modal_leaveTo]: transitionName === 'modal',
[$style.transition_send_leaveTo]: transitionName === 'send',
})"
:duration="transitionDuration" appear @afterLeave="onClosed" @enter="emit('opening')" @afterEnter="onOpened"
:duration="transitionDuration" appear @afterLeave="onClosed" @enter="onOpening" @afterEnter="onOpened"
>
<div v-show="manualShowing != null ? manualShowing : showing" ref="modalRootEl" v-hotkey.global="keymap" :class="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
<div data-cy-bg :data-cy-transparent="isEnableBgTransparent" class="_modalBg" :class="[$style.bg, { [$style.bgTransparent]: isEnableBgTransparent }]" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
@ -97,6 +97,14 @@ const emit = defineEmits<{
provide(DI.inModal, true);
const isTransitioning = ref((() => {
if (!prefer.s.animation) return false;
if (props.manualShowing === false) return false;
return true;
})());
provide(DI.modalTransitioning, isTransitioning);
const maxHeight = ref<number>();
const fixed = ref(false);
const transformOrigin = ref('center');
@ -285,8 +293,14 @@ const align = () => {
content.value.style.top = top + 'px';
};
const onOpening = () => {
emit('opening');
isTransitioning.value = true;
};
const onOpened = () => {
emit('opened');
isTransitioning.value = false;
// contentnextTick
nextTick(() => {

View File

@ -7,9 +7,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="[$style.tabs, { [$style.centered]: props.centered }]">
<div :class="$style.tabsInner">
<button
v-for="t in tabs" :ref="(el) => tabRefs[t.key] = (el as HTMLElement)" v-tooltip.noDelay="t.title"
class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: prefer.s.animation }]"
@mousedown="(ev) => onTabMousedown(t, ev)" @click="(ev) => onTabClick(t, ev)"
v-for="t in tabs"
:ref="(el) => tabRefs[t.key] = (el as HTMLElement)"
v-tooltip.noDelay="t.title"
class="_button"
:class="[$style.tab, {
[$style.active]: t.key != null && t.key === tab,
[$style.animate]: prefer.s.animation,
}]"
@mousedown="(ev) => onTabMousedown(t, ev)"
@click="(ev) => onTabClick(t, ev)"
>
<div :class="$style.tabInner">
<i v-if="t.icon" :class="[$style.tabIcon, t.icon]"></i>
@ -36,8 +43,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
export type Tab = {
key: string;
export type Tab<K = string> = {
key: K;
onClick?: (ev: MouseEvent) => void;
iconOnly?: boolean;
title: string;
@ -45,17 +52,17 @@ export type Tab = {
};
</script>
<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, useTemplateRef, watch } from 'vue';
<script lang="ts" setup generic="const T extends Tab">
import { nextTick, onMounted, onUnmounted, useTemplateRef, ref, watch, inject } from 'vue';
import { prefer } from '@/preferences.js';
import { DI } from '@/di.js';
const props = withDefaults(defineProps<{
tabs?: Tab[];
tab?: string;
tabs?: T[];
centered?: boolean;
tabHighlightUpper?: boolean;
}>(), {
tabs: () => ([] as Tab[]),
tabs: () => ([] as T[]),
});
const emit = defineEmits<{
@ -63,6 +70,10 @@ const emit = defineEmits<{
(ev: 'tabClick', key: string);
}>();
const tab = defineModel<T['key']>('tab');
const modalTransitioning = inject(DI.modalTransitioning, ref(false));
const tabHighlightEl = useTemplateRef('tabHighlightEl');
const tabRefs: Record<string, HTMLElement | null> = {};
@ -88,7 +99,7 @@ function onTabClick(t: Tab, ev: MouseEvent): void {
}
function renderTab() {
const tabEl = props.tab ? tabRefs[props.tab] : undefined;
const tabEl = tab.value ? tabRefs[tab.value] : undefined;
if (tabEl && tabHighlightEl.value && tabHighlightEl.value.parentElement) {
// offsetWidth offsetLeft getBoundingClientRect 使
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
@ -99,7 +110,7 @@ function renderTab() {
}
}
let entering = false;
let entering = true;
async function enter(el: Element) {
if (!(el instanceof HTMLElement)) return;
@ -138,14 +149,21 @@ function afterLeave(el: Element) {
}
onMounted(() => {
watch([() => props.tab, () => props.tabs], () => {
watch([tab, () => props.tabs], () => {
nextTick(() => {
if (entering) return;
renderTab();
});
}, {
immediate: true,
});
const modalTransitioningWatchStop = watch(modalTransitioning, (to) => {
if (!to) {
entering = false;
renderTab();
modalTransitioningWatchStop();
}
}, { immediate: true });
});
onUnmounted(() => {

View File

@ -16,5 +16,6 @@ export const DI = {
currentStickyBottom: Symbol() as InjectionKey<Ref<number>>,
mfmEmojiReactCallback: Symbol() as InjectionKey<(emoji: string) => void>,
inModal: Symbol() as InjectionKey<boolean>,
modalTransitioning: Symbol() as InjectionKey<Ref<boolean>>,
inAppSearchMarkerId: Symbol() as InjectionKey<Ref<string | null>>,
};