Skip to content

Commit 06ec037

Browse files
committed
[web] Expose a semantics text field value while it is not being edited
1 parent aebd23e commit 06ec037

3 files changed

Lines changed: 82 additions & 10 deletions

File tree

‎engine/src/flutter/lib/web_ui/lib/src/engine/semantics/text_field.dart‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:js_interop';
56
import 'dart:math' as math;
67

78
import 'package:ui/ui.dart' as ui;
@@ -461,6 +462,43 @@ class SemanticTextField extends SemanticRole {
461462
_updateHintDescription();
462463

463464
_updateInputType();
465+
466+
_updateValue();
467+
}
468+
469+
/// Copies the framework's value into [editableElement] while this field is
470+
/// not being edited.
471+
///
472+
/// Without this, a field that has never been focused, such as a disabled
473+
/// field, exposes an empty value to assistive technologies.
474+
///
475+
/// While this field is the active editing element, the text editing strategy
476+
/// owns the element's value and selection, so they are left alone. Obscured
477+
/// fields are skipped because their semantics value is masked, and fields
478+
/// owned by an autofill group are skipped because the group manages their
479+
/// value.
480+
void _updateValue() {
481+
if (!semanticsObject.isValueDirty) {
482+
return;
483+
}
484+
if (SemanticsTextEditingStrategy._instance?.activeTextField == this) {
485+
return;
486+
}
487+
if (semanticsObject.flags.isObscured || _isAutofillOwned) {
488+
return;
489+
}
490+
final String value = semanticsObject.value ?? '';
491+
if (editableElement.isA<DomHTMLInputElement>()) {
492+
final input = editableElement as DomHTMLInputElement;
493+
if (input.value != value) {
494+
input.value = value;
495+
}
496+
} else if (editableElement.isA<DomHTMLTextAreaElement>()) {
497+
final textArea = editableElement as DomHTMLTextAreaElement;
498+
if (textArea.value != value) {
499+
textArea.value = value;
500+
}
501+
}
464502
}
465503

466504
void _updateHintDescription() {

‎engine/src/flutter/lib/web_ui/test/engine/semantics/semantics_test.dart‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3002,11 +3002,9 @@ void _testTextField() {
30023002
final textFieldRole = node.semanticRole! as SemanticTextField;
30033003
final inputElement = textFieldRole.editableElement as DomHTMLInputElement;
30043004

3005-
// TODO(yjbanov): this used to attempt to test that value="hello" but the
3006-
// test was a false positive. We should revise this test and
3007-
// make sure it tests the right things:
3008-
// https://github.com/flutter/flutter/issues/147200
3009-
expect(inputElement.value, '');
3005+
// `outerHTML` does not include the `value` property, so it is checked on
3006+
// the element directly.
3007+
expect(inputElement.value, 'hello');
30103008

30113009
expect(node.semanticRole?.kind, EngineSemanticsRole.textField);
30123010
expect(

‎engine/src/flutter/lib/web_ui/test/engine/semantics/text_field_test.dart‎

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,15 +94,13 @@ void testMain() {
9494

9595
expectSemanticsTree(owner(), '<sem><input type="text" /></sem>');
9696

97-
// TODO(yjbanov): this used to attempt to test that value="hello" but the
98-
// test was a false positive. We should revise this test and
99-
// make sure it tests the right things:
100-
// https://github.com/flutter/flutter/issues/147200
97+
// `outerHTML` does not include the `value` property, so it is checked
98+
// on the element directly.
10199
final SemanticsObject node = owner().debugSemanticsTree![0]!;
102100
final textFieldRole = node.semanticRole! as SemanticTextField;
103101
final inputElement = textFieldRole.editableElement as DomHTMLInputElement;
104102
expect(inputElement.tagName.toLowerCase(), 'input');
105-
expect(inputElement.value, '');
103+
expect(inputElement.value, 'hello');
106104
expect(inputElement.disabled, isFalse);
107105
expect(inputElement.getAttribute('aria-required'), isNull);
108106
});
@@ -156,6 +154,44 @@ void testMain() {
156154
final inputElement = textFieldRole.editableElement as DomHTMLInputElement;
157155
expect(inputElement.tagName.toLowerCase(), 'input');
158156
expect(inputElement.disabled, isTrue);
157+
// A disabled field is never focused, so its value has to come from
158+
// semantics for screen readers to read it.
159+
expect(inputElement.value, 'hello');
160+
});
161+
162+
test('updates the value of a field that is not being edited', () {
163+
final SemanticsObject node = createTextFieldSemantics(value: 'hello');
164+
final inputElement =
165+
(node.semanticRole! as SemanticTextField).editableElement as DomHTMLInputElement;
166+
expect(inputElement.value, 'hello');
167+
168+
createTextFieldSemantics(value: 'bye');
169+
expect(inputElement.value, 'bye');
170+
});
171+
172+
test('renders the value of a multi-line field that is not being edited', () {
173+
final SemanticsObject node = createTextFieldSemantics(value: 'hello', isMultiline: true);
174+
final textArea =
175+
(node.semanticRole! as SemanticTextField).editableElement as DomHTMLTextAreaElement;
176+
expect(textArea.value, 'hello');
177+
});
178+
179+
test('does not write the masked value of an obscured field', () {
180+
final SemanticsObject node = createTextFieldSemantics(value: '•••', isObscured: true);
181+
final inputElement =
182+
(node.semanticRole! as SemanticTextField).editableElement as DomHTMLInputElement;
183+
expect(inputElement.value, '');
184+
});
185+
186+
test('does not write the value of a field owned by an autofill group', () {
187+
final SemanticsObject node = createTextFieldSemantics(value: 'hello');
188+
final inputElement =
189+
(node.semanticRole! as SemanticTextField).editableElement as DomHTMLInputElement;
190+
inputElement.name = 'username';
191+
inputElement.value = 'autofilled';
192+
193+
createTextFieldSemantics(value: 'bye');
194+
expect(inputElement.value, 'autofilled');
159195
});
160196

161197
test('sends a SemanticsAction.focus action when browser requests focus', () async {

0 commit comments

Comments
 (0)