skip to Main Content

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


  1. The problem is because selectable[0].classList.add('selected') will run immediately, and then the click event will fire, which will remove the selected class from selectable[0].

    I don’t know if it’s a good way to do it, but delaying:

                if (node.querySelectorAll('.selected').length === 0) {
                    e.preventDefault() // prevent giving focus to the first "button"-like element
                    selectable[0].classList.add('selected')
                }
    

    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).

    Login or Signup to reply.
  2. What happens is:

    • the user presses the mouse button, the mousedown event is triggered,
    • the focusin event is triggered, the 1st element is selected
    • the user might wait for an arbitrary time before releasing the mouse button
    • the user releases the mouse button, mouseup and click 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 on mouseup, not mousedown, so keep the click (or mouseup) event handler as well.

    (That’s annoying, I think there should be a better solution, but I can’t think of one right now.)

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search