Skip to content
Permalink
Browse files
stagent.dev: Clicking email field on sign up form does not allow inpu…
…t until you click a second time

https://bugs.webkit.org/show_bug.cgi?id=245976
rdar://98341809

Reviewed by Wenson Hsieh.

Safari's AutoFill heuristic recently began (correctly) detecting email fields
on stagent.dev as autofillable. Consequently, Safari now adds an autofill
button to their email field when it is clicked.

WebKit has a longstanding bug where the selection is lost when an input
decoration is added while the selection is inside the input. Adding a decoration,
such as the autofill button, changes the structure of the shadow subtree.
Specifically, the contenteditable element is removed from the shadow root and
added to a separate container. The removal of the contenteditable element wipes
out the selection. To work around this issue, Safari has logic to restore the
selection after adding an autofill button.

However, Safari's workaround is not robust, as the HTML spec limits which input
types support the input selection APIs, such as `setSelectionStart` and
`setSelectionEnd`. Email inputs do not support the selection API, hence Safari's
workaround fails to apply to email fields where an autofill button is added.

To fix, restore the selection in WebKit, following the addition of an input
decoration, such as the autofill button. Note that this change still results in
two "selectionchange" events getting dispatched when focusing an autofillable
field for the first time. However, this matches behavior in shipping Safari.

An alternate solution considered was to avoid moving the contenteditable element
when adding a decoration. However, this change would have a much larger surface
area, and is too risky in the short term.

Note that an attempt to fix this issue was made earlier in 255229@main. However,
that fix was reverted due to crashes observed as a result of synchronous event
dispatch when restoring the selection.

* LayoutTests/fast/forms/auto-fill-button/show-auto-fill-button-while-selection-inside-field-expected.txt: Added.
* LayoutTests/fast/forms/auto-fill-button/show-auto-fill-button-while-selection-inside-field.html: Added.
* Source/WebCore/html/HTMLTextFormControlElement.h:

Expose a method returning the enumerated value of the selection direction so
that the variant of `setSelectionRange` that does not dispatch "select" events
can be used.

* Source/WebCore/html/TextFieldInputType.cpp:
(WebCore::TextFieldInputType::createShadowSubtree):
(WebCore::TextFieldInputType::createContainer):

Restore the selection after an input decoration is added. Restoration is only
performed when a decoration is added to an already created input, and is
performed asynchronously to avoid running script while adding a decoration.

* Source/WebCore/html/TextFieldInputType.h:

Canonical link: https://commits.webkit.org/256581@main
  • Loading branch information
pxlcoder committed Nov 11, 2022
1 parent c228e7c commit d66021f088795fd0954d82d1098e0ad587d23537
Show file tree
Hide file tree
Showing 5 changed files with 70 additions and 5 deletions.
@@ -0,0 +1,10 @@
This test ensures clicking on an input field, and then adding an autofill button, leaves the selection inside the field.

On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".


PASS input.value is "Test"
PASS successfullyParsed is true

TEST COMPLETE

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<script src="../../../resources/js-test.js"></script>
<script src="../../../resources/ui-helper.js"></script>
</head>
<body onload="runTest()">
<input id="input">
<script>

jsTestIsAsync = true;

description("This test ensures clicking on an input field, and then adding an autofill button, leaves the selection inside the field.");

async function runTest() {
if (!window.internals)
return;

await UIHelper.activateElement(input);

internals.setShowAutoFillButton(input, "Credentials");

await UIHelper.renderingUpdate();

document.execCommand("insertText", true, "Test");

shouldBeEqualToString("input.value", "Test");

finishJSTest();
}

