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

Merged
merged 38 commits into from
Jun 20, 2025
Merged

feat: Toggle blocks #1707

merged 38 commits into from
Jun 20, 2025

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 20, 2025 9:53am
blocknote-website ❌ Failed (Inspect) Jun 20, 2025 9:53am

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: 5435942

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
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.

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.

Awesome, almost there I think!

empty toggle list should have a placeholder "Toggle" (same as Notion)

I don't think this works?

Others:

  • let's make the font color of the "add button" placeholder more placeholder-like (gray?)
  • Is it easy to remove the vertical lines for toggled blocks? imo this still looks a bit weird

One architectural question:

I realize we're currently following notion in this design, but let's make sure having an is_toggleable prop on a heading makes more sense than having a separate ToggleHeading block. What are the pros / cons? (cc @nperez0111 )


export const defaultToggledState: ToggledState = {
set: (block, isToggled: boolean) =>
window.localStorage.setItem(
Copy link
Collaborator

Choose a reason for hiding this comment

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

should we move this logic (whether to store to local storage) to be configurable on the editor? Or want to keep it as part of the block spec (and thus the schema)?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

imo, for now, part of the block spec (lowest-level possible).

In the future, this would be a "bundle" that you can configure independently

...defaultProps,
} satisfies PropSchema;

const ToggleListItemBlockContent = createStronglyTypedTiptapNode({
Copy link
Collaborator

Choose a reason for hiding this comment

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

Food for thought; when streamlining built-in blocks, I'm curious if we can come up with a light-weight way to make this more readable (React / jsx like), without pulling in all of react. And thus, get rid of the duplicate (vanilla + react) implementation

cc @nperez0111

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I have ideas for this. Basically use jsx for actual document.createElement calls, without all of React. In my ideal, you wouldn't even need React.

@@ -204,9 +206,11 @@ NESTED BLOCKS
}

/* Unordered */
.bn-block-content[data-content-type="bulletListItem"] {
.bn-block-content[data-content-type="bulletListItem"]::before {
Copy link
Collaborator

Choose a reason for hiding this comment

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

can you explain why this changed? why the before element that wasn't here earlier?

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 I forgot to mention this in the PR description - I adjusted offsets for list items, indented blocks, and the "add block" button of the toggle blocks to make them all the same. So all of these are now vertically in-line now, and these styles are a part of that.

@nperez0111
Copy link
Contributor

I realize we're currently following notion in this design, but let's make sure having an is_toggleable prop on a heading makes more sense than having a separate ToggleHeading block. What are the pros / cons? (cc @nperez0111 )

My argument for this was that let's say you were generating the table of contents for a page, would you consider a heading that happens to be toggle-able, not a heading? By making it a different block, you change the semantics of what it is, whereas toggling is purely a visual thing (much like setting a background color wouldn't change the type of block it is)

@YousefED
Copy link
Collaborator

I realize we're currently following notion in this design, but let's make sure having an is_toggleable prop on a heading makes more sense than having a separate ToggleHeading block. What are the pros / cons? (cc @nperez0111 )

My argument for this was that let's say you were generating the table of contents for a page, would you consider a heading that happens to be toggle-able, not a heading? By making it a different block, you change the semantics of what it is, whereas toggling is purely a visual thing (much like setting a background color wouldn't change the type of block it is)

Good point. Going to challenge a bit more to make sure we're confident (I still don't think I have a preference, or maybe start to prefer the current implementation):

  • Following your reasoning, does it still make sense the toggle list items are different from regular lists?
  • Having separate blocks would solve things like this, right? feat: Toggle blocks #1707 (comment)

@nperez0111
Copy link
Contributor

Good point. Going to challenge a bit more to make sure we're confident (I still don't think I have a preference, or maybe start to prefer the current implementation):

* Following your reasoning, does it still make sense the toggle list items are different from regular lists?

Good point, I'd argue that these should actually be named toggleBlocks rather than a list item, and they roughly correspond to the <details> element in HTML: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details.

UI-wise, the naming could be different for understanding maybe, but, I think, they are functionally equivalent. List does sort of convey that hitting enter, you'd get more of them.

List items are actually the perfect demonstration of what I mean here about semantics, it is always an li but what it is wrapped (e.g. <ul>) in that determines it's display.

* Having separate blocks would solve things like this, right? [feat: Toggle blocks #1707 (comment)](https://github.com/TypeCellOS/BlockNote/pull/1707#discussion_r2150279943)

I think what would solve this is the ability to easily customize the schema. In this case, I'd want an option to remove isToggable from the prop schema, and if that prop schema is completely missing, we could hide the slash menu items as not being applicable.

@matthewlipski
Copy link
Collaborator Author

To follow up with the naming convention for toggleListItem - worth mentioning that this is how Notion represents them too.

Also r.e. the <details> element, I agree this would be nice to use though I'm not 100% sure it fits within our HTML structure. Basically the hidden content, i.e. the blockGroup, should be inside it below the <summary> element, which we can't really do since it's outside of the blockContent element.

@@ -157,7 +157,7 @@ export const nl: Dictionary = {
placeholders: {
default: "Voer tekst in of type '/' voor commando's",
heading: "Kop",
toggleList: "Schakelaar",
toggleListItem: "Schakelaar",
Copy link
Collaborator

Choose a reason for hiding this comment

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

This word means "switch" (as in, "light switch" / "light toggle").

Can we give your AI some extra context to improve the translations?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm, I'm just using Copilot built into VSCode so it should at least have the context of the english translation file, any way I can add to this?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think you can @ mention specific files in the chat? It also helps to just chat explanation of what the toggle is (i.e.: a block that expands in a document editor)

@YousefED
Copy link
Collaborator

List does sort of convey that hitting enter, you'd get more of them.

@nperez0111 that's also the case.

I'm starting to think that the current naming is ok - unless anyone feels otherwise after the above discussion

@nperez0111
Copy link
Contributor

I think the current naming is okay if it keeps the newline behavior. If someone wants to disable that, then it should be called a toggle block.

But, obviously no way for us to enforce it.

Agreed, naming is fine here.

@nperez0111 nperez0111 merged commit 788c5b8 into main Jun 20, 2025
7 of 8 checks passed
@nperez0111 nperez0111 deleted the toggle-blocks branch June 20, 2025 11:11
@nperez0111
Copy link
Contributor

Thought about it, & decided it was fine like this:

  • If you want there to be both toggle & not toggable headings, that is the default
  • If you want only not toggable headings, we could disable the view layer from wrapping the heading with the creatToggleWrapper
  • If you want only toggable headings, you would similarly just enforce that the view layer to always be wrapped in a toggleable

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. Spinner showing in all icons Support for folding?
3 participants