Support password fields for a11y (#15497)

* Support password fields for a11y

* rename to obscured

* Roll engine to c3ab0c9143029f0267a05b99effbfbd280a4901b
This commit is contained in:
Michael Goderbauer 2018-03-20 00:26:10 -07:00 committed by GitHub
parent 568ce697b4
commit 619ebd67a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 99 additions and 5 deletions

View file

@ -1 +1 @@
1348ab5b63adc18148f161876a4b1cacd5ec0779
c3ab0c9143029f0267a05b99effbfbd280a4901b

View file

@ -831,6 +831,9 @@ class RenderCustomPaint extends RenderProxyBox {
if (properties.inMutuallyExclusiveGroup != null) {
config.isInMutuallyExclusiveGroup = properties.inMutuallyExclusiveGroup;
}
if (properties.obscured != null) {
config.isObscured = properties.obscured;
}
if (properties.header != null) {
config.isHeader = properties.header;
}

View file

@ -132,12 +132,14 @@ class RenderEditable extends RenderBox {
this.onSelectionChanged,
this.onCaretChanged,
this.ignorePointer: false,
bool obscureText: false,
}) : assert(textAlign != null),
assert(textDirection != null, 'RenderEditable created without a textDirection.'),
assert(maxLines == null || maxLines > 0),
assert(textScaleFactor != null),
assert(offset != null),
assert(ignorePointer != null),
assert(obscureText != null),
_textPainter = new TextPainter(
text: text,
textAlign: textAlign,
@ -150,7 +152,8 @@ class RenderEditable extends RenderBox {
_maxLines = maxLines,
_selectionColor = selectionColor,
_selection = selection,
_offset = offset {
_offset = offset,
_obscureText = obscureText {
assert(_showCursor != null);
assert(!_showCursor.value || cursorColor != null);
_tap = new TapGestureRecognizer(debugOwner: this)
@ -160,6 +163,9 @@ class RenderEditable extends RenderBox {
..onLongPress = _handleLongPress;
}
/// Character used to obscure text if [obscureText] is true.
static const String obscuringCharacter = '';
/// Called when the selection changes.
SelectionChangedHandler onSelectionChanged;
@ -175,6 +181,16 @@ class RenderEditable extends RenderBox {
/// The default value of this property is false.
bool ignorePointer;
/// Whether to hide the text being edited (e.g., for passwords).
bool get obscureText => _obscureText;
bool _obscureText;
set obscureText(bool value) {
if (_obscureText == value)
return;
_obscureText = value;
markNeedsSemanticsUpdate();
}
Rect _lastCaretRect;
/// Marks the render object as needing to be laid out again and have its text
@ -351,7 +367,10 @@ class RenderEditable extends RenderBox {
super.describeSemanticsConfiguration(config);
config
..value = text.toPlainText()
..value = obscureText
? obscuringCharacter * text.toPlainText().length
: text.toPlainText()
..isObscured = obscureText
..textDirection = textDirection
..isFocused = hasFocus
..isTextField = true;

View file

@ -3019,6 +3019,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
bool textField,
bool focused,
bool inMutuallyExclusiveGroup,
bool obscured,
String label,
String value,
String increasedValue,
@ -3053,6 +3054,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_textField = textField,
_focused = focused,
_inMutuallyExclusiveGroup = inMutuallyExclusiveGroup,
_obscured = obscured,
_label = label,
_value = value,
_increasedValue = increasedValue,
@ -3201,6 +3203,17 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate();
}
/// If non-null, sets the [SemanticsNode.isObscured] semantic to the given
/// value.
bool get obscured => _obscured;
bool _obscured;
set obscured(bool value) {
if (obscured == value)
return;
_obscured = value;
markNeedsSemanticsUpdate();
}
/// If non-null, sets the [SemanticsNode.label] semantic to the given value.
///
/// The reading direction is given by [textDirection].
@ -3638,6 +3651,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.isFocused = focused;
if (inMutuallyExclusiveGroup != null)
config.isInMutuallyExclusiveGroup = inMutuallyExclusiveGroup;
if (obscured != null)
config.isObscured = obscured;
if (label != null)
config.label = label;
if (value != null)

View file

@ -319,6 +319,7 @@ class SemanticsProperties extends DiagnosticableTree {
this.textField,
this.focused,
this.inMutuallyExclusiveGroup,
this.obscured,
this.label,
this.value,
this.increasedValue,
@ -398,6 +399,13 @@ class SemanticsProperties extends DiagnosticableTree {
/// For example, a radio button is in a mutually exclusive group because only
/// one radio button in that group can be marked as [checked].
final bool inMutuallyExclusiveGroup;
/// If non-null, whether [value] should be obscured.
///
/// This option is usually set in combination with [textField] to indicate
/// that the text field contains a password (or other sensitive information).
/// Doing so instructs screen readers to not read out the [value].
final bool obscured;
/// Provides a textual description of the widget.
///
@ -2405,6 +2413,16 @@ class SemanticsConfiguration {
_setFlag(SemanticsFlag.isTextField, value);
}
/// Whether the [value] should be obscured.
///
/// This option is usually set in combination with [textField] to indicate
/// that the text field contains a password (or other sensitive information).
/// Doing so instructs screen readers to not read out the [value].
bool get isObscured => _hasFlag(SemanticsFlag.isObscured);
set isObscured(bool value) {
_setFlag(SemanticsFlag.isObscured, value);
}
/// The currently selected text (or the position of the cursor) within [value]
/// if this node represents a text field.
TextSelection get textSelection => _textSelection;

View file

@ -4892,6 +4892,7 @@ class Semantics extends SingleChildRenderObjectWidget {
bool textField,
bool focused,
bool inMutuallyExclusiveGroup,
bool obscured,
String label,
String value,
String increasedValue,
@ -4929,6 +4930,7 @@ class Semantics extends SingleChildRenderObjectWidget {
textField: textField,
focused: focused,
inMutuallyExclusiveGroup: inMutuallyExclusiveGroup,
obscured: obscured,
label: label,
value: value,
increasedValue: increasedValue,
@ -5007,6 +5009,7 @@ class Semantics extends SingleChildRenderObjectWidget {
textField: properties.textField,
focused: properties.focused,
inMutuallyExclusiveGroup: properties.inMutuallyExclusiveGroup,
obscured: properties.obscured,
label: properties.label,
value: properties.value,
increasedValue: properties.increasedValue,

View file

@ -759,6 +759,7 @@ class _Editable extends LeafRenderObjectWidget {
onSelectionChanged: onSelectionChanged,
onCaretChanged: onCaretChanged,
ignorePointer: rendererIgnoresPointer,
obscureText: obscureText,
);
}
@ -778,7 +779,8 @@ class _Editable extends LeafRenderObjectWidget {
..offset = offset
..onSelectionChanged = onSelectionChanged
..onCaretChanged = onCaretChanged
..ignorePointer = rendererIgnoresPointer;
..ignorePointer = rendererIgnoresPointer
..obscureText = obscureText;
}
TextSpan get _styledTextSpan {
@ -801,7 +803,7 @@ class _Editable extends LeafRenderObjectWidget {
String text = value.text;
if (obscureText) {
text = new String.fromCharCodes(new List<int>.filled(text.length, 0x2022));
text = RenderEditable.obscuringCharacter * text.length;
final int o = obscureShowCharacterAtIndex;
if (o != null && o >= 0 && o < text.length)
text = text.replaceRange(o, o + 1, value.text.substring(o, o + 1));

View file

@ -415,6 +415,7 @@ void _defineTests() {
focused: true,
inMutuallyExclusiveGroup: true,
header: true,
obscured: true,
),
),
),

View file

@ -575,6 +575,38 @@ void main() {
semantics.dispose();
});
testWidgets('password fields have correct semantics', (WidgetTester tester) async {
final SemanticsTester semantics = new SemanticsTester(tester);
controller.text = 'super-secret-password!!1';
await tester.pumpWidget(new MaterialApp(
home: new EditableText(
obscureText: true,
controller: controller,
focusNode: focusNode,
style: textStyle,
cursorColor: cursorColor,
),
));
final String expectedValue = '' * controller.text.length;
expect(semantics, hasSemantics(new TestSemantics(
children: <TestSemantics>[
new TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isObscured],
value: expectedValue,
textDirection: TextDirection.ltr,
nextNodeId: -1,
previousNodeId: -1,
),
],
), ignoreTransform: true, ignoreRect: true, ignoreId: true));
semantics.dispose();
});
group('a11y copy/cut/paste', () {
Future<Null> _buildApp(MockTextSelectionControls controls, WidgetTester tester) {
return tester.pumpWidget(new MaterialApp(

View file

@ -470,6 +470,7 @@ void main() {
focused: true,
inMutuallyExclusiveGroup: true,
header: true,
obscured: true,
)
);