Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions lib/web_ui/lib/src/engine/semantics/text_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 <input type="password">
// 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
Expand Down
127 changes: 83 additions & 44 deletions lib/web_ui/test/engine/semantics/text_field_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -92,41 +93,53 @@ void testMain() {
test('renders a text field', () {
createTextFieldSemantics(value: 'hello');

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

// 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(),
'<sem><input type="password" /></sem>',
);

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(), '''<sem><input /></sem>''');
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"]')!;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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, '');
Expand All @@ -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);
Expand All @@ -280,7 +293,7 @@ void testMain() {
expect(editableElement.selectionEnd, 0);

// Update from framework
const MethodCall setEditingState =
const setEditingState =
MethodCall('TextInput.setEditingState', <String, dynamic>{
'text': 'updated',
'selectionBase': 2,
Expand All @@ -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);
Expand All @@ -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,
);
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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(),
'<sem><textarea style="-webkit-text-security: circle"></textarea></sem>',
);

strategy.disable();
});

test('Does not position or size its DOM element', () {
strategy.enable(
singlelineConfig,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -547,6 +585,7 @@ SemanticsObject createTextFieldSemantics({
isTextField: true,
isFocused: isFocused,
isMultiline: isMultiline,
isObscured: isObscured,
hasTap: true,
rect: rect,
textDirection: ui.TextDirection.ltr,
Expand Down