Skip to content

Table of Content Component

Gabriel Walt edited this page Sep 10, 2021 · 34 revisions

Component that renders a table of content to help navigating the page content.

The implementation of this feature is tracked by issue #1816.
For discussing this spec, please use the discussion thread #1822.

User Story

As an author, I want to insert a table of content to my page, so that visitors can easily navigate to the titles that are within the page.

Requirements

  • Lists all H1-H6 elements that are rendered on the page, regardless of which component renders them (core title or teaser components, custom components, RTE, etc).
  • Lists also those titles that are coming from the page template or from (nested) experience fragments.
  • Links to the corresponding component on the page by using the id attribute that is set on the closest parent from the title element.
  • Creates a nested list of the titles found on the page, rendering them either with UL/LI or OL/LI elements.
  • Authoring capabilities:
    • Capability to choose how to render the list (unordered list - the default, or ordered list).
    • Capability to restrict the start level (default being 1) and the end level (default being 6) of the titles listed.
    • Capability to restrict which title elements are to be listed based on their CSS class names, or of the class name of the elements containing them.
  • The component must not impact page speed in any substantial ways:
    • It must not rely on JavaScript that might slow down the page in any way.
    • It must avoid any cumulative layout shifts that could be introduced by client-side rendering.
    • It must not slow down the first byte sent to the browser.
    • Any HTML rewriter must not block the output, or at least not until the placeholder of the TOC component is reached on the page.

Known Limitations

  • At least in the initial version, container components like Carousel, Tabs and Accordion will not yet be able to automatically navigate to the title items that they might contain when the visitor wants to navigate to them.

Dialog

Edit Dialog

  • "List type" label, with info popup "Whether to display the table of content as an unordered list of bullet points, or as an ordered listed of numbered items".
    Drop-down with options "unordered list" and "ordered list", defaulting to "unordered list".
    Option is hidden when the corresponding design configuration is not set to "no restriction".
  • "Title start level" label, with info popup "The minimum title level to report in the table of content.". "
    Drop-down with options "1"-"6", defaulting to "1".
    Option is hidden when the corresponding design configuration is not set to "no restriction".
  • "Title stop level" label, with info popup "The maximum title level to report in the table of content.". "
    Drop-down with options "1"-"6", defaulting to "6".
    Option is hidden when corresponding design configuration is not set to "no restriction".
  • "ID" label, with info popup "HTML ID attribute to apply to the component.".
    Text field that works as for other components that have an ID property.

Design Dialog

  • "Settings" tab
    • "Restrict list type" label, with info popup "Whether the author should be able to choose the list type or not."
      Drop-down with options "no restriction", "unordered list" or "ordered list", defaulting to "no restriction".
    • "Restrict title start level" label, with info popup "Whether the author should be able to choose the the minimum title level to report in the table of content."
      Drop-down with options "no restriction" and "1"-"6", defaulting to "no restriction".
    • "Restrict title stop level" label, with info popup "Whether the author should be able to choose the the maximum title level to report in the table of content."\ Drop-down with options "no restriction", 1-6, defaulting to "no restriction".
    • "Include class names" label, with info popup "If set, only titles with those class names, or contained within elements of the indicated class names will be considered."
      Multi-list of text inputs.
    • "Ignore class names" label, with info popup "If set, titles with those class names, or contained within elements of the indicated class names will be ignored."
      Multi-list of text inputs.
  • "Styles" tab with options as for any other core component.

Model JSON

The JSON model simply outputs the configuration properties as it cannot know how the titles have been rendered:

  • :type: String representing the resource type of the component.
  • id: String representing the HTML ID of the component.
  • listType: String representing the list type ("ul" or "ol") configured in the design dialog, or in the edit dialog, or default to "ul".
  • startLevel: Number representing the start level configured in the design dialog, or in the edit dialog, or default to 1.
  • stopLevel: Number representing the stop level configured in the design dialog, or in the edit dialog, or default to 6.
  • includeClasses: Array of strings representing the configured class names to include. If none configured, don't output this property in the JSON.
  • ignoreClasses: Array of strings representing the configured class names to ignore. If none configured, don't output this property in the JSON.

