The ARIA tree spec says that when the tree widget gets keyboard focus, then the first item should get focus (if no element has focus).
I have implemented this behavior below, but when my click handler fires I get a flash of focus on the first element…
How do I prevent this? (this is in the context of svelte if that matters..)
function ariaTree(node) {
const selectable = node.querySelectorAll('.leaf, summary')
function clickHandler(e) {
selectable.forEach(item => item.classList.remove('selected'))
e.target.classList.add('selected')
}
selectable.forEach(item => {
item.addEventListener('click', clickHandler)
})
// https://www.w3.org/WAI/ARIA/apg/patterns/treeview/
// ..when a .. tree receives focus .. if no node is focused ..
// select the first node.
node.addEventListener('focusin', e => {
if (node.querySelectorAll('.selected').length === 0) {
e.preventDefault() // prevent giving focus to the first "button"-like element
selectable[0].classList.add('selected')
}
})
//return {destroy() {...}}
}
ariaTree(document.querySelector('ul[role="tree"]'))
.wrapper {
display: flex;
&>* {flex: 1}
}
ul[role=tree] li {
list-style-type: none
}
.selected {
background-color: green;
color: white;
}
:where(.leaf, summary):hover {
background-color: navy;
color: white;
}
<div class="wrapper">
<p>
Preceeding element to the tree widget...<br>
Press F7 to enable caret browsing (will display a cursor where the current
keyboard focus is), click inside this text, then tab to get to the
tree widget (the first item should get selection/focus).
<br><br>
Refresh the page (so no item in the tree widget has focus), then click on the last
element (world). There is a flash of focus on the first element...
</p>
<ul role="tree" tabindex="0">
<li class="leaf">
hello
</li>
<li class="branch">
<details open>
<summary>beautiful</summary>
<ul>
<li class="leaf">world</li>
</ul>
</details>
</li>
</ul>
</div>
2
Answers
The problem is because
selectable[0].classList.add('selected')
will run immediately, and then theclick
event will fire, which will remove theselected
class fromselectable[0]
.I don’t know if it’s a good way to do it, but delaying:
1 millisecond using setTimeout (so this code runs after the
click
event has fired) I imagine would work. But from an accessible point’s of view, this might be a bad solution (I don’t know much about ARIA).What happens is:
mousedown
event is triggered,focusin
event is triggered, the 1st element is selectedmouseup
andclick
is triggered, and the correct element is highlighted.You need to detect if the
focusin
event was caused by a mouse click on an element, or by something else (e.g. tab key).I think you probably need to add an additional
mousedown
event for that purpose. But note that usually you want things to happen onmouseup
, notmousedown
, so keep theclick
(ormouseup
) event handler as well.(That’s annoying, I think there should be a better solution, but I can’t think of one right now.)