focus-visible
focus-visible copied to clipboard
Should :focus-visible match when returning focus or programmatically focusing?
When a user closes a modal, focus is programmatically returned to the previously focused element. For example, this is the case when using react-modal.
In this case, should :focus-ring apply to the focussed element or not?
I guess the same question applies to anytime when an element is focussed programmatically.
My guess is that, if the element that focus is returning to had :focus-ring when it was initially focussed, it should also have :focus-ring when focus is returned.
This one comes up a lot. @marcysutton, @alice, what do y'all think?
My hunch is if the user used a keyboard to close the modal (pressing escape or focusing the close button and pressing enter/space) then when focus is returned it should match :focus-ring. However, if the user used a mouse to close the modal then when focus is returned it would not match :focus-ring.
if the user used a keyboard to close the modal (pressing escape or focusing the close button and pressing enter/space) then when focus is returned it should match :focus-ring. However, if the user used a mouse to close the modal then when focus is returned it would not match :focus-ring.
I'd agree with that. If you use the keyboard to operate UI controls, you'll need a focus state at all times. The user's input modality is the important part here, not whether it was programmatically focused–that's an implementation detail.
Interesting, do you know if this is how the browser version of :focus-ring
will behave? Or is it still under consideration?
I would agree with Rob and Marcy's comments.
Re browser implementations, the spec deliberately doesn't specify when the pseudo-selector should match, so it'll be up to the browsers to decide.
Do you think this polyfill should move forward with the proposed behaviour, or does it need to wait?
I think the way the polyfill works today (latest version, at least) is if the user closes the modal via a keyboard action, the next element to receive programmatic focus would match :focus-ring.
I think the way the polyfill works today
@robdodson I just tried this and I don't think it does work that way currently.
Here is a demo: https://stackblitz.com/edit/wicg-focus-ring-test-programmatically-focusing
Clicking moveFocus
button programmatically focuses the foo
button.
When actioning moveFocus
with the keyboard, I would expect foo
to receive the focus-ring
, but it does not.
It seems that programmatically focused elements are not receiving .focus-visible
by any means currently. In fact I think such elements should always match :focus-visible
because even if they are focused from a mouse event, the focused element and the one being clicked may not always the same.
And people may be using mouse and keyboard at the same time, like open a dialog by clicking a button, and press enter to confirm an alert popup. The later one has a big chance to have an “OK” button which is programmatically focused. Moreover, for a confirm popup there might be both “OK” and “cancel” buttons so :focus-visible
should be essential.
In general, I think all focus events not originating from a pointer event (which indicates that the user probably know where the “focus” is upon clicks) should match :focus-visible
. The difference is that for devices with “linear access” need explicit focus indication while for those with “random access” to the screen, the visual hint becomes not so important. So should we just adjust the :focus-visible
logic from “handling keyboard triggered focuses” to “exclude pointer events triggered focuses” instead?
@OliverJAsh I think i found the issue.
https://github.com/WICG/focus-visible/blob/master/src/focus-visible.js#L82-L88
That has a list of accepted keys for moving focus. I think we added that for a previous radio group issue. Let me dig into this a bit more to see if we actually want to keep that array around.
ah yes... the reason we limit it to a known set of keys is because folks were complaining that their web app's keyboard shortcuts (like cmd-b to open a sidebar or something) were triggering keyboard modality and they didn't like that.
Having spent some time digging around in Chromium recently, I now know that Chromium doesn't check which key was used to move focus, it just checks to see if a mouse was used.
I'm actually working on implementing :focus-visible
behind a flag in Chromium and so far its behavior is similar to what @Justineo describes:
In general, I think all focus events not originating from a pointer event (which indicates that the user probably know where the “focus” is upon clicks) should match :focus-visible.
I image we'll want the polyfill to match this same behavior. So I'm tempted to just remove this array of allowed keys and make it so any keyboard press triggers :focus-visible
and since that will leave the hadKeyboardEvent
flag on in our polyfill, it means calling focus()
after a keyboard press will also apply .focus-ring
.
Actually we may need to do some additional work to fully match what @Justineo was describing. If you use a mouse to click an element that calls focus on another element, we need that other element to match .focus-visible
as well. Otherwise you end up with behavior like this, which I think is just weird (i'm using spacebar to press the other button because mouse clicking the first button calls focus() on it).
i'm using spacebar to press the other button because mouse clicking the first button calls focus() on it
@robdodson, yes that's indeed the problem I've been suffering from. Programmatically focusing an element indicates that the focus has moved away, in this case :focus-visible
will be really helpful.
And recently I found that in Firefox and Safari, mouse clicks on <button>
s won't move the focus to them. Instead, after the clicks, document.activeElement
remains to be <body>
(see here). I'm not sure if such behavior is documented in specs, but it seems a little weird to me. But if we adopt this approach, we may never actually need :focus-visible
anymore (we can always add visual styles to :focus
).
And recently I found that in Firefox and Safari, mouse clicks on
As you mentioned, I also think this behavior is interesting but weird and kinda wrong. If I mouse click on something then I assume it's focused and that I can also press spacebar to click it again. Either way, I don't know if we'd be able to change the way :focus
matching works in Chrome without getting a flood of folks complaining that we broke their site. The hope with :focus-visible
is to arrive at a consistent model that all browsers implement and then just shift the whole ecosystem over to using to it as we slowly wait for :focus
to fade away :P
I prefer :focus-visible
over the current behavior of Firefox/Safari (which seems to be a hack to somehow mimic the visual effect of Chrome's behavior) because the later one introduced side effects on user interaction as @robdodson just mentioned.
Let's get back to the current issue on the polyfill. The current implementation only works if the target element is focused via navigation keys like tab and arrow keys. If we hit space or enter on a <button>
and then focus the target element programmatically, it won't receive .focus-visible
. While for Chrome, it shows a focus ring on any programmatically focused element.
ATM I have no idea about how to detect programmatical focus and always apply .focus-visible
in the polyfill with the current keyboard-event-based approach. So what I'm proposing is, can we just apply .focus-visible
on any focus unless the target element is focused directly from a pointer event?
Yeah I plan to ship a fix for this today. I was working on another PR yesterday that I wanted to land first.
Hoping this PR fixes the issue: https://github.com/WICG/focus-visible/pull/118
Also want to share a link to a comment to something somewhat related: https://github.com/WICG/focus-visible/pull/64#issuecomment-363877207
bah! I realized the PR only solves the issue if a keyboard was used. Gotta think a bit more how to do this for a mouse...
bah! I realized the PR only solves the issue if a keyboard was used.
Yep. That's why I proposed to only exclude focuses following a pointer event (mousedown
, touchstart
, ...) instead of looking for those following a keyboard event.
exclude focuses following a pointer event
And we have to check if the focused element is the same as the one received the pointer event earlier. If not, it may be programmatically focused or some native mechanisms which changes focus, like focusing an <input>
after clicking a <label for="...">
.
Yep. That's why I proposed to only exclude focuses following a pointer event (mousedown, touchstart, ...) instead of looking for those following a keyboard event.
Yeah I'm not sure if that would solve this either. If, for instance, we set a flag on mouseDown that signals to the system to not apply focus-visible someone could do:
foo.addEventListener('mousedown', function() {
bar.focus(); // we don't know to put .focus-visible on bar
}, true);
i think i have a fix for this...
After talking with Alice and Marcy about this, I think we arrived at a bit of an impasse.
To recap,
Option 1: If a user mouse clicks (or touches) something that moves focus, we still consider them in "mouse modality", and don't apply focus-visible to the new element.
Option 2: We always apply focus-visible when focus()
is called, so the user can clearly tell where focus is.
Per earlier discussion in this thread, it sounded like we were in favor of option 1, but @Justineo and I discussed some situations where it might be misleading, and started to look into option 2.
The upside of option 2 is that it's very clear where focus has moved to. But the con(s) are that it isn't helpful on touch/mobile displays, and many developers already disable :focus
exactly because it matches in this situation. We might end up with folks doing :focus, :focus-visible { outline: none }
to get around it—we definitely don't want that.
Alice and Marcy were in favor of option 1, I think I'm more in favor of option 2, though I can see why option 1 makes sense (because ultimately what we care about is the user's input modality).
The easiest next step is to just fix the immediate problem at hand (the source of this bug) which is that only some keys trigger keyboard modality. I believe #118 will fix this issue. I've also created a branch with the option 2 behavior for folks who would prefer to use that in their app.
I've also reached out to a Chrome engineer to figure out if it will be possible for :focus-visible
to ship with the option 1 behavior, or if it will need to be more like option 2 (option 2 is essentially the way :focus works today). If it sounds like doing option 1 is going to be too difficult, we may have to switch to the option 2 behavior at some point down the line.
Agreed: the spec is deliberately vague so that we can ship with "good enough" behaviour, and tweak over time.
In my opinion we should err on the side of not showing a focus ring if in doubt, as in the short term users can force it to show via hitting a key on the keyboard, and hopefully in the medium term we will provide hooks for developers to force it to show.
But the con(s) are that it isn't helpful on touch/mobile displays, and many developers already disable :focus exactly because it matches in this situation.
As the spec allows UAs to decide whether the focus should be “visible”, I think mobile browsers can just choose to not match :focus-visible
under such situation. Our earlier discussion may be just based on desktop browsers (at least for me). On the polyfill side I'm not sure if we can apply UA detection (or based on some heuristics like screen sizes) to achieve this.
In my opinion, if keyboard navigation is unavailable, visual hint for current focus doesn't make much sense because next navigation can only be from another pointer event. Otherwise, we cannot predict next navigation is from pointer or keyboard. In this case, :focus-visible
does make sense and should be matched when the focused element is not the previously interacted element from a pointer event (either being keyboard navigation or programmatically changed).
Otherwise, we cannot predict next navigation is from pointer or keyboard.
Agreed.
In this case, :focus-visible does make sense and should be matched when the focused element is not the previously interacted element from a pointer event (either being keyboard navigation or programmatically changed).
I disagree. I think we should only match :focus-visible
in situations where we have some level of confidence that a keyboard is about to be used - i.e. if the user just used a keyboard, or is using a control which requires the use of a keyboard.
User's input modality can be mixed. eg. with one's right hand on the mouse and left hand on the keyboard. If not visual hint indicating that keyboard navigation is available, one may never be aware of this. For those keyboard navigation is just optional, not matching :focus-visible
may stop them from using keyboard in the first place (while keyboard + mouse may greatly improve efficiency, especially for operating those backend management systems).
Actually I think the current behavior on Chrome works exactly as I expected: when we hit on a button to call for an confirm dialog via confirm()
, the cancel button is focused with a focus ring. That's how I know I can just hit tab and space to confirm and proceed. If there's no such visual hint, I may continue to use mouse to click on the ok button instead but actually I'm able to proceed without moving my pointer.
I think dialogs are a special case here.
It might be a quite typical case for people to programmatically move focus IMO. And it's the problem the current issue want to solve after all.