w3c / uievents

UI Events
https://w3c.github.io/uievents/
Other
145 stars 51 forks source link

Should an element remain focused and receive key events if it's display:none-ed? #236

Open dbates-wk opened 5 years ago

dbates-wk commented 5 years ago

Consider a page with the following markup:

<!DOCTYPE html>
<html>
<style>
.collapse {
    display: none;
}
</style>
<body>
<p>This page focuses a text field, then hides the field by display:none-ing its containing block after one second.</p>
<div id="test">
    <input id="input" type="text" onblur="logMessage('field blur')" onfocus="logMessage('field focus')" onkeydown="logMessage('field keydown')">
</div>
<pre id="console"></pre>
<script>
function runTest()
{
    document.getElementById("test").classList.remove("collapse");
    document.getElementById("input").focus();

    window.setTimeout(() => {
        document.getElementById("test").classList.add("collapse");
    }, 1000);
}

function logMessage(message)
{
    document.getElementById("console").appendChild(document.createTextNode(message + "\n"));
}

runTest();
</script>
</body>
</html>

Load the page, wait one second. What events do you see printed? In Safari on macOS Mojave and Firefox for Mac 67.0.4 I see:

field focus

In Chrome for Mac version 75.0.3770.100 and Chrome Canary for Mac version 77.0.3835.0 I see, emphasis mine:

field focus
**field blur**

So, should a blur event be fired?

So that was the most important question. Many things fall out from the answer to that question. Just to give a sample of things that fall out try this: press a key, like 'a'. Then in Safari I see, emphasis mine:

field focus
**field keydown**

Both Firefox and Chrome don't fire such keydowns. But remember, Firefox never fired a blur event (above) so the field is still focused? So it should get key events?

The spec does not seem to specify what the behavior should be when an element's display property is changed to "none". Can we please get some spec. text for this?

dbates-wk commented 5 years ago

Shoutout @garykac @travisleithead @rniwa @annevk @hober

dbates-wk commented 5 years ago

@smfr

rniwa commented 5 years ago

Firefox and WebKit don't fire blur event when the element is removed from the DOM either. For consistency, it's probably better if we didn't fire blur event in the case.

Also, firing blur event in this scenario poses a question as to when the event needs to be fired. We definitely don't want to expose the timing at which the style resolution had happened so that would mean the event needs to be dispatched when the rendering is updated.

I'm pretty sure Chrome sets up some kind of 0s timer to fire blur event in this case. But this approach is problematic in that it would mean we'd have to update the style immediately after running each task to detect that the focused element is no longer rendered. Ideally, we want to delay such a style resolution up until the next rendering opportunity occurs.

<!DOCTYPE html>
<html>
<body>
<pre id="log"></pre>
<script>

log = (text) => document.getElementById('log').textContent += text + '\n';

window.onload = () => {
    const input = document.createElement('input');
    document.body.appendChild(input);
    input.focus();
    input.addEventListener('blur', () => log('blur event fired'));
    input.style.display = 'none';
    setTimeout(() => {
        log('setTimeout fired');
    }, 0);
}

</script>
</body>
</html>

@rakina @tkent-google @domenic

annevk commented 5 years ago

As far as specifying this goes, I think it should go into HTML as that largely defines the focus model for the web. (There's various open issues here and in whatwg/html to make it better.)

As far as behavior goes, treating it equivalently to tree removal seems reasonable. We don't really have a "loses its CSS box" primitive at the moment, but adding that seems somewhat reasonable. Even if the blur event is not fired, there are other ways to observe whether the element lost focus, right? E.g., activeElement? So depending on what accessors flush layout this might still be observable before a more ideal point in time.

dbates-wk commented 5 years ago

Even if the blur event is not fired, there are other ways to observe whether the element lost focus, right? E.g., activeElement?

Looking at activeElement does not seem be a way to observe this (and I can't help be think: why would it be? Last I recall it represented the focused element ignoring window activation <-- the AppKit/UIKit/GUI toolkit concept, haven't checked the spec, yet. FYI, In Safari and Firefox document.activeElement is still the <input> after the display:none'ing in the example. Only Chrome changes the activeElement, but then it also dispatches a blur 😕

dbates-wk commented 5 years ago

As far as specifying this goes, I think it should go into HTML as that largely defines the focus model for the web. (There's various open issues here and in whatwg/html to make it better.)

Great! Let's add some spec text!

othermaciej commented 5 years ago

I did a bit of digging in the HTML spec. I believe the Chrome behavior (dispatch blur on an element that becomes display: none) is currently required by the HTML Living Standard.

The focus fixup rule says:

When the designated focused area of the document is removed from that Document in some way (e.g. it stops being a focusable area, it is removed from the DOM, it becomes expressly inert, etc.), designate the Document's viewport to be the new focused area of the document.

The requirements for being a focusable area include being rendered for most elements, which requires producing CSS boxes. Thus a display:none element would not be a focusable area. This will run the focus update steps which dispatch the blur event.

This behavior logically makes sense to me, because a display: none element can’t be focused in the first place, so an element that becomes display: none shouldn’t retain focus, and everything that happens upon loss of focus should happen.

rniwa commented 5 years ago

Note that WebKit's behavior to not lose focus (blur) the element which loses CSS box due to display:none was a compatibility requirement on facebook's mobile site as recently as five years ago. This is also the behavior IE had and WebKit deliberately decided not to remove focus from such an element seven years ago.

It's possible that the compatibility story has changed in recent years due to Blink's market share but I wouldn't discount the major Web compatibility risk Gecko and WebKi would face if we were to implement Blink's relatively new behavior. In fact, Blink is literally the only major engine which exhibits this behavior so I'm inclined to say the spec needs to be updated to match WebKit/Gecko/IE behavior instead assuming Blink is willing to change its behavior back.

rniwa commented 5 years ago
<!DOCTYPE html>
<html>
<body>
<pre id="log"></pre>
<script>

onload = () => {
    const input = document.createElement('input');
    document.body.appendChild(input);
    input.focus();
    input.addEventListener('blur', () => log.textContent += 'blur');
    input.style.display = 'none';
    document.body.getBoundingClientRect();
    input.style.display = null;
}

</script>
</body>
</html>

Doesn't log blur in Chrome regardless of document.body.getBoundingClientRect() is executed or not so this seems to indicate that Chrome is indeed waiting for some time to decide whether the element has become invisible or not. The following example also seems to always logs blur and this is problematic because it implies that we'd have to resolve the style after each task. We don't want to do that.


<!DOCTYPE html>
<html>
<body>
<pre id="log"></pre>
<script>

onload = () => {
    const input = document.createElement('input');
    document.body.appendChild(input);
    input.focus();
    input.addEventListener('blur', () => log.textContent += 'blur');
    input.style.display = 'none';
    setTimeout(() => {
        input.style.display = null;
    }, 0);
}

</script>
</body>
</html>
tkent-google commented 5 years ago

I agree that the blur behavior in this case is not important for site compatibility, and we can change the HTML specification. However, dispatching no events and keeping activeElement aren't logical and such behavior would be a pitfall for web developers. So I wonder if we can define an algorithm which is easily implementable.

A wild idea is to add something like activeElement-focsusable-check step to Update the rendering. It can handle many cases such as disconnecting focused element, changing tabindex/contenteditable attributes, changing disabled state as well as loosing CSS box.

rniwa commented 5 years ago

A wild idea is to add something like activeElement-focsusable-check step to Update the rendering. It can handle many cases such as disconnecting focused element, changing tabindex/contenteditable attributes, changing disabled state as well as loosing CSS box.

That's probably the only sane way to define this behavior.