</script>
</body>
</html>
@@ -82,6 +82,8 @@ class HTMLTextFormControlElement : public HTMLFormControlElementWithState {
void setSelectionRange(unsigned start, unsigned end, const String& direction, const AXTextStateChangeIntent& = AXTextStateChangeIntent());
WEBCORE_EXPORT bool setSelectionRange(unsigned start, unsigned end, TextFieldSelectionDirection = SelectionHasNoDirection, SelectionRevealMode = SelectionRevealMode::DoNotReveal, const AXTextStateChangeIntent& = AXTextStateChangeIntent());

TextFieldSelectionDirection computeSelectionDirection() const;

std::optional<SimpleRange> selection() const;
String selectedText() const;

@@ -142,8 +144,6 @@ class HTMLTextFormControlElement : public HTMLFormControlElementWithState {

bool isTextFormControlElement() const final { return true; }

TextFieldSelectionDirection computeSelectionDirection() const;

void dispatchFocusEvent(RefPtr<Element>&& oldFocusedElement, const FocusOptions&) final;
void dispatchBlurEvent(RefPtr<Element>&& newFocusedElement) final;
bool childShouldCreateRenderer(const Node&) const override;
@@ -40,6 +40,7 @@
#include "Editor.h"
#include "ElementInlines.h"
#include "ElementRareData.h"
#include "EventLoop.h"
#include "EventNames.h"
#include "Frame.h"
#include "FrameSelection.h"
@@ -362,7 +363,7 @@ void TextFieldInputType::createShadowSubtree()
return;
}

createContainer();
createContainer(PreserveSelectionRange::No);
updatePlaceholderText();

if (shouldHaveSpinButton) {
@@ -822,7 +823,7 @@ void TextFieldInputType::autoFillButtonElementWasClicked()
page->chrome().client().handleAutoFillButtonClick(*element());
}

void TextFieldInputType::createContainer()
void TextFieldInputType::createContainer(PreserveSelectionRange preserveSelection)
{
ASSERT(!m_container);
ASSERT(element());
@@ -831,13 +832,32 @@ void TextFieldInputType::createContainer()

ScriptDisallowedScope::EventAllowedScope allowedScope(*element()->userAgentShadowRoot());

// FIXME: <https://webkit.org/b/245977> Suppress selectionchange events during subtree modification.
std::optional<std::tuple<unsigned, unsigned, TextFieldSelectionDirection>> selectionState;
if (preserveSelection == PreserveSelectionRange::Yes && enclosingTextFormControl(element()->document().selection().selection().start()) == element())
selectionState = { element()->selectionStart(), element()->selectionEnd(), element()->computeSelectionDirection() };

m_container = TextControlInnerContainer::create(element()->document());
element()->userAgentShadowRoot()->appendChild(*m_container);
m_container->setPseudo(ShadowPseudoIds::webkitTextfieldDecorationContainer());

m_innerBlock = TextControlInnerElement::create(element()->document());
m_container->appendChild(*m_innerBlock);
m_innerBlock->appendChild(*m_innerText);

if (selectionState) {
element()->document().eventLoop().queueTask(TaskSource::DOMManipulation, [selectionState = *selectionState, element = WeakPtr { element() }] {
if (!element || !element->focused())
return;

auto selection = element->document().selection().selection();
if (selection.start().deprecatedNode() != element->userAgentShadowRoot())
return;

auto [selectionStart, selectionEnd, selectionDirection] = selectionState;
element->setSelectionRange(selectionStart, selectionEnd, selectionDirection);
});
}
}

void TextFieldInputType::createAutoFillButton(AutoFillButtonType autoFillButtonType)
@@ -121,7 +121,8 @@ class TextFieldInputType : public InputType, protected SpinButtonElement::SpinBu
bool shouldDrawCapsLockIndicator() const;
bool shouldDrawAutoFillButton() const;

void createContainer();
enum class PreserveSelectionRange : bool { No, Yes };
void createContainer(PreserveSelectionRange = PreserveSelectionRange::Yes);
void createAutoFillButton(AutoFillButtonType);

#if ENABLE(DATALIST_ELEMENT)

0 comments on commit d66021f

Please sign in to comment.