Skip to content

Commit

Permalink
Fire selectionchange event on input and textarea elements
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=271033

Reviewed by Sihui Liu and Wenson Hsieh.

This PR updates the implementation of selectionchange event to be fired on input element and textarea
element instead of document whenever either real selection or cached selection offsets have been updated
to match the specification: https://w3c.github.io/selection-api/#selectionchange-event

* LayoutTests/imported/w3c/web-platform-tests/selection/textcontrols/selectionchange-bubble-expected.txt:
* LayoutTests/imported/w3c/web-platform-tests/selection/textcontrols/selectionchange-expected.txt:
* LayoutTests/platform/gtk/imported/w3c/web-platform-tests/selection/textcontrols/selectionchange-expected.txt: Added.

* Source/WebCore/editing/FrameSelection.cpp:
(WebCore::FrameSelection::setSelectionWithoutUpdatingAppearance): Fire selectionchange event on input
element and textarea element when the selection resides within those elements.

* Source/WebCore/html/HTMLTextAreaElement.cpp:
(WebCore::HTMLTextAreaElement::setValueCommon): Fixed a bug that this code was always updating selection
even when TextControlSetValueSelection::DoNotSet is passed in.

* Source/WebCore/html/HTMLTextFormControlElement.cpp:
(WebCore::HTMLTextFormControlElement::selectionChanged): Made this function return a boolean indicating
whether the selection end points have changed or not.

(WebCore::HTMLTextFormControlElement::scheduleSelectEvent):
* Source/WebCore/html/HTMLTextFormControlElement.h:

Canonical link: https://commits.webkit.org/276238@main
  • Loading branch information
rniwa committed Mar 16, 2024
1 parent 473ac0f commit 0a064d4
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 64 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@



Harness Error (TIMEOUT), message = null

TIMEOUT selectionchange bubbles from input Test timed out
NOTRUN selectionchange bubbles from input when focused
NOTRUN selectionchange bubbles from textarea
NOTRUN selectionchange bubbles from textarea when focused
PASS selectionchange bubbles from input
PASS selectionchange bubbles from input when focused
PASS selectionchange bubbles from textarea
PASS selectionchange bubbles from textarea when focused

Original file line number Diff line number Diff line change
@@ -1,64 +1,64 @@



