Repository: wagtail/wagtail

Issue:
MultiFieldPanel and InlinePanel help_text no longer renders in 4.0
### Issue Summary

In Wagtail 4.0, the `help_text` argument in the MultiFieldPanel is no longer rendered, whereas it was rendered in 2.x and 3.0.x.

### Steps to Reproduce

1. Start fresh Wagtail installations for 4.0 and 3.0.2 as per the instructions at [Getting Started](https://docs.wagtail.org/en/stable/getting_started/tutorial.html). For each:
2. Merge in the application at https://github.com/dkirkham/wagtail-ui-testpage
3. Add `'testpage'` to `INSTALLED_APPS` in `settings/base.py`
4. Migrate and runserver
5. Login to the Wagtail admin interface
6. Create a child `Testpage` of the home page
7. Go to the `Event Details` tab
8. Observe the `Orderables Header` multifield.

In 3.0.2, the `help_text` content appears at the right when hovering over the multifield:

<img width="1048" alt="image" src="https://user-images.githubusercontent.com/1977376/188302297-eb10a78d-d309-46b8-a33e-fcf7a497fbfb.png">

In 4.0, the `help_text` is not rendered:

<img width="999" alt="image" src="https://user-images.githubusercontent.com/1977376/188302340-ae0b3fb3-621c-42fe-a518-ab0c3f920e94.png">

The `help_text` is carried through the 4.0 code and is available in the template context.

-   I have confirmed that this issue can be reproduced as described on a fresh Wagtail project: (yes)

### Comments
This issue could be considered a bug, a documentation bug or a feature request.

At the moment, I'm illustrating a difference in functional behaviour, in that the `help_text` that was visible in 3.0.2 is no longer visible. If this was the design intent with the new UI, then it is a documentation bug in that it should have been commented in the Upgrade Considerations part of the 4.0 release notes.

In my initial Slack support discussion thread ([here](https://app.slack.com/client/T0K33F93J/threads/thread/C81FGJR2S-1661926377.673069)) it was suggested that the solution might be to use a `HelpPanel`. This may be a reasonable and more flexible solution – given the reason I originally used the `MultiFieldPanel` was mainly to get the red banners. If the UI team think this is the best solution, then that should be documented. But to maintain the original function, it might still make sense to at least render the `help_text` if it is provided.

Incidently, the `InlinePanel label` that is rendered in the 4.0, as part of the InlinePanel and above the button that instantiates each new Orderable form, may be superfluous. This should also be considered by the UI team.

### Technical details

-   Python version: Testing with 3.9, but probably irrelevant
-   Django version: Testing with default versions for each wagtail release, but probably irrelevant
-   Wagtail version: As per the instructions - 3.0.2 and 4.0
-   Browser version: Screenshots with Firefox 103, but probably irrelevant



Discussion:
@dkirkham thank you for reporting this! I implemented most of the changes to MultiFieldPanel, and I wasn’t aware it supports a help text. I don’t think I’ve ever seen this in action nor in the docs. 

Do you have any thoughts on your side on whether this should be considered a bug or not? I’ll investigate on my end as well.
Hi @thibaudcolas,

I had to go back to my original code, and I can see in one of the earliest commits in that project I have `MultiFieldPanel`s with `help_text` parameters. That was with Wagtail 2.7.1, early in 2020.

Whether I did look at the Wagtail source code at the time, saw something in Kalob's excellent videos or a StackOverflow answer, or serendipitously confused `FieldPanel`s with `Field`s which do support `help_text`, I don't know.

While I agree the `help_text` parameter is not documented, it is in the code for `MultiFieldPanel`s parent `Panel` class and the value is passed to the rendering context. The change in the templates between 3.0 and 4.0 resulted in the change of behaviour.

Because my use depends on undocumented behaviour, it is difficult to argue this is a bug. However, I've used this behaviour and I've found some instances of this in public GitHub repos that also do, so the idea of using `help_text` within a `MultiFieldPanel` appears to be in the wild.

Given this, it might make sense to support rendering `help_text` if it is present, which will be a simple addition to the relevant template. If there is a specific desire not to support this behaviour, then a warning might make sense, with the suggestion to use a `HelpPanel` instead. Some documentation to this effect against `MultiFieldPanel` would also help.

I'm happy to change my code whichever way you decide, and if you want, I could prepare a Wagtail PR accordingly.
Idea - we drop the concept of `help_text` on panels (it is confusing and gets muddied up with `help_text` on fields), and we migrate to something like `description` or `subtitle`.

I think this is a super useful feature and recall using it a long time ago on `MultiFieldPanel` but as it's not documented currently maybe we have a little room to move (make this useful again) without pulling the whole code out.
I haven’t checked the code @lb-, but I presume the `help_text` from each `Field` is passed to `help_text` in the corresponding `FieldPanel`, so most of the current functionality would need to be kept.

Given that, would your suggestion apply just to `MultiFieldPanel`s or to all panel types?

Or is it easier to keep the <=3.0.x behaviour, albeit adapted to the new UI, with one or two lines of template code?
I got to investigate this a bit,

- I found 3 such MultiFieldPanel `help_text` definitions on sites I have access to the code for
- This seems to have worked for all panels _at the top level_, because the rendering actually came from our [ObjectList’s `object_list.html` template](https://github.com/wagtail/wagtail/blob/v3.0/wagtail/admin/templates/wagtailadmin/panels/object_list.html#L14-L20), it wasn’t specific to any panel type.

While I was looking into this, I realised we’re also no longer rendering `help_text` for InlinePanel, which is clearly a bug.

---

Considering all of the above, I think I’m leaning towards at least restoring the previous behavior for `help_text` in MultiFieldPanel (and FieldRowPanel) for Wagtail 4.0. I’m not sure what would be the best way to go about this, there are a few options:

1. Re-add the help_text rendering for all ObjectList children, as before. I find it a bit confusing that `help_text` would only work for those panels when used without nesting, but at least we’d be back to feature parity with Wagtail 3
2. Add the help_text rendering separately to InlinePanel, MultiFieldPanel, and any other panel type we see relevant.
3. More involved changes as suggested by LB

Options 1. and 2. should only be a few lines of template code to the effect of:

```html
{% if panel.help_text %}
    <div class="help">{{ panel.help_text }}</div>
{% endif %}
```

My hunch is that option 2. would make more sense with the current direction of the panels API. @gasman since you’ve worked on this very recently I assume the best approach will be more obvious to you?
Just caught up with Matthew, we’re keen on:

> 2. Add the help_text rendering separately to InlinePanel, MultiFieldPanel, and any other panel type we see relevant.

We want individual panels to look after their own rendering as much as possible, and it wasn’t intentional for us to remove `help_text` support for MultiFieldPanel, even if that wasn’t a documented feature.
Thanks @thibaudcolas, I could have a go preparing a PR along these lines if you like.

Resolving patch:
diff --git a/wagtail/admin/panels.py b/wagtail/admin/panels.py
--- a/wagtail/admin/panels.py
+++ b/wagtail/admin/panels.py
@@ -717,7 +717,7 @@ def __init__(self, **kwargs):
             else:
                 self.heading = self.bound_field.label
 
-            self.help_text = self.bound_field.help_text
+            self.help_text = self.panel.help_text or self.bound_field.help_text
 
         @property
         def field_name(self):


Test patch:
diff --git a/wagtail/admin/tests/pages/test_edit_page.py b/wagtail/admin/tests/pages/test_edit_page.py
--- a/wagtail/admin/tests/pages/test_edit_page.py
+++ b/wagtail/admin/tests/pages/test_edit_page.py
@@ -119,12 +119,19 @@ def test_page_edit(self):
         self.assertEqual(response["Content-Type"], "text/html; charset=utf-8")
         self.assertContains(response, 'id="status-sidebar-live"')
 
-        # Test InlinePanel labels/headings
+        # Test help text defined on FieldPanel
+        self.assertContains(response, "Who this event is for")
+
+        # Test InlinePanel labels/headings/help text
         self.assertContains(
             response,
             '<label class="w-field__label" for="id_speakers-__prefix__-last_name" id="id_speakers-__prefix__-last_name-label">',
         )
         self.assertContains(response, "Add speakers")
+        self.assertContains(response, "Put the keynote speaker first")
+
+        # Test MultiFieldPanel help text
+        self.assertContains(response, "For SEO nerds only")
 
         # test register_page_action_menu_item hook
         self.assertContains(
diff --git a/wagtail/admin/tests/test_edit_handlers.py b/wagtail/admin/tests/test_edit_handlers.py
--- a/wagtail/admin/tests/test_edit_handlers.py
+++ b/wagtail/admin/tests/test_edit_handlers.py
@@ -728,7 +728,8 @@ def setUp(self):
             [
                 FieldPanel("date_from", classname="col4", heading="Start"),
                 FieldPanel("date_to", classname="coltwo"),
-            ]
+            ],
+            help_text="Confirmed event dates only",
         ).bind_to_model(EventPage)
 
     def test_render_html(self):
@@ -756,9 +757,12 @@ def test_render_html(self):
             result,
         )
 
-        # check that help text is included
+        # check that field help text is included
         self.assertIn("Not required if event is on a single day", result)
 
+        # check that row help text is included
+        self.assertIn("Confirmed event dates only", result)
+
         # check that the populated form field is included
         self.assertIn('value="2014-07-22"', result)
 
diff --git a/wagtail/snippets/tests/test_snippets.py b/wagtail/snippets/tests/test_snippets.py
--- a/wagtail/snippets/tests/test_snippets.py
+++ b/wagtail/snippets/tests/test_snippets.py
@@ -650,6 +650,8 @@ def test_snippet_with_tabbed_interface(self):
             response,
             '<a id="tab-label-other" href="#tab-other" class="w-tabs__tab " role="tab" aria-selected="false" tabindex="-1">',
         )
+        self.assertContains(response, "Other panels help text")
+        self.assertContains(response, "Top-level help text")
 
     def test_create_with_limited_permissions(self):
         self.user.is_superuser = False
diff --git a/wagtail/test/testapp/models.py b/wagtail/test/testapp/models.py
--- a/wagtail/test/testapp/models.py
+++ b/wagtail/test/testapp/models.py
@@ -382,12 +382,17 @@ class EventPage(Page):
         FieldPanel("time_from"),
         FieldPanel("time_to"),
         FieldPanel("location"),
-        FieldPanel("audience"),
+        FieldPanel("audience", help_text="Who this event is for"),
         FieldPanel("cost"),
         FieldPanel("signup_link"),
         InlinePanel("carousel_items", label="Carousel items"),
         FieldPanel("body"),
-        InlinePanel("speakers", label="Speakers", heading="Speaker lineup"),
+        InlinePanel(
+            "speakers",
+            label="Speakers",
+            heading="Speaker lineup",
+            help_text="Put the keynote speaker first",
+        ),
         InlinePanel("related_links", label="Related links"),
         FieldPanel("categories"),
         # InlinePanel related model uses `pk` not `id`
@@ -395,7 +400,9 @@ class EventPage(Page):
     ]
 
     promote_panels = [
-        MultiFieldPanel(COMMON_PANELS, "Common page configuration"),
+        MultiFieldPanel(
+            COMMON_PANELS, "Common page configuration", help_text="For SEO nerds only"
+        ),
         FieldPanel("feed_image"),
     ]
 
@@ -948,8 +955,11 @@ class AdvertWithTabbedInterface(models.Model):
     edit_handler = TabbedInterface(
         [
             ObjectList(advert_panels, heading="Advert"),
-            ObjectList(other_panels, heading="Other"),
-        ]
+            ObjectList(
+                other_panels, heading="Other", help_text="Other panels help text"
+            ),
+        ],
+        help_text="Top-level help text",
     )
 
     def __str__(self):