Unverified Commit 9d70c85f authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

a11y improvements for textfield (#40390)

parent 2bd7f9ff
......@@ -827,14 +827,18 @@ class _RenderDecoration extends RenderBox {
visitor(prefix);
if (prefixIcon != null)
visitor(prefixIcon);
if (isFocused && hint != null) {
// Bypass opacity to always read hint when focused. This prevents the
// label from changing when text is entered.
final RenderProxyBox typedHint = hint;
visitor(typedHint.child);
} else if (!isFocused && label != null) {
if (label != null) {
visitor(label);
}
if (hint != null) {
if (isFocused) {
visitor(hint);
} else if (label == null) {
visitor(hint);
}
}
if (input != null)
visitor(input);
if (suffixIcon != null)
......@@ -2060,6 +2064,7 @@ class _InputDecoratorState extends State<InputDecorator> with TickerProviderStat
opacity: (isEmpty && !_hasInlineLabel) ? 1.0 : 0.0,
duration: _kTransitionDuration,
curve: _kTransitionCurve,
alwaysIncludeSemantics: true,
child: Text(
decoration.hintText,
style: hintStyle,
......
......@@ -1274,6 +1274,7 @@ class AnimatedOpacity extends ImplicitlyAnimatedWidget {
@required this.opacity,
Curve curve = Curves.linear,
@required Duration duration,
this.alwaysIncludeSemantics = false,
}) : assert(opacity != null && opacity >= 0.0 && opacity <= 1.0),
super(key: key, curve: curve, duration: duration);
......@@ -1290,6 +1291,16 @@ class AnimatedOpacity extends ImplicitlyAnimatedWidget {
/// The opacity must not be null.
final double opacity;
/// Whether the semantic information of the children is always included.
///
/// Defaults to false.
///
/// When true, regardless of the opacity settings the child semantic
/// information is exposed as if the widget were fully visible. This is
/// useful in cases where labels may be hidden during animations that
/// would otherwise contribute relevant semantics.
final bool alwaysIncludeSemantics;
@override
_AnimatedOpacityState createState() => _AnimatedOpacityState();
......@@ -1319,6 +1330,7 @@ class _AnimatedOpacityState extends ImplicitlyAnimatedWidgetState<AnimatedOpacit
return FadeTransition(
opacity: _opacityAnimation,
child: widget.child,
alwaysIncludeSemantics: widget.alwaysIncludeSemantics,
);
}
}
......
......@@ -4212,6 +4212,100 @@ void main() {
expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY);
});
testWidgets('TextField semantics include label when unfocused and label/hint when focused', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = TextEditingController(text: 'value');
final Key key = UniqueKey();
await tester.pumpWidget(
overlay(
child: TextField(
key: key,
controller: controller,
decoration: const InputDecoration(
hintText: 'hint',
labelText: 'label',
),
),
),
);
final SemanticsNode node = tester.getSemantics(find.byKey(key));
expect(node.label, 'label');
expect(node.value, 'value');
// Focus text field.
await tester.tap(find.byKey(key));
await tester.pump();
expect(node.label, 'label\nhint');
expect(node.value, 'value');
semantics.dispose();
});
testWidgets('TextField semantics always include label when no hint is given', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = TextEditingController(text: 'value');
final Key key = UniqueKey();
await tester.pumpWidget(
overlay(
child: TextField(
key: key,
controller: controller,
decoration: const InputDecoration(
labelText: 'label',
),
),
),
);
final SemanticsNode node = tester.getSemantics(find.byKey(key));
expect(node.label, 'label');
expect(node.value, 'value');
// Focus text field.
await tester.tap(find.byKey(key));
await tester.pump();
expect(node.label, 'label');
expect(node.value, 'value');
semantics.dispose();
});
testWidgets('TextField semantics always include hint when no label is given', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = TextEditingController(text: 'value');
final Key key = UniqueKey();
await tester.pumpWidget(
overlay(
child: TextField(
key: key,
controller: controller,
decoration: const InputDecoration(
hintText: 'hint',
),
),
),
);
final SemanticsNode node = tester.getSemantics(find.byKey(key));
expect(node.label, 'hint');
expect(node.value, 'value');
// Focus text field.
await tester.tap(find.byKey(key));
await tester.pump();
expect(node.label, 'hint');
expect(node.value, 'value');
semantics.dispose();
});
testWidgets('TextField semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
final TextEditingController controller = TextEditingController();
......@@ -4769,7 +4863,7 @@ void main() {
expect(semantics, hasSemantics(TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
label: 'hint',
label: 'label\nhint',
id: 1,
textDirection: TextDirection.ltr,
textSelection: const TextSelection(baseOffset: 0, extentOffset: 0),
......
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