FAIL Modifying selectionStart value of the input element assert_equals: expected 1 but got 0
FAIL Modifying selectionEnd value of the input element assert_equals: expected 1 but got 0
FAIL Calling setSelectionRange() on the input element assert_equals: expected 1 but got 0
FAIL Calling select() on the input element assert_equals: expected 1 but got 0
FAIL Calling setRangeText() on the input element assert_equals: expected 1 but got 0
PASS Modifying selectionStart value of the input element
PASS Modifying selectionEnd value of the input element
PASS Calling setSelectionRange() on the input element
PASS Calling select() on the input element
PASS Calling setRangeText() on the input element
PASS Setting initial zero selectionStart value on the input element
FAIL Setting the same selectionStart value twice on the input element assert_equals: expected 1 but got 0
PASS Setting the same selectionStart value twice on the input element
PASS Setting initial zero selectionEnd value on the input element
FAIL Setting the same selectionEnd value twice on the input element assert_equals: expected 1 but got 0
PASS Setting the same selectionEnd value twice on the input element
PASS Setting initial zero selection range on the input element
FAIL Setting the same selection range twice on the input element assert_equals: expected 1 but got 0
FAIL Calling select() twice on the input element assert_equals: expected 1 but got 0
FAIL Calling setRangeText() after select() on the input element assert_equals: expected 2 but got 0
FAIL Calling setRangeText() repeatedly on the input element assert_equals: expected 4 but got 0
PASS Setting the same selection range twice on the input element
PASS Calling select() twice on the input element
PASS Calling setRangeText() after select() on the input element
PASS Calling setRangeText() repeatedly on the input element
PASS Calling setRangeText() on empty the input element
FAIL Modifying selectionStart value of the disconnected input element assert_equals: expected 1 but got 0
FAIL Modifying selectionEnd value of the disconnected input element assert_equals: expected 1 but got 0
FAIL Calling setSelectionRange() on the disconnected input element assert_equals: expected 1 but got 0
FAIL Calling select() on the disconnected input element assert_equals: expected 1 but got 0
FAIL Calling setRangeText() on the disconnected input element assert_equals: expected 1 but got 0
PASS Modifying selectionStart value of the disconnected input element
PASS Modifying selectionEnd value of the disconnected input element
PASS Calling setSelectionRange() on the disconnected input element
PASS Calling select() on the disconnected input element
PASS Calling setRangeText() on the disconnected input element
PASS Setting initial zero selectionStart value on the disconnected input element
FAIL Setting the same selectionStart value twice on the disconnected input element assert_equals: expected 1 but got 0
PASS Setting the same selectionStart value twice on the disconnected input element
PASS Setting initial zero selectionEnd value on the disconnected input element
FAIL Setting the same selectionEnd value twice on the disconnected input element assert_equals: expected 1 but got 0
PASS Setting the same selectionEnd value twice on the disconnected input element
PASS Setting initial zero selection range on the disconnected input element
FAIL Setting the same selection range twice on the disconnected input element assert_equals: expected 1 but got 0
FAIL Calling select() twice on the disconnected input element assert_equals: expected 1 but got 0
FAIL Calling setRangeText() after select() on the disconnected input element assert_equals: expected 2 but got 0
FAIL Calling setRangeText() repeatedly on the disconnected input element assert_equals: expected 4 but got 0
PASS Setting the same selection range twice on the disconnected input element
PASS Calling select() twice on the disconnected input element
PASS Calling setRangeText() after select() on the disconnected input element
PASS Calling setRangeText() repeatedly on the disconnected input element
PASS Calling setRangeText() on empty the disconnected input element
FAIL Modifying selectionStart value of the textarea element assert_equals: expected 1 but got 0
FAIL Modifying selectionEnd value of the textarea element assert_equals: expected 1 but got 0
FAIL Calling setSelectionRange() on the textarea element assert_equals: expected 1 but got 0
FAIL Calling select() on the textarea element assert_equals: expected 1 but got 0
FAIL Calling setRangeText() on the textarea element assert_equals: expected 1 but got 0
PASS Modifying selectionStart value of the textarea element
PASS Modifying selectionEnd value of the textarea element
PASS Calling setSelectionRange() on the textarea element
PASS Calling select() on the textarea element
PASS Calling setRangeText() on the textarea element
PASS Setting initial zero selectionStart value on the textarea element
FAIL Setting the same selectionStart value twice on the textarea element assert_equals: expected 1 but got 0
PASS Setting the same selectionStart value twice on the textarea element
PASS Setting initial zero selectionEnd value on the textarea element
FAIL Setting the same selectionEnd value twice on the textarea element assert_equals: expected 1 but got 0
PASS Setting the same selectionEnd value twice on the textarea element
PASS Setting initial zero selection range on the textarea element
FAIL Setting the same selection range twice on the textarea element assert_equals: expected 1 but got 0
FAIL Calling select() twice on the textarea element assert_equals: expected 1 but got 0
FAIL Calling setRangeText() after select() on the textarea element assert_equals: expected 2 but got 0
FAIL Calling setRangeText() repeatedly on the textarea element assert_equals: expected 4 but got 0
PASS Setting the same selection range twice on the textarea element
PASS Calling select() twice on the textarea element
PASS Calling setRangeText() after select() on the textarea element
PASS Calling setRangeText() repeatedly on the textarea element
PASS Calling setRangeText() on empty the textarea element
FAIL Modifying selectionStart value of the disconnected textarea element assert_equals: expected 1 but got 0
FAIL Modifying selectionEnd value of the disconnected textarea element assert_equals: expected 1 but got 0
FAIL Calling setSelectionRange() on the disconnected textarea element assert_equals: expected 1 but got 0
FAIL Calling select() on the disconnected textarea element assert_equals: expected 1 but got 0
FAIL Calling setRangeText() on the disconnected textarea element assert_equals: expected 1 but got 0
PASS Modifying selectionStart value of the disconnected textarea element
PASS Modifying selectionEnd value of the disconnected textarea element
PASS Calling setSelectionRange() on the disconnected textarea element
PASS Calling select() on the disconnected textarea element
PASS Calling setRangeText() on the disconnected textarea element
PASS Setting initial zero selectionStart value on the disconnected textarea element
FAIL Setting the same selectionStart value twice on the disconnected textarea element assert_equals: expected 1 but got 0
PASS Setting the same selectionStart value twice on the disconnected textarea element
PASS Setting initial zero selectionEnd value on the disconnected textarea element
FAIL Setting the same selectionEnd value twice on the disconnected textarea element assert_equals: expected 1 but got 0
PASS Setting the same selectionEnd value twice on the disconnected textarea element
PASS Setting initial zero selection range on the disconnected textarea element
FAIL Setting the same selection range twice on the disconnected textarea element assert_equals: expected 1 but got 0
FAIL Calling select() twice on the disconnected textarea element assert_equals: expected 1 but got 0
FAIL Calling setRangeText() after select() on the disconnected textarea element assert_equals: expected 2 but got 0
FAIL Calling setRangeText() repeatedly on the disconnected textarea element assert_equals: expected 4 but got 0
PASS Setting the same selection range twice on the disconnected textarea element
PASS Calling select() twice on the disconnected textarea element
PASS Calling setRangeText() after select() on the disconnected textarea element
PASS Calling setRangeText() repeatedly on the disconnected textarea element
PASS Calling setRangeText() on empty the disconnected textarea element

Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@



