Skip to content

feat: Toggle blocks #1707

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

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open

feat: Toggle blocks #1707

wants to merge 35 commits into from

Conversation

matthewlipski
Copy link
Collaborator

@matthewlipski matthewlipski commented May 21, 2025

Toggle wrapper element/component

This PR adds a new component, ToggleWrapper, as well as a vanilla HTML element implementation of it, createToggleWrapper. These can be used to wrap existing blocks, or create new ones and allow the user to toggle showing/hiding their children.

The toggle wrapper renders a button to the left of the block to show/hide the children. Additionally, if children are shown but none exist, a button is rendered to prompt the user to add a block. Because the toggled state is only in the view, toggling the visibility of children only happens client-side when using collaboration.

A toggledState object can also be passed to the toggle wrapper, which includes a get and set function to be able to preserve the state on re-render and reload. By default, this is done using local storage.

Schema changes

This PR also adds toggle headings and toggle list items to the default editor schema.

The toggle headings are implemented by adding an isToggleable prop, and rendering the block in a toggle wrapper when this is set to true.

The toggle list item is a new, separate block, which always renders a paragraph in a toggle wrapper. Like the other list items though, hitting Enter in one creates a new block of the same type, which is why I made it a separate block rather than adding isToggleable to the existing paragraph or list item blocks. Additionally, the block is exported differently to formats like docx, as the content gets prepended with a ">".

TODO

Currently some xl-ai tests are failing, and these have been temporarily disabled.

Closes #196
Closes #549
Closes #910

Copy link

vercel bot commented May 21, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Updated (UTC)
blocknote ✅ Ready (Inspect) Visit Preview Jun 17, 2025 8:51am
blocknote-website ✅ Ready (Inspect) Visit Preview Jun 17, 2025 8:51am

Copy link

pkg-pr-new bot commented May 21, 2025

Open in StackBlitz

@blocknote/ariakit

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/ariakit@1707

@blocknote/code-block

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/code-block@1707

@blocknote/core

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/core@1707

@blocknote/mantine

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/mantine@1707

@blocknote/react

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/react@1707

@blocknote/server-util

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/server-util@1707

@blocknote/shadcn

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/shadcn@1707

@blocknote/xl-ai

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-ai@1707

@blocknote/xl-docx-exporter

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-docx-exporter@1707

@blocknote/xl-multi-column

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-multi-column@1707

@blocknote/xl-odt-exporter

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-odt-exporter@1707

@blocknote/xl-pdf-exporter

npm i https://pkg.pr.new/TypeCellOS/BlockNote/@blocknote/xl-pdf-exporter@1707

commit: df3c678

Copy link
Contributor

@nperez0111 nperez0111 left a comment

Choose a reason for hiding this comment

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

What I'm more interested in seeing here is how we can add this to existing block types (such as headings with a prop) rather than as a separate element like this implements.

Copy link
Collaborator

@YousefED YousefED left a comment

Choose a reason for hiding this comment

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

Nice! Still need to dive a bit deeper in the code but in general looks great.

Let's make the UX a bit more polished. When comparing with other products (e.g. Notion), this catches my attention:

  • let's make the arrow similar to notion (the rounded caret doesn't work well with the font), and same size regardless of heading size
  • Can you pay extra attention to alignment and margins? These are some of the most impactful design concepts. For example, the nested content should be vertically aligned with the parent content (if it helps, remove the vertical border on the left)
  • empty toggle list should have a placeholder "Toggle" (same as Notion)
  • dragging a collapsed toggle to a different position expands it's state
  • Nice to have: dropping blocks in an empty toggle

Others:

  • Toggle heading doesn't appear in formatting toolbar dropdown
  • We need to design an API of where to store "collapsed state" for the current user
  • Typing in an empty toggle collapses the arrow (but does show the children placeholder)
  • key down in an empty toggle doesn't work. In general, make sure to test keyboard handling
  • nice to have (but probably not trivial?): cmd+shift+arrow skip over collapsed blocks

@matthewlipski
Copy link
Collaborator Author

Note about enter handling - when hitting enter within a togglable block, Notion does one of 2 things:

  • If the child blocks are visible, it splits the parent block and moves the content after the cursor into a new child at the start.
  • If the child blocks are visible, it splits the parent block and moves the content after the cursor into a new sibling after.

Since the BlockNote API currently doesn't have a way of splitting blocks in this way (getSelectionCutBlocks only works if the selection is not collapsed and even then doesn't give us all the info we need), I'm skipping this for now. Though I can ofc just implement that in the ProseMirror/TipTap API if it's worth the time.

Copy link
Contributor

@nperez0111 nperez0111 left a comment

Choose a reason for hiding this comment

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

Does isToggleable have to show up on every element now? Can it not just be assumed false if undefined? Or, do we always render "default values"?

Comment on lines +125 to +135
{
onItemClick: () => {
insertOrUpdateBlock(editor, {
type: "heading",
props: { level: 1, isToggleable: true },
});
},
key: "toggle_heading",
...editor.dictionary.slash_menu.toggle_heading,
},
{
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a way to disable toggleable headings but not headings?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think so, you could just do getDefaultSlashMenuItems(...).filter((item) => !item.key.startsWith("toggle_heading")) right? So pretty much the same way you'd filter out any other items from the menu. Or do you mean in the schema itself?

Copy link
Contributor

Choose a reason for hiding this comment

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

I meant from the schema

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, in that case not really but I think it's ok for now, seems like something that will get fixed either way when converting default blocks to use the custom blocks API.

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.

The ability for the blockgroup element to collapse/expand. Support for folding? Spinner showing in all icons
3 participants