Дата публикации
26 Мая 2021
Дата изменения
26 Мая 2021
Уникальных просмотров
7.674

Оглавление

Модальное окно — один из важнейших элементов пользовательского интерфейса. Он позволяет отображать что-либо или получать ответ от пользователя, не переходя на другие страницы. В данной статье, я создам компонент модального окна на Vue.js, который можно будет повторно использовать на каждой странице.

У компонента будет несколько важных преимуществ:

Создание шаблона HTML / CSS

Для начала создаем компонент с любыми именем, базовой разметкой и стилями для отображения модального окна:

<template>
  <div class="simple-modal">
    <div class="simple-modal-backdrop">
      <div class="simple-modal-container">
        <div class="simple-modal-content">
          <header class="simple-modal-header">
            Modal title
          </header>
          <section class="simple-modal-body">
            Modal body
          </section>
          <footer class="simple-modal-footer">
            <button type="button">
              Close
            </button>
          </footer>
        </div>
      </div>
    </div>
  </div>
</template>

<style lang="scss">
  .simple-modal {
    &-backdrop {
      position: fixed;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
      width: 100%;
      height: 100%;
      background-color: rgba(0, 0, 0, 0.8);
      transition: opacity 0.3s ease;
      z-index: 9999;
    }

    &-container {
      position: fixed;
      top: 0;
      right: 0;
      left: 0;
      bottom: 0;
      width: auto;
      margin: 16px;
    }

    &-content {
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      width: 100%;
      max-width: 500px;
      margin: 1.75rem auto;
      padding: 20px 30px;
      border-radius: 5px;
      color: #000;
      background-color: #fff;
      transform: translate(0, 0);
      transition: all 0.3s ease;
      box-sizing: border-box;
    }

    &-header {
      padding-bottom: 16px;
      font-size: 25px;
      text-align: center;
    }

    &-footer {
      display: flex;
      flex-direction: column;
      justify-content: center;
      height: 80px;
      text-align: center;
    }
  }
</style>

Закрытие модального окна

Теперь необходимо создать функционал для закрытия модального окна. В компоненте модального окна создаем метод closeModal, в нем с помощью emit функции триггерим родительский компонент, который слушает close слушатель:

<script>
export default {
  name: 'Modal',
  methods: {
    closeModal() {
      this.$emit('close');
    },
  },
};
</script>

В родительском компоненте, вешаем слушатель close, который будет переключать состояние модального окна с помощью метода toggleModal:

<template>
  <div class="home">
    <Modal
      v-show="isShowModal"
      @close="toggleModal"
    />
    <button @click="toggleModal">
      Open Modal
    </button>
  </div>
</template>

<script>
import Modal from '@/components/app/Modals/Modal';

export default {
  components: {
    Modal,
  },
  data: () => ({
    isShowModal: false,
  }),
  methods: {
    toggleModal() {
      this.isShowModal = !this.isShowModal;
    },
  },
};
</script>

Теперь в компоненте модального окна нужно повесить события закрытия модального окна на background и кнопку Close. Однако теперь, если кликнуть внутри модального окна, оно закроется. Происходит это, потому что метод закрытия модального окна, который мы повесили на background срабатывает и на дочерних элементах. Мы можем это исправить, повесив директиву @click.stop на контент самого модального окна:

<template>
  <div class="simple-modal">
    <div
      class="simple-modal-backdrop"
      @click="closeModal"
    >
      <div class="simple-modal-container">
        <div
          class="simple-modal-content"
          @click.stop
        >
          <header class="simple-modal-header">
            Modal title
          </header>
          <section class="simple-modal-body">
            Modal body
          </section>
          <footer class="simple-modal-footer">
            <button
              type="button"
              @click="closeModal"
            >
              Close
            </button>
          </footer>
        </div>
      </div>
    </div>
  </div>
</template>

Закрытие модального окна при нажатии клавиши Esc

