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_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; | ||||
| 
 | ||||
| 	// contentの子要素にアクセスするためレンダリングの完了を待つ必要がある(nextTickが必要) | ||||
| 	nextTick(() => { | ||||
|  |  | |||
|  | @ -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(() => { | ||||
|  |  | |||
|  | @ -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>>, | ||||
| }; | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue