Closed travisleithead closed 7 months ago
@travisleithead If I recall (and this convo was awhile ago), I think the issue here was that not every instance where someone may want to use focusgroup
should automatically return someone to the previously focused instance. If someone wanted focusgroup behavior for a menu with menu-items I would not expect focusgroup to return me to the last item I invoked, I would expect it to drop me to the first focusable menu-item.
I agree this would be a benefit to users but IMO it would likely need to be opt-in and not prescribed for all because it isn't applicable for all instances.
@chrisdholt are we talking about a menu that is a dropdown, expanding when you click something? I would say if the menu closes in between, that would reset it. Otherwise, I don't see an issue with returning to the last focused item.
I wrote this in an earlier iteration of the explainer (e.g., https://github.com/MicrosoftEdge/MSEdgeExplainers/commit/6fd4e17e36df119d59e4968ca11c365b9096e3fe) which captures my thinking at the time. I still feel hesitant about starting to mess around with the sequential tab focus algorithm.
The "memory" part of a focusgroup could be added as an additional attribute value, such as "sticky" which would then cause the focusgroup to remember the last focused item and return to it. However, supporting such a feature would require potentially interfering with sequential focus navigation. For example, it becomes very challenging to predict where focus might go when entering a focusgroup. The "memory element" (saved element that is a tab stop) could be at the end of a list of tab-focusable elements. When entering the focusgroup, does the platform ignore and skip the set of tab-focusable elements that should come before the memory element, to jump directly to the "memory element"? Does it only jump to the memory element if it happens to be before (in tabindex navigation order) any of the other tab-able elements? Rather than tackle these concerns initially, this is left as a possible future extension.
But, let's brainstorm a bit. The use of tabindex>=0 is fully under author control. We have to assume that if an author marks an element with tabindex>=0 that they want that element to participate in sequential focus navigation. I think we have two options for adding "memory": one that respects the author's use of tabindex>=0 and one that dismisses their intent (within the scope of a focusgroup).
(focusgroup values below are for discussion purposes only ;-)
memory-reset
I think not having a memory is a good default given the relative potential surprises of the other approaches. We can view this as having a "reset-able" memory: e.g., as soon as focus is moved by the tab key (entering, exiting, or within a focusgroup's scope), the last focused item is forgotten, and the focusgroup "resets" back to its state as declared in the DOM by virtual of tabindex values. (This is the current stateless design.)
memory-roving
This first approach attempts to integrate with the sequential focus navigation order within a focusgroup. The idea is that the element that currently has focus acts as if it has a tabindex=0 value (even if it does not). Entering the focusgroup for the first time still respects the sequential focus navigation order, as does pressing the tab key (for elements marked with tabindex>=0 within the focusgroup). However, when the arrow keys move the focus to an element that was focusable with tabindex=-1, that element is treated as tabindex=0 until focus is moved to another element within the focusgroup, in which case it goes back to its tabindex=-1 state. So, given this:
<element tabindex=0> before </element>
<parent focusgroup>
<element tabindex=0> A </element>
<element tabindex=-1> B </element>
<element tabindex=0> C </element>
<element tabindex=-1> D </element>
</parent>
<element tabindex=0> after </element>
The natural sequential focus navigation order (for Tab) is [ before | A | C | after ]
.
If arrow keys move the focus to B, B is added to the order: [ before | A | B | C | after ]
.
When focus is moved to C (either by Tab or arrow keys), B is dropped from the order (because focus memory is still within the scope of the focusgroup): [ before | A | C | after ]
When focus goes to D: [ before | A | C | D | after ]
, and then the user Tabs out of the focusgroup to after
, D stays in the sequential navigation order [ before | A | C | D | after ]
. In this way, re-entering the focusgroup from the end (Shift+Tab) takes the user to the last focused element that is proximally closest to their entry-point in sequential navigation order.
It's not a perfect solution, but tries to respect the rest of the tabindex=0 values set by the author.
memory-override
This approach is to completely ignore the differentiation of tabindex=-1 and tabindex=0 for the purposes of sequential navigation order. Instead, the platform would wholly manage the sequential focus navigation order by implementing the roving tabindex pattern across everything focusable in the focusgroup's scope. In the previous example above, the tabindex=0 values would be ignored. The platform would pick an initial entry point (likely the first focusable item in DOM order if forward-tabbing into the focusgroup, or the last item if reverse-tabbing into the focusgroup) as the singleton tab stop for the focusgroup. No other tab stops would be possible. As the arrow keys move the focus around, the platform "remembers" that current element so that when tab moves focus out of the focusgroup (in either direction), that same element will be the designated return element when focus re-enters (either from a forward or reverse direction).
If there are other variations or approaches, I'm curious to hear what you think.
My POR was to defer this feature until CSS Toggles is farther along. It seems to me that rather than have a "last focused" memory, what authors really want is to have focus return to a current "selected" element (e.g., an active tab in a tab row). That selected item (or N selected items), introduces a third variable for consideration that might be more explicitly controllable by authors than the memory-roving
approach above.
@jcsteh @scottaohara @benbeaudry
@chrisdholt are we talking about a menu that is a dropdown, expanding when you click something? I would say if the menu closes in between, that would reset it. Otherwise, I don't see an issue with returning to the last focused item.
Sure - both could use focusgroup though (a menu which expands from a menu button or a static menu) and so you'd need to be able to choose the correct behavior for each. I agree on the value here. My point was primarily why it wasn't default behavior and why I think it would need to be opt-in. Again, we discussed this on one of our calls w/r/t behavior, just trying to recall examples we discussed specifically where it wasn't universal.
My opinions, so take with a grain of salt:
- tabindex > 0 is evil, and it should be strongly discouraged, and even not supported in a focusgroup.
Agreed here, I don't think we should encourage or support this (@travisleithead I think I'm on record on this in notes somewhere 😄 )
- When focusgroup is used everything that is focusable in any way should just be normalized to be focusable. Any value of tabindex is treated the same.
- memory of the last focus should be the default. The memory can clear when the focusgroup element is hidden, which takes care of the dropdown case.
So that's a +1 from @aleventhal for the focusgroup=memory-override
behavior that I desribed above, with the extra feature of "forgetting" when content is hidden.
It does result in the erasure of use cases where authors want to have multiple tab stops within a focusgroup. E.g., this is a use case employed by Microsoft Edge's tab row today. Not sure we can just drop that use case.
@travisleithead thanks, is there a demo of that use case I can peek at?
@travisleithead thanks, is there a demo of that use case I can peek at?
not a demo, but I think I show the use case in more detail in this recorded OpenUI meeting - around time index 04:44. Two permanent tab stops and a "roving" tab stop.
Travis summed up pretty well the three approaches we have: reset, roving or override.
Reset is trivial and doesn't solve this issue. It's also our current approach.
I think that the memory-roving approach is interesting and is our best shot at dealing with fixed tab stops within a focusgroup and keeping track of the last visited focusgroup-item (by setting its tabindex value to 0). However, I see a couple of potential issues with it:
This approach is one that would just "make sense" for the end user, so I like a lot. Making sure that there is always only one tab stop per focusgroup and leaving it be managed by the platform is a simple and great solution.
However, I think this would render the "natively tabbable" elements (buttons, links, etc.) not tabbable anymore and would potentially be very confusing for the end user.
Example:
<div focusgroup=horizontal>
<a href=#>Link 1</a>
<a href=#>Link 2</a>
<a href=#>Link 3</a>
<a href=#>Link 4</a>
</div>
The user might expect to be able to tab to each link, but because these links are now within a focusgroup, they aren't tabbable. This would feel weird in my opinion.
Note that for this to be a problem, the links and buttons would need to be the focusgroup items directly - not descendants of focusgroup items.
For example, this wouldn't be an issue:
<ul focusgroup=vertical>
<li tabindex=-1><a href=#>Link 1</a></li>
<li tabindex=-1><a href=#>Link 2</a></li>
<li tabindex=-1><a href=#>Link 3</a></li>
<li tabindex=-1><a href=#>Link 4</a></li>
</ul>
I think the memory-override approach is the most user-friendly, but the least developer-friendly because it breaks the tabbable behavior developers have come to expect - links, buttons, and any other element with tabindex>=0 won't be tabbable within a focusgroup. For this reason alone, I don't think this should be the default behavior - I'd like the developer to really opt-in that mode, knowing full well that it will break your usual tabindex values. This is just my opinion and should be taken with a grain of salt.
There are some good points here.
Is anyone up for having a video meeting on this topic? Just +1 this message if you want want to join.
There hasn't been any discussion on this issue for a while, so we're marking it as stale. If you choose to kick off the discussion again, we'll remove the 'stale' label.
Looking again at adding a memory (and making it the default behavior):
I do like the simplicity of the "memory-override" approach outlined above where the platform hijacks all the focusable elements, however, I think I found a problem use case that would need to be handled somehow. The scenario is:
<div focusgroup>
<a href="somewhere">hyperlink</a>
<label for=text>What's your name?</label>
<input id=text type=text>
<button>do something</button>
</div>
I'd like to propose a 4th approach, not mentioned above, that is similar to the current approach of not having a memory (and respecting all tabindex values), but which does add-in a "memory" and the memory is only used when re-entering the scope of the focusgroup (it's less complicated than "memory-roving"), and I hope it could make a good default behavior. If we need to add a way to opt-out, we could do that too.
In this approach, tabindex=0 and tabindex=-1 values are completely ignored but only when focus enters or re-enters a focusgroup. After that (once inside), it's business-as-usual for the Tab key with respect to the document's tab order given wherever focus redirected to. So, in the prior example, focus would still get trapped in the input type=text element in step 3, but step 4 would jump to the button element (next in tab order). If the user then leaves the focusgroup and returns (step 5) they would go to the last element they had focused, but could then use Tab to move around again (if they got stuck).
This approach allows webdevs to easily create roving tabindex behavior where there is only ever one tab stop, by setting everything tab-able to tabindex=-1 within a focusgroup, for example:
<div focusgroup>
<a href="somewhere" tabindex=-1>hyperlink</a>
<label for=text>What's your name?</label>
<input id=text type=text tabindex=-1>
<button tabindex=-1>do something</button>
</div>
which, of course, re-introduces the issue outlined above where the user can get stuck in the input, but at least in that case, they introduced the problem themselves, and can fix it by making some other element in the focusgroup tab-able.
And if we consider a case that doesn't involve an element like <input>
that captures keystrokes, then the combination of setting everything focusable to tabindex=-1 and adding a focusgroup acts just like the "memory-override" case.
Regarding @benbeaudry's extra question:
If a focusgroup doesn't have any tabbable focusgroup-item, should the focusgroup automatically make the first/last element tabbable (depending on the tab navigation direction)? Someone mentioned that in a comment above and I really liked the idea... should this be a default behavior regardless of "memory" mode, or would this lead to more issues?
I think this is nicely solved with the "entrance-memory" proposal. The first entrance to the focusgroup disregards what the tabindex values are and selects the focusable element that the focusgroup's memory is tracking, or if there is nothing in the memory, I think it should select the first focusable item in DOM order in the focusgroup (including if the first entrance is from Shift-Tab, reverse direction entrance).
Note: @gfellerph proposed an alternative approach that sticks with "memory-override", but addresses the problem of getting trapped in the input element by allowing the platform to temporarily make the next/previous focusable elements around the input element have tabindex=0 behavior to allow for a Tab-escape, and then reverting back to the normal behavior after focus moves. This would have to be pretty smart in case the next/previous elements were also focus-input traps too.
Need to also figure out under what conditions the memory of the focusgroup will be forgotten.
It could also get disabled, inert or excluded from the focusgroup by CSS or attribute, basically if it becomes not focusable (I wish there was a browser API like node.focusable
).
@travisleithead, is the difference between "memory-override" and "entrance-memory" that tabindex
settings by the author are respected the first time a focusgroup candidate gets focus but then thrown away for "memory-override" mode while "memory-override" will not initially respect the authors tabindex?
Not initialised (no element received focus yet), the author made the first button not focusable:
<div focusgroup>
<button tabindex="-1">First</button> <!-- set by the author, is skipped when user tabs into the focusgroup -->
<input type="text">
<input type="text">
<input type="text">
<button>Last</button>
</div>
User tabs into the focusgroup, first input receives focus as the button is being skipped:
<div focusgroup>
<button tabindex="0">First</button> <!-- gets activated for backwards tab exit -->
<input type="text" tabindex="0"> <!-- arrow keys are now handled by the input field, roving focus is trapped -->
<input type="text" tabindex="0"> <!-- gets activated for forwards tab exit -->
<input type="text" tabindex="-1">
<button tabindex="-1">Last</button>
</div>
In this state, it could also be possible to use selectionStart
to determine if the cursor is at the beginning or the end of the input field and handle arrow keypresses from there on to move back to the focusgroup. This would get a little harder with <audio>
, <video>
and friends if they have visible controls.
After pressing tab:
<div focusgroup>
<button tabindex="-1">First</button>
<input type="text" tabindex="0"> <!-- gets activated for backwards tab exit -->
<input type="text" tabindex="0"> <!-- has focus -->
<input type="text" tabindex="0"> <!-- gets activated for forwards tab exit -->
<button tabindex="-1">Last</button>
</div>
The whole focusgroup looses focus because the user clicked somewhere else:
<div focusgroup>
<button tabindex="-1">First</button>
<input type="text" tabindex="-1"> <!-- resets when the sibling input lost focus -->
<input type="text" tabindex="0"> <!-- memorised -->
<input type="text" tabindex="-1"> <!-- resets when the sibling input lost focus -->
<button tabindex="-1">Last</button>
</div>
All this tabindex setting is of course just necessary if there is no other way to take an element out of focus order while keeping it interactive otherwise.
@gfellerph thanks for illustrating your idea with examples.
@travisleithead, is the difference between "memory-override" and "entrance-memory" that tabindex settings by the author are respected the first time a focusgroup candidate gets focus but then thrown away for "memory-override" mode while "memory-override" will not initially respect the authors tabindex?
Did you mean 'but then thrown away for "entrance memory" mode while "memory-override" will not initially respect the authors tabindex?'? I'm not sure that quite captures it. Let me clarify using your example setup. (And remember that focusgroup will not actually change tabindex values in the DOM--I assume your examples are illustrative of what is happening internally).
From the not-initialized state (no memory yet), a tab into the focusgroup would put the focus on the <button>
in your example in both "entrance-memory" and "memory-override".
<input>
and so on like normal.<button>
is selected for the same reason, and then it works like a typical roving tabindex set. A tab key press would then jump out of the focusgroup.Hope that helps.
Current plan:
From a recent telecon (see issue #990), it was noted:
Make the entire focusgroup focusable (by default) so that it's always a tab stop (another alternative to tricky memory/tabindex behavior changes) (@keithamus)
This could be another alternative to needing to introduce special-case handling for tabindex...?
Note from @kbrilla copied here from #990
Roving tabindex with focusgroup (take 2)
(from this doc) Needs ability to opt out from memory mechanism as for example common pattern is using tabindex=0 on selected option and when you tab into/tab back into you always want that selected item to be first focused not the last item where you were (for example using arrows). From what I understand this will not be possible as memory mechanism takes priority before tabindex=0
I think this makes sense and is a good use case for having an opt-out.
The discussion in the meeting just been swayed me against the idea of making the whole group focusable so I withdraw my position on that.
The Open UI Community Group just discussed [focusgroup] Why not include memory of last focused item in group?
.
PR #1021 has landed and includes the changes for an "entrance-memory" as described above. That PR is a major re-write, so might be worth reading the explainer again top-to-bottom. Note, it also includes definition of a memory opt-out via no-memory
or focus-group-memory: none
.
Transferred from https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/579
Opened by @aleventhal
Focusgroup is really neat. I'm super happy to see it being worked on, and the number of scenarios handled is amazing.
Regarding this part, I admit I'm very disappointed that focusgroup doesn't handle this for us:
Is this not being handled because of the reliance on tabindex, which already hard codes whether something is tabbable or just focusable?
Maybe all we need is another property, like focus-group-memory: on|off|value. The default would be on. When used, all focusable descendants would get a computed tabindex of 0 or -1, overriding the attribute value.
User impact: the impact would be the highest in containers with many controls. Imagine tabbing into a grid and being at column 50, row 50. When you tab to the toolbar and come back, it would sure be helpful to be in the right place.
CC @travisleithead @benbeaudry
@jcsteh wdyt?
@benbeaudry wrote:
@jcsteh responded: