Skip to content

[lexical] Bug Fix: scroll collapsed selection horizontally in overflow containers#8495

Draft
Frank20021 wants to merge 3 commits into
facebook:mainfrom
Frank20021:feat/8459-code-block-horizontal-scroll
Draft

[lexical] Bug Fix: scroll collapsed selection horizontally in overflow containers#8495
Frank20021 wants to merge 3 commits into
facebook:mainfrom
Frank20021:feat/8459-code-block-horizontal-scroll

Conversation

@Frank20021

Copy link
Copy Markdown

Description

Current behavior: When the caret sits inside a horizontally scrollable block (for example a code block with long lines and overflow-x: auto), moving the collapsed selection with keyboard shortcuts such as Ctrl+ArrowLeft / Ctrl+ArrowRight can place the caret outside the visible scroll region. scrollIntoViewIfNeeded only adjusted vertical scroll and only walked ancestors starting from the editor root, so inner scroll containers were not scrolled on the X axis.

This PR: Extends scrollIntoViewIfNeeded to:

  • Walk scroll ancestors starting from an optional DOM “chain start” (the caret’s DOM node chain), so nested horizontal scrollers are included.
  • Apply horizontal scrolling on elements that can scroll horizontally (scrollWidth > clientWidth), updating scrollLeft similarly to the existing scrollTop logic.
  • Scroll the window on both axes via scrollBy, using visualViewport when present and applying horizontal scroll-padding on the document element in addition to the existing vertical padding.

Collapsed selection updates in LexicalSelection pass the chain start derived from the active Range / Text / HTMLElement target.

Related to #8459 (addresses the horizontal auto-scroll / caret visibility part; VS Code–style word wrap remains a possible follow-up).

Test plan

Automated

  • pnpm exec vitest run packages/lexical/src/__tests__/unit/LexicalUtils.test.ts -t "scrollIntoViewIfNeeded" --project=unit
  • New tests: horizontal overflow scrolls right; caret left of the visible region scrolls left.

Manual

  1. Run playground, insert a code block, paste or type a very long single line so a horizontal scrollbar appears on the code block.
  2. Put the caret in the middle of that line; use Ctrl+ArrowRight to move to end of line (or Ctrl+ArrowLeft to start) and confirm the code block scrolls horizontally so the caret stays visible.
  3. Repeat with normal arrow keys along the line to confirm scrolling still feels correct.

Before

Caret could move off-screen horizontally inside the code block while the block’s scrollLeft did not follow.

After

Caret stays within the visible horizontal range of the code block (and other scrollable ancestors) when the collapsed selection is updated.

@meta-cla

meta-cla Bot commented May 11, 2026

Copy link
Copy Markdown

Hi @Frank20021!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@vercel

vercel Bot commented May 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview, Comment May 15, 2026 11:12pm
lexical-playground Ready Ready Preview, Comment May 15, 2026 11:12pm

Request Review

@meta-cla

meta-cla Bot commented May 11, 2026

Copy link
Copy Markdown

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label May 11, 2026
@meta-cla

meta-cla Bot commented May 11, 2026

Copy link
Copy Markdown

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

Comment on lines +3226 to +3231
if (selectionTarget instanceof Text) {
return selectionTarget.parentElement;
}
if (selectionTarget instanceof HTMLElement) {
return selectionTarget;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should never use instanceof with DOM because it does not work across iframes, this is why we have guard functions like isDOMNode, isDOMTextNode, etc.

@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label May 11, 2026
@Frank20021

Frank20021 commented May 12, 2026 via email

Copy link
Copy Markdown
Author

Comment thread packages/lexical/src/LexicalUtils.ts Outdated
Comment on lines +1441 to +1442
const element: HTMLElement | null =
scrollChainStartElement != null ? scrollChainStartElement : rootElement;

@etrepum etrepum May 15, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const element: HTMLElement | null =
scrollChainStartElement != null ? scrollChainStartElement : rootElement;
let element: HTMLElement | null = scrollChainStartElement || rootElement;

I don't see a reason to have both an element and a walk variable, let's get rid of this walk variable and use element like we did before. element isn't re-used anywhere so it doesn't make sense to keep the initial value around as a constant.

Comment thread packages/lexical/src/LexicalUtils.ts Outdated
Comment on lines +1444 to +1447
let walk: HTMLElement | null = element;

while (walk !== null) {
const isBodyElement = walk === doc.body;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let walk: HTMLElement | null = element;
while (walk !== null) {
const isBodyElement = walk === doc.body;
while (element !== null) {
const isBodyElement = element === doc.body;

Comment thread packages/lexical/src/LexicalUtils.ts Outdated
}
} else {
const targetRect = element.getBoundingClientRect();
const targetRect = walk.getBoundingClientRect();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const targetRect = walk.getBoundingClientRect();
const targetRect = element.getBoundingClientRect();

Comment thread packages/lexical/src/LexicalUtils.ts Outdated
break;
}
element = getParentElement(element);
walk = getParentElement(walk);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
walk = getParentElement(walk);
element = getParentElement(element);

@etrepum etrepum left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this does follow the input text to and scroll the right it doesn't seem to scroll correctly to the left. For example, create a CodeBlock and type enough text to scroll and then press enter. It will scroll to the left but not all the way. Possibly due to the line numbers.

@etrepum etrepum marked this pull request as draft May 19, 2026 18:01

@potatowagon potatowagon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — Scroll Collapsed Selection Horizontally in Overflow Containers

Assessment: Looks good to land

What I verified:

  1. Bug fix logic: The existing scrollIntoViewIfNeeded only handled vertical scrolling. This PR extends it to also handle horizontal scrolling, which is important for code blocks or any container with overflow-x: auto/scroll. The fix adds currentLeft/currentRight/targetLeft/targetRight tracking parallel to the existing vertical logic.

  2. Scroll chain improvement: A new scrollChainStartElement parameter lets the walk begin at the caret's immediate parent rather than the root, so intermediate scrollable containers (e.g., a code block) get scrolled first before bubbling up to the document.

  3. Edge cases: The maxScrollX > 0 guard prevents unnecessary scroll attempts on non-scrollable elements. CSS scroll-padding is correctly accounted for on both axes. The visualViewport path also gets horizontal bounds.

  4. Test coverage: Unit test added that creates an overflow container, positions a selection rect beyond the right edge, and verifies scrollIntoViewIfNeeded scrolls it into view.

  5. CI status: Full CI suite green (40 checks pass).

  6. Risk: Low — extends existing scroll logic in a consistent, parallel manner. The added scrollChainStartElement parameter is optional, so existing callers are unaffected.

— via Navi on behalf of potatowagon

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. extended-tests Run extended e2e tests on a PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants