Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flutter views can gain focus #54985

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,20 @@ final class ViewFocusBinding {
StreamSubscription<int>? _onViewCreatedListener;

void init() {
// We need a global listener here to know if the user was pressing "shift"
// when the Flutter view receives focus, to move the Flutter focus to the
// *last* focusable element.
domDocument.body?.addEventListener(_keyDown, _handleKeyDown);
domDocument.body?.addEventListener(_keyUp, _handleKeyUp);
domDocument.body?.addEventListener(_focusin, _handleFocusin);
domDocument.body?.addEventListener(_focusout, _handleFocusout);

// If so, update `_handleViewCreated` and add a `_handleViewDisposed` to attach
// and remove the focus/blur listener.
_onViewCreatedListener = _viewManager.onViewCreated.listen(_handleViewCreated);
}

void dispose() {
domDocument.body?.removeEventListener(_keyDown, _handleKeyDown);
domDocument.body?.removeEventListener(_keyUp, _handleKeyUp);
domDocument.body?.removeEventListener(_focusin, _handleFocusin);
domDocument.body?.removeEventListener(_focusout, _handleFocusout);
_onViewCreatedListener?.cancel();
}

Expand All @@ -48,13 +50,14 @@ final class ViewFocusBinding {
}
final DomElement? viewElement = _viewManager[viewId]?.dom.rootElement;

if (state == ui.ViewFocusState.focused) {
// Only move the focus to the flutter view if nothing inside it is focused already.
if (viewId != _viewId(domDocument.activeElement)) {
viewElement?.focusWithoutScroll();
}
} else {
viewElement?.blur();
switch (state) {
case ui.ViewFocusState.focused:
// Only move the focus to the flutter view if nothing inside it is focused already.
if (viewId != _viewId(domDocument.activeElement)) {
viewElement?.focusWithoutScroll();
}
case ui.ViewFocusState.unfocused:
viewElement?.blur();
}
}

Expand Down Expand Up @@ -115,8 +118,8 @@ final class ViewFocusBinding {
direction: _viewFocusDirection,
);
}
_maybeMarkViewAsFocusable(_lastViewId, reachableByKeyboard: true);
_maybeMarkViewAsFocusable(viewId, reachableByKeyboard: false);
_updateViewKeyboardReachability(_lastViewId, reachable: true);
_updateViewKeyboardReachability(viewId, reachable: false);
_lastViewId = viewId;
_onViewFocusChange(event);
}
Expand All @@ -127,29 +130,32 @@ final class ViewFocusBinding {
}

void _handleViewCreated(int viewId) {
_maybeMarkViewAsFocusable(viewId, reachableByKeyboard: true);
final DomElement? rootElement = _viewManager[viewId]?.dom.rootElement;

rootElement?.addEventListener(_focusin, _handleFocusin);
rootElement?.addEventListener(_focusout, _handleFocusout);

_updateViewKeyboardReachability(viewId, reachable: true);
}

void _maybeMarkViewAsFocusable(
// Controls whether the Flutter view identified by [viewId] is reachable by
// keyboard.
void _updateViewKeyboardReachability(
int? viewId, {
required bool reachableByKeyboard,
required bool reachable,
}) {
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);
}
// A tabindex with value zero means the DOM element can be reached by using
ditman marked this conversation as resolved.
Show resolved Hide resolved
// 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
ditman marked this conversation as resolved.
Show resolved Hide resolved
// 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', reachable ? 0 : -1);
}

static const String _focusin = 'focusin';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,28 +68,6 @@ void testMain() {
expect(view2.dom.rootElement.getAttribute('tabindex'), '0');
});

test('never marks the views as focusable with semantincs enabled', () async {
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we also add some tests? :)

Copy link
Member

Choose a reason for hiding this comment

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

Well, there's tests already (this file is just showing the removed test, but there's others to verify the rest of the functionality!)

EngineSemantics.instance.semanticsEnabled = true;

final EngineFlutterView view1 = createAndRegisterView(dispatcher);
final EngineFlutterView view2 = createAndRegisterView(dispatcher);

expect(view1.dom.rootElement.getAttribute('tabindex'), isNull);
expect(view2.dom.rootElement.getAttribute('tabindex'), isNull);

view1.dom.rootElement.focusWithoutScroll();
expect(view1.dom.rootElement.getAttribute('tabindex'), isNull);
expect(view2.dom.rootElement.getAttribute('tabindex'), isNull);

view2.dom.rootElement.focusWithoutScroll();
expect(view1.dom.rootElement.getAttribute('tabindex'), isNull);
expect(view2.dom.rootElement.getAttribute('tabindex'), isNull);

view2.dom.rootElement.blur();
expect(view1.dom.rootElement.getAttribute('tabindex'), isNull);
expect(view2.dom.rootElement.getAttribute('tabindex'), isNull);
});

test('fires a focus event - a view was focused', () async {
final EngineFlutterView view = createAndRegisterView(dispatcher);

Expand Down