iOS tap handling on CupertinoTextField (#24034)
This commit is contained in:
parent
0155ee7157
commit
988bfc166d
@ -1,7 +1,9 @@
|
||||
// Copyright 2018 The Chromium 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:async';
|
||||
|
||||
import 'package:flutter/gestures.dart' show kDoubleTapTimeout, kDoubleTapSlop;
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
@ -418,6 +420,13 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
||||
FocusNode _focusNode;
|
||||
FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode());
|
||||
|
||||
// Is shortly after a previous single tap when not null.
|
||||
Timer _doubleTapTimer;
|
||||
Offset _lastTapOffset;
|
||||
// True if second tap down of a double tap is detected. Used to discard
|
||||
// subsequent tap up / tap hold of the same tap.
|
||||
bool _isDoubleTap = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@ -447,6 +456,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
||||
void dispose() {
|
||||
_focusNode?.dispose();
|
||||
_controller?.removeListener(updateKeepAlive);
|
||||
_doubleTapTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -456,17 +466,54 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
||||
|
||||
RenderEditable get _renderEditable => _editableTextKey.currentState.renderEditable;
|
||||
|
||||
// The down handler is force-run on success of a single tap and optimistically
|
||||
// run before a long press success.
|
||||
void _handleTapDown(TapDownDetails details) {
|
||||
_renderEditable.handleTapDown(details);
|
||||
// This isn't detected as a double tap gesture in the gesture recognizer
|
||||
// because it's 2 single taps, each of which may do different things depending
|
||||
// on whether it's a single tap, the first tap of a double tap, the second
|
||||
// tap held down, a clean double tap etc.
|
||||
if (_doubleTapTimer != null && _isWithinDoubleTapTolerance(details.globalPosition)) {
|
||||
// If there was already a previous tap, the second down hold/tap is a
|
||||
// double tap.
|
||||
_renderEditable.selectWord(cause: SelectionChangedCause.doubleTap);
|
||||
_doubleTapTimer.cancel();
|
||||
_doubleTapTimeout();
|
||||
_isDoubleTap = true;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTap() {
|
||||
_renderEditable.handleTap();
|
||||
_requestKeyboard();
|
||||
void _handleTapUp(TapUpDetails details) {
|
||||
if (!_isDoubleTap) {
|
||||
_renderEditable.selectWordEdge(cause: SelectionChangedCause.tap);
|
||||
_lastTapOffset = details.globalPosition;
|
||||
_doubleTapTimer = Timer(kDoubleTapTimeout, _doubleTapTimeout);
|
||||
_requestKeyboard();
|
||||
}
|
||||
_isDoubleTap = false;
|
||||
}
|
||||
|
||||
void _handleLongPress() {
|
||||
_renderEditable.handleLongPress();
|
||||
if (!_isDoubleTap) {
|
||||
_renderEditable.selectPosition(cause: SelectionChangedCause.longPress);
|
||||
}
|
||||
_isDoubleTap = false;
|
||||
}
|
||||
|
||||
void _doubleTapTimeout() {
|
||||
_doubleTapTimer = null;
|
||||
_lastTapOffset = null;
|
||||
}
|
||||
|
||||
bool _isWithinDoubleTapTolerance(Offset secondTapOffset) {
|
||||
assert(secondTapOffset != null);
|
||||
if (_lastTapOffset == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Offset difference = secondTapOffset - _lastTapOffset;
|
||||
return difference.distance <= kDoubleTapSlop;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -648,7 +695,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTapDown: _handleTapDown,
|
||||
onTap: _handleTap,
|
||||
onTapUp: _handleTapUp,
|
||||
onLongPress: _handleLongPress,
|
||||
excludeFromSemantics: true,
|
||||
child: _addTextDependentAttachments(paddedEditable),
|
||||
|
@ -32,6 +32,10 @@ enum SelectionChangedCause {
|
||||
/// of the cursor) to change.
|
||||
tap,
|
||||
|
||||
/// The user tapped twice in quick succession on the text and that caused
|
||||
/// the selection (or the location of the cursor) to change.
|
||||
doubleTap,
|
||||
|
||||
/// The user long-pressed the text and that caused the selection (or the
|
||||
/// location of the cursor) to change.
|
||||
longPress,
|
||||
@ -190,7 +194,7 @@ class RenderEditable extends RenderBox {
|
||||
|
||||
/// If true [handleEvent] does nothing and it's assumed that this
|
||||
/// renderer will be notified of input gestures via [handleTapDown],
|
||||
/// [handleTap], and [handleLongPress].
|
||||
/// [handleTap], [handleDoubleTap], and [handleLongPress].
|
||||
///
|
||||
/// The default value of this property is false.
|
||||
bool ignorePointer;
|
||||
@ -1081,18 +1085,23 @@ class RenderEditable extends RenderBox {
|
||||
/// When [ignorePointer] is true, an ancestor widget must respond to tap
|
||||
/// events by calling this method.
|
||||
void handleTap() {
|
||||
_layoutText(constraints.maxWidth);
|
||||
assert(_lastTapDownPosition != null);
|
||||
if (onSelectionChanged != null) {
|
||||
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition));
|
||||
onSelectionChanged(TextSelection.fromPosition(position), this, SelectionChangedCause.tap);
|
||||
}
|
||||
selectPosition(cause: SelectionChangedCause.tap);
|
||||
}
|
||||
void _handleTap() {
|
||||
assert(!ignorePointer);
|
||||
handleTap();
|
||||
}
|
||||
|
||||
/// If [ignorePointer] is false (the default) then this method is called by
|
||||
/// the internal gesture recognizer's [DoubleTapGestureRecognizer.onDoubleTap]
|
||||
/// callback.
|
||||
///
|
||||
/// When [ignorePointer] is true, an ancestor widget must respond to double
|
||||
/// tap events by calling this method.
|
||||
void handleDoubleTap() {
|
||||
selectWord(cause: SelectionChangedCause.doubleTap);
|
||||
}
|
||||
|
||||
/// If [ignorePointer] is false (the default) then this method is called by
|
||||
/// the internal gesture recognizer's [LongPressRecognizer.onLongPress]
|
||||
/// callback.
|
||||
@ -1100,18 +1109,59 @@ class RenderEditable extends RenderBox {
|
||||
/// When [ignorePointer] is true, an ancestor widget must respond to long
|
||||
/// press events by calling this method.
|
||||
void handleLongPress() {
|
||||
_layoutText(constraints.maxWidth);
|
||||
assert(_lastTapDownPosition != null);
|
||||
if (onSelectionChanged != null) {
|
||||
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition));
|
||||
onSelectionChanged(_selectWordAtOffset(position), this, SelectionChangedCause.longPress);
|
||||
}
|
||||
selectWord(cause: SelectionChangedCause.longPress);
|
||||
}
|
||||
void _handleLongPress() {
|
||||
assert(!ignorePointer);
|
||||
handleLongPress();
|
||||
}
|
||||
|
||||
/// Move selection to the location of the last tap down.
|
||||
void selectPosition({@required SelectionChangedCause cause}) {
|
||||
assert(cause != null);
|
||||
_layoutText(constraints.maxWidth);
|
||||
assert(_lastTapDownPosition != null);
|
||||
if (onSelectionChanged != null) {
|
||||
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition));
|
||||
onSelectionChanged(TextSelection.fromPosition(position), this, cause);
|
||||
}
|
||||
}
|
||||
|
||||
/// Select a word around the location of the last tap down.
|
||||
void selectWord({@required SelectionChangedCause cause}) {
|
||||
assert(cause != null);
|
||||
_layoutText(constraints.maxWidth);
|
||||
assert(_lastTapDownPosition != null);
|
||||
if (onSelectionChanged != null) {
|
||||
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition));
|
||||
onSelectionChanged(_selectWordAtOffset(position), this, cause);
|
||||
}
|
||||
}
|
||||
|
||||
/// Move the selection to the beginning or end of a word.
|
||||
void selectWordEdge({@required SelectionChangedCause cause}) {
|
||||
assert(cause != null);
|
||||
_layoutText(constraints.maxWidth);
|
||||
assert(_lastTapDownPosition != null);
|
||||
if (onSelectionChanged != null) {
|
||||
final TextPosition position = _textPainter.getPositionForOffset(globalToLocal(_lastTapDownPosition));
|
||||
final TextRange word = _textPainter.getWordBoundary(position);
|
||||
if (position.offset - word.start <= 1) {
|
||||
onSelectionChanged(
|
||||
TextSelection.collapsed(offset: word.start, affinity: TextAffinity.downstream),
|
||||
this,
|
||||
cause,
|
||||
);
|
||||
} else {
|
||||
onSelectionChanged(
|
||||
TextSelection.collapsed(offset: word.end, affinity: TextAffinity.upstream),
|
||||
this,
|
||||
cause,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextSelection _selectWordAtOffset(TextPosition position) {
|
||||
assert(_textLayoutLastWidth == constraints.maxWidth);
|
||||
final TextRange word = _textPainter.getWordBoundary(position);
|
||||
|
@ -740,7 +740,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
final bool longPress = cause == SelectionChangedCause.longPress;
|
||||
if (cause != SelectionChangedCause.keyboard && (_value.text.isNotEmpty || longPress))
|
||||
_selectionOverlay.showHandles();
|
||||
if (longPress)
|
||||
if (longPress || cause == SelectionChangedCause.doubleTap)
|
||||
_selectionOverlay.showToolbar();
|
||||
if (widget.onSelectionChanged != null)
|
||||
widget.onSelectionChanged(selection, cause);
|
||||
|
@ -685,4 +685,417 @@ void main() {
|
||||
expect(find.text("j'aime la poutine"), findsOneWidget);
|
||||
expect(find.text('field 2'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'tap moves cursor to the edge of the word it tapped on',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
|
||||
|
||||
await tester.tapAt(textfieldStart + const Offset(50.0, 5.0));
|
||||
await tester.pump();
|
||||
|
||||
// We moved the cursor.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
|
||||
);
|
||||
|
||||
// But don't trigger the toolbar.
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'slow double tap does not trigger double tap',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
|
||||
|
||||
await tester.tapAt(textfieldStart + const Offset(50.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.tapAt(textfieldStart + const Offset(50.0, 5.0));
|
||||
await tester.pump();
|
||||
|
||||
// Plain collapsed selection.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
|
||||
);
|
||||
|
||||
// No toolbar.
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'double tap selects word and first tap of double tap moves cursor',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
|
||||
|
||||
await tester.tapAt(textfieldStart + const Offset(50.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
// First tap moved the cursor.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
|
||||
);
|
||||
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
||||
await tester.pump();
|
||||
|
||||
// Second tap selects the word around the cursor.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection(baseOffset: 8, extentOffset: 12),
|
||||
);
|
||||
|
||||
// Selected text shows 3 toolbar buttons.
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'double tap hold selects word',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
|
||||
|
||||
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
final TestGesture gesture =
|
||||
await tester.startGesture(textfieldStart + const Offset(150.0, 5.0));
|
||||
// Hold the press.
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection(baseOffset: 8, extentOffset: 12),
|
||||
);
|
||||
|
||||
// Selected text shows 3 toolbar buttons.
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
||||
|
||||
await gesture.up();
|
||||
await tester.pump();
|
||||
|
||||
// Still selected.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection(baseOffset: 8, extentOffset: 12),
|
||||
);
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'tap after a double tap select is not affected',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
|
||||
|
||||
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
// First tap moved the cursor.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
|
||||
);
|
||||
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
await tester.tapAt(textfieldStart + const Offset(100.0, 5.0));
|
||||
await tester.pump();
|
||||
|
||||
// Plain collapsed selection at the edge of first word. In iOS 12, the
|
||||
// the first tap after a double tap ends up putting the cursor at where
|
||||
// you tapped instead of the edge like every other single tap. This is
|
||||
// likely a bug in iOS 12 and not present in other versions.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
|
||||
);
|
||||
|
||||
// No toolbar.
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'long press moves cursor to the exact long press position and shows toolbar',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
|
||||
|
||||
await tester.longPressAt(textfieldStart + const Offset(50.0, 5.0));
|
||||
await tester.pump();
|
||||
|
||||
// Collapsed cursor for iOS long press.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 3, affinity: TextAffinity.upstream),
|
||||
);
|
||||
|
||||
// Collapsed toolbar shows 2 buttons.
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(2));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'long press tap is not a double tap',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
|
||||
|
||||
await tester.longPressAt(textfieldStart + const Offset(50.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
await tester.tapAt(textfieldStart + const Offset(50.0, 5.0));
|
||||
await tester.pump();
|
||||
|
||||
// We ended up moving the cursor to the edge of the same word and dismissed
|
||||
// the toolbar.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
|
||||
);
|
||||
|
||||
// Collapsed toolbar shows 2 buttons.
|
||||
expect(find.byType(CupertinoButton), findsNothing);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'long tap after a double tap select is not affected',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
|
||||
|
||||
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
// First tap moved the cursor to the beginning of the second word.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
|
||||
);
|
||||
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
await tester.longPressAt(textfieldStart + const Offset(100.0, 5.0));
|
||||
await tester.pump();
|
||||
|
||||
// Plain collapsed selection at the exact tap position.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 6, affinity: TextAffinity.upstream),
|
||||
);
|
||||
|
||||
// Long press toolbar.
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(2));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'double tap after a long tap is not affected',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
|
||||
|
||||
await tester.longPressAt(textfieldStart + const Offset(50.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
|
||||
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
// First tap moved the cursor.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
|
||||
);
|
||||
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
||||
await tester.pump();
|
||||
|
||||
// Double tap selection.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection(baseOffset: 8, extentOffset: 12),
|
||||
);
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'double tap chains work',
|
||||
(WidgetTester tester) async {
|
||||
final TextEditingController controller = TextEditingController(
|
||||
text: 'Atwater Peel Sherbrooke Bonaventure',
|
||||
);
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: Center(
|
||||
child: CupertinoTextField(
|
||||
controller: controller,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Offset textfieldStart = tester.getTopLeft(find.byType(CupertinoTextField));
|
||||
|
||||
await tester.tapAt(textfieldStart + const Offset(50.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
|
||||
);
|
||||
await tester.tapAt(textfieldStart + const Offset(50.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection(baseOffset: 0, extentOffset: 7),
|
||||
);
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
||||
|
||||
// Double tap selecting the same word somewhere else is fine.
|
||||
await tester.tapAt(textfieldStart + const Offset(100.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
// First tap moved the cursor.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 7, affinity: TextAffinity.upstream),
|
||||
);
|
||||
await tester.tapAt(textfieldStart + const Offset(100.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection(baseOffset: 0, extentOffset: 7),
|
||||
);
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
||||
|
||||
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
// First tap moved the cursor.
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection.collapsed(offset: 8, affinity: TextAffinity.downstream),
|
||||
);
|
||||
await tester.tapAt(textfieldStart + const Offset(150.0, 5.0));
|
||||
await tester.pump(const Duration(milliseconds: 50));
|
||||
expect(
|
||||
controller.selection,
|
||||
const TextSelection(baseOffset: 8, extentOffset: 12),
|
||||
);
|
||||
expect(find.byType(CupertinoButton), findsNWidgets(3));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user