#11242·tinymce

`fixInLISelection` cancels drag-select when clicking to the right of a plain list item's text (regression in 8.8.0)

Author: gidomandersCreated Aug 25, 2026Updated Sep 1, 2026
Labelstype: bug

Provide detailed reproduction steps (if any)

Quirks.fixInLISelection calls preventDefault() on mousedown for any click landing to the right of a list item's text, including list items that contain no block child. That cancels the browser's native drag-selection, so a selection gesture started there produces nothing.

Minimal demo (paste into a fiddle — inline mode is not required, a classic editor reproduces it too):

xml
<div id="editor">
  <p>Paragraph above the list.</p>
  <ul>
    <li>First list item</li>
    <li>Second list item, click to the right of this text</li>
  </ul>
  <h2>A heading after the list</h2>
  <p>Paragraph below the heading.</p>
</div>

<script>
tinymce.init({ selector: '#editor', plugins: 'lists', license_key: 'gpl' });
</script>
  1. Press the left mouse button in the empty space to the right of the text of the second <li> (same line as the text, past its last character).
  2. Keeping the button down, drag anywhere — up into the first list item, or down past the heading.
  3. Release.

Both drag directions fail. For contrast, performing the identical gesture starting to the right of the <h2> text works normally — the difference is the isListItem guard in the handler.

✔️ Expected result

A text selection is created, anchored at the end of the list item's text and extending to wherever the pointer was released — the same as starting the drag to the right of a paragraph or heading.

❌ Actual result

No selection is created. The caret is placed at the end of the list item's text and the selection stays collapsed for the whole gesture. Measured on mousedown: event.defaultPrevented === true; throughout mousemove and after mouseup: selection.isCollapsed === true, anchor and focus both at the end offset of the list item's text node.

The stack at the cancelling call is executeHandlers → dispatch → dispatch → fixInLISelection handler → Optional.fold → Arr.each → Arr.each → selectPos → preventDefault.

❓ Possible solution

The preventDefault() lives in the shared helper:

javascript
const selectPos = (editor, e, pos) => {
  e.preventDefault();          // cancels the whole gesture, drag included
  editor.focus();
  editor.selection.setRng(pos.toRange());
};

Two options, in order of preference:

  1. Correct the caret without cancelling the gesture. Instead of repositioning on mousedown, do it on mouseup/click and only when the resulting selection is still collapsed. A drag then leaves its own selection intact, while a plain click still gets the corrected caret — this keeps both branches of fixInLISelection working, including the original Chromium case.

  2. Minimal change: drop the preventDefault() from the no-block-child branch. The Chromium bug this quirk works around (issues.chromium.org/issues/40767343) concerns a LI that contains a block element — as the function's own comment states. For a plain <li>text</li> the browser already places the caret at the end of the text, so the correction is a no-op and the preventDefault() is pure loss.

For what it's worth, the following works as a local workaround and leaves the block-child branch untouched (prepended so it runs before the quirk handler):

javascript
const hasBlockChild = (li) => Array.from(li.childNodes).some((c) =>
  c.nodeName === 'BR' || (c.nodeType === 1 && getComputedStyle(c).display === 'block'));

editor.on('mousedown', (e) => {
  if (e.detail !== 1 || e.button !== 0) return;
  const li = e.target?.closest?.('li');
  if (!li || hasBlockChild(li) || !li.lastChild) return;
  const rng = document.createRange();
  rng.selectNodeContents(li);
  const rects = [...rng.getClientRects()];
  const line = rects[rects.length - 1];
  if (!line) return;
  if (e.clientX >= line.right && e.clientY >= line.top && e.clientY <= line.bottom) {
    e.preventDefault = () => {};
  }
}, true);

️ Other details

  • Browser:
    • Chrome / Chromium 148
    • Vivaldi 8.1.4087.64 / Chromium 150.0.7871.232
    • The quirk is Chromium-guarded, so Firefox and Safari are unaffected
  • OS: Windows 10 and macOS 15
  • First affected version: 8.8.0
  • Worked in version: 8.7.0

In 8.7.0 the handler was firstBlockChildOrNewLine(target).each(...), so it only ran for list items that actually contain a block child. PR #11172 (milestone 8.8) turned that into .fold(...) and added the click-after-last-child branch, which is what brings plain list items into scope.