Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[lexical][lexical-react][lexical-table] Fix: Add horizontal scroll for tables #6713

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

GermanJablo
Copy link
Contributor

Description

Closes #6480

Having tables overflow is a mistake, as their content can conflict with other elements of the interface:

image

Another incorrect behavior is the one that occurs in the Lexical Playground, where if the table is too large, the entire editor becomes larger. The horizontal scroll bar is not placed over the table, but over the entire content.

image

A super quick way to fix overflow is to add a max-width of 100% to the table.

The problem is, as this user noted, that this creates conflicts with the column width property.

This is what OneNote does and I've always hated it. Sometimes columns resize in a magical or hard-to-understand way. It's better to give the user the flexibility to adjust column widths to their liking and to have overflow if needed.

The correct solution to this is for tables to have an overflow scroll independent of the rest of the editor, similar to what Notion does:

image

The problem is that the <table> tag doesn't support overflow: scroll. A div is needed to wrap it.

This PR introduces a new Scrollable node that automatically wraps tables.

BREAKING CHANGE: TablePlugin now requires the editor to also register the ScrollableNode node. All tables are automatically wrapped in a ScrollableNode.

Test plan

Before

image

After

image

Copy link

vercel bot commented Oct 7, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
lexical ✅ Ready (Inspect) Visit Preview 💬 Add feedback Oct 7, 2024 9:26pm
lexical-playground ✅ Ready (Inspect) Visit Preview 💬 Add feedback Oct 7, 2024 9:26pm

@facebook-github-bot facebook-github-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 Oct 7, 2024
Copy link

github-actions bot commented Oct 7, 2024

size-limit report 📦

Path Size
lexical - cjs 29.91 KB (0%)
lexical - esm 29.72 KB (0%)
@lexical/rich-text - cjs 38.55 KB (0%)
@lexical/rich-text - esm 31.6 KB (0%)
@lexical/plain-text - cjs 37.09 KB (0%)
@lexical/plain-text - esm 28.92 KB (0%)
@lexical/react - cjs 40.29 KB (0%)
@lexical/react - esm 32.99 KB (0%)

Comment on lines +26 to +31
/**
* This node allows the nodes it wraps to have a width greater than that of the editor,
* encapsulating only its content in a horizontal scroll. You'll want to use it to wrap
* nodes that have a "width" property and are NOT decoratorNodes. See TableNode as an example.
*/
export class ScrollableNode extends ElementNode {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

At first I wrapped TableNode and ImageNode, and so I put it in core. Then I realized that since it's a decoratorNode in ImageNode you can replicate the same behavior much easier without using this node. So I moved the node to lexical-table.
Wrapping another node with ScrollableNode should be easy if you want to do it in the future. At the moment TableNode is the only one that has a width property and it's not DecoratorNode.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it would be tricky to use this class as-is with other element types because of the transform that requires the child to be a table


createDOM(_config: EditorConfig, _editor: LexicalEditor): HTMLElement {
const dom = document.createElement('div');
dom.classList.add('lexical-scrollable');
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Required for exportDOM and importDOM. I've included new tests to verify that it works correctly.

Copy link
Collaborator

@etrepum etrepum left a comment

Choose a reason for hiding this comment

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

I don't have time for QA right now or to audit the tests but I took a quick look at the code. Seems like a useful improvement! The downside is that it is a bit backwards incompatible due to the additional node and requirements for table use. Would maybe be a bit nicer if it was done in a way that could be backwards compatible. @ivailop7 should have a look

Comment on lines +26 to +31
/**
* This node allows the nodes it wraps to have a width greater than that of the editor,
* encapsulating only its content in a horizontal scroll. You'll want to use it to wrap
* nodes that have a "width" property and are NOT decoratorNodes. See TableNode as an example.
*/
export class ScrollableNode extends ElementNode {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it would be tricky to use this class as-is with other element types because of the transform that requires the child to be a table

Comment on lines +88 to +109
const selection = $getSelection();
if ((node as ScrollableNode).isEmpty()) {
const root = $getRoot();
if (root.__first === node.getKey() && root.getChildrenSize() === 1) {
root.append($createParagraphNode());
node.remove();
} else {
node.remove();
}
return;
}
const firstChild: ElementNode = (node as ScrollableNode).getFirstChild()!;
if (firstChild && !$isTableNode(firstChild)) {
firstChild.remove();
}
if (
$isRangeSelection(selection) &&
selection.anchor.key === node.getKey()
) {
firstChild.getFirstDescendant()!.selectStart();
}
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

It's not entirely clear what this is defending against but it doesn't look like it's doing it correctly since the last branch can execute when firstChild is not a table (might not have a first descendant) and is not connected to the root. I would put the child check before the empty check, remove all children except the first that passes the table check, and then if there's no table node found then remove the node (and fix up the root if necessary).

if (direction === 'backward') {
siblingNode.selectEnd();
} else {
const firstDescendant = siblingNode.getFirstDescendant()!;
Copy link
Collaborator

Choose a reason for hiding this comment

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

it would be better to use a runtime invariant rather than a non-null assertion here, there isn't a runtime check that this is non-empty

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.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Feature: Scroll for table
3 participants