Red*_*ury 9 javascript accessibility
我创建了一个与键盘导航配合良好的复合小部件网格。我遇到的一个问题是,当当前焦点元素所在的网格中的某一行时,焦点将返回到该<body>元素。我希望能够将焦点拯救到最接近的有意义的交互元素(在上面或下面的行中)。
我的问题是:
\n当当前聚焦的元素从 DOM 中删除时,如何将焦点设置到最近的交互元素(仍在 dom 中)?
\n我尝试将焦点/模糊事件与 setTimeout 结合使用来获取正确的信号,但没有得到任何结果。
\n还尝试在当前聚焦的元素上使用 MutationObserver,这有效,但我遇到了问题,因为网格实际上是滚动的,因此由于行被虚拟滚动器回收,当前聚焦的元素可能会从 DOM 中删除,在这种情况下我不想救援焦点(这会导致网格不断向上滚动到新的“救援”焦点,并且您永远无法到达底部)
\nconst 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您必须始终确保焦点永远不会丢失。正如您自己所经历的那样,这是键盘可访问性的黄金法则。
DOM 元素删除的一般规则如下:如果必须从 DOM 中删除当前获得焦点的元素,则必须在删除之前将焦点移至其他位置。
如果您之前没有移动焦点,那么一旦元素被删除,焦点就会移动到任意位置,并且键盘可访问性也会被破坏。有时焦点甚至根本移不到任何地方,没有任何方法可以在没有鼠标的情况下恢复它。
在您的示例中,您应该像这样删除一行:
document.activeElement并向上查看 DOM 层次结构以确定它。如果您无法准确控制删除行的方式和时间,那么您的网格控件设计得很糟糕。你必须修复它。
| 归档时间: |
|
| 查看次数: |
2489 次 |
| 最近记录: |