Skip to content

[@platejs/utils] BlockPlaceholderPlugin suppresses placeholder on single empty list item (indent-list) #4951

@bacaxnot

Description

@bacaxnot

Bug

BlockPlaceholderPlugin does not render its placeholder when the editor contains exactly one empty block that has been converted to a list item via @platejs/list's indent-list (e.g., via - autoformat). The placeholder renders correctly on subsequent empty list items created after the first.

Versions

  • @platejs/utils@52.3.21
  • @platejs/list (indent-list style)
  • react-day-picker unused here — irrelevant
  • Bun runtime

Reproduction

  1. Create a Plate editor with BlockPlaceholderPlugin configured (placeholders: { p: "Write here" }, default query: ({ path }) => path.length === 1) and @platejs/list in indent-list style.
  2. Editor opens with the default empty value: [{ type: "p", children: [{ text: "" }] }]. Placeholder renders.
  3. Focus the editor and type - (or 1. ). The autoformat converts block 0 into an empty list item: { type: "p", listStyleType: "disc", indent: 1, children: [{ text: "" }] }.
  4. Expected: placeholder continues to render on this single empty list item.
  5. Actual: placeholder disappears. No placeholder visible anywhere in the editor.
  6. Press Enter. A second empty list item is created. Placeholder now renders on block 1 ("Write here" appears at the second bullet).

Root cause

The render gate in BlockPlaceholderPlugin (node_modules/@platejs/utils/dist/react, around line 238) reads:

if (
  query({ ...ctx, node: element, path }) &&
  placeholder &&
  editor.api.isEmpty(element) &&
  !editor.api.isEmpty()
) setOption("_target", { node: element, placeholder: placeholders[placeholder] });

editor.api.isEmpty() (no args) returns true when editor.children.length === 1 && isEmpty(editor, editor.children[0]). Slate's element-level isEmpty only inspects element.children — it ignores extra properties like listStyleType and indent. So a single empty list item (indent-list keeps type: "p" and adds those properties) is still "empty" to Slate, editor.api.isEmpty() returns true, and the !editor.api.isEmpty() guard suppresses the placeholder.

The intent of that guard is clearly "don't render the placeholder when the editor is in its pristine initial state." But indent-list transformations produce a state that is not pristine from the user's perspective yet still registers as empty under the current check.

Proposed fix

Replace the !editor.api.isEmpty() guard with a check that also considers block-level structural properties:

// Pseudocode — don't suppress placeholder when the single empty block has list/indent properties.
const isPristineEditor =
  editor.children.length === 1 &&
  editor.api.isEmpty(editor.children[0]) &&
  !editor.children[0].listStyleType &&
  !editor.children[0].indent;

if (query(...) && placeholder && editor.api.isEmpty(element) && !isPristineEditor) setOption(...);

Alternatively, expose the guard as a plugin option (e.g., suppressOnPristineEditor: boolean, or a shouldRender(editor, element) override) so consumers can configure the behavior per editor. This would be the most flexible fix — and query alone cannot currently override it because it runs before the hardcoded check.

Workaround for other users hitting this

  • Render a custom placeholder overlay outside the plugin, gated on the "single empty list item" state.
  • Or patch the plugin via patch-package to change the hardcoded guard.

Happy to contribute a PR if you'd like.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingneeds-triageMaintainer needs to evaluate this issueplugin:list

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions