mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Fix unexpected ViewFocus events when Text Editing utilities change focus in the middle of a blur call. (flutter/engine#54965)
In [some cases][1], text editing utilities re-focus the `<input />` element during a blur event. This causes an unusual sequence of `focusin` and `focusout` events, leading to the engine sending unintended events.
Consider the following HTML code:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="container">
<input type="" value="1" id="input1">
<input type="" value="2" id="input2">
<input type="" value="3" id="input3">
</div>
<script>
container.addEventListener('focusin', (ev) => {
console.log('focusin: focus was gained by', ev.target);
});
container.addEventListener('focusout', (ev) => {
console.log('focusout: focus is leaving', ev.target, 'and it will go to', ev.relatedTarget);
});
</script>
</body>
</html>
```
Clicking input1, then input2, then input3 produces the following console logs:
```
// Input1 is clicked
focusin: focus was gained by <input type value=â"1" id=â"input1">â
// Input2 is clicked
focusout: focus is leaving <input type value=â"1" id=â"input1">â and it will go to <input type value=â"2" id=â"input2">â
focusin: focus was gained by <input type value=â"2" id=â"input2">â
// Input3 is clicked
focusout: focus is leaving <input type value=â"2" id=â"input2">â and it will go to <input type value=â"3" id=â"input3">â
focusin: focus was gained by <input type value=â"3" id=â"input3">â
```
Now, let's add a blur handler that changes focus:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="container">
<input type="" value="1" id="input1">
<input type="" value="2" id="input2">
<input type="" value="3" id="input3">
</div>
<script>
container.addEventListener('focusin', (ev) => {
console.log('focusin: focus was gained by', ev.target);
});
container.addEventListener('focusout', (ev) => {
console.log('focusout: focus is leaving', ev.target, 'and it will go to', ev.relatedTarget);
});
input2.addEventListener('blur', (ev) => {
input2.focus();
});
</script>
</body>
</html>
```
The log sequence changes and gives the wrong impression that no dom element has focus:
```
// Input1 is clicked
focusin: focus was gained by <input type value=â"1" id=â"input1">â
// Input2 is clicked
focusout: focus is leaving <input type value=â"1" id=â"input1">â and it will go to <input type value=â"2" id=â"input2">â
focusin: focus was gained by <input type value=â"2" id=â"input2">â
// Input3 is clicked, but the handler kicks in and instead of the following line being a focusout, it results in a focusin call first.
focusin: focus was gained by <input type value=â"2" id=â"input2">â
focusout: focus is leaving <input type value=â"2" id=â"input2">â and it will go to null
```
In addition to that, during `focusout` processing, `activeElement` typically points to `<body />`. However, if an element is focused during a `blur` event, `activeElement` points to that focused element. Although, undocumented it can be verified with:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="container">
<input type="" value="1" id="input1">
<input type="" value="2" id="input2">
<input type="" value="3" id="input3">
</div>
<script>
container.addEventListener('focusin', (ev) => {
console.log('focusin: was gained by', ev.target);
});
container.addEventListener('focusout', (ev) => {
console.log('document.hasFocus()', document.hasFocus());
console.log('document.activeElement', document.activeElement);
console.log('focusout: focus is leaving', ev.target, 'and it will go to', ev.relatedTarget);
});
input2.addEventListener('blur', (ev) => {
input2.focus();
});
</script>
</body>
</html>
```
We leverage these behaviors to ignore `focusout` events when the document has focus but `activeElement` is not `<body />`.
https://github.com/flutter/flutter/issues/153022
[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
This commit is contained in:
parent
31a579bf40
commit
cf40f73057
@ -339,6 +339,10 @@ extension DomHTMLDocumentExtension on DomHTMLDocument {
|
||||
@JS('visibilityState')
|
||||
external JSString get _visibilityState;
|
||||
String get visibilityState => _visibilityState.toDart;
|
||||
|
||||
@JS('hasFocus')
|
||||
external JSBoolean _hasFocus();
|
||||
bool hasFocus() => _hasFocus().toDart;
|
||||
}
|
||||
|
||||
@JS('document')
|
||||
|
||||
@ -64,6 +64,16 @@ final class ViewFocusBinding {
|
||||
});
|
||||
|
||||
late final DomEventListener _handleFocusout = createDomEventListener((DomEvent event) {
|
||||
// During focusout processing, activeElement typically points to <body />.
|
||||
// However, if an element is focused during a blur event, activeElement points to that focused element.
|
||||
// We leverage this behavior to ignore focusout events where the document has focus but activeElement is not <body />.
|
||||
//
|
||||
// Refer to https://github.com/flutter/engine/pull/54965 for more info.
|
||||
final bool wasFocusInvoked = domDocument.hasFocus() && domDocument.activeElement != domDocument.body;
|
||||
if (wasFocusInvoked) {
|
||||
return;
|
||||
}
|
||||
|
||||
event as DomFocusEvent;
|
||||
_handleFocusChange(event.relatedTarget as DomElement?);
|
||||
});
|
||||
|
||||
@ -270,6 +270,30 @@ void testMain() {
|
||||
expect(dispatchedViewFocusEvents[0].state, ui.ViewFocusState.focused);
|
||||
expect(dispatchedViewFocusEvents[0].direction, ui.ViewFocusDirection.forward);
|
||||
});
|
||||
|
||||
test('works even if focus is changed in the middle of a blur call', () {
|
||||
final DomElement input1 = createDomElement('input');
|
||||
final DomElement input2 = createDomElement('input');
|
||||
final EngineFlutterView view = createAndRegisterView(dispatcher);
|
||||
final DomEventListener focusInput1Listener = createDomEventListener((DomEvent event) {
|
||||
input1.focusWithoutScroll();
|
||||
});
|
||||
|
||||
view.dom.rootElement.append(input1);
|
||||
view.dom.rootElement.append(input2);
|
||||
|
||||
input1.addEventListener('blur', focusInput1Listener);
|
||||
input1.focusWithoutScroll();
|
||||
// The event handler above should move the focus back to input1.
|
||||
input2.focusWithoutScroll();
|
||||
input1.removeEventListener('blur', focusInput1Listener);
|
||||
|
||||
expect(dispatchedViewFocusEvents, hasLength(1));
|
||||
|
||||
expect(dispatchedViewFocusEvents[0].viewId, view.viewId);
|
||||
expect(dispatchedViewFocusEvents[0].state, ui.ViewFocusState.focused);
|
||||
expect(dispatchedViewFocusEvents[0].direction, ui.ViewFocusDirection.forward);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user