[focusgroup] Default behaviour for key conflict elements
The focusgroup explainer interactive content inside focusgroups section describes that some elements like <input type="number"> or <input type="number"> already have actions assigned to arrow keys that conflict with the focusgroup navigation. There is a list of potential solutions to this issue in the explainer.
Fortunately, there are several solutions to this problem:
- Remove tabindex=-1 from the “Increase/Decrease Font” buttons.
- Move the “Increase/Decrease Font” buttons before the combobox. (Refer to “Avoid including controls whose operation requires the pair of arrow keys used for toolbar navigation” in the Toolbar control pattern.) Additionally, opt-out the control from focusgroup participation so that arrow keys skip it. Alternatively, turn off the focusgroup’s memory so that focus isn’t automatically returned to the combobox.
- Use script to intercept focusgroup-related keydown events on the and move focus manually. Also consider limiting the focusgroup to one axis and reserving the other axis for operating the
<input>.
I'd like to propose another approach that would provide a default behaviour that can be enhanced by authors.
Turning off roving behaviour when a key conflict element gets focused
It's best explained with a demo: https://gfellerph.github.io/focusgroup-polyfill/. The first example "regular focusgroup" in this experimental implementation has an input field amongst other focusable elements inside the focusgroup. The memory functionality is on. However, when the input field is focused by arrow key navigation, the memory functionality on all other focusgroup candidates is turned off. Tab or shift tab leaves the input field and focuses the next focusable element in either direction. If this is a focusgroup candidate and not a key conflict element, memory behaviour is restored. Tabbing again will leave the focusgroup. The key conflicting <input> can never be the memory element. To change the behaviour, the following options are (still) available:
- Skipping over the input when navigating with arrow keys can be achieved by setting
<input focusgroup="none"> - Disabling the arrow keys on input fields and other elements that already use arrow keys for other actions is left to the author
- Disabling memory behaviour enables all tab stops
This approach would provide a default way of handling this edge case when none of the above solutions are implemented.
I think this is a very reasonable direction, what do you think of this further simplification?
With:
A "key conflict element" is a focusgroup item that already have actions assigned to arrow keys that conflict with focusgroup navigation.
I am wondering if there would need to be any behavioral difference to focusgroup=none once the focus is within that "key conflict element", if they should be treated the same, the behavior could be:
If focus is inside of a "key conflict element", treat this element to be not part of the focusgroup (
focusgroup=none) for the purposes of sequential focus navigation.
This way there is more language and behavioral consistency across approaches, what do you think? Any edge cases I'm not thinking about?
The only difference between our two explanations is that in my simplification, <input> is still allowed to be the memory element, though, I think this would give a more consistent experience, e.g.:
<div focusgroup="toolbar">
<button>Bold</button>
<button>Italic</button>
<button>Save</button>
<label for="search">Search:</label><input id="search" type="text"/>
</div>
<button>Rotate</button>
In this example, the author may want the ability for the input to retain memory as normal, and this behavior seems the most intuitive to me, or at least a reasonable default. (even though they might be better served by moving the input outside of the focusgroup)
If focus is inside of a "key conflict element", treat this element to be not part of the focusgroup (focusgroup=none) for the purposes of sequential focus navigation.
I think that's correct and can be left as is. There are differences in behaviour however if focus is not inside the key conflict element, but I think that's already stated in the scoped explainer.
The only difference between our two explanations is that in my simplification, is still allowed to be the memory element, though, I think this would give a more consistent experience, e.g.:
I think here could be an edge case at least in terms of how I'm expecting a memory element to behave. Generally I'm expecting the memory element to be the only tab stop of the focusgroup and when I'm tabbing through the page, the memory element would be the only element in the focusgroup to get focused while I'm tabbing by. I might be wrong about this, but to my understanding a key conflict element can't possibly be the memory element in every case, because if it would be, there might be no possibility to reach the other focusgroup candidates that are being skipped over in the tab order. At least if key conflict elements are allowed to keep their initial key bindings within focusgroups (and I would think it would be an accessibility issue if the focusgroup would override key bindings).
Taking your example, I'll try to explain what I mean:
<div focusgroup="toolbar">
<button>Bold</button>
<button>Italic</button>
<button>Save</button>
<label for="search">Search:</label><input id="search" type="text"/>
</div>
<button>Rotate</button>
- Tab to the focusgroup, "Bold" gets focus
- Press right arrow key 3 times, search input gets focus and is now the memory element
- Press tab, "Rotate" button gets focus
- Press shift+tab, search input gets focus again because it's the memory element of the focusgroup
- Press left arrow, focus remains in the input field because it's a default key binding of the input
- Press shift+tab again, [what happens now?]
If step 6 focuses the "Save" button, the input field does not behave like other memory elements would, because it does not allow to skip other focusgroup candidates in tab sequence order.
If step 6 moves focus out of the focusgroup to some element above, how would other focusgroup candidates be able to receive focus again?
If step 5 would already focus the "Save" button, the default input keybindings would be overwritten by the focusgroup behaviour.
The issue becomes more apparent when the input field is not the first or last candidate. Then a tab or shift+tab press should always go to the next focusgroup candidate, otherwise they would become unreachable by keyboard navigation. The only case I could imagine where a key conflict element with focus would behave as a regular memory element would be if the focus is moved by the cursor (or other means) outside of the focusgroup.
You're right, stating that key conflict elements can never be the memory element would be wrong since in your example it can behave like one - at least in one direction. On the other hand, I think stating that key conflict elements can be the memory element could be confusing as well because in practice they might not behave as expected in every case.
- Press left arrow, focus remains in the input field because it's a default key binding of the input
- Press shift+tab again, [what happens now?]
My thinking: Since focus is within a "key-conflict element", we will now treat this element to be not a part of the focusgroup for the purposes of sequential navigation, including the fact that now that this <input> element can't be the memory element for the focusgroup, as we are temporarily treating it as focusgroup=none.
Here is another example that articulates the difference:
<div focusgroup="toolbar">
<button tabindex='-1'>Bold</button>
<button tabindex='0'>Italic</button>
<label for="search">Search:</label><input tabindex='-1' id="search" type="text"/>
<button tabindex='-1'>Save</button>
</div>
<button>Rotate</button>
- User clicks or otherwise moves focus to the "Save" button.
- User hits left once and enters search input field and hits enter; script sends them to some element down the page past the Rotate button.
- User moves focus to the Rotate button (either through shift+tab or clicking it)
- User hits shift+tab. [what happens now?]
Options:
- The "Seach" input field is focused, since that is where they left off.
- The "Italic" button is focused, since memory was disabled, and this was marked as
tabindex="0" - The "Save" button is focused, since memory was disabled (but not cleared) when focus moved to the "Search" input.
I think option 1 is the most intuitive, as this is where the user last was, if they were on any of the other focusable elements in the same situation, they would expect to be brought back the same way due to memory.
As an aside:
I am almost wondering if these "key conflict elements" should always be considered focusgroup="none", I haven't thought of or seen an example where the experience is improved by having them participate in a focusgroup, and the asymmetrical behavior of entering and leaving doesn't seem intuitive. I am not ready to be too restrictive here as there could be use-cases I haven't thought of yet, but I am starting to lean in that direction.
That's a valid use case for the input field to remain the memory element, I'd expect focus to return to the search field as well (even though I don't like it because it makes writing the polyfill harder 😉).
As an aside: I am almost wondering if these "key conflict elements" should always be considered focusgroup="none", I haven't thought of or seen an example where the experience is improved by having them participate in a focusgroup, and the asymmetrical behavior of entering and leaving doesn't seem intuitive. I am not ready to be too restrictive here as there could be use-cases I haven't thought of yet, but I am starting to lean in that direction.
I'm not sure if skipping over key conflict elements is preferable to asymmetrical entering/leaving behaviour. Skipping an element could feel like an error, leaving users confused and wondering how to reach that search input field.
<!-- The example from above without tabindices -->
<div focusgroup="toolbar">
<button>Bold</button>
<button>Italic</button>
<label for="search">Search:</label><input id="search" type="text"/>
<button>Save</button>
</div>
<button>Rotate</button>
Approach 1: Key conflict elements are treated as focusgroup="none"
Tab order:
- Bold button
- Search Input
- Rotate button
User might wonder: How to access the "Save" button? Shift + Tab (2x) then Right arrow (2x)
Arrow key order:
- Bold button
- Italic button
- Save button
How to access the search field? Shift + Tab.
Approach 2: Key conflict elements are treated as candidates
Tab order:
- Bold button
- Rotate button
How to access the "Save" button? Shift + Tab then Right arrow (3x)
Arrow key order:
- Bold button
- Italic button
- Search input
How to exit search input? Tab would focus "Save" button.
If every key conflict element is treated as focusgroup="none", there would be no possibility to get the other behaviour whereas with the approach 2, authors could decide which one they prefer. It would be interesting to compare both approaches in a user testing.
Apologies if this is mentioned already but one possible is that it's a normal member so you can arrow key into the conflict (e.g. input) but you then effectively disable the focusgroup such that tab moves to the next focusable element even one within the focusgroup. Which then re-enables the arrow key behaviour?
Alternatively you could make it so a search input takes the sideways arrow keys but then you can use up and down to deal with it. This doesn't work for stuff like native radio inputs (unless they're "fixed") or a textarea.
I think the answer to this might be depend on the pattern, it should also be based on what users of these patterns actually want to happen in these cases (obviously we should heavily discourage this behaviour).
Apologies if this is mentioned already but one possible is that it's a normal member so you can arrow key into the conflict (e.g. input) but you then effectively disable the focusgroup such that tab moves to the next focusable element even one within the focusgroup. Which then re-enables the arrow key behaviour?
@lukewarlow, this basically aligns with my initial proposal, but it might be wise to include just the previous and next focusgroup siblings in the tab order after hitting a key conflict element, not the whole focusgroup.
I think I have come around to this idea:
When focus is within a "key conflict element" the neighbor immediately following and immediately preceding the "key conflict element" are now considered in normal sequential navigation with tab/shift+tab. Memory does not apply as we are already inside the focusgroup, not re-entering it.
<div focusgroup="toolbar">
<button>Bold</button>
<button>Italic</button>
<label for="search">Search:</label><input id="search" type="text"/>
<button focusgroup="none">Tab to get here</button>
<button>Save</button>
</div>
<button>Rotate</button>
<div focusgroup="toolbar">
<button>Share</button>
<button>Duplicate</button>
<label for="find">Find:</label><input id="find" type="text"/>
<button>Close</button>
</div>
From "Search", a tab will focus the button "Tab to get here", and a shift tab will focus "Italic" This example highlights that while the "Italic" and "Save" buttons are both neighbors of "Seach" tab ordering still applies. From "Find", a tab will move focus to "Close", and shift-tab will move focus to "Duplicate"
I'll draft an explainer PR to include this behavior, and link that here once finished, but in the meantime, please feel free to continue to comment here if you see flaws in this approach.
@gfellerph I'd love you to take a look at the new PR I put up to update the explainer around this topic, let me know what you think! https://github.com/openui/open-ui/pull/1289