-
Notifications
You must be signed in to change notification settings - Fork 6k
Conversation
// focus on something inside it. There could be two possibilities: | ||
// | ||
// 1. The framework explicitly marked a node inside the dialog as focused | ||
// via the `isFocusabe` and `isFocused` flags. In this case, the node |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: typo isFocusabe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! Done.
It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact Hixie or stuartmorgan on the #hackers channel in Chat (don't just cc them here, they won't see it! Use Discord!). If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix? Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
966382a
to
e6272f2
Compare
test-exempt: has tests |
spent 10 minutes looking, and I have no idea. Seems like |
if (semanticsObject.isFocusable) { | ||
final Focusable? focusable = this.focusable; | ||
if (focusable != null) { | ||
return focusable.focusAsRouteDefault(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this returns false
, should it not continue further below to make more attempts at focusing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be confusing if a node is explicitly focusable but when its focus management logic rejects focus we look for other ways for it to take it. It's probably better to look for another node that's more willing to take focus. But maybe I'm missing something. Do you have an example where we'd want to insist that a node take focus even if its focus management doesn't want it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have any particular example here. Just wondering in the abstract whether the logic applied to non-focusable items below should be applied to focusable items that don't take focus because in both cases, the object is not able to take direct focus. It's all up to you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Let's take a disabled button, for example. The button does not take input focus because it's disabled. However, a screen reader can still find it via traversal, so it could theoretically be as much of an auto-focus candidate as static text is. Is this what you mean?
I'll need to test what effect calling .focus()
on disabled controls has. I imagine it may be a noop, or it may depend on whether an element carries intrinsic semantics/focusability (e.g. <button>
, <input>
), or whether it's just a "div" with ARIA attributes on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.focus()
is a noop on disabled native controls unless they have any valid tabindex on it (including -1), thereby allowing programmatic focus, as you're already aware below.
The disabled button is an interesting example. I think with that you've sold me.
But to confirm: are focusable entities that don't get primary focus considered focusable? If so, then I wonder whether one should recurse.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. What I'll do is keep it as is for now, and wait for real world feedback to see how this logic could be improved. Even with the corner cases, I think the new approach already improves the situation considerably.
@@ -2270,6 +2352,23 @@ class EngineSemanticsOwner { | |||
return true; | |||
}()); | |||
} | |||
|
|||
/// True, if any semantics node requested focus explicitly. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unclear on what the duration of the "requested focus" part is. Is this true if any semantics node has ever requested focus? Or in the last animation frame (and this it's intended to be turned to false at the end of the animation frame)? Or is this referring to autofocus
(I imagine not since the DFS traversal above sets it to false)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I expanded the docs. The relevant time frame for this to be true
is during a semantics update (in the middle of updateSemantics
invocation). Otherwise, this field is almost always false
. Simply put, if any node in the semantics update requests focus explicitly, then there's no need to apply default focus. So the flag is flipped to true
to shut off the default. At the end of the update the flag is reset back to false
allowing the default logic to be used on future updates.
@@ -2873,6 +2894,210 @@ void _testDialog() { | |||
|
|||
semantics().semanticsEnabled = false; | |||
}); | |||
|
|||
// Test the simple scenario of a dialog coming up and containing focusable | |||
// descentants that are not initially focused. The expectation is that the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: "descentants"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
}); | ||
|
||
// Test the scenario of a dialog coming up and containing non-focusable | ||
// descentants that can have a11y focus. The expectation is that the first |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: "descentants"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
|
||
// Test the scenario of a dialog coming up and containing non-focusable | ||
// descentants that can have a11y focus. The expectation is that the first | ||
// descendant will be auto-focused. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"...even if it's not focusable."
(surprising to me, as I expected it to go to the first focusable descendant!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are there any guidelines around this? My logic to focus on the first accessibility-focusable element is as follows:
For a screen reader user, nearly everything is focusable. The screen reader can focus on things that keyboard and mouse cannot, such as text (headings, paragraph), images, and other read-only elements. What the framework considers "focusable" is widgets that have input focus. You can interact with those widgets using keyboard and/or mouse. So let's say we auto-focused on the first widget from that subset. Consider a dialog with some headings, text, and buttons. Your screen reader starts in some random widget in the middle. To figure out what's on the dialog, you have to go back and forth to find the boundaries of the dialog and read available information. But if we focus on the first thing that can have accessibility focus, then all you need is to traverse the dialog forward, read through the text, then find the button you are interested in pressing. Defaulting to the first button may not even be the correct button to focus on by default. If it is desired that focus goes to an input-focusable widget, the app can still focus on it explicitly. The logic in this PR only kicks in if the framework did not communicate explicit focus to any descendant.
But it's totally possible that I'm not thinking about this correctly.
@@ -236,4 +236,7 @@ class Scrollable extends PrimaryRoleManager { | |||
semanticsObject.owner.removeGestureModeListener(_gestureModeListener); | |||
_gestureModeListener = null; | |||
} | |||
|
|||
@override | |||
bool focusAsRouteDefault() => false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if it's focusable, though? E.g. we've had to make some scrollable child containers focusable so that a keyboard-only user can scroll it with left/right arrow keys.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
// Case 2: nothing requested explicit focus. Focus on the first descendant. | ||
_setDefaultFocus(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may need to add a 3rd case:
- Nothing was focused before the semantics update, and nothing requested focus during this update, so leave everything as is.
This 3rd case will be common when Flutter is embedded inside another app, where the screen reader might be focusing somewhere else outside of the Flutter app. We don't want to steal that focus, do we?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If something in the embedded flutter view triggered a route change inside, we should bring the focus here I think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In other platform we send screen change accessibilityEvent and let OS decide whether to move the accessibility focus. so we are in an uncharted territory here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When we have focusability at the FlutterView
level, one possible middle ground could be:
/// Case 3: the route popped up inside a FlutterView that's not currently
focused. Do not auto-focus inside the route because that could
"steal" focus from an element outside the current FlutterView.
In the meantime, I agree with @chunhtai that if a route change happened, it is likely because the user took some action to push that route, and the most common outcome should be that a widget inside the new route takes focus. We can revisit when we have FlutterView
focusability (currently worked on by @tugorez).
// focus on something inside it. There could be two possibilities: | ||
// | ||
// 1. The framework explicitly marked a node inside the dialog as focused | ||
// via the `isFocusable` and `isFocused` flags. In this case, the node |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isFocusable/isFocused are refering to keyboard focus. For a sighted user it is ok if some element in the middle of the page request keyboard focus and the system bring focus to there. For a visually impaired user, this will make them confused.
Should we always do (2) if screen reader is turned on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It really depends on the type of dialog. For example for a role="alertdialog"
, it's very common to move focus into the Ok button since when the focus first moves into the alert dialog, the screen reader vocalizes the entire contents of the dialog. Of course, the choice to focus on the "Ok" dialog would ideally by done via an explicit autofocus
rather than implicit focus. So maybe you may decide that that consideration might be less relevant for fallback behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the web we do not know if the screen reader is turned on, so we have to pick one option. I'm not sure if focusing on the first keyboard-focusable will produce good result, as there's no guarantee that the widget will be correct one, and landing in the middle of the dialog, the user will have to traverse backwards then forwards to find the widget they are interested in.
I think keeping the logic simple (i.e. what it is in the PR right now) while leaving the option to use autofocus
on the framework should be flexible enough.
@@ -16,6 +16,39 @@ class Dialog extends PrimaryRoleManager { | |||
// names its own route an `aria-label` is used instead of `aria-describedby`. | |||
addFocusManagement(); | |||
addLiveRegion(); | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should this class be named Route instead? It took me a while to understand a dialog is corresponding to scopesRoute = true
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PrimaryRoleManager
is called Dialog
because it sets the ARIA role="dialog"
, which is the unique role identifier. I can rename it, but I'd prefer doing it in a separate PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If every route is a role="dialog", wouldn't the screen reader announce every MaterialPageRoute a dialog when focused? Moving this to a separate pr sgtm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently "dialog" is the only available ARIA role. A route expressed as a transition to a different screen is expressed on the web by changing the URL (which we already do via UrlStrategy
) and updating the title of the page (which is expressed via the Title widget). So I think for routes-that-are-not-dialogs we could improve the situation if the framework communicated the difference somehow. Happy to collaborate on improving this.
// 2. No node inside the route takes focus explicitly. In this case, the | ||
// expectation is to look through all nodes in traversal order and focus | ||
// on the first one. | ||
semanticsObject.owner.addOneTimePostUpdateCallback(() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how does the debouncing work if there are multiple dialogs created in a update
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have not considered it. I hope apps do not create multiple dialogs in a single update. It seems like a messy UX implementation. I think the framework removes semantics from all background routes anyway, so the engine would only observe the top-most one, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if there are nested navigators or a pop up route like dropdownButtons that somehow open simultaneous , the former is probably more common than the latter.
Another case is that I do know customer money use scopeRoute + namesRoute combo when they are animating page change effect within one page, but they are targeting mobile.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Looking at the code, what will happen right now is that the last dialog to show up will "win" the focus because addOneTimePostUpdateCallback
callbacks are processed in FIFO order. So whatever order the framework communicates dialogs, the last one to be initialized will win. So I think it should work, but there could be corner-cases. I'm willing to look into this as a follow-up. The current implementation covers all use-cases that we have in the a11y_assessments
app.
// Case 2: nothing requested explicit focus. Focus on the first descendant. | ||
_setDefaultFocus(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If something in the embedded flutter view triggered a route change inside, we should bring the focus here I think.
// Case 2: nothing requested explicit focus. Focus on the first descendant. | ||
_setDefaultFocus(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In other platform we send screen change accessibilityEvent and let OS decide whether to move the accessibility focus. so we are in an uncharted territory here.
e6272f2
to
f011591
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, the strategy SGTM. mdebbar@'s comment seems like a particularly interesting edge case to think through carefully, however.
if (semanticsObject.isFocusable) { | ||
final Focusable? focusable = this.focusable; | ||
if (focusable != null) { | ||
return focusable.focusAsRouteDefault(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have any particular example here. Just wondering in the abstract whether the logic applied to non-focusable items below should be applied to focusable items that don't take focus because in both cases, the object is not able to take direct focus. It's all up to you.
// focus on something inside it. There could be two possibilities: | ||
// | ||
// 1. The framework explicitly marked a node inside the dialog as focused | ||
// via the `isFocusable` and `isFocused` flags. In this case, the node |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It really depends on the type of dialog. For example for a role="alertdialog"
, it's very common to move focus into the Ok button since when the focus first moves into the alert dialog, the screen reader vocalizes the entire contents of the dialog. Of course, the choice to focus on the "Ok" dialog would ideally by done via an explicit autofocus
rather than implicit focus. So maybe you may decide that that consideration might be less relevant for fallback behavior.
f011591
to
7b6952c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for all the changes! Things LGTM. But I imagine you'd want someone else's LG on this PR.
if (semanticsObject.isFocusable) { | ||
final Focusable? focusable = this.focusable; | ||
if (focusable != null) { | ||
return focusable.focusAsRouteDefault(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.focus()
is a noop on disabled native controls unless they have any valid tabindex on it (including -1), thereby allowing programmatic focus, as you're already aware below.
The disabled button is an interesting example. I think with that you've sold me.
But to confirm: are focusable entities that don't get primary focus considered focusable? If so, then I wonder whether one should recurse.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
/// programmatically, simulating the screen reader choosing a default element | ||
/// to focus on. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we inform the framework about the element we chose here? That way, when the user hits tab, the framework would continue the traversal correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do inform the framework via a didGainAccessibilityFocus
event, which happen by relevant nodes receiving the DOM focus
event.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The framework now will send focus event when route changes
See https://github.com/flutter/flutter/blob/9003f138033726a275b524539670ef3ae74e8ddf/packages/flutter/lib/src/widgets/navigator.dart#L3048
It currently only send the event for mobile, if we include web here, I think this may just work if the web engine focus the div based on this even
@@ -16,6 +16,39 @@ class Dialog extends PrimaryRoleManager { | |||
// names its own route an `aria-label` is used instead of `aria-describedby`. | |||
addFocusManagement(); | |||
addLiveRegion(); | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If every route is a role="dialog", wouldn't the screen reader announce every MaterialPageRoute a dialog when focused? Moving this to a separate pr sgtm
// 2. No node inside the route takes focus explicitly. In this case, the | ||
// expectation is to look through all nodes in traversal order and focus | ||
// on the first one. | ||
semanticsObject.owner.addOneTimePostUpdateCallback(() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if there are nested navigators or a pop up route like dropdownButtons that somehow open simultaneous , the former is probably more common than the latter.
Another case is that I do know customer money use scopeRoute + namesRoute combo when they are animating page change effect within one page, but they are targeting mobile.
actually this will only send event when the page has been visited before. When the page first pushed onto screen it won't send any event. You will still need a mechanism to determine first focus. |
// 1. The framework explicitly marked a node inside the dialog as focused | ||
// via the `isFocusable` and `isFocused` flags. In this case, the node | ||
// will request focus directly and there's nothing to do on top of that. | ||
// 2. No node inside the route takes focus explicitly. In this case, the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case this route is reveal when the previous top-most routes are popped, you can listen to the send focus event if we start sending event for web in framework.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep. I've been getting updates from @Hangyujin about it. I'm planning to implementing for web, if/when it's ready on the framework side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it has been implemented on ios and android now.
related PR: ios: #48252
framework: flutter/flutter#135771
callback(this); | ||
_currentChildrenInRenderOrder?.forEach((SemanticsObject child) { | ||
child.visitDepthFirst(callback); | ||
child.visitDepthFirstInRenderOrder(callback); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked at the code the _currentChildrenInRenderOrder
is based on childrenInTraversalOrder
, what is the difference between this and visitDepthFirstInTraversalOrder
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
childrenInRenderOrder
is the order the child elements are rendered into the DOM. IOW, if you do containerElement.elements
you will get children listed in childrenInRenderOrder
. Separately each child may have a z-index
, which dictates its hit test order relative to siblings (also paint order, but our semantics DOM is invisible so that doesn't matter).
Effectively, childrenInRenderOrder
dictates the traversal order in the DOM. This is the order screen readers will observe. The logic that translates childrenInTraversalOrder
/childrenInHitTestOrder
to childrenInRenderOrder
/z-index
is in updateChildren()
. We use a different name because it does not guarantee every possible traversal order the framework could use. The web limitations are similar to iOS though, so I think in practice they are the same.
The reason this function uses childrenInRenderOrder
is to be in sync with the order that the screen reader observes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand the difference between childrenInHitTestOrder and visitDepthFirstInRenderOrder. I was confused at the difference between visitDepthFirstInRenderOrder
and visitDepthFirstInTraversalOrder
, they seem to be the same.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see. You are correct. If the tree is fully consistent the two should effectively return the same result. Currently, the only useful difference is that visitDepthFirstInTraversalOrder
ensures that the tree is finalized and is in consistent state before you traverse it. There are some asserts that enforce it. However, visitDepthFirstInRenderOrder
can operate on unfinalized/inconsistent tree; it just returns partial results, which is useful for debugging and assertion messages. For example, _computeNodeMapConsistencyMessage
uses it and it generates an error that fully explains where things are amiss. If it tried using visitDepthFirstInTraversalOrder
it wouldn't be able to print the partial message because visitDepthFirstInTraversalOrder
would crash on any missing node in the tree.
However, this difference doesn't seem to be too interesting. Let me see if I can clean it up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn't find a way to fully remove visitDepthFirstInRenderOrder
because it's still needed to be able to walk a potentially broken tree (for assert
s). But it's a private method only, so I made it private and debug-only, added a doc about its difference from the other method, and stopped using it in a non-debug context.
Co-authored-by: Mouad Debbar <mouad.debbar@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, my last concern is visitDepthFirstInRenderOrder
and visitDepthFirstInTraversalOrder
, but it is minor.
…140527) flutter/engine@1b1b2a1...737e6f8 2023-12-21 skia-flutter-autoroll@skia.org Roll Dart SDK from bcf68d22f0fa to fe94d9b88531 (1 revision) (flutter/engine#49343) 2023-12-21 1961493+harryterkelsen@users.noreply.github.com Reland "[web] Enforce onDrawFrame/onBeginFrame render rule" (flutter/engine#49336) 2023-12-21 magder@google.com Turn on scenario app screenshots (flutter/engine#49066) 2023-12-21 chris@bracken.jp Revert onrender change (flutter/engine#49333) 2023-12-21 yjbanov@google.com [web] autofocus in new routes (flutter/engine#47727) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-engine-flutter-autoroll Please CC jimgraham@google.com,rmistry@google.com,zra@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
…lutter#140527) flutter/engine@1b1b2a1...737e6f8 2023-12-21 skia-flutter-autoroll@skia.org Roll Dart SDK from bcf68d22f0fa to fe94d9b88531 (1 revision) (flutter/engine#49343) 2023-12-21 1961493+harryterkelsen@users.noreply.github.com Reland "[web] Enforce onDrawFrame/onBeginFrame render rule" (flutter/engine#49336) 2023-12-21 magder@google.com Turn on scenario app screenshots (flutter/engine#49066) 2023-12-21 chris@bracken.jp Revert onrender change (flutter/engine#49333) 2023-12-21 yjbanov@google.com [web] autofocus in new routes (flutter/engine#47727) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-engine-flutter-autoroll Please CC jimgraham@google.com,rmistry@google.com,zra@google.com on the revert to ensure that a human is aware of the problem. To file a bug in Flutter: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
As of flutter/engine#47727 the web engine is able to find a default widget to focus on when a new route it pushed. The mobile engine already did that for some time. So `autofocus` is no longer necessary.
Fixes flutter/flutter#138371
When a new route pops up the expectation is that the screen reader focuses on something in the new route. Since routes typically result in DOM nodes being replaced, the current effect is that the screen reader simply unfocuses from the page, causing the user to have to refocus on back on the page and look for elements to interact with, which is a poor user experience. The current workaround is to use
autofocus
, but that doesn't scale as it's easy to forget, and if the route in question is maintained by a different person you may not even have enough control over it to setautofocus
on anything. For example, this is the case with Flutter's default date picker. All you have isshowDatePicker
and there's no way to control the focus.With this change the route (managed by the
Dialog
primary role) will check if a widget requested explicit focus (perhaps usingautofocus
), and if not, looks for the first descendant that a screen reader can focus on, and requests focus on it. The auto-focused element does not have to be literally focusable. For example, plainText
nodes do not have input focus (i.e. they are notisFocusable
) but screen readers can still focus on them. If such an element is found, the web engine requests that the browser move focus to it programmatically (element.focus()
), which causes the screen reader to move the a11y focus to it as well, but it setstabindex=-1
so the element is not focusable via keyboard or mouse.