Skip to content

Commit ae7fcc7

Browse files
authored
Updating the Slider Widget to allow up and down arrow keys to navigate out of the slider when in directional NavigationMode. (#103149)
1 parent 2f65753 commit ae7fcc7

File tree

2 files changed

+117
-2
lines changed

2 files changed

+117
-2
lines changed

packages/flutter/lib/src/material/slider.dart

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -475,13 +475,22 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
475475
Timer? interactionTimer;
476476

477477
final GlobalKey _renderObjectKey = GlobalKey();
478+
478479
// Keyboard mapping for a focused slider.
479-
final Map<ShortcutActivator, Intent> _shortcutMap = const <ShortcutActivator, Intent>{
480+
static const Map<ShortcutActivator, Intent> _traditionalNavShortcutMap = <ShortcutActivator, Intent>{
480481
SingleActivator(LogicalKeyboardKey.arrowUp): _AdjustSliderIntent.up(),
481482
SingleActivator(LogicalKeyboardKey.arrowDown): _AdjustSliderIntent.down(),
482483
SingleActivator(LogicalKeyboardKey.arrowLeft): _AdjustSliderIntent.left(),
483484
SingleActivator(LogicalKeyboardKey.arrowRight): _AdjustSliderIntent.right(),
484485
};
486+
487+
// Keyboard mapping for a focused slider when using directional navigation.
488+
// The vertical inputs are not handled to allow navigating out of the slider.
489+
static const Map<ShortcutActivator, Intent> _directionalNavShortcutMap = <ShortcutActivator, Intent>{
490+
SingleActivator(LogicalKeyboardKey.arrowLeft): _AdjustSliderIntent.left(),
491+
SingleActivator(LogicalKeyboardKey.arrowRight): _AdjustSliderIntent.right(),
492+
};
493+
485494
// Action mapping for a focused slider.
486495
late Map<Type, Action<Intent>> _actionMap;
487496

@@ -735,13 +744,23 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
735744
break;
736745
}
737746

747+
final Map<ShortcutActivator, Intent> shortcutMap;
748+
switch (MediaQuery.of(context).navigationMode) {
749+
case NavigationMode.directional:
750+
shortcutMap = _directionalNavShortcutMap;
751+
break;
752+
case NavigationMode.traditional:
753+
shortcutMap = _traditionalNavShortcutMap;
754+
break;
755+
}
756+
738757
return Semantics(
739758
container: true,
740759
slider: true,
741760
onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus,
742761
child: FocusableActionDetector(
743762
actions: _actionMap,
744-
shortcuts: _shortcutMap,
763+
shortcuts: shortcutMap,
745764
focusNode: focusNode,
746765
autofocus: widget.autofocus,
747766
enabled: _enabled,

packages/flutter/test/material/slider_test.dart

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2154,6 +2154,102 @@ void main() {
21542154
expect(value, 0.5);
21552155
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
21562156

2157+
testWidgets('In directional nav, Slider can be navigated out of by using up and down arrows', (WidgetTester tester) async {
2158+
const Map<ShortcutActivator, Intent> shortcuts = <ShortcutActivator, Intent>{
2159+
SingleActivator(LogicalKeyboardKey.arrowLeft): DirectionalFocusIntent(TraversalDirection.left),
2160+
SingleActivator(LogicalKeyboardKey.arrowRight): DirectionalFocusIntent(TraversalDirection.right),
2161+
SingleActivator(LogicalKeyboardKey.arrowDown): DirectionalFocusIntent(TraversalDirection.down),
2162+
SingleActivator(LogicalKeyboardKey.arrowUp): DirectionalFocusIntent(TraversalDirection.up),
2163+
};
2164+
2165+
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
2166+
double topSliderValue = 0.5;
2167+
double bottomSliderValue = 0.5;
2168+
await tester.pumpWidget(
2169+
MaterialApp(
2170+
home: Shortcuts(
2171+
shortcuts: shortcuts,
2172+
child: Material(
2173+
child: Center(
2174+
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
2175+
return MediaQuery(
2176+
data: const MediaQueryData(navigationMode: NavigationMode.directional),
2177+
child: Column(
2178+
children: <Widget>[
2179+
Slider(
2180+
value: topSliderValue,
2181+
onChanged: (double newValue) {
2182+
setState(() {
2183+
topSliderValue = newValue;
2184+
});
2185+
},
2186+
autofocus: true,
2187+
),
2188+
Slider(
2189+
value: bottomSliderValue,
2190+
onChanged: (double newValue) {
2191+
setState(() {
2192+
bottomSliderValue = newValue;
2193+
});
2194+
},
2195+
),
2196+
]
2197+
),
2198+
);
2199+
}),
2200+
),
2201+
),
2202+
),
2203+
),
2204+
);
2205+
await tester.pumpAndSettle();
2206+
2207+
// The top slider is auto-focused and can be adjusted with left and right arrow keys.
2208+
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
2209+
await tester.pumpAndSettle();
2210+
expect(topSliderValue, 0.55, reason: 'focused top Slider increased after first arrowRight');
2211+
expect(bottomSliderValue, 0.5, reason: 'unfocused bottom Slider unaffected by first arrowRight');
2212+
2213+
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
2214+
await tester.pumpAndSettle();
2215+
expect(topSliderValue, 0.5, reason: 'focused top Slider decreased after first arrowLeft');
2216+
expect(bottomSliderValue, 0.5, reason: 'unfocused bottom Slider unaffected by first arrowLeft');
2217+
2218+
// Pressing the down-arrow key moves focus down to the bottom slider
2219+
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
2220+
await tester.pumpAndSettle();
2221+
expect(topSliderValue, 0.5, reason: 'arrowDown unfocuses top Slider, does not alter its value');
2222+
expect(bottomSliderValue, 0.5, reason: 'arrowDown focuses bottom Slider, does not alter its value');
2223+
2224+
// The bottom slider is now focused and can be adjusted with left and right arrow keys.
2225+
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
2226+
await tester.pumpAndSettle();
2227+
expect(topSliderValue, 0.5, reason: 'unfocused top Slider unaffected by second arrowRight');
2228+
expect(bottomSliderValue, 0.55, reason: 'focused bottom Slider increased by second arrowRight');
2229+
2230+
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
2231+
await tester.pumpAndSettle();
2232+
expect(topSliderValue, 0.5, reason: 'unfocused top Slider unaffected by second arrowLeft');
2233+
expect(bottomSliderValue, 0.5, reason: 'focused bottom Slider decreased by second arrowLeft');
2234+
2235+
// Pressing the up-arrow key moves focus back up to the top slider
2236+
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
2237+
await tester.pumpAndSettle();
2238+
expect(topSliderValue, 0.5, reason: 'arrowUp focuses top Slider, does not alter its value');
2239+
expect(bottomSliderValue, 0.5, reason: 'arrowUp unfocuses bottom Slider, does not alter its value');
2240+
2241+
// The top slider is now focused again and can be adjusted with left and right arrow keys.
2242+
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
2243+
await tester.pumpAndSettle();
2244+
expect(topSliderValue, 0.55, reason: 'focused top Slider increased after third arrowRight');
2245+
expect(bottomSliderValue, 0.5, reason: 'unfocused bottom Slider unaffected by third arrowRight');
2246+
2247+
await tester.sendKeyEvent(LogicalKeyboardKey.arrowLeft);
2248+
await tester.pumpAndSettle();
2249+
expect(topSliderValue, 0.5, reason: 'focused top Slider decreased after third arrowRight');
2250+
expect(bottomSliderValue, 0.5, reason: 'unfocused bottom Slider unaffected by third arrowRight');
2251+
});
2252+
21572253
testWidgets('Slider gains keyboard focus when it gains semantics focus on Windows', (WidgetTester tester) async {
21582254
final SemanticsTester semantics = SemanticsTester(tester);
21592255
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;

0 commit comments

Comments
 (0)