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
9 changes: 5 additions & 4 deletions lib/web_ui/lib/src/engine/platform_dispatcher.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
_addLocaleChangedListener();
registerHotRestartListener(dispose);
AppLifecycleState.instance.addListener(_setAppLifecycleState);
ViewFocusBinding.instance.addListener(invokeOnViewFocusChange);
_viewFocusBinding.init();
domDocument.body?.prepend(accessibilityPlaceholder);
_onViewDisposedListener = viewManager.onViewDisposed.listen((_) {
// Send a metrics changed event to the framework when a view is disposed.
Expand Down Expand Up @@ -123,7 +123,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
_removeLocaleChangedListener();
HighContrastSupport.instance.removeListener(_updateHighContrast);
AppLifecycleState.instance.removeListener(_setAppLifecycleState);
ViewFocusBinding.instance.removeListener(invokeOnViewFocusChange);
_viewFocusBinding.dispose();
accessibilityPlaceholder.remove();
_onViewDisposedListener.cancel();
viewManager.dispose();
Expand Down Expand Up @@ -228,6 +228,9 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
}
}

late final ViewFocusBinding _viewFocusBinding =
ViewFocusBinding(viewManager, invokeOnViewFocusChange);

@override
ui.ViewFocusChangeCallback? get onViewFocusChange => _onViewFocusChange;
ui.ViewFocusChangeCallback? _onViewFocusChange;
Expand All @@ -248,7 +251,6 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
);
}


@override
void requestViewFocusChange({
required int viewId,
Expand All @@ -258,7 +260,6 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
// TODO(tugorez): implement this method. At the moment will be a no op call.
}


/// A set of views which have rendered in the current `onBeginFrame` or
/// `onDrawFrame` scope.
Set<ui.FlutterView>? _viewsRenderedInCurrentFrame;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,36 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
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._();
ViewFocusBinding(this._viewManager, this._onViewFocusChange);

/// The [ViewFocusBinding] singleton.
static final ViewFocusBinding instance = ViewFocusBinding._();
final FlutterViewManager _viewManager;
final ui.ViewFocusChangeCallback _onViewFocusChange;

final List<ui.ViewFocusChangeCallback> _listeners = <ui.ViewFocusChangeCallback>[];
int? _lastViewId;
ui.ViewFocusDirection _viewFocusDirection = ui.ViewFocusDirection.forward;

/// Subscribes the [listener] to [ui.ViewFocusEvent] events.
void addListener(ui.ViewFocusChangeCallback listener) {
if (_listeners.isEmpty) {
domDocument.body?.addEventListener(_keyDown, _handleKeyDown);
domDocument.body?.addEventListener(_keyUp, _handleKeyUp);
domDocument.body?.addEventListener(_focusin, _handleFocusin);
domDocument.body?.addEventListener(_focusout, _handleFocusout);
}
_listeners.add(listener);
}
StreamSubscription<int>? _onViewCreatedListener;

/// Removes the [listener] from the [ui.ViewFocusEvent] events subscription.
void removeListener(ui.ViewFocusChangeCallback listener) {
_listeners.remove(listener);
if (_listeners.isEmpty) {
domDocument.body?.removeEventListener(_keyDown, _handleKeyDown);
domDocument.body?.removeEventListener(_keyUp, _handleKeyUp);
domDocument.body?.removeEventListener(_focusin, _handleFocusin);
domDocument.body?.removeEventListener(_focusout, _handleFocusout);
}
void init() {
domDocument.body?.addEventListener(_keyDown, _handleKeyDown);
domDocument.body?.addEventListener(_keyUp, _handleKeyUp);
domDocument.body?.addEventListener(_focusin, _handleFocusin);
domDocument.body?.addEventListener(_focusout, _handleFocusout);
_onViewCreatedListener = _viewManager.onViewCreated.listen(_handleViewCreated);
}

void _notify(ui.ViewFocusEvent event) {
for (final ui.ViewFocusChangeCallback listener in _listeners) {
listener(event);
}
void dispose() {
domDocument.body?.removeEventListener(_keyDown, _handleKeyDown);
domDocument.body?.removeEventListener(_keyUp, _handleKeyUp);
domDocument.body?.removeEventListener(_focusin, _handleFocusin);
domDocument.body?.removeEventListener(_focusout, _handleFocusout);
_onViewCreatedListener?.cancel();
}

late final DomEventListener _handleFocusin = createDomEventListener((DomEvent event) {
Expand Down Expand Up @@ -71,6 +60,7 @@ final class ViewFocusBinding {
if (viewId == _lastViewId) {
return;
}

final ui.ViewFocusEvent event;
if (viewId == null) {
event = ui.ViewFocusEvent(
Expand All @@ -85,18 +75,44 @@ final class ViewFocusBinding {
direction: _viewFocusDirection,
);
}
_maybeMarkViewAsFocusable(_lastViewId, reachableByKeyboard: true);
_maybeMarkViewAsFocusable(viewId, reachableByKeyboard: false);
_lastViewId = viewId;
_notify(event);
_onViewFocusChange(event);
}

int? _viewId(DomElement? element) {
final DomElement? rootElement = element?.closest(DomManager.flutterViewTagName);
if (rootElement == null) {
return null;
}
return _viewManager.viewIdForRootElement(rootElement);
}

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);
void _handleViewCreated(int viewId) {
_maybeMarkViewAsFocusable(viewId, reachableByKeyboard: true);
}

void _maybeMarkViewAsFocusable(
int? viewId, {
required bool reachableByKeyboard,
}) {
if (viewId == null) {
return;
}

final DomElement? rootElement = _viewManager[viewId]?.dom.rootElement;
if (EngineSemantics.instance.semanticsEnabled) {
rootElement?.removeAttribute('tabindex');
} else {
// A tabindex with value zero means the DOM element can be reached by using
// the keyboard (tab, shift + tab). When its value is -1 it is still focusable
// but can't be focused by the result of keyboard events This is specially
// important when the semantics tree is enabled as it puts DOM nodes inside
// the flutter view and having it with a zero tabindex messes the focus
// traversal order when pressing tab or shift tab.
rootElement?.setAttribute('tabindex', reachableByKeyboard ? 0 : -1);
}
}

static const String _focusin = 'focusin';
Expand Down
10 changes: 10 additions & 0 deletions lib/web_ui/lib/src/engine/view_embedder/flutter_view_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ class FlutterViewManager {
return _jsViewOptions[viewId];
}

/// Returns the [viewId] if [rootElement] corresponds to any of the [views].
int? viewIdForRootElement(DomElement rootElement) {
for(final EngineFlutterView view in views) {
if (view.dom.rootElement == rootElement) {
return view.viewId;
}
}
return null;
}

void dispose() {
// We need to call `toList()` in order to avoid concurrent modification
// inside the loop.
Expand Down
5 changes: 5 additions & 0 deletions lib/web_ui/lib/src/engine/view_embedder/style_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ void applyGlobalCssRulesToSheet(
// Hide placeholder text
'$cssSelectorPrefix .flt-text-editing::placeholder {'
' opacity: 0;'
'}'

// Hide outline when the flutter-view root element is focused.
'$cssSelectorPrefix:focus {'
' outline: none;'
'}',
);

Expand Down
Loading