Skip to content

Comments

Add renderText and leafPosition#5850

Merged
dylans merged 18 commits intoianstormtaylor:mainfrom
udecode:feat/render-leaf-decoration
Apr 29, 2025
Merged

Add renderText and leafPosition#5850
dylans merged 18 commits intoianstormtaylor:mainfrom
udecode:feat/render-leaf-decoration

Conversation

@zbeyens
Copy link
Contributor

@zbeyens zbeyens commented Apr 28, 2025

Description
Add optional position property to leaves returned by Text.decorations() and introduce new renderText prop for customizing text node rendering. These two features provide complementary solutions for handling text node customization, particularly when dealing with decorated text that gets split into multiple leaves.

Example
Before:

Text.decorations({ text: 'abc', mark: 'bold' }, [...])
// Returns:
[
  { text: 'a', mark: 'bold' },
  { text: 'b', mark: 'bold', decoration: 'highlight' },
  { text: 'c', mark: 'bold' }
]

After:

Text.decorations({ text: 'abc', mark: 'bold' }, [...])
// Returns leaves with position info only when split by decorations:
[
  { 
    leaf: { text: 'a', mark: 'bold' },
    position: { start: 0, end: 1, isFirst: true }
  },
  { 
    leaf: { text: 'b', mark: 'bold', decoration: 'highlight' },
    position: { start: 1, end: 2 }
  },
  { 
    leaf: { text: 'c', mark: 'bold' },
    position: { start: 2, end: 3, isLast: true }
  }
]

// Single leaf = no position info needed:
[{ leaf: { text: 'abc', mark: 'bold' } }]

Context
When working with decorated text nodes, it's often necessary to know the exact position of each leaf within the original text. A common use case is rendering an element exactly once after a text node, even when that text node has been split into multiple leaves by decorations.

This PR provides two different approaches to solve this problem:

  1. Using the new leafPosition property in renderLeaf:
const renderLeaf = ({ leaf, text, leafPosition, ...props }) => {
  return (
    <>
      <span {...props.attributes}>{props.children}</span>
      {/* Render tooltip only once, after the last leaf of this text node */}
      {leafPosition?.isLast && <Tooltip />}
    </>
  )
}
  1. Alternatively, using the new renderText prop to handle the entire text node:
const renderText = ({ text, children, attributes }) => {
  return (
    <span {...attributes} data-custom-text>
      {children}
      {/* Tooltip is rendered once per text node, regardless of decorations */}
      <Tooltip />
    </span>
  )
}

<Editable
  renderText={renderText}
  // ... other props
/>
  • The leafPosition prop is only added when a text node is actually split by decorations.
  • The renderText prop offers an alternative approach by letting you customize the container of all leaves within a text node.

@changeset-bot
Copy link

changeset-bot bot commented Apr 28, 2025

🦋 Changeset detected

Latest commit: db6db77

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
slate-react Minor
slate Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@zbeyens zbeyens changed the title Add Leaf with start and end props Add Leaf with position Apr 28, 2025
@12joan
Copy link
Contributor

12joan commented Apr 28, 2025

I wonder if it would be better to put this information in a separate property of RenderLeafProps from leaf? That would prevent unintuitive behaviour like Node.extractProps returning position for decorated text nodes when called from renderLeaf, and it would keep the interface of leaf consistent in all cases.

The property could be named to make it clearer that it refers to the position of the leaf within a decoration, rather than its position within the parent element, for example.

@zbeyens
Copy link
Contributor Author

zbeyens commented Apr 28, 2025

Good point @12joan, I've refactored to use leafPosition instead. I've also added renderText that would be an alternative solution to our use-case.

@zbeyens zbeyens changed the title Add Leaf with position Add renderText and leafPosition Apr 28, 2025
Comment on lines 93 to 94
'data-slate-node': 'text',
ref: callbackRef,
Copy link
Contributor

Choose a reason for hiding this comment

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

We probably don't want to apply data-slate-node="text" to multiple different layers of the DOM hierarchy, and we certainly don't want to apply the ref twice. I would probably omit attributes from renderText, or possibly leave it as an empty object if we want to preserve the ability to add a distinct ref later without it being a breaking change. I can't think of any reason why that would be necessary, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I took those attributes from the default behavior for backward compatibility.

Unless I'm missing something, there can't be nested renderText.

Copy link
Contributor

Choose a reason for hiding this comment

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

textContent also has those attributes, and is passed as children to renderText.

  const textContent = (
    <span data-slate-node="text" ref={callbackRef}>
      {children}
    </span>
  )

  if (renderText) {
    return renderText({
      text,
      children: textContent,
      attributes: {
        'data-slate-node': 'text',
        ref: callbackRef,
      },
    })
  }

If renderText is implemented as ({ attributes, children }) => <span {...attributes}>{children}</span>, then the resulting JSX is:

<span data-slate-node="text" ref={refCallback}>
  <span data-slate-node="text" ref={refCallback}>
    {children}
  </span>
</span>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, I meant to use children instead of textContent there. Updating...

Copy link
Contributor

Choose a reason for hiding this comment

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

Got it. Since that's your intent, perhaps we could use the same pattern as renderElement to provide a default renderText that just renders a span with those attributes? That would eliminate the need for an if statement here.

https://github.com/ianstormtaylor/slate/blob/main/packages/slate-react/src/components/element.tsx#L44

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@12joan
Copy link
Contributor

12joan commented Apr 28, 2025

Good point @12joan, I've refactored to use leafPosition instead. I've also added renderText that would be an alternative solution to our use-case.

Neat solution with renderText!

@dylans dylans merged commit 22a3dda into ianstormtaylor:main Apr 29, 2025
7 of 11 checks passed
@github-actions github-actions bot mentioned this pull request Apr 29, 2025
christianhg added a commit to christianhg/slate that referenced this pull request May 7, 2025
`renderText` was introduced in ianstormtaylor#5850, but the prop types didn't get exported
with it.
christianhg added a commit to christianhg/slate that referenced this pull request May 7, 2025
`renderText` was introduced in ianstormtaylor#5850, but the prop types didn't get exported
with it.
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

Successfully merging this pull request may close these issues.

3 participants