Skip to content

Commit

Permalink
Make AdjustCaretFrameForLineEnd stop scanning text node if first no…
Browse files Browse the repository at this point in the history
…de is editable but reached non-editable one

When pressing `ArrowLeft` key at:
```
<p>abc</p><p><span contenteditable=false>def</span>{}<br></p>
```
, caret is moved into the non-editable text because
`nsCaret::GetCaretFrameForNodeOffset` looks for a preceding text node from
the `BRFrame`. This is required if the preceding text node ends with a
collapsible white-space but followed by a `<br>` because the text frame
should not contain the white-space rect and `BRFrame` frame should be next
to it, i.e., the white-space looks like a overflown content.

So, for rendering a caret, the method needs to return non-editable text frame
even in the case, but for considering new caret position in the DOM tree, it
should not return non-editable text frame.

Therefore, this patch adds a param to `nsCaret::GetCaretFrameForNodeOffset()`
which forcibly return editable content frame or not and makes
`Selection::GetPrimaryOrCaretFrameForNodeOffset` call it with the new option
because its callers are the handler of caret navigation.

Differential Revision: https://phabricator.services.mozilla.com/D196259

bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1612076
gecko-commit: 7e32f178b02fef938f803492c4bfd6d897ffcb21
gecko-reviewers: emilio
  • Loading branch information
masayuki-nakano authored and moz-wptsync-bot committed Dec 19, 2023
1 parent 967023d commit 1cea438
Showing 1 changed file with 189 additions and 0 deletions.
189 changes: 189 additions & 0 deletions selection/caret/move-around-contenteditable-false.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Don't move caret to non-editable node from a editable node</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<script>
"use strict";

function getRangeDescription(range) {
function getNodeDescription(node) {
if (!node) {
return "null";
}
switch (node.nodeType) {
case Node.TEXT_NODE:
return `${node.nodeName} "${node.data}"`;
case Node.ELEMENT_NODE:
return `<${node.nodeName.toLowerCase()}>`;
default:
return `${node.nodeName}`;
}
}
if (range === null) {
return "null";
}
if (range === undefined) {
return "undefined";
}
return range.startContainer == range.endContainer &&
range.startOffset == range.endOffset
? `(${getNodeDescription(range.startContainer)}, ${range.startOffset})`
: `(${getNodeDescription(range.startContainer)}, ${
range.startOffset
}) - (${getNodeDescription(range.endContainer)}, ${range.endOffset})`;
}

function sendArrowRightKey() {
const kArrowRight = "\uE014";
return new test_driver.Actions()
.keyDown(kArrowRight)
.keyUp(kArrowRight)
.send();
}

function sendArrowLeftKey() {
const kArrowLeft = "\uE012";
return new test_driver.Actions()
.keyDown(kArrowLeft)
.keyUp(kArrowLeft)
.send();
}

promise_test(async () => {
await new Promise(resolve => {
addEventListener("load", resolve, {once: true});
});
}, "Initializing tests");

promise_test(async t => {
const editingHost = document.querySelector("div[contenteditable]");
editingHost.focus();
const p = editingHost.querySelector("p");
getSelection().collapse(p.firstChild, "abc".length);
await sendArrowRightKey();
test(() => {
assert_equals(
getRangeDescription(getSelection().getRangeAt(0)),
getRangeDescription({
startContainer: p.nextSibling,
startOffset: 0,
endContainer: p.nextSibling,
endOffset: 0,
}),
);
}, `${t.name}: first arrow-right should move caret before non-editable text`);
await sendArrowRightKey();
test(() => {
assert_equals(
getRangeDescription(getSelection().getRangeAt(0)),
getRangeDescription({
startContainer: p.nextSibling,
startOffset: 1,
endContainer: p.nextSibling,
endOffset: 1,
}),
);
}, `${t.name}: second arrow-right should move caret after non-editable text`);
}, "Move caret from end of editable text node to <br> following non-editable text in next paragraph");

promise_test(async t => {
const editingHost = document.querySelector("div[contenteditable]");
editingHost.focus();
const p = editingHost.querySelector("p");
getSelection().collapse(p.nextSibling, 1);
await sendArrowLeftKey();
assert_false(
editingHost.querySelector("[contenteditable=false]").contains(getSelection().focusNode),
"focus node should not be the non-editable nodes"
);
assert_false(
editingHost.querySelector("[contenteditable=false]").contains(getSelection().anchorNode),
"anchor node should not be the non-editable nodes"
);
test(() => {
assert_equals(
getRangeDescription(getSelection().getRangeAt(0)),
getRangeDescription({
startContainer: p.nextSibling,
startOffset: 0,
endContainer: p.nextSibling,
endOffset: 0,
}),
);
}, `${t.name}: first arrow-left should move caret before non-editable text`);
}, "Move caret from <br> following non-editable text to end of preceding editable text in next paragraph");

promise_test(async t => {
const editingHost = document.querySelector("div[contenteditable] + div[contenteditable]");
editingHost.focus();
const p = editingHost.querySelector("p");
getSelection().collapse(p.firstChild, 0);
await sendArrowRightKey();
test(() => {
assert_equals(
getRangeDescription(getSelection().getRangeAt(0)),
getRangeDescription({
startContainer: p.nextSibling,
startOffset: 0,
endContainer: p.nextSibling,
endOffset: 0,
}),
);
}, `${t.name}: first arrow-right should move caret before non-editable text`);
await sendArrowRightKey();
test(() => {
assert_equals(
getRangeDescription(getSelection().getRangeAt(0)),
getRangeDescription({
startContainer: editingHost.querySelector("[contenteditable=false]").nextSibling,
startOffset: 0,
endContainer: editingHost.querySelector("[contenteditable=false]").nextSibling,
endOffset: 0,
}),
);
}, `${t.name}: second arrow-right should move caret after non-editable text`);
}, "Move caret from empty editable paragraph to editable text following non-editable text in next paragraph");

promise_test(async t => {
const editingHost = document.querySelector("div[contenteditable] + div[contenteditable]");
editingHost.focus();
const p = editingHost.querySelector("p");
getSelection().collapse(editingHost.querySelector("[contenteditable=false]").nextSibling, 0);
await sendArrowLeftKey();
assert_false(
editingHost.querySelector("[contenteditable=false]").contains(getSelection().focusNode),
"focus node should not be the non-editable nodes"
);
assert_false(
editingHost.querySelector("[contenteditable=false]").contains(getSelection().anchorNode),
"anchor node should not be the non-editable nodes"
);
test(() => {
assert_equals(
getRangeDescription(getSelection().getRangeAt(0)),
getRangeDescription({
startContainer: p.nextSibling,
startOffset: 0,
endContainer: p.nextSibling,
endOffset: 0,
}),
);
}, `${t.name}: first arrow-left should move caret before non-editable text`);
}, "Move caret from start of text following non-editable text to empty preceding editable paragraph");
</script>
</head>
<body>
<div contenteditable>
<p>abc</p><p><span contenteditable="false">def</span><br></p>
</div>
<div contenteditable>
<p><br></p><p><span contenteditable="false">abc</span>def</p>
</div>
</body>
</html>

0 comments on commit 1cea438

Please sign in to comment.