Mac Page Up / Page Down in text fields (#105497)
Adds support for Mac/iOS's behavior of scrolling (but not moving the cursor) when using page up/down in a text field.
This commit is contained in:
parent
497a52809d
commit
7e36cf17e7
@ -1767,7 +1767,10 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
|||||||
// fall through to the defaultShortcuts.
|
// fall through to the defaultShortcuts.
|
||||||
child: DefaultTextEditingShortcuts(
|
child: DefaultTextEditingShortcuts(
|
||||||
child: Actions(
|
child: Actions(
|
||||||
actions: widget.actions ?? WidgetsApp.defaultActions,
|
actions: widget.actions ?? <Type, Action<Intent>>{
|
||||||
|
...WidgetsApp.defaultActions,
|
||||||
|
ScrollIntent: Action<ScrollIntent>.overridable(context: context, defaultAction: ScrollAction()),
|
||||||
|
},
|
||||||
child: FocusTraversalGroup(
|
child: FocusTraversalGroup(
|
||||||
policy: ReadingOrderTraversalPolicy(),
|
policy: ReadingOrderTraversalPolicy(),
|
||||||
child: TapRegionSurface(
|
child: TapRegionSurface(
|
||||||
|
@ -3,11 +3,13 @@
|
|||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/painting.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'actions.dart';
|
import 'actions.dart';
|
||||||
import 'focus_traversal.dart';
|
import 'focus_traversal.dart';
|
||||||
import 'framework.dart';
|
import 'framework.dart';
|
||||||
|
import 'scrollable.dart';
|
||||||
import 'shortcuts.dart';
|
import 'shortcuts.dart';
|
||||||
import 'text_editing_intents.dart';
|
import 'text_editing_intents.dart';
|
||||||
|
|
||||||
@ -157,8 +159,8 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
|
|||||||
/// {@macro flutter.widgets.ProxyWidget.child}
|
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
// These are shortcuts are shared between most platforms except macOS for it
|
// These shortcuts are shared between all platforms except Apple platforms,
|
||||||
// uses different modifier keys as the line/word modifier.
|
// because they use different modifier keys as the line/word modifier.
|
||||||
static final Map<ShortcutActivator, Intent> _commonShortcuts = <ShortcutActivator, Intent>{
|
static final Map<ShortcutActivator, Intent> _commonShortcuts = <ShortcutActivator, Intent>{
|
||||||
// Delete Shortcuts.
|
// Delete Shortcuts.
|
||||||
for (final bool pressShift in const <bool>[true, false])
|
for (final bool pressShift in const <bool>[true, false])
|
||||||
@ -315,6 +317,8 @@ class DefaultTextEditingShortcuts extends StatelessWidget {
|
|||||||
const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExpandSelectionToDocumentBoundaryIntent(forward: false),
|
const SingleActivator(LogicalKeyboardKey.home, shift: true): const ExpandSelectionToDocumentBoundaryIntent(forward: false),
|
||||||
const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExpandSelectionToDocumentBoundaryIntent(forward: true),
|
const SingleActivator(LogicalKeyboardKey.end, shift: true): const ExpandSelectionToDocumentBoundaryIntent(forward: true),
|
||||||
|
|
||||||
|
const SingleActivator(LogicalKeyboardKey.pageUp): const ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page),
|
||||||
|
const SingleActivator(LogicalKeyboardKey.pageDown): const ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
|
||||||
const SingleActivator(LogicalKeyboardKey.pageUp, shift: true): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: false),
|
const SingleActivator(LogicalKeyboardKey.pageUp, shift: true): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: false),
|
||||||
const SingleActivator(LogicalKeyboardKey.pageDown, shift: true): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: false),
|
const SingleActivator(LogicalKeyboardKey.pageDown, shift: true): const ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: false),
|
||||||
|
|
||||||
@ -553,9 +557,8 @@ Intent? intentForMacOSSelector(String selectorName) {
|
|||||||
'scrollToBeginningOfDocument:': ScrollToDocumentBoundaryIntent(forward: false),
|
'scrollToBeginningOfDocument:': ScrollToDocumentBoundaryIntent(forward: false),
|
||||||
'scrollToEndOfDocument:': ScrollToDocumentBoundaryIntent(forward: true),
|
'scrollToEndOfDocument:': ScrollToDocumentBoundaryIntent(forward: true),
|
||||||
|
|
||||||
// TODO(knopp): Page Up/Down intents are missing (https://github.com/flutter/flutter/pull/105497)
|
'scrollPageUp:': ScrollIntent(direction: AxisDirection.up, type: ScrollIncrementType.page),
|
||||||
'scrollPageUp:': ScrollToDocumentBoundaryIntent(forward: false),
|
'scrollPageDown:': ScrollIntent(direction: AxisDirection.down, type: ScrollIncrementType.page),
|
||||||
'scrollPageDown:': ScrollToDocumentBoundaryIntent(forward: true),
|
|
||||||
'pageUpAndModifySelection:': ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: false),
|
'pageUpAndModifySelection:': ExtendSelectionVerticallyToAdjacentPageIntent(forward: false, collapseSelection: false),
|
||||||
'pageDownAndModifySelection:': ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: false),
|
'pageDownAndModifySelection:': ExtendSelectionVerticallyToAdjacentPageIntent(forward: true, collapseSelection: false),
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ import 'media_query.dart';
|
|||||||
import 'scroll_configuration.dart';
|
import 'scroll_configuration.dart';
|
||||||
import 'scroll_controller.dart';
|
import 'scroll_controller.dart';
|
||||||
import 'scroll_physics.dart';
|
import 'scroll_physics.dart';
|
||||||
|
import 'scroll_position.dart';
|
||||||
import 'scrollable.dart';
|
import 'scrollable.dart';
|
||||||
import 'shortcuts.dart';
|
import 'shortcuts.dart';
|
||||||
import 'spell_check.dart';
|
import 'spell_check.dart';
|
||||||
@ -1907,6 +1908,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
|
|
||||||
TextSelectionOverlay? _selectionOverlay;
|
TextSelectionOverlay? _selectionOverlay;
|
||||||
|
|
||||||
|
final GlobalKey _scrollableKey = GlobalKey();
|
||||||
ScrollController? _internalScrollController;
|
ScrollController? _internalScrollController;
|
||||||
ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ??= ScrollController());
|
ScrollController get _scrollController => widget.scrollController ?? (_internalScrollController ??= ScrollController());
|
||||||
|
|
||||||
@ -3953,6 +3955,96 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles [ScrollIntent] by scrolling the [Scrollable] inside of
|
||||||
|
/// [EditableText].
|
||||||
|
void _scroll(ScrollIntent intent) {
|
||||||
|
if (intent.type != ScrollIncrementType.page) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ScrollPosition position = _scrollController.position;
|
||||||
|
if (widget.maxLines == 1) {
|
||||||
|
_scrollController.jumpTo(position.maxScrollExtent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the field isn't scrollable, do nothing. For example, when the lines of
|
||||||
|
// text is less than maxLines, the field has nothing to scroll.
|
||||||
|
if (position.maxScrollExtent == 0.0 && position.minScrollExtent == 0.0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ScrollableState? state = _scrollableKey.currentState as ScrollableState?;
|
||||||
|
final double increment = ScrollAction.getDirectionalIncrement(state!, intent);
|
||||||
|
final double destination = clampDouble(
|
||||||
|
position.pixels + increment,
|
||||||
|
position.minScrollExtent,
|
||||||
|
position.maxScrollExtent,
|
||||||
|
);
|
||||||
|
if (destination == position.pixels) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_scrollController.jumpTo(destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extend the selection down by page if the `forward` parameter is true, or
|
||||||
|
/// up by page otherwise.
|
||||||
|
void _extendSelectionByPage(ExtendSelectionByPageIntent intent) {
|
||||||
|
if (widget.maxLines == 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final TextSelection nextSelection;
|
||||||
|
final Rect extentRect = renderEditable.getLocalRectForCaret(
|
||||||
|
_value.selection.extent,
|
||||||
|
);
|
||||||
|
final ScrollableState? state = _scrollableKey.currentState as ScrollableState?;
|
||||||
|
final double increment = ScrollAction.getDirectionalIncrement(
|
||||||
|
state!,
|
||||||
|
ScrollIntent(
|
||||||
|
direction: intent.forward ? AxisDirection.down : AxisDirection.up,
|
||||||
|
type: ScrollIncrementType.page,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final ScrollPosition position = _scrollController.position;
|
||||||
|
if (intent.forward) {
|
||||||
|
if (_value.selection.extentOffset >= _value.text.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Offset nextExtentOffset =
|
||||||
|
Offset(extentRect.left, extentRect.top + increment);
|
||||||
|
final double height = position.maxScrollExtent + renderEditable.size.height;
|
||||||
|
final TextPosition nextExtent = nextExtentOffset.dy + position.pixels >= height
|
||||||
|
? TextPosition(offset: _value.text.length)
|
||||||
|
: renderEditable.getPositionForPoint(
|
||||||
|
renderEditable.localToGlobal(nextExtentOffset),
|
||||||
|
);
|
||||||
|
nextSelection = _value.selection.copyWith(
|
||||||
|
extentOffset: nextExtent.offset,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (_value.selection.extentOffset <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Offset nextExtentOffset =
|
||||||
|
Offset(extentRect.left, extentRect.top + increment);
|
||||||
|
final TextPosition nextExtent = nextExtentOffset.dy + position.pixels <= 0
|
||||||
|
? const TextPosition(offset: 0)
|
||||||
|
: renderEditable.getPositionForPoint(
|
||||||
|
renderEditable.localToGlobal(nextExtentOffset),
|
||||||
|
);
|
||||||
|
nextSelection = _value.selection.copyWith(
|
||||||
|
extentOffset: nextExtent.offset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bringIntoView(nextSelection.extent);
|
||||||
|
userUpdateTextEditingValue(
|
||||||
|
_value.copyWith(selection: nextSelection),
|
||||||
|
SelectionChangedCause.keyboard,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _updateSelection(UpdateSelectionIntent intent) {
|
void _updateSelection(UpdateSelectionIntent intent) {
|
||||||
bringIntoView(intent.newSelection.extent);
|
bringIntoView(intent.newSelection.extent);
|
||||||
userUpdateTextEditingValue(
|
userUpdateTextEditingValue(
|
||||||
@ -4058,6 +4150,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
|
|
||||||
// Extend/Move Selection
|
// Extend/Move Selection
|
||||||
ExtendSelectionByCharacterIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionByCharacterIntent>(this, false, _characterBoundary)),
|
ExtendSelectionByCharacterIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionByCharacterIntent>(this, false, _characterBoundary)),
|
||||||
|
ExtendSelectionByPageIntent: _makeOverridable(CallbackAction<ExtendSelectionByPageIntent>(onInvoke: _extendSelectionByPage)),
|
||||||
ExtendSelectionToNextWordBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToNextWordBoundaryIntent>(this, true, _nextWordBoundary)),
|
ExtendSelectionToNextWordBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToNextWordBoundaryIntent>(this, true, _nextWordBoundary)),
|
||||||
ExtendSelectionToLineBreakIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToLineBreakIntent>(this, true, _linebreak)),
|
ExtendSelectionToLineBreakIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToLineBreakIntent>(this, true, _linebreak)),
|
||||||
ExpandSelectionToLineBreakIntent: _makeOverridable(CallbackAction<ExpandSelectionToLineBreakIntent>(onInvoke: _expandSelectionToLinebreak)),
|
ExpandSelectionToLineBreakIntent: _makeOverridable(CallbackAction<ExpandSelectionToLineBreakIntent>(onInvoke: _expandSelectionToLinebreak)),
|
||||||
@ -4067,6 +4160,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
ExtendSelectionToDocumentBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToDocumentBoundaryIntent>(this, true, _documentBoundary)),
|
ExtendSelectionToDocumentBoundaryIntent: _makeOverridable(_UpdateTextSelectionAction<ExtendSelectionToDocumentBoundaryIntent>(this, true, _documentBoundary)),
|
||||||
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable(_ExtendSelectionOrCaretPositionAction(this, _nextWordBoundary)),
|
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent: _makeOverridable(_ExtendSelectionOrCaretPositionAction(this, _nextWordBoundary)),
|
||||||
ScrollToDocumentBoundaryIntent: _makeOverridable(CallbackAction<ScrollToDocumentBoundaryIntent>(onInvoke: _scrollToDocumentBoundary)),
|
ScrollToDocumentBoundaryIntent: _makeOverridable(CallbackAction<ScrollToDocumentBoundaryIntent>(onInvoke: _scrollToDocumentBoundary)),
|
||||||
|
ScrollIntent: CallbackAction<ScrollIntent>(onInvoke: _scroll),
|
||||||
|
|
||||||
// Copy Paste
|
// Copy Paste
|
||||||
SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)),
|
SelectAllTextIntent: _makeOverridable(_SelectAllAction(this)),
|
||||||
@ -4099,6 +4193,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
includeSemantics: false,
|
includeSemantics: false,
|
||||||
debugLabel: kReleaseMode ? null : 'EditableText',
|
debugLabel: kReleaseMode ? null : 'EditableText',
|
||||||
child: Scrollable(
|
child: Scrollable(
|
||||||
|
key: _scrollableKey,
|
||||||
excludeFromSemantics: true,
|
excludeFromSemantics: true,
|
||||||
axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
|
axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
|
@ -1789,14 +1789,14 @@ class ScrollAction extends Action<ScrollIntent> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the scroll increment for a single scroll request, for use when
|
/// Returns the scroll increment for a single scroll request, for use when
|
||||||
// scrolling using a hardware keyboard.
|
/// scrolling using a hardware keyboard.
|
||||||
//
|
///
|
||||||
// Must not be called when the position is null, or when any of the position
|
/// Must not be called when the position is null, or when any of the position
|
||||||
// metrics (pixels, viewportDimension, maxScrollExtent, minScrollExtent) are
|
/// metrics (pixels, viewportDimension, maxScrollExtent, minScrollExtent) are
|
||||||
// null. The type and state arguments must not be null, and the widget must
|
/// null. The type and state arguments must not be null, and the widget must
|
||||||
// have already been laid out so that the position fields are valid.
|
/// have already been laid out so that the position fields are valid.
|
||||||
double _calculateScrollIncrement(ScrollableState state, { ScrollIncrementType type = ScrollIncrementType.line }) {
|
static double _calculateScrollIncrement(ScrollableState state, { ScrollIncrementType type = ScrollIncrementType.line }) {
|
||||||
assert(type != null);
|
assert(type != null);
|
||||||
assert(state.position != null);
|
assert(state.position != null);
|
||||||
assert(state.position.hasPixels);
|
assert(state.position.hasPixels);
|
||||||
@ -1820,9 +1820,9 @@ class ScrollAction extends Action<ScrollIntent> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find out how much of an increment to move by, taking the different
|
/// Find out how much of an increment to move by, taking the different
|
||||||
// directions into account.
|
/// directions into account.
|
||||||
double _getIncrement(ScrollableState state, ScrollIntent intent) {
|
static double getDirectionalIncrement(ScrollableState state, ScrollIntent intent) {
|
||||||
final double increment = _calculateScrollIncrement(state, type: intent.type);
|
final double increment = _calculateScrollIncrement(state, type: intent.type);
|
||||||
switch (intent.direction) {
|
switch (intent.direction) {
|
||||||
case AxisDirection.down:
|
case AxisDirection.down:
|
||||||
@ -1912,7 +1912,7 @@ class ScrollAction extends Action<ScrollIntent> {
|
|||||||
if (state!._physics != null && !state._physics!.shouldAcceptUserOffset(state.position)) {
|
if (state!._physics != null && !state._physics!.shouldAcceptUserOffset(state.position)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final double increment = _getIncrement(state, intent);
|
final double increment = getDirectionalIncrement(state, intent);
|
||||||
if (increment == 0.0) {
|
if (increment == 0.0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -245,6 +245,15 @@ class ScrollToDocumentBoundaryIntent extends DirectionalTextEditingIntent {
|
|||||||
}) : super(forward);
|
}) : super(forward);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scrolls up or down by page depending on the [forward] parameter.
|
||||||
|
/// Extends the selection up or down by page based on the [forward] parameter.
|
||||||
|
class ExtendSelectionByPageIntent extends DirectionalTextEditingIntent {
|
||||||
|
/// Creates a [ExtendSelectionByPageIntent].
|
||||||
|
const ExtendSelectionByPageIntent({
|
||||||
|
required bool forward,
|
||||||
|
}) : super(forward);
|
||||||
|
}
|
||||||
|
|
||||||
/// An [Intent] to select everything in the field.
|
/// An [Intent] to select everything in the field.
|
||||||
class SelectAllTextIntent extends Intent {
|
class SelectAllTextIntent extends Intent {
|
||||||
/// Creates an instance of [SelectAllTextIntent].
|
/// Creates an instance of [SelectAllTextIntent].
|
||||||
|
@ -8102,6 +8102,178 @@ void main() {
|
|||||||
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.windows })
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.windows })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets('pageup/pagedown keys on Apple platforms', (WidgetTester tester) async {
|
||||||
|
final TextEditingController controller = TextEditingController(text: testText);
|
||||||
|
controller.selection = const TextSelection(
|
||||||
|
baseOffset: 0,
|
||||||
|
extentOffset: 0,
|
||||||
|
affinity: TextAffinity.upstream,
|
||||||
|
);
|
||||||
|
final ScrollController scrollController = ScrollController();
|
||||||
|
const int lines = 2;
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 400,
|
||||||
|
child: EditableText(
|
||||||
|
minLines: lines,
|
||||||
|
maxLines: lines,
|
||||||
|
controller: controller,
|
||||||
|
scrollController: scrollController,
|
||||||
|
showSelectionHandles: true,
|
||||||
|
autofocus: true,
|
||||||
|
focusNode: FocusNode(),
|
||||||
|
style: Typography.material2018().black.subtitle1!,
|
||||||
|
cursorColor: Colors.blue,
|
||||||
|
backgroundCursorColor: Colors.grey,
|
||||||
|
selectionControls: materialTextSelectionControls,
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.pump(); // Wait for autofocus to take effect.
|
||||||
|
|
||||||
|
expect(controller.value.selection.isCollapsed, isTrue);
|
||||||
|
expect(controller.value.selection.baseOffset, 0);
|
||||||
|
expect(scrollController.position.pixels, 0.0);
|
||||||
|
final double lineHeight = findRenderEditable(tester).preferredLineHeight;
|
||||||
|
expect(scrollController.position.viewportDimension, lineHeight * lines);
|
||||||
|
|
||||||
|
// Page Up does nothing at the top.
|
||||||
|
await sendKeys(
|
||||||
|
tester,
|
||||||
|
<LogicalKeyboardKey>[
|
||||||
|
LogicalKeyboardKey.pageUp,
|
||||||
|
],
|
||||||
|
targetPlatform: defaultTargetPlatform,
|
||||||
|
);
|
||||||
|
expect(scrollController.position.pixels, 0.0);
|
||||||
|
|
||||||
|
// Page Down scrolls proportionally to the height of the viewport.
|
||||||
|
await sendKeys(
|
||||||
|
tester,
|
||||||
|
<LogicalKeyboardKey>[
|
||||||
|
LogicalKeyboardKey.pageDown,
|
||||||
|
],
|
||||||
|
targetPlatform: defaultTargetPlatform,
|
||||||
|
);
|
||||||
|
expect(scrollController.position.pixels, lineHeight * lines * 0.8);
|
||||||
|
|
||||||
|
// Another Page Down reaches the bottom.
|
||||||
|
await sendKeys(
|
||||||
|
tester,
|
||||||
|
<LogicalKeyboardKey>[
|
||||||
|
LogicalKeyboardKey.pageDown,
|
||||||
|
],
|
||||||
|
targetPlatform: defaultTargetPlatform,
|
||||||
|
);
|
||||||
|
expect(scrollController.position.pixels, lineHeight * lines);
|
||||||
|
|
||||||
|
// Page Up now scrolls back up proportionally to the height of the viewport.
|
||||||
|
await sendKeys(
|
||||||
|
tester,
|
||||||
|
<LogicalKeyboardKey>[
|
||||||
|
LogicalKeyboardKey.pageUp,
|
||||||
|
],
|
||||||
|
targetPlatform: defaultTargetPlatform,
|
||||||
|
);
|
||||||
|
expect(scrollController.position.pixels, lineHeight * lines - lineHeight * lines * 0.8);
|
||||||
|
|
||||||
|
// Another Page Up reaches the top.
|
||||||
|
await sendKeys(
|
||||||
|
tester,
|
||||||
|
<LogicalKeyboardKey>[
|
||||||
|
LogicalKeyboardKey.pageUp,
|
||||||
|
],
|
||||||
|
targetPlatform: defaultTargetPlatform,
|
||||||
|
);
|
||||||
|
expect(scrollController.position.pixels, 0.0);
|
||||||
|
},
|
||||||
|
skip: kIsWeb, // [intended] on web these keys are handled by the browser.
|
||||||
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('pageup/pagedown keys in a one line field on Apple platforms', (WidgetTester tester) async {
|
||||||
|
final TextEditingController controller = TextEditingController(text: testText);
|
||||||
|
controller.selection = const TextSelection(
|
||||||
|
baseOffset: 0,
|
||||||
|
extentOffset: 0,
|
||||||
|
affinity: TextAffinity.upstream,
|
||||||
|
);
|
||||||
|
final ScrollController scrollController = ScrollController();
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 400,
|
||||||
|
child: EditableText(
|
||||||
|
minLines: 1,
|
||||||
|
controller: controller,
|
||||||
|
scrollController: scrollController,
|
||||||
|
showSelectionHandles: true,
|
||||||
|
autofocus: true,
|
||||||
|
focusNode: FocusNode(),
|
||||||
|
style: Typography.material2018().black.subtitle1!,
|
||||||
|
cursorColor: Colors.blue,
|
||||||
|
backgroundCursorColor: Colors.grey,
|
||||||
|
selectionControls: materialTextSelectionControls,
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
await tester.pump(); // Wait for autofocus to take effect.
|
||||||
|
|
||||||
|
expect(controller.value.selection.isCollapsed, isTrue);
|
||||||
|
expect(controller.value.selection.baseOffset, 0);
|
||||||
|
expect(scrollController.position.pixels, 0.0);
|
||||||
|
|
||||||
|
// Page Up scrolls to the end.
|
||||||
|
await sendKeys(
|
||||||
|
tester,
|
||||||
|
<LogicalKeyboardKey>[
|
||||||
|
LogicalKeyboardKey.pageUp,
|
||||||
|
],
|
||||||
|
targetPlatform: defaultTargetPlatform,
|
||||||
|
);
|
||||||
|
expect(scrollController.position.pixels, scrollController.position.maxScrollExtent);
|
||||||
|
expect(controller.value.selection.isCollapsed, isTrue);
|
||||||
|
expect(controller.value.selection.baseOffset, 0);
|
||||||
|
|
||||||
|
// Return scroll to the start.
|
||||||
|
await sendKeys(
|
||||||
|
tester,
|
||||||
|
<LogicalKeyboardKey>[
|
||||||
|
LogicalKeyboardKey.home,
|
||||||
|
],
|
||||||
|
targetPlatform: defaultTargetPlatform,
|
||||||
|
);
|
||||||
|
expect(scrollController.position.pixels, 0.0);
|
||||||
|
expect(controller.value.selection.isCollapsed, isTrue);
|
||||||
|
expect(controller.value.selection.baseOffset, 0);
|
||||||
|
|
||||||
|
// Page Down also scrolls to the end.
|
||||||
|
await sendKeys(
|
||||||
|
tester,
|
||||||
|
<LogicalKeyboardKey>[
|
||||||
|
LogicalKeyboardKey.pageDown,
|
||||||
|
],
|
||||||
|
targetPlatform: defaultTargetPlatform,
|
||||||
|
);
|
||||||
|
expect(scrollController.position.pixels, scrollController.position.maxScrollExtent);
|
||||||
|
expect(controller.value.selection.isCollapsed, isTrue);
|
||||||
|
expect(controller.value.selection.baseOffset, 0);
|
||||||
|
},
|
||||||
|
skip: kIsWeb, // [intended] on web these keys are handled by the browser.
|
||||||
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
|
||||||
|
);
|
||||||
|
|
||||||
// Regression test for https://github.com/flutter/flutter/issues/31287
|
// Regression test for https://github.com/flutter/flutter/issues/31287
|
||||||
testWidgets('text selection handle visibility', (WidgetTester tester) async {
|
testWidgets('text selection handle visibility', (WidgetTester tester) async {
|
||||||
// Text with two separate words to select.
|
// Text with two separate words to select.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user