Skip to content

Commit

Permalink
Disable backspace/delete handling on iOS & macOS (#115900)
Browse files Browse the repository at this point in the history
* Disable backspace/delete handling on iOS

* fix tests

* review

* macOS too

* review
  • Loading branch information
LongCatIsLooong authored Nov 29, 2022
1 parent e669683 commit 24db45e
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ import 'text_editing_intents.dart';
/// lower in the widget tree than this. See the [Action] class for an example
/// of remapping an [Intent] to a custom [Action].
///
/// The [Shortcuts] widget usually takes precedence over system keybindings.
/// Proceed with caution if the shortcut you wish to override is also used by
/// the system. For example, overriding [LogicalKeyboardKey.backspace] could
/// cause CJK input methods to discard more text than they should when the
/// backspace key is pressed during text composition on iOS.
///
/// {@tool snippet}
///
/// This example shows how to use an additional [Shortcuts] widget to override
Expand Down Expand Up @@ -440,13 +446,28 @@ class DefaultTextEditingShortcuts extends StatelessWidget {

static final Map<ShortcutActivator, Intent> _macDisablingTextShortcuts = <ShortcutActivator, Intent>{
..._commonDisablingTextShortcuts,
..._iOSDisablingTextShortcuts,
const SingleActivator(LogicalKeyboardKey.escape): const DoNothingAndStopPropagationTextIntent(),
const SingleActivator(LogicalKeyboardKey.tab): const DoNothingAndStopPropagationTextIntent(),
const SingleActivator(LogicalKeyboardKey.tab, shift: true): const DoNothingAndStopPropagationTextIntent(),
const SingleActivator(LogicalKeyboardKey.arrowDown, shift: true, alt: true): const DoNothingAndStopPropagationTextIntent(),
const SingleActivator(LogicalKeyboardKey.arrowUp, shift: true, alt: true): const DoNothingAndStopPropagationTextIntent(),
};

// Hand backspace/delete events that do not depend on text layout (delete
// character and delete to the next word) back to the IME to allow it to
// update composing text properly.
static const Map<ShortcutActivator, Intent> _iOSDisablingTextShortcuts = <ShortcutActivator, Intent>{
SingleActivator(LogicalKeyboardKey.backspace): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.backspace, shift: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.delete): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.delete, shift: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.backspace, alt: true, shift: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.backspace, alt: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.delete, alt: true, shift: true): DoNothingAndStopPropagationTextIntent(),
SingleActivator(LogicalKeyboardKey.delete, alt: true): DoNothingAndStopPropagationTextIntent(),
};

static Map<ShortcutActivator, Intent> get _shortcuts {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
Expand All @@ -469,13 +490,13 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
return _webDisablingTextShortcuts;
}
switch (defaultTargetPlatform) {

case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.iOS:
case TargetPlatform.linux:
case TargetPlatform.windows:
return null;
case TargetPlatform.iOS:
return _iOSDisablingTextShortcuts;
case TargetPlatform.macOS:
return _macDisablingTextShortcuts;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,87 @@ void main() {
),
);
}

group('iOS: do not delete/backspace events', () {
final TargetPlatformVariant iOS = TargetPlatformVariant.only(TargetPlatform.iOS);
final FocusNode editable = FocusNode();
final FocusNode spy = FocusNode();

testWidgets('backspace with and without word modifier', (WidgetTester tester) async {
tester.binding.testTextInput.unregister();
addTearDown(tester.binding.testTextInput.register);

await tester.pumpWidget(
buildSpyAboveEditableText(
editableFocusNode: editable,
spyFocusNode: spy,
),
);
editable.requestFocus();
await tester.pump();
final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));

for (int altShiftState = 0; altShiftState < 1 << 2; altShiftState += 1) {
final bool alt = altShiftState & 0x1 != 0;
final bool shift = altShiftState & 0x2 != 0;
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.backspace, alt: alt, shift: shift));
}
await tester.pump();

expect(state.lastIntent, isNull);
}, variant: iOS);

testWidgets('delete with and without word modifier', (WidgetTester tester) async {
tester.binding.testTextInput.unregister();
addTearDown(tester.binding.testTextInput.register);

await tester.pumpWidget(
buildSpyAboveEditableText(
editableFocusNode: editable,
spyFocusNode: spy,
),
);
editable.requestFocus();
await tester.pump();
final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));

for (int altShiftState = 0; altShiftState < 1 << 2; altShiftState += 1) {
final bool alt = altShiftState & 0x1 != 0;
final bool shift = altShiftState & 0x2 != 0;
await sendKeyCombination(tester, SingleActivator(LogicalKeyboardKey.delete, alt: alt, shift: shift));
}
await tester.pump();

expect(state.lastIntent, isNull);
}, variant: iOS);

