mirror of
https://github.com/flutter/flutter
synced 2024-09-21 17:22:30 +00:00
Support password fields for a11y (#15497)
* Support password fields for a11y * rename to obscured * Roll engine to c3ab0c9143029f0267a05b99effbfbd280a4901b
This commit is contained in:
parent
568ce697b4
commit
619ebd67a9
|
@ -1 +1 @@
|
|||
1348ab5b63adc18148f161876a4b1cacd5ec0779
|
||||
c3ab0c9143029f0267a05b99effbfbd280a4901b
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -415,6 +415,7 @@ void _defineTests() {
|
|||
focused: true,
|
||||
inMutuallyExclusiveGroup: true,
|
||||
header: true,
|
||||
obscured: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -470,6 +470,7 @@ void main() {
|
|||
focused: true,
|
||||
inMutuallyExclusiveGroup: true,
|
||||
header: true,
|
||||
obscured: true,
|
||||
)
|
||||
);
|
||||
|
||||
|
|
Loading…
Reference in a new issue