diff --git a/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs b/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs
index aee91bc356b..4811ea087bf 100644
--- a/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs
+++ b/packages/lexical-playground/__tests__/e2e/Indentation.spec.mjs
@@ -14,9 +14,23 @@ import {
html,
initialize,
insertTable,
+ selectFromAlignDropdown,
test,
} from '../utils/index.mjs';
+async function toggleBulletList(page) {
+ await click(page, '.block-controls');
+ await click(page, '.dropdown .icon.bullet-list');
+}
+
+async function clickIndentButton(page, times = 1) {
+ for (let i = 0; i < times; i++) {
+ await selectFromAlignDropdown(page, '.indent');
+ }
+}
+
+const MAX_INDENT = 7;
+
test.describe('Identation', () => {
test.beforeEach(({isCollab, page}) =>
initialize({isCollab, page, tableHorizontalScroll: false}),
@@ -474,4 +488,117 @@ test.describe('Identation', () => {
`,
);
});
+
+ test(`Can only indent paragraph until the max depth`, async ({
+ page,
+ isPlainText,
+ }) => {
+ test.skip(isPlainText);
+ await focusEditor(page);
+ await clickIndentButton(page, MAX_INDENT);
+
+ const expectedHTML =
+ '
';
+
+ await assertHTML(page, expectedHTML);
+ await clickIndentButton(page, MAX_INDENT);
+
+ // should stay the same
+ await assertHTML(page, expectedHTML);
+ });
+
+ test(`Can only indent until the max depth when list is empty`, async ({
+ page,
+ isPlainText,
+ }) => {
+ test.skip(isPlainText);
+ await focusEditor(page);
+ await toggleBulletList(page);
+
+ await clickIndentButton(page, MAX_INDENT);
+
+ await assertHTML(
+ page,
+ '',
+ );
+
+ await clickIndentButton(page);
+
+ // should stay the same
+ await assertHTML(
+ page,
+ '',
+ );
+ });
+
+ test(`Can only indent until the max depth when list has content`, async ({
+ page,
+ isPlainText,
+ }) => {
+ test.skip(isPlainText);
+ await focusEditor(page);
+ await toggleBulletList(page);
+ await page.keyboard.type('World');
+
+ await clickIndentButton(page, MAX_INDENT);
+
+ await assertHTML(
+ page,
+ '',
+ );
+
+ await clickIndentButton(page);
+
+ // should stay the same
+ await assertHTML(
+ page,
+ '',
+ );
+ });
+
+ test(`Can only indent until the max depth a list with nested lists`, async ({
+ page,
+ isPlainText,
+ }) => {
+ test.skip(isPlainText);
+
+ await focusEditor(page);
+ await toggleBulletList(page);
+ await page.keyboard.type('Hello');
+ await page.keyboard.press('Enter');
+ await page.keyboard.type('from');
+ await clickIndentButton(page);
+ await page.keyboard.press('Enter');
+ await page.keyboard.type('the');
+ await clickIndentButton(page);
+ await page.keyboard.press('Enter');
+ await page.keyboard.type('other');
+ await clickIndentButton(page);
+ await page.keyboard.press('Enter');
+ await page.keyboard.type('side');
+ await clickIndentButton(page);
+ await page.keyboard.press('Enter');
+
+ await assertHTML(
+ page,
+ '',
+ );
+
+ await selectAll(page);
+
+ await clickIndentButton(page, 3);
+
+ await assertHTML(
+ page,
+ '',
+ );
+
+ await clickIndentButton(page);
+
+ // should stay the same
+ await assertHTML(
+ page,
+ '',
+ );
+ });
});
diff --git a/packages/lexical-playground/__tests__/e2e/ListMaxIndentLevel.spec.mjs b/packages/lexical-playground/__tests__/e2e/ListMaxIndentLevel.spec.mjs
deleted file mode 100644
index fe37a84b209..00000000000
--- a/packages/lexical-playground/__tests__/e2e/ListMaxIndentLevel.spec.mjs
+++ /dev/null
@@ -1,128 +0,0 @@
-/**
- * Copyright (c) Meta Platforms, Inc. and affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- *
- */
-
-import {selectAll} from '../keyboardShortcuts/index.mjs';
-import {
- assertHTML,
- click,
- focusEditor,
- initialize,
- selectFromAlignDropdown,
- test,
-} from '../utils/index.mjs';
-
-async function toggleBulletList(page) {
- await click(page, '.block-controls');
- await click(page, '.dropdown .icon.bullet-list');
-}
-
-async function clickIndentButton(page, times = 1) {
- for (let i = 0; i < times; i++) {
- await selectFromAlignDropdown(page, '.indent');
- }
-}
-
-const MAX_INDENT_LEVEL = 6;
-
-test.describe('Nested List', () => {
- test.beforeEach(({isCollab, page}) => initialize({isCollab, page}));
- test(`Can only indent until the max depth when list is empty`, async ({
- page,
- isPlainText,
- }) => {
- test.skip(isPlainText);
- await focusEditor(page);
- await toggleBulletList(page);
-
- await clickIndentButton(page, MAX_INDENT_LEVEL);
-
- await assertHTML(
- page,
- '',
- );
-
- await clickIndentButton(page, MAX_INDENT_LEVEL);
-
- // should stay the same
- await assertHTML(
- page,
- '',
- );
- });
-
- test(`Can only indent until the max depth when list has content`, async ({
- page,
- isPlainText,
- }) => {
- test.skip(isPlainText);
- await focusEditor(page);
- await toggleBulletList(page);
- await page.keyboard.type('World');
-
- await clickIndentButton(page, MAX_INDENT_LEVEL);
-
- await assertHTML(
- page,
- '',
- );
-
- await clickIndentButton(page, MAX_INDENT_LEVEL);
-
- // should stay the same
- await assertHTML(
- page,
- '',
- );
- });
-
- test(`Can only indent until the max depth a list with nested lists`, async ({
- page,
- isPlainText,
- }) => {
- test.skip(isPlainText);
-
- await focusEditor(page);
- await toggleBulletList(page);
- await page.keyboard.type('Hello');
- await page.keyboard.press('Enter');
- await page.keyboard.type('from');
- await clickIndentButton(page);
- await page.keyboard.press('Enter');
- await page.keyboard.type('the');
- await clickIndentButton(page);
- await page.keyboard.press('Enter');
- await page.keyboard.type('other');
- await clickIndentButton(page);
- await page.keyboard.press('Enter');
- await page.keyboard.type('side');
- await clickIndentButton(page);
- await page.keyboard.press('Enter');
-
- await assertHTML(
- page,
- '',
- );
-
- await selectAll(page);
-
- await clickIndentButton(page, 3);
-
- await assertHTML(
- page,
- '',
- );
-
- await clickIndentButton(page, MAX_INDENT_LEVEL);
-
- // should stay the same
- await assertHTML(
- page,
- '',
- );
- });
-});
diff --git a/packages/lexical-playground/src/Editor.tsx b/packages/lexical-playground/src/Editor.tsx
index 1b752ced18c..ec1d2c0d1fa 100644
--- a/packages/lexical-playground/src/Editor.tsx
+++ b/packages/lexical-playground/src/Editor.tsx
@@ -55,7 +55,6 @@ import InlineImagePlugin from './plugins/InlineImagePlugin';
import KeywordsPlugin from './plugins/KeywordsPlugin';
import {LayoutPlugin} from './plugins/LayoutPlugin/LayoutPlugin';
import LinkPlugin from './plugins/LinkPlugin';
-import ListMaxIndentLevelPlugin from './plugins/ListMaxIndentLevelPlugin';
import MarkdownShortcutPlugin from './plugins/MarkdownShortcutPlugin';
import {MaxLengthPlugin} from './plugins/MaxLengthPlugin';
import MentionsPlugin from './plugins/MentionsPlugin';
@@ -200,7 +199,6 @@ export default function Editor(): JSX.Element {
-
-
+
diff --git a/packages/lexical-playground/src/plugins/ListMaxIndentLevelPlugin/index.ts b/packages/lexical-playground/src/plugins/ListMaxIndentLevelPlugin/index.ts
deleted file mode 100644
index 198543784c1..00000000000
--- a/packages/lexical-playground/src/plugins/ListMaxIndentLevelPlugin/index.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * Copyright (c) Meta Platforms, Inc. and affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- *
- */
-
-import type {ElementNode, RangeSelection} from 'lexical';
-
-import {$getListDepth, $isListItemNode, $isListNode} from '@lexical/list';
-import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
-import {
- $getSelection,
- $isElementNode,
- $isRangeSelection,
- COMMAND_PRIORITY_CRITICAL,
- INDENT_CONTENT_COMMAND,
-} from 'lexical';
-import {useEffect} from 'react';
-
-function getElementNodesInSelection(
- selection: RangeSelection,
-): Set {
- const nodesInSelection = selection.getNodes();
-
- if (nodesInSelection.length === 0) {
- return new Set([
- selection.anchor.getNode().getParentOrThrow(),
- selection.focus.getNode().getParentOrThrow(),
- ]);
- }
-
- return new Set(
- nodesInSelection.map((n) => ($isElementNode(n) ? n : n.getParentOrThrow())),
- );
-}
-
-function $shouldPreventIndent(maxDepth: number): boolean {
- const selection = $getSelection();
-
- if (!$isRangeSelection(selection)) {
- return false;
- }
-
- const elementNodesInSelection: Set =
- getElementNodesInSelection(selection);
-
- let totalDepth = 0;
-
- for (const elementNode of elementNodesInSelection) {
- if ($isListNode(elementNode)) {
- totalDepth = Math.max($getListDepth(elementNode) + 1, totalDepth);
- } else if ($isListItemNode(elementNode)) {
- const parent = elementNode.getParent();
-
- if (!$isListNode(parent)) {
- throw new Error(
- 'ListMaxIndentLevelPlugin: A ListItemNode must have a ListNode for a parent.',
- );
- }
-
- totalDepth = Math.max($getListDepth(parent) + 1, totalDepth);
- }
- }
-
- return totalDepth > maxDepth;
-}
-
-export default function ListMaxIndentLevelPlugin({
- maxDepth = 7,
-}: {
- maxDepth?: number;
-}): null {
- const [editor] = useLexicalComposerContext();
-
- useEffect(() => {
- return editor.registerCommand(
- INDENT_CONTENT_COMMAND,
- () => $shouldPreventIndent(maxDepth),
- COMMAND_PRIORITY_CRITICAL,
- );
- }, [editor, maxDepth]);
- return null;
-}
diff --git a/packages/lexical-react/flow/LexicalTabIndentationPlugin.js.flow b/packages/lexical-react/flow/LexicalTabIndentationPlugin.js.flow
index 44784b01131..64d9ff87caa 100644
--- a/packages/lexical-react/flow/LexicalTabIndentationPlugin.js.flow
+++ b/packages/lexical-react/flow/LexicalTabIndentationPlugin.js.flow
@@ -11,6 +11,11 @@ import type {LexicalEditor} from 'lexical';
declare export function registerTabIndentation(
editor: LexicalEditor,
+ maxIndent?: number,
): () => void;
-declare export function TabIndentationPlugin(): null;
+type Props = $ReadOnly<{
+ maxIndent?: number,
+}>;
+
+declare export function TabIndentationPlugin(props: Props): null;
diff --git a/packages/lexical-react/src/LexicalTabIndentationPlugin.tsx b/packages/lexical-react/src/LexicalTabIndentationPlugin.tsx
index db8c0e8a6e2..84025cc7472 100644
--- a/packages/lexical-react/src/LexicalTabIndentationPlugin.tsx
+++ b/packages/lexical-react/src/LexicalTabIndentationPlugin.tsx
@@ -9,13 +9,18 @@
import type {LexicalCommand, LexicalEditor, RangeSelection} from 'lexical';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
-import {$filter, $getNearestBlockElementAncestorOrThrow} from '@lexical/utils';
+import {
+ $filter,
+ $getNearestBlockElementAncestorOrThrow,
+ mergeRegister,
+} from '@lexical/utils';
import {
$createRangeSelection,
$getSelection,
$isBlockElementNode,
$isRangeSelection,
$normalizeSelection__EXPERIMENTAL,
+ COMMAND_PRIORITY_CRITICAL,
COMMAND_PRIORITY_EDITOR,
INDENT_CONTENT_COMMAND,
INSERT_TAB_COMMAND,
@@ -57,24 +62,51 @@ function $indentOverTab(selection: RangeSelection): boolean {
return false;
}
-export function registerTabIndentation(editor: LexicalEditor) {
- return editor.registerCommand(
- KEY_TAB_COMMAND,
- (event) => {
- const selection = $getSelection();
- if (!$isRangeSelection(selection)) {
- return false;
- }
+export function registerTabIndentation(
+ editor: LexicalEditor,
+ maxIndent?: number,
+) {
+ return mergeRegister(
+ editor.registerCommand(
+ KEY_TAB_COMMAND,
+ (event) => {
+ const selection = $getSelection();
+ if (!$isRangeSelection(selection)) {
+ return false;
+ }
+ event.preventDefault();
+ const command: LexicalCommand = $indentOverTab(selection)
+ ? event.shiftKey
+ ? OUTDENT_CONTENT_COMMAND
+ : INDENT_CONTENT_COMMAND
+ : INSERT_TAB_COMMAND;
+ return editor.dispatchCommand(command, undefined);
+ },
+ COMMAND_PRIORITY_EDITOR,
+ ),
+
+ editor.registerCommand(
+ INDENT_CONTENT_COMMAND,
+ () => {
+ if (maxIndent == null) {
+ return false;
+ }
+
+ const selection = $getSelection();
+ if (!$isRangeSelection(selection)) {
+ return false;
+ }
+
+ const indents = selection
+ .getNodes()
+ .map((node) =>
+ $getNearestBlockElementAncestorOrThrow(node).getIndent(),
+ );
- event.preventDefault();
- const command: LexicalCommand = $indentOverTab(selection)
- ? event.shiftKey
- ? OUTDENT_CONTENT_COMMAND
- : INDENT_CONTENT_COMMAND
- : INSERT_TAB_COMMAND;
- return editor.dispatchCommand(command, undefined);
- },
- COMMAND_PRIORITY_EDITOR,
+ return Math.max(...indents) + 1 >= maxIndent;
+ },
+ COMMAND_PRIORITY_CRITICAL,
+ ),
);
}
@@ -83,11 +115,11 @@ export function registerTabIndentation(editor: LexicalEditor) {
* recommend using this plugin as it could negatively affect acessibility for keyboard
* users, causing focus to become trapped within the editor.
*/
-export function TabIndentationPlugin(): null {
+export function TabIndentationPlugin({maxIndent}: {maxIndent?: number}): null {
const [editor] = useLexicalComposerContext();
useEffect(() => {
- return registerTabIndentation(editor);
- }, [editor]);
+ return registerTabIndentation(editor, maxIndent);
+ }, [editor, maxIndent]);
return null;
}