Bug 1712255 - Defer SetMaxLength in SetValueFromSetRangeText r=masayuki

Capping selection range in SetValue early makes the subsequent SetSelectionRange call unable to detect actual selection range change. This patch defers it so that select events can be consistently fired.

Differential Revision: https://phabricator.services.mozilla.com/D115729
This commit is contained in:
Kagami Sascha Rosylight
2021-05-24 03:11:27 +00:00
parent 9c36f0af49
commit 55be2e6e45
6 changed files with 58 additions and 7 deletions

View File

@@ -5441,6 +5441,7 @@ void HTMLInputElement::GetValueFromSetRangeText(nsAString& aValue) {
nsresult HTMLInputElement::SetValueFromSetRangeText(const nsAString& aValue) {
return SetValueInternal(aValue, {ValueSetterOption::ByContentAPI,
ValueSetterOption::BySetRangeTextAPI,
ValueSetterOption::SetValueChanged});
}

View File

@@ -660,6 +660,7 @@ void HTMLTextAreaElement::GetValueFromSetRangeText(nsAString& aValue) {
nsresult HTMLTextAreaElement::SetValueFromSetRangeText(
const nsAString& aValue) {
return SetValueInternal(aValue, {ValueSetterOption::ByContentAPI,
ValueSetterOption::BySetRangeTextAPI,
ValueSetterOption::SetValueChanged});
}

View File

@@ -2338,7 +2338,10 @@ void TextControlState::SetRangeText(const nsAString& aReplacement,
}
SetSelectionRange(selectionStart, selectionEnd, Optional<nsAString>(), aRv);
// The instance may have already been deleted here.
if (IsSelectionCached()) {
// SetValueFromSetRangeText skipped SetMaxLength, set it here properly
GetSelectionProperties().SetMaxLength(value.Length());
}
}
void TextControlState::DestroyEditor() {
@@ -2902,7 +2905,13 @@ bool TextControlState::SetValueWithoutTextEditor(
aHandlingSetValue.ValueSetterOptionsRef()));
SelectionProperties& props = GetSelectionProperties();
props.SetMaxLength(aHandlingSetValue.GetSettingValue().Length());
// Setting a max length and thus capping selection range early prevents
// selection change detection in setRangeText. Temporarily disable
// capping here with UINT32_MAX, and set it later in ::SetRangeText().
props.SetMaxLength(aHandlingSetValue.ValueSetterOptionsRef().contains(
ValueSetterOption::BySetRangeTextAPI)
? UINT32_MAX
: aHandlingSetValue.GetSettingValue().Length());
if (aHandlingSetValue.ValueSetterOptionsRef().contains(
ValueSetterOption::MoveCursorToEndIfValueChanged)) {
props.SetStart(aHandlingSetValue.GetSettingValue().Length());

View File

@@ -183,6 +183,9 @@ class TextControlState final : public SupportsWeakPtr {
// The value is changed by changing value attribute of the element or
// something like setRangeText().
ByContentAPI,
// The value is changed by setRangeText(). Intended to prevent silent
// selection range change.
BySetRangeTextAPI,
// Whether SetValueChanged should be called as a result of this value
// change.
SetValueChanged,

View File

@@ -32,6 +32,12 @@
input,
]
function untilEvent(element, eventName) {
return new Promise((resolve) => {
element.addEventListener(eventName, resolve, { once: true });
});
}
elements.forEach(function(element) {
test(function() {
element.value = "foobar";
@@ -105,16 +111,16 @@
});
}, element.id + " setRangeText without argument throws a type error");
async_test(function() {
promise_test(async (t) => {
// At this point there are already "select" events queued up on
// "element". Give them time to fire; otherwise we can get spurious
// passes.
//
// This is unfortunately racy in that we might _still_ get spurious
// passes. I'm not sure how best to handle that.
this.step_timeout(function() {
t.step_timeout(function() {
var q = false;
element.onselect = this.step_func_done(function(e) {
element.onselect = t.step_func_done(function(e) {
assert_true(q, "event should be queued");
assert_true(e.isTrusted, "event is trusted");
assert_true(e.bubbles, "event bubbles");
@@ -125,5 +131,25 @@
}, 10);
}, element.id + " setRangeText fires a select event");
promise_test(async () => {
element.value = "XXXXXXXXXXXXXXXXXXX";
const { length } = element.value;
element.setSelectionRange(0, length);
await untilEvent(element, "select");
element.setRangeText("foo", 2, 2);
await untilEvent(element, "select");
assert_equals(element.selectionStart, 0, ".selectionStart");
assert_equals(element.selectionEnd, length + 3, ".selectionEnd");
}, element.id + " setRangeText fires a select event when fully selected");
promise_test(async () => {
element.value = "XXXXXXXXXXXXXXXXXXX";
element.select();
await untilEvent(element, "select");
element.setRangeText("foo", 2, 2);
await untilEvent(element, "select");
assert_equals(element.selectionStart, 0, ".selectionStart");
assert_equals(element.selectionEnd, element.value.length, ".selectionEnd");
}, element.id + " setRangeText fires a select event after select()");
})
</script>

View File

@@ -11,8 +11,8 @@
}
</style>
<input id="input" value="XXXXXXXXXXXXXXXXXXX" width="200"><br>
<textarea id="textarea" width="200">XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX</textarea>
<input id="input" width="200"><br>
<textarea id="textarea" width="200"></textarea>
<script>
class SelectionChangeCollector {
@@ -40,6 +40,7 @@
],
async initialize() {
for (const collector of this.collectors) {
collector.target.value = "XXXXXXXXXXXXXXXXXXX";
collector.target.blur();
collector.target.setSelectionRange(0, 0);
}
@@ -175,5 +176,15 @@
await data.assert_empty_spin();
assert_equals(collector.events.length, 1);
}, `Calling select() twice on ${name}`);
promise_test(async () => {
await data.initialize();
target.select();
target.setRangeText("foo", 2, 6);
await data.assert_empty_spin();
assert_equals(collector.events.length, 2);
}, `Calling setRangeText() after select() on ${name}`);
}
</script>