Skip to content

Commit

Permalink
Updating the Slider Widget to allow up and down arrow keys to navigat…
Browse files Browse the repository at this point in the history
…e out of the slider when in directional NavigationMode. (flutter#103149)
  • Loading branch information
egramond authored May 7, 2022
1 parent 2f65753 commit ae7fcc7
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 2 deletions.
23 changes: 21 additions & 2 deletions packages/flutter/lib/src/material/slider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -475,13 +475,22 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
Timer? interactionTimer;

final GlobalKey _renderObjectKey = GlobalKey();

// Keyboard mapping for a focused slider.
final Map<ShortcutActivator, Intent> _shortcutMap = const <ShortcutActivator, Intent>{
static const Map<ShortcutActivator, Intent> _traditionalNavShortcutMap = <ShortcutActivator, Intent>{
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<ShortcutActivator, Intent> _directionalNavShortcutMap = <ShortcutActivator, Intent>{
SingleActivator(LogicalKeyboardKey.arrowLeft): _AdjustSliderIntent.left(),
SingleActivator(LogicalKeyboardKey.arrowRight): _AdjustSliderIntent.right(),
};

// Action mapping for a focused slider.
late Map<Type, Action<Intent>> _actionMap;

Expand Down Expand Up @@ -735,13 +744,23 @@ class _SliderState extends State<Slider> with TickerProviderStateMixin {
break;
}

final Map<ShortcutActivator, Intent> 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,
Expand Down
96 changes: 96 additions & 0 deletions packages/flutter/test/material/slider_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2154,6 +2154,102 @@ void main() {
expect(value, 0.5);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ 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<ShortcutActivator, Intent> shortcuts = <ShortcutActivator, Intent>{
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: <Widget>[
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!;
Expand Down

0 comments on commit ae7fcc7

Please sign in to comment.