w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.43k stars 652 forks source link

[selectors] :focus-visible matches on initial programmatic focus #5885

Open mrego opened 3 years ago

mrego commented 3 years ago

We have a test focus-visible-010.html that checks that a programmatic focus on the load event, causes that the element getting focused matches :focus-visible. This test passes in the 2 implementations of :focus-visible (Chromium and Firefox).

However the spec doesn't mention anything about this in the suggestions list, and given that 2 browsers follow that, and we have a test, maybe it'd be nice to add that to the list too.

The spec mentions 2 cases of programmatic focus:

  • If the active element matches :focus-visible, and a script causes focus to move elsewhere, the newly focused element should match :focus-visible.
  • Conversely, if the active element does not match :focus-visible, and a script causes focus to move elsewhere, the newly focused element should not match :focus-visible.

But it doesn't mention what happens when there's no active element before the programmatic focus. WDYT?

CC @alice @emilio

alice commented 3 years ago

Yeah, we probably should have included that in the heuristics listed in the spec when we added it in our implementation. I suspect I was just exhausted at the time.

I would support adding some language as the second bullet point (after the comment about user preferences) like:

By default, if there is no other signal to indicate whether focus should be made visible (such as a programmatic focus after page load, or focus triggered by the autofocus attribute), :focus-visible should match on the active element.

mrego commented 3 years ago

I think we should consider 2 cases:

  1. Script focus on load event or via autofocus.
    <div id="target" tabindex="0">Target</div>
    <script>
    window.addEventListener("load", () => target.focus());
    </script>
  2. Script focus at any point when there's no current active element.
    <div id="target" tabindex="0">Target</div>
    <script>
    setTimeout(() => target.focus(), 1000);
    </script>

I guess we want :focus-visible to match in both cases, at least that's what Chromium and Firefox do.

alice commented 3 years ago

Hm, what happens in the second case after a blur()? The code above is still directly after page load, it's just longer after a page load.

https://codepen.io/sundress/pen/WNGqobM tests the same thing, but after a user has interacted with the page, blurring the previously active element before the delay. In my testing, Chrome "remembers" the previous value for :focus-visible (i.e. matches when the "Press this button" button was pressed using the keyboard, doesn't match when it was pressed using a mouse). The same behaviour happens without the delay as well.

It seems that Firefox has not (yet?) implemented this suggested heuristic: "If the active element matches :focus-visible, and a script causes focus to move elsewhere, the newly focused element should match :focus-visible."

What do you think the behaviour should be for this case?

emilio commented 3 years ago

Firefox implements that heuristic. But there's no active element since blur() was called, so that heuristic doesn't apply, what am I missing?

emilio commented 3 years ago

If you're on mac, buttons don't get focused by mouse. That's platform behavior (WebKit does the same).

emilio commented 3 years ago

It seems to me that if there's no previous active element (so, blur() was called, or there's no focused element or what not), showing the outline is the sensible thing to do. That heuristic seems to agree (or my reading of it, maybe?).

How is the user supposed to know what's focused otherwise?

mrego commented 3 years ago

Firefox implement the heuristics in the first comment as it passes focus-visible-{014,015,016}.html tests.

I believe I'm aligned with @emilio, I don't really care if this is just after page load, or after the user has interacted with the website. In my mind, if nothing is focused (there's no active element), and a script focus something, it's good to match :focus-visible.

So maybe the 2 heuristics in the spec could be reworded in just one, something like:

If the active element does not match :focus-visible (or there is no active element), and a script causes focus to move elsewhere, the newly focused element should match :focus-visible.

alice commented 3 years ago

I updated the codepen to use focusable divs instead of buttons - focusable divs are focused on click in WebKit and Firefox. Now I can see that Firefox shows a focus outline after programmatic focus after a blur(), but not if the blur() is skipped. (The delay makes no difference in behaviour.)

We didn't write the current language into the spec by accident; it was the result of a lot of thought and discussion, for example: https://github.com/WICG/focus-visible/issues/88

A couple of questions to think about:

emilio commented 3 years ago

We didn't write the current language into the spec by accident; it was the result of a lot of thought and discussion, for example: WICG/focus-visible#88

Sure, and I agree with the spec language :). I guess the "no active element" case really kinda falls through all the conditions of the spec, though I think the Firefox behavior is the right one, because otherwise you don't show outlines for random focus moves that the user has no way of knowing about.

Under what circumstances do authors programmatically move focus?

