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 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>>
|
|||||||
|
|
||||||
// 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>>
|
|||||||
}
|
}
|
||||||
|
|
||||||
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>>
|
|||||||
// 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>>
|
|||||||
_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() {
|
|||||||
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();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user