Repository: wagtail/wagtail

Issue:
Feature request: page editor minimap
### Is your proposal related to a problem?

Long page editing UIs are tedious to navigate.

### Describe the solution you'd like

The minimap is a proposed feature from the [page editor 2022](https://github.com/wagtail/wagtail/discussions/7739) project, to help with navigation of those pages. We’re currently [looking for sponsors or contributors](https://wagtail.org/blog/build-update-on-new-page-editor/) to help deliver the minimap.

This new component serves different use cases:

- For mouse users, the minimap presents all of the page’s sections (panels, potentially blocks, potentially fields) as a table of contents that can be reached at any point
- For keyboard users, the minimap functions as a set of skip links – to jump straight to any part of the page
- For sighted users, the minimap always displays a visual indicator of the user’s current position through the page structure
- When opened, it also has a button to collapse all of the page’s sections at once.

Here is what it looks like in practice:

![Image](https://user-images.githubusercontent.com/877585/173399135-edebeb20-88af-47a7-9ecd-7f6234c9433f.gif)

[Figma design link](https://www.figma.com/file/h67EsVXdWsfu38WGGxWfpi/Wagtail-Design-System?node-id=4859%3A44694)

### Additional context

- Expecting this to be built based on our upcoming refactor of field panels, to use a more semantic document outline, as well as having the ability to link to individual section headings
- The collapsing is a feature of individual sections – "collapse all" just triggers the action for all of them at once.
- The currently-active (visible?) sections of the page are shown in the minimap, using the IntersectionObserver API


Discussion:
I have a working POC for this here https://github.com/lb-/wagtail/commits/experiments/mini-map (see last commit).

Note that this is implemented using Stimulus as it makes it easier to scope event listeners and register new behaviour to dynamically injected elements (e.g. blocks). Could be refactored to non-stimulus but would require more code.

## Recording

https://user-images.githubusercontent.com/1396140/173516073-49083162-80b9-463d-89d8-0d4b6184dda4.mp4


## Summary of POC

* Each `h2` or `h3` we care about is a Stimulus target (which is used to build the mini-map list)
* Each container, where relevant, around those headers gets an intersection observer attached (via a separate, generic intersection observer controller in Stimulus), which dispatches events based on scrolling / moving in or out of view
  * https://github.com/lb-/wagtail/blob/experiments/mini-map/client/src/components/StreamField/blocks/BaseSequenceBlock.js#L149 (blocks)
* The minimap controller is scoped to one tab's object list, so that there is only one sidebar for each tab
    * https://github.com/lb-/wagtail/blob/experiments/mini-map/wagtail/admin/templates/wagtailadmin/panels/object_list.html -> add data attributes and base `aside` element for the sidebar
* Mini map controller code https://github.com/lb-/wagtail/blob/experiments/mini-map/client/src/controllers/MiniMapController.ts
* Intersection observer code https://github.com/lb-/wagtail/blob/experiments/mini-map/client/src/controllers/IntersectionObserverController.ts
* React component used for the inner `ul` as it is much simpler to re-render content in a list through React, also a good example of how Stimulus & React can work together https://github.com/lb-/wagtail/blob/experiments/mini-map/client/src/components/MiniMapIndex/MiniMapIndex.tsx
* interactions are heavily debounced, no smarts yet as to when/when not to dispatch (e.g. only when observer has changed), so can be up to 300ms delay in some interactions.
* It will update the minimap when blocks are added/removed and has two levels of 'depth' but can handle more easily
* basic hover to expand behaviour but really roughly styled
* No smarts as to finding the 'icon' but I think that the current DOM structure does not make this easy
* Added some code to auto-inject an id if one is not on a header (might be a better way)
* Scroll into view is basic, does not consider sticky header offset
* No unit tests and typing fails for some code
* There may be a better way :) 
* Would be very easy to add other 'header' elements to be listened to or even non-headers as opt in via data attributes

## Potential issue with block headers

I noticed that the `h3` inside blocks is always empty, I think this may be an accessibility bug but not sure, let me know if this should be raised.

```
                <h3 data-block-title class="c-sf-block__header__title"></h3>
```

https://github.com/wagtail/wagtail/blob/cb43536f07f0a43412b3ff1c8c0d8ed61af5b3db/client/src/components/StreamField/blocks/BaseSequenceBlock.js#L149



Update - I pushed some changes to the branch mentioned above today - behaviour is much smoother and also was able to work around the smooth scroll and scrolling to content under the header easily with come CSS (instead of more JS).

Resolving patch:
diff --git a/wagtail/admin/views/home.py b/wagtail/admin/views/home.py
--- a/wagtail/admin/views/home.py
+++ b/wagtail/admin/views/home.py
@@ -329,7 +329,11 @@ def icons():
         )
         combined_icon_markup = ""
         for icon in all_icons:
-            combined_icon_markup += render_to_string(icon).replace("svg", "symbol")
+            combined_icon_markup += (
+                render_to_string(icon)
+                .replace('xmlns="http://www.w3.org/2000/svg"', "")
+                .replace("svg", "symbol")
+            )
 
         _full_sprite_html = render_to_string(
             "wagtailadmin/shared/icons.html", {"icons": combined_icon_markup}


Test patch: