LibWeb: Always focus on editing host if currently not focused

We were constraining the focusing behavior for editing hosts a bit too
much; regardless of how the selection changed, if the start container is
inside an editing host and it's currently not focused, we should focus
it. This fixes focus stealing by other elements that set a selection
inside an editing host on a click event, for example.
This commit is contained in:
Jelle Raaijmakers 2025-08-20 10:49:28 +02:00 committed by Jelle Raaijmakers
commit 738eb68dda
Notes: github-actions[bot] 2025-08-20 10:05:42 +00:00
3 changed files with 33 additions and 11 deletions

View file

@ -549,9 +549,8 @@ void Selection::set_range(GC::Ptr<DOM::Range> range)
// https://developer.mozilla.org/en-US/docs/Web/API/Selection#behavior_of_selection_api_in_terms_of_editing_host_focus_changes
// AD-HOC: Focus editing host if the previous selection was outside of it. There seems to be no spec for this.
if (range && range->start_container()->is_editable_or_editing_host()) {
GC::Ptr old_editing_host = old_range ? old_range->start_container()->editing_host() : nullptr;
GC::Ref new_editing_host = *range->start_container()->editing_host();
if (new_editing_host != old_editing_host && document()->focused_element() != new_editing_host) {
if (document()->focused_element() != new_editing_host) {
// FIXME: Determine and propagate the right focus trigger.
HTML::run_focusing_steps(new_editing_host, nullptr, HTML::FocusTrigger::Other);
}

View file

@ -1,2 +1,9 @@
-- Simple editing host --
Range: [object Text] 1 [object Text] 2
document.activeElement: [object HTMLDivElement]
<DIV id="a">
-- Editing host with nested <span> --
Range: [object Text] 1 [object Text] 1
<DIV id="b">
-- Refocusing on same editing host --
Range: [object Text] 0 [object Text] 0
<DIV id="b">

View file

@ -1,15 +1,31 @@
<!DOCTYPE html>
<div contenteditable>foo</div>
<div id="a" contenteditable>foo</div>
<div id="b" contenteditable>foo<span>bar</span></div>
<button id="c">press me</button>
<script src="include.js"></script>
<script>
test(() => {
const divElm = document.querySelector('div[contenteditable]');
const range = document.createRange();
range.setStart(divElm.childNodes[0], 1);
range.setEnd(divElm.childNodes[0], 2);
window.getSelection().addRange(range);
function reportSelectionAndFocus() {
const range = getSelection().getRangeAt(0);
println(`Range: ${range.startContainer} ${range.startOffset} ${range.endContainer} ${range.endOffset}`);
printElement(document.activeElement);
}
println(`Range: ${range.startContainer} ${range.startOffset} ${range.endContainer} ${range.endOffset}`);
println(`document.activeElement: ${document.activeElement}`);
println('-- Simple editing host --');
const divA = document.querySelector('div#a');
getSelection().setBaseAndExtent(divA.childNodes[0], 1, divA.childNodes[0], 2);
reportSelectionAndFocus();
println('-- Editing host with nested <span> --');
const divB = document.querySelector('div#b');
getSelection().setBaseAndExtent(divB.childNodes[1].childNodes[0], 1, divB.childNodes[1].childNodes[0], 1);
reportSelectionAndFocus();
println('-- Refocusing on same editing host --');
const buttonElm = document.querySelector('button#c');
buttonElm.addEventListener('click', () => getSelection().setBaseAndExtent(divB.childNodes[0], 0, divB.childNodes[0], 0));
const buttonRect = buttonElm.getBoundingClientRect();
internals.click(buttonRect.left + 5, buttonRect.top + 5);
reportSelectionAndFocus();
});
</script>