From b611ee1976a7d06a0b7f3f97cae17dc26f216a07 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Fri, 21 Aug 2026 11:11:08 +0800 Subject: [PATCH] MDL-75699 [docs] Document modal dialogue heading structure Modal dialogue titles are now rendered as an h2 rather than an h5, so that opening a dialogue no longer breaks the page's heading hierarchy. Add a "Heading structure" section to the Modal Dialogues guide covering the title element and why it is fixed at level 2, nesting body headings from h3, sizing headings with utility classes rather than by element, overriding the header block, and guarding the structure with an accessibility Behat scenario. The change is backported, so the section is added to the 4.5, 5.1 and 5.2 versioned copies too. The 4.5 copy documents the Bootstrap 4 `h5` utility class instead of `fs-5`, and 4.5 and 5.1 omit the modulename_help example, which only applies from 5.2. Also add a Moodle 5.3 developer update entry. Co-Authored-By: Claude Opus 5 --- docs/devupdate.md | 18 ++++ docs/guides/javascript/modal/index.md | 88 +++++++++++++++++++ .../guides/javascript/modal/index.md | 88 +++++++++++++++++++ .../guides/javascript/modal/index.md | 88 +++++++++++++++++++ .../guides/javascript/modal/index.md | 88 +++++++++++++++++++ 5 files changed, 370 insertions(+) diff --git a/docs/devupdate.md b/docs/devupdate.md index 4b3df850c..33dd781f4 100644 --- a/docs/devupdate.md +++ b/docs/devupdate.md @@ -28,3 +28,21 @@ The `moodle_page` class now includes `set_supplementary_content()` and `get_supp For instance, `mod_forum` uses this new mechanism to display a "Go to all discussions" link in the sticky footer when viewing an individual discussion. For more information, see the [Adding supplementary content to the sticky footer](./apis/plugintypes/format/linear_navigation.md#adding-supplementary-content-to-the-sticky-footer) section. + +## Modal dialogue titles are now `

` elements + + + +The title rendered by the `core/modal` template is now an `
`, so that opening a dialogue no longer breaks the page's heading hierarchy for assistive technology users. The title's appearance is unchanged, because its size is now set by the `fs-5` utility class rather than by the element. + +If your plugin renders headings inside dialogue content, they must be nested beneath this `

`, so the first level available to you is `

`. Headings that previously nested beneath the old `

` will now skip levels. This includes content that is rendered into a dialogue without being authored as part of one -- for example, the Markdown headings in an activity module's `modulename_help` string, which core has lowered from `######` to `####` for this reason. + +If your plugin renders its own modal header markup, or overrides the `header` block of the `core/modal` template, apply the same `
+``` + +The level is deliberately fixed at `

`. A dialogue can be opened from anywhere in a page, so it cannot know what the surrounding heading structure is, and the page behind it already provides the single `

`. Placing the title at level 2 keeps it directly beneath that `

` wherever the dialogue is opened from. + +Its appearance comes from the `fs-5` [Bootstrap font size utility class](https://getbootstrap.com/docs/5.3/utilities/text/#font-size), not from the element. Never change the heading element to make a title look bigger or smaller -- change the utility class. + +:::info[Why `

` and not `

`?] + +[Bootstrap's own documentation](https://getbootstrap.com/docs/5.3/components/modal/) uses an `

` for the modal title, on the basis that a dialogue is its own document context. Moodle uses an `

` instead because the page behind the dialogue already has an `

`, and accessibility auditing tools commonly report a second `

` as a failure. + +::: + +### Headings within a dialogue {/* #headings-within-a-dialogue */} + +Because the title is an `

`, the first heading level available to you inside the body or footer of a dialogue is `

