Unverified Commit 619ebd67 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Support password fields for a11y (#15497)

* Support password fields for a11y

* rename to obscured

* Roll engine to c3ab0c9143029f0267a05b99effbfbd280a4901b
parent 568ce697
1348ab5b63adc18148f161876a4b1cacd5ec0779 c3ab0c9143029f0267a05b99effbfbd280a4901b
...@@ -831,6 +831,9 @@ class RenderCustomPaint extends RenderProxyBox { ...@@ -831,6 +831,9 @@ class RenderCustomPaint extends RenderProxyBox {
if (properties.inMutuallyExclusiveGroup != null) { if (properties.inMutuallyExclusiveGroup != null) {
config.isInMutuallyExclusiveGroup = properties.inMutuallyExclusiveGroup; config.isInMutuallyExclusiveGroup = properties.inMutuallyExclusiveGroup;
} }
if (properties.obscured != null) {
config.isObscured = properties.obscured;
}
if (properties.header != null) { if (properties.header != null) {
config.isHeader = properties.header; config.isHeader = properties.header;
} }
......
...@@ -132,12 +132,14 @@ class RenderEditable extends RenderBox { ...@@ -132,12 +132,14 @@ class RenderEditable extends RenderBox {
this.onSelectionChanged, this.onSelectionChanged,
this.onCaretChanged, this.onCaretChanged,
this.ignorePointer: false, this.ignorePointer: false,
bool obscureText: false,
}) : 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(textScaleFactor != null), assert(textScaleFactor != null),
assert(offset != null), assert(offset != null),
assert(ignorePointer != null), assert(ignorePointer != null),
assert(obscureText != null),
_textPainter = new TextPainter( _textPainter = new TextPainter(
text: text, text: text,
textAlign: textAlign, textAlign: textAlign,
...@@ -150,7 +152,8 @@ class RenderEditable extends RenderBox { ...@@ -150,7 +152,8 @@ class RenderEditable extends RenderBox {
_maxLines = maxLines, _maxLines = maxLines,
_selectionColor = selectionColor, _selectionColor = selectionColor,
_selection = selection, _selection = selection,
_offset = offset { _offset = offset,
_obscureText = obscureText {
assert(_showCursor != null); assert(_showCursor != null);
assert(!_showCursor.value || cursorColor != null); assert(!_showCursor.value || cursorColor != null);
_tap = new TapGestureRecognizer(debugOwner: this) _tap = new TapGestureRecognizer(debugOwner: this)
...@@ -160,6 +163,9 @@ class RenderEditable extends RenderBox { ...@@ -160,6 +163,9 @@ class RenderEditable extends RenderBox {
..onLongPress = _handleLongPress; ..onLongPress = _handleLongPress;
} }
/// Character used to obscure text if [obscureText] is true.
static const String obscuringCharacter = '•';
/// Called when the selection changes. /// Called when the selection changes.
SelectionChangedHandler onSelectionChanged; SelectionChangedHandler onSelectionChanged;
...@@ -175,6 +181,16 @@ class RenderEditable extends RenderBox { ...@@ -175,6 +181,16 @@ class RenderEditable extends RenderBox {
/// The default value of this property is false. /// The default value of this property is false.
bool ignorePointer; 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; Rect _lastCaretRect;
/// Marks the render object as needing to be laid out again and have its text /// Marks the render object as needing to be laid out again and have its text
...@@ -351,7 +367,10 @@ class RenderEditable extends RenderBox { ...@@ -351,7 +367,10 @@ class RenderEditable extends RenderBox {
super.describeSemanticsConfiguration(config); super.describeSemanticsConfiguration(config);
config config
..value = text.toPlainText() ..value = obscureText
? obscuringCharacter * text.toPlainText().length
: text.toPlainText()
..isObscured = obscureText
..textDirection = textDirection ..textDirection = textDirection
..isFocused = hasFocus ..isFocused = hasFocus
..isTextField = true; ..isTextField = true;
......
...@@ -3019,6 +3019,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3019,6 +3019,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
bool textField, bool textField,
bool focused, bool focused,
bool inMutuallyExclusiveGroup, bool inMutuallyExclusiveGroup,
bool obscured,
String label, String label,
String value, String value,
String increasedValue, String increasedValue,
...@@ -3053,6 +3054,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3053,6 +3054,7 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
_textField = textField, _textField = textField,
_focused = focused, _focused = focused,
_inMutuallyExclusiveGroup = inMutuallyExclusiveGroup, _inMutuallyExclusiveGroup = inMutuallyExclusiveGroup,
_obscured = obscured,
_label = label, _label = label,
_value = value, _value = value,
_increasedValue = increasedValue, _increasedValue = increasedValue,
...@@ -3201,6 +3203,17 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3201,6 +3203,17 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
markNeedsSemanticsUpdate(); 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. /// If non-null, sets the [SemanticsNode.label] semantic to the given value.
/// ///
/// The reading direction is given by [textDirection]. /// The reading direction is given by [textDirection].
...@@ -3638,6 +3651,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox { ...@@ -3638,6 +3651,8 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.isFocused = focused; config.isFocused = focused;
if (inMutuallyExclusiveGroup != null) if (inMutuallyExclusiveGroup != null)
config.isInMutuallyExclusiveGroup = inMutuallyExclusiveGroup; config.isInMutuallyExclusiveGroup = inMutuallyExclusiveGroup;
if (obscured != null)
config.isObscured = obscured;
if (label != null) if (label != null)
config.label = label; config.label = label;
if (value != null) if (value != null)
......
...@@ -319,6 +319,7 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -319,6 +319,7 @@ class SemanticsProperties extends DiagnosticableTree {
this.textField, this.textField,
this.focused, this.focused,
this.inMutuallyExclusiveGroup, this.inMutuallyExclusiveGroup,
this.obscured,
this.label, this.label,
this.value, this.value,
this.increasedValue, this.increasedValue,
...@@ -399,6 +400,13 @@ class SemanticsProperties extends DiagnosticableTree { ...@@ -399,6 +400,13 @@ class SemanticsProperties extends DiagnosticableTree {
/// one radio button in that group can be marked as [checked]. /// one radio button in that group can be marked as [checked].
final bool inMutuallyExclusiveGroup; 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. /// Provides a textual description of the widget.
/// ///
/// If a label is provided, there must either by an ambient [Directionality] /// If a label is provided, there must either by an ambient [Directionality]
...@@ -2405,6 +2413,16 @@ class SemanticsConfiguration { ...@@ -2405,6 +2413,16 @@ class SemanticsConfiguration {
_setFlag(SemanticsFlag.isTextField, value); _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] /// The currently selected text (or the position of the cursor) within [value]
/// if this node represents a text field. /// if this node represents a text field.
TextSelection get textSelection => _textSelection; TextSelection get textSelection => _textSelection;
......
...@@ -4892,6 +4892,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4892,6 +4892,7 @@ class Semantics extends SingleChildRenderObjectWidget {
bool textField, bool textField,
bool focused, bool focused,
bool inMutuallyExclusiveGroup, bool inMutuallyExclusiveGroup,
bool obscured,
String label, String label,
String value, String value,
String increasedValue, String increasedValue,
...@@ -4929,6 +4930,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -4929,6 +4930,7 @@ class Semantics extends SingleChildRenderObjectWidget {
textField: textField, textField: textField,
focused: focused, focused: focused,
inMutuallyExclusiveGroup: inMutuallyExclusiveGroup, inMutuallyExclusiveGroup: inMutuallyExclusiveGroup,
obscured: obscured,
label: label, label: label,
value: value, value: value,
increasedValue: increasedValue, increasedValue: increasedValue,
...@@ -5007,6 +5009,7 @@ class Semantics extends SingleChildRenderObjectWidget { ...@@ -5007,6 +5009,7 @@ class Semantics extends SingleChildRenderObjectWidget {
textField: properties.textField, textField: properties.textField,
focused: properties.focused, focused: properties.focused,
inMutuallyExclusiveGroup: properties.inMutuallyExclusiveGroup, inMutuallyExclusiveGroup: properties.inMutuallyExclusiveGroup,
obscured: properties.obscured,
label: properties.label, label: properties.label,
value: properties.value, value: properties.value,
increasedValue: properties.increasedValue, increasedValue: properties.increasedValue,
......
...@@ -759,6 +759,7 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -759,6 +759,7 @@ class _Editable extends LeafRenderObjectWidget {
onSelectionChanged: onSelectionChanged, onSelectionChanged: onSelectionChanged,
onCaretChanged: onCaretChanged, onCaretChanged: onCaretChanged,
ignorePointer: rendererIgnoresPointer, ignorePointer: rendererIgnoresPointer,
obscureText: obscureText,
); );
} }
...@@ -778,7 +779,8 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -778,7 +779,8 @@ class _Editable extends LeafRenderObjectWidget {
..offset = offset ..offset = offset
..onSelectionChanged = onSelectionChanged ..onSelectionChanged = onSelectionChanged
..onCaretChanged = onCaretChanged ..onCaretChanged = onCaretChanged
..ignorePointer = rendererIgnoresPointer; ..ignorePointer = rendererIgnoresPointer
..obscureText = obscureText;
} }
TextSpan get _styledTextSpan { TextSpan get _styledTextSpan {
...@@ -801,7 +803,7 @@ class _Editable extends LeafRenderObjectWidget { ...@@ -801,7 +803,7 @@ class _Editable extends LeafRenderObjectWidget {
String text = value.text; String text = value.text;
if (obscureText) { if (obscureText) {
text = new String.fromCharCodes(new List<int>.filled(text.length, 0x2022)); text = RenderEditable.obscuringCharacter * text.length;
final int o = obscureShowCharacterAtIndex; final int o = obscureShowCharacterAtIndex;
if (o != null && o >= 0 && o < text.length) if (o != null && o >= 0 && o < text.length)
text = text.replaceRange(o, o + 1, value.text.substring(o, o + 1)); text = text.replaceRange(o, o + 1, value.text.substring(o, o + 1));
......
...@@ -415,6 +415,7 @@ void _defineTests() { ...@@ -415,6 +415,7 @@ void _defineTests() {
focused: true, focused: true,
inMutuallyExclusiveGroup: true, inMutuallyExclusiveGroup: true,
header: true, header: true,
obscured: true,
), ),
), ),
), ),
......
...@@ -575,6 +575,38 @@ void main() { ...@@ -575,6 +575,38 @@ void main() {
semantics.dispose(); 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', () { group('a11y copy/cut/paste', () {
Future<Null> _buildApp(MockTextSelectionControls controls, WidgetTester tester) { Future<Null> _buildApp(MockTextSelectionControls controls, WidgetTester tester) {
return tester.pumpWidget(new MaterialApp( return tester.pumpWidget(new MaterialApp(
......
...@@ -470,6 +470,7 @@ void main() { ...@@ -470,6 +470,7 @@ void main() {
focused: true, focused: true,
inMutuallyExclusiveGroup: true, inMutuallyExclusiveGroup: true,
header: true, header: true,
obscured: true,
) )
); );
......
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