Skip to content

[lexical-table] Feature: add config for opting in to nested tables#7983

Merged
etrepum merged 4 commits intofacebook:mainfrom
james-atticus:allow-nested-tables-option
Nov 11, 2025
Merged

[lexical-table] Feature: add config for opting in to nested tables#7983
etrepum merged 4 commits intofacebook:mainfrom
james-atticus:allow-nested-tables-option

Conversation

@james-atticus
Copy link
Copy Markdown
Contributor

Description

#7192 made changes to try and prevent nested tables. They weren't removed from the document tree however, and could still be inserted when not using commands. @zurfyx also advised that nested tables are wanted at Meta, even if they're not fully supported (we also want to be able to use them).

This PR adds a config option to the plugin/extension allowing developers to opt in to allowing nested tables.

Note: there are no fixes/improvements for nested tables included (eg: #7154 is still an issue). The config option is marked @experimental to convey that nested tables are not properly supported.

@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 Nov 11, 2025
@vercel
Copy link
Copy Markdown

vercel bot commented Nov 11, 2025

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

Project Deployment Preview Comments Updated (UTC)
lexical Ready Ready Preview Comment Nov 11, 2025 7:00am
lexical-playground Ready Ready Preview Comment Nov 11, 2025 7:00am

if (selection === null) {
throw new Error('Expected valid selection');
}
editor.dispatchCommand(SELECTION_INSERT_CLIPBOARD_NODES_COMMAND, {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This wasn't actually testing anything before: verified by removing the command handler and the test still passed. We need to instead use $insertGeneratedNodes which only progresses if all the handlers for SELECTION_INSERT_CLIPBOARD_NODES_COMMAND return false.

Comment on lines +128 to +129
const extension = getExtensionDependencyFromEditor(editor, TableExtension);
extension.output.hasNestedTables.value = true;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@etrepum I couldn't find any examples in the codebase where extension config was being updated after-the-fact. Let me know if this isn't the right way to go about it.

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.

When output properties are signals they're intended to be mutable after the fact, doing it this way is fine. It's a little awkward because this would normally just be configured up front, but it is a sort of partial test that the signal is wired up correctly.

Comment on lines +178 to +192
editor.update(
() => {
const root = $getRoot().clear();
const table = $createTableNode();
const row = $createTableRowNode();
const cell = $createTableCellNode();
const paragraph = $createParagraphNode();
cell.append(paragraph);
row.append(cell);
table.append(row);
root.append(table);
paragraph.select();
},
{discrete: true},
);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Switched from async to discrete: true, and added a paragraph inside the cell (otherwise $insertGeneratedNodes throws). Test is otherwise the same as it was before.

root.append(table);
cell.select();
});
it('Prevents nested tables by default', async () => {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Github is struggling a bit with this diff. Recommend reviewing commits individually and with whitespace hidden.

});
}

export function registerPreventNestedTablesHandlers(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This might be better named as just registerPreventNestedTables. Open to suggestions.

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.

For the INSERT_TABLE_COMMAND handler this could also be implemented by passing the signal through to the insertTableCommandListener so that it can peek() the current value, which would prevent the need for registering a separate command listener at a higher priority to check this condition

isCollab,
}) => {
test.skip(isPlainText);
await initialize({hasNestedTables: true, isCollab, page});
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same as the tests removed in #7192 but with hasNestedTables: true added.

etrepum
etrepum previously approved these changes Nov 11, 2025
Copy link
Copy Markdown
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.

This looks fine, I don't see a good reason to support this experiment in both the extension and the plugin but I don't think that's a blocker just more busywork

Comment on lines +101 to +107
// Prevent nested tables being inserted when the feature isn't enabled
useEffect(() => {
if (!hasNestedTables) {
return registerPreventNestedTablesHandlers(editor);
}
}, [editor, hasNestedTables]);

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.

This might be a good opportunity to just stop adding features to the react plugin and only support it in the extension

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I took a stab at this but I think it'd require a large refactor of the playground App/Editor components. We'd have to replace TablePlugin with TableExtension, and I wasn't sure how to properly update the extension's config based on the values from useSettings.

I've switched to using signals for registerTablePlugin though, which avoids having to add a new registerPreventNestedTablesHandlers function.

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.

Basically the same way you did in the test should work, from a useEffect to control the timing. Not a big deal either way

editor.getEditorState().read(() => {
const root = $getRoot();
const table = root.getFirstChild();
if (!$isTableNode(table)) {
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.

not that we are using vitest you can use its assert to do this sort of thing in a more concise way

export type {TableDOMCell} from './LexicalTableObserver';
export {$getTableAndElementByKey, TableObserver} from './LexicalTableObserver';
export {
registerPreventNestedTablesHandlers,
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.

If we only support this feature in the extension we could not export this function at all and then its name wouldn't matter

});
}

export function registerPreventNestedTablesHandlers(
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.

For the INSERT_TABLE_COMMAND handler this could also be implemented by passing the signal through to the insertTableCommandListener so that it can peek() the current value, which would prevent the need for registering a separate command listener at a higher priority to check this condition

@etrepum etrepum added the extended-tests Run extended e2e tests on a PR label Nov 11, 2025
Copy link
Copy Markdown
Member

@zurfyx zurfyx left a comment

Choose a reason for hiding this comment

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

Thank you James! I'll defer the stamp to @etrepum since he already started a proper review

@etrepum etrepum added this pull request to the merge queue Nov 11, 2025
Merged via the queue into facebook:main with commit 6296fe9 Nov 11, 2025
48 checks passed
@etrepum etrepum mentioned this pull request Dec 10, 2025
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