Dismiss Autocomplete with ESC (#97790)
This commit is contained in:
parent
7c3f79f725
commit
919d20511c
@ -277,8 +277,11 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
|
||||
late final Map<Type, Action<Intent>> _actionMap;
|
||||
late final _AutocompleteCallbackAction<AutocompletePreviousOptionIntent> _previousOptionAction;
|
||||
late final _AutocompleteCallbackAction<AutocompleteNextOptionIntent> _nextOptionAction;
|
||||
late final _AutocompleteCallbackAction<DismissIntent> _hideOptionsAction;
|
||||
Iterable<T> _options = Iterable<T>.empty();
|
||||
T? _selection;
|
||||
bool _userHidOptions = false;
|
||||
String _lastFieldText = '';
|
||||
final ValueNotifier<int> _highlightedOptionIndex = ValueNotifier<int>(0);
|
||||
|
||||
static const Map<ShortcutActivator, Intent> _shortcuts = <ShortcutActivator, Intent>{
|
||||
@ -291,31 +294,41 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
|
||||
|
||||
// True iff the state indicates that the options should be visible.
|
||||
bool get _shouldShowOptions {
|
||||
return _focusNode.hasFocus && _selection == null && _options.isNotEmpty;
|
||||
return !_userHidOptions && _focusNode.hasFocus && _selection == null && _options.isNotEmpty;
|
||||
}
|
||||
|
||||
// Called when _textEditingController changes.
|
||||
Future<void> _onChangedField() async {
|
||||
final TextEditingValue value = _textEditingController.value;
|
||||
final Iterable<T> options = await widget.optionsBuilder(
|
||||
_textEditingController.value,
|
||||
value,
|
||||
);
|
||||
_options = options;
|
||||
_updateHighlight(_highlightedOptionIndex.value);
|
||||
if (_selection != null
|
||||
&& _textEditingController.text != widget.displayStringForOption(_selection!)) {
|
||||
&& value.text != widget.displayStringForOption(_selection!)) {
|
||||
_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();
|
||||
}
|
||||
|
||||
// Called when the field's FocusNode changes.
|
||||
void _onChangedFocus() {
|
||||
// Options should no longer be hidden when the field is re-focused.
|
||||
_userHidOptions = !_focusNode.hasFocus;
|
||||
_updateOverlay();
|
||||
}
|
||||
|
||||
// Called from fieldViewBuilder when the user submits the field.
|
||||
void _onFieldSubmitted() {
|
||||
if (_options.isEmpty) {
|
||||
if (_options.isEmpty || _userHidOptions) {
|
||||
return;
|
||||
}
|
||||
_select(_options.elementAt(_highlightedOptionIndex.value));
|
||||
@ -340,13 +353,30 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
|
||||
}
|
||||
|
||||
void _highlightPreviousOption(AutocompletePreviousOptionIntent intent) {
|
||||
if (_userHidOptions) {
|
||||
_userHidOptions = false;
|
||||
_updateOverlay();
|
||||
return;
|
||||
}
|
||||
_updateHighlight(_highlightedOptionIndex.value - 1);
|
||||
}
|
||||
|
||||
void _highlightNextOption(AutocompleteNextOptionIntent intent) {
|
||||
if (_userHidOptions) {
|
||||
_userHidOptions = false;
|
||||
_updateOverlay();
|
||||
return;
|
||||
}
|
||||
_updateHighlight(_highlightedOptionIndex.value + 1);
|
||||
}
|
||||
|
||||
void _hideOptions(DismissIntent intent) {
|
||||
if (!_userHidOptions) {
|
||||
_userHidOptions = true;
|
||||
_updateOverlay();
|
||||
}
|
||||
}
|
||||
|
||||
void _setActionsEnabled(bool enabled) {
|
||||
// The enabled state determines whether the action will consume the
|
||||
// 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>>
|
||||
// can be used to navigate them.
|
||||
_previousOptionAction.enabled = enabled;
|
||||
_nextOptionAction.enabled = enabled;
|
||||
_hideOptionsAction.enabled = enabled;
|
||||
}
|
||||
|
||||
// Hide or show the options overlay, if needed.
|
||||
void _updateOverlay() {
|
||||
_setActionsEnabled(_shouldShowOptions);
|
||||
_setActionsEnabled(_focusNode.hasFocus && _selection == null && _options.isNotEmpty);
|
||||
if (_shouldShowOptions) {
|
||||
_floatingOptions?.remove();
|
||||
_floatingOptions = OverlayEntry(
|
||||
@ -434,9 +465,11 @@ class _RawAutocompleteState<T extends Object> extends State<RawAutocomplete<T>>
|
||||
_focusNode.addListener(_onChangedFocus);
|
||||
_previousOptionAction = _AutocompleteCallbackAction<AutocompletePreviousOptionIntent>(onInvoke: _highlightPreviousOption);
|
||||
_nextOptionAction = _AutocompleteCallbackAction<AutocompleteNextOptionIntent>(onInvoke: _highlightNextOption);
|
||||
_hideOptionsAction = _AutocompleteCallbackAction<DismissIntent>(onInvoke: _hideOptions);
|
||||
_actionMap = <Type, Action<Intent>> {
|
||||
AutocompletePreviousOptionIntent: _previousOptionAction,
|
||||
AutocompleteNextOptionIntent: _nextOptionAction,
|
||||
DismissIntent: _hideOptionsAction,
|
||||
};
|
||||
SchedulerBinding.instance.addPostFrameCallback((Duration _) {
|
||||
_updateOverlay();
|
||||
|
@ -793,6 +793,96 @@ void main() {
|
||||
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 {
|
||||
final GlobalKey fieldKey = GlobalKey();
|
||||
final GlobalKey optionsKey = GlobalKey();
|
||||
|
Loading…
x
Reference in New Issue
Block a user