fix(frontend): modalの中でtabsを使用する際にハイライトが変な位置に出る問題を修正
This commit is contained in:
parent
3954837cfa
commit
3b0ec46990
|
@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
[$style.transition_modal_leaveTo]: transitionName === 'modal',
|
[$style.transition_modal_leaveTo]: transitionName === 'modal',
|
||||||
[$style.transition_send_leaveTo]: transitionName === 'send',
|
[$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 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>
|
<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);
|
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 maxHeight = ref<number>();
|
||||||
const fixed = ref(false);
|
const fixed = ref(false);
|
||||||
const transformOrigin = ref('center');
|
const transformOrigin = ref('center');
|
||||||
|
@ -285,8 +293,14 @@ const align = () => {
|
||||||
content.value.style.top = top + 'px';
|
content.value.style.top = top + 'px';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onOpening = () => {
|
||||||
|
emit('opening');
|
||||||
|
isTransitioning.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
const onOpened = () => {
|
const onOpened = () => {
|
||||||
emit('opened');
|
emit('opened');
|
||||||
|
isTransitioning.value = false;
|
||||||
|
|
||||||
// contentの子要素にアクセスするためレンダリングの完了を待つ必要がある(nextTickが必要)
|
// contentの子要素にアクセスするためレンダリングの完了を待つ必要がある(nextTickが必要)
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
|
|
@ -7,9 +7,16 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<div :class="[$style.tabs, { [$style.centered]: props.centered }]">
|
<div :class="[$style.tabs, { [$style.centered]: props.centered }]">
|
||||||
<div :class="$style.tabsInner">
|
<div :class="$style.tabsInner">
|
||||||
<button
|
<button
|
||||||
v-for="t in tabs" :ref="(el) => tabRefs[t.key] = (el as HTMLElement)" v-tooltip.noDelay="t.title"
|
v-for="t in tabs"
|
||||||
class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: prefer.s.animation }]"
|
:ref="(el) => tabRefs[t.key] = (el as HTMLElement)"
|
||||||
@mousedown="(ev) => onTabMousedown(t, ev)" @click="(ev) => onTabClick(t, ev)"
|
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">
|
<div :class="$style.tabInner">
|
||||||
<i v-if="t.icon" :class="[$style.tabIcon, t.icon]"></i>
|
<i v-if="t.icon" :class="[$style.tabIcon, t.icon]"></i>
|
||||||
|
@ -36,8 +43,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export type Tab = {
|
export type Tab<K = string> = {
|
||||||
key: string;
|
key: K;
|
||||||
onClick?: (ev: MouseEvent) => void;
|
onClick?: (ev: MouseEvent) => void;
|
||||||
iconOnly?: boolean;
|
iconOnly?: boolean;
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -45,17 +52,17 @@ export type Tab = {
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup generic="const T extends Tab">
|
||||||
import { nextTick, onMounted, onUnmounted, useTemplateRef, watch } from 'vue';
|
import { nextTick, onMounted, onUnmounted, useTemplateRef, ref, watch, inject } from 'vue';
|
||||||
import { prefer } from '@/preferences.js';
|
import { prefer } from '@/preferences.js';
|
||||||
|
import { DI } from '@/di.js';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
tabs?: Tab[];
|
tabs?: T[];
|
||||||
tab?: string;
|
|
||||||
centered?: boolean;
|
centered?: boolean;
|
||||||
tabHighlightUpper?: boolean;
|
tabHighlightUpper?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
tabs: () => ([] as Tab[]),
|
tabs: () => ([] as T[]),
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
@ -63,6 +70,10 @@ const emit = defineEmits<{
|
||||||
(ev: 'tabClick', key: string);
|
(ev: 'tabClick', key: string);
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const tab = defineModel<T['key']>('tab');
|
||||||
|
|
||||||
|
const modalTransitioning = inject(DI.modalTransitioning, ref(false));
|
||||||
|
|
||||||
const tabHighlightEl = useTemplateRef('tabHighlightEl');
|
const tabHighlightEl = useTemplateRef('tabHighlightEl');
|
||||||
const tabRefs: Record<string, HTMLElement | null> = {};
|
const tabRefs: Record<string, HTMLElement | null> = {};
|
||||||
|
|
||||||
|
@ -88,7 +99,7 @@ function onTabClick(t: Tab, ev: MouseEvent): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTab() {
|
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) {
|
if (tabEl && tabHighlightEl.value && tabHighlightEl.value.parentElement) {
|
||||||
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
|
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
|
||||||
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
|
// 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) {
|
async function enter(el: Element) {
|
||||||
if (!(el instanceof HTMLElement)) return;
|
if (!(el instanceof HTMLElement)) return;
|
||||||
|
@ -138,14 +149,21 @@ function afterLeave(el: Element) {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
watch([() => props.tab, () => props.tabs], () => {
|
watch([tab, () => props.tabs], () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (entering) return;
|
if (entering) return;
|
||||||
renderTab();
|
renderTab();
|
||||||
});
|
});
|
||||||
}, {
|
|
||||||
immediate: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const modalTransitioningWatchStop = watch(modalTransitioning, (to) => {
|
||||||
|
if (!to) {
|
||||||
|
entering = false;
|
||||||
|
renderTab();
|
||||||
|
modalTransitioningWatchStop();
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|
|
@ -16,5 +16,6 @@ export const DI = {
|
||||||
currentStickyBottom: Symbol() as InjectionKey<Ref<number>>,
|
currentStickyBottom: Symbol() as InjectionKey<Ref<number>>,
|
||||||
mfmEmojiReactCallback: Symbol() as InjectionKey<(emoji: string) => void>,
|
mfmEmojiReactCallback: Symbol() as InjectionKey<(emoji: string) => void>,
|
||||||
inModal: Symbol() as InjectionKey<boolean>,
|
inModal: Symbol() as InjectionKey<boolean>,
|
||||||
|
modalTransitioning: Symbol() as InjectionKey<Ref<boolean>>,
|
||||||
inAppSearchMarkerId: Symbol() as InjectionKey<Ref<string | null>>,
|
inAppSearchMarkerId: Symbol() as InjectionKey<Ref<string | null>>,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue