FocusTraversalAPI icon indicating copy to clipboard operation
FocusTraversalAPI copied to clipboard

node.nextActiveElement & node.previousActiveElement

Open ddamato opened this issue 4 years ago • 2 comments

I saw your talk last night at JSConf about this issue and I resonate with your pain. I've also had questions about how to get focus information and was up for most of the night thinking about it and your proposal. I appreciate all the leg work you've done to expose this issue.

I'd like you to consider changing a viewpoint for a moment. As someone well versed in custom elements, think about how that API is structured. It's very low-level and able to be built on top of for more robust and custom solutions. I propose that we only ask browser vendors to expose two properties for which I believe everything else you are asking for can be built from: node.nextActiveElement and node.previousActiveElement.

node.nextActiveElement is akin to node.nextElementSibling; it is a reference to the next element that would receive focus if the user was to tab to it from the current node. This is basically what the .next(el) method is asking for in your spec. The node in this case can also hold context. Meaning if the node represented a <dialog-box/>, we could trap the focus within the node. If you simply called dialogBox.nextActiveElement it would point to a reference to the next element to receive focus in the <dialog-box/>. In the case of custom elements, when we want to continue outside of the custom element to the next focusable element, it's just a call to document.nextActiveElement.

One of the reasons why I gravitate toward this approach is that the elements of the DOM may change over the lifecycle. What was once queried in the list of focusable elements at page load might not be the same as when you need to know the next element. React is a good example, where entire DOM trees may (dis)appear. Similar to node.nextElementSibling, it'll know which element that is next when referencing the current element in the current tree structure.

As for extending to an API that could be made outside of the browser vendors (as a package for example like your polyfill):

  • .hasFocusable() - node === document.activeElement
  • .isFocusable() - node.focus() && node === document.activeElement (you could return focus back to the previous node if you'd like by storing it before this check).
  • .next() - node.nextActiveElement
  • .previous() - node.previousActiveElement
  • .forward() - node.nextActiveElement.focus()
  • .backward() - node.previousActiveElement.focus()
  • .first() - On page load, document.activeElement and store it.
  • .last() - One page load, document.activeElement.previousActiveElement and store it.

I'm not sure what you'd use .isFocusable() for as a check prior to moving focus. I'd rather just attempt to focus the element that needs focus and then check if it was focused. Then I can move on accordingly if the focus was or wasn't achieved.

The .first() and .last() references might change over the lifecycle (as explained above) but I honestly can't find a use case for when you'd need the first or last focusable element on the page. The primary use case is exactly what you've described. Once the user has completed a task, you want to move them to the next task. You can dictate what the starting point is using tabindex="1" and then the last element should be the .previousActiveElement node at tabindex="1".

Managing a history should be trivial, you could just push and pop element references in an array as you listen to 'focusin' events on the document by reading from the event object provided in the callback.

document.addEventListener('focusin', (ev) => focusHistory.push(ev.target));

Naming the methods I'm certainly open to ideas for. I strayed from using the word "focus" because your API was using it heavily and I wanted to separate the ideas from each other in this explainer. I'd be fine with calling these references something more descriptive. I believe nextFocusableElement will work but nextElementFocus probably wont (nextElementFocus.focus() just sounds odd).

What do you think?

ddamato avatar Sep 12 '19 15:09 ddamato

@ddamato Thank you for extensive comment, and sorry its been so long before I respond...

While I do understand what you are suggesting, I do not think it is any better of an approach than what I have proposed. I feel like the developer ease of use of either approach is about the same. Likewise, both approaches account for the DOM changing out from under you as you can see if you look at the polyfill. Your approach, I assume would handle it by computing the values of nextActiveElement/previousActiveElement as getters. It is worth noting that one consideration is performing this calculation could be long on extensive DOM structures and may require a promise which would make the getter approach less viable. I'm still thinking about that one...

Also, I think at one point you imply that nextActiveElement could be set, which I don't think would fly very well with the specification people at all. I like the idea you are getting at there, but I don't think we could sell it.

I do believe it is an absolute requirement of the proposal that the browsers expose the isFocusable() behavior as it has applications beyond just traversal. I think the CSS people are also proposing something similar.

All that said, thank you for your comments, and please, feel free to continue to try and change my mind.

arei avatar Oct 29 '19 21:10 arei

I don't believe the calculation on which DOM element is next to be focused would be expensive, the browser is already doing that work as soon as the person hits Tab on the keyboard.

Also not recommending that nextActiveElement could be set. Sorry, if I was ambiguous in the explanation. I believe it should just be a getter, just like nextElementSibling.

I'm curious, how do you expect isFocusable() to be used? What examples are you interested in?

The reason for my recommendation above is to simply provide a low-level API with one (maybe two) feature requests that can be extended to a more manageable API once exposed. Similar to the way people find it to be a chore to work with the Custom Elements API so frameworks have been built on top of them to make it easier to work with. If we ask only for nextActiveElement (and previous), I can expect delivery to be easier than attempting to ask for more features that could be created from that one feature.

ddamato avatar Oct 30 '19 17:10 ddamato