I expect the current spec language is pretty useful to do stuff like: Click a button, open a menu, move the focus to that menu, or stuff like that.

Why would someone not currently or likely to immediately start using the keyboard want to know what the currently focused element is?

I don't know how this question is particularly relevant to this issue, but I agree that elements that accept keyboard input should always trigger focus-visible.

alice commented 3 years ago

... I think the Firefox behavior is the right one, because otherwise you don't show outlines for random focus moves that the user has no way of knowing about.

There's not much practical difference between

newFocusTarget.focus();

and

document.activeElement.blur();
newFocusTarget.focus();

Why should they result in different behaviour?

Under what circumstances do authors programmatically move focus?

I expect the current spec language is pretty useful to do stuff like: Click a button, open a menu, move the focus to that menu, or stuff like that.

Agreed, that is what the current spec covers - those are cases where a user interaction has caused focus to move, so we cue off the user interaction.

It's exceptionally hard to think of a case other than immediately after page load when an author would move focus not in response to a user interaction.

Given that Firefox and Safari have internally inconsistent (but consistent with the operating system) behaviour for focus on click on macOS (focus is set when clicking a focusable <div>, but not when clicking a <button>, for example), it might be worth adding some language to capture the case where a user has interacted with an element which does not receive focus on click in some cases, and that interaction caused focus to move.

Why would someone not currently or likely to immediately start using the keyboard want to know what the currently focused element is?

I don't know how this question is particularly relevant to this issue, but I agree that elements that accept keyboard input should always trigger focus-visible.

It's relevant to this earlier question:

How is the user supposed to know what's focused otherwise?

My answer is that if the user is not about to use the keyboard, they don't need to know, and the remainder of the rules (including my proposed language from https://github.com/w3c/csswg-drafts/issues/5885#issuecomment-765008329) ensure that in the majority of cases where they would be likely to be interested in what element is focused, the focus is shown.

emilio commented 3 years ago

... I think the Firefox behavior is the right one, because otherwise you don't show outlines for random focus moves that the user has no way of knowing about.

There's not much practical difference between

newFocusTarget.focus();

and

document.activeElement.blur();
newFocusTarget.focus();

Why should they result in different behaviour?

Well, because the way you're "transferring" the knowledge of whether focus came from a pointing device or keyboard or what not in the rules in the spec is via whether the previously focused element matched :focus-visible. It's an heuristic, there's nothing saying that the page can't move the focus 10s later randomly after you click a button, and the focus won't be visible then. But the heuristic is useful because that is unlikely to happen.

My answer is that if the user is not about to use the keyboard, they don't need to know, and the remainder of the rules (including my proposed language from #5885 (comment)) ensure that in the majority of cases where they would be likely to be interested in what element is focused, the focus is shown.

Well, sure, but you can't guess intent from a focus() call. For example, I find the firefox behavior quite useful when I'm going back to a tab (Firefox will programmatically restore the focus to where it was, so I know where the next tab key press will get me).

fvsch commented 3 years ago

As a UI developer, I like the current heuristic on paper. It’s simple enough that it can be explained, which helps to a) use it right and b) work around it in edge cases.

But if I understand it right, the specifics of “no focus for clicked buttons” on macOS (WebKit and Firefox) make things much less straightforward when clicking a button then moving the focus programmatically.

Is it correct that it makes it impossible, on macOS, to programmatically move focus to a target element — e.g. the first focusable element in a modal — after a click on a button and have :focus-visible NOT apply to that target element?

This could mean that we will have to avoid using :focus-visible styles as product owners and QA report bugs with steps-to-reproduce such as:

  1. Click the button to open a modal.
  2. The modal shows up on the screen. In the modal, the first button [or link] has a big blue border.

Remove that border.

The only workarounds I can think of all damage accessibility, e.g.:

bkardell commented 3 years ago

So, it seems there is some confusion here and I think that some of this actually comes from how we have written things as much as anything else.

