Skip to content

Commit 2ace238

Browse files
authored
Add right arrow to complete FAutocompelte & toggle to hide hint in FMultiSelect (#650)
* Hide hint * Add autocomplete right arrow to complete * Prepare Forui for review * Update windows-latest goldens --------- Co-authored-by: Pante <[email protected]>
1 parent 2742b70 commit 2ace238

File tree

11 files changed

+140
-13
lines changed

11 files changed

+140
-13
lines changed

docs/app/docs/form/autocomplete/page.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ FAutocomplete(
6565
suffixBuilder: (context, style, states) => Icon(FIcons.search),
6666
popoverConstraints: const FAutoWidthPortalConstraints(maxHeight: 400),
6767
clearable: (value) => value.text.isNotEmpty,
68+
rightArrowToComplete: true,
6869
initialText: 'Canada',
6970
items: [
7071
'United States',
@@ -101,6 +102,7 @@ FAutocomplete.builder(
101102
prefixBuilder: (context, style, states) => Icon(FIcons.globe),
102103
suffixBuilder: (context, style, states) => Icon(FIcons.search),
103104
popoverConstraints: const FAutoWidthPortalConstraints(maxHeight: 300),
105+
rightArrowToComplete: true,
104106
clearable: (value) => value.text.isNotEmpty,
105107
);
106108
```

docs/app/docs/form/multi-select/page.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ FMultiSelect<Locale>(
8181
label: const Text('Country'),
8282
description: const Text('Select your country of residence'),
8383
hint: Text('Choose a country'),
84+
keepHint: true,
8485
format: (value) => Text(value.toUpperCase()),
8586
sort: (a, b) => a.compareTo(b),
8687
onChange: (value) => print('Selected country: $value'),
@@ -107,6 +108,7 @@ FMultiSelect<String>.rich(
107108
label: const Text('Country'),
108109
description: const Text('Select your country of residence'),
109110
hint: Text('Choose a country'),
111+
keepHint: true,
110112
format: (value) => Text(value.toUpperCase()),
111113
sort: (a, b) => a.compareTo(b),
112114
onChange: (value) => print('Selected country: $value'),

forui/lib/src/widgets/autocomplete/autocomplete.dart

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,10 @@ class FAutocomplete extends StatefulWidget with FFormFieldProperties<String> {
276276
/// Defaults to returning the given child.
277277
final FFieldBuilder<FAutocompleteStyle> builder;
278278

279+
/// Whether the autocomplete should complete the text when a completion is available and the user presses right arrow.
280+
/// Defaults to false.
281+
final bool rightArrowToComplete;
282+
279283
/// A callback that produces a list of items based on the query either synchronously or asynchronously.
280284
final FutureOr<Iterable<String>> Function(String text) filter;
281285

@@ -377,6 +381,7 @@ class FAutocomplete extends StatefulWidget with FFormFieldProperties<String> {
377381
FPopoverHideRegion hideRegion = FPopoverHideRegion.excludeChild,
378382
bool autoHide = true,
379383
FFieldBuilder<FAutocompleteStyle> builder = _builder,
384+
bool rightArrowToComplete = false,
380385
FutureOr<Iterable<String>> Function(String)? filter,
381386
FAutoCompleteContentBuilder? contentBuilder,
382387
ScrollController? contentScrollController,
@@ -463,6 +468,7 @@ class FAutocomplete extends StatefulWidget with FFormFieldProperties<String> {
463468
hideRegion: hideRegion,
464469
autoHide: autoHide,
465470
builder: builder,
471+
rightArrowToComplete: rightArrowToComplete,
466472
contentScrollController: contentScrollController,
467473
contentPhysics: contentPhysics,
468474
contentDivider: contentDivider,
@@ -549,6 +555,7 @@ class FAutocomplete extends StatefulWidget with FFormFieldProperties<String> {
549555
this.hideRegion = FPopoverHideRegion.excludeChild,
550556
this.autoHide = true,
551557
this.builder = _builder,
558+
this.rightArrowToComplete = false,
552559
this.contentScrollController,
553560
this.contentPhysics = const ClampingScrollPhysics(),
554561
this.contentDivider = FItemDivider.none,
@@ -648,6 +655,7 @@ class FAutocomplete extends StatefulWidget with FFormFieldProperties<String> {
648655
..add(ObjectFlagProperty.has('onTapHide', onTapHide))
649656
..add(FlagProperty('autoHide', value: autoHide, ifTrue: 'autoHide'))
650657
..add(ObjectFlagProperty.has('builder', builder))
658+
..add(FlagProperty('rightArrowToComplete', value: rightArrowToComplete, ifTrue: 'rightArrowToComplete'))
651659
..add(ObjectFlagProperty.has('filter', filter))
652660
..add(ObjectFlagProperty.has('contentEmptyBuilder', contentEmptyBuilder))
653661
..add(DiagnosticsProperty('contentScrollController', contentScrollController))
@@ -875,20 +883,24 @@ class _State extends State<FAutocomplete> with SingleTickerProviderStateMixin {
875883
const SingleActivator(LogicalKeyboardKey.arrowDown): () =>
876884
_popoverFocus.descendants.firstOrNull?.requestFocus(),
877885
if (_controller.current case (:final replacement, completion: final _))
878-
const SingleActivator(LogicalKeyboardKey.tab): () {
879-
if (widget.autoHide) {
880-
_controller.popover.hide();
881-
}
882-
_previous = replacement;
883-
_controller.complete();
884-
},
886+
const SingleActivator(LogicalKeyboardKey.tab): () => _complete(replacement),
887+
if (_controller.current case (:final replacement, completion: final _) when widget.rightArrowToComplete)
888+
const SingleActivator(LogicalKeyboardKey.arrowRight): () => _complete(replacement),
885889
},
886890
child: widget.builder(context, style, states, field),
887891
),
888892
),
889893
),
890894
);
891895
}
896+
897+
void _complete(String replacement) {
898+
if (widget.autoHide) {
899+
_controller.popover.hide();
900+
}
901+
_previous = replacement;
902+
_controller.complete();
903+
}
892904
}
893905

894906
/// An [FAutocomplete]'s style.

forui/lib/src/widgets/select/multi/field.dart

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ class Field<T> extends FormField<Set<T>> {
2121
final Widget? label;
2222
final Widget? description;
2323
final Widget? hint;
24+
final bool keepHint;
2425
final int Function(T, T)? sort;
2526
final Widget Function(T) format;
2627
final FMultiSelectTagBuilder<T> tagBuilder;
@@ -49,6 +50,7 @@ class Field<T> extends FormField<Set<T>> {
4950
required this.label,
5051
required this.description,
5152
required this.hint,
53+
required this.keepHint,
5254
required this.sort,
5355
required this.format,
5456
required this.tagBuilder,
@@ -141,13 +143,14 @@ class Field<T> extends FormField<Set<T>> {
141143
children: [
142144
for (final value in values)
143145
tagBuilder(context, state._controller, style, value, format(value)),
144-
Padding(
145-
padding: style.fieldStyle.hintPadding,
146-
child: DefaultTextStyle.merge(
147-
style: style.fieldStyle.hintTextStyle.resolve(states),
148-
child: hint ?? Text(localizations.multiSelectHint),
146+
if (keepHint || state._controller.value.isEmpty)
147+
Padding(
148+
padding: style.fieldStyle.hintPadding,
149+
child: DefaultTextStyle.merge(
150+
style: style.fieldStyle.hintTextStyle.resolve(states),
151+
child: hint ?? Text(localizations.multiSelectHint),
152+
),
149153
),
150-
),
151154
],
152155
),
153156
),
@@ -199,6 +202,7 @@ class Field<T> extends FormField<Set<T>> {
199202
..add(EnumProperty('autovalidateMode', autovalidateMode))
200203
..add(StringProperty('forceErrorText', forceErrorText))
201204
..add(ObjectFlagProperty.has('validator', validator))
205+
..add(FlagProperty('keepHint', value: keepHint, ifTrue: 'keepHint'))
202206
..add(ObjectFlagProperty.has('sort', sort))
203207
..add(ObjectFlagProperty.has('format', format))
204208
..add(ObjectFlagProperty.has('tagBuilder', tagBuilder))

forui/lib/src/widgets/select/multi/select.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ abstract class FMultiSelect<T> extends StatelessWidget {
123123
/// The hint.
124124
final Widget? hint;
125125

126+
/// Whether to keep the hint visible when there are selected items. Defaults to true.
127+
final bool keepHint;
128+
126129
/// The function used to sort the selected items. Defaults to the order in which they were selected.
127130
final int Function(T, T)? sort;
128131

@@ -220,6 +223,7 @@ abstract class FMultiSelect<T> extends StatelessWidget {
220223
String? Function(Set<T>) validator = _defaultValidator,
221224
Widget Function(BuildContext, String) errorBuilder = FFormFieldProperties.defaultErrorBuilder,
222225
Widget? hint,
226+
bool keepHint = true,
223227
int Function(T, T)? sort,
224228
Widget Function(BuildContext, FMultiSelectController<T>, FMultiSelectStyle, T, Widget)? tagBuilder,
225229
TextAlign textAlign = TextAlign.start,
@@ -263,6 +267,7 @@ abstract class FMultiSelect<T> extends StatelessWidget {
263267
validator: validator,
264268
errorBuilder: errorBuilder,
265269
hint: hint,
270+
keepHint: keepHint,
266271
textAlign: textAlign,
267272
textDirection: textDirection,
268273
clearable: clearable,
@@ -306,6 +311,7 @@ abstract class FMultiSelect<T> extends StatelessWidget {
306311
String? Function(Set<T>) validator,
307312
Widget Function(BuildContext, String) errorBuilder,
308313
Widget? hint,
314+
bool keepHint,
309315
int Function(T, T)? sort,
310316
FMultiSelectTagBuilder<T>? tagBuilder,
311317
TextAlign textAlign,
@@ -366,6 +372,7 @@ abstract class FMultiSelect<T> extends StatelessWidget {
366372
String? Function(Set<T>) validator = _defaultValidator,
367373
Widget Function(BuildContext, String) errorBuilder = FFormFieldProperties.defaultErrorBuilder,
368374
Widget? hint,
375+
bool keepHint = true,
369376
int Function(T, T)? sort,
370377
FMultiSelectTagBuilder<T>? tagBuilder,
371378
TextAlign textAlign = TextAlign.start,
@@ -419,6 +426,7 @@ abstract class FMultiSelect<T> extends StatelessWidget {
419426
validator: validator,
420427
errorBuilder: errorBuilder,
421428
hint: hint,
429+
keepHint: keepHint,
422430
sort: sort,
423431
tagBuilder: tagBuilder,
424432
textAlign: textAlign,
@@ -475,6 +483,7 @@ abstract class FMultiSelect<T> extends StatelessWidget {
475483
String? Function(Set<T>) validator,
476484
Widget Function(BuildContext, String) errorBuilder,
477485
Widget? hint,
486+
bool keepHint,
478487
int Function(T, T)? sort,
479488
FMultiSelectTagBuilder<T>? tagBuilder,
480489
TextAlign textAlign,
@@ -516,6 +525,7 @@ abstract class FMultiSelect<T> extends StatelessWidget {
516525
this.validator = _defaultValidator,
517526
this.errorBuilder = FFormFieldProperties.defaultErrorBuilder,
518527
this.hint,
528+
this.keepHint = true,
519529
this.sort,
520530
this.textAlign = TextAlign.start,
521531
this.textDirection,
@@ -568,6 +578,7 @@ abstract class FMultiSelect<T> extends StatelessWidget {
568578
description: description,
569579
onChange: onChange,
570580
hint: hint,
581+
keepHint: keepHint,
571582
sort: sort,
572583
format: format,
573584
tagBuilder: tagBuilder,
@@ -615,6 +626,7 @@ abstract class FMultiSelect<T> extends StatelessWidget {
615626
..add(ObjectFlagProperty.has('sort', sort))
616627
..add(ObjectFlagProperty.has('tagBuilder', tagBuilder))
617628
..add(StringProperty('forceErrorText', forceErrorText))
629+
..add(FlagProperty('keepHint', value: keepHint, ifTrue: 'keepHint'))
618630
..add(ObjectFlagProperty.has('validator', validator))
619631
..add(EnumProperty('textAlign', textAlign))
620632
..add(EnumProperty('textDirection', textDirection))
@@ -659,6 +671,7 @@ class _BasicSelect<T> extends FMultiSelect<T> {
659671
super.validator,
660672
super.errorBuilder,
661673
super.hint,
674+
super.keepHint,
662675
super.sort,
663676
super.textAlign,
664677
super.textDirection,
@@ -731,6 +744,7 @@ class _SearchSelect<T> extends FMultiSelect<T> {
731744
super.validator,
732745
super.errorBuilder,
733746
super.hint,
747+
super.keepHint,
734748
super.sort,
735749
super.textAlign,
736750
super.textDirection,
29.3 KB
Loading
28 KB
Loading
27.8 KB
Loading
27 KB
Loading

forui/test/src/widgets/autocomplete/autocomplete_test.dart

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,68 @@ void main() {
171171
});
172172
});
173173

174+
group('right arrow completion', () {
175+
testWidgets('right arrow does nothing', (tester) async {
176+
final autocompleteFocus = autoDispose(FocusNode());
177+
final buttonFocus = autoDispose(FocusNode());
178+
179+
await tester.pumpWidget(
180+
TestScaffold.app(
181+
child: Column(
182+
children: [
183+
FAutocomplete(key: key, controller: controller, focusNode: autocompleteFocus, items: fruits),
184+
FButton(onPress: () {}, focusNode: buttonFocus, child: const Text('button')),
185+
],
186+
),
187+
),
188+
);
189+
190+
await tester.enterText(find.byKey(key), 'b');
191+
await tester.pumpAndSettle();
192+
193+
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
194+
await tester.pumpAndSettle();
195+
196+
expect(controller.popover.status.isForwardOrCompleted, true);
197+
expect(autocompleteFocus.hasFocus, true);
198+
expect(buttonFocus.hasFocus, false);
199+
expect(find.text('b'), findsOne);
200+
});
201+
202+
testWidgets('right arrow when completion available completes text', (tester) async {
203+
final autocompleteFocus = autoDispose(FocusNode());
204+
final buttonFocus = autoDispose(FocusNode());
205+
206+
await tester.pumpWidget(
207+
TestScaffold.app(
208+
child: Column(
209+
children: [
210+
FAutocomplete(
211+
key: key,
212+
controller: controller,
213+
focusNode: autocompleteFocus,
214+
rightArrowToComplete: true,
215+
items: fruits,
216+
),
217+
FButton(onPress: () {}, focusNode: buttonFocus, child: const Text('button')),
218+
],
219+
),
220+
),
221+
);
222+
223+
await tester.enterText(find.byKey(key), 'b');
224+
await tester.pumpAndSettle();
225+
226+
await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
227+
await tester.pumpAndSettle();
228+
229+
expect(controller.popover.status.isForwardOrCompleted, false);
230+
expect(autocompleteFocus.hasFocus, true);
231+
expect(buttonFocus.hasFocus, false);
232+
expect(find.text('Banana'), findsOne);
233+
});
234+
});
235+
174236
group('keyboard navigation', () {
175237
testWidgets('arrow key navigation & selection', (tester) async {
176238
final focus = autoDispose(FocusNode());

0 commit comments

Comments
 (0)