Vue

Action Sheet를 트랜지션 + slot으로 만들기

frontChoi 2025. 3. 9. 14:04
반응형

Action Sheet

회사에서 내부 개발중에 웹뷰에 들어가는 웹을 개발하는 작업을 하게 되었고,

보통 모바일에서는 ActionSheet가 필요하다

여기서 말하는 Action Sheet는 밑에서 부터 위로 올라오는 Sheet이다

 

여기서 들어가는 애니메이션이 2가지가 들어간다

  • Dim 처리 되어있는 부분인 Opacity를 0 =>1
  • Sheet영역이 위치를 -100% => 0%

 

 

트랜지션으로 FadeIn/FadeOut , Bottom To Top/ Top to Bottom 만들기

 

🛝 트랜지션으로 FadeIn / FadeOut

처음으로 fadeout / fadeIn 시킬 css를 작성한다.

fade-enter-from : 트랜지션 시작전 

fade-enter-active : 애니메이션 시작되면 발동하며, opactiy 를 0.5초동안 변하는것을 보여준다(opacity 0 => 1)

 

fade-leave-to : 애니메이션 종료

fade-leave-active: 애니메이션 종료하면 발동되며, opactiy 를 0.5초동안 변하는것을 보여준다(opacity 1 => 0)

/* Fade In Animation */
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s ease;
}

.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}

 

다음으로는 vue에 Transition을 선언하고 하위에 v-if으로 감싸준다

<template>
  <!-- Fade In / Fade Out -->
  <Transition :name="'fade'">
    <div class="actionshhet-wrap" v-if="isOpenActionSheetContainer"></div>
  </Transition>
</template>
<script setup lang="ts">
/* 1.props으로 부터 ActionSheet를 보여줄지에 대한 값을 받드록 한다. */

type ActionSheet2Props = {
  isshowactionsheet?: boolean
}

//2. 기본값 세팅
const props = withDefaults(defineProps<ActionSheet2Props>(), {
  isshowactionsheet: false,
})

import { ref, watch } from 'vue'

const isOpenActionSheetContainer = ref(false)


//3.props값이 true으로 변경될 경우, isOpenActionSheetContainer = true를 통해 
// Opacity 0 => 1 로 변경하도록 한다.
watch(
  () => props.isshowactionsheet,
  (val) => {
    if (val) {
      isOpenActionSheetContainer.value = true
    }
  },
)

</script>

Opacity 0 => 1

 

 

🛝 트랜지션으로 Bottom To Top/ Top to Bottom

이번에는 Opacity가 진입 이후에 Bottom To Top / Top To Bottom으로 가게끔 할 것이다.

bottom to top / top to bottom 을 css쪽에 선언한다

bottom to top : -100% => 0% 으로 나오게

top to bottom : 0% => -100% 

/* BottomToTop In Animation */
.bottomtotop-enter-active,
.bottomtotop-leave-active {
  transition: 0.5s;
  position: absolute;
  bottom: 0;
}

.bottomtotop-enter-from,
.bottomtotop-leave-to {
  transition: 0.5s;
  position: absolute;
  bottom: -100%;
}

 

이번에는 뒤에 Opacity가 활성화 된 이후에 Bottom To. Top이 와야 하므로

상위 Transition에 enter 이벤트를 설정한다(onRootEnter함수)

 

<template>
  <!-- 1.onRootEnter는 최상위 트랜지션이 활성화 되면 발동한다.-->
  <Transition :name="'fade'" @enter="onRootEnter">
    <div class="actionshhet-wrap" v-if="isOpenActionSheetContainer">
      <Transition name="bottomtotop">
        <div v-if="isBottomToTop" class="bottomsheet" ref="actionsheet"></div>
      </Transition>
    </div>
  </Transition>
</template>
<script setup lang="ts">
type ActionSheet2Props = {
  isshowactionsheet?: boolean
}

const props = withDefaults(defineProps<ActionSheet2Props>(), {
  isshowactionsheet: false,
})

import { ref, watch } from 'vue'

const isOpenActionSheetContainer = ref(false)
const isBottomToTop = ref(false)

/**
 * @description 진입 애니메이션 시작
 */
 <!-- 2.onRootEnter은 하위 트랜지션을 활성화 하기 위해 isBottomToTop 선언 및 true으로 변경한다 -->
function onRootEnter() {
  // BottomToTop 뜨도록 값 변경
  isBottomToTop.value = true
}

watch(
  () => props.isshowactionsheet,
  (val) => {
    if (val) {
      isOpenActionSheetContainer.value = true
    }
  },
)
</script>
<style scoped src="./actionsheet2.css"></style>

 

