Skip to content

Commit

Permalink
Request focus if SemanticsAction.focus is sent to a focusable widge…
Browse files Browse the repository at this point in the history
…t (#142942)

## Description

This causes the `Focus` widget to request focus on its focus node if the accessibility system (screen reader) focuses a widget via the `SemanticsAction.focus` action.

## Related Issues
 - flutter/flutter#83809

## Tests
 - Added a test to make sure that focus is requested when `SemanticsAction.focus` is sent by the engine.
  • Loading branch information
gspencergoog authored Jun 5, 2024
1 parent 4d21d2d commit dd700e6
Show file tree
Hide file tree
Showing 58 changed files with 281 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ void main() {
isEnabled: true,
isFocusable: true,
hasTapAction: true,
hasFocusAction: true,
label: 'Update border shape',
));

Expand All @@ -29,6 +30,7 @@ void main() {
isEnabled: true,
isFocusable: true,
hasTapAction: true,
hasFocusAction: true,
label: 'Reset chips',
));

Expand Down
8 changes: 8 additions & 0 deletions packages/flutter/lib/src/widgets/focus_scope.dart
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,14 @@ class _FocusState extends State<Focus> {
Widget child = widget.child;
if (widget.includeSemantics) {
child = Semantics(
// Automatically request the focus for a focusable widget when it
// receives an input focus action from the semantics. Nothing is needed
// for losing the focus because if focus is lost, that means another
// node will gain focus and take focus from this widget.
onFocus:
_couldRequestFocus
? focusNode.requestFocus
: null,
focusable: _couldRequestFocus,
focused: _hadPrimaryFocus,
child: widget.child,
Expand Down
10 changes: 7 additions & 3 deletions packages/flutter/test/cupertino/checkbox_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ void main() {
hasEnabledState: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
isFocusable: true,
));

Expand All @@ -54,6 +55,7 @@ void main() {
isChecked: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
isFocusable: true,
));

Expand All @@ -73,6 +75,7 @@ void main() {
hasEnabledState: true,
// isFocusable is delayed by 1 frame.
isFocusable: true,
hasFocusAction: true,
));

await tester.pump();
Expand Down Expand Up @@ -178,6 +181,7 @@ void main() {
hasEnabledState: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
isFocusable: true,
));
handle.dispose();
Expand Down Expand Up @@ -247,7 +251,7 @@ void main() {
SemanticsFlag.isFocusable,
SemanticsFlag.isCheckStateMixed,
],
actions: <SemanticsAction>[SemanticsAction.tap],
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
), hasLength(1));

await tester.pumpWidget(
Expand All @@ -268,7 +272,7 @@ void main() {
SemanticsFlag.isChecked,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
), hasLength(1));

await tester.pumpWidget(
Expand All @@ -288,7 +292,7 @@ void main() {
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
), hasLength(1));

semantics.dispose();
Expand Down
4 changes: 4 additions & 0 deletions packages/flutter/test/cupertino/radio_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ void main() {
],
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.focus,
],
),
);
Expand All @@ -171,6 +172,7 @@ void main() {
hasEnabledState: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
isFocusable: true,
isInMutuallyExclusiveGroup: true,
));
Expand All @@ -190,6 +192,7 @@ void main() {
hasEnabledState: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
isFocusable: true,
isInMutuallyExclusiveGroup: true,
isChecked: true,
Expand All @@ -210,6 +213,7 @@ void main() {
hasEnabledState: true,
isFocusable: true,
isInMutuallyExclusiveGroup: true,
hasFocusAction: true,
));

await tester.pump();
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter/test/cupertino/route_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1945,7 +1945,7 @@ void main() {
await tester.pumpAndSettle();

expect(semantics, isNot(includesNodeWith(
actions: <SemanticsAction>[SemanticsAction.tap],
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
label: 'Dismiss',
)));
debugDefaultTargetPlatformOverride = null;
Expand Down
2 changes: 1 addition & 1 deletion packages/flutter/test/cupertino/text_field_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2621,7 +2621,7 @@ void main() {
),
);

expect(semantics, isNot(includesNodeWith(actions: <SemanticsAction>[SemanticsAction.tap])));
expect(semantics, isNot(includesNodeWith(actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus])));

semantics.dispose();
});
Expand Down
2 changes: 2 additions & 0 deletions packages/flutter/test/material/back_button_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ void main() {
hasEnabledState: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
isFocusable: true,
));
handle.dispose();
Expand Down Expand Up @@ -258,6 +259,7 @@ void main() {
hasEnabledState: true,
isEnabled: true,
hasTapAction: true,
hasFocusAction: true,
isFocusable: true,
));
handle.dispose();
Expand Down
14 changes: 12 additions & 2 deletions packages/flutter/test/material/bottom_navigation_bar_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2111,6 +2111,7 @@ void main() {
isFocusable: true,
isSelected: true,
hasTapAction: true,
hasFocusAction: true,
),
);
expect(
Expand All @@ -2120,6 +2121,7 @@ void main() {
textDirection: TextDirection.ltr,
isFocusable: true,
hasTapAction: true,
hasFocusAction: true,
),
);
expect(
Expand All @@ -2129,6 +2131,7 @@ void main() {
textDirection: TextDirection.ltr,
isFocusable: true,
hasTapAction: true,
hasFocusAction: true,
),
);
});
Expand Down Expand Up @@ -2165,6 +2168,7 @@ void main() {
isFocusable: true,
isSelected: true,
hasTapAction: true,
hasFocusAction: true,
),
);
expect(
Expand All @@ -2174,6 +2178,7 @@ void main() {
textDirection: TextDirection.ltr,
isFocusable: true,
hasTapAction: true,
hasFocusAction: true,
),
);
expect(
Expand All @@ -2183,6 +2188,7 @@ void main() {
textDirection: TextDirection.ltr,
isFocusable: true,
hasTapAction: true,
hasFocusAction: true,
),
);
});
Expand Down Expand Up @@ -2515,6 +2521,7 @@ void main() {
isFocusable: true,
isSelected: true,
hasTapAction: true,
hasFocusAction: true,
),
);
expect(
Expand All @@ -2524,6 +2531,7 @@ void main() {
textDirection: TextDirection.ltr,
isFocusable: true,
hasTapAction: true,
hasFocusAction: true,
),
);
});
Expand Down Expand Up @@ -2558,6 +2566,7 @@ void main() {
isFocusable: true,
isSelected: true,
hasTapAction: true,
hasFocusAction: true,
),
);
expect(
Expand All @@ -2567,6 +2576,7 @@ void main() {
textDirection: TextDirection.ltr,
isFocusable: true,
hasTapAction: true,
hasFocusAction: true,
),
);
});
Expand Down Expand Up @@ -2747,13 +2757,13 @@ void main() {
SemanticsFlag.isSelected,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[SemanticsAction.tap],
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
label: 'A\nTab 1 of 2',
textDirection: TextDirection.ltr,
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isFocusable],
actions: <SemanticsAction>[SemanticsAction.tap],
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
label: 'B\nTab 2 of 2',
textDirection: TextDirection.ltr,
),
Expand Down
Loading

0 comments on commit dd700e6

Please sign in to comment.