-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Refactor web text editing shortcuts #103377
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
It looks like this pull request may not have tests. Please make sure to add tests before merging. If you need an exemption to this rule, contact Hixie on the #hackers channel in Chat (don't just cc him here, he won't see it! He's on Discord!). If you are not sure if you need tests, consider this rule of thumb: the purpose of a test is to make sure someone doesn't accidentally revert the fix. Ask yourself, is there anything in your PR that you feel it is important we not accidentally revert back to how it was before your fix? Reviewers: Read the Tree Hygiene page and make sure this patch meets those guidelines before LGTMing. |
77e9481 to
5ef4084
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These shortcuts were ignored because the shortcut is built on top of the DefaultTextEditingShortcuts which prevented these shortcut from bubbling up.
I am not sure if this is the correct behavior, but there are tests to verify the arrow key does not cause scroll in CupertinoTextField, so I remove these shortcuts to maintain the same behavior
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe @Piinks knows? These shortcuts allow scrolling on a normal Scrollable, and I think they were not used by text inputs because the arrow keys move the cursor instead.
justinmc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously, if you wanted to remap ctrl-A to ctrl-B, you could do this on all platforms by using a Shortcuts widget at the root of the app and mapping ctrl-A to DoNothingIntent and ctrl-B to SelectAllTextIntent (see code below). This worked on web as well as other platforms.
With this PR, on web, ctrl-A is correctly blocked but ctrl-B doesn't work because SelectAllTextIntent isn't used anymore in EditableText. Is it possible to still include those Intent to Action mappings?
Code
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
@override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Shortcuts(
shortcuts: {
const SingleActivator(LogicalKeyboardKey.keyB, control: true): const SelectAllTextIntent(SelectionChangedCause.keyboard),
const SingleActivator(LogicalKeyboardKey.keyA, control: true): DoNothingIntent(),
},
child: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: const <Widget>[
TextField(),
],
),
),
),
),
);
}
}
packages/flutter/lib/src/widgets/default_text_editing_shortcuts.dart
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe @Piinks knows? These shortcuts allow scrolling on a normal Scrollable, and I think they were not used by text inputs because the arrow keys move the cursor instead.
|
requirement
|
|
Either I need to annotate the textediting intent that fired from the DefaultTextEditingShortcut so the actions in EditableText know when to ignore these intent if platform is web, or I create a new intent subclass that allows for parent shortcut to override. Either way iseems hacky. I guess it will be easier to keep this as is and add web specific shortcut back to SelectionArea. |
|
I guess the tests are still worth adding |
1d3e0b1 to
f7a7b6e
Compare
f7a7b6e to
75b64b3
Compare
|
I figured out a solution that fix all the requirements |
| expect(copySpy.invoked, isTrue); | ||
| expect(pasteSpy.invoked, isTrue); | ||
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), skip: kIsWeb); // [intended] Web uses a different set of shortcuts. | ||
| }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
web can now handle test editing shortcut as long as the context in not inside TextField
justinmc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my comment with the code sample, I want to make sure I understand this and can reproduce the desired behavior. Then I can make sure the docs are clear about it. Otherwise looks good.
Also, are you familiar with @LongCatIsLooong's Action.overridable? I don't think that it would improve this situation really, but just making sure it's on your radar.
| if (kIsWeb) { | ||
| // On web platform, this shortcuts makes sure the following: | ||
| // | ||
| // 1. Shortcuts fired in TextField are swallowed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"fired in TextField" => "fired when a TextField is focused"
Let me know if that's not what you mean. I think that's more explicit. Same for point 2 below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By "swallowed" do you mean that Flutter ignores them and passes them through to the browser?
| // | ||
| // 1. Shortcuts fired in TextField are swallowed. | ||
| // 2. Shortcuts fired above TextField will still trigger corresponding | ||
| // intents. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which intents?
| /// Creates a [DefaultTextEditingShortcuts] widget that provides the default text editing | ||
| /// shortcuts on the current platform. | ||
| DefaultTextEditingShortcuts({ | ||
| const DefaultTextEditingShortcuts({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's cool that this can be const now.
| // [DoNothingAndStopPropagationTextIntent] will not be handled because the | ||
| // context is above the TextField. The key event will then trigger the | ||
| // default Text Editing Shortcuts. | ||
| result = Shortcuts( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see, it took me a couple of times reading this code before I even realized there are 2 nested Shortcuts widgets now.
Can you explain these two situations in code? So in the app below running the code from this PR, I would expect that after tapping the button, ctrl/cmd - A would invoke SelectAllTextIntent, but it would not reach _SelectAllAction in EditableText due to the focus being outside of EditableText?
Code
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
MyApp({Key? key}) : super(key: key);
static const String _title = 'Flutter Code Sample';
final FocusNode _focusNode = FocusNode();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: _title,
home: Shortcuts(
shortcuts: {
const SingleActivator(LogicalKeyboardKey.keyB, control: true): const SelectAllTextIntent(SelectionChangedCause.keyboard),
const SingleActivator(LogicalKeyboardKey.keyB, meta: true): const SelectAllTextIntent(SelectionChangedCause.keyboard),
//const SingleActivator(LogicalKeyboardKey.keyA, control: true): DoNothingIntent(),
//const SingleActivator(LogicalKeyboardKey.keyA, meta: true): DoNothingIntent(),
},
child: Scaffold(
appBar: AppBar(title: const Text(_title)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Focus(
focusNode: _focusNode,
child: TextButton(
onPressed: () {
_focusNode.requestFocus();
},
child: const Text('Press to focus me'),
),
),
const TextField(),
],
),
),
),
),
);
}
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this situation, in Web ctrl+A will first fire DoNothingAndStopPropagationTextIntent, but no one will handle it since the focus is above the TextField. It will then fire SelectAllTextIntent but still no one will handle it since the focus is above the TextField.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Understood and I verified this behavior on web and Mac with my code example 👍
|
I can't think of a way to solve this using Action.override. Do you have other idea in your mind? These are all the situations I need to cover on web If shortcuts are fired when focusNode is below TextField
If shortcuts are fired when focusNode is above TextField
|
justinmc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just some docs improvements really, otherwise everything looks good to me. Thanks for being patient with my careful code review. I was able to run my demo app that I pasted in my last review and verify all of the behavior you mentioned. I think this is a clean way to do this 👍 .
| Widget build(BuildContext context) { | ||
| Widget result = child; | ||
| if (kIsWeb) { | ||
| // On web platform, these shortcuts makes sure the following: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"On web platform" => "On the web"
"makes sure" => "make sure of"
| // 1. Shortcuts fired when a TextField is focused are ignored and | ||
| // forwarded to the browser. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shortcuts fired when an EditableText is focused are ignored and forwarded to the browser by the EditableText's Actions, because it maps DoNothingAndStopPropagationTextIntent to DoNothingAction.
| // 2. Shortcuts fired when no TextField is focused will still trigger | ||
| // default text editing Shortcuts |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shortcuts fired when no EditableText is focused will still trigger _shortcuts assuming DoNothingAndStopPropagationTextIntent is unhandled elsewhere.
| // In (2), the disabling shortcuts will still fire; however, the | ||
| // [DoNothingAndStopPropagationTextIntent] will not be handled because the | ||
| // context is above the TextField. The key event will then trigger the | ||
| // default Text Editing Shortcuts. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once the above changes have been made to 1 and 2, remove this paragraph?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the docs here will be good with the changes above, but it has the effect of coupling DefaultTextEditingShortcuts and EditableText. Maybe that's not a big deal because the coupling is kind of inherent. Or we could be generic in the comments above to remove the coupling, but maybe it's not worth it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be fine unless we would have other text editing widget that do no use EditableText.
| // [DoNothingAndStopPropagationTextIntent] will not be handled because the | ||
| // context is above the TextField. The key event will then trigger the | ||
| // default Text Editing Shortcuts. | ||
| result = Shortcuts( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Understood and I verified this behavior on web and Mac with my code example 👍
| // context is above the TextField. The key event will then trigger the | ||
| // default Text Editing Shortcuts. | ||
| result = Shortcuts( | ||
| debugLabel: '<Web disabling text shortcuts>', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: "Web Disabling Text Editing Shortcuts"?
Looking at other debugLabels in the codebase, most capitalize all of the first letters like this, but not all...
justinmc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👍
## Description This PRs fixes a Web issue on Linux related to numpad keys. In #145464, I introduced numpad shortcuts for Linux. These shortcuts work well on a desktop Linux application but they broke the Linux+Web numpad logic. When I added these shortcuts, I expected them to not be active on Web (because I knew that on Web, those shortcuts are handled by the browser). But there is a trick: text editing shortcuts are still defined on Web but they are disabled at the editable text level so one can use them in components that are not `EditableText` (see #103377). In this PR, I used the same approach than for other text editing shortcuts: when on web associate those shortcuts to the `DoNothingAndStopPropagationTextIntent` intent. ## Related Issue Fixes #148447. ## Tests Updates 2 tests. Adds 2 tests.
## Description This PRs fixes a Web issue on Linux related to numpad keys. In flutter#145464, I introduced numpad shortcuts for Linux. These shortcuts work well on a desktop Linux application but they broke the Linux+Web numpad logic. When I added these shortcuts, I expected them to not be active on Web (because I knew that on Web, those shortcuts are handled by the browser). But there is a trick: text editing shortcuts are still defined on Web but they are disabled at the editable text level so one can use them in components that are not `EditableText` (see flutter#103377). In this PR, I used the same approach than for other text editing shortcuts: when on web associate those shortcuts to the `DoNothingAndStopPropagationTextIntent` intent. ## Related Issue Fixes flutter#148447. ## Tests Updates 2 tests. Adds 2 tests.
## Description This PRs fixes a Web issue on Linux related to numpad keys. In flutter#145464, I introduced numpad shortcuts for Linux. These shortcuts work well on a desktop Linux application but they broke the Linux+Web numpad logic. When I added these shortcuts, I expected them to not be active on Web (because I knew that on Web, those shortcuts are handled by the browser). But there is a trick: text editing shortcuts are still defined on Web but they are disabled at the editable text level so one can use them in components that are not `EditableText` (see flutter#103377). In this PR, I used the same approach than for other text editing shortcuts: when on web associate those shortcuts to the `DoNothingAndStopPropagationTextIntent` intent. ## Related Issue Fixes flutter#148447. ## Tests Updates 2 tests. Adds 2 tests.
Previously the text editing shortcuts were disabled on web because shortcuts are handled on the platform side for text fields.
This pr changes it that the is update disabled if the focus node is under the Editable Text so that other widgets who want to handle the shortcuts if it is fired above the EditableText
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.