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

Bug: Cannot escape lists with Enter key #4893

Open
pistachiomatt opened this issue Aug 16, 2023 · 10 comments
Open

Bug: Cannot escape lists with Enter key #4893

pistachiomatt opened this issue Aug 16, 2023 · 10 comments

Comments

@pistachiomatt
Copy link

pistachiomatt commented Aug 16, 2023

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: #4266)

@djheyson
Copy link

@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>;
}

@pistachiomatt
Copy link
Author

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.

@DanielOrtel
Copy link

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.

@CRIMSON-CORP
Copy link

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

@Atsuyoshi-Funahashi
Copy link

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.

@Rooster212
Copy link

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
Copy link

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
Copy link

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
Copy link

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!

@qaws5503
Copy link

qaws5503 commented Apr 25, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants