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 1 commit
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
Next Next commit
Marks the flutter views as focusable.
When a given flutter view is focused its tabindex will be -1
When a given flutter view is not focused its tabindex will be 0
  • Loading branch information
tugorez committed Mar 7, 2024
commit deaf50b88d417fac28704a9f4cf52ef94595da9a
9 changes: 7 additions & 2 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?.append(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,11 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
}
}

late final ViewFocusBinding _viewFocusBinding = ViewFocusBinding(
viewManager: viewManager,
onViewFocusChange: invokeOnViewFocusChange,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this class exposed a stream of events, then you don't need to pass the onViewFocusChange through its constructor, and the viewManager can default to EnginePlatformDispatcher.instance.viewManager, so ViewFocusBinding can become a singleton again :P

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm I find a bit weird the fact that EnginePlatformDispatcher depends on ViewFocusBinding and viceversa.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

? It already depends on it, I'm just saying that instead of:

final _viewFocusBinding = ViewFocusBinding(
  viewManager: viewManager,
  onViewFocusChange: invokeOnViewFocusChange,
)

you do:

final _viewFocusBinding = ViewFocusBinding(
  viewManager: viewManager,
); // or: ViewFocusBinding.instance;

// elsewhere

_viewFocusBinding.onViewFocusChange.listen((ViewFocusChange event) {
  // Do whatever we need with the event now...
});

That way you don't need to pass a tear-off into the ViewFocusBinding instance?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

? It already depends on it, I'm just saying that instead of:

How so? The view focus binding class doesn't have any dependencies on EnginePlatformDispatcher only on FlutterViewFocusManager

Copy link
Member

@ditman ditman Feb 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The view focus binding class doesn't have any dependencies on EnginePlatformDispatcher only on FlutterViewFocusManager

Could you create the view focus binding class outside of the EnginePlatformDispatcher, without using the EnginePlatformDispatcher (to retrieve the viewManager)? I think that's a fairly strong dependency :P

);

@override
ui.ViewFocusChangeCallback? get onViewFocusChange => _onViewFocusChange;
ui.ViewFocusChangeCallback? _onViewFocusChange;
Expand Down
105 changes: 60 additions & 45 deletions lib/web_ui/lib/src/engine/platform_dispatcher/view_focus_binding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,41 @@
// 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({
required FlutterViewManager viewManager,
required ui.ViewFocusChangeCallback onViewFocusChange,
}):
_viewManager = viewManager,
_onViewFocusChange = 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;
StreamSubscription<int>? _onViewCreatedListener;

/// 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);
}

/// 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(_markViewAsFocusable);
}

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 All @@ -67,36 +61,57 @@ final class ViewFocusBinding {
});

void _handleFocusChange(DomElement? focusedElement) {
final int? lastViewId = _lastViewId;
final int? viewId = _viewId(focusedElement);
if (viewId == _lastViewId) {
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 {
if (viewId != null) {
event = ui.ViewFocusEvent(
viewId: viewId,
state: ui.ViewFocusState.focused,
direction: _viewFocusDirection,
);
} else {
event = ui.ViewFocusEvent(
viewId: lastViewId!,
state: ui.ViewFocusState.unfocused,
direction: ui.ViewFocusDirection.undefined,
);
}
_lastViewId = viewId;
_notify(event);
_markViewAsFocusable(lastViewId);
_markViewAsFocusable(viewId, reachableByKeyboard: false);
_onViewFocusChange(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);
int? _viewId(DomElement? element) {
final DomElement? viewElement = element?.closest(DomManager.flutterViewTagName);
for (final EngineFlutterView view in _viewManager.views) {
if (view.dom.rootElement == viewElement) {
return view.viewId;
}
}
return null;
}

void _markViewAsFocusable(
int? viewId, {
bool reachableByKeyboard = true,
}) {
if (viewId == null) {
return;
}
// 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.
final int tabIndex = reachableByKeyboard ? 0 : -1;
_viewManager[viewId]?.dom.rootElement.setAttribute('tabindex', tabIndex);
}

static const String _focusin = 'focusin';
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