`: + +```mustache title="mod/example/templates/my_modal.mustache" +{{< core/modal }} + {{$title}}{{#str}} pluginname, mod_example {{/str}}{{/title}} + {{$body}} +

{{#str}} settings, mod_example {{/str}}

+ {{! ... }} +

{{#str}} advancedsettings, mod_example {{/str}}

+ {{! ... }} + {{/body}} +{{/ core/modal }} +``` + +Skipping a level -- going straight from the `

` title to an `

`, for example -- breaks the heading hierarchy that assistive technology users rely on to navigate the dialogue. See the [heading requirements](/general/development/process/peer-review/accessibility-checklist#page-headers-and-title) in the accessibility peer review checklist. + +This applies to content that is rendered *into* a dialogue as much as to the dialogue's own template. If a language string, filter, or renderer emits headings, and that output can be displayed in a dialogue, its heading levels must fit beneath the `

` title too. The `modulename_help` strings shown by the activity chooser are one example: their Markdown headings start at `####` (`

`) so that they nest correctly under the activity name. + +:::tip[Sizing nested headings] + +If a nested heading needs to look smaller than its level implies, apply an `fs-*` utility class, or set the size in your theme's SCSS. Choose the heading element for its meaning and the class for its appearance. + +::: + +### Overriding the header {/* #overriding-the-header */} + +The `core/modal` template exposes a `header` block, which replaces the whole `modal-header` region including the title element. If you override it, you must render your own heading and it must keep the same level, id, and `modal-title` class, otherwise the dialogue loses the accessible name that `aria-labelledby` points at: + +```mustache title="Overriding the header block" +{{< core/modal }} + {{$header}} +

+ {{! Any additional header content. }} + {{/header}} +{{/ core/modal }} +``` + +In most cases you should override the `title` block instead, and leave the heading itself to `core/modal`. + +:::note[Dialogues that are not built with `core/modal`] + +Some dialogue-like components render their own `modal-header` markup rather than extending `core/modal` -- `tool_usertours` tour steps are one example in core. These follow the same rule: the title is an `

`. + +::: + +### Testing the heading structure {/* #testing-the-heading-structure */} + +The `best-practice` axe ruleset checks heading order, so an `@accessibility` Behat scenario is the simplest way to guard the structure of a dialogue: + +```gherkin +@accessibility +Scenario: The example dialogue has a valid heading structure + Given I open the example dialogue + Then the "Example dialogue" "dialogue" should meet accessibility standards with "best-practice" extra tests +``` + +Assert on the semantics rather than the presentation. `"h2.modal-title" "css_element"` is a stable assertion; including the `fs-5` utility class in the selector is not, because the class that sets the title's size may change. + +See [Accessibility testing](/general/development/policies/accessibility/testing) for more on writing accessibility tests. + +:::note[Earlier releases] + +Before [MDL-75699](https://tracker.moodle.org/browse/MDL-75699) the dialogue title was an `

`, and headings inside a dialogue were expected to nest beneath that. If you are writing code that must also run on releases from before that fix, be aware that the same markup produces a different heading hierarchy there. + +::: + ## Creating a custom modal type {/* #creating-a-custom-modal-type */} In some situations it is desirable to write a brand new modal. diff --git a/versioned_docs/version-4.5/guides/javascript/modal/index.md b/versioned_docs/version-4.5/guides/javascript/modal/index.md index b79eaf999..4ad4221d0 100644 --- a/versioned_docs/version-4.5/guides/javascript/modal/index.md +++ b/versioned_docs/version-4.5/guides/javascript/modal/index.md @@ -218,6 +218,94 @@ export const init = async () => { +## Heading structure {/* #heading-structure */} + +The `core/modal` template renders the dialogue title as a level 2 heading: + +```mustache title="The title element in core/modal" +
+``` + +The level is deliberately fixed at `

`. A dialogue can be opened from anywhere in a page, so it cannot know what the surrounding heading structure is, and the page behind it already provides the single `

`. Placing the title at level 2 keeps it directly beneath that `

` wherever the dialogue is opened from. + +Its appearance comes from the `h5` [Bootstrap typography utility class](https://getbootstrap.com/docs/4.6/content/typography/), not from the element. Never change the heading element to make a title look bigger or smaller -- change the utility class. + +:::info[Why `

` and not `

`?] + +[Bootstrap 4's own example](https://getbootstrap.com/docs/4.6/components/modal/) uses an `

` for the modal title, which is where Moodle's original markup came from -- it picks the element for its size rather than for its place in the document. Bootstrap 5 corrected this to an `

`, on the basis that a dialogue is its own document context. Moodle uses an `

` instead, because the page behind the dialogue already has an `

`, and accessibility auditing tools commonly report a second `

` as a failure. + +::: + +### Headings within a dialogue {/* #headings-within-a-dialogue */} + +Because the title is an `

`, the first heading level available to you inside the body or footer of a dialogue is `

`: + +```mustache title="mod/example/templates/my_modal.mustache" +{{< core/modal }} + {{$title}}{{#str}} pluginname, mod_example {{/str}}{{/title}} + {{$body}} +

{{#str}} settings, mod_example {{/str}}

+ {{! ... }} +

{{#str}} advancedsettings, mod_example {{/str}}

+ {{! ... }} + {{/body}} +{{/ core/modal }} +``` + +Skipping a level -- going straight from the `

` title to an `

`, for example -- breaks the heading hierarchy that assistive technology users rely on to navigate the dialogue. See the [heading requirements](/general/development/process/peer-review/accessibility-checklist#page-headers-and-title) in the accessibility peer review checklist. + +This applies to content that is rendered *into* a dialogue as much as to the dialogue's own template. If a language string, filter, or renderer emits headings, and that output can be displayed in a dialogue, its heading levels must fit beneath the `

` title too. + +:::tip[Sizing nested headings] + +If a nested heading needs to look smaller than its level implies, apply a heading utility class such as `h6`, or set the size in your theme's SCSS. Choose the heading element for its meaning and the class for its appearance. + +::: + +### Overriding the header {/* #overriding-the-header */} + +The `core/modal` template exposes a `header` block, which replaces the whole `modal-header` region including the title element. If you override it, you must render your own heading and it must keep the same level, id, and `modal-title` class, otherwise the dialogue loses the accessible name that `aria-labelledby` points at: + +```mustache title="Overriding the header block" +{{< core/modal }} + {{$header}} +

+ {{! Any additional header content. }} + {{/header}} +{{/ core/modal }} +``` + +In most cases you should override the `title` block instead, and leave the heading itself to `core/modal`. + +:::note[Dialogues that are not built with `core/modal`] + +Some dialogue-like components render their own `modal-header` markup rather than extending `core/modal` -- `tool_usertours` tour steps are one example in core. These follow the same rule: the title is an `

`. + +::: + +### Testing the heading structure {/* #testing-the-heading-structure */} + +The `best-practice` axe ruleset checks heading order, so an `@accessibility` Behat scenario is the simplest way to guard the structure of a dialogue: + +```gherkin +@accessibility +Scenario: The example dialogue has a valid heading structure + Given I open the example dialogue + Then the "Example dialogue" "dialogue" should meet accessibility standards with "best-practice" extra tests +``` + +Assert on the semantics rather than the presentation. `"h2.modal-title" "css_element"` is a stable assertion; including the `h5` utility class in the selector is not, because the class that sets the title's size may change. + +See [Accessibility testing](/general/development/policies/accessibility/testing) for more on writing accessibility tests. + +:::note[Earlier releases] + +Before [MDL-75699](https://tracker.moodle.org/browse/MDL-75699) the dialogue title was an `

`, and headings inside a dialogue were expected to nest beneath that. If you are writing code that must also run on releases from before that fix, be aware that the same markup produces a different heading hierarchy there. + +::: + ## Creating a custom modal type {/* #creating-a-custom-modal-type */} In some situations it is desirable to write a brand new modal. diff --git a/versioned_docs/version-5.1/guides/javascript/modal/index.md b/versioned_docs/version-5.1/guides/javascript/modal/index.md index b79eaf999..433f4a1e6 100644 --- a/versioned_docs/version-5.1/guides/javascript/modal/index.md +++ b/versioned_docs/version-5.1/guides/javascript/modal/index.md @@ -218,6 +218,94 @@ export const init = async () => { +## Heading structure {/* #heading-structure */} + +The `core/modal` template renders the dialogue title as a level 2 heading: + +```mustache title="The title element in core/modal" +
+``` + +The level is deliberately fixed at `

`. A dialogue can be opened from anywhere in a page, so it cannot know what the surrounding heading structure is, and the page behind it already provides the single `

`. Placing the title at level 2 keeps it directly beneath that `

` wherever the dialogue is opened from. + +Its appearance comes from the `fs-5` [Bootstrap font size utility class](https://getbootstrap.com/docs/5.3/utilities/text/#font-size), not from the element. Never change the heading element to make a title look bigger or smaller -- change the utility class. + +:::info[Why `

` and not `

`?] + +[Bootstrap's own documentation](https://getbootstrap.com/docs/5.3/components/modal/) uses an `

` for the modal title, on the basis that a dialogue is its own document context. Moodle uses an `

` instead because the page behind the dialogue already has an `

`, and accessibility auditing tools commonly report a second `

` as a failure. + +::: + +### Headings within a dialogue {/* #headings-within-a-dialogue */} + +Because the title is an `

`, the first heading level available to you inside the body or footer of a dialogue is `

`: + +```mustache title="mod/example/templates/my_modal.mustache" +{{< core/modal }} + {{$title}}{{#str}} pluginname, mod_example {{/str}}{{/title}} + {{$body}} +

{{#str}} settings, mod_example {{/str}}

+ {{! ... }} +

{{#str}} advancedsettings, mod_example {{/str}}

+ {{! ... }} + {{/body}} +{{/ core/modal }} +``` + +Skipping a level -- going straight from the `

` title to an `

`, for example -- breaks the heading hierarchy that assistive technology users rely on to navigate the dialogue. See the [heading requirements](/general/development/process/peer-review/accessibility-checklist#page-headers-and-title) in the accessibility peer review checklist. + +This applies to content that is rendered *into* a dialogue as much as to the dialogue's own template. If a language string, filter, or renderer emits headings, and that output can be displayed in a dialogue, its heading levels must fit beneath the `

` title too. + +:::tip[Sizing nested headings] + +If a nested heading needs to look smaller than its level implies, apply an `fs-*` utility class, or set the size in your theme's SCSS. Choose the heading element for its meaning and the class for its appearance. + +::: + +### Overriding the header {/* #overriding-the-header */} + +The `core/modal` template exposes a `header` block, which replaces the whole `modal-header` region including the title element. If you override it, you must render your own heading and it must keep the same level, id, and `modal-title` class, otherwise the dialogue loses the accessible name that `aria-labelledby` points at: + +```mustache title="Overriding the header block" +{{< core/modal }} + {{$header}} +

+ {{! Any additional header content. }} + {{/header}} +{{/ core/modal }} +``` + +In most cases you should override the `title` block instead, and leave the heading itself to `core/modal`. + +:::note[Dialogues that are not built with `core/modal`] + +Some dialogue-like components render their own `modal-header` markup rather than extending `core/modal` -- `tool_usertours` tour steps are one example in core. These follow the same rule: the title is an `

`. + +::: + +### Testing the heading structure {/* #testing-the-heading-structure */} + +The `best-practice` axe ruleset checks heading order, so an `@accessibility` Behat scenario is the simplest way to guard the structure of a dialogue: + +```gherkin +@accessibility +Scenario: The example dialogue has a valid heading structure + Given I open the example dialogue + Then the "Example dialogue" "dialogue" should meet accessibility standards with "best-practice" extra tests +``` + +Assert on the semantics rather than the presentation. `"h2.modal-title" "css_element"` is a stable assertion; including the `fs-5` utility class in the selector is not, because the class that sets the title's size may change. + +See [Accessibility testing](/general/development/policies/accessibility/testing) for more on writing accessibility tests. + +:::note[Earlier releases] + +Before [MDL-75699](https://tracker.moodle.org/browse/MDL-75699) the dialogue title was an `

`, and headings inside a dialogue were expected to nest beneath that. If you are writing code that must also run on releases from before that fix, be aware that the same markup produces a different heading hierarchy there. + +::: + ## Creating a custom modal type {/* #creating-a-custom-modal-type */} In some situations it is desirable to write a brand new modal. diff --git a/versioned_docs/version-5.2/guides/javascript/modal/index.md b/versioned_docs/version-5.2/guides/javascript/modal/index.md index b79eaf999..30ee7b4e4 100644 --- a/versioned_docs/version-5.2/guides/javascript/modal/index.md +++ b/versioned_docs/version-5.2/guides/javascript/modal/index.md @@ -218,6 +218,94 @@ export const init = async () => { +## Heading structure {/* #heading-structure */} + +The `core/modal` template renders the dialogue title as a level 2 heading: + +```mustache title="The title element in core/modal" +
+``` + +The level is deliberately fixed at `

`. A dialogue can be opened from anywhere in a page, so it cannot know what the surrounding heading structure is, and the page behind it already provides the single `

`. Placing the title at level 2 keeps it directly beneath that `

` wherever the dialogue is opened from. + +Its appearance comes from the `fs-5` [Bootstrap font size utility class](https://getbootstrap.com/docs/5.3/utilities/text/#font-size), not from the element. Never change the heading element to make a title look bigger or smaller -- change the utility class. + +:::info[Why `

` and not `

`?] + +[Bootstrap's own documentation](https://getbootstrap.com/docs/5.3/components/modal/) uses an `

` for the modal title, on the basis that a dialogue is its own document context. Moodle uses an `

` instead because the page behind the dialogue already has an `

`, and accessibility auditing tools commonly report a second `

` as a failure. + +::: + +### Headings within a dialogue {/* #headings-within-a-dialogue */} + +Because the title is an `

`, the first heading level available to you inside the body or footer of a dialogue is `

`: + +```mustache title="mod/example/templates/my_modal.mustache" +{{< core/modal }} + {{$title}}{{#str}} pluginname, mod_example {{/str}}{{/title}} + {{$body}} +

{{#str}} settings, mod_example {{/str}}

+ {{! ... }} +

{{#str}} advancedsettings, mod_example {{/str}}

+ {{! ... }} + {{/body}} +{{/ core/modal }} +``` + +Skipping a level -- going straight from the `

` title to an `

`, for example -- breaks the heading hierarchy that assistive technology users rely on to navigate the dialogue. See the [heading requirements](/general/development/process/peer-review/accessibility-checklist#page-headers-and-title) in the accessibility peer review checklist. + +This applies to content that is rendered *into* a dialogue as much as to the dialogue's own template. If a language string, filter, or renderer emits headings, and that output can be displayed in a dialogue, its heading levels must fit beneath the `

` title too. The `modulename_help` strings shown by the activity chooser are one example: their Markdown headings start at `####` (`

`) so that they nest correctly under the activity name. + +:::tip[Sizing nested headings] + +If a nested heading needs to look smaller than its level implies, apply an `fs-*` utility class, or set the size in your theme's SCSS. Choose the heading element for its meaning and the class for its appearance. + +::: + +### Overriding the header {/* #overriding-the-header */} + +The `core/modal` template exposes a `header` block, which replaces the whole `modal-header` region including the title element. If you override it, you must render your own heading and it must keep the same level, id, and `modal-title` class, otherwise the dialogue loses the accessible name that `aria-labelledby` points at: + +```mustache title="Overriding the header block" +{{< core/modal }} + {{$header}} +

+ {{! Any additional header content. }} + {{/header}} +{{/ core/modal }} +``` + +In most cases you should override the `title` block instead, and leave the heading itself to `core/modal`. + +:::note[Dialogues that are not built with `core/modal`] + +Some dialogue-like components render their own `modal-header` markup rather than extending `core/modal` -- `tool_usertours` tour steps are one example in core. These follow the same rule: the title is an `

`. + +::: + +### Testing the heading structure {/* #testing-the-heading-structure */} + +The `best-practice` axe ruleset checks heading order, so an `@accessibility` Behat scenario is the simplest way to guard the structure of a dialogue: + +```gherkin +@accessibility +Scenario: The example dialogue has a valid heading structure + Given I open the example dialogue + Then the "Example dialogue" "dialogue" should meet accessibility standards with "best-practice" extra tests +``` + +Assert on the semantics rather than the presentation. `"h2.modal-title" "css_element"` is a stable assertion; including the `fs-5` utility class in the selector is not, because the class that sets the title's size may change. + +See [Accessibility testing](/general/development/policies/accessibility/testing) for more on writing accessibility tests. + +:::note[Earlier releases] + +Before [MDL-75699](https://tracker.moodle.org/browse/MDL-75699) the dialogue title was an `

`, and headings inside a dialogue were expected to nest beneath that. If you are writing code that must also run on releases from before that fix, be aware that the same markup produces a different heading hierarchy there. + +::: + ## Creating a custom modal type {/* #creating-a-custom-modal-type */} In some situations it is desirable to write a brand new modal.