Для решения этой задачи, в компоненте модального окна нам нужно будет добавить еще один метод, а так же создать и уничтожить слушатель на событие keydown. В методе нужно будет сделать две проверки, на то что нажата именно клавиша Esc и что состояние модального окна равно true, иначе даже при закрытом модальном окне, при нажатии на Esc будет переключаться его состояние:

<script>
export default {
  name: 'PAModal',
  props: {
    show: {
      type: Boolean,
      default: false,
    },
  },
  mounted() {
    window.addEventListener('keydown', this.escCloseModal);
  },
  destroy() {
    window.removeEventListener('keydown', this.escCloseModal);
  },
  methods: {
    closeModal() {
      this.$emit('close');
    },
    escCloseModal(e) {
      if (this.show && e.key === 'Escape') {
        this.closeModal();
      }
    },
  },
};
</script>

Как видите, мы добавили первый props, который будет передавать в компонент его текущее состояние для того, чтобы проходила проверка в методе escCloseModal. В родительском компоненте прокинем в props состояние модального окна:

<Modal
  v-show="isShowModal"
  :show="isShowModal"
  @close="toggleModal"
/>

Использование слотов для рендера контента

Для вставки html разметки или любого контента в модальное окно, мы будем использовать слоты. Для этого немного модифицируем компонент модального окна и пропишем 3 слота для header, body и footer:

<template>
  <div class="simple-modal">
    <div
      class="simple-modal-backdrop"
      @click="closeModal"
    >
      <div class="simple-modal-container">
        <div
          class="simple-modal-content"
          @click.stop
        >
          <header class="simple-modal-header">
            <slot name="header">
              Modal title
            </slot>
          </header>
          <section class="simple-modal-body">
            <slot name="body">
              Modal body
            </slot>
          </section>
          <footer class="simple-modal-footer">
            <slot name="footer" />
            <button
              type="button"
              @click="closeModal"
            >
              Close
            </button>
          </footer>
        </div>
      </div>
    </div>
  </div>
</template>

В слотах указано содержимое по умолчанию. Давайте в родительском компоненте пропишем уникальное содержимое:

<template>
  <div class="home">
    <Modal
      v-show="isShowModal"
      :show="isShowModal"
      @close="toggleModal"
    >
      <template #header>
        Наши правила по работе
      </template>
      <template #body>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
      </template>
    </Modal>
    <button @click="toggleModal">
      Open Modal
    </button>
  </div>
</template>

Добавление прокрутки для контента

Если содержимое контента модального окна достаточно большое, модальное окно некрасиво растянется. Чтобы этого избежать, нам нужно предусмотреть возможность добавления прокрутки к контенту модального окна. Сделаем это следующим образом:

Добавляем props:

scrollable: {
  type: Boolean,
  default: false,
},

Новый css селектор:

&-scrollable {
  height: 500px;
  overflow-y: scroll;
}

В шаблонах модального окна добавляем байнд класса:

<div
  class="simple-modal-content"
  :class="{ 'simple-modal-scrollable': scrollable }"
  @click.stop
>

Теперь переключая props в родительском компоненте, можно добавить к модальному окну прокрутку.

Анимация модального окна

Теперь нам нужно сделать плавную анимацию появления и скрытия модального окна. Обрамляем всю разметку в тег transition с названием стилей анимации:

<template>
  <transition name="simple-modal">
    <div class="simple-modal">
      <div
        class="simple-modal-backdrop"
        @click="closeModal"
      >
        <div class="simple-modal-container">
          <div
            class="simple-modal-content"
            :class="{ 'simple-modal-scrollable': scrollable }"
            @click.stop
          >
            <header class="simple-modal-header">
              <slot name="header">
                Modal title
              </slot>
            </header>
            <section class="simple-modal-body">
              <slot name="body">
                Modal body
              </slot>
            </section>
            <footer class="simple-modal-footer">
              <slot name="footer" />
              <button
                type="button"
                @click="closeModal"
              >
                Close
              </button>
            </footer>
          </div>
        </div>
      </div>
    </div>
  </transition>
</template>

И пишем саму анимацию на чистом css:

