Unverified Commit b798b27b authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

Text field vertical align (#34355)

Adds the `textAlignVertical` param to TextField and InputDecorator, allowing arbitrary vertical positioning of text in its input.
parent 2b0c3518
...@@ -574,7 +574,6 @@ class _RenderDecorationLayout { ...@@ -574,7 +574,6 @@ class _RenderDecorationLayout {
const _RenderDecorationLayout({ const _RenderDecorationLayout({
this.boxToBaseline, this.boxToBaseline,
this.inputBaseline, // for InputBorderType.underline this.inputBaseline, // for InputBorderType.underline
this.outlineBaseline, // for InputBorderType.outline
this.subtextBaseline, this.subtextBaseline,
this.containerHeight, this.containerHeight,
this.subtextHeight, this.subtextHeight,
...@@ -582,7 +581,6 @@ class _RenderDecorationLayout { ...@@ -582,7 +581,6 @@ class _RenderDecorationLayout {
final Map<RenderBox, double> boxToBaseline; final Map<RenderBox, double> boxToBaseline;
final double inputBaseline; final double inputBaseline;
final double outlineBaseline;
final double subtextBaseline; // helper/error counter final double subtextBaseline; // helper/error counter
final double containerHeight; final double containerHeight;
final double subtextHeight; final double subtextHeight;
...@@ -596,6 +594,7 @@ class _RenderDecoration extends RenderBox { ...@@ -596,6 +594,7 @@ class _RenderDecoration extends RenderBox {
@required TextBaseline textBaseline, @required TextBaseline textBaseline,
@required bool isFocused, @required bool isFocused,
@required bool expands, @required bool expands,
TextAlignVertical textAlignVertical,
}) : assert(decoration != null), }) : assert(decoration != null),
assert(textDirection != null), assert(textDirection != null),
assert(textBaseline != null), assert(textBaseline != null),
...@@ -603,6 +602,7 @@ class _RenderDecoration extends RenderBox { ...@@ -603,6 +602,7 @@ class _RenderDecoration extends RenderBox {
_decoration = decoration, _decoration = decoration,
_textDirection = textDirection, _textDirection = textDirection,
_textBaseline = textBaseline, _textBaseline = textBaseline,
_textAlignVertical = textAlignVertical,
_isFocused = isFocused, _isFocused = isFocused,
_expands = expands; _expands = expands;
...@@ -746,6 +746,27 @@ class _RenderDecoration extends RenderBox { ...@@ -746,6 +746,27 @@ class _RenderDecoration extends RenderBox {
markNeedsLayout(); markNeedsLayout();
} }
TextAlignVertical get textAlignVertical {
if (_textAlignVertical == null) {
return _isOutlineAligned ? TextAlignVertical.center : TextAlignVertical.top;
}
return _textAlignVertical;
}
TextAlignVertical _textAlignVertical;
set textAlignVertical(TextAlignVertical value) {
assert(value != null);
if (_textAlignVertical == value) {
return;
}
// No need to relayout if the effective value is still the same.
if (textAlignVertical.y == value.y) {
_textAlignVertical = value;
return;
}
_textAlignVertical = value;
markNeedsLayout();
}
bool get isFocused => _isFocused; bool get isFocused => _isFocused;
bool _isFocused; bool _isFocused;
set isFocused(bool value) { set isFocused(bool value) {
...@@ -766,6 +787,12 @@ class _RenderDecoration extends RenderBox { ...@@ -766,6 +787,12 @@ class _RenderDecoration extends RenderBox {
markNeedsLayout(); markNeedsLayout();
} }
// Indicates that the decoration should be aligned to accommodate an outline
// border.
bool get _isOutlineAligned {
return !decoration.isCollapsed && decoration.border.isOutline;
}
@override @override
void attach(PipelineOwner owner) { void attach(PipelineOwner owner) {
super.attach(owner); super.attach(owner);
...@@ -862,7 +889,7 @@ class _RenderDecoration extends RenderBox { ...@@ -862,7 +889,7 @@ class _RenderDecoration extends RenderBox {
EdgeInsets get contentPadding => decoration.contentPadding; EdgeInsets get contentPadding => decoration.contentPadding;
// Lay out the given box if needed, and return its baseline // Lay out the given box if needed, and return its baseline.
double _layoutLineBox(RenderBox box, BoxConstraints constraints) { double _layoutLineBox(RenderBox box, BoxConstraints constraints) {
if (box == null) { if (box == null) {
return 0.0; return 0.0;
...@@ -1006,21 +1033,34 @@ class _RenderDecoration extends RenderBox { ...@@ -1006,21 +1033,34 @@ class _RenderDecoration extends RenderBox {
? maxContainerHeight ? maxContainerHeight
: math.min(contentHeight, maxContainerHeight); : math.min(contentHeight, maxContainerHeight);
// Always position the prefix/suffix in the same place (baseline). // Try to consider the prefix/suffix as part of the text when aligning it.
// If the prefix/suffix overflows however, allow it to extend outside of the
// input and align the remaining part of the text and prefix/suffix.
final double overflow = math.max(0, contentHeight - maxContainerHeight); final double overflow = math.max(0, contentHeight - maxContainerHeight);
final double baselineAdjustment = fixAboveInput - overflow; // Map textAlignVertical from -1:1 to 0:1 so that it can be used to scale
// the baseline from its minimum to maximum values.
final double textAlignVerticalFactor = (textAlignVertical.y + 1.0) / 2.0;
// Adjust to try to fit top overflow inside the input on an inverse scale of
// textAlignVertical, so that top aligned text adjusts the most and bottom
// aligned text doesn't adjust at all.
final double baselineAdjustment = fixAboveInput - overflow * (1 - textAlignVerticalFactor);
// The baselines that will be used to draw the actual input text content. // The baselines that will be used to draw the actual input text content.
final double inputBaseline = contentPadding.top final double topInputBaseline = contentPadding.top
+ topHeight + topHeight
+ inputInternalBaseline + inputInternalBaseline
+ baselineAdjustment; + baselineAdjustment;
// The text in the input when an outline border is present is centered final double maxContentHeight = containerHeight
// within the container less 2.0 dps at the top to account for the vertical - contentPadding.top
// space occupied by the floating label. - topHeight
final double outlineBaseline = inputInternalBaseline - contentPadding.bottom;
+ baselineAdjustment / 2 final double alignableHeight = fixAboveInput + inputHeight + fixBelowInput;
+ (containerHeight - (2.0 + inputHeight)) / 2.0; // When outline aligned, the baseline is vertically centered by default, and
// outlinePadding is used to account for the presence of the border and
// floating label.
final double outlinePadding = _isOutlineAligned ? 10.0 : 0;
final double textAlignVerticalOffset = (maxContentHeight - alignableHeight - outlinePadding) * textAlignVerticalFactor;
final double inputBaseline = topInputBaseline + textAlignVerticalOffset;
// Find the positions of the text below the input when it exists. // Find the positions of the text below the input when it exists.
double subtextCounterBaseline = 0; double subtextCounterBaseline = 0;
...@@ -1050,7 +1090,6 @@ class _RenderDecoration extends RenderBox { ...@@ -1050,7 +1090,6 @@ class _RenderDecoration extends RenderBox {
boxToBaseline: boxToBaseline, boxToBaseline: boxToBaseline,
containerHeight: containerHeight, containerHeight: containerHeight,
inputBaseline: inputBaseline, inputBaseline: inputBaseline,
outlineBaseline: outlineBaseline,
subtextBaseline: subtextBaseline, subtextBaseline: subtextBaseline,
subtextHeight: subtextHeight, subtextHeight: subtextHeight,
); );
...@@ -1160,9 +1199,7 @@ class _RenderDecoration extends RenderBox { ...@@ -1160,9 +1199,7 @@ class _RenderDecoration extends RenderBox {
final double right = overallWidth - contentPadding.right; final double right = overallWidth - contentPadding.right;
height = layout.containerHeight; height = layout.containerHeight;
baseline = decoration.isCollapsed || !decoration.border.isOutline baseline = layout.inputBaseline;
? layout.inputBaseline
: layout.outlineBaseline;
if (icon != null) { if (icon != null) {
double x; double x;
...@@ -1213,12 +1250,13 @@ class _RenderDecoration extends RenderBox { ...@@ -1213,12 +1250,13 @@ class _RenderDecoration extends RenderBox {
start -= contentPadding.left; start -= contentPadding.left;
start += centerLayout(prefixIcon, start); start += centerLayout(prefixIcon, start);
} }
if (label != null) if (label != null) {
if (decoration.alignLabelWithHint) { if (decoration.alignLabelWithHint) {
baselineLayout(label, start); baselineLayout(label, start);
} else { } else {
centerLayout(label, start); centerLayout(label, start);
} }
}
if (prefix != null) if (prefix != null)
start += baselineLayout(prefix, start); start += baselineLayout(prefix, start);
if (input != null) if (input != null)
...@@ -1512,6 +1550,7 @@ class _RenderDecorationElement extends RenderObjectElement { ...@@ -1512,6 +1550,7 @@ class _RenderDecorationElement extends RenderObjectElement {
class _Decorator extends RenderObjectWidget { class _Decorator extends RenderObjectWidget {
const _Decorator({ const _Decorator({
Key key, Key key,
@required this.textAlignVertical,
@required this.decoration, @required this.decoration,
@required this.textDirection, @required this.textDirection,
@required this.textBaseline, @required this.textBaseline,
...@@ -1526,6 +1565,7 @@ class _Decorator extends RenderObjectWidget { ...@@ -1526,6 +1565,7 @@ class _Decorator extends RenderObjectWidget {
final _Decoration decoration; final _Decoration decoration;
final TextDirection textDirection; final TextDirection textDirection;
final TextBaseline textBaseline; final TextBaseline textBaseline;
final TextAlignVertical textAlignVertical;
final bool isFocused; final bool isFocused;
final bool expands; final bool expands;
...@@ -1538,6 +1578,7 @@ class _Decorator extends RenderObjectWidget { ...@@ -1538,6 +1578,7 @@ class _Decorator extends RenderObjectWidget {
decoration: decoration, decoration: decoration,
textDirection: textDirection, textDirection: textDirection,
textBaseline: textBaseline, textBaseline: textBaseline,
textAlignVertical: textAlignVertical,
isFocused: isFocused, isFocused: isFocused,
expands: expands, expands: expands,
); );
...@@ -1612,6 +1653,7 @@ class InputDecorator extends StatefulWidget { ...@@ -1612,6 +1653,7 @@ class InputDecorator extends StatefulWidget {
this.decoration, this.decoration,
this.baseStyle, this.baseStyle,
this.textAlign, this.textAlign,
this.textAlignVertical,
this.isFocused = false, this.isFocused = false,
this.isHovering = false, this.isHovering = false,
this.expands = false, this.expands = false,
...@@ -1643,6 +1685,20 @@ class InputDecorator extends StatefulWidget { ...@@ -1643,6 +1685,20 @@ class InputDecorator extends StatefulWidget {
/// How the text in the decoration should be aligned horizontally. /// How the text in the decoration should be aligned horizontally.
final TextAlign textAlign; final TextAlign textAlign;
/// {@template flutter.widgets.inputDecorator.textAlignVertical}
/// How the text should be aligned vertically.
///
/// Determines the alignment of the baseline within the available space of
/// the input (typically a TextField). For example, TextAlignVertical.top will
/// place the baseline such that the text, and any attached decoration like
/// prefix and suffix, is as close to the top of the input as possible without
/// overflowing. The heights of the prefix and suffix are similarly included
/// for other alignment values. If the height is greater than the height
/// available, then the prefix and suffix will be allowed to overflow first
/// before the text scrolls.
/// {@endtemplate}
final TextAlignVertical textAlignVertical;
/// Whether the input field has focus. /// Whether the input field has focus.
/// ///
/// Determines the position of the label text and the color and weight of the /// Determines the position of the label text and the color and weight of the
...@@ -2148,6 +2204,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat ...@@ -2148,6 +2204,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
), ),
textDirection: textDirection, textDirection: textDirection,
textBaseline: textBaseline, textBaseline: textBaseline,
textAlignVertical: widget.textAlignVertical,
isFocused: isFocused, isFocused: isFocused,
expands: widget.expands, expands: widget.expands,
); );
...@@ -3468,3 +3525,42 @@ class InputDecorationTheme extends Diagnosticable { ...@@ -3468,3 +3525,42 @@ class InputDecorationTheme extends Diagnosticable {
properties.add(DiagnosticsProperty<bool>('alignLabelWithHint', alignLabelWithHint, defaultValue: defaultTheme.alignLabelWithHint)); properties.add(DiagnosticsProperty<bool>('alignLabelWithHint', alignLabelWithHint, defaultValue: defaultTheme.alignLabelWithHint));
} }
} }
/// The vertical alignment of text within an input.
///
/// A single [y] value that can range from -1.0 to 1.0. -1.0 aligns to the top
/// of the input so that the top of the first line of text fits within the input
/// and its padding. 0.0 aligns to the center of the input. 1.0 aligns so that
/// the bottom of the last line of text aligns with the bottom interior edge of
/// the input.
///
/// See also:
///
/// * [TextField.textAlignVertical], which is passed on to the [InputDecorator].
/// * [InputDecorator.textAlignVertical], which defines the alignment of
/// prefix, input, and suffix, within the [InputDecorator].
class TextAlignVertical {
/// Construct TextAlignVertical from any given y value.
const TextAlignVertical({
@required this.y,
}) : assert(y != null),
assert(y >= -1.0 && y <= 1.0);
/// A value ranging from -1.0 to 1.0 that defines the topmost and bottommost
/// locations of the top and bottom of the input text box.
final double y;
/// Aligns a TextField's input Text with the topmost location within the
/// TextField.
static const TextAlignVertical top = TextAlignVertical(y: -1.0);
/// Aligns a TextField's input Text to the center of the TextField.
static const TextAlignVertical center = TextAlignVertical(y: 0.0);
/// Aligns a TextField's input Text with the bottommost location within the
/// TextField.
static const TextAlignVertical bottom = TextAlignVertical(y: 1.0);
@override
String toString() {
return '$runtimeType(y: $y)';
}
}
...@@ -149,6 +149,7 @@ class TextField extends StatefulWidget { ...@@ -149,6 +149,7 @@ class TextField extends StatefulWidget {
this.style, this.style,
this.strutStyle, this.strutStyle,
this.textAlign = TextAlign.start, this.textAlign = TextAlign.start,
this.textAlignVertical,
this.textDirection, this.textDirection,
this.readOnly = false, this.readOnly = false,
this.showCursor, this.showCursor,
...@@ -278,6 +279,9 @@ class TextField extends StatefulWidget { ...@@ -278,6 +279,9 @@ class TextField extends StatefulWidget {
/// {@macro flutter.widgets.editableText.textAlign} /// {@macro flutter.widgets.editableText.textAlign}
final TextAlign textAlign; final TextAlign textAlign;
/// {@macro flutter.material.inputDecorator.textAlignVertical}
final TextAlignVertical textAlignVertical;
/// {@macro flutter.widgets.editableText.textDirection} /// {@macro flutter.widgets.editableText.textDirection}
final TextDirection textDirection; final TextDirection textDirection;
...@@ -506,6 +510,7 @@ class TextField extends StatefulWidget { ...@@ -506,6 +510,7 @@ class TextField extends StatefulWidget {
properties.add(EnumProperty<TextInputAction>('textInputAction', textInputAction, defaultValue: null)); properties.add(EnumProperty<TextInputAction>('textInputAction', textInputAction, defaultValue: null));
properties.add(EnumProperty<TextCapitalization>('textCapitalization', textCapitalization, defaultValue: TextCapitalization.none)); properties.add(EnumProperty<TextCapitalization>('textCapitalization', textCapitalization, defaultValue: TextCapitalization.none));
properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: TextAlign.start)); properties.add(EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: TextAlign.start));
properties.add(DiagnosticsProperty<TextAlignVertical>('textAlignVertical', textAlignVertical, defaultValue: null));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null)); properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0)); properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null)); properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius, defaultValue: null));
...@@ -1009,6 +1014,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi ...@@ -1009,6 +1014,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
decoration: _getEffectiveDecoration(), decoration: _getEffectiveDecoration(),
baseStyle: widget.style, baseStyle: widget.style,
textAlign: widget.textAlign, textAlign: widget.textAlign,
textAlignVertical: widget.textAlignVertical,
isHovering: _isHovering, isHovering: _isHovering,
isFocused: focusNode.hasFocus, isFocused: focusNode.hasFocus,
isEmpty: controller.value.text.isEmpty, isEmpty: controller.value.text.isEmpty,
......
...@@ -2153,8 +2153,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im ...@@ -2153,8 +2153,9 @@ abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin im
renderers.add(renderer); renderers.add(renderer);
} }
final Matrix4 transform = Matrix4.identity(); final Matrix4 transform = Matrix4.identity();
for (int index = renderers.length - 1; index > 0; index -= 1) for (int index = renderers.length - 1; index > 0; index -= 1) {
renderers[index].applyPaintTransform(renderers[index - 1], transform); renderers[index].applyPaintTransform(renderers[index - 1], transform);
}
return transform; return transform;
} }
......
...@@ -15,10 +15,12 @@ Widget buildInputDecorator({ ...@@ -15,10 +15,12 @@ Widget buildInputDecorator({
InputDecoration decoration = const InputDecoration(), InputDecoration decoration = const InputDecoration(),
InputDecorationTheme inputDecorationTheme, InputDecorationTheme inputDecorationTheme,
TextDirection textDirection = TextDirection.ltr, TextDirection textDirection = TextDirection.ltr,
bool expands = false,
bool isEmpty = false, bool isEmpty = false,
bool isFocused = false, bool isFocused = false,
bool isHovering = false, bool isHovering = false,
TextStyle baseStyle, TextStyle baseStyle,
TextAlignVertical textAlignVertical,
Widget child = const Text( Widget child = const Text(
'text', 'text',
style: TextStyle(fontFamily: 'Ahem', fontSize: 16.0), style: TextStyle(fontFamily: 'Ahem', fontSize: 16.0),
...@@ -37,11 +39,13 @@ Widget buildInputDecorator({ ...@@ -37,11 +39,13 @@ Widget buildInputDecorator({
child: Directionality( child: Directionality(
textDirection: textDirection, textDirection: textDirection,
child: InputDecorator( child: InputDecorator(
expands: expands,
decoration: decoration, decoration: decoration,
isEmpty: isEmpty, isEmpty: isEmpty,
isFocused: isFocused, isFocused: isFocused,
isHovering: isHovering, isHovering: isHovering,
baseStyle: baseStyle, baseStyle: baseStyle,
textAlignVertical: textAlignVertical,
child: child, child: child,
), ),
), ),
...@@ -277,13 +281,20 @@ void main() { ...@@ -277,13 +281,20 @@ void main() {
expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy); expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy);
}); });
testWidgets('InputDecorator alignLabelWithHint for multiline TextField no-strut', (WidgetTester tester) async { group('alignLabelWithHint', () {
group('expands false', () {
testWidgets('multiline TextField no-strut', (WidgetTester tester) async {
const String text = 'text';
final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController();
Widget buildFrame(bool alignLabelWithHint) { Widget buildFrame(bool alignLabelWithHint) {
return MaterialApp( return MaterialApp(
home: Material( home: Material(
child: Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: TextField( child: TextField(
controller: controller,
focusNode: focusNode,
maxLines: 8, maxLines: 8,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'label', labelText: 'label',
...@@ -297,26 +308,43 @@ void main() { ...@@ -297,26 +308,43 @@ void main() {
); );
} }
// alignLabelWithHint: false centers the label in the TextField // alignLabelWithHint: false centers the label in the TextField.
await tester.pumpWidget(buildFrame(false)); await tester.pumpWidget(buildFrame(false));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('label')).dy, 76.0); expect(tester.getTopLeft(find.text('label')).dy, 76.0);
expect(tester.getBottomLeft(find.text('label')).dy, 92.0); expect(tester.getBottomLeft(find.text('label')).dy, 92.0);
// Entering text still happens at the top.
await tester.enterText(find.byType(TextField), text);
expect(tester.getTopLeft(find.text(text)).dy, 28.0);
controller.clear();
focusNode.unfocus();
// alignLabelWithHint: true aligns the label with the hint. // alignLabelWithHint: true aligns the label with the hint.
await tester.pumpWidget(buildFrame(true)); await tester.pumpWidget(buildFrame(true));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy); expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy);
expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy); expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy);
// Entering text still happens at the top.
await tester.enterText(find.byType(TextField), text);
expect(tester.getTopLeft(find.text(text)).dy, 28.0);
controller.clear();
focusNode.unfocus();
}); });
testWidgets('InputDecorator alignLabelWithHint for multiline TextField', (WidgetTester tester) async { testWidgets('multiline TextField', (WidgetTester tester) async {
const String text = 'text';
final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController();
Widget buildFrame(bool alignLabelWithHint) { Widget buildFrame(bool alignLabelWithHint) {
return MaterialApp( return MaterialApp(
home: Material( home: Material(
child: Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: TextField( child: TextField(
controller: controller,
focusNode: focusNode,
maxLines: 8, maxLines: 8,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'label', labelText: 'label',
...@@ -329,17 +357,140 @@ void main() { ...@@ -329,17 +357,140 @@ void main() {
); );
} }
// alignLabelWithHint: false centers the label in the TextField // alignLabelWithHint: false centers the label in the TextField.
await tester.pumpWidget(buildFrame(false)); await tester.pumpWidget(buildFrame(false));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('label')).dy, 76.0); expect(tester.getTopLeft(find.text('label')).dy, 76.0);
expect(tester.getBottomLeft(find.text('label')).dy, 92.0); expect(tester.getBottomLeft(find.text('label')).dy, 92.0);
// Entering text still happens at the top.
await tester.enterText(find.byType(InputDecorator), text);
expect(tester.getTopLeft(find.text(text)).dy, 28.0);
controller.clear();
focusNode.unfocus();
// alignLabelWithHint: true aligns the label with the hint. // alignLabelWithHint: true aligns the label with the hint.
await tester.pumpWidget(buildFrame(true)); await tester.pumpWidget(buildFrame(true));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy); expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy);
expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy); expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy);
// Entering text still happens at the top.
await tester.enterText(find.byType(InputDecorator), text);
expect(tester.getTopLeft(find.text(text)).dy, 28.0);
controller.clear();
focusNode.unfocus();
});
});
group('expands true', () {
testWidgets('multiline TextField', (WidgetTester tester) async {
const String text = 'text';
final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController();
Widget buildFrame(bool alignLabelWithHint) {
return MaterialApp(
home: Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: TextField(
controller: controller,
focusNode: focusNode,
maxLines: null,
expands: true,
decoration: InputDecoration(
labelText: 'label',
alignLabelWithHint: alignLabelWithHint,
hintText: 'hint',
),
),
),
),
);
}
// alignLabelWithHint: false centers the label in the TextField.
await tester.pumpWidget(buildFrame(false));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('label')).dy, 292.0);
expect(tester.getBottomLeft(find.text('label')).dy, 308.0);
// Entering text still happens at the top.
await tester.enterText(find.byType(InputDecorator), text);
expect(tester.getTopLeft(find.text(text)).dy, 28.0);
controller.clear();
focusNode.unfocus();
// alignLabelWithHint: true aligns the label with the hint at the top.
await tester.pumpWidget(buildFrame(true));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('label')).dy, 28.0);
expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy);
expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy);
// Entering text still happens at the top.
await tester.enterText(find.byType(InputDecorator), text);
expect(tester.getTopLeft(find.text(text)).dy, 28.0);
controller.clear();
focusNode.unfocus();
});
testWidgets('multiline TextField with outline border', (WidgetTester tester) async {
const String text = 'text';
final FocusNode focusNode = FocusNode();
final TextEditingController controller = TextEditingController();
Widget buildFrame(bool alignLabelWithHint) {
return MaterialApp(
home: Material(
child: Directionality(
textDirection: TextDirection.ltr,
child: TextField(
controller: controller,
focusNode: focusNode,
maxLines: null,
expands: true,
decoration: InputDecoration(
labelText: 'label',
alignLabelWithHint: alignLabelWithHint,
hintText: 'hint',
border: OutlineInputBorder(
borderSide: const BorderSide(width: 1, color: Colors.black, style: BorderStyle.solid),
borderRadius: BorderRadius.circular(0),
),
),
),
),
),
);
}
// alignLabelWithHint: false centers the label in the TextField.
await tester.pumpWidget(buildFrame(false));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('label')).dy, 292.0);
expect(tester.getBottomLeft(find.text('label')).dy, 308.0);
// Entering text happens in the center as well.
await tester.enterText(find.byType(InputDecorator), text);
expect(tester.getTopLeft(find.text(text)).dy, 291.0);
controller.clear();
focusNode.unfocus();
// alignLabelWithHint: true aligns keeps the label in the center because
// that's where the hint is.
await tester.pumpWidget(buildFrame(true));
await tester.pumpAndSettle();
expect(tester.getTopLeft(find.text('label')).dy, 291.0);
expect(tester.getTopLeft(find.text('label')).dy, tester.getTopLeft(find.text('hint')).dy);
expect(tester.getBottomLeft(find.text('label')).dy, tester.getBottomLeft(find.text('hint')).dy);
// Entering text still happens in the center.
await tester.enterText(find.byType(InputDecorator), text);
expect(tester.getTopLeft(find.text(text)).dy, 291.0);
controller.clear();
focusNode.unfocus();
});
});
}); });
// Overall height for this InputDecorator is 40.0dps // Overall height for this InputDecorator is 40.0dps
...@@ -1178,6 +1329,471 @@ void main() { ...@@ -1178,6 +1329,471 @@ void main() {
expect(tester.getTopLeft(find.byKey(prefixKey)).dy, 0.0); expect(tester.getTopLeft(find.byKey(prefixKey)).dy, 0.0);
}); });
group('textAlignVertical position', () {
group('simple case', () {
testWidgets('align top (default)', (WidgetTester tester) async {
const String text = 'text';
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
expands: true, // so we have a tall input where align can vary
decoration: const InputDecoration(
filled: true,
),
textAlignVertical: TextAlignVertical.top, // default when no border
// Set the fontSize so that everything works out to whole numbers.
child: const Text(
text,
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
),
),
);
// Same as the default case above.
expect(tester.getTopLeft(find.text(text)).dy, closeTo(12.0, .0001));
});
testWidgets('align center', (WidgetTester tester) async {
const String text = 'text';
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
expands: true,
decoration: const InputDecoration(
filled: true,
),
textAlignVertical: TextAlignVertical.center,
// Set the fontSize so that everything works out to whole numbers.
child: const Text(
text,
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
),
),
);
// Below the top aligned case.
expect(tester.getTopLeft(find.text(text)).dy, closeTo(290.0, .0001));
});
testWidgets('align bottom', (WidgetTester tester) async {
const String text = 'text';
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
expands: true,
decoration: const InputDecoration(
filled: true,
),
textAlignVertical: TextAlignVertical.bottom,
// Set the fontSize so that everything works out to whole numbers.
child: const Text(
text,
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
),
),
);
// Below the center aligned case.
expect(tester.getTopLeft(find.text(text)).dy, closeTo(568.0, .0001));
});
testWidgets('align as a double', (WidgetTester tester) async {
const String text = 'text';
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
expands: true,
decoration: const InputDecoration(
filled: true,
),
textAlignVertical: const TextAlignVertical(y: 0.75),
// Set the fontSize so that everything works out to whole numbers.
child: const Text(
text,
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
),
),
);
// In between the center and bottom aligned cases.
expect(tester.getTopLeft(find.text(text)).dy, closeTo(498.5, .0001));
});
});
group('outline border', () {
testWidgets('align top', (WidgetTester tester) async {
const String text = 'text';
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
expands: true, // so we have a tall input where align can vary
decoration: const InputDecoration(
filled: true,
border: OutlineInputBorder(),
),
textAlignVertical: TextAlignVertical.top,
// Set the fontSize so that everything works out to whole numbers.
child: const Text(
text,
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
),
),
);
// Similar to the case without a border, but with a little extra room at
// the top to make room for the border.
expect(tester.getTopLeft(find.text(text)).dy, closeTo(24.0, .0001));
});
testWidgets('align center (default)', (WidgetTester tester) async {
const String text = 'text';
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
expands: true,
decoration: const InputDecoration(
filled: true,
border: OutlineInputBorder(),
),
textAlignVertical: TextAlignVertical.center, // default when border
// Set the fontSize so that everything works out to whole numbers.
child: const Text(
text,
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
),
),
);
// Below the top aligned case.
expect(tester.getTopLeft(find.text(text)).dy, closeTo(289.0, .0001));
});
testWidgets('align bottom', (WidgetTester tester) async {
const String text = 'text';
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
expands: true,
decoration: const InputDecoration(
filled: true,
border: OutlineInputBorder(),
),
textAlignVertical: TextAlignVertical.bottom,
// Set the fontSize so that everything works out to whole numbers.
child: const Text(
text,
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
),
),
);
// Below the center aligned case.
expect(tester.getTopLeft(find.text(text)).dy, closeTo(554.0, .0001));
});
});
group('prefix', () {
testWidgets('InputDecorator tall prefix align top', (WidgetTester tester) async {
const Key pKey = Key('p');
const String text = 'text';
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
decoration: InputDecoration(
prefix: Container(
key: pKey,
height: 100,
width: 10,
),
filled: true,
),
textAlignVertical: TextAlignVertical.top, // default when no border
// Set the fontSize so that everything works out to whole numbers.
child: const Text(
text,
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
),
),
);
// Same as the default case above.
expect(tester.getTopLeft(find.text(text)).dy, closeTo(96, .0001));
expect(tester.getTopLeft(find.byKey(pKey)).dy, 12.0);
});
testWidgets('InputDecorator tall prefix align center', (WidgetTester tester) async {
const Key pKey = Key('p');
const String text = 'text';
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
decoration: InputDecoration(
prefix: Container(
key: pKey,
height: 100,
width: 10,
),
filled: true,
),
textAlignVertical: TextAlignVertical.center,
// Set the fontSize so that everything works out to whole numbers.
child: const Text(
text,
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
),
),
);
// Same as the default case above.
expect(tester.getTopLeft(find.text(text)).dy, closeTo(96.0, .0001));
expect(tester.getTopLeft(find.byKey(pKey)).dy, 12.0);
});
testWidgets('InputDecorator tall prefix align bottom', (WidgetTester tester) async {
const Key pKey = Key('p');
const String text = 'text';
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
decoration: InputDecoration(
prefix: Container(
key: pKey,
height: 100,
width: 10,
),
filled: true,
),
textAlignVertical: TextAlignVertical.bottom,
// Set the fontSize so that everything works out to whole numbers.
child: const Text(
text,
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
),
),
);
// Top of the input + 100 prefix height - overlap
expect(tester.getTopLeft(find.text(text)).dy, closeTo(96.0, .0001));
expect(tester.getTopLeft(find.byKey(pKey)).dy, 12.0);
});
});
group('outline border and prefix', () {
testWidgets('InputDecorator tall prefix align center', (WidgetTester tester) async {
const Key pKey = Key('p');
const String text = 'text';
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
expands: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
prefix: Container(
key: pKey,
height: 100,
width: 10,
),
filled: true,
),
textAlignVertical: TextAlignVertical.center, // default when border
// Set the fontSize so that everything works out to whole numbers.
child: const Text(
text,
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
),
),
);
// In the middle of the expanded InputDecorator.
expect(tester.getTopLeft(find.text(text)).dy, closeTo(331.0, .0001));
expect(tester.getTopLeft(find.byKey(pKey)).dy, closeTo(247.0, .0001));
});
testWidgets('InputDecorator tall prefix with border align top', (WidgetTester tester) async {
const Key pKey = Key('p');
const String text = 'text';
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
expands: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
prefix: Container(
key: pKey,
height: 100,
width: 10,
),
filled: true,
),
textAlignVertical: TextAlignVertical.top,
// Set the fontSize so that everything works out to whole numbers.
child: const Text(
text,
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
),
),
);
// Above the center example.
expect(tester.getTopLeft(find.text(text)).dy, closeTo(108.0, .0001));
// The prefix is positioned at the top of the input, so this value is
// the same as the top aligned test without a prefix.
expect(tester.getTopLeft(find.byKey(pKey)).dy, 24.0);
});
testWidgets('InputDecorator tall prefix with border align bottom', (WidgetTester tester) async {
const Key pKey = Key('p');
const String text = 'text';
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
expands: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
prefix: Container(
key: pKey,
height: 100,
width: 10,
),
filled: true,
),
textAlignVertical: TextAlignVertical.bottom,
// Set the fontSize so that everything works out to whole numbers.
child: const Text(
text,
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
),
),
);
// Below the center example.
expect(tester.getTopLeft(find.text(text)).dy, closeTo(554.0, .0001));
expect(tester.getTopLeft(find.byKey(pKey)).dy, closeTo(470.0, .0001));
});
testWidgets('InputDecorator tall prefix with border align double', (WidgetTester tester) async {
const Key pKey = Key('p');
const String text = 'text';
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
expands: true,
decoration: InputDecoration(
border: const OutlineInputBorder(),
prefix: Container(
key: pKey,
height: 100,
width: 10,
),
filled: true,
),
textAlignVertical: const TextAlignVertical(y: 0.1),
// Set the fontSize so that everything works out to whole numbers.
child: const Text(
text,
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
),
),
);
// Between the top and center examples.
expect(tester.getTopLeft(find.text(text)).dy, closeTo(353.3, .0001));
expect(tester.getTopLeft(find.byKey(pKey)).dy, closeTo(269.3, .0001));
});
});
group('label', () {
testWidgets('align top (default)', (WidgetTester tester) async {
const String text = 'text';
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
expands: true, // so we have a tall input where align can vary
decoration: const InputDecoration(
labelText: 'label',
filled: true,
),
textAlignVertical: TextAlignVertical.top, // default
// Set the fontSize so that everything works out to whole numbers.
child: const Text(
text,
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
),
),
);
// The label causes the text to start slightly lower than it would
// otherwise.
expect(tester.getTopLeft(find.text(text)).dy, closeTo(28.0, .0001));
});
testWidgets('align center', (WidgetTester tester) async {
const String text = 'text';
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
expands: true, // so we have a tall input where align can vary
decoration: const InputDecoration(
labelText: 'label',
filled: true,
),
textAlignVertical: TextAlignVertical.center,
// Set the fontSize so that everything works out to whole numbers.
child: const Text(
text,
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
),
),
);
// The label reduces the amount of space available for text, so the
// center is slightly lower.
expect(tester.getTopLeft(find.text(text)).dy, closeTo(298.0, .0001));
});
testWidgets('align bottom', (WidgetTester tester) async {
const String text = 'text';
await tester.pumpWidget(
buildInputDecorator(
// isEmpty: false (default)
// isFocused: false (default)
expands: true, // so we have a tall input where align can vary
decoration: const InputDecoration(
labelText: 'label',
filled: true,
),
textAlignVertical: TextAlignVertical.bottom,
// Set the fontSize so that everything works out to whole numbers.
child: const Text(
text,
style: TextStyle(fontFamily: 'Ahem', fontSize: 20.0),
),
),
);
// The label reduces the amount of space available for text, but the
// bottom line is still in the same place.
expect(tester.getTopLeft(find.text(text)).dy, closeTo(568.0, .0001));
});
});
});
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(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment