Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 5e0df01

Browse files
authored
Make the web engine publish view forward focus and unfocus events (#50177)
Make the flutter web engine publish view focus events. Relevant Issues are: * Design doc link: https://github.com/flutter/website/actions/runs/7560898849/job/20588395967 * Design doc: flutter/flutter#141711 * Focus in web multiview: flutter/flutter#137443 * Platform dispatcher changes: #49841 [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent 1b8f23b commit 5e0df01

File tree

7 files changed

+239
-1
lines changed

7 files changed

+239
-1
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6002,6 +6002,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/onscreen_logging.dart + ../..
60026002
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/picture.dart + ../../../flutter/LICENSE
60036003
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart + ../../../flutter/LICENSE
60046004
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/platform_dispatcher/app_lifecycle_state.dart + ../../../flutter/LICENSE
6005+
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart + ../../../flutter/LICENSE
60056006
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/platform_views.dart + ../../../flutter/LICENSE
60066007
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart + ../../../flutter/LICENSE
60076008
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/message_handler.dart + ../../../flutter/LICENSE
@@ -8842,6 +8843,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/onscreen_logging.dart
88428843
FILE: ../../../flutter/lib/web_ui/lib/src/engine/picture.dart
88438844
FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart
88448845
FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_dispatcher/app_lifecycle_state.dart
8846+
FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart
88458847
FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views.dart
88468848
FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart
88478849
FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/message_handler.dart

lib/web_ui/lib/src/engine.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ export 'engine/onscreen_logging.dart';
124124
export 'engine/picture.dart';
125125
export 'engine/platform_dispatcher.dart';
126126
export 'engine/platform_dispatcher/app_lifecycle_state.dart';
127+
export 'engine/platform_dispatcher/view_focus_binding.dart';
127128
export 'engine/platform_views.dart';
128129
export 'engine/platform_views/content_manager.dart';
129130
export 'engine/platform_views/message_handler.dart';

lib/web_ui/lib/src/engine/dom.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,10 @@ extension DomElementExtension on DomElement {
608608
external DomElement? _querySelector(JSString selectors);
609609
DomElement? querySelector(String selectors) => _querySelector(selectors.toJS);
610610

611+
@JS('closest')
612+
external DomElement? _closest(JSString selectors);
613+
DomElement? closest(String selectors) => _closest(selectors.toJS);
614+
611615
@JS('matches')
612616
external JSBoolean _matches(JSString selectors);
613617
bool matches(String selectors) => _matches(selectors.toJS).toDart;

lib/web_ui/lib/src/engine/platform_dispatcher.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
7878
_addLocaleChangedListener();
7979
registerHotRestartListener(dispose);
8080
AppLifecycleState.instance.addListener(_setAppLifecycleState);
81+
ViewFocusBinding.instance.addListener(invokeOnViewFocusChange);
8182
_onViewDisposedListener = viewManager.onViewDisposed.listen((_) {
8283
// Send a metrics changed event to the framework when a view is disposed.
8384
// View creation/resize is handled by the `_didResize` handler in the
@@ -114,6 +115,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
114115
_removeLocaleChangedListener();
115116
HighContrastSupport.instance.removeListener(_updateHighContrast);
116117
AppLifecycleState.instance.removeListener(_setAppLifecycleState);
118+
ViewFocusBinding.instance.removeListener(invokeOnViewFocusChange);
117119
_onViewDisposedListener.cancel();
118120
viewManager.dispose();
119121
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:ui/src/engine.dart';
6+
import 'package:ui/ui.dart' as ui;
7+
8+
/// Tracks the [FlutterView]s focus changes.
9+
final class ViewFocusBinding {
10+
/// Creates a [ViewFocusBinding] instance.
11+
ViewFocusBinding._();
12+
13+
/// The [ViewFocusBinding] singleton.
14+
static final ViewFocusBinding instance = ViewFocusBinding._();
15+
16+
final List<ui.ViewFocusChangeCallback> _listeners = <ui.ViewFocusChangeCallback>[];
17+
18+
/// Subscribes the [listener] to [ui.ViewFocusEvent] events.
19+
void addListener(ui.ViewFocusChangeCallback listener) {
20+
if (_listeners.isEmpty) {
21+
domDocument.body?.addEventListener(_focusin, _focusChangeHandler, true);
22+
domDocument.body?.addEventListener(_focusout, _focusChangeHandler, true);
23+
}
24+
_listeners.add(listener);
25+
}
26+
27+
/// Removes the [listener] from the [ui.ViewFocusEvent] events subscription.
28+
void removeListener(ui.ViewFocusChangeCallback listener) {
29+
_listeners.remove(listener);
30+
if (_listeners.isEmpty) {
31+
domDocument.body?.removeEventListener(_focusin, _focusChangeHandler, true);
32+
domDocument.body?.removeEventListener(_focusout, _focusChangeHandler, true);
33+
}
34+
}
35+
36+
void _notify(ui.ViewFocusEvent event) {
37+
for (final ui.ViewFocusChangeCallback listener in _listeners) {
38+
listener(event);
39+
}
40+
}
41+
42+
int? _lastViewId;
43+
late final DomEventListener _focusChangeHandler = createDomEventListener((DomEvent event) {
44+
final int? viewId = _viewId(domDocument.activeElement);
45+
if (viewId == _lastViewId) {
46+
return;
47+
}
48+
49+
final ui.ViewFocusEvent event;
50+
if (viewId == null) {
51+
event = ui.ViewFocusEvent(
52+
viewId: _lastViewId!,
53+
state: ui.ViewFocusState.unfocused,
54+
direction: ui.ViewFocusDirection.undefined,
55+
);
56+
} else {
57+
event = ui.ViewFocusEvent(
58+
viewId: viewId,
59+
state: ui.ViewFocusState.focused,
60+
direction: ui.ViewFocusDirection.forward,
61+
);
62+
}
63+
_lastViewId = viewId;
64+
_notify(event);
65+
});
66+
67+
static int? _viewId(DomElement? element) {
68+
final DomElement? viewElement = element?.closest(
69+
DomManager.flutterViewTagName,
70+
);
71+
final String? viewIdAttribute = viewElement?.getAttribute(
72+
GlobalHtmlAttributes.flutterViewIdAttributeName,
73+
);
74+
return viewIdAttribute == null ? null : int.tryParse(viewIdAttribute);
75+
}
76+
77+
static const String _focusin = 'focusin';
78+
static const String _focusout = 'focusout';
79+
}

lib/web_ui/lib/src/engine/view_embedder/global_html_attributes.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import '../dom.dart';
1717
class GlobalHtmlAttributes {
1818
GlobalHtmlAttributes({required this.rootElement, required this.hostElement});
1919

20+
/// The [FlutterView.viewId] attribute name.
21+
static const String flutterViewIdAttributeName = 'flt-view-id';
22+
2023
final DomElement rootElement;
2124
final DomElement hostElement;
2225

@@ -34,7 +37,7 @@ class GlobalHtmlAttributes {
3437
// Example:
3538
//
3639
// document.querySelector('flutter-view[flt-view-id="$viewId"]')
37-
rootElement.setAttribute('flt-view-id', viewId);
40+
rootElement.setAttribute(flutterViewIdAttributeName, viewId);
3841

3942
// How was the current renderer selected?
4043
final String rendererSelection = autoDetectRenderer
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:test/bootstrap/browser.dart';
6+
import 'package:test/test.dart';
7+
import 'package:ui/src/engine.dart';
8+
import 'package:ui/ui.dart' as ui;
9+
10+
void main() {
11+
internalBootstrapBrowserTest(() => testMain);
12+
}
13+
14+
void testMain() {
15+
group(ViewFocusBinding, () {
16+
late EnginePlatformDispatcher platformDispatcher;
17+
18+
setUp(() {
19+
platformDispatcher = EnginePlatformDispatcher.instance;
20+
domDocument.activeElement?.blur();
21+
});
22+
23+
test('fires a focus event - a view was focused', () async {
24+
final List<ui.ViewFocusEvent> viewFocusEvents = <ui.ViewFocusEvent>[];
25+
final DomElement div = createDomElement('div');
26+
final EngineFlutterView view = EngineFlutterView(platformDispatcher, div);
27+
final DomElement focusableViewElement = div
28+
.querySelector(DomManager.flutterViewTagName)!
29+
..setAttribute('tabindex', 0);
30+
31+
platformDispatcher.onViewFocusChange = viewFocusEvents.add;
32+
domDocument.body!.append(div);
33+
focusableViewElement.focus();
34+
35+
expect(viewFocusEvents, hasLength(1));
36+
37+
expect(viewFocusEvents[0].viewId, view.viewId);
38+
expect(viewFocusEvents[0].state, ui.ViewFocusState.focused);
39+
expect(viewFocusEvents[0].direction, ui.ViewFocusDirection.forward);
40+
});
41+
42+
test('fires a focus event - a view was unfocused', () async {
43+
final List<ui.ViewFocusEvent> viewFocusEvents = <ui.ViewFocusEvent>[];
44+
final DomElement div = createDomElement('div');
45+
final EngineFlutterView view = EngineFlutterView(platformDispatcher, div);
46+
final DomElement focusableViewElement = div
47+
.querySelector(DomManager.flutterViewTagName)!
48+
..setAttribute('tabindex', 0);
49+
50+
platformDispatcher.onViewFocusChange = viewFocusEvents.add;
51+
domDocument.body!.append(div);
52+
focusableViewElement.focus();
53+
focusableViewElement.blur();
54+
55+
expect(viewFocusEvents, hasLength(2));
56+
57+
expect(viewFocusEvents[0].viewId, view.viewId);
58+
expect(viewFocusEvents[0].state, ui.ViewFocusState.focused);
59+
expect(viewFocusEvents[0].direction, ui.ViewFocusDirection.forward);
60+
61+
expect(viewFocusEvents[1].viewId, view.viewId);
62+
expect(viewFocusEvents[1].state, ui.ViewFocusState.unfocused);
63+
expect(viewFocusEvents[1].direction, ui.ViewFocusDirection.undefined);
64+
});
65+
66+
test('fires a focus event - focus transitions between views', () async {
67+
final List<ui.ViewFocusEvent> viewFocusEvents = <ui.ViewFocusEvent>[];
68+
final DomElement div1 = createDomElement('div');
69+
final DomElement div2 = createDomElement('div');
70+
final EngineFlutterView view1 =
71+
EngineFlutterView(platformDispatcher, div1);
72+
final EngineFlutterView view2 =
73+
EngineFlutterView(platformDispatcher, div2);
74+
final DomElement focusableViewElement1 = div1
75+
.querySelector(DomManager.flutterViewTagName)!
76+
..setAttribute('tabindex', 0);
77+
final DomElement focusableViewElement2 = div2
78+
.querySelector(DomManager.flutterViewTagName)!
79+
..setAttribute('tabindex', 0);
80+
81+
domDocument.body!.append(div1);
82+
domDocument.body!.append(div2);
83+
84+
platformDispatcher.onViewFocusChange = viewFocusEvents.add;
85+
86+
focusableViewElement1.focus();
87+
focusableViewElement2.focus();
88+
89+
expect(viewFocusEvents, hasLength(3));
90+
91+
expect(viewFocusEvents[0].viewId, view1.viewId);
92+
expect(viewFocusEvents[0].state, ui.ViewFocusState.focused);
93+
expect(viewFocusEvents[0].direction, ui.ViewFocusDirection.forward);
94+
95+
expect(viewFocusEvents[1].viewId, view1.viewId);
96+
expect(viewFocusEvents[1].state, ui.ViewFocusState.unfocused);
97+
expect(viewFocusEvents[1].direction, ui.ViewFocusDirection.undefined);
98+
99+
expect(viewFocusEvents[2].viewId, view2.viewId);
100+
expect(viewFocusEvents[2].state, ui.ViewFocusState.focused);
101+
expect(viewFocusEvents[2].direction, ui.ViewFocusDirection.forward);
102+
});
103+
104+
test('fires a focus event - focus transitions on and off views', () async {
105+
final List<ui.ViewFocusEvent> viewFocusEvents = <ui.ViewFocusEvent>[];
106+
final DomElement div1 = createDomElement('div');
107+
final DomElement div2 = createDomElement('div');
108+
final EngineFlutterView view1 =
109+
EngineFlutterView(platformDispatcher, div1);
110+
final EngineFlutterView view2 =
111+
EngineFlutterView(platformDispatcher, div2);
112+
final DomElement focusableViewElement1 = div1
113+
.querySelector(DomManager.flutterViewTagName)!
114+
..setAttribute('tabindex', 0);
115+
final DomElement focusableViewElement2 = div2
116+
.querySelector(DomManager.flutterViewTagName)!
117+
..setAttribute('tabindex', 0);
118+
119+
domDocument.body!.append(div1);
120+
domDocument.body!.append(div2);
121+
122+
platformDispatcher.onViewFocusChange = viewFocusEvents.add;
123+
124+
focusableViewElement1.focus();
125+
focusableViewElement2.focus();
126+
focusableViewElement2.blur();
127+
128+
expect(viewFocusEvents, hasLength(4));
129+
130+
expect(viewFocusEvents[0].viewId, view1.viewId);
131+
expect(viewFocusEvents[0].state, ui.ViewFocusState.focused);
132+
expect(viewFocusEvents[0].direction, ui.ViewFocusDirection.forward);
133+
134+
expect(viewFocusEvents[1].viewId, view1.viewId);
135+
expect(viewFocusEvents[1].state, ui.ViewFocusState.unfocused);
136+
expect(viewFocusEvents[1].direction, ui.ViewFocusDirection.undefined);
137+
138+
expect(viewFocusEvents[2].viewId, view2.viewId);
139+
expect(viewFocusEvents[2].state, ui.ViewFocusState.focused);
140+
expect(viewFocusEvents[2].direction, ui.ViewFocusDirection.forward);
141+
142+
expect(viewFocusEvents[3].viewId, view2.viewId);
143+
expect(viewFocusEvents[3].state, ui.ViewFocusState.unfocused);
144+
expect(viewFocusEvents[3].direction, ui.ViewFocusDirection.undefined);
145+
});
146+
});
147+
}

0 commit comments

Comments
 (0)