mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Fix TextField height issues (#27205)
* Create new TextField attribute to control maxLength behaviour * Create test case for maxLinesIncrementalHeight * fix maxLinesIncrementalHeight set method * fix editable_test.dart * Just introducing some proposed API additions, renaming to expands * Pass minLines and expands through to child widgets and validate * minLines can't be null, and expands can't be true when maxLines is 1 * Implement minLines and maxLines height sizing logic * Simplify minLines validation logic * expands parameter proof of concept * min/max mixup bug fix, and tests work with expands false * Test expands=true cases, and fix textPainter.height being out of date * Test all behavior matrix cases * min/max assertion more strict, can't be equal * Tests work that were missing expands serialization * Action sheet tests no longer fail due to rounding error * TextFieldFocus test no longer mysteriously fails * TODOs for making expands nullable. Will depend on how Expanded wrapping works * Expanded growth happens when expanded is true and maxLines is null * Test Expanded wrapper * No more overflow when wrapped in Expanded * Docs improvements * expands can be null * Simplify error cases to support existing behavior * Docs examples and other docs cleanup * Expansion up to perfectly meet the parent size * Fix analyze null error * Fix test after move to nullable expands * minLines defaults to null * expands is now exclusively for expanding to parent height and not growth between min and max * _layout rewritten to handle max height. Need to fix prefix tests and reenable expands * Tests for textfield overflowing parent * layoutLineBox is documented and private * expands works in new _layout * _layout return numbers seem to perfectly match original _layout * inputWidth comment after trying it out and failing tests * Fix analyze errors * WIP prefix/suffix do affect height * Prefix/suffix and icons affect height, tests pass, but I'm still visually verifying identical to original * Tall prefix test that verifies pixel perfect layout * Fix overflowing edge case and test it * Clean up comments, old code, and todos * Changing _expands causes relayout. Wasnt able to figure out how to test though... * Clean up code review comments * Fix misalignment when tall prefix and border, and clean up related test * Simple code review cleanup * Bring back inputWidth to _layout method * Fix rounding errors showing up in mac tests * Fix flake by reordering tests. Without this, the dreaded intrinsicwidth flake is reproducible 50% of the time on my machine. * Fix more rounding error mac tests
This commit is contained in:
parent
80082ac4d3
commit
9e9f48dabb
@ -552,13 +552,16 @@ class _RenderDecoration extends RenderBox {
|
|||||||
@required TextDirection textDirection,
|
@required TextDirection textDirection,
|
||||||
@required TextBaseline textBaseline,
|
@required TextBaseline textBaseline,
|
||||||
@required bool isFocused,
|
@required bool isFocused,
|
||||||
|
@required bool expands,
|
||||||
}) : assert(decoration != null),
|
}) : assert(decoration != null),
|
||||||
assert(textDirection != null),
|
assert(textDirection != null),
|
||||||
assert(textBaseline != null),
|
assert(textBaseline != null),
|
||||||
|
assert(expands != null),
|
||||||
_decoration = decoration,
|
_decoration = decoration,
|
||||||
_textDirection = textDirection,
|
_textDirection = textDirection,
|
||||||
_textBaseline = textBaseline,
|
_textBaseline = textBaseline,
|
||||||
_isFocused = isFocused;
|
_isFocused = isFocused,
|
||||||
|
_expands = expands;
|
||||||
|
|
||||||
final Map<_DecorationSlot, RenderBox> slotToChild = <_DecorationSlot, RenderBox>{};
|
final Map<_DecorationSlot, RenderBox> slotToChild = <_DecorationSlot, RenderBox>{};
|
||||||
final Map<RenderBox, _DecorationSlot> childToSlot = <RenderBox, _DecorationSlot>{};
|
final Map<RenderBox, _DecorationSlot> childToSlot = <RenderBox, _DecorationSlot>{};
|
||||||
@ -709,6 +712,16 @@ class _RenderDecoration extends RenderBox {
|
|||||||
markNeedsSemanticsUpdate();
|
markNeedsSemanticsUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get expands => _expands;
|
||||||
|
bool _expands = false;
|
||||||
|
set expands(bool value) {
|
||||||
|
assert(value != null);
|
||||||
|
if (_expands == value)
|
||||||
|
return;
|
||||||
|
_expands = value;
|
||||||
|
markNeedsLayout();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void attach(PipelineOwner owner) {
|
void attach(PipelineOwner owner) {
|
||||||
super.attach(owner);
|
super.attach(owner);
|
||||||
@ -804,34 +817,31 @@ class _RenderDecoration extends RenderBox {
|
|||||||
|
|
||||||
EdgeInsets get contentPadding => decoration.contentPadding;
|
EdgeInsets get contentPadding => decoration.contentPadding;
|
||||||
|
|
||||||
// Returns a value used by performLayout to position all
|
// Lay out the given box if needed, and return its baseline
|
||||||
// of the renderers. This method applies layout to all of the renderers
|
double _layoutLineBox(RenderBox box, BoxConstraints constraints) {
|
||||||
// except the container. For convenience, the container is laid out
|
if (box == null) {
|
||||||
// in performLayout().
|
return 0.0;
|
||||||
_RenderDecorationLayout _layout(BoxConstraints layoutConstraints) {
|
|
||||||
final Map<RenderBox, double> boxToBaseline = <RenderBox, double>{};
|
|
||||||
BoxConstraints boxConstraints = layoutConstraints.loosen();
|
|
||||||
double aboveBaseline = 0.0;
|
|
||||||
double belowBaseline = 0.0;
|
|
||||||
void layoutLineBox(RenderBox box) {
|
|
||||||
if (box == null)
|
|
||||||
return;
|
|
||||||
box.layout(boxConstraints, parentUsesSize: true);
|
|
||||||
final double baseline = box.getDistanceToBaseline(textBaseline);
|
|
||||||
assert(baseline != null && baseline >= 0.0);
|
|
||||||
boxToBaseline[box] = baseline;
|
|
||||||
aboveBaseline = math.max(baseline, aboveBaseline);
|
|
||||||
belowBaseline = math.max(box.size.height - baseline, belowBaseline);
|
|
||||||
}
|
}
|
||||||
layoutLineBox(prefix);
|
box.layout(constraints, parentUsesSize: true);
|
||||||
layoutLineBox(suffix);
|
final double baseline = box.getDistanceToBaseline(textBaseline);
|
||||||
|
assert(baseline != null && baseline >= 0.0);
|
||||||
|
return baseline;
|
||||||
|
}
|
||||||
|
|
||||||
if (icon != null)
|
// Returns a value used by performLayout to position all of the renderers.
|
||||||
icon.layout(boxConstraints, parentUsesSize: true);
|
// This method applies layout to all of the renderers except the container.
|
||||||
if (prefixIcon != null)
|
// For convenience, the container is laid out in performLayout().
|
||||||
prefixIcon.layout(boxConstraints, parentUsesSize: true);
|
_RenderDecorationLayout _layout(BoxConstraints layoutConstraints) {
|
||||||
if (suffixIcon != null)
|
// Margin on each side of subtext (counter and helperError)
|
||||||
suffixIcon.layout(boxConstraints, parentUsesSize: true);
|
final Map<RenderBox, double> boxToBaseline = <RenderBox, double>{};
|
||||||
|
final BoxConstraints boxConstraints = layoutConstraints.loosen();
|
||||||
|
|
||||||
|
// Layout all the widgets used by InputDecorator
|
||||||
|
boxToBaseline[prefix] = _layoutLineBox(prefix, boxConstraints);
|
||||||
|
boxToBaseline[suffix] = _layoutLineBox(suffix, boxConstraints);
|
||||||
|
boxToBaseline[icon] = _layoutLineBox(icon, boxConstraints);
|
||||||
|
boxToBaseline[prefixIcon] = _layoutLineBox(prefixIcon, boxConstraints);
|
||||||
|
boxToBaseline[suffixIcon] = _layoutLineBox(suffixIcon, boxConstraints);
|
||||||
|
|
||||||
final double inputWidth = math.max(0.0, constraints.maxWidth - (
|
final double inputWidth = math.max(0.0, constraints.maxWidth - (
|
||||||
_boxSize(icon).width
|
_boxSize(icon).width
|
||||||
@ -841,72 +851,144 @@ class _RenderDecoration extends RenderBox {
|
|||||||
+ _boxSize(suffix).width
|
+ _boxSize(suffix).width
|
||||||
+ _boxSize(suffixIcon).width
|
+ _boxSize(suffixIcon).width
|
||||||
+ contentPadding.right));
|
+ contentPadding.right));
|
||||||
|
boxToBaseline[label] = _layoutLineBox(
|
||||||
|
label,
|
||||||
|
boxConstraints.copyWith(maxWidth: inputWidth),
|
||||||
|
);
|
||||||
|
boxToBaseline[hint] = _layoutLineBox(
|
||||||
|
hint,
|
||||||
|
boxConstraints.copyWith(minWidth: inputWidth, maxWidth: inputWidth),
|
||||||
|
);
|
||||||
|
boxToBaseline[counter] = _layoutLineBox(counter, boxConstraints);
|
||||||
|
|
||||||
boxConstraints = boxConstraints.copyWith(maxWidth: inputWidth);
|
// The helper or error text can occupy the full width less the space
|
||||||
if (label != null) {
|
// occupied by the icon and counter.
|
||||||
if (decoration.alignLabelWithHint) {
|
boxToBaseline[helperError] = _layoutLineBox(
|
||||||
// The label is aligned with the hint, at the baseline
|
helperError,
|
||||||
layoutLineBox(label);
|
boxConstraints.copyWith(
|
||||||
} else {
|
|
||||||
// The label is centered, not baseline aligned
|
|
||||||
label.layout(boxConstraints, parentUsesSize: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boxConstraints = boxConstraints.copyWith(minWidth: inputWidth);
|
|
||||||
layoutLineBox(hint);
|
|
||||||
layoutLineBox(input);
|
|
||||||
|
|
||||||
double inputBaseline = contentPadding.top + aboveBaseline;
|
|
||||||
double containerHeight = contentPadding.top
|
|
||||||
+ aboveBaseline
|
|
||||||
+ belowBaseline
|
|
||||||
+ contentPadding.bottom;
|
|
||||||
|
|
||||||
if (label != null) {
|
|
||||||
// floatingLabelHeight includes the vertical gap between the inline
|
|
||||||
// elements and the floating label.
|
|
||||||
containerHeight += decoration.floatingLabelHeight;
|
|
||||||
inputBaseline += decoration.floatingLabelHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
containerHeight = math.max(
|
|
||||||
containerHeight,
|
|
||||||
math.max(
|
|
||||||
_boxSize(suffixIcon).height,
|
|
||||||
_boxSize(prefixIcon).height));
|
|
||||||
|
|
||||||
// Inline text within an outline border is centered within the container
|
|
||||||
// less 2.0 dps at the top to account for the vertical space occupied
|
|
||||||
// by the floating label.
|
|
||||||
final double outlineBaseline = aboveBaseline +
|
|
||||||
(containerHeight - (2.0 + aboveBaseline + belowBaseline)) / 2.0;
|
|
||||||
|
|
||||||
double subtextBaseline = 0.0;
|
|
||||||
double subtextHeight = 0.0;
|
|
||||||
if (helperError != null || counter != null) {
|
|
||||||
boxConstraints = layoutConstraints.loosen();
|
|
||||||
aboveBaseline = 0.0;
|
|
||||||
belowBaseline = 0.0;
|
|
||||||
layoutLineBox(counter);
|
|
||||||
|
|
||||||
// The helper or error text can occupy the full width less the space
|
|
||||||
// occupied by the icon and counter.
|
|
||||||
boxConstraints = boxConstraints.copyWith(
|
|
||||||
maxWidth: math.max(0.0, boxConstraints.maxWidth
|
maxWidth: math.max(0.0, boxConstraints.maxWidth
|
||||||
- _boxSize(icon).width
|
- _boxSize(icon).width
|
||||||
- _boxSize(counter).width
|
- _boxSize(counter).width
|
||||||
- contentPadding.horizontal,
|
- contentPadding.horizontal,
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
layoutLineBox(helperError);
|
);
|
||||||
|
|
||||||
if (aboveBaseline + belowBaseline > 0.0) {
|
// The height of the input needs to accommodate label above and counter and
|
||||||
const double subtextGap = 8.0;
|
// helperError below, when they exist.
|
||||||
subtextBaseline = containerHeight + subtextGap + aboveBaseline;
|
const double subtextGap = 8.0;
|
||||||
subtextHeight = subtextGap + aboveBaseline + belowBaseline;
|
final double labelHeight = label == null
|
||||||
}
|
? 0
|
||||||
|
: decoration.floatingLabelHeight;
|
||||||
|
final double topHeight = decoration.border.isOutline
|
||||||
|
? math.max(labelHeight - boxToBaseline[label], 0)
|
||||||
|
: labelHeight;
|
||||||
|
final double counterHeight = counter == null
|
||||||
|
? 0
|
||||||
|
: boxToBaseline[counter] + subtextGap * 2;
|
||||||
|
final _HelperError helperErrorWidget = decoration.helperError;
|
||||||
|
final double helperErrorHeight = helperErrorWidget.helperText == null
|
||||||
|
? 0
|
||||||
|
: helperError.size.height + subtextGap * 2;
|
||||||
|
final double bottomHeight = math.max(
|
||||||
|
counterHeight,
|
||||||
|
helperErrorHeight,
|
||||||
|
);
|
||||||
|
boxToBaseline[input] = _layoutLineBox(
|
||||||
|
input,
|
||||||
|
boxConstraints.deflate(EdgeInsets.only(
|
||||||
|
top: contentPadding.top + topHeight,
|
||||||
|
bottom: contentPadding.bottom + bottomHeight,
|
||||||
|
)).copyWith(
|
||||||
|
minWidth: inputWidth,
|
||||||
|
maxWidth: inputWidth,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// The field can be occupied by a hint or by the input itself
|
||||||
|
final double hintHeight = hint == null ? 0 : hint.size.height;
|
||||||
|
final double inputDirectHeight = input == null ? 0 : input.size.height;
|
||||||
|
final double inputHeight = math.max(hintHeight, inputDirectHeight);
|
||||||
|
final double inputInternalBaseline = math.max(
|
||||||
|
boxToBaseline[input],
|
||||||
|
boxToBaseline[hint],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate the amount that prefix/suffix affects height above and below
|
||||||
|
// the input.
|
||||||
|
final double prefixHeight = prefix == null ? 0 : prefix.size.height;
|
||||||
|
final double suffixHeight = suffix == null ? 0 : suffix.size.height;
|
||||||
|
final double fixHeight = math.max(
|
||||||
|
boxToBaseline[prefix],
|
||||||
|
boxToBaseline[suffix],
|
||||||
|
);
|
||||||
|
final double fixAboveInput = math.max(0, fixHeight - inputInternalBaseline);
|
||||||
|
final double fixBelowBaseline = math.max(
|
||||||
|
prefixHeight - boxToBaseline[prefix],
|
||||||
|
suffixHeight - boxToBaseline[suffix],
|
||||||
|
);
|
||||||
|
final double fixBelowInput = math.max(
|
||||||
|
0,
|
||||||
|
fixBelowBaseline - (inputHeight - inputInternalBaseline),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate the height of the input text container.
|
||||||
|
final double prefixIconHeight = prefixIcon == null ? 0 : prefixIcon.size.height;
|
||||||
|
final double suffixIconHeight = suffixIcon == null ? 0 : suffixIcon.size.height;
|
||||||
|
final double fixIconHeight = math.max(prefixIconHeight, suffixIconHeight);
|
||||||
|
final double contentHeight = math.max(
|
||||||
|
fixIconHeight,
|
||||||
|
topHeight
|
||||||
|
+ contentPadding.top
|
||||||
|
+ fixAboveInput
|
||||||
|
+ inputHeight
|
||||||
|
+ fixBelowInput
|
||||||
|
+ contentPadding.bottom,
|
||||||
|
);
|
||||||
|
final double maxContainerHeight = boxConstraints.maxHeight - bottomHeight;
|
||||||
|
final double containerHeight = expands
|
||||||
|
? maxContainerHeight
|
||||||
|
: math.min(contentHeight, maxContainerHeight);
|
||||||
|
|
||||||
|
// Always position the prefix/suffix in the same place (baseline).
|
||||||
|
final double overflow = math.max(0, contentHeight - maxContainerHeight);
|
||||||
|
final double baselineAdjustment = fixAboveInput - overflow;
|
||||||
|
|
||||||
|
// The baselines that will be used to draw the actual input text content.
|
||||||
|
final double inputBaseline = contentPadding.top
|
||||||
|
+ topHeight
|
||||||
|
+ inputInternalBaseline
|
||||||
|
+ baselineAdjustment;
|
||||||
|
// The text in the input when an outline border is present is centered
|
||||||
|
// within the container less 2.0 dps at the top to account for the vertical
|
||||||
|
// space occupied by the floating label.
|
||||||
|
final double outlineBaseline = inputInternalBaseline
|
||||||
|
+ baselineAdjustment / 2
|
||||||
|
+ (containerHeight - (2.0 + inputHeight)) / 2.0;
|
||||||
|
|
||||||
|
// Find the positions of the text below the input when it exists.
|
||||||
|
double subtextCounterBaseline = 0;
|
||||||
|
double subtextHelperBaseline = 0;
|
||||||
|
double subtextCounterHeight = 0;
|
||||||
|
double subtextHelperHeight = 0;
|
||||||
|
if (counter != null) {
|
||||||
|
subtextCounterBaseline =
|
||||||
|
containerHeight + subtextGap + boxToBaseline[counter];
|
||||||
|
subtextCounterHeight = counter.size.height + subtextGap;
|
||||||
}
|
}
|
||||||
|
if (helperErrorWidget.helperText != null) {
|
||||||
|
subtextHelperBaseline =
|
||||||
|
containerHeight + subtextGap + boxToBaseline[helperError];
|
||||||
|
subtextHelperHeight = helperError.size.height + subtextGap;
|
||||||
|
}
|
||||||
|
final double subtextBaseline = math.max(
|
||||||
|
subtextCounterBaseline,
|
||||||
|
subtextHelperBaseline,
|
||||||
|
);
|
||||||
|
final double subtextHeight = math.max(
|
||||||
|
subtextCounterHeight,
|
||||||
|
subtextHelperHeight,
|
||||||
|
);
|
||||||
|
|
||||||
return _RenderDecorationLayout(
|
return _RenderDecorationLayout(
|
||||||
boxToBaseline: boxToBaseline,
|
boxToBaseline: boxToBaseline,
|
||||||
@ -1370,15 +1452,18 @@ class _Decorator extends RenderObjectWidget {
|
|||||||
@required this.textDirection,
|
@required this.textDirection,
|
||||||
@required this.textBaseline,
|
@required this.textBaseline,
|
||||||
@required this.isFocused,
|
@required this.isFocused,
|
||||||
|
@required this.expands,
|
||||||
}) : assert(decoration != null),
|
}) : assert(decoration != null),
|
||||||
assert(textDirection != null),
|
assert(textDirection != null),
|
||||||
assert(textBaseline != null),
|
assert(textBaseline != null),
|
||||||
|
assert(expands != null),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
final _Decoration decoration;
|
final _Decoration decoration;
|
||||||
final TextDirection textDirection;
|
final TextDirection textDirection;
|
||||||
final TextBaseline textBaseline;
|
final TextBaseline textBaseline;
|
||||||
final bool isFocused;
|
final bool isFocused;
|
||||||
|
final bool expands;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_RenderDecorationElement createElement() => _RenderDecorationElement(this);
|
_RenderDecorationElement createElement() => _RenderDecorationElement(this);
|
||||||
@ -1390,6 +1475,7 @@ class _Decorator extends RenderObjectWidget {
|
|||||||
textDirection: textDirection,
|
textDirection: textDirection,
|
||||||
textBaseline: textBaseline,
|
textBaseline: textBaseline,
|
||||||
isFocused: isFocused,
|
isFocused: isFocused,
|
||||||
|
expands: expands,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1399,6 +1485,7 @@ class _Decorator extends RenderObjectWidget {
|
|||||||
..decoration = decoration
|
..decoration = decoration
|
||||||
..textDirection = textDirection
|
..textDirection = textDirection
|
||||||
..textBaseline = textBaseline
|
..textBaseline = textBaseline
|
||||||
|
..expands = expands
|
||||||
..isFocused = isFocused;
|
..isFocused = isFocused;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1461,6 +1548,7 @@ class InputDecorator extends StatefulWidget {
|
|||||||
this.baseStyle,
|
this.baseStyle,
|
||||||
this.textAlign,
|
this.textAlign,
|
||||||
this.isFocused = false,
|
this.isFocused = false,
|
||||||
|
this.expands = false,
|
||||||
this.isEmpty = false,
|
this.isEmpty = false,
|
||||||
this.child,
|
this.child,
|
||||||
}) : assert(isFocused != null),
|
}) : assert(isFocused != null),
|
||||||
@ -1495,6 +1583,19 @@ class InputDecorator extends StatefulWidget {
|
|||||||
/// Defaults to false.
|
/// Defaults to false.
|
||||||
final bool isFocused;
|
final bool isFocused;
|
||||||
|
|
||||||
|
/// If true, the height of the input field will be as large as possible.
|
||||||
|
///
|
||||||
|
/// If wrapped in a widget that constrains its child's height, like Expanded
|
||||||
|
/// or SizedBox, the input field will only be affected if [expands] is set to
|
||||||
|
/// true.
|
||||||
|
///
|
||||||
|
/// See [TextField.minLines] and [TextField.maxLines] for related ways to
|
||||||
|
/// affect the height of an input. When [expands] is true, both must be null
|
||||||
|
/// in order to avoid ambiguity in determining the height.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
final bool expands;
|
||||||
|
|
||||||
/// Whether the input field is empty.
|
/// Whether the input field is empty.
|
||||||
///
|
///
|
||||||
/// Determines the position of the label text and whether to display the hint
|
/// Determines the position of the label text and whether to display the hint
|
||||||
@ -1533,6 +1634,7 @@ class InputDecorator extends StatefulWidget {
|
|||||||
properties.add(DiagnosticsProperty<InputDecoration>('decoration', decoration));
|
properties.add(DiagnosticsProperty<InputDecoration>('decoration', decoration));
|
||||||
properties.add(DiagnosticsProperty<TextStyle>('baseStyle', baseStyle, defaultValue: null));
|
properties.add(DiagnosticsProperty<TextStyle>('baseStyle', baseStyle, defaultValue: null));
|
||||||
properties.add(DiagnosticsProperty<bool>('isFocused', isFocused));
|
properties.add(DiagnosticsProperty<bool>('isFocused', isFocused));
|
||||||
|
properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
|
||||||
properties.add(DiagnosticsProperty<bool>('isEmpty', isEmpty));
|
properties.add(DiagnosticsProperty<bool>('isEmpty', isEmpty));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1928,6 +2030,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
|
|||||||
textDirection: textDirection,
|
textDirection: textDirection,
|
||||||
textBaseline: textBaseline,
|
textBaseline: textBaseline,
|
||||||
isFocused: isFocused,
|
isFocused: isFocused,
|
||||||
|
expands: widget.expands,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,6 +147,8 @@ class TextField extends StatefulWidget {
|
|||||||
this.obscureText = false,
|
this.obscureText = false,
|
||||||
this.autocorrect = true,
|
this.autocorrect = true,
|
||||||
this.maxLines = 1,
|
this.maxLines = 1,
|
||||||
|
this.minLines,
|
||||||
|
this.expands = false,
|
||||||
this.maxLength,
|
this.maxLength,
|
||||||
this.maxLengthEnforced = true,
|
this.maxLengthEnforced = true,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
@ -171,6 +173,16 @@ class TextField extends StatefulWidget {
|
|||||||
assert(scrollPadding != null),
|
assert(scrollPadding != null),
|
||||||
assert(dragStartBehavior != null),
|
assert(dragStartBehavior != null),
|
||||||
assert(maxLines == null || maxLines > 0),
|
assert(maxLines == null || maxLines > 0),
|
||||||
|
assert(minLines == null || minLines > 0),
|
||||||
|
assert(
|
||||||
|
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
|
||||||
|
'minLines can\'t be greater than maxLines',
|
||||||
|
),
|
||||||
|
assert(expands != null),
|
||||||
|
assert(
|
||||||
|
!expands || (maxLines == null && minLines == null),
|
||||||
|
'minLines and maxLines must be null when expands is true.',
|
||||||
|
),
|
||||||
assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
|
assert(maxLength == null || maxLength == TextField.noMaxLength || maxLength > 0),
|
||||||
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
@ -269,6 +281,12 @@ class TextField extends StatefulWidget {
|
|||||||
/// {@macro flutter.widgets.editableText.maxLines}
|
/// {@macro flutter.widgets.editableText.maxLines}
|
||||||
final int maxLines;
|
final int maxLines;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.minLines}
|
||||||
|
final int minLines;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.expands}
|
||||||
|
final bool expands;
|
||||||
|
|
||||||
/// If [maxLength] is set to this value, only the "current input length"
|
/// If [maxLength] is set to this value, only the "current input length"
|
||||||
/// part of the character counter is shown.
|
/// part of the character counter is shown.
|
||||||
static const int noMaxLength = -1;
|
static const int noMaxLength = -1;
|
||||||
@ -457,6 +475,8 @@ class TextField extends StatefulWidget {
|
|||||||
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
|
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
|
||||||
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
|
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
|
||||||
properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
|
properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
|
||||||
|
properties.add(IntProperty('minLines', minLines, defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
|
||||||
properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
|
properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
|
||||||
properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, defaultValue: true, ifFalse: 'maxLength not enforced'));
|
properties.add(FlagProperty('maxLengthEnforced', value: maxLengthEnforced, defaultValue: true, ifFalse: 'maxLength not enforced'));
|
||||||
properties.add(EnumProperty<TextInputAction>('textInputAction', textInputAction, defaultValue: null));
|
properties.add(EnumProperty<TextInputAction>('textInputAction', textInputAction, defaultValue: null));
|
||||||
@ -851,6 +871,8 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
|||||||
obscureText: widget.obscureText,
|
obscureText: widget.obscureText,
|
||||||
autocorrect: widget.autocorrect,
|
autocorrect: widget.autocorrect,
|
||||||
maxLines: widget.maxLines,
|
maxLines: widget.maxLines,
|
||||||
|
minLines: widget.minLines,
|
||||||
|
expands: widget.expands,
|
||||||
selectionColor: themeData.textSelectionColor,
|
selectionColor: themeData.textSelectionColor,
|
||||||
selectionControls: widget.selectionEnabled ? textSelectionControls : null,
|
selectionControls: widget.selectionEnabled ? textSelectionControls : null,
|
||||||
onChanged: widget.onChanged,
|
onChanged: widget.onChanged,
|
||||||
@ -883,6 +905,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
|
|||||||
textAlign: widget.textAlign,
|
textAlign: widget.textAlign,
|
||||||
isFocused: focusNode.hasFocus,
|
isFocused: focusNode.hasFocus,
|
||||||
isEmpty: controller.value.text.isEmpty,
|
isEmpty: controller.value.text.isEmpty,
|
||||||
|
expands: widget.expands,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -1801,9 +1801,9 @@ abstract class RenderBox extends RenderObject {
|
|||||||
testIntrinsicsForValues(getMinIntrinsicWidth, getMaxIntrinsicWidth, 'Width', double.infinity);
|
testIntrinsicsForValues(getMinIntrinsicWidth, getMaxIntrinsicWidth, 'Width', double.infinity);
|
||||||
testIntrinsicsForValues(getMinIntrinsicHeight, getMaxIntrinsicHeight, 'Height', double.infinity);
|
testIntrinsicsForValues(getMinIntrinsicHeight, getMaxIntrinsicHeight, 'Height', double.infinity);
|
||||||
if (constraints.hasBoundedWidth)
|
if (constraints.hasBoundedWidth)
|
||||||
testIntrinsicsForValues(getMinIntrinsicWidth, getMaxIntrinsicWidth, 'Width', constraints.maxWidth);
|
testIntrinsicsForValues(getMinIntrinsicWidth, getMaxIntrinsicWidth, 'Width', constraints.maxHeight);
|
||||||
if (constraints.hasBoundedHeight)
|
if (constraints.hasBoundedHeight)
|
||||||
testIntrinsicsForValues(getMinIntrinsicHeight, getMaxIntrinsicHeight, 'Height', constraints.maxHeight);
|
testIntrinsicsForValues(getMinIntrinsicHeight, getMaxIntrinsicHeight, 'Height', constraints.maxWidth);
|
||||||
|
|
||||||
// TODO(ianh): Test that values are internally consistent in more ways than the above.
|
// TODO(ianh): Test that values are internally consistent in more ways than the above.
|
||||||
|
|
||||||
|
@ -144,6 +144,8 @@ class RenderEditable extends RenderBox {
|
|||||||
ValueNotifier<bool> showCursor,
|
ValueNotifier<bool> showCursor,
|
||||||
bool hasFocus,
|
bool hasFocus,
|
||||||
int maxLines = 1,
|
int maxLines = 1,
|
||||||
|
int minLines,
|
||||||
|
bool expands = false,
|
||||||
StrutStyle strutStyle,
|
StrutStyle strutStyle,
|
||||||
Color selectionColor,
|
Color selectionColor,
|
||||||
double textScaleFactor = 1.0,
|
double textScaleFactor = 1.0,
|
||||||
@ -165,6 +167,16 @@ class RenderEditable extends RenderBox {
|
|||||||
}) : assert(textAlign != null),
|
}) : assert(textAlign != null),
|
||||||
assert(textDirection != null, 'RenderEditable created without a textDirection.'),
|
assert(textDirection != null, 'RenderEditable created without a textDirection.'),
|
||||||
assert(maxLines == null || maxLines > 0),
|
assert(maxLines == null || maxLines > 0),
|
||||||
|
assert(minLines == null || minLines > 0),
|
||||||
|
assert(
|
||||||
|
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
|
||||||
|
'minLines can\'t be greater than maxLines',
|
||||||
|
),
|
||||||
|
assert(expands != null),
|
||||||
|
assert(
|
||||||
|
!expands || (maxLines == null && minLines == null),
|
||||||
|
'minLines and maxLines must be null when expands is true.',
|
||||||
|
),
|
||||||
assert(textScaleFactor != null),
|
assert(textScaleFactor != null),
|
||||||
assert(offset != null),
|
assert(offset != null),
|
||||||
assert(ignorePointer != null),
|
assert(ignorePointer != null),
|
||||||
@ -186,6 +198,8 @@ class RenderEditable extends RenderBox {
|
|||||||
_showCursor = showCursor ?? ValueNotifier<bool>(false),
|
_showCursor = showCursor ?? ValueNotifier<bool>(false),
|
||||||
_hasFocus = hasFocus ?? false,
|
_hasFocus = hasFocus ?? false,
|
||||||
_maxLines = maxLines,
|
_maxLines = maxLines,
|
||||||
|
_minLines = minLines,
|
||||||
|
_expands = expands,
|
||||||
_selectionColor = selectionColor,
|
_selectionColor = selectionColor,
|
||||||
_selection = selection,
|
_selection = selection,
|
||||||
_offset = offset,
|
_offset = offset,
|
||||||
@ -691,6 +705,29 @@ class RenderEditable extends RenderBox {
|
|||||||
markNeedsTextLayout();
|
markNeedsTextLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.minLines}
|
||||||
|
int get minLines => _minLines;
|
||||||
|
int _minLines;
|
||||||
|
/// The value may be null. If it is not null, then it must be greater than zero.
|
||||||
|
set minLines(int value) {
|
||||||
|
assert(value == null || value > 0);
|
||||||
|
if (minLines == value)
|
||||||
|
return;
|
||||||
|
_minLines = value;
|
||||||
|
markNeedsTextLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.editableText.expands}
|
||||||
|
bool get expands => _expands;
|
||||||
|
bool _expands;
|
||||||
|
set expands(bool value) {
|
||||||
|
assert(value != null);
|
||||||
|
if (expands == value)
|
||||||
|
return;
|
||||||
|
_expands = value;
|
||||||
|
markNeedsTextLayout();
|
||||||
|
}
|
||||||
|
|
||||||
/// The color to use when painting the selection.
|
/// The color to use when painting the selection.
|
||||||
Color get selectionColor => _selectionColor;
|
Color get selectionColor => _selectionColor;
|
||||||
Color _selectionColor;
|
Color _selectionColor;
|
||||||
@ -1150,8 +1187,28 @@ class RenderEditable extends RenderBox {
|
|||||||
double get preferredLineHeight => _textPainter.preferredLineHeight;
|
double get preferredLineHeight => _textPainter.preferredLineHeight;
|
||||||
|
|
||||||
double _preferredHeight(double width) {
|
double _preferredHeight(double width) {
|
||||||
if (maxLines != null)
|
// Lock height to maxLines if needed
|
||||||
|
final bool lockedMax = maxLines != null && minLines == null;
|
||||||
|
final bool lockedBoth = minLines != null && minLines == maxLines;
|
||||||
|
final bool singleLine = maxLines == 1;
|
||||||
|
if (singleLine || lockedMax || lockedBoth) {
|
||||||
return preferredLineHeight * maxLines;
|
return preferredLineHeight * maxLines;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp height to minLines or maxLines if needed
|
||||||
|
final bool minLimited = minLines != null && minLines > 1;
|
||||||
|
final bool maxLimited = maxLines != null;
|
||||||
|
if (minLimited || maxLimited) {
|
||||||
|
_layoutText(width);
|
||||||
|
if (minLimited && _textPainter.height < preferredLineHeight * minLines) {
|
||||||
|
return preferredLineHeight * minLines;
|
||||||
|
}
|
||||||
|
if (maxLimited && _textPainter.height > preferredLineHeight * maxLines) {
|
||||||
|
return preferredLineHeight * maxLines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the height based on the content
|
||||||
if (width == double.infinity) {
|
if (width == double.infinity) {
|
||||||
final String text = _textPainter.text.toPlainText();
|
final String text = _textPainter.text.toPlainText();
|
||||||
int lines = 1;
|
int lines = 1;
|
||||||
@ -1613,6 +1670,8 @@ class RenderEditable extends RenderBox {
|
|||||||
properties.add(DiagnosticsProperty<Color>('cursorColor', cursorColor));
|
properties.add(DiagnosticsProperty<Color>('cursorColor', cursorColor));
|
||||||
properties.add(DiagnosticsProperty<ValueNotifier<bool>>('showCursor', showCursor));
|
properties.add(DiagnosticsProperty<ValueNotifier<bool>>('showCursor', showCursor));
|
||||||
properties.add(IntProperty('maxLines', maxLines));
|
properties.add(IntProperty('maxLines', maxLines));
|
||||||
|
properties.add(IntProperty('minLines', minLines));
|
||||||
|
properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
|
||||||
properties.add(DiagnosticsProperty<Color>('selectionColor', selectionColor));
|
properties.add(DiagnosticsProperty<Color>('selectionColor', selectionColor));
|
||||||
properties.add(DoubleProperty('textScaleFactor', textScaleFactor));
|
properties.add(DoubleProperty('textScaleFactor', textScaleFactor));
|
||||||
properties.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
|
properties.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
|
||||||
|
@ -278,6 +278,8 @@ class EditableText extends StatefulWidget {
|
|||||||
this.locale,
|
this.locale,
|
||||||
this.textScaleFactor,
|
this.textScaleFactor,
|
||||||
this.maxLines = 1,
|
this.maxLines = 1,
|
||||||
|
this.minLines,
|
||||||
|
this.expands = false,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
this.selectionColor,
|
this.selectionColor,
|
||||||
this.selectionControls,
|
this.selectionControls,
|
||||||
@ -310,6 +312,16 @@ class EditableText extends StatefulWidget {
|
|||||||
assert(backgroundCursorColor != null),
|
assert(backgroundCursorColor != null),
|
||||||
assert(textAlign != null),
|
assert(textAlign != null),
|
||||||
assert(maxLines == null || maxLines > 0),
|
assert(maxLines == null || maxLines > 0),
|
||||||
|
assert(minLines == null || minLines > 0),
|
||||||
|
assert(
|
||||||
|
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
|
||||||
|
'minLines can\'t be greater than maxLines',
|
||||||
|
),
|
||||||
|
assert(expands != null),
|
||||||
|
assert(
|
||||||
|
!expands || (maxLines == null && minLines == null),
|
||||||
|
'minLines and maxLines must be null when expands is true.',
|
||||||
|
),
|
||||||
assert(autofocus != null),
|
assert(autofocus != null),
|
||||||
assert(rendererIgnoresPointer != null),
|
assert(rendererIgnoresPointer != null),
|
||||||
assert(scrollPadding != null),
|
assert(scrollPadding != null),
|
||||||
@ -465,12 +477,78 @@ class EditableText extends StatefulWidget {
|
|||||||
/// container will start with enough vertical space for one line and
|
/// container will start with enough vertical space for one line and
|
||||||
/// automatically grow to accommodate additional lines as they are entered.
|
/// automatically grow to accommodate additional lines as they are entered.
|
||||||
///
|
///
|
||||||
/// If it is not null, the value must be greater than zero. If it is greater
|
/// If this is not null, the value must be greater than zero, and it will lock
|
||||||
/// than 1, it will take up enough horizontal space to accommodate that number
|
/// the input to the given number of lines and take up enough horizontal space
|
||||||
/// of lines.
|
/// to accommodate that number of lines. Setting [minLines] as well allows the
|
||||||
|
/// input to grow between the indicated range.
|
||||||
|
///
|
||||||
|
/// The full set of behaviors possible with [minLines] and [maxLines] are as
|
||||||
|
/// follows. These examples apply equally to `TextField`, `TextFormField`, and
|
||||||
|
/// `EditableText`.
|
||||||
|
///
|
||||||
|
/// Input that occupies a single line and scrolls horizontally as needed.
|
||||||
|
/// ```dart
|
||||||
|
/// TextField()
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Input whose height grows from one line up to as many lines as needed for
|
||||||
|
/// the text that was entered. If a height limit is imposed by its parent, it
|
||||||
|
/// will scroll vertically when its height reaches that limit.
|
||||||
|
/// ```dart
|
||||||
|
/// TextField(maxLines: null)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The input's height is large enough for the given number of lines. If
|
||||||
|
/// additional lines are entered the input scrolls vertically.
|
||||||
|
/// ```dart
|
||||||
|
/// TextField(maxLines: 2)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Input whose height grows with content between a min and max. An infinite
|
||||||
|
/// max is possible with `maxLines: null`.
|
||||||
|
/// ```dart
|
||||||
|
/// TextField(minLines: 2, maxLines: 4)
|
||||||
|
/// ```
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final int maxLines;
|
final int maxLines;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.editableText.minLines}
|
||||||
|
/// The minimum number of lines to occupy when the content spans fewer lines.
|
||||||
|
|
||||||
|
/// When [maxLines] is set as well, the height will grow between the indicated
|
||||||
|
/// range of lines. When [maxLines] is null, it will grow as high as needed,
|
||||||
|
/// starting from [minLines].
|
||||||
|
///
|
||||||
|
/// See the examples in [maxLines] for the complete picture of how [maxLines]
|
||||||
|
/// and [minLines] interact to produce various behaviors.
|
||||||
|
///
|
||||||
|
/// Defaults to null.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final int minLines;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.editableText.expands}
|
||||||
|
/// Whether this widget's height will be sized to fill its parent.
|
||||||
|
///
|
||||||
|
/// If set to true and wrapped in a parent widget like [Expanded] or
|
||||||
|
/// [SizedBox], the input will expand to fill the parent.
|
||||||
|
///
|
||||||
|
/// [maxLines] and [minLines] must both be null when this is set to true,
|
||||||
|
/// otherwise an error is thrown.
|
||||||
|
///
|
||||||
|
/// Defaults to false.
|
||||||
|
///
|
||||||
|
/// See the examples in [maxLines] for the complete picture of how [maxLines],
|
||||||
|
/// [minLines], and [expands] interact to produce various behaviors.
|
||||||
|
///
|
||||||
|
/// Input that matches the height of its parent
|
||||||
|
/// ```dart
|
||||||
|
/// Expanded(
|
||||||
|
/// child: TextField(maxLines: null, expands: true),
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// {@endtemplate}
|
||||||
|
final bool expands;
|
||||||
|
|
||||||
/// {@template flutter.widgets.editableText.autofocus}
|
/// {@template flutter.widgets.editableText.autofocus}
|
||||||
/// Whether this text field should focus itself if nothing else is already
|
/// Whether this text field should focus itself if nothing else is already
|
||||||
/// focused.
|
/// focused.
|
||||||
@ -676,6 +754,8 @@ class EditableText extends StatefulWidget {
|
|||||||
properties.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
|
properties.add(DiagnosticsProperty<Locale>('locale', locale, defaultValue: null));
|
||||||
properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null));
|
properties.add(DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null));
|
||||||
properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
|
properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
|
||||||
|
properties.add(IntProperty('minLines', minLines, defaultValue: null));
|
||||||
|
properties.add(DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
|
||||||
properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
|
properties.add(DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
|
||||||
properties.add(DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: null));
|
properties.add(DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: null));
|
||||||
}
|
}
|
||||||
@ -795,7 +875,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
// If this is a multiline EditableText, do nothing for a "newline"
|
// If this is a multiline EditableText, do nothing for a "newline"
|
||||||
// action; The newline is already inserted. Otherwise, finalize
|
// action; The newline is already inserted. Otherwise, finalize
|
||||||
// editing.
|
// editing.
|
||||||
if (widget.maxLines == 1)
|
if (!_isMultiline)
|
||||||
_finalizeEditing(true);
|
_finalizeEditing(true);
|
||||||
break;
|
break;
|
||||||
case TextInputAction.done:
|
case TextInputAction.done:
|
||||||
@ -1333,6 +1413,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
: _cursorVisibilityNotifier,
|
: _cursorVisibilityNotifier,
|
||||||
hasFocus: _hasFocus,
|
hasFocus: _hasFocus,
|
||||||
maxLines: widget.maxLines,
|
maxLines: widget.maxLines,
|
||||||
|
minLines: widget.minLines,
|
||||||
|
expands: widget.expands,
|
||||||
strutStyle: widget.strutStyle,
|
strutStyle: widget.strutStyle,
|
||||||
selectionColor: widget.selectionColor,
|
selectionColor: widget.selectionColor,
|
||||||
textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
|
textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
|
||||||
@ -1403,6 +1485,8 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
this.showCursor,
|
this.showCursor,
|
||||||
this.hasFocus,
|
this.hasFocus,
|
||||||
this.maxLines,
|
this.maxLines,
|
||||||
|
this.minLines,
|
||||||
|
this.expands,
|
||||||
this.strutStyle,
|
this.strutStyle,
|
||||||
this.selectionColor,
|
this.selectionColor,
|
||||||
this.textScaleFactor,
|
this.textScaleFactor,
|
||||||
@ -1433,6 +1517,8 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
final ValueNotifier<bool> showCursor;
|
final ValueNotifier<bool> showCursor;
|
||||||
final bool hasFocus;
|
final bool hasFocus;
|
||||||
final int maxLines;
|
final int maxLines;
|
||||||
|
final int minLines;
|
||||||
|
final bool expands;
|
||||||
final StrutStyle strutStyle;
|
final StrutStyle strutStyle;
|
||||||
final Color selectionColor;
|
final Color selectionColor;
|
||||||
final double textScaleFactor;
|
final double textScaleFactor;
|
||||||
@ -1462,6 +1548,8 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
showCursor: showCursor,
|
showCursor: showCursor,
|
||||||
hasFocus: hasFocus,
|
hasFocus: hasFocus,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
|
minLines: minLines,
|
||||||
|
expands: expands,
|
||||||
strutStyle: strutStyle,
|
strutStyle: strutStyle,
|
||||||
selectionColor: selectionColor,
|
selectionColor: selectionColor,
|
||||||
textScaleFactor: textScaleFactor,
|
textScaleFactor: textScaleFactor,
|
||||||
@ -1492,6 +1580,8 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
..showCursor = showCursor
|
..showCursor = showCursor
|
||||||
..hasFocus = hasFocus
|
..hasFocus = hasFocus
|
||||||
..maxLines = maxLines
|
..maxLines = maxLines
|
||||||
|
..minLines = minLines
|
||||||
|
..expands = expands
|
||||||
..strutStyle = strutStyle
|
..strutStyle = strutStyle
|
||||||
..selectionColor = selectionColor
|
..selectionColor = selectionColor
|
||||||
..textScaleFactor = textScaleFactor
|
..textScaleFactor = textScaleFactor
|
||||||
|
@ -993,6 +993,100 @@ void main() {
|
|||||||
expect(tester.getTopRight(find.text('text')).dx, lessThanOrEqualTo(tester.getTopRight(find.byKey(sKey)).dx));
|
expect(tester.getTopRight(find.text('text')).dx, lessThanOrEqualTo(tester.getTopRight(find.byKey(sKey)).dx));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('InputDecorator tall prefix', (WidgetTester tester) async {
|
||||||
|
const Key pKey = Key('p');
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildInputDecorator(
|
||||||
|
// isEmpty: false (default)
|
||||||
|
// isFocused: false (default)
|
||||||
|
decoration: InputDecoration(
|
||||||
|
prefix: Container(
|
||||||
|
key: pKey,
|
||||||
|
height: 100,
|
||||||
|
width: 10,
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
),
|
||||||
|
// Set the fontSize so that everything works out to whole numbers.
|
||||||
|
child: const Text(
|
||||||
|
'text',
|
||||||
|
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Overall height for this InputDecorator is ~127.2dps because
|
||||||
|
// the prefix is 100dps tall, but it aligns with the input's baseline,
|
||||||
|
// overlapping the input a bit.
|
||||||
|
// 12 - top padding
|
||||||
|
// 100 - total height of prefix
|
||||||
|
// -16 - input prefix overlap (distance input top to baseline, not exact)
|
||||||
|
// 20 - input text (ahem font size 16dps)
|
||||||
|
// 0 - bottom prefix/suffix padding
|
||||||
|
// 12 - bottom padding
|
||||||
|
|
||||||
|
expect(tester.getSize(find.byType(InputDecorator)).width, 800.0);
|
||||||
|
expect(tester.getSize(find.byType(InputDecorator)).height, closeTo(128.0, .0001));
|
||||||
|
expect(tester.getSize(find.text('text')).height, 20.0);
|
||||||
|
expect(tester.getSize(find.byKey(pKey)).height, 100.0);
|
||||||
|
expect(tester.getTopLeft(find.text('text')).dy, closeTo(96, .0001)); // 12 + 100 - 16
|
||||||
|
expect(tester.getTopLeft(find.byKey(pKey)).dy, 12.0);
|
||||||
|
|
||||||
|
// layout is a row: [prefix text suffix]
|
||||||
|
expect(tester.getTopLeft(find.byKey(pKey)).dx, 12.0);
|
||||||
|
expect(tester.getTopRight(find.byKey(pKey)).dx, tester.getTopLeft(find.text('text')).dx);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('InputDecorator tall prefix with border', (WidgetTester tester) async {
|
||||||
|
const Key pKey = Key('p');
|
||||||
|
await tester.pumpWidget(
|
||||||
|
buildInputDecorator(
|
||||||
|
// isEmpty: false (default)
|
||||||
|
// isFocused: false (default)
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
prefix: Container(
|
||||||
|
key: pKey,
|
||||||
|
height: 100,
|
||||||
|
width: 10,
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
),
|
||||||
|
// Set the fontSize so that everything works out to whole numbers.
|
||||||
|
child: const Text(
|
||||||
|
'text',
|
||||||
|
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Overall height for this InputDecorator is ~127.2dps because
|
||||||
|
// the prefix is 100dps tall, but it aligns with the input's baseline,
|
||||||
|
// overlapping the input a bit.
|
||||||
|
// 24 - top padding
|
||||||
|
// 100 - total height of prefix
|
||||||
|
// -16 - input prefix overlap (distance input top to baseline, not exact)
|
||||||
|
// 20 - input text (ahem font size 16dps)
|
||||||
|
// 0 - bottom prefix/suffix padding
|
||||||
|
// 16 - bottom padding
|
||||||
|
// When a border is present, the input text and prefix/suffix are centered
|
||||||
|
// within the input. Here, that will be content of height 106, including 2
|
||||||
|
// extra pixels of space, centered within an input of height 144. That gives
|
||||||
|
// 19 pixels of space on each side of the content, so the prefix is
|
||||||
|
// positioned at 19, and the text is at 19+100-16=103.
|
||||||
|
|
||||||
|
expect(tester.getSize(find.byType(InputDecorator)).width, 800.0);
|
||||||
|
expect(tester.getSize(find.byType(InputDecorator)).height, closeTo(144, .0001));
|
||||||
|
expect(tester.getSize(find.text('text')).height, 20.0);
|
||||||
|
expect(tester.getSize(find.byKey(pKey)).height, 100.0);
|
||||||
|
expect(tester.getTopLeft(find.text('text')).dy, closeTo(103, .0001));
|
||||||
|
expect(tester.getTopLeft(find.byKey(pKey)).dy, 19.0);
|
||||||
|
|
||||||
|
// layout is a row: [prefix text suffix]
|
||||||
|
expect(tester.getTopLeft(find.byKey(pKey)).dx, 12.0);
|
||||||
|
expect(tester.getTopRight(find.byKey(pKey)).dx, tester.getTopLeft(find.text('text')).dx);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('InputDecorator prefixIcon/suffixIcon', (WidgetTester tester) async {
|
testWidgets('InputDecorator prefixIcon/suffixIcon', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
buildInputDecorator(
|
buildInputDecorator(
|
||||||
@ -1075,7 +1169,6 @@ void main() {
|
|||||||
expect(tester.getTopLeft(find.byKey(prefixKey)).dy, 0.0);
|
expect(tester.getTopLeft(find.byKey(prefixKey)).dy, 0.0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
testWidgets('counter text has correct right margin - LTR, not dense', (WidgetTester tester) async {
|
testWidgets('counter text has correct right margin - LTR, not dense', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
buildInputDecorator(
|
buildInputDecorator(
|
||||||
@ -1463,12 +1556,12 @@ void main() {
|
|||||||
testWidgets('InputDecoration outline shape with no border and no floating placeholder', (WidgetTester tester) async {
|
testWidgets('InputDecoration outline shape with no border and no floating placeholder', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
buildInputDecorator(
|
buildInputDecorator(
|
||||||
// isFocused: false (default)
|
// isFocused: false (default)
|
||||||
isEmpty: true,
|
isEmpty: true,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
border: OutlineInputBorder(borderSide: BorderSide.none),
|
border: OutlineInputBorder(borderSide: BorderSide.none),
|
||||||
hasFloatingPlaceholder: false,
|
hasFloatingPlaceholder: false,
|
||||||
labelText: 'label',
|
labelText: 'label',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -6,6 +6,49 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
testWidgets('Dialog interaction', (WidgetTester tester) async {
|
||||||
|
expect(tester.testTextInput.isVisible, isFalse);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MaterialApp(
|
||||||
|
home: Material(
|
||||||
|
child: Center(
|
||||||
|
child: TextField(
|
||||||
|
autofocus: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tester.testTextInput.isVisible, isTrue);
|
||||||
|
|
||||||
|
final BuildContext context = tester.element(find.byType(TextField));
|
||||||
|
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) => const SimpleDialog(title: Text('Dialog')),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(tester.testTextInput.isVisible, isFalse);
|
||||||
|
|
||||||
|
Navigator.of(tester.element(find.text('Dialog'))).pop();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(tester.testTextInput.isVisible, isFalse);
|
||||||
|
|
||||||
|
await tester.tap(find.byType(TextField));
|
||||||
|
await tester.idle();
|
||||||
|
|
||||||
|
expect(tester.testTextInput.isVisible, isTrue);
|
||||||
|
|
||||||
|
await tester.pumpWidget(Container());
|
||||||
|
|
||||||
|
expect(tester.testTextInput.isVisible, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Request focus shows keyboard', (WidgetTester tester) async {
|
testWidgets('Request focus shows keyboard', (WidgetTester tester) async {
|
||||||
final FocusNode focusNode = FocusNode();
|
final FocusNode focusNode = FocusNode();
|
||||||
|
|
||||||
@ -89,49 +132,6 @@ void main() {
|
|||||||
expect(tester.testTextInput.isVisible, isFalse);
|
expect(tester.testTextInput.isVisible, isFalse);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Dialog interaction', (WidgetTester tester) async {
|
|
||||||
expect(tester.testTextInput.isVisible, isFalse);
|
|
||||||
|
|
||||||
await tester.pumpWidget(
|
|
||||||
const MaterialApp(
|
|
||||||
home: Material(
|
|
||||||
child: Center(
|
|
||||||
child: TextField(
|
|
||||||
autofocus: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(tester.testTextInput.isVisible, isTrue);
|
|
||||||
|
|
||||||
final BuildContext context = tester.element(find.byType(TextField));
|
|
||||||
|
|
||||||
showDialog<void>(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) => const SimpleDialog(title: Text('Dialog')),
|
|
||||||
);
|
|
||||||
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
expect(tester.testTextInput.isVisible, isFalse);
|
|
||||||
|
|
||||||
Navigator.of(tester.element(find.text('Dialog'))).pop();
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
expect(tester.testTextInput.isVisible, isFalse);
|
|
||||||
|
|
||||||
await tester.tap(find.byType(TextField));
|
|
||||||
await tester.idle();
|
|
||||||
|
|
||||||
expect(tester.testTextInput.isVisible, isTrue);
|
|
||||||
|
|
||||||
await tester.pumpWidget(Container());
|
|
||||||
|
|
||||||
expect(tester.testTextInput.isVisible, isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('Focus triggers keep-alive', (WidgetTester tester) async {
|
testWidgets('Focus triggers keep-alive', (WidgetTester tester) async {
|
||||||
final FocusNode focusNode = FocusNode();
|
final FocusNode focusNode = FocusNode();
|
||||||
|
|
||||||
|
@ -174,6 +174,24 @@ void main() {
|
|||||||
debugResetSemanticsIdCounter();
|
debugResetSemanticsIdCounter();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final Key textFieldKey = UniqueKey();
|
||||||
|
Widget textFieldBuilder({
|
||||||
|
int maxLines = 1,
|
||||||
|
int minLines,
|
||||||
|
}) {
|
||||||
|
return boilerplate(
|
||||||
|
child: TextField(
|
||||||
|
key: textFieldKey,
|
||||||
|
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
||||||
|
maxLines: maxLines,
|
||||||
|
minLines: minLines,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Placeholder',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
testWidgets('TextField passes onEditingComplete to EditableText', (WidgetTester tester) async {
|
testWidgets('TextField passes onEditingComplete to EditableText', (WidgetTester tester) async {
|
||||||
final VoidCallback onEditingComplete = () {};
|
final VoidCallback onEditingComplete = () {};
|
||||||
|
|
||||||
@ -883,23 +901,8 @@ void main() {
|
|||||||
expect(controller.selection.isCollapsed, false);
|
expect(controller.selection.isCollapsed, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('Multiline text will wrap up to maxLines', (WidgetTester tester) async {
|
testWidgets('TextField height with minLines unset', (WidgetTester tester) async {
|
||||||
final Key textFieldKey = UniqueKey();
|
await tester.pumpWidget(textFieldBuilder());
|
||||||
|
|
||||||
Widget builder(int maxLines) {
|
|
||||||
return boilerplate(
|
|
||||||
child: TextField(
|
|
||||||
key: textFieldKey,
|
|
||||||
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
|
||||||
maxLines: maxLines,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
hintText: 'Placeholder',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await tester.pumpWidget(builder(null));
|
|
||||||
|
|
||||||
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
|
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
|
||||||
|
|
||||||
@ -907,46 +910,278 @@ void main() {
|
|||||||
final Size emptyInputSize = inputBox.size;
|
final Size emptyInputSize = inputBox.size;
|
||||||
|
|
||||||
await tester.enterText(find.byType(TextField), 'No wrapping here.');
|
await tester.enterText(find.byType(TextField), 'No wrapping here.');
|
||||||
await tester.pumpWidget(builder(null));
|
await tester.pumpWidget(textFieldBuilder());
|
||||||
expect(findInputBox(), equals(inputBox));
|
expect(findInputBox(), equals(inputBox));
|
||||||
expect(inputBox.size, equals(emptyInputSize));
|
expect(inputBox.size, equals(emptyInputSize));
|
||||||
|
|
||||||
await tester.pumpWidget(builder(3));
|
// Even when entering multiline text, TextField doesn't grow. It's a single
|
||||||
|
// line input.
|
||||||
|
await tester.enterText(find.byType(TextField), kThreeLines);
|
||||||
|
await tester.pumpWidget(textFieldBuilder());
|
||||||
expect(findInputBox(), equals(inputBox));
|
expect(findInputBox(), equals(inputBox));
|
||||||
expect(inputBox.size, greaterThan(emptyInputSize));
|
expect(inputBox.size, equals(emptyInputSize));
|
||||||
|
|
||||||
|
// maxLines: 3 makes the TextField 3 lines tall
|
||||||
|
await tester.enterText(find.byType(TextField), '');
|
||||||
|
await tester.pumpWidget(textFieldBuilder(maxLines: 3));
|
||||||
|
expect(findInputBox(), equals(inputBox));
|
||||||
|
expect(inputBox.size.height, greaterThan(emptyInputSize.height));
|
||||||
|
expect(inputBox.size.width, emptyInputSize.width);
|
||||||
|
|
||||||
final Size threeLineInputSize = inputBox.size;
|
final Size threeLineInputSize = inputBox.size;
|
||||||
|
|
||||||
|
// Filling with 3 lines of text stays the same size
|
||||||
await tester.enterText(find.byType(TextField), kThreeLines);
|
await tester.enterText(find.byType(TextField), kThreeLines);
|
||||||
await tester.pumpWidget(builder(null));
|
await tester.pumpWidget(textFieldBuilder(maxLines: 3));
|
||||||
expect(findInputBox(), equals(inputBox));
|
|
||||||
expect(inputBox.size, greaterThan(emptyInputSize));
|
|
||||||
|
|
||||||
await tester.enterText(find.byType(TextField), kThreeLines);
|
|
||||||
await tester.pumpWidget(builder(null));
|
|
||||||
expect(findInputBox(), equals(inputBox));
|
expect(findInputBox(), equals(inputBox));
|
||||||
expect(inputBox.size, threeLineInputSize);
|
expect(inputBox.size, threeLineInputSize);
|
||||||
|
|
||||||
// An extra line won't increase the size because we max at 3.
|
// An extra line won't increase the size because we max at 3.
|
||||||
await tester.enterText(find.byType(TextField), kMoreThanFourLines);
|
await tester.enterText(find.byType(TextField), kMoreThanFourLines);
|
||||||
await tester.pumpWidget(builder(3));
|
await tester.pumpWidget(textFieldBuilder(maxLines: 3));
|
||||||
expect(findInputBox(), equals(inputBox));
|
expect(findInputBox(), equals(inputBox));
|
||||||
expect(inputBox.size, threeLineInputSize);
|
expect(inputBox.size, threeLineInputSize);
|
||||||
|
|
||||||
// But now it will... but it will max at four
|
// But now it will... but it will max at four
|
||||||
await tester.enterText(find.byType(TextField), kMoreThanFourLines);
|
await tester.enterText(find.byType(TextField), kMoreThanFourLines);
|
||||||
await tester.pumpWidget(builder(4));
|
await tester.pumpWidget(textFieldBuilder(maxLines: 4));
|
||||||
expect(findInputBox(), equals(inputBox));
|
expect(findInputBox(), equals(inputBox));
|
||||||
expect(inputBox.size, greaterThan(threeLineInputSize));
|
expect(inputBox.size.height, greaterThan(threeLineInputSize.height));
|
||||||
|
expect(inputBox.size.width, threeLineInputSize.width);
|
||||||
|
|
||||||
final Size fourLineInputSize = inputBox.size;
|
final Size fourLineInputSize = inputBox.size;
|
||||||
|
|
||||||
// Now it won't max out until the end
|
// Now it won't max out until the end
|
||||||
await tester.pumpWidget(builder(null));
|
await tester.enterText(find.byType(TextField), '');
|
||||||
|
await tester.pumpWidget(textFieldBuilder(maxLines: null));
|
||||||
expect(findInputBox(), equals(inputBox));
|
expect(findInputBox(), equals(inputBox));
|
||||||
expect(inputBox.size, greaterThan(fourLineInputSize));
|
expect(inputBox.size, equals(emptyInputSize));
|
||||||
|
await tester.enterText(find.byType(TextField), kThreeLines);
|
||||||
|
await tester.pump();
|
||||||
|
expect(inputBox.size, equals(threeLineInputSize));
|
||||||
|
await tester.enterText(find.byType(TextField), kMoreThanFourLines);
|
||||||
|
await tester.pump();
|
||||||
|
expect(inputBox.size.height, greaterThan(fourLineInputSize.height));
|
||||||
|
expect(inputBox.size.width, fourLineInputSize.width);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('TextField height with minLines and maxLines', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(textFieldBuilder());
|
||||||
|
|
||||||
|
RenderBox findInputBox() => tester.renderObject(find.byKey(textFieldKey));
|
||||||
|
|
||||||
|
final RenderBox inputBox = findInputBox();
|
||||||
|
final Size emptyInputSize = inputBox.size;
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextField), 'No wrapping here.');
|
||||||
|
await tester.pumpWidget(textFieldBuilder());
|
||||||
|
expect(findInputBox(), equals(inputBox));
|
||||||
|
expect(inputBox.size, equals(emptyInputSize));
|
||||||
|
|
||||||
|
// min and max set to same value locks height to value.
|
||||||
|
await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: 3));
|
||||||
|
expect(findInputBox(), equals(inputBox));
|
||||||
|
expect(inputBox.size.height, greaterThan(emptyInputSize.height));
|
||||||
|
expect(inputBox.size.width, emptyInputSize.width);
|
||||||
|
|
||||||
|
final Size threeLineInputSize = inputBox.size;
|
||||||
|
|
||||||
|
// maxLines: null with minLines set grows beyond minLines
|
||||||
|
await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: null));
|
||||||
|
expect(findInputBox(), equals(inputBox));
|
||||||
|
expect(inputBox.size, threeLineInputSize);
|
||||||
|
await tester.enterText(find.byType(TextField), kMoreThanFourLines);
|
||||||
|
await tester.pump();
|
||||||
|
expect(inputBox.size.height, greaterThan(threeLineInputSize.height));
|
||||||
|
expect(inputBox.size.width, threeLineInputSize.width);
|
||||||
|
|
||||||
|
// With minLines and maxLines set, input will expand through the range
|
||||||
|
await tester.enterText(find.byType(TextField), '');
|
||||||
|
await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: 4));
|
||||||
|
expect(findInputBox(), equals(inputBox));
|
||||||
|
expect(inputBox.size, equals(threeLineInputSize));
|
||||||
|
await tester.enterText(find.byType(TextField), kMoreThanFourLines);
|
||||||
|
await tester.pump();
|
||||||
|
expect(inputBox.size.height, greaterThan(threeLineInputSize.height));
|
||||||
|
expect(inputBox.size.width, threeLineInputSize.width);
|
||||||
|
|
||||||
|
// minLines can't be greater than maxLines.
|
||||||
|
expect(() async {
|
||||||
|
await tester.pumpWidget(textFieldBuilder(minLines: 3, maxLines: 2));
|
||||||
|
}, throwsAssertionError);
|
||||||
|
expect(() async {
|
||||||
|
await tester.pumpWidget(textFieldBuilder(minLines: 3));
|
||||||
|
}, throwsAssertionError);
|
||||||
|
|
||||||
|
// maxLines defaults to 1 and can't be less than minLines
|
||||||
|
expect(() async {
|
||||||
|
await tester.pumpWidget(textFieldBuilder(minLines: 3));
|
||||||
|
}, throwsAssertionError);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Multiline text when wrapped in Expanded', (WidgetTester tester) async {
|
||||||
|
Widget expandedTextFieldBuilder({
|
||||||
|
int maxLines = 1,
|
||||||
|
int minLines,
|
||||||
|
bool expands = false,
|
||||||
|
}) {
|
||||||
|
return boilerplate(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
key: textFieldKey,
|
||||||
|
style: const TextStyle(color: Colors.black, fontSize: 34.0),
|
||||||
|
maxLines: maxLines,
|
||||||
|
minLines: minLines,
|
||||||
|
expands: expands,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
hintText: 'Placeholder',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.pumpWidget(expandedTextFieldBuilder());
|
||||||
|
|
||||||
|
RenderBox findBorder() {
|
||||||
|
return tester.renderObject(find.descendant(
|
||||||
|
of: find.byType(InputDecorator),
|
||||||
|
matching: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_BorderContainer'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
final RenderBox border = findBorder();
|
||||||
|
|
||||||
|
// Without expanded: true and maxLines: null, the TextField does not expand
|
||||||
|
// to fill its parent when wrapped in an Expanded widget.
|
||||||
|
final Size unexpandedInputSize = border.size;
|
||||||
|
|
||||||
|
// It does expand to fill its parent when expands: true, maxLines: null, and
|
||||||
|
// it's wrapped in an Expanded widget.
|
||||||
|
await tester.pumpWidget(expandedTextFieldBuilder(expands: true, maxLines: null));
|
||||||
|
expect(border.size.height, greaterThan(unexpandedInputSize.height));
|
||||||
|
expect(border.size.width, unexpandedInputSize.width);
|
||||||
|
|
||||||
|
// min/maxLines that is not null and expands: true contradict each other.
|
||||||
|
expect(() async {
|
||||||
|
await tester.pumpWidget(expandedTextFieldBuilder(expands: true, maxLines: 4));
|
||||||
|
}, throwsAssertionError);
|
||||||
|
expect(() async {
|
||||||
|
await tester.pumpWidget(expandedTextFieldBuilder(expands: true, minLines: 1, maxLines: null));
|
||||||
|
}, throwsAssertionError);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Growable TextField when content height exceeds parent', (WidgetTester tester) async {
|
||||||
|
const double height = 200.0;
|
||||||
|
const double padding = 24.0;
|
||||||
|
|
||||||
|
Widget containedTextFieldBuilder({
|
||||||
|
Widget counter,
|
||||||
|
String helperText,
|
||||||
|
String labelText,
|
||||||
|
Widget prefix,
|
||||||
|
}) {
|
||||||
|
return boilerplate(
|
||||||
|
child: Container(
|
||||||
|
height: height,
|
||||||
|
child: TextField(
|
||||||
|
key: textFieldKey,
|
||||||
|
maxLines: null,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
counter: counter,
|
||||||
|
helperText: helperText,
|
||||||
|
labelText: labelText,
|
||||||
|
prefix: prefix,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.pumpWidget(containedTextFieldBuilder());
|
||||||
|
RenderBox findEditableText() => tester.renderObject(find.byType(EditableText));
|
||||||
|
|
||||||
|
final RenderBox inputBox = findEditableText();
|
||||||
|
|
||||||
|
// With no decoration and when overflowing with content, the EditableText
|
||||||
|
// takes up the full height minus the padding, so the input fits perfectly
|
||||||
|
// inside the parent.
|
||||||
|
await tester.enterText(find.byType(TextField), 'a\n' * 11);
|
||||||
|
await tester.pump();
|
||||||
|
expect(findEditableText(), equals(inputBox));
|
||||||
|
expect(inputBox.size.height, height - padding);
|
||||||
|
|
||||||
|
// Adding a counter causes the EditableText to shrink to fit the counter
|
||||||
|
// inside the parent as well.
|
||||||
|
const double counterHeight = 40.0;
|
||||||
|
const double subtextGap = 8.0 * 2;
|
||||||
|
const double counterSpace = counterHeight + subtextGap;
|
||||||
|
await tester.pumpWidget(containedTextFieldBuilder(
|
||||||
|
counter: Container(height: counterHeight),
|
||||||
|
));
|
||||||
|
expect(findEditableText(), equals(inputBox));
|
||||||
|
expect(inputBox.size.height, height - padding - counterSpace);
|
||||||
|
|
||||||
|
// Including helperText causes the EditableText to shrink to fit the text
|
||||||
|
// inside the parent as well.
|
||||||
|
await tester.pumpWidget(containedTextFieldBuilder(
|
||||||
|
helperText: 'I am helperText',
|
||||||
|
));
|
||||||
|
expect(findEditableText(), equals(inputBox));
|
||||||
|
const double helperTextSpace = 28.0;
|
||||||
|
expect(inputBox.size.height, height - padding - helperTextSpace);
|
||||||
|
|
||||||
|
// When both helperText and counter are present, EditableText shrinks by the
|
||||||
|
// height of the taller of the two in order to fit both within the parent.
|
||||||
|
await tester.pumpWidget(containedTextFieldBuilder(
|
||||||
|
counter: Container(height: counterHeight),
|
||||||
|
helperText: 'I am helperText',
|
||||||
|
));
|
||||||
|
expect(findEditableText(), equals(inputBox));
|
||||||
|
expect(inputBox.size.height, height - padding - counterSpace);
|
||||||
|
|
||||||
|
// When a label is present, EditableText shrinks to fit it at the top so
|
||||||
|
// that the bottom of the input still lines up perfectly with the parent.
|
||||||
|
await tester.pumpWidget(containedTextFieldBuilder(
|
||||||
|
labelText: 'I am labelText',
|
||||||
|
));
|
||||||
|
const double labelSpace = 16.0;
|
||||||
|
expect(findEditableText(), equals(inputBox));
|
||||||
|
expect(inputBox.size.height, height - padding - labelSpace);
|
||||||
|
|
||||||
|
// When decoration is present on the top and bottom, EditableText shrinks to
|
||||||
|
// fit both inside the parent independently.
|
||||||
|
await tester.pumpWidget(containedTextFieldBuilder(
|
||||||
|
counter: Container(height: counterHeight),
|
||||||
|
labelText: 'I am labelText',
|
||||||
|
));
|
||||||
|
expect(findEditableText(), equals(inputBox));
|
||||||
|
expect(inputBox.size.height, height - padding - counterSpace - labelSpace);
|
||||||
|
|
||||||
|
// When a prefix or suffix is present in an input that's full of content,
|
||||||
|
// it is ignored and allowed to expand beyond the top of the input. Other
|
||||||
|
// top and bottom decoration is still respected.
|
||||||
|
await tester.pumpWidget(containedTextFieldBuilder(
|
||||||
|
counter: Container(height: counterHeight),
|
||||||
|
labelText: 'I am labelText',
|
||||||
|
prefix: Container(
|
||||||
|
width: 10,
|
||||||
|
height: 60,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
expect(findEditableText(), equals(inputBox));
|
||||||
|
expect(
|
||||||
|
inputBox.size.height,
|
||||||
|
height
|
||||||
|
- padding
|
||||||
|
- labelSpace
|
||||||
|
- counterSpace,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Multiline hint text will wrap up to maxLines', (WidgetTester tester) async {
|
testWidgets('Multiline hint text will wrap up to maxLines', (WidgetTester tester) async {
|
||||||
final Key textFieldKey = UniqueKey();
|
final Key textFieldKey = UniqueKey();
|
||||||
|
@ -56,6 +56,7 @@ void main() {
|
|||||||
' │ cursorColor: null\n'
|
' │ cursorColor: null\n'
|
||||||
' │ showCursor: ValueNotifier<bool>#00000(false)\n'
|
' │ showCursor: ValueNotifier<bool>#00000(false)\n'
|
||||||
' │ maxLines: 1\n'
|
' │ maxLines: 1\n'
|
||||||
|
' │ minLines: null\n'
|
||||||
' │ selectionColor: null\n'
|
' │ selectionColor: null\n'
|
||||||
' │ textScaleFactor: 1.0\n'
|
' │ textScaleFactor: 1.0\n'
|
||||||
' │ locale: ja_JP\n'
|
' │ locale: ja_JP\n'
|
||||||
|
Loading…
Reference in New Issue
Block a user