Here are the current heuristic rules from the spec. They are bullets in the spec, but I am using numbers to make it a little easier to compare, but I guess I also think they are ordered points...

  1. If a user has expressed a preference (such as via a system preference or a browser setting) to always see a visible focus indicator, the user agent should honor this by having :focus-visible always match on the active element, regardless of any other factors. (Another option may be for the user agent to show its own focus indicator regardless of author styles.)

  2. Any element which supports keyboard input (such as an input element, or any other element which may trigger a virtual keyboard to be shown on focus if a physical keyboard is not present) should always match :focus-visible when focused.

  3. If the user interacts with the page via the keyboard, the currently focused element should match :focus-visible (i.e. keyboard usage may change whether this pseudo-class matches even if it doesn’t affect :focus).

  4. If the user interacts with the page via a pointing device, such that the focus is moved to a new element which does not support user input, the newly focused element should not match :focus-visible.

  5. If the active element matches :focus-visible, and a script causes focus to move elsewhere, the newly focused element should match :focus-visible.

  6. Conversely, if the active element does not match :focus-visible, and a script causes focus to move elsewhere, the newly focused element should not match :focus-visible.


I kind of think that we're getting trapped in some words/phrasing... I think this is the problematic part "If the active element matches :focus-visible, and a script causes focus to move elsewhere". I believe that what I am seeing is mostly that how it is being read isn't uniform: Emilio has interpreted this (I think) as "as focus is initiated, look to see if there is an active element". Thus, if a blur has happened, there isn't, so to him his treatment makes sense. However, maybe a better way to say this is ""we look at how they last interacted with the page". Thus, if you look at rego's examples, and consider Alice's followons about blur and her pens, you can see that she is trying to show that's not right. I kind of personally feel like our initial take on this which talked somehow about modality was important. I kind of still wonder if there should be some concept like that, as least in words (though, in practice even a prop might be good)...

In any case, I have attempted to provide a modified set of rules that @alice and I, I think, would agree too and I wonder if make the intents clearer?

  1. If a user has expressed a preference (such as via a system preference or a browser setting) to always see a visible focus indicator, the user agent should honor this by having :focus-visible always match on the active element, regardless of any other factors. (Another option may be for the user agent to show its own focus indicator regardless of author styles.)

  2. Any element which supports keyboard input (such as an input element, or any other element which may trigger a virtual keyboard to be shown on focus if a physical keyboard is not present) should always match :focus-visible when focused.

  3. If the user interacts with the page via the keyboard, the currently focused element should match :focus-visible (i.e. keyboard usage may change whether this pseudo-class matches even if it doesn’t affect :focus).

  4. If the user interacts with the page via a pointing device, such that the focus is moved to a new element which does not support user input, the newly focused element should not match :focus-visible.

  5. If the user has not interacted with the page, and a script (or similar behavior via autofocus) causes focus to be set, the newly focused element should match :focus-visible

  6. If the user's last interaction with the page would cause an element to match :focus-visible, and a script causes focus to move elsewhere, the newly focused element should match :focus-visible.

  7. Conversely, if the user's last interaction with the page would cause an element to to not match :focus-sible, and a script causes focus to move elsewhere, the newly focused element should not match :focus-visible.

css-meeting-bot commented 3 years ago

The CSS Working Group just discussed [selectors] :focus-visible matches on initial programmatic focus.

The full IRC log of that discussion <dael> Topic: [selectors] :focus-visible matches on initial programmatic focus
<dael> github: https://github.com/w3c/csswg-drafts/issues/5885
<Rossen_> q
<dael> rego: This is about if focus-visible should match after programmatic focus. Current heuristics, though not normative, talk about if active element matches then next element will match. Spec didn't mention anything about if there's no active element
<dael> rego: Proposal is to change a bit so instead of saying it's the active element, you have to check last user interaction. ANd if none you match focus-visible. But if someone clicked a button and there was blur you wouldn't match b/c first was not a mouse
<dael> emilio: What interactions count and which don't? That's an issue. I think current heuristic is fine. It's a problem on mac b/c mac doesn't focus a bunch when you click on it. A lot of programs when you click and then move focus the heuristics say button wasn't focused so new thing shouldn't mtach
<dael> emilio: On mac since element isn't focusable by mouse you hit this issue. Good thing to come up with something that works. Gneerally agree with proposal, but which interactions count and which don't? I want the definition to be clear
<dael> emilio: Does an interaction a second ago prevent programmatic focus from matching?
<dael> florian: Confused. Are we trying to define when a UA shows a focus ring? If not, why defining when focus-visible shows?
<dael> rego: Everyone is following heuristics. If we have clear heuristics then it's better
<dael> florian: They're supposed to be heuritstics about when they show focus ring and focus-visible matches.
<dael> emilio: True
<dael> florian: So we're talking about the combo, not getting out of sync
<dael> emilio: Right
<dael> emilio: I'm okay with the proposal. I jsut want whatever this interaction means to be clarified
<dael> florian: Wondering if right spec and place. I htink CSS spec has what it needs. We match browser focus ring and the pseudo class. When the browser shows focus feels more like HTML. Should we move them and call them normative?
<dael> emilio: It's an option. Fine with that
<dael> florian: If discussing state of doc and state of UI it's not a very css-y topic
<dael> fantasai: This text is just an example. It's not normative
<dael> florian: If you want normative it belongs in HTML, right?
<dael> Rossen_: What are we doing with this?
<dael> rego: Keep going on the issue and see if we should move this
<dael> florian: Just before we wrap up, my impression is even though called heuristisc we're trying to harmoize browsers about when they show/don't show focus ring
<dael> Rossen_: And make it more detectable
<dael> florian: We have detectable. We have the pseudo class that matches browser behavior. If we want to define browser we should have that in html
<dael> Rossen_: I think right next step are add any pseudo class discussions in the topic and rego will continue working with html group to see if there's additional behavior to define there
mrego commented 3 years ago