.simple-modal-enter,
.simple-modal-leave-to {
  opacity: 0;
}

.simple-modal-enter-active,
.simple-modal-leave-active {
  transition: opacity 0.2s ease;
}

Добавление accessibility c ARIA attributes

Добавление role = "dialog" поможет идентифицировать компонент модального окна как диалог приложения, отделенный от остальной части пользовательского интерфейса.

Нам нужно правильно разметить модальное окно с помощью атрибутов aria-labelledby и aria-describeby, а так же добавить атрибут aria-label к кнопке закрытия модального окна. Но сделаем мы это через props, чтобы указывать уникальные идентификаторы для атрибутов:

headerId: {
  type: String,
  required: true,
  default: null,
},
bodyId: {
  type: String,
  required: true,
  default: null,
},

Ну и раскидать props в разметке модального окна:

<template>
  <transition name="simple-modal">
    <div class="simple-modal">
      <div
        class="simple-modal-backdrop"
        @click="closeModal"
      >
        <div class="simple-modal-container">
          <div
            class="simple-modal-content"
            :class="{ 'simple-modal-scrollable': scrollable }"
            role="dialog"
            :aria-labelledby="headerId"
            :aria-describedby="bodyId"
            @click.stop
          >
            <header
              :id="headerId"
              class="simple-modal-header"
            >
              <slot
                :id="bodyId"
                name="header"
              >
                Modal title
              </slot>
            </header>
            <section class="simple-modal-body">
              <slot name="body">
                Modal body
              </slot>
            </section>
            <footer class="simple-modal-footer">
              <slot name="footer" />
              <button
                type="button"
                @click="closeModal"
              >
                Close
              </button>
            </footer>
          </div>
        </div>
      </div>
    </div>
  </transition>
</template>

В родительском компоненте допишем props:

<template>
  <div class="home">
    <Modal
      v-show="isShowModal"
      :show="isShowModal"
      :scrollable="true"
      header-id="modalHeader"
      body-id="modalBody"
      @close="toggleModal"
    >
      <template #header>
        Наши правила по работе
      </template>
      <template #body>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
      </template>
    </Modal>
    <button @click="toggleModal">
      Open Modal
    </button>
  </div>
</template>

Гибкая стилизация модального окна

Теперь реализуем возможность менять оформление модального окна. Для этого создадим новый props:

modalClass: {
  type: String,
  default: 'simple-modal',
},

В разметке заменим заглавный класс модалки:

<template>
  <transition :name="modalClass">
    <div :class="modalClass">
      <div
        :class="`${modalClass}-backdrop`"
        @click="closeModal"
      >
        <div :class="[{'simple-modal-scrollable': scrollable}, `${modalClass}-container`]">
          <div
            :class="`${modalClass}-content`"
            role="dialog"
            :aria-labelledby="headerId"
            :aria-describedby="bodyId"
            @click.stop
          >
            <header
              :id="headerId"
              :class="`${modalClass}-header`"
            >
              <slot
                :id="bodyId"
                name="header"
              >
                Modal title
              </slot>
            </header>
            <section :class="`${modalClass}-body`">
              <slot name="body">
                Modal body
              </slot>
            </section>
            <footer :class="`${modalClass}-footer`">
              <slot name="footer" />
              <button
                type="button"
                @click="closeModal"
              >
                Close
              </button>
            </footer>
          </div>
        </div>
      </div>
    </div>
  </transition>
</template>

Теперь мы можем задать класс для модалки, описать его стили и использовать один компонент для разного оформления модальных окон.

Финальный вариант

Компонент модального окна:

<template>
  <transition :name="modalClass">
    <div :class="modalClass">
      <div
        :class="`${modalClass}-backdrop`"
        @click="closeModal"
      >
        <div :class="[{'simple-modal-scrollable': scrollable}, `${modalClass}-container`]">
          <div
            :class="`${modalClass}-content`"
            role="dialog"
            :aria-labelledby="headerId"
            :aria-describedby="bodyId"
            @click.stop
          >
            <header
              :id="headerId"
              :class="`${modalClass}-header`"
            >
              <slot
                :id="bodyId"
                name="header"
              >
                Modal title
              </slot>
            </header>
            <section :class="`${modalClass}-body`">
              <slot name="body">
                Modal body
              </slot>
            </section>
            <footer :class="`${modalClass}-footer`">
              <slot name="footer" />
              <button
                type="button"
                @click="closeModal"
              >
                Close
              </button>
            </footer>
          </div>
        </div>
      </div>
    </div>
  </transition>
</template>

<script>
export default {
  name: 'PAModal',
  props: {
    show: {
      type: Boolean,
      default: false,
    },
    scrollable: {
      type: Boolean,
      default: false,
    },
    headerId: {
      type: String,
      required: true,
      default: null,
    },
    bodyId: {
      type: String,
      required: true,
      default: null,
    },
    modalClass: {
      type: String,
      default: 'simple-modal',
    },
  },
  mounted() {
    window.addEventListener('keydown', this.escCloseModal);
  },
  destroy() {
    window.removeEventListener('keydown', this.escCloseModal);
  },
  methods: {
    closeModal() {
      this.$emit('close');
    },
    escCloseModal(e) {
      if (this.show && e.key === 'Escape') {
        this.closeModal();
      }
    },
  },
};
</script>

<style lang="scss">
  .simple-modal {
    &-backdrop {
      position: fixed;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
      width: 100%;
      height: 100%;
      background-color: rgba(0, 0, 0, 0.8);
      transition: opacity 0.3s ease;
      z-index: 9999;
    }

    &-container {
      position: fixed;
      top: 0;
      right: 0;
      left: 0;
      bottom: 0;
      width: auto;
      margin: 16px;
    }

    &-scrollable {
      overflow-x: hidden;
      overflow-y: auto;
    }

    &-content {
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      width: 100%;
      max-width: 500px;
      margin: 1.75rem auto;
      padding: 20px 30px;
      border-radius: 5px;
      color: #000;
      background-color: #fff;
      box-sizing: border-box;
      transform: translate(0, 0);
      transition: all 0.3s ease;
    }

    &-header {
      padding-bottom: 16px;
      font-size: 25px;
      text-align: center;
    }

    &-footer {
      display: flex;
      flex-direction: column;
      justify-content: center;
      height: 80px;
      text-align: center;
    }

    &-enter,
    &-leave-to {
      opacity: 0;
    }

    &-enter-active,
    &-leave-active {
      transition: opacity 0.2s ease;
    }
  }
</style>

Вызов компонента модального окна в родительском компоненте:

<template>
  <div class="home">
    <Modal
      v-show="isShowModal2"
      :show="isShowModal2"
      :scrollable="true"
      header-id="modalHeader"
      body-id="modalBody"
      @close="toggleModal"
    >
      <template #header>
        Наши правила по работе
      </template>
      <template #body>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
        <p>
          Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consequuntur debitis deserunt
          doloremque ducimus eum facilis iure labore laborum, odio officia optio pariatur, placeat
          quos rem, rerum sapiente sed temporibus velit.
        </p>
      </template>
    </Modal>
    <button @click="toggleModal">
      Open Modal
    </button>
  </div>
</template>

<script>
import Modal from '@/components/app/Modals/Modal';

export default {
  components: {
    Modal,
  },
  data: () => ({
    isShowModal: false,
  }),
  methods: {
    toggleModal() {
      this.isShowModal = !this.isShowModal;
    },
  },
};
</script>

Вот и все. Получился простой, но довольно гибкий компонент модального окна. 

Благодарность автору

Если по какой-либо причине вы хотите поблагодарить автора данного ресурса, вы можете это сделать одним из удобных для вас способов ниже.

Один из самых популярных способов поблагодарить автора, воспользоваться сервисом Яндекс.Деньги.

Время работы: 0,1240 s
Время запросов: 0,1240 s
Количество запросов: 28
Источник: cache