lexical icon indicating copy to clipboard operation
lexical copied to clipboard

Bug: Cannot escape lists with Enter key

Open pistachiomatt opened this issue 2 years ago • 10 comments

Lexical version: 0.12

Steps To Reproduce

  1. Create a list in RichTextPlugin
  2. Press Enter twice. The list continues forever; there is no way to escape it Expected: Pressing enter in an empty list reduces the list by 1 level (or exits it when it's the first level)

Demo: https://codesandbox.io/s/lexical-plain-text-example-forked-9lq9y8

image

(This bug was previously reported and closed, but it appears to have reoccured: https://github.com/facebook/lexical/issues/4266)

pistachiomatt avatar Aug 16 '23 05:08 pistachiomatt

@pistachiomatt try to import ListPlugin. Tested and it worked for me here! I hope it helps.

import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
import editorConfig from "./editorConfig";

// New component
import { ListPlugin } from '@lexical/react/LexicalListPlugin';

export default function Editor() {
  return (
    <LexicalComposer initialConfig={editorConfig}>
      <div className="editor-container">
        <RichTextPlugin
          contentEditable={<ContentEditable className="editor-input" />}
          placeholder={<Placeholder />}
          ErrorBoundary={LexicalErrorBoundary}
        />

        {/* New Component */}
        <ListPlugin />

      </div>
    </LexicalComposer>
  );
}

function Placeholder() {
  return <div className="editor-placeholder">Enter some plain text...</div>;
}

djheyson avatar Aug 16 '23 13:08 djheyson

Thanks! It works. A bit strange that a plugin is required to fix a bug. Perhaps this ticket should be to merge the plugin into core.

pistachiomatt avatar Aug 16 '23 15:08 pistachiomatt

If lists are in your editor, you already have it imported, so I highly doubt that's the reason for it, especially since I'm also seeing this.

I had the same issue, but only rarely, and there is no difference between the extracted jsons between a list that works correctly with the enter key, and one that creates infinite lists and can't be escaped. Honestly unsure what could be causing it, but it does seem very strange overall, and I can't even provide a repro with a codebase, because I can't pin down the exact reason it is happening. I literally copy-pasted the content of one editor where the bug appeared into another and it no longer showed up.

DanielOrtel avatar Aug 28 '23 00:08 DanielOrtel

I have this issue as well, im using Vanilla js, i cannot exit the list with double enter keypress

CRIMSON-CORP avatar Jan 19 '24 12:01 CRIMSON-CORP

I also encountered this bug.

It seems that the issue was caused by inadvertently adding libraries that are already defined by default within 'lexical', such as 'yarn add @lexical/code @lexical/markdown'.

Once I removed all lexical libraries other than 'lexical' and '@lexical/react', everything started working smoothly.

Atsuyoshi-Funahashi avatar Jan 31 '24 08:01 Atsuyoshi-Funahashi

I am experiencing this too.

I followed what @Atsuyoshi-Funahashi mentioned, removing all other lexical libraries, which may have had an impact as I'm sure I've seen it working on my implementation. But, I cannot seem to get it to work at all now. I'm sure I did have it working before.

I'll continue to dig and I will feedback if I can work out what the cause is.

Rooster212 avatar Feb 22 '24 15:02 Rooster212

Ok, interestingly when I render my component separately in Storybook I don't get this issue occurring. However I do when it's embedded in my application. In storybook I am just using my WYSIWYGEditor component, whereas In my application I am wrapping the editor in a react-hook-form Controller to set the value on change with some debounce logic, like so:

import { Control, Controller } from "react-hook-form";
import { Item } from "../../../external";
import WYSIWYGEditor from "../../wysiwyg";

export interface WYSIWYGEditorProps {
  item: Item;
  isSaving: boolean;
  viewOnly: boolean;
  control: Control<Item>;
}

const ItemDescription = ({
  isSaving,
  viewOnly,
  control,
  item,
}: WYSIWYGEditorProps): JSX.Element => {
  return (
    <>
      <Controller
        name="description"
        control={control}
        render={({
          field: { onChange, value, name },
        }) => {
          return (
            <WYSIWYGEditor
              isSaving={isSaving}
              setValue={onChange}
              value={value}
              viewOnly={viewOnly}
              namespace={`${item.itemId}${name}`}
            />
          );
        }}
      ></Controller>
    </>
  );
};
export default ItemDescription;

Rooster212 avatar Feb 23 '24 12:02 Rooster212

I've nailed it down to, it only happens when I pre-populate the editor with the initial value, with a list in it.

My code to load the initial value is a plugin that I've added:

/* eslint-disable react-hooks/exhaustive-deps */
import { useEffect, useState } from "react";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { $generateNodesFromDOM } from "@lexical/html";
import { $createParagraphNode, $getRoot } from "lexical";

type LoadInitialValuePluginProps = {
  namespace: string;
  initialValue?: string;
};

export function LoadInitialValuePlugin({
  initialValue,
}: LoadInitialValuePluginProps) {
  const [editor] = useLexicalComposerContext();
  const [hasWritten, setHasWritten] = useState(false);

  // Run useEffect with no dependencies - this will only run at load
  useEffect(() => {
    editor.update(
      () => {
        if (initialValue && !hasWritten) {
          // Make this only run once
          setHasWritten(true);

          // In the browser you can use the native DOMParser API to parse the HTML string.
          // Once you have the DOM instance it's easy to generate LexicalNodes.
          const parser = new DOMParser();
          const v = parser.parseFromString(initialValue, "text/html");

          // Retrieve the root
          const root = $getRoot();

          // Clear the root
          root.clear();

          // Generate nodes from the DOM, which generates HTML nodes
          const nodes = $generateNodesFromDOM(editor, v);
          const para = $createParagraphNode();
          para.append(...nodes);
          root.append(para);
        }
      },
      {
        onUpdate: () => {
          // Debugging purposes
          console.info(editor.toJSON());
        },
      }
    );
  }, []);

  return null;
}

This is how it looks in my editor: image

This is the generated state:

Details
{
    "children": [
        {
            "children": [
                {
                    "children": [
                        {
                            "detail": 0,
                            "format": 1,
                            "mode": "normal",
                            "style": "",
                            "text": "Hello, this is a new description",
                            "type": "text",
                            "version": 1
                        }
                    ],
                    "direction": "ltr",
                    "format": "",
                    "indent": 0,
                    "type": "paragraph",
                    "version": 1
                },
                {
                    "children": [
                        {
                            "detail": 0,
                            "format": 2,
                            "mode": "normal",
                            "style": "",
                            "text": "This is some italic text",
                            "type": "text",
                            "version": 1
                        }
                    ],
                    "direction": "ltr",
                    "format": "",
                    "indent": 0,
                    "type": "paragraph",
                    "version": 1
                },
                {
                    "children": [
                        {
                            "children": [
                                {
                                    "detail": 0,
                                    "format": 0,
                                    "mode": "normal",
                                    "style": "",
                                    "text": "This is a list item",
                                    "type": "text",
                                    "version": 1
                                }
                            ],
                            "direction": "ltr",
                            "format": "",
                            "indent": 0,
                            "type": "listitem",
                            "version": 1,
                            "value": 1
                        },
                        {
                            "children": [
                                {
                                    "detail": 0,
                                    "format": 0,
                                    "mode": "normal",
                                    "style": "",
                                    "text": "This is another list item",
                                    "type": "text",
                                    "version": 1
                                }
                            ],
                            "direction": "ltr",
                            "format": "",
                            "indent": 0,
                            "type": "listitem",
                            "version": 1,
                            "value": 2
                        }
                    ],
                    "direction": "ltr",
                    "format": "",
                    "indent": 0,
                    "type": "list",
                    "version": 1,
                    "listType": "number",
                    "start": 1,
                    "tag": "ol"
                }
            ],
            "direction": "ltr",
            "format": "",
            "indent": 0,
            "type": "paragraph",
            "version": 1
        }
    ],
    "direction": "ltr",
    "format": "",
    "indent": 0,
    "type": "root",
    "version": 1
}

Rooster212 avatar Feb 23 '24 14:02 Rooster212

I fixed it in my case now!

Content view: image

Here is a diff between me entering content, and the initial load image

The obvious thing is that it's wrapped in an extra paragraph.

I've updated my code that populates the initial value on load to no longer wrap the content in a paragraph.

My initial code is in my previous comment above - now my nodes are populated in the root with this:

const nodes = $generateNodesFromDOM(editor, v);
root.append(...nodes);

Now it all works as expected for me.

I feel like there might be a bug here, I'm not entirely sure why wrapping it in a paragraph would do this - there is potentially a bug in the function $handleListInsertParagraph in the lexical-list package. I will dig further into it when I have some more time.

I hope that helps someone else at least!

Rooster212 avatar Feb 23 '24 14:02 Rooster212

Edit

I Still having this problem on my app today. And after I dig into the code compare to playground, I found out that I didn't import ListPlugin from "@lexical/react/LexicalListPlugin". After added this plugin I can escape list through Enter key.

jimmy-chiang avatar Apr 25 '24 07:04 jimmy-chiang