testWidgets('Exception: deleting to line boundary is handled by the framework', (WidgetTester tester) async {
tester.binding.testTextInput.unregister();
addTearDown(tester.binding.testTextInput.register);

await tester.pumpWidget(
buildSpyAboveEditableText(
editableFocusNode: editable,
spyFocusNode: spy,
),
);
editable.requestFocus();
await tester.pump();
final ActionSpyState state = tester.state<ActionSpyState>(find.byType(ActionSpy));

for (int keyState = 0; keyState < 1 << 2; keyState += 1) {
final bool shift = keyState & 0x1 != 0;
final LogicalKeyboardKey key = keyState & 0x2 != 0 ? LogicalKeyboardKey.delete : LogicalKeyboardKey.backspace;

state.lastIntent = null;
final SingleActivator activator = SingleActivator(key, meta: true, shift: shift);
await sendKeyCombination(tester, activator);
await tester.pump();
expect(state.lastIntent, isA<DeleteToLineBreakIntent>(), reason: '$activator');
}
}, variant: iOS);
}, skip: kIsWeb); // [intended] specific tests target non-web.

group('macOS does not accept shortcuts if focus under EditableText', () {
final TargetPlatformVariant macOSOnly = TargetPlatformVariant.only(TargetPlatform.macOS);

Expand Down Expand Up @@ -400,6 +481,10 @@ class ActionSpyState extends State<ActionSpy> {
ExtendSelectionVerticallyToAdjacentLineIntent: CallbackAction<ExtendSelectionVerticallyToAdjacentLineIntent>(onInvoke: _captureIntent),
ExtendSelectionToDocumentBoundaryIntent: CallbackAction<ExtendSelectionToDocumentBoundaryIntent>(onInvoke: _captureIntent),
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: CallbackAction<ExtendSelectionToNextWordBoundaryOrCaretLocationIntent>(onInvoke: _captureIntent),

DeleteToLineBreakIntent: CallbackAction<DeleteToLineBreakIntent>(onInvoke: _captureIntent),
DeleteToNextWordBoundaryIntent: CallbackAction<DeleteToNextWordBoundaryIntent>(onInvoke: _captureIntent),
DeleteCharacterIntent: CallbackAction<DeleteCharacterIntent>(onInvoke: _captureIntent),
};

// ignore: use_setters_to_change_properties
Expand Down
42 changes: 21 additions & 21 deletions packages/flutter/test/widgets/editable_text_shortcuts_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 19),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));

testWidgets('backspace readonly', (WidgetTester tester) async {
controller.text = testText;
Expand Down Expand Up @@ -215,7 +215,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 71),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));

testWidgets('backspace inside of a cluster', (WidgetTester tester) async {
controller.text = testCluster;
Expand All @@ -236,7 +236,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 0),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));

testWidgets('backspace at cluster boundary', (WidgetTester tester) async {
controller.text = testCluster;
Expand All @@ -257,7 +257,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 0),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
});

group('delete: ', () {
Expand Down Expand Up @@ -287,7 +287,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 20),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));

testWidgets('delete readonly', (WidgetTester tester) async {
controller.text = testText;
Expand All @@ -305,7 +305,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 20, affinity: TextAffinity.upstream),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));

testWidgets('delete at start', (WidgetTester tester) async {
controller.text = testText;
Expand All @@ -328,7 +328,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 0),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));

testWidgets('delete at end', (WidgetTester tester) async {
controller.text = testText;
Expand Down Expand Up @@ -373,7 +373,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 0),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));

testWidgets('delete at cluster boundary', (WidgetTester tester) async {
controller.text = testCluster;
Expand All @@ -394,7 +394,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 8),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
});

group('Non-collapsed delete', () {
Expand All @@ -420,7 +420,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 8),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));

testWidgets('at the boundaries of a cluster', (WidgetTester tester) async {
controller.text = testCluster;
Expand All @@ -441,7 +441,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 8),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));

testWidgets('cross-cluster', (WidgetTester tester) async {
controller.text = testCluster;
Expand All @@ -462,7 +462,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 0),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));

testWidgets('cross-cluster obscured text', (WidgetTester tester) async {
controller.text = testCluster;
Expand All @@ -483,7 +483,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 1),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
});

group('word modifier + backspace', () {
Expand Down Expand Up @@ -516,7 +516,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 24),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));

testWidgets('readonly', (WidgetTester tester) async {
controller.text = testText;
Expand Down Expand Up @@ -581,7 +581,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 71),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));

testWidgets('inside of a cluster', (WidgetTester tester) async {
controller.text = testCluster;
Expand All @@ -602,7 +602,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 0),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));

testWidgets('at cluster boundary', (WidgetTester tester) async {
controller.text = testCluster;
Expand All @@ -623,7 +623,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 0),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
});

group('word modifier + delete', () {
Expand Down Expand Up @@ -656,7 +656,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 23),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));

testWidgets('readonly', (WidgetTester tester) async {
controller.text = testText;
Expand Down Expand Up @@ -697,7 +697,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 0),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));

testWidgets('at end', (WidgetTester tester) async {
controller.text = testText;
Expand Down Expand Up @@ -735,7 +735,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 0),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));

testWidgets('at cluster boundary', (WidgetTester tester) async {
controller.text = testCluster;
Expand All @@ -756,7 +756,7 @@ void main() {
controller.selection,
const TextSelection.collapsed(offset: 8),
);
}, variant: TargetPlatformVariant.all());
}, variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS }));
});

group('line modifier + backspace', () {
Expand Down
Loading

0 comments on commit 24db45e

Please sign in to comment.