From a56ff4890d9d09c3d6d8759f6fcfe8b374af07ba Mon Sep 17 00:00:00 2001 From: Yegor Date: Fri, 4 Apr 2025 13:30:18 -0700 Subject: [PATCH] [web] fix text selection offset in multi-line fields (#166565) Fixes https://github.com/flutter/flutter/issues/162698 --- .../lib/web_ui/lib/src/engine/dom.dart | 2 + .../lib/src/engine/semantics/text_field.dart | 3 +- .../src/engine/text_editing/input_type.dart | 11 ++++- .../src/engine/text_editing/text_editing.dart | 2 - .../web_ui/test/engine/text_editing_test.dart | 43 +++++++++++++++++++ 5 files changed, 56 insertions(+), 5 deletions(-) diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart index f9ed204f15..6887a9d12d 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/dom.dart @@ -543,6 +543,7 @@ extension type DomCSSStyleDeclaration._(JSObject _) implements JSObject { set textAlign(String value) => setProperty('text-align', value); set font(String value) => setProperty('font', value); set cursor(String value) => setProperty('cursor', value); + set scrollbarWidth(String value) => setProperty('scrollbar-width', value); String get width => getPropertyValue('width'); String get height => getPropertyValue('height'); String get position => getPropertyValue('position'); @@ -604,6 +605,7 @@ extension type DomCSSStyleDeclaration._(JSObject _) implements JSObject { String get textAlign => getPropertyValue('text-align'); String get font => getPropertyValue('font'); String get cursor => getPropertyValue('cursor'); + String get scrollbarWidth => getPropertyValue('scrollbar-width'); external String getPropertyValue(String property); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/text_field.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/text_field.dart index 7bd9492984..c50e55d840 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/text_field.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/semantics/text_field.dart @@ -6,6 +6,7 @@ import 'package:ui/ui.dart' as ui; import '../dom.dart'; import '../platform_dispatcher.dart'; +import '../text_editing/input_type.dart'; import '../text_editing/text_editing.dart'; import 'semantics.dart'; @@ -230,7 +231,7 @@ class SemanticTextField extends SemanticRole { } DomHTMLTextAreaElement _createMultiLineField() { - final textArea = createDomHTMLTextAreaElement(); + final textArea = createMultilineTextArea(); if (semanticsObject.hasFlag(ui.SemanticsFlag.isObscured)) { // -webkit-text-security is not standard, but it's the best we can do. diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/input_type.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/input_type.dart index c5518427a6..8a90067872 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/input_type.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/input_type.dart @@ -120,7 +120,7 @@ class MultilineNoTextInputType extends MultilineInputType { String? get inputmodeAttribute => 'none'; @override - DomHTMLElement createDomElement() => createDomHTMLTextAreaElement(); + DomHTMLElement createDomElement() => createMultilineTextArea(); } /// Single-line text input type. @@ -184,5 +184,12 @@ class MultilineInputType extends EngineInputType { String? get inputmodeAttribute => null; @override - DomHTMLElement createDomElement() => createDomHTMLTextAreaElement(); + DomHTMLElement createDomElement() => createMultilineTextArea(); +} + +DomHTMLTextAreaElement createMultilineTextArea() { + final element = createDomHTMLTextAreaElement(); + // Scrollbar width affects text layout. This zeroes out the scrollbar width. + element.style.scrollbarWidth = 'none'; + return element; } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart index 79aae60ac6..244e7f4e2d 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/text_editing/text_editing.dart @@ -64,7 +64,6 @@ void _setStaticStyleAttributes(DomHTMLElement domElement) { // For more details, see: https://developer.mozilla.org/en-US/docs/Web/CSS/forced-color-adjust ..setProperty('forced-color-adjust', 'none') ..whiteSpace = 'pre-wrap' - ..alignContent = 'center' ..position = 'absolute' ..top = '0' ..left = '0' @@ -108,7 +107,6 @@ void _styleAutofillElements( final DomCSSStyleDeclaration elementStyle = domElement.style; elementStyle ..whiteSpace = 'pre-wrap' - ..alignContent = 'center' ..padding = '0' ..opacity = '1' ..color = 'transparent' diff --git a/engine/src/flutter/lib/web_ui/test/engine/text_editing_test.dart b/engine/src/flutter/lib/web_ui/test/engine/text_editing_test.dart index a6370f48cc..ae14801ec2 100644 --- a/engine/src/flutter/lib/web_ui/test/engine/text_editing_test.dart +++ b/engine/src/flutter/lib/web_ui/test/engine/text_editing_test.dart @@ -840,6 +840,45 @@ Future testMain() async { expect(spy.messages, isEmpty); }); + test('Does not align content in autofill group elements', () { + final setClient = MethodCall('TextInput.setClient', [ + 123, + createFlutterConfig('text'), + ]); + sendFrameworkMessage(codec.encodeMethodCall(setClient)); + + const setEditingState = MethodCall('TextInput.setEditingState', { + 'text': 'abcd', + 'selectionBase': 2, + 'selectionExtent': 3, + }); + sendFrameworkMessage(codec.encodeMethodCall(setEditingState)); + + const show = MethodCall('TextInput.show'); + sendFrameworkMessage(codec.encodeMethodCall(show)); + + // Form elements + { + final formElement = textEditing!.configuration!.autofillGroup!.formElement; + expect(formElement.style.alignContent, isEmpty); + + // Should contain one and one + expect(formElement.children, hasLength(2)); + + final inputElement = formElement.children.first; + expect(inputElement.style.alignContent, isEmpty); + + final submitElement = formElement.children.last; + expect(submitElement.style.alignContent, isEmpty); + } + + // Active element + { + final DomHTMLElement activeElement = textEditing!.strategy.activeDomElement; + expect(activeElement.style.alignContent, isEmpty); + } + }); + test('focus and connection with blur', () async { // In all the desktop browsers we are keeping the connection // open, keep the text editing element focused if it receives a blur @@ -3585,6 +3624,10 @@ Future testMain() async { // though it supports forced-colors. Safari doesn't support forced-colors // so this isn't a problem there. }, skip: isFirefox || isSafari); + + test('Multi-line text area scrollbars are zero-width', () { + expect(createMultilineTextArea().style.scrollbarWidth, 'none'); + }); }); }