Skip to content

Allow StructBlocks to be collapsible #13216

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 4 commits into from
Jul 12, 2025

Conversation

laymonage
Copy link
Member

@laymonage laymonage commented Jul 8, 2025

Screen.Recording.2025-07-09.at.16.57.08.mov

Alternative to #2299.

To test:

diff --git a/bakerydemo/base/blocks.py b/bakerydemo/base/blocks.py
index 263d22f..3d6d5e4 100644
--- a/bakerydemo/base/blocks.py
+++ b/bakerydemo/base/blocks.py
@@ -23,6 +23,32 @@ def get_image_api_representation(image):
     }


+class ThemeSettingsBlock(StructBlock):
+    theme = ChoiceBlock(
+        choices=[
+            ("banana", "Banana"),
+            ("cherry", "Cherry"),
+            ("lime", "Lime"),
+        ],
+        required=False,
+        default="banana",
+        help_text="Select the theme for the block",
+    )
+    font_size = ChoiceBlock(
+        choices=[
+            ("small", "Small"),
+            ("medium", "Medium"),
+            ("large", "Large"),
+        ],
+        required=False,
+        default="medium",
+        help_text="Select the font size for the block",
+    )
+
+    class Meta:
+        icon = "cog"
+
+
 class CaptionedImageBlock(StructBlock):
     """
     Custom `StructBlock` for utilizing images with associated caption and
@@ -73,6 +99,7 @@ class HeadingBlock(StructBlock):
         blank=True,
         required=False,
     )
+    settings = ThemeSettingsBlock(collapsed=False)

     class Meta:
         icon = "title"
@@ -88,6 +115,7 @@ class BlockQuote(StructBlock):

     text = TextBlock()
     attribute_name = CharBlock(blank=True, required=False, label="e.g. Mary Berry")
+    settings = ThemeSettingsBlock(collapsed=True)

     class Meta:
         icon = "openquote"
diff --git a/bakerydemo/static/css/main.css b/bakerydemo/static/css/main.css
index 8f06b82..2b08185 100644
--- a/bakerydemo/static/css/main.css
+++ b/bakerydemo/static/css/main.css
@@ -2105,3 +2105,20 @@ input[type='radio'] {
   max-height: 100vh;
   margin-inline: auto;
 }
+
+.banana {
+  background-color: gold;
+}
+
+.cherry {
+  background-color: darkred;
+  color: white !important;
+}
+
+.cherry * {
+  color: white !important;
+}
+
+.lime {
+  background-color: limegreen;
+}
diff --git a/bakerydemo/templates/blocks/blockquote.html b/bakerydemo/templates/blocks/blockquote.html
index 1e20401..d9042b0 100644
--- a/bakerydemo/templates/blocks/blockquote.html
+++ b/bakerydemo/templates/blocks/blockquote.html
@@ -1,5 +1,5 @@
 {% load wagtailimages_tags %}

-<blockquote><p class="text">{{ self.text }}</p>
+<blockquote class="{{ self.settings.theme }}"><p class="text">{{ self.text }}</p>
     <p class="attribute-name">{{ self.attribute_name}}</p>
 </blockquote>
diff --git a/bakerydemo/templates/blocks/heading_block.html b/bakerydemo/templates/blocks/heading_block.html
index a5ed6e6..b424a8d 100644
--- a/bakerydemo/templates/blocks/heading_block.html
+++ b/bakerydemo/templates/blocks/heading_block.html
@@ -4,12 +4,12 @@
 {% endcomment %}

 {% if self.size == 'h2' %}
-    <h2>{{ self.heading_text }}</h2>
+    <h2 class="{{ self.settings.theme }}">{{ self.heading_text }}</h2>

 {% elif self.size == 'h3' %}
-    <h3>{{ self.heading_text }}</h3>
+    <h3 class="{{ self.settings.theme }}">{{ self.heading_text }}</h3>

 {% elif self.size == 'h4' %}
-    <h4>{{ self.heading_text }}</h4>
+    <h4 class="{{ self.settings.theme }}">{{ self.heading_text }}</h4>

 {% endif %}

@laymonage laymonage force-pushed the feature/collapsible-struct-block branch 2 times, most recently from f4cba86 to 9d31077 Compare July 9, 2025 15:50
@laymonage laymonage marked this pull request as ready for review July 9, 2025 16:00
@laymonage laymonage changed the title WIP: Allow StructBlocks to be collapsible Allow StructBlocks to be collapsible Jul 9, 2025
@laymonage laymonage self-assigned this Jul 9, 2025
@laymonage laymonage moved this to 👀 In review in Wagtail 7.1* release planning Jul 9, 2025
@@ -26,18 +26,35 @@ You can then provide custom CSS for this block, targeted at the specified classn
Wagtail's editor styling has some built-in styling for the `struct-block` class and other related elements. If you specify a value for `form_classname`, it will overwrite the classes that are already applied to `StructBlock`, so you must remember to specify the `struct-block` as well.
```

In addition, the `StructBlock`'s `Meta` class also accepts a `collapsed` attribute. When set to `None` (the default), the block is not collapsible. When set to `True` or `False`, the block is wrapped in a collapsible panel and initially displayed in a collapsed or expanded state in the editing interface, respectively. This can be useful for blocks with many sub-blocks, or blocks that are not expected to be edited frequently.
Copy link
Member Author

@laymonage laymonage Jul 9, 2025

Choose a reason for hiding this comment

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

Not 100% sure on this API design, but this allows us to mirror the collapsed option on StreamBlock and ListBlock, while keeping the current behaviour of any existing StructBlocks (i.e. collapsed = None).

We can't make StructBlocks collapsible by default, as this means they'd have two collapsible wrappers when used in StreamBlock or ListBlock, unless we want to handle that case specifically.

Copy link
Member

Choose a reason for hiding this comment

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

I think that’s the right approach. Definitely should consider making StructBlock collapsible by default IMO, in which case yes this would be worth adding extra logic to handle. But not in this PR!

@thibaudcolas thibaudcolas force-pushed the feature/collapsible-struct-block branch from 9d31077 to 08ed045 Compare July 12, 2025 09:44
@thibaudcolas thibaudcolas self-requested a review July 12, 2025 09:45
The class name passed as `form_classname` (defaults to `struct-block`).

**`block_definition`**
**`collapsed`**\
The initial collapsible state of the block (defaults to `None`). Note that the collapsible panel wrapper is not automatically applied to the block's form template. You must write your own wrapper if you want the block to be collapsible.
Copy link
Member Author

Choose a reason for hiding this comment

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

Not 100% sure either. We could wrap the custom form template in the collapsible section if that's desired, but it means we have to handle both cases in the StructBlock rendering logic.

Copy link
Member

@thibaudcolas thibaudcolas left a comment

Choose a reason for hiding this comment

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

All working well! A few things I’d nitpick in the implementation and there seems to be one or two bugs that were in this code before:

  • Toggle’s aria-expanded always set to true in initial rendering. I’ve not changed that yet as we also have the issue everywhere else. If you agree the code is off, could you open a follow-up issue?
  • Translation support for the aria-label. Will pick that up while merging.

@@ -2,6 +2,7 @@ import $ from 'jquery';
import { FieldBlockDefinition } from './FieldBlock';
import { StreamBlockDefinition } from './StreamBlock';
import { StructBlockDefinition } from './StructBlock';
import { initCollapsiblePanels } from '../../../includes/panels';
Copy link
Member

Choose a reason for hiding this comment

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

Don’t see this being used anywhere?

block.set_name("test_structblock")
js_args = StructBlockAdapter().js_args(block)

self.assertIs(js_args[2].get("collapsed"), case)
Copy link
Member

Choose a reason for hiding this comment

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

Noting as far as I can see all other unit tests for js_argsmeta directly access the attributes (js_args[2]["collapsed"]). And I believe collapsed is always returned? So could be nice to switch for consistency.


test('setError shows non-block errors', () => {
boundBlock.setError({
messages: ['This is just generally wrong.'],
Copy link
Member

Choose a reason for hiding this comment

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

🔥

<use href="#icon-link"></use>
</svg>
</a>
<button class="w-panel__toggle" type="button" aria-label="${'Toggle section'}" aria-describedby="${headingId}" data-panel-toggle aria-controls="${contentId}" aria-expanded="true">
Copy link
Member

Choose a reason for hiding this comment

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

Hmm, not specific to your changes but shouldn’t this aria-expanded be toggled between true and false based on the collapsed state?

blockTypeIcon,
blockTypeLabel,
collapsed,
} = this.props;
Copy link
Member

Choose a reason for hiding this comment

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

Oh no, he’s reinvented React!

Comment on lines +37 to +40
// Keep in sync with wagtailadmin/shared/panel.html
template.innerHTML = /* html */ `
Copy link
Member

Choose a reason for hiding this comment

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

I wonder at what point we need this so many times that we should find a better technique.

<use href="#icon-link"></use>
</svg>
</a>
<button class="w-panel__toggle" type="button" aria-label="${'Toggle section'}" aria-describedby="${headingId}" data-panel-toggle aria-controls="${contentId}" aria-expanded="true">
Copy link
Member

Choose a reason for hiding this comment

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

Uh-oh, we missed the un-translateable Toggle section

</div>
</section>
`;
return template.content.firstElementChild as HTMLElement;
Copy link
Member

Choose a reason for hiding this comment

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

oh TypeScript

@@ -26,18 +26,35 @@ You can then provide custom CSS for this block, targeted at the specified classn
Wagtail's editor styling has some built-in styling for the `struct-block` class and other related elements. If you specify a value for `form_classname`, it will overwrite the classes that are already applied to `StructBlock`, so you must remember to specify the `struct-block` as well.
```

In addition, the `StructBlock`'s `Meta` class also accepts a `collapsed` attribute. When set to `None` (the default), the block is not collapsible. When set to `True` or `False`, the block is wrapped in a collapsible panel and initially displayed in a collapsed or expanded state in the editing interface, respectively. This can be useful for blocks with many sub-blocks, or blocks that are not expected to be edited frequently.
Copy link
Member

Choose a reason for hiding this comment

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

I think that’s the right approach. Definitely should consider making StructBlock collapsible by default IMO, in which case yes this would be worth adding extra logic to handle. But not in this PR!

@thibaudcolas thibaudcolas force-pushed the feature/collapsible-struct-block branch from 08ed045 to d8d7b12 Compare July 12, 2025 11:22
@thibaudcolas thibaudcolas merged commit 13bc129 into wagtail:main Jul 12, 2025
12 of 14 checks passed
@github-project-automation github-project-automation bot moved this from 👀 In review to ✅ Done in Wagtail 7.1* release planning Jul 12, 2025
@thibaudcolas thibaudcolas added this to the 7.1 milestone Jul 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: ✅ Done
Development

Successfully merging this pull request may close these issues.

2 participants