From the last time we discussed this:

emilio: What interactions count and which don't?

I think it's important we define this, does a random click on any part of the page is an interaction related to this or not? And there are other kind of special situations in which it'd be nice to define what's a meaningful interaction regarding these heursitics.

For that reason I created a series of tests with different examples and use cases (maybe more could be added), it'd be nice to reach an agreement in how they should work before we can prepare the spec text (even the the spec text would be for HTML spec and not the CSS one): https://github.com/web-platform-tests/wpt/pull/27806

SaidMarar commented 1 year ago

Hi all,

Well i agree with @mrego "the user interacts with the page" is not clear. Is it interacting with focusable elements ? or with everything in the page ?

In this example the rule number 4 fails when we mouse-click on an element that is not focusable and we move focus to another element with an initial programmatic focus.

Current behavior:

As long as we dont click on focusable element the :focus-visible will always match after initial programmatic focus.

Please check the example for detailed scenarios.

oliviertassinari commented 1 month ago

Is there any discussion to drop this ":focus-visible matches on initial programmatic focus" heuristic? I'm trying to understand why it behaves like this by default. It breaks my intuition.

I have experienced two wrong end-user behaviors that feels like comes from this heuristic:

  1. https://github.com/mui/material-ui/issues/23747
  2. https://next.mui.com/ :focus-visible triggers but feels like it makes no sense in this context:

https://github.com/user-attachments/assets/7f531d12-ee7b-44f0-a75e-848f857cc166

emilio commented 1 month ago

I think the heuristic generally makes sense. If there's a programmatic call where it doesn't you can pass focusVisible: false to focus()

oliviertassinari commented 1 month ago

@emilio to rely on element.focus({ focusVisible: true/false }) https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#focusvisible / https://html.spec.whatwg.org/multipage/interaction.html#focus-management-apis. I imagine that we need to:

Maybe it would make sense to have element.focus({ modality: 'keyboard' / 'pointer' }) instead. I would tell the browser the origin modality of the focus, did it come form a mouse over, a keydown event, etc?

emilio commented 1 month ago

change the spec to support false too, it seems to be only about forcing true in the option description.

Wdym? False is supported, MDN is just wrong. The spec differentiates between true, false, and not provided (which is where the heuristic kicks in)

oliviertassinari commented 1 month ago

@emilio I see only a mention of the behavior when focusVisible: true

SCR-20240714-pgnp

https://html.spec.whatwg.org/multipage/interaction.html#dom-focusoptions-focusvisible

emilio commented 1 month ago

Right, thus if it's false it never indicates focus, which means it'll not match the pseudo-class.

The spec seems clear, but I wrote it so I'm obviously biased :)

But the spec is basically saying (in JS terms):

let matchesFocusVisible = options.focusVisible || (options.focusVisible === undefined && heuristicMatches);

Right? Which means that the false case is well-defined, it just works by not indicating focus.

I'll send an MDN PR tomorrow once I'm on my desktop, if I don't forget :)

oliviertassinari commented 1 month ago

Right, thus if it's false it never indicates focus, which means it'll not match the pseudo-class.

@emilio it feels like this contradict this issue description:

We have a test focus-visible-010.html that checks that a programmatic focus on the load event, causes that the element getting focused matches :focus-visible. This test passes in the 2 implementations of :focus-visible (Chromium and Firefox).

emilio commented 1 month ago

That test is not using focusVisible: false, thus using the heuristic.