Flutter views can gain focus (flutter/engine#54985)
I am unsure why the `tabindex` was removed when semantics were enabled. It seems this change was made without a clear explanation (by me). This PR shouldn't cause any issues as Flutter Views already have a tabindex, we're not adding a new one. This change is necessary because the semantics text strategy refocuses the view on deactivation, requiring the Flutter view to be focusable. ThIs PR is a requirement to enable https://github.com/flutter/engine/pull/54966. https://github.com/flutter/flutter/issues/153022 [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
This commit is contained in:
parent
f20a08ea1f
commit
04f26f4377
@ -27,18 +27,20 @@ final class ViewFocusBinding {
|
|||||||
StreamSubscription<int>? _onViewCreatedListener;
|
StreamSubscription<int>? _onViewCreatedListener;
|
||||||
|
|
||||||
void init() {
|
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(_keyDown, _handleKeyDown);
|
||||||
domDocument.body?.addEventListener(_keyUp, _handleKeyUp);
|
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);
|
_onViewCreatedListener = _viewManager.onViewCreated.listen(_handleViewCreated);
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
domDocument.body?.removeEventListener(_keyDown, _handleKeyDown);
|
domDocument.body?.removeEventListener(_keyDown, _handleKeyDown);
|
||||||
domDocument.body?.removeEventListener(_keyUp, _handleKeyUp);
|
domDocument.body?.removeEventListener(_keyUp, _handleKeyUp);
|
||||||
domDocument.body?.removeEventListener(_focusin, _handleFocusin);
|
|
||||||
domDocument.body?.removeEventListener(_focusout, _handleFocusout);
|
|
||||||
_onViewCreatedListener?.cancel();
|
_onViewCreatedListener?.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,12 +50,13 @@ final class ViewFocusBinding {
|
|||||||
}
|
}
|
||||||
final DomElement? viewElement = _viewManager[viewId]?.dom.rootElement;
|
final DomElement? viewElement = _viewManager[viewId]?.dom.rootElement;
|
||||||
|
|
||||||
if (state == ui.ViewFocusState.focused) {
|
switch (state) {
|
||||||
|
case ui.ViewFocusState.focused:
|
||||||
// Only move the focus to the flutter view if nothing inside it is focused already.
|
// Only move the focus to the flutter view if nothing inside it is focused already.
|
||||||
if (viewId != _viewId(domDocument.activeElement)) {
|
if (viewId != _viewId(domDocument.activeElement)) {
|
||||||
viewElement?.focusWithoutScroll();
|
viewElement?.focusWithoutScroll();
|
||||||
}
|
}
|
||||||
} else {
|
case ui.ViewFocusState.unfocused:
|
||||||
viewElement?.blur();
|
viewElement?.blur();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -115,8 +118,8 @@ final class ViewFocusBinding {
|
|||||||
direction: _viewFocusDirection,
|
direction: _viewFocusDirection,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_maybeMarkViewAsFocusable(_lastViewId, reachableByKeyboard: true);
|
_updateViewKeyboardReachability(_lastViewId, reachable: true);
|
||||||
_maybeMarkViewAsFocusable(viewId, reachableByKeyboard: false);
|
_updateViewKeyboardReachability(viewId, reachable: false);
|
||||||
_lastViewId = viewId;
|
_lastViewId = viewId;
|
||||||
_onViewFocusChange(event);
|
_onViewFocusChange(event);
|
||||||
}
|
}
|
||||||
@ -127,29 +130,32 @@ final class ViewFocusBinding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleViewCreated(int viewId) {
|
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, {
|
int? viewId, {
|
||||||
required bool reachableByKeyboard,
|
required bool reachable,
|
||||||
}) {
|
}) {
|
||||||
if (viewId == null) {
|
if (viewId == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final DomElement? rootElement = _viewManager[viewId]?.dom.rootElement;
|
final DomElement? rootElement = _viewManager[viewId]?.dom.rootElement;
|
||||||
if (EngineSemantics.instance.semanticsEnabled) {
|
// A tabindex with value zero means the DOM element can be reached using the
|
||||||
rootElement?.removeAttribute('tabindex');
|
// keyboard (tab, shift + tab). When its value is -1 it is still focusable
|
||||||
} else {
|
// but can't be focused as the result of keyboard events. This is specially
|
||||||
// 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
|
// 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
|
// the flutter view and having it with a zero tabindex messes the focus
|
||||||
// traversal order when pressing tab or shift tab.
|
// traversal order when pressing tab or shift tab.
|
||||||
rootElement?.setAttribute('tabindex', reachableByKeyboard ? 0 : -1);
|
rootElement?.setAttribute('tabindex', reachable ? 0 : -1);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static const String _focusin = 'focusin';
|
static const String _focusin = 'focusin';
|
||||||
|
@ -68,28 +68,6 @@ void testMain() {
|
|||||||
expect(view2.dom.rootElement.getAttribute('tabindex'), '0');
|
expect(view2.dom.rootElement.getAttribute('tabindex'), '0');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('never marks the views as focusable with semantincs enabled', () async {
|
|
||||||
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 {
|
test('fires a focus event - a view was focused', () async {
|
||||||
final EngineFlutterView view = createAndRegisterView(dispatcher);
|
final EngineFlutterView view = createAndRegisterView(dispatcher);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user