Vue.js 3.0 Responsible Navigation Drawer 예제

|

화면 크기에 따라 동작이 바뀌는 Navigation Drawer

화면의 너비(width)에 따라 슬라이딩 및 오버레이 동작으로 작동하는 Navigation Drawer 입니다. 브라우저의 폭이 넓으면 슬라이딩(Sliding, Push) 방식으로 동작하며, 폭이 좁은 경우에는 오버레이(Overlay, OffCanvas)로 동작합니다.

BootStrap과 scss를 사용하지만, 실제 동작은 순수 Vue와 css로 동작합니다. Vue 3.0의 Composition API를 활용해서 구현되었으며, 화면 크기는 Media Query를 이용해서 확인합니다.

필요 패키지 설치

npm i bootstrap --save
npm i bootstrap-icons --save
npm i sass --save
npm i sass-loader --save

소스 코드들

main.js

import { createApp } from "vue";
import App from "./App.vue";
import bootstrap from "bootstrap";
import "bootstrap-icons/font/bootstrap-icons.css";

createApp(App).use(bootstrap).mount("#app");

App.vue

<template>
  <div id="app">
    <NavigationDrawer
      id="drawer"
      :class="isDrawerOpened ? 'opened' : 'closed'"
      @menu-selected="drawerMenuSelected"
    />
    <div
      id="drawer-dismiss-panel"
      v-if="isDrawerOpened"
      @click="closeDrawer"
    ></div>
    <div id="main" :class="{ 'drawer-opened': isDrawerOpened }">
      <AppBar id="appbar" @toggle-drawer="onToggleDrawer" />

      <div id="content">This is a navigation drawer sample.</div>
    </div>
  </div>
</template>

<script setup>
import NavigationDrawer from "@/components/NavigationDrawer.vue";
import AppBar from "@/components/AppBar.vue";
import { ref } from "@vue/reactivity";

const isDrawerOpened = ref(false);

const onToggleDrawer = () => {
  isDrawerOpened.value = !isDrawerOpened.value;
};

const closeDrawer = () => {
  isDrawerOpened.value = false;
};

const drawerMenuSelected = (menu) => {
  console.log(`Menu(${menu}) is selected !!`);
};
</script>

<style lang="scss" scoped>
$primary: #009688;
$secondary: #4db6ac;
$dark: #004d40;
$accent: #64ffda;
$translucent: rgba(0, 0, 0, 0.7);

#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;

  display: flex;
  flex-direction: row;
  background: black;
  color: white;

  width: 100vw;
  height: 100vh;
}

#drawer {
  position: absolute;
  z-index: 10;
  width: 180px;
  margin-left: -180px;
  height: 100vh;
  overflow: hidden;
  transition: 0.3s;
}
#drawer.opened {
  margin-left: 0px;
}
#drawer.closed {
  margin-left: -180px;
}
#drawer-dismiss-panel {
  display: none;
}

#main {
  position: absolute;
  z-index: 1;
  width: 100vw;
  height: 100vh;
  transition: 0.3s;
}
#main.drawer-opened {
  margin-left: 180px;
  width: calc(100% - 180px);
}
#main.drawer-closed {
  margin-left: 0px;
  width: 100%;
}

#appbar {
  margin: 0;
  background: $primary;
}

#content {
  margin: 0;
  color: black;
  background-color: whitesmoke;
  width: 100%;
  height: 100%;
}

@media screen and (max-width: 480px) {
  #drawer {
    position: fixed;
  }
  #drawer-dismiss-panel {
    display: inline-block;
    position: fix;
    z-index: 5;
    width: 100vw;
    height: 100vh;
    background-color: $translucent;
  }
  #main {
    display: block;
    position: absolute;
  }
  #main.drawer-opened {
    margin-left: 0px;
    width: 100%;
  }
}
</style>

components/AppBar.vue

<template>
  <div id="appbar">
    <div
      id="hamburg-menu"
      class="align-horizontal-center align-vertical-center"
      @click="toggleDrawer"
    >
      <i class="bi bi-list"></i>
    </div>
    <div id="title" class="align-vertical-center">AppBar Title</div>
    <div id="action-items">
      <i class="action-item bi bi-heart"></i>
      <i class="action-item bi bi-three-dots-vertical"></i>
    </div>
  </div>
</template>

<script setup>
import { defineEmits } from "vue";

const emit = defineEmits(["toggle-drawer"]);
const toggleDrawer = () => {
  emit("toggle-drawer");
};
</script>

<style lang="scss" scoped>
$accent: #64ffda;

.align-horizontal-center {
  display: flex;
  flex-direction: row;
  justify-content: center;
}

.align-vertical-center {
  display: flex;
  flex-direction: row;
  align-items: center;
}

#appbar {
  display: flex;
  flex-direction: row;
  height: 48px;
}

#hamburg-menu {
  width: 48px;
  height: 48px;
}
#hamburg-menu:hover {
  color: black;
  background: $accent;
}

#title {
  flex: 1;
  height: 48px;
}

#action-items {
  display: flex;
  flex-direction: row;
  height: 48px;
}

.action-item {
  width: 48px;
  height: 48px;
  display: flex;
  justify-content: center;
  align-items: center;
}
.action-item:hover {
  color: black;
  background: $accent;
}
</style>

components/NavigationDrawer.vue

<template>
  <div id="drawer">
    <div id="header">SnowDeer Drawer</div>
    <div id="content">
      <div class="menu-item">
        <i class="bi bi-house"></i>
        <span>Home</span>
      </div>
      <div class="menu-item">
        <i class="bi bi-gift"></i>
        <span>Hello</span>
      </div>
      <div class="menu-item">
        <i class="bi bi-info-square"></i>
        <span>About</span>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
$primary: #009688;
$secondary: #4db6ac;
$dark: #004d40;
$accent: #64ffda;
$translucent: rgba(0, 0, 0, 0.7);

#header {
  padding-top: 20px;
  padding-left: 12px;
  height: 64px;
  font-size: large;
  font-weight: bold;
  color: $accent;
  background: $dark;
  white-space: nowrap;
}

#content {
  background: $secondary;
  height: 100vh;
}

.menu-item {
  padding: 16px;
  cursor: pointer;
}

.menu-item:hover {
  color: black;
  background: turquoise;
}
</style>