3

I am improving accessibility in a web-app.

I would like to cycle through all potentially tabbable / focusable elements

I note that jQuery has its own pseudo-selector, :tabbable but this isn't native.

I've never used jQuery much and I'm in no hurry to start.

I also note that in these two blog posts:

the (similar) solutions look like:

const keyboardfocusableElements = document.querySelectorAll(
  'a[href], button, input, textarea, select, details, [tabindex]'
);

I'm guessing we might be able to add a few more items:

  • audio
  • button
  • canvas
  • details
  • iframe
  • input
  • select
  • summary
  • textarea
  • video
  • [accesskey]
  • [contenteditable]
  • [href]
  • [tabindex]

though perhaps some of these elements only become tabbable / focusable when they include the tabindex attribute...?

Sticking with the list immediately above (for now), leaves us with the following:

const tabbableElements = document.querySelectorAll(
  'audio, button, canvas, details, iframe, input, select, summary, textarea, video, [accesskey], [contenteditable], [href], [tabindex]'
);

which isn't terrible, but you'd be forgiven for thinking there might be something more elegant.

Is there a conciser approach to grabbing all potentially tabbable / focusable elements in a document?

Something like a CSS Level 4 pseudo-selector ? (I've not found anything along those lines...)

Rounin
  • 27,134
  • 9
  • 83
  • 108
  • I don't think there is a more elegant way natively by just using javascript. – kennarddh Nov 06 '22 at 16:42
  • 1
    When not in this list [MDN: ARIA states and properties](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes) why not create your own? `aria-focusable` as a *custom attribute*? Apart from traversing a list in JS, of course. – Rene van der Lende Nov 06 '22 at 16:42
  • That's not a bad suggestion, @RenevanderLende. It's mildly inconvenient to have to manually stick a custom attribute on all elements that require it, but I appreciate that something like `data-allows-tab-focus` would be self-explanatory to anyone looking at the markup subsequently. – Rounin Nov 06 '22 at 18:20
  • 1
    For that matter, if I'm going to add attributes manually, to indicate tabbability... then the attribute might as well be `tabindex`. – Rounin Nov 06 '22 at 18:21
  • 1
    `tabindex` sure does, but that attribute already has designated functionality the browser acts upon and may interfere as such with the functionality you are aiming for. A simple `data/custom = "true/false"` does not. Generally speaking I'd say *custom functionality needs custom attributes* just not to interfere with default behavior. – Rene van der Lende Nov 07 '22 at 01:00
  • I hear what you're saying @RenevanderLende, though, in this case, I'm looking for all elements which exhibit the tab/focus behaviour of elements with the `tabindex` attribute, even when that attribute isn't explicitly present. It appears that one solution is simply to make sure that attribute *is* explicitly present. – Rounin Nov 07 '22 at 01:24
  • If by any chance you want to establish a focus trap, there are other approaches which don’t need to identify all focusable elements. `inert` and `aria-hidden` on a all-surrounding wrapper, for example, or intercepting the `focusout` event to place it where you intend (instead of the user agent). – Andy Nov 08 '22 at 14:42

1 Answers1

1

It occurred to me I should head over to jQuery and see what they were doing for:

jQuery states:

Elements of the following type are focusable if they are not disabled: <input>, <select>, <textarea>, <button>, and <object>. Anchors are focusable if they have an href or tabindex attribute. <area> elements are focusable if they are inside a named map, have an href attribute, and there is a visible image using the map. All other elements are focusable based solely on their tabindex attribute and visibility.

So, the approach I'm going with is to find all the elements which satisfy the conditions above - and any other conditions I may choose to add - and ensure, manually, that each has an explicit tabindex attribute.

Then I can use:

document.querySelectorAll('[tabindex]');

to select all potentially tabbable / focusable elements.

I remain open to other approaches, but, although only semi-automated, this seems the most practical way forward I can come up with for now.

Rounin
  • 27,134
  • 9
  • 83
  • 108
  • Is this for your own app or are you coding some framework? Asking because ANY element can be given a `tabindex` (making it *tabbable*), not only input elements. Just adding insult to injury... (see demo on [MDN: tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex)) – Rene van der Lende Nov 07 '22 at 01:37
  • 1
    Oh, maybe use `:is/:where` in `querySelectorAll`... (neat little puzzle) => `:is(input element list)[tabindex]`. Something like that... Needs to be >= `0` though – Rene van der Lende Nov 07 '22 at 01:43
  • Note that inputs of type hidden are never focusable. You must also take into account that elements under display:none / visibility:hidden aren't focusable at that moment. Note also that adding explicit tabindex=0 on already naturally focusable elements like regular inputs or links may generate strange bugs, so you'd better avoid it. – QuentinC Nov 07 '22 at 05:26
  • Re: _"elements under display:none / visibility:hidden aren't focusable at that moment"_ Thanks, @QuentinC. Right. But I'm only looking for elements which are _potentially_ tabbable / focusable, not ones that are necessarily so at that moment. – Rounin Nov 07 '22 at 11:45
  • @QuentinC - Re: _"Note also that adding explicit tabindex=0 on already naturally focusable elements like regular inputs or links may generate strange bugs, so you'd better avoid it."_ Ohhh. Really? That throws a spanner in the works of the approach immediately above, then. Do you have a Bugzilla or a GitHub reference, please? Many thanks. – Rounin Nov 07 '22 at 11:47
  • Re: _"Is this for your own app"_ - yes, it is. _"ANY element can be given a tabindex (making it tabbable)"_ Sure. But, for example, both ` – Rounin Nov 07 '22 at 11:48
  • @Rounin: Of course I don't have any bug reference in mind, but I think about things like [this question](https://stackoverflow.com/questions/74330753/keep-accessibility-behaviour-with-tabindex-or-similar-when-tabbing-through-e). I don't say that the useless tabindex=0 are causing bugs to that other person, but just in doubt... – QuentinC Nov 07 '22 at 16:02
  • By the way, why do you need it? If you lose track of all focusable elements, maybe you have too many or your UI is too complex? Or if you need to do something on all of them although they are so different in nature (buttons aren't at all like input fields, etc.), maybe there is a simpler way or you are maybe not doing it the right way. Just a thought. – QuentinC Nov 07 '22 at 16:10
  • I'm thinking for example about creating a focus trap for dialog boxes. The simplest solution isn't to put all focusable elements outside to tabindex=-1 and then restore them afterwards, but specially act when pressing tab on the last / shift+tab on the first element of the dialog box instead. It's just an example, I don't know what you are trying to do. – QuentinC Nov 07 '22 at 16:16
  • Re: _"By the way, why do you need it?"_ I'm writing an app which has lightbox consoles which include tabbable elements. If none of the lightboxes are displaying I want to be able to tab through the visible elements on the main screen without _also_ tabbing through the children of several lightboxes. When one of the lightboxes is showing, I want to be able to tab through its children but nothing else. To achieve these outcomes, I'm cycling through all tabbable elements in the document and depending on the class of their container, I am toggling the `tabindex` from `-` to `+` or from `+` to `-`. – Rounin Nov 07 '22 at 18:52
  • Hmm, it quite looks like the modal pattern. Also, if something isn't displayed, it should implicitly not be focusable either and so you shouldn't bother about it. If you hide something potentially focusable from display with something else than display:none / visibility:hidden, I think that you have a more fundamental accessibility problem. – QuentinC Nov 08 '22 at 06:18
  • Re: _"Also, if something isn't displayed, it should implicitly not be focusable either"_ Yes, that's correct, if it has `display: none` applied to it. But if the element can't be seen because it's positioned outside the perimeter of the viewport, then, I suppose, it's still technically displayed, even though it can't be seen. The reason for _not using_ `display: none` is because toggling between `display: none` and `display: block` interferes with _CSS Transitions and Animations_. (It always used to, anyway - maybe it doesn't anymore?) – Rounin Nov 08 '22 at 22:04
  • To summarise, what I have at present is a console sitting below the bottom of the viewport which, when a button is pressed rises up into the visible viewport via a _CSS Transition_. When the console is sitting below the bottom of the viewport, I don't want any of its children to be tabbable and when it rises up into the visible viewport, I want _only_ its children to be tabbable and nothing else, (even though other UI elements are still partially visible behind the console). – Rounin Nov 08 '22 at 22:20