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..d1cf80852a61d 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,33 @@ class SemanticTextField extends SemanticRole { return true; } + DomHTMLInputElement _createSingleLineField() { + return createDomHTMLInputElement() + ..type = semanticsObject.hasFlag(ui.SemanticsFlag.isObscured) + ? 'password' + : 'text'; + } + + DomHTMLTextAreaElement _createMultiLineField() { + final textArea = createDomHTMLTextAreaElement(); + + if (semanticsObject.hasFlag(ui.SemanticsFlag.isObscured)) { + // -webkit-text-security is not standard, but it's the best we can do. + // Another option would be to create a single-line + // but that may have layout quirks, since it cannot represent multi-line + // text. Worst case with -webkit-text-security is the browser does not + // support it and it does not obscure text. However, that's not a huge + // problem because semantic DOM is already invisible. + textArea.style.setProperty('-webkit-text-security', 'circle'); + } + + return textArea; + } + void _initializeEditableElement() { editableElement = semanticsObject.hasFlag(ui.SemanticsFlag.isMultiline) - ? createDomHTMLTextAreaElement() - : createDomHTMLInputElement(); + ? _createMultiLineField() + : _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..c93dc3bf4a86f 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,11 @@ void testMain() { isMultiline: true, ); - final DomHTMLTextAreaElement textArea = - strategy.domElement! as DomHTMLTextAreaElement; + final textArea = strategy.domElement! as DomHTMLTextAreaElement; + expect( + textArea.style.getPropertyValue('-webkit-text-security'), + '', + ); expect(owner().semanticsHost.ownerDocument?.activeElement, strategy.domElement); @@ -435,6 +451,27 @@ void testMain() { expect(strategy.isEnabled, isFalse); }); + test('multi-line and obscured', () { + strategy.enable( + multilineConfig, + onChange: (_, __) {}, + onAction: (_) {}, + ); + createTextFieldSemantics( + value: 'hello', + isFocused: true, + isMultiline: true, + isObscured: true, + ); + + expectSemanticsTree( + owner(), + '', + ); + + strategy.disable(); + }); + test('Does not position or size its DOM element', () { strategy.enable( singlelineConfig, @@ -444,7 +481,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 +571,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 +585,7 @@ SemanticsObject createTextFieldSemantics({ isTextField: true, isFocused: isFocused, isMultiline: isMultiline, + isObscured: isObscured, hasTap: true, rect: rect, textDirection: ui.TextDirection.ltr,