PASS Modifying selectionStart value of the input element
PASS Modifying selectionEnd value of the input element
PASS Calling setSelectionRange() on the input element
PASS Calling select() on the input element
PASS Calling setRangeText() on the input element
PASS Setting initial zero selectionStart value on the input element
PASS Setting the same selectionStart value twice on the input element
PASS Setting initial zero selectionEnd value on the input element
PASS Setting the same selectionEnd value twice on the input element
PASS Setting initial zero selection range on the input element
PASS Setting the same selection range twice on the input element
PASS Calling select() twice on the input element
PASS Calling setRangeText() after select() on the input element
PASS Calling setRangeText() repeatedly on the input element
FAIL Calling setRangeText() on empty the input element assert_equals: expected 0 but got 1
PASS Modifying selectionStart value of the disconnected input element
PASS Modifying selectionEnd value of the disconnected input element
PASS Calling setSelectionRange() on the disconnected input element
PASS Calling select() on the disconnected input element
PASS Calling setRangeText() on the disconnected input element
PASS Setting initial zero selectionStart value on the disconnected input element
PASS Setting the same selectionStart value twice on the disconnected input element
PASS Setting initial zero selectionEnd value on the disconnected input element
PASS Setting the same selectionEnd value twice on the disconnected input element
PASS Setting initial zero selection range on the disconnected input element
PASS Setting the same selection range twice on the disconnected input element
PASS Calling select() twice on the disconnected input element
PASS Calling setRangeText() after select() on the disconnected input element
PASS Calling setRangeText() repeatedly on the disconnected input element
FAIL Calling setRangeText() on empty the disconnected input element assert_equals: expected 0 but got 1
PASS Modifying selectionStart value of the textarea element
PASS Modifying selectionEnd value of the textarea element
PASS Calling setSelectionRange() on the textarea element
PASS Calling select() on the textarea element
PASS Calling setRangeText() on the textarea element
PASS Setting initial zero selectionStart value on the textarea element
PASS Setting the same selectionStart value twice on the textarea element
PASS Setting initial zero selectionEnd value on the textarea element
PASS Setting the same selectionEnd value twice on the textarea element
PASS Setting initial zero selection range on the textarea element
PASS Setting the same selection range twice on the textarea element
PASS Calling select() twice on the textarea element
PASS Calling setRangeText() after select() on the textarea element
PASS Calling setRangeText() repeatedly on the textarea element
PASS Calling setRangeText() on empty the textarea element
PASS Modifying selectionStart value of the disconnected textarea element
PASS Modifying selectionEnd value of the disconnected textarea element
PASS Calling setSelectionRange() on the disconnected textarea element
PASS Calling select() on the disconnected textarea element
PASS Calling setRangeText() on the disconnected textarea element
PASS Setting initial zero selectionStart value on the disconnected textarea element
PASS Setting the same selectionStart value twice on the disconnected textarea element
PASS Setting initial zero selectionEnd value on the disconnected textarea element
PASS Setting the same selectionEnd value twice on the disconnected textarea element
PASS Setting initial zero selection range on the disconnected textarea element
PASS Setting the same selection range twice on the disconnected textarea element
PASS Calling select() twice on the disconnected textarea element
PASS Calling setRangeText() after select() on the disconnected textarea element
PASS Calling setRangeText() repeatedly on the disconnected textarea element
FAIL Calling setRangeText() on empty the disconnected textarea element assert_equals: expected 0 but got 1

24 changes: 19 additions & 5 deletions Source/WebCore/editing/FrameSelection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
#include "FloatQuad.h"
#include "FocusController.h"
#include "FrameTree.h"
#include "GCReachableRef.h"
#include "GraphicsContext.h"
#include "HTMLBodyElement.h"
#include "HTMLFormElement.h"
Expand Down Expand Up @@ -407,8 +408,11 @@ bool FrameSelection::setSelectionWithoutUpdatingAppearance(const VisibleSelectio
}

// Selection offsets should increase when LF is inserted before the caret in InsertLineBreakCommand. See <https://webkit.org/b/56061>.
if (RefPtr textControl = enclosingTextFormControl(newSelection.start()))
textControl->selectionChanged(options.contains(SetSelectionOption::FireSelectEvent));
// https://www.w3.org/TR/selection-api/#selectionchange-event
RefPtr textControl = enclosingTextFormControl(newSelection.start());
bool shouldScheduleSelectionChangeEvent = willMutateSelection;
if (textControl)
shouldScheduleSelectionChangeEvent = textControl->selectionChanged(options.contains(SetSelectionOption::FireSelectEvent));

