Unverified Commit 919d2051 authored by Markus Aksli's avatar Markus Aksli Committed by GitHub

Dismiss Autocomplete with ESC (#97790)

parent 7c3f79f7
...@@ -277,8 +277,11 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>> ...@@ -277,8 +277,11 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
late final Map<Type, Action<Intent>> _actionMap; late final Map<Type, Action<Intent>> _actionMap;
late final _AutocompleteCallbackAction<AutocompletePreviousOptionIntent> _previousOptionAction; late final _AutocompleteCallbackAction<AutocompletePreviousOptionIntent> _previousOptionAction;
late final _AutocompleteCallbackAction<AutocompleteNextOptionIntent> _nextOptionAction; late final _AutocompleteCallbackAction<AutocompleteNextOptionIntent> _nextOptionAction;
late final _AutocompleteCallbackAction<DismissIntent> _hideOptionsAction;
Iterable<T> _options = Iterable<T>.empty(); Iterable<T> _options = Iterable<T>.empty();
T? _selection; T? _selection;
bool _userHidOptions = false;
String _lastFieldText = '';
final ValueNotifier<int> _highlightedOptionIndex = ValueNotifier<int>(0); final ValueNotifier<int> _highlightedOptionIndex = ValueNotifier<int>(0);
static const Map<ShortcutActivator, Intent> _shortcuts = <ShortcutActivator, Intent>{ static const Map<ShortcutActivator, Intent> _shortcuts = <ShortcutActivator, Intent>{
...@@ -291,31 +294,41 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>> ...@@ -291,31 +294,41 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
// True iff the state indicates that the options should be visible. // True iff the state indicates that the options should be visible.
bool get _shouldShowOptions { bool get _shouldShowOptions {
return _focusNode.hasFocus && _selection == null && _options.isNotEmpty; return !_userHidOptions && _focusNode.hasFocus && _selection == null && _options.isNotEmpty;
} }
// Called when _textEditingController changes. // Called when _textEditingController changes.
Future<void> _onChangedField() async { Future<void> _onChangedField() async {
final TextEditingValue value = _textEditingController.value;
final Iterable<T> options = await widget.optionsBuilder( final Iterable<T> options = await widget.optionsBuilder(
_textEditingController.value, value,
); );
_options = options; _options = options;
_updateHighlight(_highlightedOptionIndex.value); _updateHighlight(_highlightedOptionIndex.value);
if (_selection != null if (_selection != null
&& _textEditingController.text != widget.displayStringForOption(_selection!)) { && value.text != widget.displayStringForOption(_selection!)) {
_selection = null; _selection = null;
} }
// Make sure the options are no longer hidden if the content of the field
// changes (ignore selection changes).
if (value.text != _lastFieldText) {
_userHidOptions = false;
_lastFieldText = value.text;
}
_updateOverlay(); _updateOverlay();
} }
// Called when the field's FocusNode changes. // Called when the field's FocusNode changes.
void _onChangedFocus() { void _onChangedFocus() {
// Options should no longer be hidden when the field is re-focused.
_userHidOptions = !_focusNode.hasFocus;
_updateOverlay(); _updateOverlay();
} }
// Called from fieldViewBuilder when the user submits the field. // Called from fieldViewBuilder when the user submits the field.
void _onFieldSubmitted() { void _onFieldSubmitted() {
if (_options.isEmpty) { if (_options.isEmpty || _userHidOptions) {
return; return;
} }
_select(_options.elementAt(_highlightedOptionIndex.value)); _select(_options.elementAt(_highlightedOptionIndex.value));
...@@ -340,13 +353,30 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>> ...@@ -340,13 +353,30 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
} }
void _highlightPreviousOption(AutocompletePreviousOptionIntent intent) { void _highlightPreviousOption(AutocompletePreviousOptionIntent intent) {
if (_userHidOptions) {
_userHidOptions = false;
_updateOverlay();
return;
}
_updateHighlight(_highlightedOptionIndex.value - 1); _updateHighlight(_highlightedOptionIndex.value - 1);
} }
void _highlightNextOption(AutocompleteNextOptionIntent intent) { void _highlightNextOption(AutocompleteNextOptionIntent intent) {
if (_userHidOptions) {
_userHidOptions = false;
_updateOverlay();
return;
}
_updateHighlight(_highlightedOptionIndex.value + 1); _updateHighlight(_highlightedOptionIndex.value + 1);
} }
void _hideOptions(DismissIntent intent) {
if (!_userHidOptions) {
_userHidOptions = true;
_updateOverlay();
}
}
void _setActionsEnabled(bool enabled) { void _setActionsEnabled(bool enabled) {
// The enabled state determines whether the action will consume the // The enabled state determines whether the action will consume the
// key shortcut or let it continue on to the underlying text field. // key shortcut or let it continue on to the underlying text field.
...@@ -354,11 +384,12 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>> ...@@ -354,11 +384,12 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
// can be used to navigate them. // can be used to navigate them.
_previousOptionAction.enabled = enabled; _previousOptionAction.enabled = enabled;
_nextOptionAction.enabled = enabled; _nextOptionAction.enabled = enabled;
_hideOptionsAction.enabled = enabled;
} }
// Hide or show the options overlay, if needed. // Hide or show the options overlay, if needed.
void _updateOverlay() { void _updateOverlay() {
_setActionsEnabled(_shouldShowOptions); _setActionsEnabled(_focusNode.hasFocus && _selection == null && _options.isNotEmpty);
if (_shouldShowOptions) { if (_shouldShowOptions) {
_floatingOptions?.remove(); _floatingOptions?.remove();
_floatingOptions = OverlayEntry( _floatingOptions = OverlayEntry(
...@@ -434,9 +465,11 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>> ...@@ -434,9 +465,11 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
_focusNode.addListener(_onChangedFocus); _focusNode.addListener(_onChangedFocus);
_previousOptionAction = _AutocompleteCallbackAction<AutocompletePreviousOptionIntent>(onInvoke: _highlightPreviousOption); _previousOptionAction = _AutocompleteCallbackAction<AutocompletePreviousOptionIntent>(onInvoke: _highlightPreviousOption);
_nextOptionAction = _AutocompleteCallbackAction<AutocompleteNextOptionIntent>(onInvoke: _highlightNextOption); _nextOptionAction = _AutocompleteCallbackAction<AutocompleteNextOptionIntent>(onInvoke: _highlightNextOption);
_hideOptionsAction = _AutocompleteCallbackAction<DismissIntent>(onInvoke: _hideOptions);
_actionMap = <Type, Action<Intent>> { _actionMap = <Type, Action<Intent>> {
AutocompletePreviousOptionIntent: _previousOptionAction, AutocompletePreviousOptionIntent: _previousOptionAction,
AutocompleteNextOptionIntent: _nextOptionAction, AutocompleteNextOptionIntent: _nextOptionAction,
DismissIntent: _hideOptionsAction,
}; };
SchedulerBinding.instance.addPostFrameCallback((Duration _) { SchedulerBinding.instance.addPostFrameCallback((Duration _) {
_updateOverlay(); _updateOverlay();
......
...@@ -793,6 +793,96 @@ void main() { ...@@ -793,6 +793,96 @@ void main() {
expect(textEditingController.text, 'goose'); expect(textEditingController.text, 'goose');
}); });
testWidgets('can hide and show options with the keyboard', (WidgetTester tester) async {
final GlobalKey fieldKey = GlobalKey();
final GlobalKey optionsKey = GlobalKey();
late Iterable<String> lastOptions;
late FocusNode focusNode;
late TextEditingController textEditingController;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: RawAutocomplete<String>(
optionsBuilder: (TextEditingValue textEditingValue) {
return kOptions.where((String option) {
return option.contains(textEditingValue.text.toLowerCase());
});
},
fieldViewBuilder: (BuildContext context, TextEditingController fieldTextEditingController, FocusNode fieldFocusNode, VoidCallback onFieldSubmitted) {
focusNode = fieldFocusNode;
textEditingController = fieldTextEditingController;
return TextFormField(
key: fieldKey,
focusNode: focusNode,
controller: textEditingController,
onFieldSubmitted: (String value) {
onFieldSubmitted();
},
);
},
optionsViewBuilder: (BuildContext context, AutocompleteOnSelected<String> onSelected, Iterable<String> options) {
lastOptions = options;
return Container(key: optionsKey);
},
),
),
),
);
// Enter text. The options are filtered by the text.
focusNode.requestFocus();
await tester.enterText(find.byKey(fieldKey), 'ele');
await tester.pumpAndSettle();
expect(find.byKey(fieldKey), findsOneWidget);
expect(find.byKey(optionsKey), findsOneWidget);
expect(lastOptions.length, 2);
expect(lastOptions.elementAt(0), 'chameleon');
expect(lastOptions.elementAt(1), 'elephant');
// Hide the options.
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pump();
expect(find.byKey(fieldKey), findsOneWidget);
expect(find.byKey(optionsKey), findsNothing);
// Show the options again by pressing arrow keys
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
await tester.pump();
expect(find.byKey(optionsKey), findsOneWidget);
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pump();
expect(find.byKey(optionsKey), findsNothing);
await tester.sendKeyEvent(LogicalKeyboardKey.arrowUp);
await tester.pump();
expect(find.byKey(optionsKey), findsOneWidget);
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pump();
expect(find.byKey(optionsKey), findsNothing);
// Show the options again by re-focusing the field.
focusNode.unfocus();
await tester.pump();
expect(find.byKey(optionsKey), findsNothing);
focusNode.requestFocus();
await tester.pump();
expect(find.byKey(optionsKey), findsOneWidget);
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pump();
expect(find.byKey(optionsKey), findsNothing);
// Show the options again by editing the text (but not when selecting text
// or moving the caret).
await tester.enterText(find.byKey(fieldKey), 'elep');
await tester.pump();
expect(find.byKey(optionsKey), findsOneWidget);
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
await tester.pump();
expect(find.byKey(optionsKey), findsNothing);
textEditingController.selection = TextSelection.fromPosition(const TextPosition(offset: 3));
await tester.pump();
expect(find.byKey(optionsKey), findsNothing);
});
testWidgets('optionsViewBuilders can use AutocompleteHighlightedOption to highlight selected option', (WidgetTester tester) async { testWidgets('optionsViewBuilders can use AutocompleteHighlightedOption to highlight selected option', (WidgetTester tester) async {
final GlobalKey fieldKey = GlobalKey(); final GlobalKey fieldKey = GlobalKey();
final GlobalKey optionsKey = GlobalKey(); final GlobalKey optionsKey = GlobalKey();
......
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