Unverified Commit 3fd737c8 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

a11y on Android: copy/cut/paste (#14343)

With a little refactor and unification of how availability of copy/cut/paste is determined across platforms.

Minor remaining issue: https://github.com/flutter/flutter/issues/14331.

Fixes https://github.com/flutter/flutter/issues/13501.
parent 217bcfe2
......@@ -6,7 +6,6 @@ import 'dart:math' as math;
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'button.dart';
......@@ -59,16 +58,12 @@ class _TextSelectionToolbarNotchPainter extends CustomPainter {
class _TextSelectionToolbar extends StatelessWidget {
const _TextSelectionToolbar({
Key key,
this.delegate,
this.handleCut,
this.handleCopy,
this.handlePaste,
this.handleSelectAll,
}) : super(key: key);
final TextSelectionDelegate delegate;
TextEditingValue get value => delegate.textEditingValue;
final VoidCallback handleCut;
final VoidCallback handleCopy;
final VoidCallback handlePaste;
......@@ -80,20 +75,24 @@ class _TextSelectionToolbar extends StatelessWidget {
final Widget onePhysicalPixelVerticalDivider =
new SizedBox(width: 1.0 / MediaQuery.of(context).devicePixelRatio);
if (!value.selection.isCollapsed) {
if (handleCut != null)
items.add(_buildToolbarButton('Cut', handleCut));
items.add(onePhysicalPixelVerticalDivider);
if (handleCopy != null) {
if (items.isNotEmpty)
items.add(onePhysicalPixelVerticalDivider);
items.add(_buildToolbarButton('Copy', handleCopy));
}
// TODO(https://github.com/flutter/flutter/issues/11254):
// This should probably be grayed-out if there is nothing to paste.
if (items.isNotEmpty)
items.add(onePhysicalPixelVerticalDivider);
items.add(_buildToolbarButton('Paste', handlePaste));
if (handlePaste != null) {
if (items.isNotEmpty)
items.add(onePhysicalPixelVerticalDivider);
items.add(_buildToolbarButton('Paste', handlePaste));
}
if (value.text.isNotEmpty && value.selection.isCollapsed) {
items.add(onePhysicalPixelVerticalDivider);
if (handleSelectAll != null) {
if (items.isNotEmpty)
items.add(onePhysicalPixelVerticalDivider);
items.add(_buildToolbarButton('Select All', handleSelectAll));
}
......@@ -236,11 +235,10 @@ class _CupertinoTextSelectionControls extends TextSelectionControls {
position,
),
child: new _TextSelectionToolbar(
delegate: delegate,
handleCut: () => handleCut(delegate),
handleCopy: () => handleCopy(delegate),
handlePaste: () => handlePaste(delegate),
handleSelectAll: () => handleSelectAll(delegate),
handleCut: canCut(delegate) ? () => handleCut(delegate) : null,
handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null,
handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null,
handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
),
)
);
......
......@@ -6,7 +6,6 @@ import 'dart:math' as math;
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'flat_button.dart';
import 'material.dart';
......@@ -22,16 +21,12 @@ const double _kToolbarScreenPadding = 8.0;
class _TextSelectionToolbar extends StatelessWidget {
const _TextSelectionToolbar({
Key key,
this.delegate,
this.handleCut,
this.handleCopy,
this.handlePaste,
this.handleSelectAll,
}) : super(key: key);
final TextSelectionDelegate delegate;
TextEditingValue get value => delegate.textEditingValue;
final VoidCallback handleCut;
final VoidCallback handleCopy;
final VoidCallback handlePaste;
......@@ -42,20 +37,14 @@ class _TextSelectionToolbar extends StatelessWidget {
final List<Widget> items = <Widget>[];
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
if (!value.selection.isCollapsed) {
if (handleCut != null)
items.add(new FlatButton(child: new Text(localizations.cutButtonLabel), onPressed: handleCut));
if (handleCopy != null)
items.add(new FlatButton(child: new Text(localizations.copyButtonLabel), onPressed: handleCopy));
}
items.add(new FlatButton(
child: new Text(localizations.pasteButtonLabel),
// TODO(https://github.com/flutter/flutter/issues/11254):
// This should probably be grayed-out if there is nothing to paste.
onPressed: handlePaste,
));
if (value.text.isNotEmpty) {
if (value.selection.isCollapsed)
items.add(new FlatButton(child: new Text(localizations.selectAllButtonLabel), onPressed: handleSelectAll));
}
if (handlePaste != null)
items.add(new FlatButton(child: new Text(localizations.pasteButtonLabel), onPressed: handlePaste,));
if (handleSelectAll != null)
items.add(new FlatButton(child: new Text(localizations.selectAllButtonLabel), onPressed: handleSelectAll));
return new Material(
elevation: 1.0,
......@@ -152,11 +141,10 @@ class _MaterialTextSelectionControls extends TextSelectionControls {
position,
),
child: new _TextSelectionToolbar(
delegate: delegate,
handleCut: () => handleCut(delegate),
handleCopy: () => handleCopy(delegate),
handlePaste: () => handlePaste(delegate),
handleSelectAll: () => handleSelectAll(delegate),
handleCut: canCut(delegate) ? () => handleCut(delegate) : null,
handleCopy: canCopy(delegate) ? () => handleCopy(delegate) : null,
handlePaste: canPaste(delegate) ? () => handlePaste(delegate) : null,
handleSelectAll: canSelectAll(delegate) ? () => handleSelectAll(delegate) : null,
),
)
);
......
......@@ -308,7 +308,7 @@ class EditableText extends StatefulWidget {
}
/// State for a [EditableText].
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin implements TextInputClient {
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin implements TextInputClient, TextSelectionDelegate {
Timer _cursorTimer;
final ValueNotifier<bool> _showCursor = new ValueNotifier<bool>(false);
final GlobalKey _editableKey = new GlobalKey();
......@@ -516,8 +516,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
debugRequiredFor: widget,
layerLink: _layerLink,
renderObject: renderObject,
onSelectionOverlayChanged: _handleSelectionOverlayChanged,
selectionControls: widget.selectionControls,
selectionDelegate: this,
);
final bool longPress = cause == SelectionChangedCause.longPress;
if (cause != SelectionChangedCause.keyboard && (_value.text.isNotEmpty || longPress))
......@@ -529,12 +529,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
void _handleSelectionOverlayChanged(TextEditingValue value, Rect caretRect) {
assert(!value.composing.isValid); // composing range must be empty while selecting.
_formatAndSetValue(value);
_scrollController.jumpTo(_getScrollOffsetForCaret(caretRect));
}
bool _textChangedSinceLastCaretUpdate = false;
void _handleCaretChanged(Rect caretRect) {
......@@ -643,10 +637,30 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
/// when [ignorePointer] is true. See [RenderEditable.ignorePointer].
RenderEditable get renderEditable => _editableKey.currentContext.findRenderObject();
@override
TextEditingValue get textEditingValue => _value;
@override
set textEditingValue(TextEditingValue value) {
_selectionOverlay?.update(value);
_formatAndSetValue(value);
}
@override
void bringIntoView(TextPosition position) {
_scrollController.jumpTo(_getScrollOffsetForCaret(renderEditable.getLocalRectForCaret(position)));
}
@override
void hideToolbar() {
_selectionOverlay?.hide();
}
@override
Widget build(BuildContext context) {
FocusScope.of(context).reparentIfNeeded(widget.focusNode);
super.build(context); // See AutomaticKeepAliveClientMixin.
final TextSelectionControls controls = widget.selectionControls;
return new Scrollable(
excludeFromSemantics: true,
axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
......@@ -655,25 +669,30 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return new CompositedTransformTarget(
link: _layerLink,
child: new _Editable(
key: _editableKey,
value: _value,
style: widget.style,
cursorColor: widget.cursorColor,
showCursor: _showCursor,
hasFocus: _hasFocus,
maxLines: widget.maxLines,
selectionColor: widget.selectionColor,
textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0,
textAlign: widget.textAlign,
textDirection: _textDirection,
obscureText: widget.obscureText,
obscureShowCharacterAtIndex: _obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null,
autocorrect: widget.autocorrect,
offset: offset,
onSelectionChanged: _handleSelectionChanged,
onCaretChanged: _handleCaretChanged,
rendererIgnoresPointer: widget.rendererIgnoresPointer,
child: new Semantics(
onCopy: _hasFocus && controls?.canCopy(this) == true ? () => controls.handleCopy(this) : null,
onCut: _hasFocus && controls?.canCut(this) == true ? () => controls.handleCut(this) : null,
onPaste: _hasFocus && controls?.canPaste(this) == true ? () => controls.handlePaste(this) : null,
child: new _Editable(
key: _editableKey,
value: _value,
style: widget.style,
cursorColor: widget.cursorColor,
showCursor: _showCursor,
hasFocus: _hasFocus,
maxLines: widget.maxLines,
selectionColor: widget.selectionColor,
textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0,
textAlign: widget.textAlign,
textDirection: _textDirection,
obscureText: widget.obscureText,
obscureShowCharacterAtIndex: _obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null,
autocorrect: widget.autocorrect,
offset: offset,
onSelectionChanged: _handleSelectionChanged,
onCaretChanged: _handleCaretChanged,
rendererIgnoresPointer: widget.rendererIgnoresPointer,
),
),
);
},
......
......@@ -72,6 +72,10 @@ abstract class TextSelectionDelegate {
/// Hides the text selection toolbar.
void hideToolbar();
/// Brings the provided [TextPosition] into the visible area of the text
/// input.
void bringIntoView(TextPosition position);
}
/// An interface for building the selection UI, to be provided by the
......@@ -93,6 +97,49 @@ abstract class TextSelectionControls {
/// Returns the size of the selection handle.
Size get handleSize;
/// Whether the current selection of the text field managed by the given
/// `delegate` can be removed from the text field and placed into the
/// [Clipboard].
///
/// By default, false is returned when nothing is selected in the text field.
///
/// Subclasses can use this to decide if they should expose the cut
/// functionality to the user.
bool canCut(TextSelectionDelegate delegate) {
return !delegate.textEditingValue.selection.isCollapsed;
}
/// Whether the current selection of the text field managed by the given
/// `delegate` can be copied to the [Clipboard].
///
/// By default, false is returned when nothing is selected in the text field.
///
/// Subclasses can use this to decide if they should expose the copy
/// functionality to the user.
bool canCopy(TextSelectionDelegate delegate) {
return !delegate.textEditingValue.selection.isCollapsed;
}
/// Whether the current [Clipboard] content can be pasted into the text field
/// managed by the given `delegate`.
///
/// Subclasses can use this to decide if they should expose the paste
/// functionality to the user.
bool canPaste(TextSelectionDelegate delegate) {
// TODO(goderbauer): return false when clipboard is empty, https://github.com/flutter/flutter/issues/11254
return true;
}
/// Whether the the current selection of the text field managed by the given
/// `delegate` can be extended to include the entire content of the text
/// field.
///
/// Subclasses can use this to decide if they should expose the select all
/// functionality to the user.
bool canSelectAll(TextSelectionDelegate delegate) {
return delegate.textEditingValue.text.isNotEmpty && delegate.textEditingValue.selection.isCollapsed;
}
/// Copy the current selection of the text field managed by the given
/// `delegate` to the [Clipboard]. Then, remove the selected text from the
/// text field and hide the toolbar.
......@@ -111,6 +158,7 @@ abstract class TextSelectionControls {
offset: value.selection.start
),
);
delegate.bringIntoView(delegate.textEditingValue.selection.extent);
delegate.hideToolbar();
}
......@@ -129,6 +177,7 @@ abstract class TextSelectionControls {
text: value.text,
selection: new TextSelection.collapsed(offset: value.selection.end),
);
delegate.bringIntoView(delegate.textEditingValue.selection.extent);
delegate.hideToolbar();
}
......@@ -156,6 +205,7 @@ abstract class TextSelectionControls {
),
);
}
delegate.bringIntoView(delegate.textEditingValue.selection.extent);
delegate.hideToolbar();
}
......@@ -174,6 +224,7 @@ abstract class TextSelectionControls {
extentOffset: delegate.textEditingValue.text.length
),
);
delegate.bringIntoView(delegate.textEditingValue.selection.extent);
}
}
......@@ -181,7 +232,7 @@ abstract class TextSelectionControls {
///
/// The selection handles are displayed in the [Overlay] that most closely
/// encloses the given [BuildContext].
class TextSelectionOverlay implements TextSelectionDelegate {
class TextSelectionOverlay {
/// Creates an object that manages overly entries for selection handles.
///
/// The [context] must not be null and must have an [Overlay] as an ancestor.
......@@ -191,8 +242,8 @@ class TextSelectionOverlay implements TextSelectionDelegate {
this.debugRequiredFor,
@required this.layerLink,
@required this.renderObject,
this.onSelectionOverlayChanged,
this.selectionControls,
this.selectionDelegate,
}): assert(value != null),
assert(context != null),
_value = value {
......@@ -220,15 +271,13 @@ class TextSelectionOverlay implements TextSelectionDelegate {
/// The editable line in which the selected text is being displayed.
final RenderEditable renderObject;
/// Called when the the selection changes.
///
/// For example, if the use drags one of the selection handles, this function
/// will be called with a new input value with an updated selection.
final TextSelectionOverlayChanged onSelectionOverlayChanged;
/// Builds text selection handles and toolbar.
final TextSelectionControls selectionControls;
/// The delegate for manipulating the current selection in the owning
/// text field.
final TextSelectionDelegate selectionDelegate;
/// Controls the fade-in animations.
static const Duration _kFadeDuration = const Duration(milliseconds: 150);
AnimationController _handleController;
......@@ -372,24 +421,23 @@ class TextSelectionOverlay implements TextSelectionDelegate {
link: layerLink,
showWhenUnlinked: false,
offset: -editingRegion.topLeft,
child: selectionControls.buildToolbar(context, editingRegion, midpoint, this),
child: selectionControls.buildToolbar(context, editingRegion, midpoint, selectionDelegate),
),
);
}
void _handleSelectionHandleChanged(TextSelection newSelection, _TextSelectionHandlePosition position) {
Rect caretRect;
TextPosition textPosition;
switch (position) {
case _TextSelectionHandlePosition.start:
caretRect = renderObject.getLocalRectForCaret(newSelection.base);
textPosition = newSelection.base;
break;
case _TextSelectionHandlePosition.end:
caretRect = renderObject.getLocalRectForCaret(newSelection.extent);
textPosition =newSelection.extent;
break;
}
update(_value.copyWith(selection: newSelection, composing: TextRange.empty));
if (onSelectionOverlayChanged != null)
onSelectionOverlayChanged(_value, caretRect);
selectionDelegate.textEditingValue = _value.copyWith(selection: newSelection, composing: TextRange.empty);
selectionDelegate.bringIntoView(textPosition);
}
void _handleSelectionHandleTapped() {
......@@ -402,23 +450,6 @@ class TextSelectionOverlay implements TextSelectionDelegate {
}
}
}
@override
TextEditingValue get textEditingValue => _value;
@override
set textEditingValue(TextEditingValue newValue) {
update(newValue);
if (onSelectionOverlayChanged != null) {
final Rect caretRect = renderObject.getLocalRectForCaret(newValue.selection.extent);
onSelectionOverlayChanged(newValue, caretRect);
}
}
@override
void hideToolbar() {
hide();
}
}
/// This widget represents a single draggable text selection handle.
......
......@@ -1813,6 +1813,7 @@ void main() {
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
SemanticsAction.paste,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
......@@ -1837,6 +1838,7 @@ void main() {
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
SemanticsAction.paste,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
......@@ -1861,6 +1863,7 @@ void main() {
SemanticsAction.tap,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
SemanticsAction.paste,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
......@@ -1919,6 +1922,7 @@ void main() {
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
SemanticsAction.paste,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
......@@ -1943,6 +1947,9 @@ void main() {
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.moveCursorForwardByCharacter,
SemanticsAction.setSelection,
SemanticsAction.paste,
SemanticsAction.cut,
SemanticsAction.copy,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
......@@ -1989,6 +1996,7 @@ void main() {
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
SemanticsAction.paste,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
......@@ -2032,6 +2040,9 @@ void main() {
SemanticsAction.tap,
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
SemanticsAction.paste,
SemanticsAction.cut,
SemanticsAction.copy,
],
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
......
......@@ -9,6 +9,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart';
import 'package:mockito/mockito.dart';
import 'semantics_tester.dart';
......@@ -19,6 +20,10 @@ void main() {
final TextStyle textStyle = const TextStyle();
final Color cursorColor = const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
setUp(() {
debugResetSemanticsIdCounter();
});
testWidgets('has expected defaults', (WidgetTester tester) async {
await tester.pumpWidget(new Directionality(
textDirection: TextDirection.ltr,
......@@ -347,14 +352,14 @@ void main() {
final EditableTextState textState = tester.state(find.byType(EditableText));
expect(textState.selectionOverlay.handlesAreVisible, isTrue);
expect(textState.selectionOverlay.textEditingValue.selection, const TextSelection.collapsed(offset: 4));
expect(textState.selectionOverlay.selectionDelegate.textEditingValue.selection, const TextSelection.collapsed(offset: 4));
// Simulate selection change via keyboard and expect handles to disappear.
render.onSelectionChanged(const TextSelection.collapsed(offset: 10), render, SelectionChangedCause.keyboard);
await tester.pumpAndSettle();
expect(textState.selectionOverlay.handlesAreVisible, isFalse);
expect(textState.selectionOverlay.textEditingValue.selection, const TextSelection.collapsed(offset: 10));
expect(textState.selectionOverlay.selectionDelegate.textEditingValue.selection, const TextSelection.collapsed(offset: 10));
});
testWidgets('exposes correct cursor movement semantics', (WidgetTester tester) async {
......@@ -565,4 +570,151 @@ void main() {
semantics.dispose();
});
group('a11y copy/cut/paste', () {
Future<Null> _buildApp(MockTextSelectionControls controls, WidgetTester tester) {
return tester.pumpWidget(new MaterialApp(
home: new EditableText(
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
selectionControls: controls,
),
));
}
MockTextSelectionControls controls;
setUp(() {
controller.text = 'test';
controller.selection = new TextSelection.collapsed(offset: controller.text.length);
controls = new MockTextSelectionControls();
when(controls.buildHandle(any, any, any)).thenReturn(new Container());
when(controls.buildToolbar(any, any, any, any)).thenReturn(new Container());
});
testWidgets('are exposed', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
when(controls.canCopy(any)).thenReturn(false);
when(controls.canCut(any)).thenReturn(false);
when(controls.canPaste(any)).thenReturn(false);
await _buildApp(controls, tester);
await tester.tap(find.byType(EditableText));
await tester.pump();
expect(semantics, includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
],
));
when(controls.canCopy(any)).thenReturn(true);
await _buildApp(controls, tester);
expect(semantics, includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
SemanticsAction.copy,
],
));
when(controls.canCopy(any)).thenReturn(false);
when(controls.canPaste(any)).thenReturn(true);
await _buildApp(controls, tester);
expect(semantics, includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
SemanticsAction.paste,
],
));
when(controls.canPaste(any)).thenReturn(false);
when(controls.canCut(any)).thenReturn(true);
await _buildApp(controls, tester);
expect(semantics, includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
SemanticsAction.cut,
],
));
when(controls.canCopy(any)).thenReturn(true);
when(controls.canCut(any)).thenReturn(true);
when(controls.canPaste(any)).thenReturn(true);
await _buildApp(controls, tester);
expect(semantics, includesNodeWith(
value: 'test',
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
SemanticsAction.cut,
SemanticsAction.copy,
SemanticsAction.paste,
],
));
semantics.dispose();
});
testWidgets('can copy/cut/paste with a11y', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
when(controls.canCopy(any)).thenReturn(true);
when(controls.canCut(any)).thenReturn(true);
when(controls.canPaste(any)).thenReturn(true);
await _buildApp(controls, tester);
await tester.tap(find.byType(EditableText));
await tester.pump();
final SemanticsOwner owner = tester.binding.pipelineOwner.semanticsOwner;
const int expectedNodeId = 3;
expect(semantics, hasSemantics(new TestSemantics.root(
children: <TestSemantics>[
new TestSemantics.rootChild(
id: expectedNodeId,
flags: <SemanticsFlag>[
SemanticsFlag.isTextField,
SemanticsFlag.isFocused
],
actions: <SemanticsAction>[
SemanticsAction.moveCursorBackwardByCharacter,
SemanticsAction.setSelection,
SemanticsAction.copy,
SemanticsAction.cut,
SemanticsAction.paste
],
value: 'test',
textSelection: new TextSelection.collapsed(offset: controller.text.length),
textDirection: TextDirection.ltr,
),
],
), ignoreRect: true, ignoreTransform: true));
owner.performAction(expectedNodeId, SemanticsAction.copy);
verify(controls.handleCopy(any)).called(1);
owner.performAction(expectedNodeId, SemanticsAction.cut);
verify(controls.handleCut(any)).called(1);
owner.performAction(expectedNodeId, SemanticsAction.paste);
verify(controls.handlePaste(any)).called(1);
semantics.dispose();
});
});
}
class MockTextSelectionControls extends Mock implements TextSelectionControls {}
......@@ -257,7 +257,7 @@ class TestSemantics {
if (!ignoreTransform && transform != nodeData.transform)
return fail('expected node id $id to have transform $transform but found transform:\n${nodeData.transform}.');
if (textSelection?.baseOffset != nodeData.textSelection?.baseOffset || textSelection?.extentOffset != nodeData.textSelection?.extentOffset) {
return fail('expected node id $id to have textDirection [${textSelection?.baseOffset}, ${textSelection?.end}] but found: [${nodeData.textSelection?.baseOffset}, ${nodeData.textSelection?.extentOffset}].');
return fail('expected node id $id to have textSelection [${textSelection?.baseOffset}, ${textSelection?.end}] but found: [${nodeData.textSelection?.baseOffset}, ${nodeData.textSelection?.extentOffset}].');
}
final int childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount;
if (children.length != childrenCount)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment