从DOM中删除当前关注的元素时,如何恢复关注?

问题描述

我创建了一个composite widget grid,可以很好地与键盘导航一起使用。我遇到的一个问题是,当网格中当前焦点所在的行所在的行时,焦点返回到<body>元素。我希望能够将焦点集中在最有意义的交互式元素上(在上方或下方)。

我的问题是:

从DOM中删除当前关注的元素时,如何将焦点设置为最接近的交互式元素(仍在dom中)?

我尝试将focus / blur事件与setTimeout结合使用以获取正确的信号,但没有得到任何信号。

也尝试在当前关注的元素上使用MutationObserver,该方法可以工作,但是我遇到了问题,因为网格实际上是滚动的,因此由于虚拟滚动器回收了该行,因此当前关注的元素可以从DOM中删除。在这种情况下,我不想挽救焦点(这将导致网格不断向后滚动到新的“已救援”焦点,而您永远无法到达底部)

const grid = document.querySelector('.grid');

// Remove all buttons/links from the natural tab order
grid
  .querySelectorAll('a:not([tabindex="0"]),button:not([tabindex="0"])')
  .forEach(el => el.setAttribute('tabindex','-1'));

grid.addEventListener('keydown',(e) => {
    // Prevent scrolling
    if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
      e.preventDefault();
    }
    if (e.key === 'ArrowUp') moveFocus(grid,'up');
    if (e.key === 'ArrowDown') moveFocus(grid,'down');
    if (e.key === 'ArrowLeft') moveFocus(grid,'left');
    if (e.key === 'ArrowRight') moveFocus(grid,'right');
})  


function moveFocus(grid,direction) {
  const hasFocusableElement = ensureFocusableElementInGrid(grid)
  if (!hasFocusableElement) return;
  if (direction === 'up') focusUp(grid);
  if (direction === 'down') focusDown(grid);
  if (direction === 'left') focusLeft(grid);
  if (direction === 'right') focusRight(grid);
}

function ensureFocusableElementInGrid(grid) {
  const firstElem = grid.querySelectorAll('a,button')[0];
  const currentFocusable = grid.querySelector('[tabindex="0"]') || firstElem;

  // Happens if the grid does not contain any a or button elements.
  if (!currentFocusable) {
    return false;
  }
  currentFocusable.setAttribute('tabindex','0');
  return true;
}

function focusDown(grid) {
  const currentFocus = grid.querySelector('[tabindex="0"]');
  const nextCell = findNextCell(grid,currentFocus,p => ({
    row: p.row + 1,col: p.col,}));
  if (!nextCell) return;

  // Target the first interactive element in the cell below
  const firstElem = nextCell.querySelectorAll('a,button')[0];
  transferFocus(currentFocus,firstElem);
}

function focusUp(grid) {
  const currentFocus = grid.querySelector('[tabindex="0"]');
  const nextCell = findNextCell(grid,p => ({
    row: p.row - 1,}));
  if (!nextCell) return;

  // Target the first interactive element in the cell above
  const firstElem = nextCell.querySelectorAll('a,firstElem);
}

function focusLeft(grid) {
  const currentFocus = grid.querySelector('[tabindex="0"]');
  const nextEl = findNextElementInCell(currentFocus,-1);

  if (nextEl) {
    transferFocus(currentFocus,nextEl);
    return;
  }

  const nextCell = findNextCell(grid,p => ({
    row: p.row,col: p.col - 1,}));
  if (!nextCell) return;

  // Target the last interactive element in the cell to the left
  const prevCellElems = nextCell.querySelectorAll('a,button');
  const lastLink = prevCellElems[prevCellElems.length - 1];
  transferFocus(currentFocus,lastLink);
}

function focusRight(grid) {
  const currentFocus = grid.querySelector('[tabindex="0"]');

  // Exit early if next focusable element is found in the cell
  const nextEl = findNextElementInCell(currentFocus,1);
  if (nextEl) {
    transferFocus(currentFocus,col: p.col + 1,}));
  if (!nextCell) return;

  // Target the first interactive element in the cell to the right
  const nextCellEl = nextCell.querySelectorAll('a,button');
  const firstEl = nextCellEl[0];
  transferFocus(currentFocus,firstEl);
}

/**
 * Given an interactive element (button or a) this functions figures out it's
 * position in the grid based on aria attributes on it's parent elements.
 * @param interactiveElement element to find position of
 */
function getGridPosition(interactiveElement) {
  const row = parseInt(
    interactiveElement
      .closest('[aria-rowindex]')
      .getAttribute('aria-rowindex'),10,);
  const col = parseInt(
    interactiveElement
      .closest('[aria-colindex]')
      .getAttribute('aria-colindex'),);
  return { row,col };
}