if (!willMutateSelection)
return false;
Expand All @@ -430,9 +434,19 @@ bool FrameSelection::setSelectionWithoutUpdatingAppearance(const VisibleSelectio
m_xPosForVerticalArrowNavigation = std::nullopt;
selectFrameElementInParentIfFullySelected();
document->editor().respondToChangedSelection(oldSelection, options);
// https://www.w3.org/TR/selection-api/#selectionchange-event
// FIXME: Spec doesn't specify which task source to use.
document->queueTaskToDispatchEvent(TaskSource::UserInteraction, Event::create(eventNames().selectionchangeEvent, Event::CanBubble::No, Event::IsCancelable::No));

if (shouldScheduleSelectionChangeEvent) {
if (textControl) {
document->eventLoop().queueTask(TaskSource::UserInteraction, [textControl = GCReachableRef { *textControl }] {
textControl->dispatchEvent(Event::create(eventNames().selectionchangeEvent, Event::CanBubble::Yes, Event::IsCancelable::No));
});
} else {
document->eventLoop().queueTask(TaskSource::UserInteraction, [weakDocument = WeakPtr { document.get() }] {
if (RefPtr document = weakDocument.get())
document->dispatchEvent(Event::create(eventNames().selectionchangeEvent, Event::CanBubble::No, Event::IsCancelable::No));
});
}
}

return true;
}
Expand Down
14 changes: 8 additions & 6 deletions Source/WebCore/html/HTMLTextAreaElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -383,12 +383,14 @@ void HTMLTextAreaElement::setValueCommon(const String& newValue, TextFieldEventB
setFormControlValueMatchesRenderer(true);

auto endOfString = m_value.length();
if (document().focusedElement() == this)
setSelectionRange(endOfString, endOfString);
else if (selection == TextControlSetValueSelection::SetSelectionToEnd) {
// We don't change text selection here but need to update caret to
// the end of the text value except for initialize.
cacheSelection(endOfString, endOfString, SelectionHasNoDirection);
if (selection == TextControlSetValueSelection::SetSelectionToEnd) {
if (document().focusedElement() == this)
setSelectionRange(endOfString, endOfString);
else {
// We don't change text selection here but need to update caret to
// the end of the text value except for initialize.
cacheSelection(endOfString, endOfString, SelectionHasNoDirection);
}
} else if (shouldClamp)
cacheSelection(std::min(endOfString, selectionStartValue), std::min(endOfString, selectionEndValue), SelectionHasNoDirection);

Expand Down
9 changes: 7 additions & 2 deletions Source/WebCore/html/HTMLTextFormControlElement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -555,24 +555,29 @@ void HTMLTextFormControlElement::restoreCachedSelection(SelectionRevealMode reve
scheduleSelectEvent();
}

void HTMLTextFormControlElement::selectionChanged(bool shouldFireSelectEvent)
bool HTMLTextFormControlElement::selectionChanged(bool shouldFireSelectEvent)
{
if (!isTextField())
return;
return false;

// FIXME: Don't re-compute selection start and end if this function was called inside setSelectionRange.
// selectionStart() or selectionEnd() will return cached selection when this node doesn't have focus
unsigned previousSelectionStart = m_cachedSelectionStart;
unsigned previousSelectionEnd = m_cachedSelectionEnd;
cacheSelection(computeSelectionStart(), computeSelectionEnd(), computeSelectionDirection());

document().setHasEverHadSelectionInsideTextFormControl();

if (shouldFireSelectEvent && m_cachedSelectionStart != m_cachedSelectionEnd)
dispatchEvent(Event::create(eventNames().selectEvent, Event::CanBubble::Yes, Event::IsCancelable::No));

return previousSelectionStart != m_cachedSelectionStart || previousSelectionEnd != m_cachedSelectionEnd;
}

void HTMLTextFormControlElement::scheduleSelectEvent()
{
queueTaskToDispatchEvent(TaskSource::UserInteraction, Event::create(eventNames().selectEvent, Event::CanBubble::Yes, Event::IsCancelable::No));
queueTaskToDispatchEvent(TaskSource::UserInteraction, Event::create(eventNames().selectionchangeEvent, Event::CanBubble::Yes, Event::IsCancelable::No));
}

void HTMLTextFormControlElement::attributeChanged(const QualifiedName& name, const AtomString& oldValue, const AtomString& newValue, AttributeModificationReason attributeModificationReason)
Expand Down
2 changes: 1 addition & 1 deletion Source/WebCore/html/HTMLTextFormControlElement.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ class HTMLTextFormControlElement : public HTMLFormControlElement {

virtual bool dirAutoUsesValue() const = 0;

void selectionChanged(bool shouldFireSelectEvent);
bool selectionChanged(bool shouldFireSelectEvent);
WEBCORE_EXPORT bool lastChangeWasUserEdit() const;
void setInnerTextValue(String&&);
String innerTextValue() const;
Expand Down

0 comments on commit 0a064d4

Please sign in to comment.