-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make the focus fixup rule more explicit #8392
Conversation
Are you sure we don't want blur etc events to fire? In my implementation in Gecko we'd still fire the blur event. Also, Blink at least fires the blur event on node removal (unclear what the timing there is tho, see https://bugzilla.mozilla.org/show_bug.cgi?id=559561). Cc @EdgarChen |
Yeah, I think you are right, at least for the animation-frame case. I will update the patch for that case at least. For the node removal case, I am less sure. We have several developers complaining about Chrome's behavior there, and I guess no other browsers do it currently. Also, there is no great timing at which to fire the events; synchronously firing has the same problems as mutation events, and anything else seems pretty weird. So my preference (without yet consulting with other Chrome team members) would be to avoid firing the events for removal. |
I would like to strongly advocate for blur events to be fired on element removal. Without this, it is nearly impossible to build a working focus trap. If the focused element is removed, focus is moved to the body with no event, so no way for JS to handle this and move focus elsewhere (e.g. an adjacent element). My team recently commented on the Firefox bug (13 years old!) linked above advocating for this. We also opened a WebKit issue asking for the same thing, but it was closed with no explanation. Currently, Chrome is the only browser that implements this as we would expect. At the moment, we may have to do a very hacky workaround involving a MutationObserver on the whole document. This is unfortunately quite performance intensive, as it is impossible to observe when a particular element is removed - you have to observe the whole tree and get notified of many unrelated changes. Also, this is not even entirely working because these events fire asynchronously, which can cause other downstream issues. Firing a blur event in the browser just before the focused element is removed seems like to only way to truly fix this. |
@devongovett the problem with firing blur event on removal is timing. We generally don't want to run sync stuff when DOM is mutated, because it can change stuff like insertion validity checks etc. So whatever we do on removal, if we decide to fire the event, needs to be very precise on when it runs, and probably won't be perfect (since the script running could move the DOM node around etc). I wonder, why do you need a mutation observer for the whole document? Can't you just capture focus / blur and use a mutation observer for the current |
The problem then is if an ancestor element of the active element is removed, we wouldn't get an event. That leads to listening on the whole document. If firing a blur on the active element is not possible, would an alternative be to fire |
What was the reasoning to run the focus fixup step after rAF callbacks, but before intersection observer and resize observer callbacks? |
https://bugs.webkit.org/show_bug.cgi?id=237273 rdar://89902824 Reviewed by NOBODY (OOPS!). It's currently specced at whatwg/html#8392 as a rendering step after requestAnimationFrame callbacks. * LayoutTests/imported/w3c/web-platform-tests/inert/dynamic-inert-on-focused-element-expected.txt: * Source/WebCore/page/Page.cpp: (WebCore::Page::updateRendering): (WebCore::operator<<): * Source/WebCore/page/Page.h:
42c18da
to
c4533d4
Compare
Thanks! I've switched it to run after ResizeObserver. Still unsure what to do about blur/focus/change events on sync removal. I guess our choices are:
Maybe (3) is the way to go? |
I tend to agree that we should fire events on removal. Otherwise, as @devongovett points out, developers have no great way to tell what happened. It would seem that (3) is the best - still fire the events, but do it when convenient, and not synchronously. I support this change (to (3)), but I'm a tad worried about compat. |
The problem with (3) is that if you fire it on the element actually blurred, it won't bubble up because it's already removed... Maybe that's ok as long as we also fire |
There is also And since the issue is quite similar to DOM mutations, and microtasks were designed for MutationObserver, (6) feels quite reasonable to me. |
I thought about this more and I think any async version (so, including (3), (4), and (6)) are a no-go. Consider the following code: console.assert(document.activeElement === element1);
element1.remove(); // (1)
element2.focus(); // (2) If (1) causes
Given that some Between (1) and (5), I am still unsure. Using custom element reaction timing is clever, but this would be the first time we start using it beyond custom elements. It's also pretty weird that developers can opt in to this kind of sync-ish removal event on an arbitrary element, just by making it focused. "One weird trick to make your mutation observers sync!" I'm currently thinking that we should address @devongovett's use case via a different approach, instead of trying to make focus/blur events on removal work well. For example, he says he wants to detect that focus has changed: I think the bubbling |
Firing There are some related issues for |
I guess I didn't think through my suggestion of |
If you only fire focus on the body, without a blur, synchronously just after the element has been removed, what is the timing issue? Seems like this wouldn't have the performance impact of mutation events, which must bubble. If it's fired after removal, then additional mutations by the event listener shouldn't affect it. And if it's synchronous, the above example would work as expected. |
The issue is the same as with any synchronous mutation events: security, etc. See some discussion e.g. in whatwg/dom#533 (comment) or https://lists.w3.org/Archives/Public/public-webapps/2011JulSep/0779.html |
there is no issue with that. Firing event after the DOM mutation is fine. (It is DOMNodeRemoved which is the most problematic mutation event, it happens before the mutation). Firing event after the mutation is basically custom element reaction time. |
Yeah I figured firing it after mutation just before yielding back to the JS caller would solve that issue. I think an additional feature to make observing element removal easier in general would also be nice, but would be great to solve this case with focus events specifically if we can. Lots of developers probably won't think to add MutationObserver in addition to focus handlers, so it would be good if there were always a focus event whenever |
The problem here really just seems to be the new
...and since |
That kind of sequence still makes me quite uncomfortable. If the last event a page sees is |
* Split it into two variants: one which runs synchronously on HTML element removal, and one which runs during "update the rendering". Closes #8225. * After this split, only the "update the rendering" variant causes the normal focus algorithms to run, and thus blur and change events to fire. * Delete the confusing "somehow unfocused without another element being explicitly focused" sentence. Fixes #3847. Fixes #6729.
In the triage meeting at #8446, we had agreement to move forward with what's currently in this PR. Separately, we're interested in pursuing whatwg/dom#533 to make node removal observable, for the use cases mentioned here. As such, I think this PR is ready for review. @emilio, @nt1m, do you know the latest status on web platform tests? Especially for the cases around event timing. |
c4533d4
to
6a5109c
Compare
I have tests already reviewed in https://phabricator.services.mozilla.com/D156219 |
Glad that node removal observation will be worked on but I still think it's pretty weird that there is a case where |
However, they say "Don't check precise timing while we wait for more concrete spec edits." Gentle ping to update them with more precise timing checks :) |
Done, that also caught a bug in Gecko of course ;) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This generally looks good to me editorially. Left some minor inline nits.
@@ -77074,8 +77061,9 @@ partial interface <span id="NavigatorUserActivation-partial">Navigator</span> { | |||
named <code data-x="event-blur">blur</code> at <var>blur event target</var>, with | |||
<var>related blur target</var> as the related target.</p> | |||
|
|||
<p class="note">In some cases, e.g. if <var>entry</var> is an <code>area</code> | |||
element's shape, a scrollable region, or a <span>viewport</span>, no event is fired.</p> | |||
<p class="note" id="note-sometimes-no-blur-event">In some cases, e.g. if <var>entry</var> is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
<p class="note" id="note-sometimes-no-blur-event">In some cases, e.g. if <var>entry</var> is | |
<p class="note" id="note-sometimes-no-blur-event">In some cases, e.g., if <var>entry</var> is |
<p>For each <span>fully active</span> <code>Document</code> in <var>docs</var>, if the <span | ||
data-x="focused area of the document">focused area</span> of that <code>Document</code> is no | ||
longer a <span>focusable area</span>, then run the <span>focusing steps</span> for that | ||
<code>Document</code>'s <span>viewport</span>.</p> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is "is no longer" important or can it be "is not a"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it can be a bit confusing for the reader to see "focused area ... is not a focusable area", so "no longer" makes it a bit clearer what's going on.
But, it can also be confusing for the reader to think "no longer" might involve some sort of extra state-tracking, compared to "is not".
On balance, the latter seems worse, so I'll change it as you suggest.
https://bugs.webkit.org/show_bug.cgi?id=237273 Reviewed by Tim Nguyen. Implement step 15 of update the rendering between 14. servicing resize observers and 16. updating intersection observations: whatwg/html#8392 * LayoutTests/imported/w3c/web-platform-tests/css/css-contain/container-queries/layout-dependent-focus-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/selectors/focus-display-none-001-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/css/selectors/focus-within-display-none-001-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/inert/dynamic-inert-on-focused-element-expected.txt: * LayoutTests/imported/w3c/web-platform-tests/shadow-dom/focus/focus-shadowhost-display-none-expected.txt: * Source/WebCore/page/Page.cpp: (WebCore::Page::updateRendering): (WebCore::operator<<): * Source/WebCore/page/Page.h: Canonical link: https://commits.webkit.org/260067@main
Split it into two variants: one which runs synchronously on HTML element removal, and one which runs during "update the rendering". Closes Define timing of focus fixup rule precisely when triggered by style changes. #8225.
After this split, only the "update the rendering" variant causes the normal focus algorithms to run, and thus blur and change events to fire.
Delete the confusing "somehow unfocused without another element being explicitly focused" sentence.
Fixes #3847. Fixes #6729.
(See WHATWG Working Mode: Changes for more details.)
/infrastructure.html ( diff )
/interaction.html ( diff )
/interactive-elements.html ( diff )
/webappapis.html ( diff )