Include & Ignore Behavior

Here are some examples for the behavior of the include and ignore class names properties that can be set in the design-dialog.

Let's assume the following markup:

<h1 class="cmp-title">Page title</h1>
<div class="main">
    <div class="cmp-title">
        <h2>Main title</h2>
    </div>
    <div class="cmp-teaser">
        <h2 class="cmp-teaser__title">Main teaser</h2>
        <h3 class="cmp-teaser__subtitle">Main teaser subtitle</h3>
    </div>
</div>
<div class="sidebar">
    <div class="cmp-title">
        <h2>Sidebar title</h2>
    </div>
    <div class="cmp-teaser">
        <h2 class="cmp-teaser__title">Sidebar teaser</h2>
        <h3 class="cmp-teaser__subtitle">Sidebar teaser subtitle</h3>
    </div>
</div>

Below are different configurations, and how each should render the table of content for the above markup example.

1. Include=none and Ignore=none:

* Page title
    * Main title
    * Main teaser
        * Main teaser subtitle
    * Sidebar title
    * Sidebar teaser
        * Sidebar teaser subtitle

2. Include=none and Ignore="sidebar":

* Page title
    * Main title
    * Main teaser
        * Main teaser subtitle

3. Include="cmp-title" and Ignore=none:

* Page title
    * Main title
    * Sidebar title

4. Include="cmp-title, cmp-teaser__title" and Ignore=none:

* Page title
    * Main title
    * Main teaser
    * Sidebar title
    * Sidebar teaser

5. Include="cmp-title" and Ignore="sidebar":

* Page title
    * Main title

6. Include="cmp-title, cmp-teaser" and Ignore="sidebar, cmp-teaser__subtitle":

* Page title
    * Main title
    * Main teaser

7. Include="cmp-title, main" and Ignore="cmp-teaser__subtitle":

* Page title
    * Main title
    * Main teaser
    * Sidebar title

Nesting Behavior

Displaying the titles in a nested list can be tricky when authors jump some title levels in their content as it is not desired to have a table of content with empty levels displayed. To correctly display nesting, it helps to think of nesting as a parent to child relationship: an item should only be nested when it also has a corresponding parent. So when an H1 is followed by an H3, then the hierarchy built by the table of content should consider the H3 as a direct child of the H1 component (without any intermediate node displayed). Below are listed a few examples that try to push this logic to its limit, illustrating what the resulting table of content should be. Those can potentially serve as test cases to verify the nesting logic.

1. Simple example:

h1. Bla  |  * Bla
h2. Bli  |      * Bli
h3. Blo  |          * Blo
h4. Blu  |              * Blu

2. Simple example:

h1. Bla  |  * Bla
h2. Bli  |      * Bli
h1. Blo  |  * Blo
h2. Blu  |      * Blu

3. Ignore unused levels in-between:

h1. Bla  |  * Bla
h3. Bli  |      * Bli
h5. Blo  |          * Blo
h6. Blu  |              * Blu

4. Ignore unused levels at start:

h3. Bla  |  * Bla
h4. Bli  |      * Bli
h3. Blo  |  * Blo
h4. Blu  |      * Blu

5. Don't nest items without a parent:

h4. Bla  |  * Bla
h3. Bli  |  * Bli
h2. Blo  |  * Blo
h4. Blu  |      * Blu

6. Don't double-nest child items:

h1. Bla  |  * Bla
h3. Bli  |      * Bli
h2. Blo  |      * Blo
h3. Blu  |          * Blu

7. Combining all complexities:

h3. Bla  |  * Bla
h6. Bli  |      * Bli
h5. Blo  |      * Blo
h2. Blu  |  * Blu
h3. Ble  |      * Ble