Доступный аккордеон: как не сфокусироваться на фокусируемом элементе, когда панель скрыта?

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

Я знаю, что на фокусируемые элементы нацелены, поскольку мои панели используют height: 0 вместо display: none. Я использую высоту для переходов.

Единственное решение, которое я могу придумать, - это выбрать все элементы, на которые можно сфокусироваться, на панели и применить к ним tabindex=-1 всякий раз, когда панель скрыта. Это странно или есть лучший способ подойти к этому?

Что-то вроде этого:

focusableElms = panel.querySelectorAll("a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex='0']");

var focusableElm;
for (a = (focusableElms.length - 1); a >= 0; a--) {
  focusableElm = focusableElms[a];
  focusableElm.setAttribute("tabindex", "-1");
}

Доступный Accordion Codepen


person Jake    schedule 27.10.2020    source источник


Ответы (1)


Краткий ответ

Предполагая, что у вас нет положительных tabindexs, тогда вышеизложенное будет работать. Однако, если у вас есть набор tabindex (чего на самом деле не должно быть), то это немного сложнее.

Другое дело, что использование <details> и <summary> сделает ваше приложение более доступным.

Быстро в сторону

Как упоминал @CBroe в комментариях, использование transitionend было бы лучше, чем setTimeout. Я жил в каменном веке, думая, что у него нет хорошей поддержки, но всегда искал не тот товар на caniuse.com.

Длинный ответ

Сначала давайте получим соответствующий HTML, так как он дает нам некоторые мощные функции в современных браузерах.

Детали и резюме

<details> и <summary> автоматически предоставляют множество возможностей. Они автоматически связывают элементы управления (эквивалент aria-controls), это чистая разметка, они автоматически имеют функции открытия и закрытия в большинстве браузеров в качестве запасного варианта на случай сбоя вашего JavaScript и т. д.

Я рассматривал их ранее, поэтому вы можете узнать больше о <details> и <summary> в этом ответе, который я дал.

<details>
       <summary>Item 1</summary>
       <p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Ipsum, reiciendis!</p>
    </details>
    <details>
       <summary>Item 2</summary>
       <p>Lorem ipsum dolor sit amet consectetur, adipisicing elit. Ipsum, reiciendis!</p>
    </details>

Обработка фокуса

Самый простой способ сделать это — использовать JavaScript для изменения свойства отображения после завершения анимации (и отобразить его до ее запуска) с помощью display: none.

Итак, если ваша анимация составляет 1 секунду, вы просто устанавливаете display: block перед добавлением любого класса, который запускает анимацию высоты.

Чтобы закрыть, вы запускаете анимацию высоты (удаляя свой класс) и используете setTimeout в течение 1 секунды, чтобы затем вызвать display: none.

Очевидно, что у этого есть проблемы, так как кто-то может в конечном итоге перейти на панель аккордеона, когда она достигнет высоты 0, а затем, когда вы установите display: none, позиция фокуса страницы будет потеряна.

Альтернативный способ - установить tabindex="-1", как вы предлагаете, поскольку вы можете сделать это, как только закроете аккордеон.

Приведенный ниже пример взят из этого ответа, который я дал при настройке tabindex в анимированном разделе.

Он учитывает больше, чем вам нужно (положительные tabindexs, отключение анимации с помощью prefers-reduced-motion, тот факт, что content-editable можно использовать и т. д.), но должен дать вам необходимую информацию.

Это немного грубо по краям, но должно дать вам хорошее основание для начала.

Я добавил много комментариев в код, поэтому, надеюсь, вы поймете, какие части применимы к вам, и сможете адаптировать его для использования <details> и <summary> для завершения своего решения.

var content = document.getElementById('contentDiv');
var btn = document.getElementById('btn_toggle');
var animationDelay = 2000;

//We should account for people with vestibular motion disorders etc. if they have indicated they prefer reduced motion. We set the animation time to 0 seconds.
var motionQuery = matchMedia('(prefers-reduced-motion)');
function handleReduceMotionChanged() {
  if (motionQuery.matches) {
    animationDelay = 0;
  } else { 
    animationDelay = 2000;
  }
}
motionQuery.addListener(handleReduceMotionChanged);
handleReduceMotionChanged();



