Комментарии 21
Однозначно в закладки. Сколько не искал, нигде нет нормальной информации о таких случаях. Везде только 1 маленький компонент и ничего более.
Уже намучился писать костыли для модальных окон (не переживайте, это pet-проекты, никакого продакшена), а времени погрузиться во внутреннюю кухню Vue нет (хотя может и стоит найти для этого время).
Спасибо большое за подробную статью. Продолжайте писать дальше о таких моментах.
Да, внутри Vue, не менее интересен, чем снаружи. Рекомендую его всячески ковырять и испытывать на прочность при случае. Откроете много интересных возможностей, о которых не напишут в документации. Впрочем — это применимо для любого инструмента.
Лично для меня, изучать его — одно удовольствие. В данный момент времени — Vue — для меня идеал из себе подобных.
Хотя я постоянно мониторю эту сферу, ведь мир JS так изменчив :)
Писать однозначно буду. И немало.
Понравиться всем невозможно.
Да и нет такой цели. Цель в другом — делиться знаниями с людьми. И конкретно в этой статье речь о Vue. О случаях необычных и нестандартных, по крайне мере для новичка. А верстка здесь дело 10-е.
Но я буду рад, если вы опишите эти недостатки в реализации. Да и по верстке не помешает. Думаю и другим будет интересно.
Код не смотрел, смотрел только демки по ссылке.
Окна ни разу не модальные — TAB-ом фокус вполне себе ходит по «фоновым» элементам, позволяя работать с немодальными контролами:
Смысл статьи не в том, чтобы сделать полностью рабочую версию в виде плагина. В этой статье рассказывается, как именно это можно реализовать, а именно, один переиспользуемый динамический компонент через рендер-функции.
Ситуация с табами очень напоминает историю про хакера и солонки в кафе.
Ситуация с табами очень напоминает историю про хакера и солонки в кафе.
Вы под лозунгом «всё не предусмотришь» предлагаете всё делать по принципу «и так сойдёт»?
Есть описание, что должен представлять из себя модальный диалог:
"...users cannot interact with content outside an active dialog window...", "...That is, Tab and Shift + Tab do not move focus outside the dialog. However, unlike most non-modal dialogs, modal dialogs do not provide means for moving keyboard focus outside the dialog window without closing the dialog...".

Смысл статьи не в том, чтобы сделать полностью рабочую версию в виде плагина. В этой статье рассказывается, как именно это можно реализовать
Не важно, насколько хорошо исполнено что-то, если оно плохо делает то, для чего предназначается.
Согласен с вами. И спасибо за подсказку. Уже все поправил и перезалил.
Вот добавленное в коде
modal-wrapper.js
mounted() {
// подписываемся на событие keydown
    if (typeof document !== 'undefined') {
      document.body.addEventListener('keydown', this.handleTabKey)
    }
}
 destroyed() {
// отписываемся
    if (typeof document !== 'undefined') {
      document.body.removeEventListener('keydown', this.handleTabKey)
    }
},
methods: {
   handleTabKey(e) {
       if (e.keyCode === 9 && this.modals.length) {
         e.preventDefault() // если есть окна, глушим Tab/Shift-Tab
       }
    }
// пока полностью отключаю Tab. Надо подумать, как лучше его глушить только вне активного окна. 
}


