(toc, onclick)
| 57 | |
| 58 | // https://www.w3.org/TR/wai-aria-practices-1.2/examples/treeview/treeview-navigation.html |
| 59 | export const createTOCView = (toc, onclick) => { |
| 60 | const $toc = document.createElement("ol"); |
| 61 | $toc.setAttribute("role", "tree"); |
| 62 | const list = []; |
| 63 | const map = new Map(); |
| 64 | const createItem = createTOCItemElement(list, map, onclick); |
| 65 | $toc.replaceChildren(...toc.map((item) => createItem(item))); |
| 66 | |
| 67 | const isTreeItem = (item) => item?.getAttribute("role") === "treeitem"; |
| 68 | const getParents = function* (el) { |
| 69 | for (let parent = el.parentNode; parent !== $toc; parent = parent.parentNode) { |
| 70 | const item = parent.previousElementSibling; |
| 71 | if (isTreeItem(item)) yield item; |
| 72 | } |
| 73 | }; |
| 74 | |
| 75 | let currentItem, currentVisibleParent; |
| 76 | $toc.addEventListener("focusout", () => { |
| 77 | if (!currentItem) return; |
| 78 | // reset parent focus from last time |
| 79 | if (currentVisibleParent) currentVisibleParent.tabIndex = -1; |
| 80 | // if current item is visible, let it have the focus |
| 81 | if (currentItem.offsetParent) { |
| 82 | currentItem.tabIndex = 0; |
| 83 | return; |
| 84 | } |
| 85 | // current item is hidden; give focus to the nearest visible parent |
| 86 | for (const item of getParents(currentItem)) { |
| 87 | if (item.offsetParent) { |
| 88 | item.tabIndex = 0; |
| 89 | currentVisibleParent = item; |
| 90 | break; |
| 91 | } |
| 92 | } |
| 93 | }); |
| 94 | |
| 95 | const setCurrentHref = (href) => { |
| 96 | if (currentItem) { |
| 97 | currentItem.removeAttribute("aria-current"); |
| 98 | currentItem.tabIndex = -1; |
| 99 | } |
| 100 | const el = map.get(href); |
| 101 | if (!el) { |
| 102 | currentItem = list[0]; |
| 103 | currentItem.tabIndex = 0; |
| 104 | return; |
| 105 | } |
| 106 | for (const item of getParents(el)) item.setAttribute("aria-expanded", "true"); |
| 107 | el.setAttribute("aria-current", "page"); |
| 108 | el.tabIndex = 0; |
| 109 | el.scrollIntoView({ behavior: "smooth", block: "center" }); |
| 110 | currentItem = el; |
| 111 | }; |
| 112 | |
| 113 | const acceptNode = (node) => |
| 114 | isTreeItem(node) && node.offsetParent ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; |
| 115 | const iter = document.createTreeWalker($toc, 1, { acceptNode }); |
| 116 | const getIter = (current) => ((iter.currentNode = current), iter); |
no test coverage detected