Madogiwa Blog

主に技術系の学習メモに使っていきます。

Vue.js: Transitionに合わせて別の要素をアニメーションで動かすMEMO

あるcomponentのTransitionに合わせて別の要素もアニメーションで動かしたい時のメモ📝

オフセット付きのサイドメニューを作成するような際に一方のトランジションに合わせて他の要素をアニメーションを設定して動かしたいと場合には、JavaScriptフックを使うとできた。

JavaScript フック​ JavaScript コンポーネントのイベントを購読することで、トランジション処理にフックすることができます。

<Transition
 @before-enter="onBeforeEnter"
 @enter="onEnter"
 @after-enter="onAfterEnter"
 @enter-cancelled="onEnterCancelled"
 @before-leave="onBeforeLeave"
 @leave="onLeave"
 @after-leave="onAfterLeave"
 @leave-cancelled="onLeaveCancelled"
>
  <!-- ... -->
</Transition>

https://ja.vuejs.org/guide/built-ins/transition.html#javascript-hooks

以下のような感じでTransitionの各イベントをhookして動かしたい要素に対してtransition用のclassを付け替えてあげたりすると実装できた。

<template>
<div v-show="showWrapper" ref="menuWrapperElement" class="navigation-bar__menu-wrapper" @click="showMenu = false">
      <Transition
        name="right"
        @before-enter="handleOnBeforeMenuEnter"
        @enter="handleOnMenuEnter"
        @leave="handleOnMenuLeave"
        @after-leave="handleOnAfterMenuLeave"
      >
        <div v-show="isLogin && showMenu" class="navigation-bar__menu">
<!-- -->
</div>
</template>
<script lang="ts" setup>
const OFFSET_TARGET_SELECTOR = "body";
const TARGET_ENTER_CLASS_NAME = "navigration-bar-target-enter-from";
const TARGET_LEAVE_CLASS_NAME = "navigration-bar-target-leave-to";
const props = defineProps({ isLogin: { type: Boolean, default: false }, isOffSet: { type: Boolean, default: true } });
const showMenu = ref(false);
const showWrapper = ref(false);
const menuWrapperElement = ref<HTMLElement | null>(null);
const offsetTargetElement = ref<HTMLElement | null>(null);

onMounted(() => {
  if (props.isOffSet) offsetTargetElement.value = document.querySelector(OFFSET_TARGET_SELECTOR);
});

const handleOnBeforeMenuEnter = () => {
  showWrapper.value = true;
};

const handleOnMenuEnter = () => {
  if (!props.isOffSet) return;
  transitionTarget(offsetTargetElement.value, "enter");
  // NOTE: bodyにtranslateXを指定するとmenuごとズレてしまうのでmenuに対して反対のoffsetを指定している。
  if (menuWrapperElement.value) menuWrapperElement.value.style.transform = `translateX(${MENU_WIDTH})`;
};

const handleOnMenuLeave = () => {
  if (!props.isOffSet) return;
  transitionTarget(offsetTargetElement.value, "enter", true);
  transitionTarget(offsetTargetElement.value, "leave");
};

const handleOnAfterMenuLeave = () => {
  if (props.isOffSet) transitionTarget(offsetTargetElement.value, "leave", true);
  showWrapper.value = false;
};

const transitionTarget = (
  offsetTargetElement: HTMLElement | null,
  status: "enter" | "leave",
  remove: boolean = false,
) => {
  if (!offsetTargetElement) return;
  const targetClassList = offsetTargetElement.classList;
  const targetClass = status == "enter" ? TARGET_ENTER_CLASS_NAME : TARGET_LEAVE_CLASS_NAME;
  !remove ? targetClassList.add(targetClass) : targetClassList.remove(targetClass);
};

</script>
<style lang="scss">
.navigration-bar-target-leave-to {
  transform: none;
  transition: transform 200ms cubic-bezier(0.17, 0.67, 0.83, 0.67) 0ms;
}

.navigration-bar-target-enter-from {
  /* 右からメニューが表示されるのでメニューの幅(MENU_WIDTH)の幅分左にズラす */
  transform: translateX(-300px);
  transition: transform 200ms cubic-bezier(0.17, 0.67, 0.83, 0.67) 0ms;
}
</style>

もっといいやり方あるのかもしれない 🤔