и modal.js
mounted() {
    if(this.$el.focus) {
      this.$el.focus() // фокус переводим на окно, при монтировании
    }
},
render(h) {
...
   return h('div', { 
      style,
      attrs: { tabindex: -1}, // цепляем tabindex
      class: ['vu-modal__cmp', {
...
},



пока полностью отключаю Tab. Надо подумать, как лучше его глушить только вне активного окна.
Это как раз понятно:
function nodeContains(parentNode, node) {
  return parentNode && node && (node === parentNode || contains());

  function contains() {
    return parentNode.contains ? parentNode.contains(node) : !!(parentNode.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_CONTAINED_BY);
  }
}

if (nodeContains(activeWindow.node, document.activeElement)) {
  // сфокусированный элемент находится в activeWindow
} else {
  // сфокусированный элемент находится вне activeWindow
}


Но такой подход («глушить tab») не закрывает проблему:
  • При tab-е «снаружи» страницы (с активного контрола браузера) keydown не «стреляет», и происходит фокусировка на первый фокусируемый элемент страницы (в примере это link «Vue-universal-modal», указывающий на "#/")
  • Если не «глушить» tab внутри активного окна (а по-хорошему, глушить его нельзя, иначе пользователь не сможет tab-ом перемещяться по фокусируемым элементам активного окна), то нужно как-то определять момент, когда «сфокусирован элемент внутри окна, но при tab-бировании фокус уйдёт вовне», а вот как это сделать — я уже не очень представляю :)
На досуге буду думать над этой проблемой. Решение всегда можно найти, если хорошо поискать :)

Я делал однопиксельные скрытые поля ввода в начале и конце модального окна, которые при фокусе перекидывали фокус обратно в окно.
Так же, чтобы нельзя было из адресной строки перейти табом к контролам позади модального окна, можно вставить спрятанные поля ввола в начале и конце документа.

Да, интересно. Я правда пока не садился подумать над этой проблемой. Но возможно без костылей здесь не обойтись. Обязательно напишу, когда решу этот момент.
Во-первых — реализация в целом понравилась. Но по-моему это для простых случаев, когда в модал нужно показать уже имеющиеся данные.

Но у меня были задачи, когда нужно в модалку вывести другой компонент + чтобы была возможность дать странице с открытой модалкой свой url.
Пример: таблица со списком пользователей, клик на имя открывает модалку с компонентом UserProfile, который в свою очередь достает инфо по id.
Также компонент UserProfile пользуется и на отдельной странице — профиль пользователя.

Честно, мне пришлось городить свои огороды с named router-view.
Так любой компонент мы и сейчас можем вывести в окне.
А по поводу страницы с модалкой со своим url. Может создавать нужный роут в момент открытия окна? В реальном времени. У vue-router вроде есть возможность динамического добавления. Надо только подумать, как после закрытия окна удалять этот роут. Покопаться в исходниках роутера надо. И так каждый раз. Думаю на производительность приложения сильно этот момент не повлияет.
А для этих динамически добавляемых/удаляемых роутов сделать шаблонную страницу, где будем выводить окно. По сути окна то у нас не привязаны к страницам сейчас. Их обертка висит в корневом шаблоне приложения. Страница-пустышка нужна для роута. Роут нужен для правильной обработки адресной строки в браузере. Как то так, на первый взгляд.

А как быть в случае, если поп-ап является частью формы?


К примеру в нём происходит выбор адреса или режима работы. (реальный случай)
То есть как раз такой случай, когда поп-ап должен иметь доступ к родительской модели. И при этом в разметке он там совсем плохо размещается.


Решал такие задачи через vue-portal, но у него минусов хватает.

Для этого и есть коллбек onClose. Вызываем из формы наше окно с выбором чего-то там, передавая в опциях нужные параметры родительской модели. И в опциях передаем коллбек onClose, в который мы будем при закрытии окна методом this.$modals.close(data) передавать нужный ответ (data) и его уже использовать в родительской модели.
Код для наглядности
// запускаем из формы окно
this.$modals.open({
  component: ModalForm // компонент, отображаемый в окне, где происходит выбор чего-то,
  props: {
    params1, // родительские данные, которые будут использоваться в окне
    params2,
    ...
  },
  onClose(data) { // data мы будем передавать при закрытии окна 
    // здесь мы уже в родительской области видимости, и используем data как нам нужно
    ...
    return false // если здесь мы вернем false, то окно не закроется. Например при каком-то условии, по значениям из data
  }
})

// а в окне, например, при клике по кнопке 'Сохранить' вызываем 
this.$modals.close(data)
// тем самым запуская закрытие окна, но прежде чем окно закроется, запускается коллбек onClose, если он есть в опциях окна, c переданными в него данными

Как-то так, если я правильно понял суть проблемы.

Спасибо за пример.
Это рабочий вариант, но он несёт ряд усложнений.

Нам нужно сделать отдельный компонент, описать в нём логику передачи нужных данных в коллбэке, определить метод в родительском компоненте, который эти данные примет и использует…

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

По большому счёту, все эти пляски с шинами событий, коллбэками, порталами и прочим — просто из-за проблем с вёрсткой. Ну не хочет глубоко вложенный элемент с positon: fixed вырваться от своих родителей и стать сильным и независимым. Добавим transform к родителю и всё, приплыли.

Я использовал оба подхода.
Получалось и примерно ваше решение, и магия с github.com/LinusBorg/portal-vue

Не то, чтобы теорема Эскобара, но всё равно не идеально, везде какой противный компромисс.
В целом, считаю что решение которое предлагаете вы — менее Vue-way (в смысле «вау пара свойств и магия»), но, пожалуй, более адекватно и надежно.
Ну правильно, для каждой ситуации — свое решение.
Но главное преимущества подхода, который используется здесь — позволяет убрать дублирование кода. Вот нужно вам одно и тоже окно на нескольких страницах, вы будете его вставлять на каждую, и само окно станет привязано к данным страницам. Более того, если нужно будет сделать изменения в этом окне, нужно будет лазить по всем местам, где оно используется и делать изменения. А так мы скинули все наши компоненты с окнами в одно место, например в папку modals и все. Одно окно с дефолтными настройками — используем где хотим, и когда надо, через опции заменяем его дефолтные настройки.
Мне например так намного удобнее. И добавить родительский метод, который примет данные, для меня более очевиден. Так как компонент в окне по сути — это отдельная сущность, и изменять данные родителя напрямую, не есть хорошо.
через npm i vue-universal-modal поставить не получилось. Как вообще включить это в проект? Только руками?
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.