mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[Web][Engine] Fix composingBaseOffset and composingExtentOffset value when input japanese text (#161593)
fix https://github.com/flutter/flutter/issues/159671 When entering Japanese text and operating `shift + ← || → || ↑ || ↓` while composing a character, `setSelectionRange` set (0,0) and the composing text is disappeared. For this reason, disable shit + arrow text shortcuts on web platform. ### Movie fixed https://github.com/user-attachments/assets/ad0bd199-92a5-4e1f-9f26-0c23981c013d master branch https://github.com/user-attachments/assets/934f256e-189b-4916-bb91-a49be60f17b3 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Mouad Debbar <mouad.debbar@gmail.com>
This commit is contained in:
parent
cf007b91d3
commit
03dbf1a99c
@ -47,6 +47,11 @@ mixin CompositionAwareMixin {
|
||||
/// so it is safe to reference it to get the current composingText.
|
||||
String? composingText;
|
||||
|
||||
/// The base offset of the composing text in the `InputElement` or `TextAreaElement`.
|
||||
///
|
||||
/// Will be null if composing just started, ended, or no composing is being done.
|
||||
int? composingBase;
|
||||
|
||||
void addCompositionEventHandlers(DomHTMLElement domElement) {
|
||||
domElement.addEventListener(_kCompositionStart, _compositionStartListener);
|
||||
domElement.addEventListener(_kCompositionUpdate, _compositionUpdateListener);
|
||||
@ -61,6 +66,7 @@ mixin CompositionAwareMixin {
|
||||
|
||||
void _handleCompositionStart(DomEvent event) {
|
||||
composingText = null;
|
||||
composingBase = null;
|
||||
}
|
||||
|
||||
void _handleCompositionUpdate(DomEvent event) {
|
||||
@ -71,6 +77,7 @@ mixin CompositionAwareMixin {
|
||||
|
||||
void _handleCompositionEnd(DomEvent event) {
|
||||
composingText = null;
|
||||
composingBase = null;
|
||||
}
|
||||
|
||||
EditingState determineCompositionState(EditingState editingState) {
|
||||
@ -78,15 +85,14 @@ mixin CompositionAwareMixin {
|
||||
return editingState;
|
||||
}
|
||||
|
||||
final int composingBase = editingState.extentOffset - composingText!.length;
|
||||
|
||||
if (composingBase < 0) {
|
||||
composingBase ??= editingState.extentOffset - composingText!.length;
|
||||
if (composingBase! < 0) {
|
||||
return editingState;
|
||||
}
|
||||
|
||||
return editingState.copyWith(
|
||||
composingBaseOffset: composingBase,
|
||||
composingExtentOffset: composingBase + composingText!.length,
|
||||
composingExtentOffset: composingBase! + composingText!.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,6 +210,35 @@ Future<void> testMain() async {
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('should retain composing base offset if composing text area is changed', () {
|
||||
const String composingText = '今日は寒い日です';
|
||||
|
||||
EditingState editingState = EditingState(text: '今日は寒い日です', baseOffset: 0, extentOffset: 8);
|
||||
|
||||
final _MockWithCompositionAwareMixin mockWithCompositionAwareMixin =
|
||||
_MockWithCompositionAwareMixin();
|
||||
mockWithCompositionAwareMixin.composingText = composingText;
|
||||
|
||||
expect(
|
||||
mockWithCompositionAwareMixin.determineCompositionState(editingState),
|
||||
editingState.copyWith(composingBaseOffset: 0, composingExtentOffset: 8),
|
||||
);
|
||||
|
||||
editingState = editingState.copyWith(baseOffset: 0, extentOffset: 3);
|
||||
|
||||
expect(
|
||||
mockWithCompositionAwareMixin.determineCompositionState(editingState),
|
||||
editingState.copyWith(composingBaseOffset: 0, composingExtentOffset: 8),
|
||||
);
|
||||
|
||||
editingState = editingState.copyWith(baseOffset: 3, extentOffset: 6);
|
||||
|
||||
expect(
|
||||
mockWithCompositionAwareMixin.determineCompositionState(editingState),
|
||||
editingState.copyWith(composingBaseOffset: 0, composingExtentOffset: 8),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -5574,7 +5574,10 @@ class EditableTextState extends State<EditableText>
|
||||
),
|
||||
),
|
||||
ScrollToDocumentBoundaryIntent: _makeOverridable(
|
||||
CallbackAction<ScrollToDocumentBoundaryIntent>(onInvoke: _scrollToDocumentBoundary),
|
||||
_WebComposingDisablingCallbackAction<ScrollToDocumentBoundaryIntent>(
|
||||
this,
|
||||
onInvoke: _scrollToDocumentBoundary,
|
||||
),
|
||||
),
|
||||
ScrollIntent: CallbackAction<ScrollIntent>(onInvoke: _scroll),
|
||||
|
||||
@ -6482,7 +6485,13 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent>
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isActionEnabled => state._value.selection.isValid;
|
||||
bool get isActionEnabled {
|
||||
if (kIsWeb && state.widget.selectionEnabled && state._value.composing.isValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return state._value.selection.isValid;
|
||||
}
|
||||
}
|
||||
|
||||
class _UpdateTextSelectionVerticallyAction<T extends DirectionalCaretMovementIntent>
|
||||
@ -6562,7 +6571,28 @@ class _UpdateTextSelectionVerticallyAction<T extends DirectionalCaretMovementInt
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isActionEnabled => state._value.selection.isValid;
|
||||
bool get isActionEnabled {
|
||||
if (kIsWeb && state.widget.selectionEnabled && state._value.composing.isValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return state._value.selection.isValid;
|
||||
}
|
||||
}
|
||||
|
||||
class _WebComposingDisablingCallbackAction<T extends Intent> extends CallbackAction<T> {
|
||||
_WebComposingDisablingCallbackAction(this.state, {required super.onInvoke});
|
||||
|
||||
final EditableTextState state;
|
||||
|
||||
@override
|
||||
bool get isActionEnabled {
|
||||
if (kIsWeb && state.widget.selectionEnabled && state._value.composing.isValid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return super.isActionEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
class _SelectAllAction extends ContextAction<SelectAllTextIntent> {
|
||||
|
||||
@ -2864,4 +2864,163 @@ void main() {
|
||||
);
|
||||
});
|
||||
}, skip: !kIsWeb); // [intended] specific tests target web.
|
||||
|
||||
group(
|
||||
'Web does not accept',
|
||||
() {
|
||||
testWidgets('character modifier + arrowLeft in composing', (WidgetTester tester) async {
|
||||
const SingleActivator arrowLeft = SingleActivator(
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
shift: true,
|
||||
);
|
||||
|
||||
controller.value = const TextEditingValue(
|
||||
text: testText,
|
||||
selection: TextSelection(baseOffset: 0, extentOffset: 3),
|
||||
composing: TextRange(start: 0, end: 3),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(buildEditableText(style: const TextStyle(fontSize: 12)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await sendKeyCombination(tester, arrowLeft);
|
||||
await tester.pump();
|
||||
|
||||
// selection should not change.
|
||||
expect(controller.text, testText);
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection(baseOffset: 0, extentOffset: 3),
|
||||
reason: arrowLeft.toString(),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('character modifier + arrowRight in composing', (WidgetTester tester) async {
|
||||
const SingleActivator arrowRight = SingleActivator(
|
||||
LogicalKeyboardKey.arrowLeft,
|
||||
shift: true,
|
||||
);
|
||||
|
||||
controller.value = const TextEditingValue(
|
||||
text: testText,
|
||||
selection: TextSelection(baseOffset: 0, extentOffset: 3),
|
||||
composing: TextRange(start: 0, end: 3),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(buildEditableText(style: const TextStyle(fontSize: 12)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await sendKeyCombination(tester, arrowRight);
|
||||
await tester.pump();
|
||||
|
||||
// selection should not change.
|
||||
expect(controller.text, testText);
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection(baseOffset: 0, extentOffset: 3),
|
||||
reason: arrowRight.toString(),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('character modifier + arrowUp in composing', (WidgetTester tester) async {
|
||||
const SingleActivator arrowUp = SingleActivator(LogicalKeyboardKey.arrowUp, shift: true);
|
||||
|
||||
controller.value = const TextEditingValue(
|
||||
text: testText,
|
||||
selection: TextSelection(baseOffset: 0, extentOffset: 3),
|
||||
composing: TextRange(start: 0, end: 3),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(buildEditableText(style: const TextStyle(fontSize: 12)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await sendKeyCombination(tester, arrowUp);
|
||||
await tester.pump();
|
||||
|
||||
// selection should not change.
|
||||
expect(controller.text, testText);
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection(baseOffset: 0, extentOffset: 3),
|
||||
reason: arrowUp.toString(),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('character modifier + arrowDown in composing', (WidgetTester tester) async {
|
||||
const SingleActivator arrowDown = SingleActivator(
|
||||
LogicalKeyboardKey.arrowDown,
|
||||
shift: true,
|
||||
);
|
||||
|
||||
controller.value = const TextEditingValue(
|
||||
text: testText,
|
||||
selection: TextSelection(baseOffset: 0, extentOffset: 3),
|
||||
composing: TextRange(start: 0, end: 3),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(buildEditableText(style: const TextStyle(fontSize: 12)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await sendKeyCombination(tester, arrowDown);
|
||||
await tester.pump();
|
||||
|
||||
// selection should not change.
|
||||
expect(controller.text, testText);
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection(baseOffset: 0, extentOffset: 3),
|
||||
reason: arrowDown.toString(),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('home in composing', (WidgetTester tester) async {
|
||||
const SingleActivator home = SingleActivator(LogicalKeyboardKey.home);
|
||||
|
||||
controller.value = const TextEditingValue(
|
||||
text: testText,
|
||||
selection: TextSelection(baseOffset: 0, extentOffset: 3),
|
||||
composing: TextRange(start: 0, end: 3),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(buildEditableText(style: const TextStyle(fontSize: 12)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await sendKeyCombination(tester, home);
|
||||
await tester.pump();
|
||||
|
||||
// selection should not change.
|
||||
expect(controller.text, testText);
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection(baseOffset: 0, extentOffset: 3),
|
||||
reason: home.toString(),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('end in composing', (WidgetTester tester) async {
|
||||
const SingleActivator end = SingleActivator(LogicalKeyboardKey.end);
|
||||
|
||||
controller.value = const TextEditingValue(
|
||||
text: testText,
|
||||
selection: TextSelection(baseOffset: 0, extentOffset: 3),
|
||||
composing: TextRange(start: 0, end: 3),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(buildEditableText(style: const TextStyle(fontSize: 12)));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await sendKeyCombination(tester, end);
|
||||
await tester.pump();
|
||||
|
||||
// selection should not change.
|
||||
expect(controller.text, testText);
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection(baseOffset: 0, extentOffset: 3),
|
||||
reason: end.toString(),
|
||||
);
|
||||
});
|
||||
},
|
||||
skip: !kIsWeb, // [intended] specific tests target web.
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user