/**
 * Move focus from oldEl -> newEl
 * @param oldEl element loosing focus
 * @param newEl element gaining focus
 */
function transferFocus(oldEl,newEl) {
  if (!oldEl || !newEl) return;
  oldEl.tabIndex = -1;
  newEl.tabIndex = 0;
  newEl.focus();
}

/**
 * Find the next/previous interactive element in the cell of provded element
 * @param element element to start search from
 * @param dir direction to search in,1 : next,-1 : previous
 */
function findNextElementInCell(element,dir) {
  const cellElements = Array.from(
    element
      .closest('[aria-colindex]')
      .querySelectorAll('a,button')
  );
  const prevIndex = cellElements.findIndex(l => l === element) + dir;
  return cellElements[prevIndex];
}

/**
 * Traverse the grid in a direction until a cell with interactive elements is found
 * @param grid the grid element
 * @param element element to start search from.
 *                           It's position is calculated and used as a starting point
 * @param updatePos A function to update the position in a certain direction
 */
function findNextCell(grid,element,updatePos) {
  // recursively visit cells at given position and checks if it has any interactive elements
  const rec = currPos => {
    const nextPos = updatePos(currPos);
    const nextCell = grid.querySelector(
      `[aria-rowindex="${nextPos.row}"] [aria-colindex="${nextPos.col}"]`,);
    // No next cell found. Hitting edge of grid
    if (nextCell === null) return null;
    // Found next cell containing a or button tags,return it
    if (nextCell.querySelectorAll('a,button').length) {
      return nextCell;
    }
    // Continue searching. Visit next cell
    return rec(nextPos);
  };
  const position = getGridPosition(element);
  return rec(position);
}
.arrow-keys-indicator {
  bottom: 10px;
  right: 0;
  position: fixed;
  height: 65px;
  width: 85px;
  display: none;
}

.grid {
  display: grid;
  grid-gap: 16px;
}
.grid:focus-within ~ .arrow-keys-indicator {
  display: block;
}


.grid__header-row,.grid__row {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
}

.heart {
  /* screen reader only */
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0,0);
  white-space: nowrap;
  border: 0;
}
.grid__row:focus-within .heart,.grid__row:hover .heart {
  /* undo screen reader only */
  position: static;
  width: auto;
  height: auto;
  padding: 0;
  margin: 0;
  overflow: visible;
  clip: auto;
  white-space: normal;
}

.sr-only {
    /* screen reader only */
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0,0);
  white-space: nowrap;
  border: 0;
}
<h1>Accessible Grid</h1>
<p>Start <a href="#">pressing</a> the Tab key until you <a href="#">reach</a> the grid</p>

<div class="grid" role="grid" tabindex="0">
  <div class="grid__header-row" role="row" aria-rowindex="1">
    <div role="columnheader" aria-colindex="1">
      <button>TITLE</button>
    </div>
    <div role="columnheader" aria-colindex="2">
      <button>ALBUM</button>
    </div>
    <div role="columnheader" aria-colindex="3">DURATION</div>
  </div>
  <div class="grid__row" role="row" aria-rowindex="2">
    <div role="gridcell" aria-colindex="1">
      <div>Black Parade</div>
      <a href="#">Beyoncé</a>
    </div>
    <div role="gridcell" aria-colindex="2"></div>
    <div role="gridcell" aria-colindex="3">
      4:41
      <button class="heart">
        <span class="sr-only">Add to your liked songs</span>
        ♡
      </button>
    </div>
  </div>
  <div class="grid__row" role="row" aria-rowindex="3">
    <div role="gridcell" aria-colindex="1">
      <div>Texas Sun</div>
      <a href="#">Kruangbin</a>,<a href="#">Leon Bridges</a>
    </div>
    <div role="gridcell" aria-colindex="2">
      <a href="#">Texas Sun</a>
    </div>
    <div role="gridcell" aria-colindex="3">
      4:12
      <button class="heart">
        <span class="sr-only">Add to your liked songs</span>
        ♡
      </button>
      </div>
  </div>
  <div class="grid__row" role="row" aria-rowindex="4">
    <div role="gridcell" aria-colindex="1">
      <div>Disconnect</div>
      <a href="#">Basement</a>
    </div>
    <div role="gridcell" aria-colindex="2">
      <a href="#">Beside Myself</a>
    </div>
    <div role="gridcell" aria-colindex="3">
      3:29
      <button class="heart">
        <span class="sr-only">Add to your liked songs</span>
        ♡
      </button>
      </div>
  </div>
</div>

<img class="arrow-keys-indicator" src="https://www.w3.org/TR/wai-aria-practices/examples/grid/imgs/black_keys.png" alt=""/>

</br>

<p>The <a href="#">links</a> in this section should be <a href="#">reachable</a> with a single Tab key press if the grid is in focus.</p>

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)