Unverified Commit ed4dae3c authored by Chris Bracken's avatar Chris Bracken Committed by GitHub

Windows: Focus slider on gaining a11y focus (#95295)

Microsoft Active Accessibility (MSAA) does not include
increment/decrement keyboard shortcuts for manipulating sliders and
other similar controls. To make up for this, we give the slider keyboard
focus when it gains accessibility focus so that the user can use the
arrow keys to manipulate the slider.

Issue: https://github.com/flutter/flutter/issues/77838
parent 166f1d76
......@@ -481,6 +481,9 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
// Value Indicator Animation that appears on the Overlay.
PaintValueIndicator? paintValueIndicator;
FocusNode? _focusNode;
FocusNode get focusNode => widget.focusNode ?? _focusNode!;
@override
void initState() {
super.initState();
......@@ -507,6 +510,10 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
onInvoke: _actionHandler,
),
};
if (widget.focusNode == null) {
// Only create a new node if the widget doesn't have one.
_focusNode ??= FocusNode();
}
}
@override
......@@ -520,6 +527,7 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
overlayEntry!.remove();
overlayEntry = null;
}
_focusNode?.dispose();
super.dispose();
}
......@@ -698,13 +706,32 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
// in range_slider.dart.
Size _screenSize() => MediaQuery.of(context).size;
VoidCallback? handleDidGainAccessibilityFocus;
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
case TargetPlatform.linux:
case TargetPlatform.macOS:
break;
case TargetPlatform.windows:
handleDidGainAccessibilityFocus = () {
// Automatically activate the slider when it receives a11y focus.
if (!focusNode.hasFocus && focusNode.canRequestFocus) {
focusNode.requestFocus();
}
};
break;
}
return Semantics(
container: true,
slider: true,
onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus,
child: FocusableActionDetector(
actions: _actionMap,
shortcuts: _shortcutMap,
focusNode: widget.focusNode,
focusNode: focusNode,
autofocus: widget.autofocus,
enabled: _enabled,
onShowFocusHighlight: _handleFocusHighlightChanged,
......
......@@ -1322,8 +1322,16 @@ void main() {
children: <TestSemantics>[
TestSemantics(
id: 4,
flags: <SemanticsFlag>[SemanticsFlag.hasEnabledState, SemanticsFlag.isEnabled, SemanticsFlag.isFocusable, SemanticsFlag.isSlider],
actions: <SemanticsAction>[SemanticsAction.increase, SemanticsAction.decrease],
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
SemanticsFlag.isSlider,
],
actions: <SemanticsAction>[
SemanticsAction.increase,
SemanticsAction.decrease,
],
value: '50%',
increasedValue: '55%',
decreasedValue: '45%',
......@@ -1439,7 +1447,7 @@ void main() {
);
semantics.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows }));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux }));
testWidgets('Slider Semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
......@@ -1552,6 +1560,175 @@ void main() {
semantics.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('Slider Semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Slider(
value: 0.5,
onChanged: (double v) { },
),
),
),
));
await tester.pumpAndSettle();
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
SemanticsFlag.isSlider,
],
actions: <SemanticsAction>[
SemanticsAction.increase,
SemanticsAction.decrease,
SemanticsAction.didGainAccessibilityFocus,
],
value: '50%',
increasedValue: '55%',
decreasedValue: '45%',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
// Disable slider
await tester.pumpWidget(const MaterialApp(
home: Directionality(
textDirection: TextDirection.ltr,
child: Material(
child: Slider(
value: 0.5,
onChanged: null,
),
),
),
));
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
// isFocusable is delayed by 1 frame.
SemanticsFlag.isFocusable,
SemanticsFlag.isSlider,
],
actions: <SemanticsAction>[
SemanticsAction.didGainAccessibilityFocus,
],
value: '50%',
increasedValue: '55%',
decreasedValue: '45%',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
await tester.pump();
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isSlider,
],
actions: <SemanticsAction>[
SemanticsAction.didGainAccessibilityFocus,
],
value: '50%',
increasedValue: '55%',
decreasedValue: '45%',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
),
);
semantics.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.windows }));
testWidgets('Slider semantics with custom formatter', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
......@@ -1887,6 +2064,73 @@ void main() {
expect(value, 0.5);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('Slider gains keyboard focus when it gains semantics focus on Windows', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final FocusNode focusNode = FocusNode();
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Slider(
value: 0.5,
onChanged: (double _) {},
focusNode: focusNode,
),
),
),
);
expect(semantics, hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
id: 1,
textDirection: TextDirection.ltr,
children: <TestSemantics>[
TestSemantics(
id: 2,
children: <TestSemantics>[
TestSemantics(
id: 3,
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
children: <TestSemantics>[
TestSemantics(
id: 4,
flags: <SemanticsFlag>[
SemanticsFlag.hasEnabledState,
SemanticsFlag.isEnabled,
SemanticsFlag.isFocusable,
SemanticsFlag.isSlider,
],
actions: <SemanticsAction>[
SemanticsAction.increase,
SemanticsAction.decrease,
SemanticsAction.didGainAccessibilityFocus,
],
value: '50%',
increasedValue: '55%',
decreasedValue: '45%',
textDirection: TextDirection.ltr,
),
],
),
],
),
],
),
],
),
ignoreRect: true,
ignoreTransform: true,
));
expect(focusNode.hasFocus, isFalse);
semanticsOwner.performAction(4, SemanticsAction.didGainAccessibilityFocus);
await tester.pumpAndSettle();
expect(focusNode.hasFocus, isTrue);
semantics.dispose();
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.windows }));
testWidgets('Value indicator appears when it should', (WidgetTester tester) async {
final ThemeData baseTheme = ThemeData(
platform: TargetPlatform.android,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment