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

var tabs = $('.tab').click(function () {
  tabs.hide().filter(this.hash).show();
}).map(function () {
  return $(this.hash)[0];
});

$('.tab:first').click();

Просто, правда? Почти помещается в твит (игнорируя всю библиотеку jQuery…). Тем не менее, он пронизан проблемами, которые делают его далеко не идеальным решением.

Требования: что делает вкладку идеальной?

  1. Весь контент доступен для навигации без JavaScript (совместим с поисковыми роботами и совместим с lo-js).
  2. Роли ARIA.
  3. вкладки - это якорные ссылки, которые:
  • Кликабельно.
  • Есть блочная раскладка.
  • Их href указывает на идентификатор элемента панели.
  • Использует правильный курсор (например, курсор: указатель).
  1. Поскольку вкладки являются интерактивными, пользователь может открываться в новой вкладке / окне, и страница правильно загружается с открытой правой вкладкой.
  2. Щелчок правой кнопкой мыши (и нажатие клавиши Shift) не вызывает выбор вкладки.
  3. Встроенная в браузере кнопка назад / вперед правильно изменяет состояние выбранной вкладки (представьте, что она работает так же, как если бы на месте не было JavaScript).

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

Последние три пункта - это проблемы JavaScript. Давай исследуем это.

Дерьмовый тест

Вот несколько быстрых способов определить, плохо ли реализована система табуляции, как лакмусовая бумажка:

  • Измените вкладку, затем нажмите кнопку «Назад» (или сочетание клавиш), и она сломается.
  • Вкладка не является ссылкой, поэтому я не могу открыть ее в новой вкладке

Для меня эти две основные вещи - минимум, который должна иметь система табуляции.

Почему это важно?

У людей, которые навязывают пользователям свои «родные» приложения, не может быть больше причин, по которым Интернет - отстой. Если что-то столь простое, как вкладка, не работает, очевидно, есть больше возможностей, чтобы подтолкнуть пользователей к закрытому нативному приложению или платформе.

Если вы собираетесь стать веб-разработчиком, одна из ваших обязанностей - поддерживать устоявшиеся парадигмы интерактивности. Это не означает «не вводить новшества». Но это действительно означает: хватит испортить мою прокрутку своими плохо выполненными эффектами прокрутки. ‹/Rant› : дыхание:

Фрагмент URI, абсолютный URL-адрес или строка запроса?

