diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart index 449bc4a2508ce..be5a5893e3ade 100644 --- a/packages/flutter/lib/src/material/slider.dart +++ b/packages/flutter/lib/src/material/slider.dart @@ -475,13 +475,22 @@ class _SliderState extends State with TickerProviderStateMixin { Timer? interactionTimer; final GlobalKey _renderObjectKey = GlobalKey(); + // Keyboard mapping for a focused slider. - final Map _shortcutMap = const { + static const Map _traditionalNavShortcutMap = { SingleActivator(LogicalKeyboardKey.arrowUp): _AdjustSliderIntent.up(), SingleActivator(LogicalKeyboardKey.arrowDown): _AdjustSliderIntent.down(), SingleActivator(LogicalKeyboardKey.arrowLeft): _AdjustSliderIntent.left(), SingleActivator(LogicalKeyboardKey.arrowRight): _AdjustSliderIntent.right(), }; + + // Keyboard mapping for a focused slider when using directional navigation. + // The vertical inputs are not handled to allow navigating out of the slider. + static const Map _directionalNavShortcutMap = { + SingleActivator(LogicalKeyboardKey.arrowLeft): _AdjustSliderIntent.left(), + SingleActivator(LogicalKeyboardKey.arrowRight): _AdjustSliderIntent.right(), + }; + // Action mapping for a focused slider. late Map> _actionMap; @@ -735,13 +744,23 @@ class _SliderState extends State with TickerProviderStateMixin { break; } + final Map shortcutMap; + switch (MediaQuery.of(context).navigationMode) { + case NavigationMode.directional: + shortcutMap = _directionalNavShortcutMap; + break; + case NavigationMode.traditional: + shortcutMap = _traditionalNavShortcutMap; + break; + } + return Semantics( container: true, slider: true, onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus, child: FocusableActionDetector( actions: _actionMap, - shortcuts: _shortcutMap, + shortcuts: shortcutMap, focusNode: focusNode, autofocus: widget.autofocus, enabled: _enabled, diff --git a/packages/flutter/test/material/slider_test.dart b/packages/flutter/test/material/slider_test.dart index c2c8b6d4a5af0..8e935b7b01d14 100644 --- a/packages/flutter/test/material/slider_test.dart +++ b/packages/flutter/test/material/slider_test.dart @@ -2154,6 +2154,102 @@ void main() { expect(value, 0.5); }, variant: const TargetPlatformVariant({ TargetPlatform.iOS, TargetPlatform.macOS })); + testWidgets('In directional nav, Slider can be navigated out of by using up and down arrows', (WidgetTester tester) async { + const Map shortcuts = { + SingleActivator(LogicalKeyboardKey.arrowLeft): DirectionalFocusIntent(TraversalDirection.left), + SingleActivator(LogicalKeyboardKey.arrowRight): DirectionalFocusIntent(TraversalDirection.right), + SingleActivator(LogicalKeyboardKey.arrowDown): DirectionalFocusIntent(TraversalDirection.down), + SingleActivator(LogicalKeyboardKey.arrowUp): DirectionalFocusIntent(TraversalDirection.up), + }; + + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + double topSliderValue = 0.5; + double bottomSliderValue = 0.5; + await tester.pumpWidget( + MaterialApp( + home: Shortcuts( + shortcuts: shortcuts, + child: Material( + child: Center( + child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { + return MediaQuery( + data: const MediaQueryData(navigationMode: NavigationMode.directional), + child: Column( + children: [ + Slider( + value: topSliderValue, + onChanged: (double newValue) { + setState(() { + topSliderValue = newValue; + }); + }, + autofocus: true, + ), + Slider( + value: bottomSliderValue, + onChanged: (double newValue) { + setState(() { + bottomSliderValue = newValue; + }); + }, + ), + ] + ), + ); + }), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + // The top slider is auto-focused and can be adjusted with left and right arrow keys. + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pumpAndSettle(); + expect(topSliderValue, 0.55, reason: 'focused top Slider increased after first arrowRight'); + expect(bottomSliderValue, 0.5, reason: 'unfocused bottom Slider unaffected by first arrowRight'); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); + await tester.pumpAndSettle(); + expect(topSliderValue, 0.5, reason: 'focused top Slider decreased after first arrowLeft'); + expect(bottomSliderValue, 0.5, reason: 'unfocused bottom Slider unaffected by first arrowLeft'); + + // Pressing the down-arrow key moves focus down to the bottom slider + await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown); + await tester.pumpAndSettle(); + expect(topSliderValue, 0.5, reason: 'arrowDown unfocuses top Slider, does not alter its value'); + expect(bottomSliderValue, 0.5, reason: 'arrowDown focuses bottom Slider, does not alter its value'); + + // The bottom slider is now focused and can be adjusted with left and right arrow keys. + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pumpAndSettle(); + expect(topSliderValue, 0.5, reason: 'unfocused top Slider unaffected by second arrowRight'); + expect(bottomSliderValue, 0.55, reason: 'focused bottom Slider increased by second arrowRight'); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); + await tester.pumpAndSettle(); + expect(topSliderValue, 0.5, reason: 'unfocused top Slider unaffected by second arrowLeft'); + expect(bottomSliderValue, 0.5, reason: 'focused bottom Slider decreased by second arrowLeft'); + + // Pressing the up-arrow key moves focus back up to the top slider + await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp); + await tester.pumpAndSettle(); + expect(topSliderValue, 0.5, reason: 'arrowUp focuses top Slider, does not alter its value'); + expect(bottomSliderValue, 0.5, reason: 'arrowUp unfocuses bottom Slider, does not alter its value'); + + // The top slider is now focused again and can be adjusted with left and right arrow keys. + await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight); + await tester.pumpAndSettle(); + expect(topSliderValue, 0.55, reason: 'focused top Slider increased after third arrowRight'); + expect(bottomSliderValue, 0.5, reason: 'unfocused bottom Slider unaffected by third arrowRight'); + + await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft); + await tester.pumpAndSettle(); + expect(topSliderValue, 0.5, reason: 'focused top Slider decreased after third arrowRight'); + expect(bottomSliderValue, 0.5, reason: 'unfocused bottom Slider unaffected by third arrowRight'); + }); + 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!;