//the main function for setting the tabindex to -1 for all children of a parent with given ID (and reversing the process)
function hideOrShowAllInteractiveItems(parentDivID){  
  //a list of selectors for all focusable elements.
  var focusableItems = ['a[href]', 'area[href]', 'input:not([disabled])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', '[tabindex]:not([disabled])', '[contenteditable=true]:not([disabled])'];
  
  //build a query string that targets the parent div ID and all children elements that are in our focusable items list.
  var queryString = "";
  for (i = 0, leni = focusableItems.length; i < leni; i++) {
    queryString += "#" + parentDivID + " " + focusableItems[i] + ", ";
  }
  queryString = queryString.replace(/,\s*$/, "");
      
  var focusableElements = document.querySelectorAll(queryString);      
  for (j = 0, lenj = focusableElements.length; j < lenj; j++) {
            
    var el = focusableElements[j];
    if(!el.hasAttribute('data-modified')){ // we use the 'data-modified' attribute to track all items that we have applied a tabindex to (as we can't use tabindex itself).
            
      // we haven't modified this element so we grab the tabindex if it has one and store it for use later when we want to restore.
      if(el.hasAttribute('tabindex')){
        el.setAttribute('data-oldTabIndex', el.getAttribute('tabindex'));
      }
              
      el.setAttribute('data-modified', true);
      el.setAttribute('tabindex', '-1'); // add `tabindex="-1"` to all items to remove them from the focus order.
              
    }else{
      //we have modified this item so we want to revert it back to the original state it was in.
      el.removeAttribute('tabindex');
      if(el.hasAttribute('data-oldtabindex')){
        el.setAttribute('tabindex', el.getAttribute('data-oldtabindex'));
        el.removeAttribute('data-oldtabindex');
      }
      el.removeAttribute('data-modified');
    }
  }
}



btn.addEventListener('click', function(){
  contentDiv.className = contentDiv.className !== 'show' ? 'show' : 'hide';
  if (contentDiv.className === 'show') {
     content.setAttribute('aria-hidden', false);
    setTimeout(function(){
      contentDiv.style.display = 'block';
      hideOrShowAllInteractiveItems('contentDiv');
    },0); 
  }
  if (contentDiv.className === 'hide') {
      content.setAttribute('aria-hidden', true);
      hideOrShowAllInteractiveItems('contentDiv');
    setTimeout(function(){
      contentDiv.style.display = 'none';
    },animationDelay); //using the animation delay set based on the users preferences.
  }
});
@keyframes in {
  0% { transform: scale(0); opacity: 0; visibility: hidden;  }
  100% { transform: scale(1); opacity: 1; visibility: visible; }
}

@keyframes out {
  0% { transform: scale(1); opacity: 1; visibility: visible; }
  100% { transform: scale(0); opacity: 0; visibility: hidden;  }
}

#contentDiv {
  background: grey;
  color: white;
  padding: 16px;
  margin-bottom: 10px;
}

#contentDiv.show {
  animation: in 2s ease both;
}

#contentDiv.hide {
  animation: out 2s ease both;
}


/*****We should account for people with vestibular motion disorders etc. if they have indicated they prefer reduced motion. ***/
@media (prefers-reduced-motion) {
  #contentDiv.show,
  #contentDiv.hide{
    animation: none;
  }
}
<div id="contentDiv" class="show">
  <p>Some information to be hidden</p>
  <input />
  <button>a button</button>
  <button tabindex="1">a button with a positive tabindex that needs restoring</button>
</div>

<button id="btn_toggle"> Hide Div </button>

person Graham Ritchie    schedule 27.10.2020
comment
Вместо тайм-аута, я думаю, было бы предпочтительнее использовать transitionend (соответственно animationend, для анимации). Тогда вы можете сохранить «конфигурацию» длительности перехода только в CSS, и не нужно изменять код JS как ну, если эта продолжительность когда-либо изменится или должна быть разной в разных местах… - person CBroe; 27.10.2020