Фрагмент URI (также известный как хэш-бит #) будет использовать mysite.com/config#content для отображения панели content. Полностью адресный URL-адрес будет mysite.com/config/content. Использование строки запроса (путем «фильтрации» страницы): mysite.com/config?tab=content.

Это решение действительно зависит от контекста вашей системы табуляции. Для чего-то вроде вкладок Github для просмотра запроса на вытягивание имеет смысл изменить полный URL-адрес.

Что касается нашей проблемы, я хочу решить проблему, когда страница не выполняет полное обновление URL-адреса, то есть ваш обычный запуск системы табуляции.

Раньше я был из школы использования хеша для отображения правой вкладки, но недавно я изучал, можно ли использовать строку запроса. Самая большая причина в том, что несколько хешей не работают, а разделенные запятыми хеш-фрагменты не имеют смысла для управления несколькими вкладками (поскольку на самом деле они ни на что не ссылаются).

В этой статье я сосредоточусь на использовании единой системы табуляции и хэша в URL-адресе для управления вкладками.

Разметка

Я собираюсь использовать суб-контент, чтобы моя разметка выглядела так (да, это демонстрация кошки…):

<ul class="tabs">
  <li><a class="tab" href="#dizzy">Dizzy</a></li>
  <li><a class="tab" href="#ninja">Ninja</a></li>
  <li><a class="tab" href="#missy">Missy</a></li>
</ul>

<div id="dizzy">
  <!-- panel content -->
</div>
<div id="ninja">
  <!-- panel content -->
</div>
<div id="missy">
  <!-- panel content -->
</div>

Важное примечание в разметке заключается в том, что ссылка, используемая для отдельной вкладки, ссылается на ее содержимое панели с помощью хеша, указывающего на идентификатор на панели. Это позволит нашему контенту подключаться без JavaScript и даст нам множество бесплатных функций, которые мы увидим, когда перейдем к написанию кода.

Системы табуляции, управляемые URL

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

Имея это в виду, давайте приступим к созданию нашего кода. Я предполагаю, что у нас есть библиотека jQuery, но я также предоставил полный код, работающий без библиотеки, ваниль, если хотите, но это зависит от относительно новой (polyfill'able) технологии, такой как classList и набор данных (который обычно имеет IE10 и все остальные браузеры).

Обратите внимание, что я начну с простейшего решения и по мере продвижения буду рефакторировать код, например, в тех местах, где я продолжаю вызывать селекторы jQuery.

function show(id) {
  // remove the selected class from the tabs,
  // and add it back to the one the user selected
  $('.tab').removeClass('selected').filter(function () {
    return (this.hash === id);
  }).addClass('selected');

  // now hide all the panels, then filter to
  // the one we're interested in, and show it
  $('.panel').hide().filter(id).show();
}

$(window).on('hashchange', function () {
  show(location.hash);
});

// initialise by showing the first panel
show('#dizzy');

Это очень хорошо работает для такого маленького кода: http://output.jsbin.com/rimone/. Обратите внимание, что у нас нет обработчиков кликов для пользователя, а кнопка Назад работает сразу после установки.

Однако есть ряд проблем, которые необходимо исправить:

  1. Инициализированная вкладка жестко привязана к первой панели, а не к тому, что указано в URL.
  2. Если в URL-адресе нет хеша, все панели скрыты (и, следовательно, ломаются).
  3. Если вы прокрутите пример до конца, вы найдете «верхнюю» ссылку, нажатие на которую нарушит нашу систему табуляции.
  4. Я специально сделал страницу длинной, чтобы, когда вы нажимаете на вкладку, вы видели, как она прокручивается в верхнюю часть вкладки, что не имеет большого значения, но немного раздражает.

Хотя, судя по нашим критериям в начале этого поста, мы уже решили пункты (4) и (5). Не страшное начало. Давайте теперь решим вопросы с 1 по 3.

Использование URL-адреса для правильной инициализации и защиты от поломки

Вместо того, чтобы произвольно выбирать первую панель из нашей коллекции, код должен читать текущий location.hash и использовать его, если он доступен.

Проблема в следующем: что, если хеш URL-адреса на самом деле не для вкладки?

Решение здесь в том, что нам нужно кэшировать список известных идентификаторов панелей. Фактически, хорошо написанный сценарий DOM не будет постоянно искать узлы в DOM. то есть, когда функция show продолжала вызывать $ (‘. tab’). each (…), это было расточительно. Результат $ (‘. Tab’) должен быть кеширован.

Итак, теперь код соберет все вкладки, затем найдет соответствующие панели из этих вкладок, и мы будем использовать этот список для удвоения значений, которые мы даем функции show (например, во время инициализации).

// collect all the tabs
var tabs = $('.tab');

// get an array of the panel ids (from the anchor hash)
var targets = tabs.map(function () {
  return this.hash;
}).get();

// use those ids to get a jQuery collection of panels
var panels = $(targets.join(','));

function show(id) {
  // if no value was given, let's take the first panel
  if (!id) {
    id = targets[0];
  }
  // remove the selected class from the tabs,
  // and add it back to the one the user selected
  tabs.removeClass('selected').filter(function () {
    return (this.hash === id);
  }).addClass('selected');

  // now hide all the panels, then filter to
  // the one we're interested in, and show it
  panels.hide().filter(id).show();
}

$(window).on('hashchange', function () {
  var hash = location.hash;
  if (targets.indexOf(hash) !== -1) {
    show(hash);
  }
});

// initialise
show(targets.indexOf(location.hash) !== -1 ? location.hash : '');

Суть определения вкладки для инициализации решается в последней строке: есть ли location.hash, есть ли он в нашем списке действительных целей (панелей)? Если да, выберите эту вкладку.

Второй недостаток, который мы видели в исходной демонстрации, заключается в том, что нажатие на ссылку «вверху» приведет к поломке вкладок, потому что сработало событие hashchange и код не проверил переданный хэш. Теперь это происходит, панели не ломаются.

Пока что у нас есть система табуляции, которая:

  • Работает без JavaScript.
  • Поддерживает щелчок правой кнопкой мыши и щелчок с зажатой клавишей Shift (и не выбирает в этих случаях).
  • Загружает правую панель, если вы начинаете с хеша.
  • Встроенная навигация браузера работает.
  • Поддержка клавиатуры работает.

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

Удаление перехода на вкладку

Простите, если вы подумаете, что вам просто нужно перехватить обработчик кликов и вернуть false. Это то, с чего я начал. Только это не выход. Если мы добавим обработчик щелчка, он прервет всю поддержку щелчка правой кнопкой мыши и щелчка Shift.

Может быть и другой способ решить эту проблему, но я нашел его, и он работает. Как я уже сказал, это немного ... волосатое.

Мы собираемся убрать атрибут id с целевой панели, когда пользователь пытается перейти к нему, а затем снова включить его, как только шоу-код начнет работать. Это изменение будет означать, что браузеру некуда будет перейти в данный момент, и он не будет переходить по странице.

Изменение включает в себя следующее:

  1. Добавьте дескриптор клика, который удаляет идентификатор с целевой панели, и кешируйте его в целевой переменной, которую мы будем использовать позже в hashchange (см. Пункт 4).
  2. В том же обработчике кликов установите location.hash равным хешу текущей ссылки - это важно, потому что оно вызывает событие hashchange независимо от того, действительно ли изменился URL, что предотвращает разрыв вкладок (попробуйте сами, удалив эту строку).
  3. Для каждой панели поместите резервную копию атрибута id в свойство данных (я назвал его старым идентификатором).
  4. Когда срабатывает событие hashchange, если у нас есть целевое значение, давайте вернем идентификатор обратно на панель.

Эти изменения приводят к окончательному коду:

/*global $*/

// a temp value to cache *what* we're about to show
var target = null;

// collect all the tabs
var tabs = $('.tab').on('click', function () {
  target = $(this.hash).removeAttr('id');

  // if the URL isn't going to change, then hashchange
  // event doesn't fire, so we trigger the update manually
  if (location.hash === this.hash) {
    // but this has to happen after the DOM update has
    // completed, so we wrap it in a setTimeout 0
    setTimeout(update, 0);
  }
});

// get an array of the panel ids (from the anchor hash)
var targets = tabs.map(function () {
  return this.hash;
}).get();

// use those ids to get a jQuery collection of panels
var panels = $(targets.join(',')).each(function () {
  // keep a copy of what the original el.id was
  $(this).data('old-id', this.id);
});

function update() {
  if (target) {
    target.attr('id', target.data('old-id'));
    target = null;
  }

  var hash = window.location.hash;
  if (targets.indexOf(hash) !== -1) {
    show(hash);
  }
}

function show(id) {
  // if no value was given, let's take the first panel
  if (!id) {
    id = targets[0];
  }
  // remove the selected class from the tabs,
  // and add it back to the one the user selected
  tabs.removeClass('selected').filter(function () {
    return (this.hash === id);
  }).addClass('selected');

  // now hide all the panels, then filter to
  // the one we're interested in, and show it
  panels.hide().filter(id).show();
}

$(window).on('hashchange', update);

// initialise
if (targets.indexOf(window.location.hash) !== -1) {
  update();
} else {
  show();
}

В этой версии http://output.jsbin.com/xilula/ теперь есть все критерии, которые я перечислил в моих исходных критериях, кроме для ролей и доступности aria. На самом деле, получение этой поддержки обходится очень дешево.

Доступность

Эта статья о Вкладках ARIA очень упростила работу системы табуляции, как я хотел.

Задачи были простыми:

  1. Добавьте aria-role set to tab для вкладок и tabpanel для панелей.
  2. Установите aria-control на вкладках так, чтобы они указывали на связанную с ними панель (по идентификатору).
  3. Я использую JavaScript, чтобы добавить tabindex = 0 ко всем элементам вкладки.
  4. Когда я добавляю выбранный класс на вкладку, я также устанавливаю для aria-selected значение true и наоборот, когда я удаляю выбранный класс, я устанавливаю для aria-selected значение false.
  5. Когда я скрываю панели, я добавляю aria-hidden = true, а когда показываю конкретную панель, я устанавливаю aria-hidden = false.

И это все. Очень небольшие изменения, чтобы получить полное подтверждение того, что система табуляции пуленепробиваемая и доступная.

Окончательная версия находится здесь: http://output.jsbin.com/lorovu/ (и версия, отличная от jQuery, как и было обещано: http://jsbin.com/sehuxo/edit?js,output).

В моей версии, в частности, не хватает поддержки курсора клавиатуры. Посетитель должен иметь возможность перемещаться по вкладкам с помощью клавиш курсора. Теоретически это также приведет к добавлению истории браузера по мере изменения пути.

Я бы хотел увидеть библиотеку, которую можно добавить в наш код, которая автоматически обеспечивает поддержку клавиатуры (которая, возможно, вызывает щелчки). Но я попробовал это сам и уже попал в кроличью нору, потому что структура DOM была специфичной для моего приложения и способ обработки пользовательских кликов был специфическим (усугублялся тем, используете ли вы библиотеку для обработки кликов и т. Д. на). Так что… быстрого решения нет, но все, безусловно, выполнимо.

В заключении

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

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

Изначально опубликовано в B: log Реми Шарпа