fix: composing in select option text field doesn't work (#4470)

* fix: composing in select option text field doesn't work

* style: improve code style
This commit is contained in:
Richard Shiue 2024-01-25 13:17:13 +08:00 committed by GitHub
parent 3f896ad64f
commit e88fb533c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 123 additions and 134 deletions

View File

@ -11,7 +11,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:textfield_tags/textfield_tags.dart';
import '../../../../grid/presentation/layout/sizes.dart';
import '../../../../grid/presentation/widgets/common/type_option_separator.dart';
@ -32,13 +31,12 @@ class SelectOptionCellEditor extends StatefulWidget {
}
class _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {
final TextEditingController textEditingController = TextEditingController();
final popoverMutex = PopoverMutex();
final tagController = TextfieldTagsController();
@override
void dispose() {
popoverMutex.dispose();
tagController.dispose();
super.dispose();
}
@ -54,14 +52,14 @@ class _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {
mainAxisSize: MainAxisSize.min,
children: [
_TextField(
textEditingController: textEditingController,
popoverMutex: popoverMutex,
tagController: tagController,
),
const TypeOptionSeparator(spacing: 0.0),
Flexible(
child: _OptionList(
textEditingController: textEditingController,
popoverMutex: popoverMutex,
tagController: tagController,
),
),
],
@ -73,12 +71,12 @@ class _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {
}
class _OptionList extends StatelessWidget {
final TextEditingController textEditingController;
final PopoverMutex popoverMutex;
final TextfieldTagsController tagController;
const _OptionList({
required this.textEditingController,
required this.popoverMutex,
required this.tagController,
});
@override
@ -117,23 +115,23 @@ class _OptionList extends StatelessWidget {
}
void onPressedAddButton(BuildContext context) {
final text = tagController.textEditingController?.text;
if (text != null) {
context.read<SelectOptionCellEditorBloc>().add(
SelectOptionEditorEvent.trySelectOption(text),
);
final text = textEditingController.text;
if (text.isNotEmpty) {
context
.read<SelectOptionCellEditorBloc>()
.add(SelectOptionEditorEvent.trySelectOption(text));
}
tagController.textEditingController?.clear();
textEditingController.clear();
}
}
class _TextField extends StatelessWidget {
final TextEditingController textEditingController;
final PopoverMutex popoverMutex;
final TextfieldTagsController tagController;
const _TextField({
required this.textEditingController,
required this.popoverMutex,
required this.tagController,
});
@override
@ -152,7 +150,7 @@ class _TextField extends StatelessWidget {
options: state.options,
selectedOptionMap: optionMap,
distanceToText: _editorPanelWidth * 0.7,
tagController: tagController,
textController: textEditingController,
textSeparators: const [','],
onClick: () => popoverMutex.close(),
newText: (text) {

View File

@ -6,17 +6,16 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:textfield_tags/textfield_tags.dart';
import 'package:flutter/services.dart';
import 'extension.dart';
class SelectOptionTextField extends StatefulWidget {
final TextfieldTagsController tagController;
final List<SelectOptionPB> options;
final LinkedHashMap<String, SelectOptionPB> selectedOptionMap;
final double distanceToText;
final List<String> textSeparators;
final TextEditingController? textController;
final TextEditingController textController;
final Function(String) onSubmitted;
final Function(String) newText;
@ -29,13 +28,12 @@ class SelectOptionTextField extends StatefulWidget {
required this.options,
required this.selectedOptionMap,
required this.distanceToText,
required this.tagController,
required this.onSubmitted,
required this.onPaste,
required this.onRemove,
required this.newText,
required this.textSeparators,
this.textController,
required this.textController,
this.onClick,
});
@ -44,103 +42,105 @@ class SelectOptionTextField extends StatefulWidget {
}
class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
final FocusNode focusNode = FocusNode();
late final TextEditingController controller;
late final FocusNode focusNode;
@override
void initState() {
super.initState();
controller = widget.textController ?? TextEditingController();
focusNode = FocusNode(
onKeyEvent: (node, event) {
if (event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.escape) {
if (!widget.textController.value.composing.isCollapsed) {
final TextRange(:start, :end) =
widget.textController.value.composing;
final text = widget.textController.text;
widget.textController.value = TextEditingValue(
text: "${text.substring(0, start)}${text.substring(end)}",
selection: TextSelection(baseOffset: start, extentOffset: start),
composing: const TextRange(start: -1, end: -1),
);
return KeyEventResult.handled;
}
}
return KeyEventResult.ignored;
},
);
WidgetsBinding.instance.addPostFrameCallback((_) {
focusNode.requestFocus();
});
widget.textController.addListener(_onChanged);
}
@override
void dispose() {
widget.textController.removeListener(_onChanged);
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextFieldTags(
textEditingController: controller,
textfieldTagsController: widget.tagController,
initialTags: widget.selectedOptionMap.keys.toList(),
return TextField(
controller: widget.textController,
focusNode: focusNode,
textSeparators: widget.textSeparators,
inputfieldBuilder: (
BuildContext context,
editController,
focusNode,
error,
onChanged,
onSubmitted,
) {
return ((context, sc, tags, onTagDelegate) {
return TextField(
controller: editController,
focusNode: focusNode,
onTap: widget.onClick,
onChanged: (text) {
if (onChanged != null) {
onChanged(text);
}
_newText(text, editController);
},
onSubmitted: (text) {
if (onSubmitted != null) {
onSubmitted(text);
}
if (text.isNotEmpty) {
widget.onSubmitted(text.trim());
focusNode.requestFocus();
}
},
maxLines: 1,
style: Theme.of(context).textTheme.bodyMedium,
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline,
width: 1.0,
),
borderRadius: Corners.s10Border,
),
isDense: true,
prefixIcon: _renderTags(context, sc),
hintText: LocaleKeys.grid_selectOption_searchOption.tr(),
hintStyle: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: Theme.of(context).hintColor),
prefixIconConstraints:
BoxConstraints(maxWidth: widget.distanceToText),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 1.0,
),
borderRadius: Corners.s10Border,
),
),
);
});
onTap: widget.onClick,
onSubmitted: (text) {
if (text.isNotEmpty) {
widget.onSubmitted(text.trim());
focusNode.requestFocus();
widget.textController.clear();
}
},
maxLines: 1,
style: Theme.of(context).textTheme.bodyMedium,
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outline,
width: 1.0,
),
borderRadius: Corners.s10Border,
),
isDense: true,
prefixIcon: _renderTags(context),
hintText: LocaleKeys.grid_selectOption_searchOption.tr(),
hintStyle: Theme.of(context)
.textTheme
.bodySmall!
.copyWith(color: Theme.of(context).hintColor),
prefixIconConstraints: BoxConstraints(maxWidth: widget.distanceToText),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.primary,
width: 1.0,
),
borderRadius: Corners.s10Border,
),
),
);
}
void _newText(String text, TextEditingController editingController) {
if (text.isEmpty) {
widget.newText('');
void _onChanged() {
if (!widget.textController.value.composing.isCollapsed) {
return;
}
final result = splitInput(text.trimLeft(), widget.textSeparators);
// split input
final (submitted, remainder) = splitInput(
widget.textController.text.trimLeft(),
widget.textSeparators,
);
editingController.text = result[1];
editingController.selection =
TextSelection.collapsed(offset: controller.text.length);
widget.onPaste(result[0], result[1]);
if (submitted.isNotEmpty) {
widget.textController.text = remainder;
widget.textController.selection =
TextSelection.collapsed(offset: widget.textController.text.length);
}
widget.onPaste(submitted, remainder);
}
Widget? _renderTags(BuildContext context, ScrollController sc) {
Widget? _renderTags(BuildContext context) {
if (widget.selectedOptionMap.isEmpty) {
return null;
}
@ -169,7 +169,7 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
},
),
child: SingleChildScrollView(
controller: sc,
controller: ScrollController(),
scrollDirection: Axis.horizontal,
child: Wrap(spacing: 4, children: children),
),
@ -180,7 +180,7 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
}
@visibleForTesting
List splitInput(String input, List<String> textSeparators) {
(List<String>, String) splitInput(String input, List<String> textSeparators) {
final List<String> splits = [];
String currentString = '';
@ -201,5 +201,5 @@ List splitInput(String input, List<String> textSeparators) {
final submittedOptions = splits.sublist(0, splits.length - 1).toList();
final remainder = splits.elementAt(splits.length - 1).trimLeft();
return [submittedOptions, remainder];
return (submittedOptions, remainder);
}

View File

@ -1872,14 +1872,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.5.9"
textfield_tags:
dependency: "direct main"
description:
name: textfield_tags
sha256: c1d215f481e7e8da5c79719825e595db4f829bf1ad3fce4c7ce43d340aa72683
url: "https://pub.dev"
source: hosted
version: "2.0.2"
textstyle_extensions:
dependency: transitive
description:

View File

@ -74,7 +74,6 @@ dependencies:
clipboard: ^0.1.3
connectivity_plus: ^5.0.2
easy_localization: ^3.0.2
textfield_tags: ^2.0.2
device_info_plus: ^9.0.1
fluttertoast: ^8.2.2
json_annotation: ^4.8.1

View File

@ -6,41 +6,43 @@ void main() {
group('split input unit test', () {
test('empty input', () {
List result = splitInput(' ', textSeparators);
expect(result[0], []);
expect(result[1], '');
var (submitted, remainder) = splitInput(' ', textSeparators);
expect(submitted, []);
expect(remainder, '');
result = splitInput(', , , ', textSeparators);
expect(result[0], []);
expect(result[1], '');
(submitted, remainder) = splitInput(', , , ', textSeparators);
expect(submitted, []);
expect(remainder, '');
});
test('simple input', () {
List result = splitInput('exampleTag', textSeparators);
expect(result[0], []);
expect(result[1], 'exampleTag');
var (submitted, remainder) = splitInput('exampleTag', textSeparators);
expect(submitted, []);
expect(remainder, 'exampleTag');
result = splitInput('tag with longer name', textSeparators);
expect(result[0], []);
expect(result[1], 'tag with longer name');
(submitted, remainder) =
splitInput('tag with longer name', textSeparators);
expect(submitted, []);
expect(remainder, 'tag with longer name');
result = splitInput('trailing space ', textSeparators);
expect(result[0], []);
expect(result[1], 'trailing space ');
(submitted, remainder) = splitInput('trailing space ', textSeparators);
expect(submitted, []);
expect(remainder, 'trailing space ');
});
test('input with commas', () {
List result = splitInput('a, b, c', textSeparators);
expect(result[0], ['a', 'b']);
expect(result[1], 'c');
var (submitted, remainder) = splitInput('a, b, c', textSeparators);
expect(submitted, ['a', 'b']);
expect(remainder, 'c');
result = splitInput('a, b, c, ', textSeparators);
expect(result[0], ['a', 'b', 'c']);
expect(result[1], '');
(submitted, remainder) = splitInput('a, b, c, ', textSeparators);
expect(submitted, ['a', 'b', 'c']);
expect(remainder, '');
result = splitInput(',tag 1 ,2nd tag, third tag ', textSeparators);
expect(result[0], ['tag 1', '2nd tag']);
expect(result[1], 'third tag ');
(submitted, remainder) =
splitInput(',tag 1 ,2nd tag, third tag ', textSeparators);
expect(submitted, ['tag 1', '2nd tag']);
expect(remainder, 'third tag ');
});
});
}

View File

@ -4,7 +4,6 @@ import 'package:appflowy/plugins/database/widgets/row/cells/select_option_cell/t
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:textfield_tags/textfield_tags.dart';
import '../bloc_test/grid_test/util.dart';
@ -22,7 +21,6 @@ void main() {
options: const [],
selectedOptionMap: LinkedHashMap<String, SelectOptionPB>(),
distanceToText: 0.0,
tagController: TextfieldTagsController(),
onSubmitted: (text) => submit = text,
onPaste: (options, remaining) {
remainder = remaining;