Add action for configuring default action of EditableText.onTapUpOutside (#162575)

This PR adds an `Action` for configuring a default action of
`EditableText.onTapUpOutside`. This is the equivalent to what
https://github.com/flutter/flutter/pull/150125 did for
`EditableText.onTapOutside`.

Fixes https://github.com/flutter/flutter/issues/162212

## 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.

<!-- 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: Victor Sanni <victorsanniay@gmail.com>
This commit is contained in:
Hannes Hultergård
2025-02-28 20:01:33 +01:00
committed by GitHub
parent 845c7779b8
commit 6958d086bc
5 changed files with 269 additions and 1 deletions

View File

@@ -0,0 +1,117 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
/// Flutter code sample for [EditableTextTapUpOutsideIntent].
void main() {
runApp(const SampleApp());
}
class SampleApp extends StatelessWidget {
const SampleApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: EditableTextTapUpOutsideIntentExample());
}
}
class EditableTextTapUpOutsideIntentExample extends StatefulWidget {
const EditableTextTapUpOutsideIntentExample({super.key});
@override
State<EditableTextTapUpOutsideIntentExample> createState() =>
_EditableTextTapUpOutsideIntentExampleState();
}
class _EditableTextTapUpOutsideIntentExampleState
extends State<EditableTextTapUpOutsideIntentExample> {
PointerDownEvent? latestPointerDownEvent;
void _handlePointerDown(EditableTextTapOutsideIntent intent) {
// Store the latest pointer down event to calculate the distance between
// the pointer down and pointer up events later.
latestPointerDownEvent = intent.pointerDownEvent;
// Match the default behavior of unfocusing on tap down on desktop platforms
// and on mobile web. Additionally, save the latest pointer down event to
// on non-web mobile platforms to calculate the distance between the pointer
// down and pointer up events later.
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();
} else {
// Store the latest pointer down event to calculate the distance
// between the pointer down and pointer up events later.
latestPointerDownEvent = intent.pointerDownEvent;
}
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();
}
}
void _handlePointerUp(EditableTextTapUpOutsideIntent intent) {
if (latestPointerDownEvent == null) {
return;
}
final double distance =
(latestPointerDownEvent!.position - intent.pointerUpEvent.position).distance;
// Unfocus on taps but not scrolls.
// kTouchSlop is a framework constant that is used to determine if a
// pointer event is a tap or a scroll.
if (distance < kTouchSlop) {
intent.focusNode.unfocus();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(20),
child: Actions(
actions: <Type, Action<Intent>>{
EditableTextTapOutsideIntent: CallbackAction<EditableTextTapOutsideIntent>(
onInvoke: _handlePointerDown,
),
EditableTextTapUpOutsideIntent: CallbackAction<EditableTextTapUpOutsideIntent>(
onInvoke: _handlePointerUp,
),
},
child: ListView(
children: <Widget>[
TextField(focusNode: FocusNode()),
...List<Widget>.generate(50, (int index) => Text('Item $index')),
],
),
),
),
);
}
}

View File

@@ -0,0 +1,50 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_api_samples/widgets/text_editing_intents/editable_text_tap_up_outside_intent.0.dart'
as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('Unfocuses TextField on tap', (WidgetTester tester) async {
await tester.pumpWidget(const example.SampleApp());
final Finder finder = find.byType(TextField);
final TextField textField = tester.firstWidget(finder);
await tester.tap(finder);
await tester.pump();
expect(textField.focusNode!.hasFocus, true);
// Tap the center of the Scaffold, outside the TextField.
await tester.tap(find.byType(Scaffold));
await tester.pump();
expect(textField.focusNode!.hasFocus, false);
});
testWidgets('Does not unfocus TextField on scroll', (WidgetTester tester) async {
await tester.pumpWidget(const example.SampleApp());
final Finder finder = find.byType(TextField);
final TextField textField = tester.firstWidget(finder);
await tester.tap(finder);
await tester.pump();
expect(textField.focusNode!.hasFocus, true);
// Tap the center of the Scaffold, outside the TextField.
await tester.drag(find.byType(Scaffold), const Offset(0.0, -100.0));
await tester.pump();
expect(textField.focusNode!.hasFocus, true);
});
}