From 817e0470620e86625201aa4741b9b5f229d687f5 Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Tue, 20 Aug 2024 16:23:56 -0700 Subject: [PATCH 1/3] annotate with the appropriate `type` attribute --- .../lib/src/engine/semantics/text_field.dart | 9 +- .../engine/semantics/text_field_test.dart | 102 ++++++++++-------- 2 files changed, 66 insertions(+), 45 deletions(-) diff --git a/lib/web_ui/lib/src/engine/semantics/text_field.dart b/lib/web_ui/lib/src/engine/semantics/text_field.dart index 29e88754aebf2..24c5cd3b6305b 100644 --- a/lib/web_ui/lib/src/engine/semantics/text_field.dart +++ b/lib/web_ui/lib/src/engine/semantics/text_field.dart @@ -223,10 +223,17 @@ class SemanticTextField extends SemanticRole { return true; } + DomHTMLInputElement _createSingleLineField() { + return createDomHTMLInputElement() + ..type = semanticsObject.hasFlag(ui.SemanticsFlag.isObscured) + ? 'password' + : 'text'; + } + void _initializeEditableElement() { editableElement = semanticsObject.hasFlag(ui.SemanticsFlag.isMultiline) ? createDomHTMLTextAreaElement() - : createDomHTMLInputElement(); + : _createSingleLineField(); _updateEnabledState(); // On iOS, even though the semantic text field is transparent, the cursor diff --git a/lib/web_ui/test/engine/semantics/text_field_test.dart b/lib/web_ui/test/engine/semantics/text_field_test.dart index fb1ad1991d1bf..4730b3d34cc62 100644 --- a/lib/web_ui/test/engine/semantics/text_field_test.dart +++ b/lib/web_ui/test/engine/semantics/text_field_test.dart @@ -60,7 +60,8 @@ void testMain() { value: 'hi', isFocused: true, ); - final SemanticTextField textField = textFieldSemantics.semanticRole! as SemanticTextField; + final SemanticTextField textField = + textFieldSemantics.semanticRole! as SemanticTextField; // ensureInitialized() isn't called prior to calling dispose() here. // Since we are conditionally calling dispose() on our @@ -92,41 +93,53 @@ void testMain() { test('renders a text field', () { createTextFieldSemantics(value: 'hello'); - expectSemanticsTree(owner(), ''' - - - '''); + expectSemanticsTree( + owner(), + '', + ); // TODO(yjbanov): this used to attempt to test that value="hello" but the // test was a false positive. We should revise this test and // make sure it tests the right things: // https://github.com/flutter/flutter/issues/147200 - final SemanticsObject node = owner().debugSemanticsTree![0]!; - final SemanticTextField textFieldRole = node.semanticRole! as SemanticTextField; - final DomHTMLInputElement inputElement = - textFieldRole.editableElement as DomHTMLInputElement; + final node = owner().debugSemanticsTree![0]!; + final textFieldRole = node.semanticRole! as SemanticTextField; + final inputElement = textFieldRole.editableElement as DomHTMLInputElement; expect(inputElement.tagName.toLowerCase(), 'input'); expect(inputElement.value, ''); expect(inputElement.disabled, isFalse); }); + test('renders a password field', () { + createTextFieldSemantics(value: 'secret', isObscured: true); + + expectSemanticsTree( + owner(), + '', + ); + + final node = owner().debugSemanticsTree![0]!; + final textFieldRole = node.semanticRole! as SemanticTextField; + final inputElement = textFieldRole.editableElement as DomHTMLInputElement; + expect(inputElement.disabled, isFalse); + }); + test('renders a disabled text field', () { createTextFieldSemantics(isEnabled: false, value: 'hello'); expectSemanticsTree(owner(), ''''''); - final SemanticsObject node = owner().debugSemanticsTree![0]!; - final SemanticTextField textFieldRole = node.semanticRole! as SemanticTextField; - final DomHTMLInputElement inputElement = - textFieldRole.editableElement as DomHTMLInputElement; + final node = owner().debugSemanticsTree![0]!; + final textFieldRole = node.semanticRole! as SemanticTextField; + final inputElement = textFieldRole.editableElement as DomHTMLInputElement; expect(inputElement.tagName.toLowerCase(), 'input'); expect(inputElement.disabled, isTrue); }); test('sends a SemanticsAction.focus action when browser requests focus', () async { - final SemanticsActionLogger logger = SemanticsActionLogger(); + final logger = SemanticsActionLogger(); createTextFieldSemantics(value: 'hello'); - final DomElement textField = owner() + final textField = owner() .semanticsHost .querySelector('input[data-semantics-role="text-field"]')!; @@ -163,14 +176,14 @@ void testMain() { ); // Create - final SemanticsObject textFieldSemantics = createTextFieldSemantics( + final textFieldSemantics = createTextFieldSemantics( value: 'hello', label: 'greeting', isFocused: true, rect: const ui.Rect.fromLTWH(0, 0, 10, 15), ); - final SemanticTextField textField = textFieldSemantics.semanticRole! as SemanticTextField; + final textField = textFieldSemantics.semanticRole! as SemanticTextField; expect(owner().semanticsHost.ownerDocument?.activeElement, strategy.domElement); expect(textField.editableElement, strategy.domElement); @@ -231,16 +244,16 @@ void testMain() { onAction: (_) {}, ); - final SemanticsObject textFieldSemantics = createTextFieldSemantics( - value: 'hello', - textSelectionBase: 1, - textSelectionExtent: 3, - isFocused: true, - rect: const ui.Rect.fromLTWH(0, 0, 10, 15)); + final textFieldSemantics = createTextFieldSemantics( + value: 'hello', + textSelectionBase: 1, + textSelectionExtent: 3, + isFocused: true, + rect: const ui.Rect.fromLTWH(0, 0, 10, 15), + ); - final SemanticTextField textField = textFieldSemantics.semanticRole! as SemanticTextField; - final DomHTMLInputElement editableElement = - textField.editableElement as DomHTMLInputElement; + final textField = textFieldSemantics.semanticRole! as SemanticTextField; + final editableElement = textField.editableElement as DomHTMLInputElement; expect(editableElement, strategy.domElement); expect(editableElement.value, ''); @@ -262,16 +275,16 @@ void testMain() { onAction: (_) {}, ); - final SemanticsObject textFieldSemantics = createTextFieldSemantics( - value: 'hello', - textSelectionBase: 1, - textSelectionExtent: 3, - isFocused: true, - rect: const ui.Rect.fromLTWH(0, 0, 10, 15)); + final textFieldSemantics = createTextFieldSemantics( + value: 'hello', + textSelectionBase: 1, + textSelectionExtent: 3, + isFocused: true, + rect: const ui.Rect.fromLTWH(0, 0, 10, 15), + ); - final SemanticTextField textField = textFieldSemantics.semanticRole! as SemanticTextField; - final DomHTMLInputElement editableElement = - textField.editableElement as DomHTMLInputElement; + final textField = textFieldSemantics.semanticRole! as SemanticTextField; + final editableElement = textField.editableElement as DomHTMLInputElement; // No updates expected on semantic updates expect(editableElement, strategy.domElement); @@ -280,7 +293,7 @@ void testMain() { expect(editableElement.selectionEnd, 0); // Update from framework - const MethodCall setEditingState = + const setEditingState = MethodCall('TextInput.setEditingState', { 'text': 'updated', 'selectionBase': 2, @@ -306,12 +319,12 @@ void testMain() { onChange: (_, __) {}, onAction: (_) {}, ); - final SemanticsObject textFieldSemantics = createTextFieldSemantics( + final textFieldSemantics = createTextFieldSemantics( value: 'hello', isFocused: true, ); - final SemanticTextField textField = textFieldSemantics.semanticRole! as SemanticTextField; + final textField = textFieldSemantics.semanticRole! as SemanticTextField; expect(textField.editableElement, strategy.domElement); expect(owner().semanticsHost.ownerDocument?.activeElement, strategy.domElement); @@ -335,7 +348,7 @@ void testMain() { expect(strategy.domElement, isNull); // During the semantics update the DOM element is created and is focused on. - final SemanticsObject textFieldSemantics = createTextFieldSemantics( + final textFieldSemantics = createTextFieldSemantics( value: 'hello', isFocused: true, ); @@ -347,7 +360,7 @@ void testMain() { expect(strategy.domElement, isNull); // It doesn't remove the DOM element. - final SemanticTextField textField = textFieldSemantics.semanticRole! as SemanticTextField; + final textField = textFieldSemantics.semanticRole! as SemanticTextField; expect(owner().semanticsHost.contains(textField.editableElement), isTrue); // Editing element is not enabled. expect(strategy.isEnabled, isFalse); @@ -412,8 +425,7 @@ void testMain() { isMultiline: true, ); - final DomHTMLTextAreaElement textArea = - strategy.domElement! as DomHTMLTextAreaElement; + final textArea = strategy.domElement! as DomHTMLTextAreaElement; expect(owner().semanticsHost.ownerDocument?.activeElement, strategy.domElement); @@ -444,7 +456,7 @@ void testMain() { // Send width and height that are different from semantics values on // purpose. - final EditableTextGeometry geometry = EditableTextGeometry( + final geometry = EditableTextGeometry( height: 12, width: 13, globalTransform: Matrix4.translationValues(14, 15, 0).storage, @@ -534,11 +546,12 @@ SemanticsObject createTextFieldSemantics({ bool isEnabled = true, bool isFocused = false, bool isMultiline = false, + bool isObscured = false, ui.Rect rect = const ui.Rect.fromLTRB(0, 0, 100, 50), int textSelectionBase = 0, int textSelectionExtent = 0, }) { - final SemanticsTester tester = SemanticsTester(owner()); + final tester = SemanticsTester(owner()); tester.updateNode( id: 0, isEnabled: isEnabled, @@ -547,6 +560,7 @@ SemanticsObject createTextFieldSemantics({ isTextField: true, isFocused: isFocused, isMultiline: isMultiline, + isObscured: isObscured, hasTap: true, rect: rect, textDirection: ui.TextDirection.ltr, From 234783fbbad66cb6e2f1bc25f11df6a62076e82e Mon Sep 17 00:00:00 2001 From: Yegor Jbanov Date: Tue, 20 Aug 2024 16:33:53 -0700 Subject: [PATCH 2/3] add an explainer in a comment --- lib/web_ui/lib/src/engine/semantics/text_field.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/web_ui/lib/src/engine/semantics/text_field.dart b/lib/web_ui/lib/src/engine/semantics/text_field.dart index 24c5cd3b6305b..738319528b911 100644 --- a/lib/web_ui/lib/src/engine/semantics/text_field.dart +++ b/lib/web_ui/lib/src/engine/semantics/text_field.dart @@ -231,6 +231,18 @@ class SemanticTextField extends SemanticRole { } void _initializeEditableElement() { + // Technically, a field can be both multi-line and obscured. However, there's + // no standard way on the web to do it (there's a proprietary Safari method + // but unclear how well that works, and practice shows - remember role="text"? + // - that Safari can yank proprietary features suddenly). + // + // The best choice in this situation is to use a non-obscured multiline + // ', + ); + + strategy.disable(); + }); + test('Does not position or size its DOM element', () { strategy.enable( singlelineConfig,