1

When inserting new html into a section, I want the first tab press to skip the first few links and focus on a later link. Currently, my solution is to insert an empty <span> with tabindex='-1', and then to call focus() on it. However, I noticed that screen-reading software will jump to this focus(). Is there another way to set the initial tab focus, or to make screen-reading software ignore the focus()? (With NVDA, the div I'm inserting into is a role='status' which should be read whenever it updates, and the focus() grabs NVDA away about 40% of the time, without any seeming pattern.)

A method that failed is tabindex. Both Firefox and Chrome don't respect tabindex for the first tab press for inserted elements. In the code sample below, the second element has the last tabindex, and yet is focused first in Firefox. Behavior in Firefox differs between the first tab press and subsequent tab presses. Chrome ignores the tabindex completely for the first tab focus, so the first tab focus is always the first link.

<!DOCTYPE html>
<html>
<head>
</head>
<body>
 <script>
 function k() {
     document.getElementById("h").innerHTML = "<button tabindex='2' onclick='k()'>first</button><button tabindex='3' onclick='k()'>second</button><button tabindex='1' onclick='k()'>third</button>";
    }
    </script>
<div id="h"><button onclick='k()'>click me</button></div>
</body>
</html>

I also tried marking the empty <span> with aria-hidden='true' but that didn't help.

John
  • 21
  • 4
  • have you tried adding tabindex=-1 and focusing it with focus, a negative tabinex value means that the element should be focusable, but should not be reachable via sequential keyboard navigation. please check these for more details http://stackoverflow.com/questions/4112289/what-is-the-html-tabindex-attribute https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex – azs06 Jan 07 '17 at 09:29
  • Yes, that's the current solution I'm using. I describe it in the first paragraph. – John Jan 07 '17 at 15:20
  • I am trying to understand why you want to prevent a keyboard user from accessing the first few links. A screen reader user, depending on the content/structure of the page, may not be navigating via the Tab key anyway so it is likely less of an issue for them. – aardrian Jan 07 '17 at 21:39
  • @aardrian, I set specific keybinds (q, w, e, r) to the first few links, so tab selection is used to select the remaining non-keybound links for unimpaired, keyboard-only users. This is for a text engine, so the combination of tabs and keybinds lets these users select any link. They will be able to access the first few links by shift-tabbing, but having tab start at a later point makes selection faster. – John Jan 08 '17 at 19:07
  • Do you have a URL? Also, what browser/version with NVDA and what version with NVDA? Do you have this problem with other screen readers? – aardrian Jan 08 '17 at 21:56
  • @aardrian https://johnnydough.github.io/a.html NVDA 2016.4, FF Dev 52. I've only tried NVDA, for one day. – John Jan 09 '17 at 00:53
  • @aardrian since the code in that link is incomprehensible, here is a testcase: https://johnnydough.github.io/test.html – John Jan 09 '17 at 01:20
  • In the testcase, clicking any of the three buttons will either make NVDA read the focus()d element "Apparently...", or make it read from the beginning of the text (which is the desired response), or make it read nothing at all. – John Jan 09 '17 at 01:34
  • 1
    I think some of the issue is that you are destroying DOM elements. Consider putting everything in the DOM and then hiding/showing what you need with CSS. Ideally in a separate container for the two sets of buttons, perhaps with an accessible name so you can drop focus onto that, or just onto the container if you don't use a header. Perhaps a role=group on each or go with a fieldset/legend combo. – aardrian Jan 09 '17 at 01:46
  • @aardrian I see what you mean. If I can avoid destroying the previous DOM elements, then the existing focus will still remain, and then the next focus is no longer an initial focus, so it should respect tabindex="n". And since I don't have to call focus(), screenreaders will work perfectly, solving my problem. The prospect of growing the page to gigabytes from leftover DOM elements worries me, but that sounds like a different question. – John Jan 09 '17 at 02:24
  • 1
    To be fair, if your page grows to gigabytes (let alone megabytes) from retained DOM elements then the focus may be the least of your concerns. Either way, good luck. – aardrian Jan 09 '17 at 03:49
  • @aardrian Thanks for your generous help. I've given up on it now after another few attempts. I started learning javascript 3 days ago and built this, so your expertise has been outstanding. – John Jan 09 '17 at 06:13

1 Answers1

1

Attempt 1: From testing, if the DOM element that owns the existing focus is destroyed, the next object to receive a tab's focus is erratic, and behaves differently on Firefox and Chrome.

On Firefox, the focus is "ghostly shifted" to the first text-order element to appear, and then the first tab will shift forward and backward from this first element. So a tab will reach the element whose tab order is after the first element, and a shift tab will reach the element whose tab order is before the first element.

On Chrome, the first tab's focus will appear on the first text-order element no matter what tabindex is.

That means that tabindex is a nonworking solution if the DOM element is destroyed.

Attempt 2: The alternative is focus(). Inserting a ghost element with tabindex='-1' style='outline:none', and then calling focus() on it, does make tab focus on the element I want it to focus on. However, this has the problem of screwing up screenreaders. Whenever focus() is called, a screenreader (NVDA tested) might do one of three things:

  1. read the new message text like I want it to (20% chance)
  2. read the Window title, then tab title, then the focus()d element, which is horrible (70% chance)
  3. read nothing at all (10% chance)

aria-hidden doesn't help. autofocus does nothing.

Attempt 3: That means focus() is not an acceptable solution. So the only alternative is to not destroy the DOM element. So the DOM element must be moved. However, the moved-element graveyard will fill up if we continually push things there. And we can't delete a graveyard element if it contains our focus element, or else we'll be in the destroyed-focus-element situation. In my situation, interactive fiction will fill this graveyard up to gigabytes in a few hours. Thus, when deleting history log elements, we must check the element and all its children to make sure focus is not contained there, and then dance around it if it is.

Attempt 4: However, it turns out that moving an element kills its focus anyway (at least on Firefox). So you can't just move it to a graveyard, you have to not touch it at all.

Once you have a surviving object that you haven't moved anywhere, you need to set tabindex='-1' on it, or else this graveyard object will mess with your tab order. However, if you do set its tabindex to -1, then (at least on Firefox), tab order now behaves like the destroyed-DOM-object situation once again.

Attempt 5:

  1. don't move that disappearing object anywhere!
  2. instead, take that object, and then hide it.
  3. build your new object around it: the old object must be placed exactly where you want the tab placement to be, in text order. Your new content will be half before it and half after it.
  4. check all of the hidden object's subnodes (recursively) for activeElement to find out where its focus is. For every non-active element that isn't a parent of the node with focus, you must delete it or else the screen reader will complain a lot. For the surviving nodes, make sure they are completely invisible and have no content, but don't use display:none, that will destroy the focus. Set tabindex = -1 on the focused node.
  5. If your focus wasn't in the object you deleted, then find the object that owns the focus and set its tabindex to between the tabindex of the first half and second half.

I have run out of patience, and I am not going to implement this proposed solution, leaving mysteriously half-deleted, half-hidden elements in the middle of new text. After 5 failed attempts, I don't have much hope left, and I don't want to saddle maintainers with pages of in-depth comments, strange unintuitive behavior, and unexpected performance concerns, just for screenreaders. I am going to stick with my old focus() solution.

John
  • 21
  • 4