当当前聚焦的元素从 DOM 中移除时,如何挽救焦点?

Red*_*ury 9 javascript accessibility

我创建了一个与键盘导航配合良好的复合小部件网格。我遇到的一个问题是,当当前焦点元素所在的网格中的某一行时,焦点将返回到该<body>元素。我希望能够将焦点拯救到最接近的有意义的交互元素(在上面或下面的行中)。

\n

我的问题是:

\n

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

\n

我尝试将焦点/模糊事件与 setTimeout 结合使用来获取正确的信号,但没有得到任何结果。

\n

还尝试在当前聚焦的元素上使用 MutationObserver,这有效,但我遇到了问题,因为网格实际上是滚动的,因此由于行被虚拟滚动器回收,当前聚焦的元素可能会从 DOM 中删除,在这种情况下我不想救援焦点(这会导致网格不断向上滚动到新的“救援”焦点,并且您永远无法到达底部)

\n

\r\n
\r\n
const grid = document.querySelector(\'.grid\');\n\n// Remove all buttons/links from the natural tab order\ngrid\n  .querySelectorAll(\'a:not([tabindex="0"]), button:not([tabindex="0"])\')\n  .forEach(el => el.setAttribute(\'tabindex\', \'-1\'));\n\ngrid.addEventListener(\'keydown\', (e) => {\n    // Prevent scrolling\n    if (e.key === \'ArrowUp\' || e.key === \'ArrowDown\') {\n      e.preventDefault();\n    }\n    if (e.key === \'ArrowUp\') moveFocus(grid, \'up\');\n    if (e.key === \'ArrowDown\') moveFocus(grid, \'down\');\n    if (e.key === \'ArrowLeft\') moveFocus(grid, \'left\');\n    if (e.key === \'ArrowRight\') moveFocus(grid, \'right\');\n})  \n\n\nfunction moveFocus(grid, direction) {\n  const hasFocusableElement = ensureFocusableElementInGrid(grid)\n  if (!hasFocusableElement) return;\n  if (direction === \'up\') focusUp(grid);\n  if (direction === \'down\') focusDown(grid);\n  if (direction === \'left\') focusLeft(grid);\n  if (direction === \'right\') focusRight(grid);\n}\n\nfunction ensureFocusableElementInGrid(grid) {\n  const firstElem = grid.querySelectorAll(\'a, button\')[0];\n  const currentFocusable = grid.querySelector(\'[tabindex="0"]\') || firstElem;\n\n  // Happens if the grid does not contain any a or button elements.\n  if (!currentFocusable) {\n    return false;\n  }\n  currentFocusable.setAttribute(\'tabindex\', \'0\');\n  return true;\n}\n\nfunction focusDown(grid) {\n  const currentFocus = grid.querySelector(\'[tabindex="0"]\');\n  const nextCell = findNextCell(grid, currentFocus, p => ({\n    row: p.row + 1,\n    col: p.col,\n  }));\n  if (!nextCell) return;\n\n  // Target the first interactive element in the cell below\n  const firstElem = nextCell.querySelectorAll(\'a, button\')[0];\n  transferFocus(currentFocus, firstElem);\n}\n\nfunction focusUp(grid) {\n  const currentFocus = grid.querySelector(\'[tabindex="0"]\');\n  const nextCell = findNextCell(grid, currentFocus, p => ({\n    row: p.row - 1,\n    col: p.col,\n  }));\n  if (!nextCell) return;\n\n  // Target the first interactive element in the cell above\n  const firstElem = nextCell.querySelectorAll(\'a, button\')[0];\n  transferFocus(currentFocus, firstElem);\n}\n\nfunction focusLeft(grid) {\n  const currentFocus = grid.querySelector(\'[tabindex="0"]\');\n  const nextEl = findNextElementInCell(currentFocus, -1);\n\n  if (nextEl) {\n    transferFocus(currentFocus, nextEl);\n    return;\n  }\n\n  const nextCell = findNextCell(grid, currentFocus, p => ({\n    row: p.row,\n    col: p.col - 1,\n  }));\n  if (!nextCell) return;\n\n  // Target the last interactive element in the cell to the left\n  const prevCellElems = nextCell.querySelectorAll(\'a, button\');\n  const lastLink = prevCellElems[prevCellElems.length - 1];\n  transferFocus(currentFocus, lastLink);\n}\n\nfunction focusRight(grid) {\n  const currentFocus = grid.querySelector(\'[tabindex="0"]\');\n\n  // Exit early if next focusable element is found in the cell\n  const nextEl = findNextElementInCell(currentFocus, 1);\n  if (nextEl) {\n    transferFocus(currentFocus, nextEl);\n    return;\n  }\n\n  const nextCell = findNextCell(grid, currentFocus, p => ({\n    row: p.row,\n    col: p.col + 1,\n  }));\n  if (!nextCell) return;\n\n  // Target the first interactive element in the cell to the right\n  const nextCellEl = nextCell.querySelectorAll(\'a, button\');\n  const firstEl = nextCellEl[0];\n  transferFocus(currentFocus, firstEl);\n}\n\n/**\n * Given an interactive element (button or a) this functions figures out it\'s\n * position in the grid based on aria attributes on it\'s parent elements.\n * @param interactiveElement element to find position of\n */\nfunction getGridPosition(interactiveElement) {\n  const row = parseInt(\n    interactiveElement\n      .closest(\'[aria-rowindex]\')\n      .getAttribute(\'aria-rowindex\'),\n    10,\n  );\n  const col = parseInt(\n    interactiveElement\n      .closest(\'[aria-colindex]\')\n      .getAttribute(\'aria-colindex\'),\n    10,\n  );\n  return { row, col };\n}\n\n/**\n * Move focus from oldEl -> newEl\n * @param oldEl element loosing focus\n * @param newEl element gaining focus\n */\nfunction transferFocus(oldEl, newEl) {\n  if (!oldEl || !newEl) return;\n  oldEl.tabIndex = -1;\n  newEl.tabIndex = 0;\n  newEl.focus();\n}\n\n/**\n * Find the next/previous interactive element in the cell of provded element\n * @param element element to start search from\n * @param dir direction to search in, 1 : next, -1 : previous\n */\nfunction findNextElementInCell(element, dir) {\n  const cellElements = Array.from(\n    element\n      .closest(\'[aria-colindex]\')\n      .querySelectorAll(\'a, button\')\n  );\n  const prevIndex = cellElements.findIndex(l => l === element) + dir;\n  return cellElements[prevIndex];\n}\n\n/**\n * Traverse the grid in a direction until a cell with interactive elements is found\n * @param grid the grid element\n * @param element element to start search from.\n *                           It\'s position is calculated and used as a starting point\n * @param updatePos A function to update the position in a certain direction\n */\nfunction findNextCell(grid, element, updatePos) {\n  // recursively visit cells at given position and checks if it has any interactive elements\n  const rec = currPos => {\n    const nextPos = updatePos(currPos);\n    const nextCell = grid.querySelector(\n      `[aria-rowindex="${nextPos.row}"] [aria-colindex="${nextPos.col}"]`,\n    );\n    // No next cell found. Hitting edge of grid\n    if (nextCell === null) return null;\n    // Found next cell containing a or button tags, return it\n    if (nextCell.querySelectorAll(\'a, button\').length) {\n      return nextCell;\n    }\n    // Continue searching. Visit next cell\n    return rec(nextPos);\n  };\n  const position = getGridPosition(element);\n  return rec(position);\n}
Run Code Online (Sandbox Code Playgroud)\r\n
.arrow-keys-indicator {\n  bottom: 10px;\n  right: 0;\n  position: fixed;\n  height: 65px;\n  width: 85px;\n  display: none;\n}\n\n.grid {\n  display: grid;\n  grid-gap: 16px;\n}\n.grid:focus-within ~ .arrow-keys-indicator {\n  display: block;\n}\n\n\n.grid__header-row,\n.grid__row {\n  display: grid;\n  grid-template-columns: 1fr 1fr 1fr;\n}\n\n.heart {\n  /* screen reader only */\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border: 0;\n}\n.grid__row:focus-within .heart,\n.grid__row:hover .heart {\n  /* undo screen reader only */\n  position: static;\n  width: auto;\n  height: auto;\n  padding: 0;\n  margin: 0;\n  overflow: visible;\n  clip: auto;\n  white-space: normal;\n}\n\n.sr-only {\n    /* screen reader only */\n  position: absolute;\n  width: 1px;\n  height: 1px;\n  padding: 0;\n  margin: -1px;\n  overflow: hidden;\n  clip: rect(0, 0, 0, 0);\n  white-space: nowrap;\n  border: 0;\n}
Run Code Online (Sandbox Code Playgroud)\r\n
<h1>Accessible Grid</h1>\n<p>Start <a href="#">pressing</a> the Tab key until you <a href="#">reach</a> the grid</p>\n\n<div class="grid" role="grid" tabindex="0">\n  <div class="grid__header-row" role="row" aria-rowindex="1">\n    <div role="columnheader" aria-colindex="1">\n      <button>TITLE</button>\n    </div>\n    <div role="columnheader" aria-colindex="2">\n      <button>ALBUM</button>\n    </div>\n    <div role="columnheader" aria-colindex="3">DURATION</div>\n  </div>\n  <div class="grid__row" role="row" aria-rowindex="2">\n    <div role="gridcell" aria-colindex="1">\n      <div>Black Parade</div>\n      <a href="#">Beyonc\xc3\xa9</a>\n    </div>\n    <div role="gridcell" aria-colindex="2"></div>\n    <div role="gridcell" aria-colindex="3">\n      4:41\n      <button class="heart">\n        <span class="sr-only">Add to your liked songs</span>\n        \xe2\x99\xa1\n      </button>\n    </div>\n  </div>\n  <div class="grid__row" role="row" aria-rowindex="3">\n    <div role="gridcell" aria-colindex="1">\n      <div>Texas Sun</div>\n      <a href="#">Kruangbin</a>,\n      <a href="#">Leon Bridges</a>\n    </div>\n    <div role="gridcell" aria-colindex="2">\n      <a href="#">Texas Sun</a>\n    </div>\n    <div role="gridcell" aria-colindex="3">\n      4:12\n      <button class="heart">\n        <span class="sr-only">Add to your liked songs</span>\n        \xe2\x99\xa1\n      </button>\n      </div>\n  </div>\n  <div class="grid__row" role="row" aria-rowindex="4">\n    <div role="gridcell" aria-colindex="1">\n      <div>Disconnect</div>\n      <a href="#">Basement</a>\n    </div>\n    <div role="gridcell" aria-colindex="2">\n      <a href="#">Beside Myself</a>\n    </div>\n    <div role="gridcell" aria-colindex="3">\n      3:29\n      <button class="heart">\n        <span class="sr-only">Add to your liked songs</span>\n        \xe2\x99\xa1\n      </button>\n      </div>\n  </div>\n</div>\n\n<img class="arrow-keys-indicator" src="https://www.w3.org/TR/wai-aria-practices/examples/grid/imgs/black_keys.png" alt=""/>\n\n</br>\n\n<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>
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n

Que*_*inC 6

您必须始终确保焦点永远不会丢失。正如您自己所经历的那样,这是键盘可访问性的黄金法则。

DOM 元素删除的一般规则如下:如果必须从 DOM 中删除当前获得焦点的元素,则必须在删除之前将焦点移至其他位置。

如果您之前没有移动焦点,那么一旦元素被删除,焦点就会移动到任意位置,并且键盘可访问性也会被破坏。有时焦点甚至根本移不到任何地方,没有任何方法可以在没有鼠标的情况下恢复它。

在您的示例中,您应该像这样删除一行:

  1. 检查焦点是否位于您要删除的行内。如果需要,请使用document.activeElement并向上查看 DOM 层次结构以确定它。
  2. 如果是,则调用 focusDown 将焦点置于下一行(如果位于最后一行,则调用 focusUp),就像用户按下向上/向下箭头一样。您可能不需要其他特殊的逻辑来找出要关注的最佳元素。
  3. 有效地从 DOM 中删除行。现在焦点不再存在,你就不再有问题了。

如果您无法准确控制删除行的方式和时间,那么您的网格控件设计得很糟糕。你必须修复它。


Tam*_*sal -3

我需要说实话,我没有正确阅读,但你觉得怎么样:

if (document.hasFocus()) document.activeElement.blur()