그렇다면 "상위 트랜지션 발동 => onRootEnter 호출 => 하위 트랜지션 발동" 으로 인하여 

아래 처럼 애니메이션이 적용된다

 

상위 트랜지션 : Opacity 0 => 1 , 1 => 0

하위 트랜지션 : BottomToTop -100% => 0%, 0% => -100%

 

🦶 액션시트 종료시에는 하위트랜지션 종료 => 상위트랜지션 종료

액션 시트 종료시에는 하위가 먼저종료 => 상위 종료가 되도록 하였다

일단 하위영역(하얀색 부분)을 제외하고 클릭하면, 닫히도록 해야한다.

 

1. onClicOutside에 onOutSideClick함수를 등록한다

2. onOutSideClick는 emit을 호출하고, emit를 통해 상위 컴퍼넌트에서 isshowactionsheet를 false시켜준다

3. watch 부분에서 isshowactionsheet가 false가 되는것이 감지되고, isBottomToTop을 false를 시켜준다

4. onLeave함수를 통해 상위 트랜지션 종료를 위해 isOpenActionSheetContainer를 false 시켜준다

<template>
  <!-- Fade In / Fade Out -->
  <Transition :name="'fade'" @enter="onRootEnter">
    <div class="actionshhet-wrap" v-if="isOpenActionSheetContainer">
      <Transition name="bottomtotop" @leave="onLeave">
        <div v-if="isBottomToTop" class="bottomsheet" ref="actionsheet"></div>
      </Transition>
    </div>
  </Transition>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { onClickOutside } from '@vueuse/core'
import { useTemplateRef } from 'vue'

//1. onClickOutSide를 통해 하얀색 영역 제외클릭하는 이벤트 등록
const target = useTemplateRef<HTMLElement>('actionsheet')
onClickOutside(target, onOutSideClick)
type ActionSheet2Props = {
  isshowactionsheet?: boolean
}
type ActionSheetEmits = {
  (event: 'outsidelick'): void
}

const props = withDefaults(defineProps<ActionSheet2Props>(), {
  isshowactionsheet: false,
})
const emits = defineEmits<ActionSheetEmits>()

const isOpenActionSheetContainer = ref(false)
const isBottomToTop = ref(false)

/**
 * @description 진입 애니메이션 시작
 */
function onRootEnter() {
  // BottomToTop 뜨도록 값 변경
  isBottomToTop.value = true
}

/**
 * @description 엘리멘트가 사라질때 동작하는 이벤트
 */
function onLeave() {
  isOpenActionSheetContainer.value = false
}

/**
 * @description 하얀색 부분 외부 클릭시 이벤트
 * @param e PointerEvent
 */
// 2. onOutSideClick 동작 이벤트이며, emit을 호출한다
function onOutSideClick(e: PointerEvent) {
  if (e.target) {
    emits('outsidelick')
  }
}

watch(
  () => props.isshowactionsheet,
  (val) => {
    if (val) {
      isOpenActionSheetContainer.value = true
    } else {
      //3. emits('outsidelick')를 통해 props.isshowactionsheet가 false으로 바뀌고
      // isBottomToTop = false 으로 바꿈으로써 종료시킨다
      isBottomToTop.value = false
    }
  },
)
</script>
<style scoped src="./actionsheet2.css"></style>

 

아래처럼 액션시트가 사라지는것을 볼 수 있다.



Slot을 통하여 템플릿화 시키기

ActionSheet의 헤더,바디,푸터를 템플릿을 하도록 하였다.

<template>
  <Transition :name="'fade'" @enter="onRootEnter">
    <div class="actionshhet-wrap" v-if="isOpenActionSheetContainer">
      <Transition name="bottomtotop" @leave="onLeave">
        <div v-if="isBottomToTop" class="bottomsheet" ref="actionsheet">
          <div class="top-bar"></div>
          <div class="bottomsheet-header">
            <h1 class="header-title">
              <!-- 헤더 -->
              <slot name="headetitle"></slot>
            </h1>
          </div>
          <div class="bottomsheet-body">
            <!-- 바디 -->
            <slot name="bottomsheetbody"></slot>
          </div>
          <div class="bottomsheet-footer">
            <!-- 푸터 -->
            <slot name="bottomsheetfooter"></slot>
          </div>
        </div>
      </Transition>
    </div>
  </Transition>
</template>

 

빨간색이 헤더이며, 파란색이 바디, 검은색이 푸터이다

반응형