Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -6002,6 +6002,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/onscreen_logging.dart + ../..
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/picture.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/platform_dispatcher/app_lifecycle_state.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/platform_views.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/message_handler.dart + ../../../flutter/LICENSE
Expand Down Expand Up @@ -8842,6 +8843,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/onscreen_logging.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/picture.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_dispatcher/app_lifecycle_state.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/content_manager.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/message_handler.dart
Expand Down
1 change: 1 addition & 0 deletions lib/web_ui/lib/src/engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export 'engine/onscreen_logging.dart';
export 'engine/picture.dart';
export 'engine/platform_dispatcher.dart';
export 'engine/platform_dispatcher/app_lifecycle_state.dart';
export 'engine/platform_dispatcher/view_focus_binding.dart';
export 'engine/platform_views.dart';
export 'engine/platform_views/content_manager.dart';
export 'engine/platform_views/message_handler.dart';
Expand Down
4 changes: 4 additions & 0 deletions lib/web_ui/lib/src/engine/dom.dart
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,10 @@ extension DomElementExtension on DomElement {
external DomElement? _querySelector(JSString selectors);
DomElement? querySelector(String selectors) => _querySelector(selectors.toJS);

@JS('closest')
external DomElement? _closest(JSString selectors);
DomElement? closest(String selectors) => _closest(selectors.toJS);

@JS('matches')
external JSBoolean _matches(JSString selectors);
bool matches(String selectors) => _matches(selectors.toJS).toDart;
Expand Down
2 changes: 2 additions & 0 deletions lib/web_ui/lib/src/engine/platform_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
_addLocaleChangedListener();
registerHotRestartListener(dispose);
AppLifecycleState.instance.addListener(_setAppLifecycleState);
ViewFocusBinding.instance.addListener(invokeOnViewFocusChange);
_onViewDisposedListener = viewManager.onViewDisposed.listen((_) {
// Send a metrics changed event to the framework when a view is disposed.
// View creation/resize is handled by the `_didResize` handler in the
Expand Down Expand Up @@ -114,6 +115,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
_removeLocaleChangedListener();
HighContrastSupport.instance.removeListener(_updateHighContrast);
AppLifecycleState.instance.removeListener(_setAppLifecycleState);
ViewFocusBinding.instance.removeListener(invokeOnViewFocusChange);
_onViewDisposedListener.cancel();
viewManager.dispose();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;

/// Tracks the [FlutterView]s focus changes.
final class ViewFocusBinding {
/// Creates a [ViewFocusBinding] instance.
ViewFocusBinding._();

/// The [ViewFocusBinding] singleton.
static final ViewFocusBinding instance = ViewFocusBinding._();

final List<ui.ViewFocusChangeCallback> _listeners = <ui.ViewFocusChangeCallback>[];

/// Subscribes the [listener] to [ui.ViewFocusEvent] events.
void addListener(ui.ViewFocusChangeCallback listener) {
if (_listeners.isEmpty) {
domDocument.body?.addEventListener(_focusin, _focusChangeHandler, true);
domDocument.body?.addEventListener(_focusout, _focusChangeHandler, true);
}
_listeners.add(listener);
}

/// Removes the [listener] from the [ui.ViewFocusEvent] events subscription.
void removeListener(ui.ViewFocusChangeCallback listener) {
_listeners.remove(listener);
if (_listeners.isEmpty) {
domDocument.body?.removeEventListener(_focusin, _focusChangeHandler, true);
domDocument.body?.removeEventListener(_focusout, _focusChangeHandler, true);
}
}

void _notify(ui.ViewFocusEvent event) {
for (final ui.ViewFocusChangeCallback listener in _listeners) {
listener(event);
}
}

int? _lastViewId;
late final DomEventListener _focusChangeHandler = createDomEventListener((DomEvent event) {
final int? viewId = _viewId(domDocument.activeElement);
if (viewId == _lastViewId) {
return;
}

final ui.ViewFocusEvent event;
if (viewId == null) {
event = ui.ViewFocusEvent(
viewId: _lastViewId!,
state: ui.ViewFocusState.unfocused,
direction: ui.ViewFocusDirection.undefined,
);
} else {
event = ui.ViewFocusEvent(
viewId: viewId,
state: ui.ViewFocusState.focused,
direction: ui.ViewFocusDirection.forward,
);
}
_lastViewId = viewId;
_notify(event);
});

static int? _viewId(DomElement? element) {
final DomElement? viewElement = element?.closest(
DomManager.flutterViewTagName,
);
final String? viewIdAttribute = viewElement?.getAttribute(
GlobalHtmlAttributes.flutterViewIdAttributeName,
);
return viewIdAttribute == null ? null : int.tryParse(viewIdAttribute);
}

static const String _focusin = 'focusin';
static const String _focusout = 'focusout';
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import '../dom.dart';
class GlobalHtmlAttributes {
GlobalHtmlAttributes({required this.rootElement, required this.hostElement});

/// The [FlutterView.viewId] attribute name.
static const String flutterViewIdAttributeName = 'flt-view-id';

final DomElement rootElement;
final DomElement hostElement;

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

// How was the current renderer selected?
final String rendererSelection = autoDetectRenderer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;

void main() {
internalBootstrapBrowserTest(() => testMain);
}

void testMain() {
group(ViewFocusBinding, () {
late EnginePlatformDispatcher platformDispatcher;

setUp(() {
platformDispatcher = EnginePlatformDispatcher.instance;
domDocument.activeElement?.blur();
});

test('fires a focus event - a view was focused', () async {
final List<ui.ViewFocusEvent> viewFocusEvents = <ui.ViewFocusEvent>[];
final DomElement div = createDomElement('div');
final EngineFlutterView view = EngineFlutterView(platformDispatcher, div);
final DomElement focusableViewElement = div
.querySelector(DomManager.flutterViewTagName)!
..setAttribute('tabindex', 0);

platformDispatcher.onViewFocusChange = viewFocusEvents.add;
domDocument.body!.append(div);
focusableViewElement.focus();

expect(viewFocusEvents, hasLength(1));

expect(viewFocusEvents[0].viewId, view.viewId);
expect(viewFocusEvents[0].state, ui.ViewFocusState.focused);
expect(viewFocusEvents[0].direction, ui.ViewFocusDirection.forward);
});

test('fires a focus event - a view was unfocused', () async {
final List<ui.ViewFocusEvent> viewFocusEvents = <ui.ViewFocusEvent>[];
final DomElement div = createDomElement('div');
final EngineFlutterView view = EngineFlutterView(platformDispatcher, div);
final DomElement focusableViewElement = div
.querySelector(DomManager.flutterViewTagName)!
..setAttribute('tabindex', 0);

platformDispatcher.onViewFocusChange = viewFocusEvents.add;
domDocument.body!.append(div);
focusableViewElement.focus();
focusableViewElement.blur();

expect(viewFocusEvents, hasLength(2));

expect(viewFocusEvents[0].viewId, view.viewId);
expect(viewFocusEvents[0].state, ui.ViewFocusState.focused);
expect(viewFocusEvents[0].direction, ui.ViewFocusDirection.forward);

expect(viewFocusEvents[1].viewId, view.viewId);
expect(viewFocusEvents[1].state, ui.ViewFocusState.unfocused);
expect(viewFocusEvents[1].direction, ui.ViewFocusDirection.undefined);
});

test('fires a focus event - focus transitions between views', () async {
final List<ui.ViewFocusEvent> viewFocusEvents = <ui.ViewFocusEvent>[];
final DomElement div1 = createDomElement('div');
final DomElement div2 = createDomElement('div');
final EngineFlutterView view1 =
EngineFlutterView(platformDispatcher, div1);
final EngineFlutterView view2 =
EngineFlutterView(platformDispatcher, div2);
final DomElement focusableViewElement1 = div1
.querySelector(DomManager.flutterViewTagName)!
..setAttribute('tabindex', 0);
final DomElement focusableViewElement2 = div2
.querySelector(DomManager.flutterViewTagName)!
..setAttribute('tabindex', 0);

domDocument.body!.append(div1);
domDocument.body!.append(div2);

platformDispatcher.onViewFocusChange = viewFocusEvents.add;

focusableViewElement1.focus();
focusableViewElement2.focus();

expect(viewFocusEvents, hasLength(3));

expect(viewFocusEvents[0].viewId, view1.viewId);
expect(viewFocusEvents[0].state, ui.ViewFocusState.focused);
expect(viewFocusEvents[0].direction, ui.ViewFocusDirection.forward);

expect(viewFocusEvents[1].viewId, view1.viewId);
expect(viewFocusEvents[1].state, ui.ViewFocusState.unfocused);
expect(viewFocusEvents[1].direction, ui.ViewFocusDirection.undefined);

expect(viewFocusEvents[2].viewId, view2.viewId);
expect(viewFocusEvents[2].state, ui.ViewFocusState.focused);
expect(viewFocusEvents[2].direction, ui.ViewFocusDirection.forward);
});

test('fires a focus event - focus transitions on and off views', () async {
final List<ui.ViewFocusEvent> viewFocusEvents = <ui.ViewFocusEvent>[];
final DomElement div1 = createDomElement('div');
final DomElement div2 = createDomElement('div');
final EngineFlutterView view1 =
EngineFlutterView(platformDispatcher, div1);
final EngineFlutterView view2 =
EngineFlutterView(platformDispatcher, div2);
final DomElement focusableViewElement1 = div1
.querySelector(DomManager.flutterViewTagName)!
..setAttribute('tabindex', 0);
final DomElement focusableViewElement2 = div2
.querySelector(DomManager.flutterViewTagName)!
..setAttribute('tabindex', 0);

domDocument.body!.append(div1);
domDocument.body!.append(div2);

platformDispatcher.onViewFocusChange = viewFocusEvents.add;

focusableViewElement1.focus();
focusableViewElement2.focus();
focusableViewElement2.blur();

expect(viewFocusEvents, hasLength(4));

expect(viewFocusEvents[0].viewId, view1.viewId);
expect(viewFocusEvents[0].state, ui.ViewFocusState.focused);
expect(viewFocusEvents[0].direction, ui.ViewFocusDirection.forward);

expect(viewFocusEvents[1].viewId, view1.viewId);
expect(viewFocusEvents[1].state, ui.ViewFocusState.unfocused);
expect(viewFocusEvents[1].direction, ui.ViewFocusDirection.undefined);

expect(viewFocusEvents[2].viewId, view2.viewId);
expect(viewFocusEvents[2].state, ui.ViewFocusState.focused);
expect(viewFocusEvents[2].direction, ui.ViewFocusDirection.forward);

expect(viewFocusEvents[3].viewId, view2.viewId);
expect(viewFocusEvents[3].state, ui.ViewFocusState.unfocused);
expect(viewFocusEvents[3].direction, ui.ViewFocusDirection.undefined);
});
});
}