focus-visible icon indicating copy to clipboard operation
focus-visible copied to clipboard

Should :focus-visible match when returning focus or programmatically focusing?

Open OliverJAsh opened this issue 7 years ago • 40 comments

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.

OliverJAsh avatar Dec 07 '17 21:12 OliverJAsh

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.

robdodson avatar Dec 12 '17 20:12 robdodson

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.

marcysutton avatar Dec 12 '17 20:12 marcysutton

Interesting, do you know if this is how the browser version of :focus-ring will behave? Or is it still under consideration?

OliverJAsh avatar Dec 13 '17 09:12 OliverJAsh

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.

alice avatar Dec 19 '17 00:12 alice

Do you think this polyfill should move forward with the proposed behaviour, or does it need to wait?

OliverJAsh avatar Dec 19 '17 14:12 OliverJAsh

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.

robdodson avatar Dec 19 '17 19:12 robdodson

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.

OliverJAsh avatar Jan 04 '18 18:01 OliverJAsh

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?

Justineo avatar Feb 04 '18 16:02 Justineo

@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.

robdodson avatar Feb 05 '18 21:02 robdodson

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.

robdodson avatar Feb 05 '18 21:02 robdodson

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).

click

robdodson avatar Feb 05 '18 21:02 robdodson

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).

Justineo avatar Feb 06 '18 03:02 Justineo

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

robdodson avatar Feb 06 '18 16:02 robdodson

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?

Justineo avatar Feb 07 '18 05:02 Justineo

Yeah I plan to ship a fix for this today. I was working on another PR yesterday that I wanted to land first.

robdodson avatar Feb 07 '18 16:02 robdodson

Hoping this PR fixes the issue: https://github.com/WICG/focus-visible/pull/118

robdodson avatar Feb 07 '18 19:02 robdodson

Also want to share a link to a comment to something somewhat related: https://github.com/WICG/focus-visible/pull/64#issuecomment-363877207

robdodson avatar Feb 07 '18 19:02 robdodson

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...

robdodson avatar Feb 07 '18 22:02 robdodson

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.

Justineo avatar Feb 08 '18 03:02 Justineo

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="...">.

Justineo avatar Feb 08 '18 03:02 Justineo

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);

robdodson avatar Feb 08 '18 19:02 robdodson

i think i have a fix for this...

robdodson avatar Feb 08 '18 21:02 robdodson

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.

robdodson avatar Feb 10 '18 00:02 robdodson

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.

alice avatar Feb 10 '18 01:02 alice

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.

Justineo avatar Feb 10 '18 02:02 Justineo

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).

Justineo avatar Feb 10 '18 03:02 Justineo

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.

alice avatar Feb 12 '18 01:02 alice

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.

feb-12-2018 12-25-53

Justineo avatar Feb 12 '18 04:02 Justineo

I think dialogs are a special case here.

alice avatar Feb 12 '18 06:02 alice

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.

Justineo avatar Feb 12 '18 07:02 Justineo