Add TapOutsideConfiguration widget (#150125)
This PR adds `TapOutsideConfiguration` where you can define a custom default logic for `onTapOutside`. ```dart TapOutsideConfiguration( behavior: AlwaysUnfocusTapOutsideBehavior(), // behavior: const NeverUnfocusTapOutsideBehavior(), // behavior: const CustomTapOutsideBehavior(), child: Column( children: [ // This TextField will use onTapOutside from TapOutsideConfiguration TextField(), // Of course you can still define onTapOutside TextField(onTapOutside: (event) => print('Tapped outside')), ], ), ) ``` Custom logic can be define like this: ```dart class CustomTapOutsideBehavior extends TapOutsideBehavior { const CustomTapOutsideBehavior(); @override void defaultOnTapOutside(PointerDownEvent event, FocusNode focusNode) { // any custom logic here } } ``` This PR implements also two simple `AlwaysUnfocusTapOutsideBehavior` and `NeverUnfocusTapOutsideBehavior`. Resolves #150123
This commit is contained in:
parent
a9ae0b8bb0
commit
279d30a44f
@ -1495,12 +1495,13 @@ class EditableText extends StatefulWidget {
|
||||
/// Called for each tap that occurs outside of the[TextFieldTapRegion] group
|
||||
/// when the text field is focused.
|
||||
///
|
||||
/// If this is null, [FocusNode.unfocus] will be called on the [focusNode] for
|
||||
/// this text field when a [PointerDownEvent] is received on another part of
|
||||
/// the UI. However, it will not unfocus as a result of mobile application
|
||||
/// touch events (which does not include mouse clicks), to conform with the
|
||||
/// platform conventions. To change this behavior, a callback may be set here
|
||||
/// that operates differently from the default.
|
||||
/// If this is null, [EditableTextTapOutsideIntent] will be invoked. In the
|
||||
/// default implementation, [FocusNode.unfocus] will be called on the
|
||||
/// [focusNode] for this text field when a [PointerDownEvent] is received on
|
||||
/// another part of the UI. However, it will not unfocus as a result of mobile
|
||||
/// application touch events (which does not include mouse clicks), to conform
|
||||
/// with the platform conventions. To change this behavior, a callback may be
|
||||
/// set here or [EditableTextTapOutsideIntent] may be overridden.
|
||||
///
|
||||
/// When adding additional controls to a text field (for example, a spinner, a
|
||||
/// button that copies the selected text, or modifies formatting), it is
|
||||
@ -1529,6 +1530,8 @@ class EditableText extends StatefulWidget {
|
||||
/// See also:
|
||||
///
|
||||
/// * [TapRegion] for how the region group is determined.
|
||||
/// * [EditableTextTapOutsideIntent] for the intent that is invoked if
|
||||
/// this is null.
|
||||
final TapRegionCallback? onTapOutside;
|
||||
|
||||
/// {@template flutter.widgets.editableText.inputFormatters}
|
||||
@ -5104,33 +5107,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
/// The default behavior used if [EditableText.onTapOutside] is null.
|
||||
///
|
||||
/// The `event` argument is the [PointerDownEvent] that caused the notification.
|
||||
void _defaultOnTapOutside(PointerDownEvent event) {
|
||||
/// The focus dropping behavior is only present on desktop platforms
|
||||
/// and mobile browsers.
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.fuchsia:
|
||||
// On mobile platforms, we don't unfocus on touch events unless they're
|
||||
// in the web browser, but we do unfocus for all other kinds of events.
|
||||
switch (event.kind) {
|
||||
case ui.PointerDeviceKind.touch:
|
||||
if (kIsWeb) {
|
||||
widget.focusNode.unfocus();
|
||||
}
|
||||
case ui.PointerDeviceKind.mouse:
|
||||
case ui.PointerDeviceKind.stylus:
|
||||
case ui.PointerDeviceKind.invertedStylus:
|
||||
case ui.PointerDeviceKind.unknown:
|
||||
widget.focusNode.unfocus();
|
||||
case ui.PointerDeviceKind.trackpad:
|
||||
throw UnimplementedError('Unexpected pointer down event for trackpad');
|
||||
}
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.windows:
|
||||
widget.focusNode.unfocus();
|
||||
}
|
||||
void _defaultOnTapOutside(BuildContext context, PointerDownEvent event) {
|
||||
Actions.invoke(context, EditableTextTapOutsideIntent(focusNode: widget.focusNode, pointerDownEvent: event));
|
||||
}
|
||||
|
||||
late final Map<Type, Action<Intent>> _actions = <Type, Action<Intent>>{
|
||||
@ -5169,6 +5147,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
PasteTextIntent: _makeOverridable(CallbackAction<PasteTextIntent>(onInvoke: (PasteTextIntent intent) => pasteText(intent.cause))),
|
||||
|
||||
TransposeCharactersIntent: _makeOverridable(_transposeCharactersAction),
|
||||
EditableTextTapOutsideIntent: _makeOverridable(_EditableTextTapOutsideAction()),
|
||||
};
|
||||
|
||||
@override
|
||||
@ -5186,14 +5165,16 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
return _CompositionCallback(
|
||||
compositeCallback: _compositeCallback,
|
||||
enabled: _hasInputConnection,
|
||||
child: TextFieldTapRegion(
|
||||
child: Actions(
|
||||
actions: _actions,
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return TextFieldTapRegion(
|
||||
groupId: widget.groupId,
|
||||
onTapOutside: _hasFocus ? widget.onTapOutside ?? _defaultOnTapOutside : null,
|
||||
onTapOutside: _hasFocus ? widget.onTapOutside ?? (PointerDownEvent event) => _defaultOnTapOutside(context, event) : null,
|
||||
debugLabel: kReleaseMode ? null : 'EditableText',
|
||||
child: MouseRegion(
|
||||
cursor: widget.mouseCursor ?? SystemMouseCursors.text,
|
||||
child: Actions(
|
||||
actions: _actions,
|
||||
child: UndoHistory<TextEditingValue>(
|
||||
value: widget.controller,
|
||||
onTriggered: (TextEditingValue value) {
|
||||
@ -5330,6 +5311,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -6083,3 +6066,37 @@ class _WebClipboardStatusNotifier extends ClipboardStatusNotifier {
|
||||
return Future<void>.value();
|
||||
}
|
||||
}
|
||||
|
||||
class _EditableTextTapOutsideAction extends ContextAction<EditableTextTapOutsideIntent> {
|
||||
_EditableTextTapOutsideAction();
|
||||
|
||||
@override
|
||||
void invoke(EditableTextTapOutsideIntent intent, [BuildContext? context]) {
|
||||
// The focus dropping behavior is only present on desktop platforms.
|
||||
switch (defaultTargetPlatform) {
|
||||
case TargetPlatform.android:
|
||||
case TargetPlatform.iOS:
|
||||
case TargetPlatform.fuchsia:
|
||||
// On mobile platforms, we don't unfocus on touch events unless they're
|
||||
// in the web browser, but we do unfocus for all other kinds of events.
|
||||
switch (intent.pointerDownEvent.kind) {
|
||||
case ui.PointerDeviceKind.touch:
|
||||
if (kIsWeb) {
|
||||
intent.focusNode.unfocus();
|
||||
}
|
||||
case ui.PointerDeviceKind.mouse:
|
||||
case ui.PointerDeviceKind.stylus:
|
||||
case ui.PointerDeviceKind.invertedStylus:
|
||||
case ui.PointerDeviceKind.unknown:
|
||||
intent.focusNode.unfocus();
|
||||
case ui.PointerDeviceKind.trackpad:
|
||||
throw UnimplementedError(
|
||||
'Unexpected pointer down event for trackpad');
|
||||
}
|
||||
case TargetPlatform.linux:
|
||||
case TargetPlatform.macOS:
|
||||
case TargetPlatform.windows:
|
||||
intent.focusNode.unfocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ library;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'actions.dart';
|
||||
import 'basic.dart';
|
||||
import 'focus_manager.dart';
|
||||
|
||||
/// An [Intent] to send the event straight to the engine.
|
||||
///
|
||||
@ -387,3 +389,26 @@ class TransposeCharactersIntent extends Intent {
|
||||
/// Creates a [TransposeCharactersIntent].
|
||||
const TransposeCharactersIntent();
|
||||
}
|
||||
|
||||
/// An [Intent] that represents a tap outside the field.
|
||||
///
|
||||
/// Invoked when the user taps outside the focused [EditableText] if
|
||||
/// [EditableText.onTapOutside] is null.
|
||||
///
|
||||
/// Override this [Intent] to modify the default behavior, which is to unfocus
|
||||
/// on a touch event on web and do nothing on other platforms.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Action.overridable] for an example on how to make an [Action]
|
||||
/// overridable.
|
||||
class EditableTextTapOutsideIntent extends Intent {
|
||||
/// Creates an [EditableTextTapOutsideIntent].
|
||||
const EditableTextTapOutsideIntent({required this.focusNode, required this.pointerDownEvent});
|
||||
|
||||
/// The [FocusNode] that this [Intent]'s action should be performed on.
|
||||
final FocusNode focusNode;
|
||||
|
||||
/// The [PointerDownEvent] that initiated this [Intent].
|
||||
final PointerDownEvent pointerDownEvent;
|
||||
}
|
||||
|
@ -12791,6 +12791,41 @@ void main() {
|
||||
// On web, using keyboard for selection is handled by the browser.
|
||||
}, skip: kIsWeb); // [intended]
|
||||
|
||||
testWidgets('can change tap outside behavior by overriding actions', (WidgetTester tester) async {
|
||||
bool myIntentWasCalled = false;
|
||||
final CallbackAction<EditableTextTapOutsideIntent> overrideAction = CallbackAction<EditableTextTapOutsideIntent>(
|
||||
onInvoke: (EditableTextTapOutsideIntent intent) { myIntentWasCalled = true; return null; },
|
||||
);
|
||||
final GlobalKey key = GlobalKey();
|
||||
await tester.pumpWidget(MaterialApp(
|
||||
home: Column(
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
key: key,
|
||||
width: 200,
|
||||
height: 200,
|
||||
),
|
||||
Actions(
|
||||
actions: <Type, Action<Intent>>{ EditableTextTapOutsideIntent: overrideAction, },
|
||||
child: EditableText(
|
||||
autofocus: true,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
style: textStyle,
|
||||
cursorColor: Colors.blue,
|
||||
backgroundCursorColor: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byKey(key), warnIfMissed: false);
|
||||
await tester.pumpAndSettle();
|
||||
expect(myIntentWasCalled, isTrue);
|
||||
expect(focusNode.hasFocus, true);
|
||||
});
|
||||
|
||||
testWidgets('ignore key event from web platform', (WidgetTester tester) async {
|
||||
controller.text = 'test\ntest';
|
||||
controller.selection = const TextSelection(
|
||||
|
Loading…
x
Reference in New Issue
Block a user