diff --git a/.upgradenotes/MDL-83869-2025011609294854.yml b/.upgradenotes/MDL-83869-2025011609294854.yml
new file mode 100644
index 0000000000000..a704c3768a913
--- /dev/null
+++ b/.upgradenotes/MDL-83869-2025011609294854.yml
@@ -0,0 +1,9 @@
+issueNumber: MDL-83869
+notes:
+ core:
+ - message: >
+ New generic collapsable section output added. Use
+ core\output\local\collapsable_section or include the
+ core/local/collapsable_section template to use it. See the full
+ documentation in the component library.
+ type: improved
diff --git a/admin/tool/componentlibrary/content/moodle/components/collapsable-sections.md b/admin/tool/componentlibrary/content/moodle/components/collapsable-sections.md
new file mode 100644
index 0000000000000..655d034177a4a
--- /dev/null
+++ b/admin/tool/componentlibrary/content/moodle/components/collapsable-sections.md
@@ -0,0 +1,158 @@
+---
+layout: docs
+title: "Collapsable sections"
+description: "A reusable collapsable section component"
+date: 2024-12-20T10:10:00+08:00
+draft: false
+tags:
+- MDL-83869
+- "5.0"
+---
+
+## How it works
+
+The collapsable section component in Moodle allows you to create sections of content that can be expanded or collapsed by the user. This is useful for organizing content in a way that doesn't overwhelm the user with too much information at once. The component is built using a combination of PHP, Mustache templates, and JavaScript.
+
+## Source files
+
+- `lib/templates/local/collapsable_section.mustache`: The Mustache template for rendering the collapsable section.
+- `lib/classes/output/local/collapsable_section.php`: The output class for the collapsable section component.
+- `lib/amd/src/local/collapsable_section/events.js`: JavaScript module for handling events related to the collapsable section.
+- `lib/amd/src/local/collapsable_section/controls.js`: JavaScript module for controlling the collapsable section.
+
+## Usage
+
+To use the collapsable section component, you need to create an instance of the `collapsable_section` class and render it using Moodle's output renderer. You can customize the title content, section content, CSS classes, and additional HTML attributes.
+
+Example:
+
+{{< php >}}
+use core\output\local\collapsable_section;
+
+// Create an instance of the collapsable section.
+$section = new collapsable_section(
+ titlecontent: 'Section Title',
+ sectioncontent: 'This is the content of the section.',
+);
+
+echo $OUTPUT->render($section);
+{{< / php >}}
+
+{{< mustache template="core/local/collapsable_section" >}}
+ {
+ "titlecontent": "Section Title",
+ "sectioncontent": "This is the content of the section."
+ }
+{{< /mustache >}}
+
+You can also add CSS classes, extra HTML attributes, and customize the expand and collapse labels of the collapsable section:
+
+{{< php >}}
+$section = new collapsable_section(
+ titlecontent: 'Section Title',
+ sectioncontent: 'This is the content of the section.',
+ open: true, // Optional parameter to set the section as open by default.
+ classes: 'p-3 rounded bg-dark text-white', // Optional parameter to add custom CSS classes.
+ extras: ['id' => 'MyCollapsableSection', 'data-foo' => 'bar'], // Optional HTML attributes.
+ expandlabel: 'Show more', // Optional label for the expand button.
+ collapselabel: 'Show less', // Optional label for the collapse button.
+);
+
+echo $OUTPUT->render($section);
+{{< / php >}}
+
+{{< mustache template="core/local/collapsable_section" >}}
+ {
+ "titlecontent": "Section Title",
+ "sectioncontent": "This is the content of the section.",
+ "open": true,
+ "classes": "p-3 rounded bg-dark text-white",
+ "elementid": "someuniqueid",
+ "extras": [
+ {
+ "attribute": "id",
+ "value": "MyCollapsableSection"
+ },
+ {
+ "attribute": "data-foo",
+ "value": "bar"
+ }
+ ],
+ "expandlabel": "Show more",
+ "collapselabel": "Show less"
+ }
+{{< /mustache >}}
+
+## Include a collapsable section from a mustache template
+
+Collapsable sections can also be included from a Mustache template using the `core/local/collapsable_section` template. This template allows you to define the title content and section content within the template.
+
+{{< mustache template="tool_componentlibrary/examples/collapsablesections/includesection" >}}
+ {
+ }
+{{< /mustache >}}
+
+## JavaScript
+
+### Control a section
+
+The collapsable sections component includes a JavaScript module for controlling the sections. This module provides methods to hide, show, and toggle the visibility of the sections.
+
+To use the JavaScript controls, you need to import the `CollapsableSection` module and create an instance from a selector:
+
+```javascript
+import CollapsableSection from 'core/local/collapsable_section/controls';
+
+const section = CollapsableSection.instanceFromSelector('#MyCollapsableSection');
+
+// Use hide, show, and toggle methods to control the section.
+section.hide();
+section.show();
+section.toggle();
+```
+
+### Get the state of a section
+
+You can also check the state of a section using the `isHidden` method:
+
+```javascript
+import CollapsableSection from 'core/local/collapsable_section/controls';
+
+const section = CollapsableSection.instanceFromSelector('#MyCollapsableSection');
+
+if (section.isVisible()) {
+ console.log('The section is hidden.');
+} else {
+ console.log('The section is visible.');
+}
+```
+
+### Events
+
+The collapsable sections component also includes a JavaScript module for handling events. This module wraps the standard Bootstrap collapsable events and provides custom event types for collapsable sections.
+
+The component triggers two main events:
+
+- `core_collapsable_section_shown`: when the collapsed content is shown.
+- `core_collapsable_section_hidden`: when the collapsed content is hidden.
+
+For convenience, the `core/local/collapsable_section/events` also list the original Bootstrap events. They should not be needed in most cases, but they are available if you need them:
+
+- `show.bs.collapse`: when the collapse is starting to show.
+- `shown.bs.collapse`: when the collapse has been shown.
+- `hide.bs.collapse`: when the collapse is starting to hide.
+- `hidden.bs.collapse`: when the collapse has been hidden.
+
+To listen for events related to the collapsable sections, you need to import the `eventTypes` from the `events` module and add event listeners:
+
+```javascript
+import {eventTypes as collapsableSectionEventTypes} from 'core/local/collapsable_section/events';
+
+document.addEventListener(collapsableSectionEventTypes.shown, event => {
+ console.log(event.target); // The HTMLElement relating to the section that was shown.
+});
+
+document.addEventListener(collapsableSectionEventTypes.hidden, event => {
+ console.log(event.target); // The HTMLElement relating to the section that was hidden.
+});
+```
diff --git a/admin/tool/componentlibrary/templates/examples/collapsablesections/includesection.mustache b/admin/tool/componentlibrary/templates/examples/collapsablesections/includesection.mustache
new file mode 100644
index 0000000000000..5f6da08b58225
--- /dev/null
+++ b/admin/tool/componentlibrary/templates/examples/collapsablesections/includesection.mustache
@@ -0,0 +1,35 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template tool_componentlibrary/examples/collapsablesections/includesection
+
+ TODO describe template includesection
+
+ Example context (json):
+ {
+ }
+}}
+{{
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ *
+ * @example
Example of controlling a collapsable section.
+ *
+ * import CollapsableSection from 'core/local/collapsable_section/controls';
+ *
+ * const section = CollapsableSection.instanceFromSelector('#MyCollapsableSection');
+ *
+ * // Use hide, show and toggle methods to control the section.
+ * section.hide();
+ */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};let initialized=!1;return _exports.default=class{static instanceFromSelector(selector){const elements=document.querySelector(selector);if(!elements)throw new Error("No elements found with the selector: "+selector);return new this(elements)}static init(){initialized||(initialized=!0,(0,_jquery.default)(document).on(_events.eventTypes.hiddenBsCollapse,(event=>{this.isCollapsableComponent(event.target)&&(0,_events.notifyCollapsableSectionHidden)(event.target)})),(0,_jquery.default)(document).on(_events.eventTypes.shownBsCollapse,(event=>{this.isCollapsableComponent(event.target)&&(0,_events.notifyCollapsableSectionShown)(event.target)})))}static isCollapsableComponent(element){return element.hasAttribute("data-mdl-component")&&"core/local/collapsable_section"===element.getAttribute("data-mdl-component")}constructor(element){this.element=element}hide(){(0,_jquery.default)(this.element).collapse("hide")}show(){(0,_jquery.default)(this.element).collapse("show")}toggle(){(0,_jquery.default)(this.element).collapse("toggle")}isVisible(){return this.element.classList.contains("show")}},_exports.default}));
+
+//# sourceMappingURL=controls.min.js.map
\ No newline at end of file
diff --git a/lib/amd/build/local/collapsable_section/controls.min.js.map b/lib/amd/build/local/collapsable_section/controls.min.js.map
new file mode 100644
index 0000000000000..2ec15aec9aa2e
--- /dev/null
+++ b/lib/amd/build/local/collapsable_section/controls.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"controls.min.js","sources":["../../../src/local/collapsable_section/controls.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * The collapsable sections controls.\n *\n * @module core/local/collapsable_section/controls\n * @copyright 2024 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * @example
Example of controlling a collapsable section.
\n *\n * import CollapsableSection from 'core/local/collapsable_section/controls';\n *\n * const section = CollapsableSection.instanceFromSelector('#MyCollapsableSection');\n *\n * // Use hide, show and toggle methods to control the section.\n * section.hide();\n */\n\nimport {\n eventTypes,\n notifyCollapsableSectionHidden,\n notifyCollapsableSectionShown\n} from 'core/local/collapsable_section/events';\n\n// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated.\nimport jQuery from 'jquery';\n\nlet initialized = false;\n\nexport default class {\n /**\n * Create a new instance from a query selector.\n *\n * @param {String} selector The selector of the collapsable section.\n * @return {CollapsableSection} The collapsable section controls.\n * @throws {Error} If no elements are found with the selector.\n */\n static instanceFromSelector(selector) {\n const elements = document.querySelector(selector);\n if (!elements) {\n throw new Error('No elements found with the selector: ' + selector);\n }\n return new this(elements);\n }\n\n /**\n * Initialize the collapsable section controls.\n */\n static init() {\n if (initialized) {\n return;\n }\n initialized = true;\n\n // We want to add extra events to the standard bootstrap collapsable events.\n // TODO: change all jquery events to custom events once MDL-71979 is integrated.\n jQuery(document).on(eventTypes.hiddenBsCollapse, event => {\n if (!this.isCollapsableComponent(event.target)) {\n return;\n }\n notifyCollapsableSectionHidden(event.target);\n });\n jQuery(document).on(eventTypes.shownBsCollapse, event => {\n if (!this.isCollapsableComponent(event.target)) {\n return;\n }\n notifyCollapsableSectionShown(event.target);\n });\n }\n\n /**\n * Check if the element is a collapsable section.\n *\n * @private\n * @param {HTMLElement} element The element to check.\n * @return {boolean} True if the element is a collapsable section.\n */\n static isCollapsableComponent(element) {\n return element.hasAttribute('data-mdl-component')\n && element.getAttribute('data-mdl-component') === 'core/local/collapsable_section';\n }\n\n /**\n * Creates an instance of the controls for a collapsable section.\n *\n * @param {HTMLElement} element - The DOM element that this control will manage.\n */\n constructor(element) {\n this.element = element;\n }\n\n /**\n * Hides the collapsible section element.\n */\n hide() {\n // TODO: change all jquery once MDL-71979 is integrated.\n jQuery(this.element).collapse('hide');\n }\n\n /**\n * Shows the collapsible section element.\n */\n show() {\n // TODO: change all jquery once MDL-71979 is integrated.\n jQuery(this.element).collapse('show');\n }\n\n /**\n * Toggle the collapsible section element.\n */\n toggle() {\n // TODO: change all jquery once MDL-71979 is integrated.\n jQuery(this.element).collapse('toggle');\n }\n\n /**\n * Check if the collapsable section is visible.\n *\n * @return {boolean} True if the collapsable section is visible.\n */\n isVisible() {\n return this.element.classList.contains('show');\n }\n}\n"],"names":["initialized","selector","elements","document","querySelector","Error","this","on","eventTypes","hiddenBsCollapse","event","isCollapsableComponent","target","shownBsCollapse","element","hasAttribute","getAttribute","constructor","hide","collapse","show","toggle","isVisible","classList","contains"],"mappings":";;;;;;;;;;;;;;;;iJAyCIA,aAAc,4DAUcC,gBAClBC,SAAWC,SAASC,cAAcH,cACnCC,eACK,IAAIG,MAAM,wCAA0CJ,iBAEvD,IAAIK,KAAKJ,wBAOZF,cAGJA,aAAc,sBAIPG,UAAUI,GAAGC,mBAAWC,kBAAkBC,QACxCJ,KAAKK,uBAAuBD,MAAME,oDAGRF,MAAME,+BAElCT,UAAUI,GAAGC,mBAAWK,iBAAiBH,QACvCJ,KAAKK,uBAAuBD,MAAME,mDAGTF,MAAME,0CAWdE,gBACnBA,QAAQC,aAAa,uBAC0B,mCAA/CD,QAAQE,aAAa,sBAQhCC,YAAYH,cACHA,QAAUA,QAMnBI,2BAEWZ,KAAKQ,SAASK,SAAS,QAMlCC,2BAEWd,KAAKQ,SAASK,SAAS,QAMlCE,6BAEWf,KAAKQ,SAASK,SAAS,UAQlCG,mBACWhB,KAAKQ,QAAQS,UAAUC,SAAS"}
\ No newline at end of file
diff --git a/lib/amd/build/local/collapsable_section/events.min.js b/lib/amd/build/local/collapsable_section/events.min.js
new file mode 100644
index 0000000000000..a6cda37c345a8
--- /dev/null
+++ b/lib/amd/build/local/collapsable_section/events.min.js
@@ -0,0 +1,20 @@
+define("core/local/collapsable_section/events",["exports","core/event_dispatcher"],(function(_exports,_event_dispatcher){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.notifyCollapsableSectionShown=_exports.notifyCollapsableSectionHidden=_exports.eventTypes=void 0;
+/**
+ * The collapsable section events.
+ *
+ * This module wraps the standard bootstrap collapsable events, but for collapsable sections.
+ *
+ * @module core/local/collapsable_section/events
+ * @copyright 2024 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ *
+ * @example
Example of listening to a collapsable section events.
+ * import {eventTypes as collapsableSectionEventTypes} from 'core/local/collapsable_section/events';
+ *
+ * document.addEventListener(collapsableSectionEventTypes.shown, event => {
+ * window.console.log(event.target); // The HTMLElement relating to the block whose content was updated.
+ * });
+ */
+const eventTypes={shown:"core_collapsable_section_shown",hidden:"core_collapsable_section_hidden",hideBsCollapse:"hide.bs.collapse",hiddenBsCollapse:"hidden.bs.collapse",showBsCollapse:"show.bs.collapse",shownBsCollapse:"shown.bs.collapse"};_exports.eventTypes=eventTypes;_exports.notifyCollapsableSectionShown=element=>(0,_event_dispatcher.dispatchEvent)(eventTypes.shown,{},element);_exports.notifyCollapsableSectionHidden=element=>(0,_event_dispatcher.dispatchEvent)(eventTypes.hidden,{},element)}));
+
+//# sourceMappingURL=events.min.js.map
\ No newline at end of file
diff --git a/lib/amd/build/local/collapsable_section/events.min.js.map b/lib/amd/build/local/collapsable_section/events.min.js.map
new file mode 100644
index 0000000000000..1004111815052
--- /dev/null
+++ b/lib/amd/build/local/collapsable_section/events.min.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"events.min.js","sources":["../../../src/local/collapsable_section/events.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * The collapsable section events.\n *\n * This module wraps the standard bootstrap collapsable events, but for collapsable sections.\n *\n * @module core/local/collapsable_section/events\n * @copyright 2024 Ferran Recio \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n *\n * @example
Example of listening to a collapsable section events.
\n * import {eventTypes as collapsableSectionEventTypes} from 'core/local/collapsable_section/events';\n *\n * document.addEventListener(collapsableSectionEventTypes.shown, event => {\n * window.console.log(event.target); // The HTMLElement relating to the block whose content was updated.\n * });\n */\n\nimport {dispatchEvent} from 'core/event_dispatcher';\n\n/**\n * Events for `core_block`.\n *\n * @constant\n * @property {String} blockContentUpdated See {@link event:blockContentUpdated}\n */\nexport const eventTypes = {\n /**\n * An event triggered when the content of a block has changed.\n *\n * @event blockContentUpdated\n * @type {CustomEvent}\n * @property {HTMLElement} target The block element that was updated\n * @property {object} detail\n * @property {number} detail.instanceId The block instance id\n */\n shown: 'core_collapsable_section_shown',\n hidden: 'core_collapsable_section_hidden',\n // All Bootstrap 4 jQuery events are wrapped while MDL-71979 is not integrated.\n hideBsCollapse: 'hide.bs.collapse',\n hiddenBsCollapse: 'hidden.bs.collapse',\n showBsCollapse: 'show.bs.collapse',\n shownBsCollapse: 'shown.bs.collapse',\n};\n\n/**\n * Trigger an event to indicate that the content of a block was updated.\n *\n * @method notifyBlockContentUpdated\n * @param {HTMLElement} element The HTMLElement containing the updated block.\n * @returns {CustomEvent}\n * @fires blockContentUpdated\n */\nexport const notifyCollapsableSectionShown = element => dispatchEvent(\n eventTypes.shown,\n {},\n element\n);\n\n/**\n * Trigger an event to indicate that the content of a block was updated.\n *\n * @method notifyBlockContentUpdated\n * @param {HTMLElement} element The HTMLElement containing the updated block.\n * @returns {CustomEvent}\n * @fires blockContentUpdated\n */\nexport const notifyCollapsableSectionHidden = element => dispatchEvent(\n eventTypes.hidden,\n {},\n element\n);\n"],"names":["eventTypes","shown","hidden","hideBsCollapse","hiddenBsCollapse","showBsCollapse","shownBsCollapse","element"],"mappings":";;;;;;;;;;;;;;;;;MAwCaA,WAAa,CAUtBC,MAAO,iCACPC,OAAQ,kCAERC,eAAgB,mBAChBC,iBAAkB,qBAClBC,eAAgB,mBAChBC,gBAAiB,2FAWwBC,UAAW,mCACpDP,WAAWC,MACX,GACAM,iDAW0CA,UAAW,mCACrDP,WAAWE,OACX,GACAK"}
\ No newline at end of file
diff --git a/lib/amd/src/local/collapsable_section/controls.js b/lib/amd/src/local/collapsable_section/controls.js
new file mode 100644
index 0000000000000..49ead59af3a25
--- /dev/null
+++ b/lib/amd/src/local/collapsable_section/controls.js
@@ -0,0 +1,138 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * The collapsable sections controls.
+ *
+ * @module core/local/collapsable_section/controls
+ * @copyright 2024 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ *
+ * @example
Example of controlling a collapsable section.
+ *
+ * import CollapsableSection from 'core/local/collapsable_section/controls';
+ *
+ * const section = CollapsableSection.instanceFromSelector('#MyCollapsableSection');
+ *
+ * // Use hide, show and toggle methods to control the section.
+ * section.hide();
+ */
+
+import {
+ eventTypes,
+ notifyCollapsableSectionHidden,
+ notifyCollapsableSectionShown
+} from 'core/local/collapsable_section/events';
+
+// The jQuery module is only used for interacting with Boostrap 4. It can we removed when MDL-71979 is integrated.
+import jQuery from 'jquery';
+
+let initialized = false;
+
+export default class {
+ /**
+ * Create a new instance from a query selector.
+ *
+ * @param {String} selector The selector of the collapsable section.
+ * @return {CollapsableSection} The collapsable section controls.
+ * @throws {Error} If no elements are found with the selector.
+ */
+ static instanceFromSelector(selector) {
+ const elements = document.querySelector(selector);
+ if (!elements) {
+ throw new Error('No elements found with the selector: ' + selector);
+ }
+ return new this(elements);
+ }
+
+ /**
+ * Initialize the collapsable section controls.
+ */
+ static init() {
+ if (initialized) {
+ return;
+ }
+ initialized = true;
+
+ // We want to add extra events to the standard bootstrap collapsable events.
+ // TODO: change all jquery events to custom events once MDL-71979 is integrated.
+ jQuery(document).on(eventTypes.hiddenBsCollapse, event => {
+ if (!this.isCollapsableComponent(event.target)) {
+ return;
+ }
+ notifyCollapsableSectionHidden(event.target);
+ });
+ jQuery(document).on(eventTypes.shownBsCollapse, event => {
+ if (!this.isCollapsableComponent(event.target)) {
+ return;
+ }
+ notifyCollapsableSectionShown(event.target);
+ });
+ }
+
+ /**
+ * Check if the element is a collapsable section.
+ *
+ * @private
+ * @param {HTMLElement} element The element to check.
+ * @return {boolean} True if the element is a collapsable section.
+ */
+ static isCollapsableComponent(element) {
+ return element.hasAttribute('data-mdl-component')
+ && element.getAttribute('data-mdl-component') === 'core/local/collapsable_section';
+ }
+
+ /**
+ * Creates an instance of the controls for a collapsable section.
+ *
+ * @param {HTMLElement} element - The DOM element that this control will manage.
+ */
+ constructor(element) {
+ this.element = element;
+ }
+
+ /**
+ * Hides the collapsible section element.
+ */
+ hide() {
+ // TODO: change all jquery once MDL-71979 is integrated.
+ jQuery(this.element).collapse('hide');
+ }
+
+ /**
+ * Shows the collapsible section element.
+ */
+ show() {
+ // TODO: change all jquery once MDL-71979 is integrated.
+ jQuery(this.element).collapse('show');
+ }
+
+ /**
+ * Toggle the collapsible section element.
+ */
+ toggle() {
+ // TODO: change all jquery once MDL-71979 is integrated.
+ jQuery(this.element).collapse('toggle');
+ }
+
+ /**
+ * Check if the collapsable section is visible.
+ *
+ * @return {boolean} True if the collapsable section is visible.
+ */
+ isVisible() {
+ return this.element.classList.contains('show');
+ }
+}
diff --git a/lib/amd/src/local/collapsable_section/events.js b/lib/amd/src/local/collapsable_section/events.js
new file mode 100644
index 0000000000000..0d32cf093cc25
--- /dev/null
+++ b/lib/amd/src/local/collapsable_section/events.js
@@ -0,0 +1,86 @@
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see .
+
+/**
+ * The collapsable section events.
+ *
+ * This module wraps the standard bootstrap collapsable events, but for collapsable sections.
+ *
+ * @module core/local/collapsable_section/events
+ * @copyright 2024 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ *
+ * @example
Example of listening to a collapsable section events.
+ * import {eventTypes as collapsableSectionEventTypes} from 'core/local/collapsable_section/events';
+ *
+ * document.addEventListener(collapsableSectionEventTypes.shown, event => {
+ * window.console.log(event.target); // The HTMLElement relating to the block whose content was updated.
+ * });
+ */
+
+import {dispatchEvent} from 'core/event_dispatcher';
+
+/**
+ * Events for `core_block`.
+ *
+ * @constant
+ * @property {String} blockContentUpdated See {@link event:blockContentUpdated}
+ */
+export const eventTypes = {
+ /**
+ * An event triggered when the content of a block has changed.
+ *
+ * @event blockContentUpdated
+ * @type {CustomEvent}
+ * @property {HTMLElement} target The block element that was updated
+ * @property {object} detail
+ * @property {number} detail.instanceId The block instance id
+ */
+ shown: 'core_collapsable_section_shown',
+ hidden: 'core_collapsable_section_hidden',
+ // All Bootstrap 4 jQuery events are wrapped while MDL-71979 is not integrated.
+ hideBsCollapse: 'hide.bs.collapse',
+ hiddenBsCollapse: 'hidden.bs.collapse',
+ showBsCollapse: 'show.bs.collapse',
+ shownBsCollapse: 'shown.bs.collapse',
+};
+
+/**
+ * Trigger an event to indicate that the content of a block was updated.
+ *
+ * @method notifyBlockContentUpdated
+ * @param {HTMLElement} element The HTMLElement containing the updated block.
+ * @returns {CustomEvent}
+ * @fires blockContentUpdated
+ */
+export const notifyCollapsableSectionShown = element => dispatchEvent(
+ eventTypes.shown,
+ {},
+ element
+);
+
+/**
+ * Trigger an event to indicate that the content of a block was updated.
+ *
+ * @method notifyBlockContentUpdated
+ * @param {HTMLElement} element The HTMLElement containing the updated block.
+ * @returns {CustomEvent}
+ * @fires blockContentUpdated
+ */
+export const notifyCollapsableSectionHidden = element => dispatchEvent(
+ eventTypes.hidden,
+ {},
+ element
+);
diff --git a/lib/classes/output/local/collapsable_section.php b/lib/classes/output/local/collapsable_section.php
new file mode 100644
index 0000000000000..de7e9ed890b44
--- /dev/null
+++ b/lib/classes/output/local/collapsable_section.php
@@ -0,0 +1,147 @@
+.
+
+namespace core\output\local;
+
+use core\output\named_templatable;
+use core\output\renderable;
+
+/**
+ * Collapsable section output.
+ *
+ * @package core
+ * @copyright 2024 Ferran Recio
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class collapsable_section implements named_templatable, renderable {
+ /**
+ * Constructor.
+ *
+ * @param string $titlecontent The content to be displayed inside the button.
+ * @param string $sectioncontent The content to be displayed inside the dialog.
+ * @param string $classes Additional CSS classes to be applied to the section.
+ * @param array $extras An attribute => value array to be added to the element.
+ * @param bool $open If the section is opened by default.
+ * @param string|null $expandlabel The label for the expand button.
+ * @param string|null $collapselabel The label for the collapse button.
+ */
+ public function __construct(
+ /** @var string $titlecontent The content to be displayed inside the button. */
+ protected string $titlecontent,
+ /** @var string $sectioncontent The content to be displayed inside the dialog. */
+ protected string $sectioncontent,
+ /** @var string $classes Additional CSS classes to be applied to the section. */
+ protected string $classes = '',
+ /** @var array $extras A attribute => value array to be added to the element. */
+ protected array $extras = [],
+ /** @var bool $open if the section is opened by default. */
+ protected bool $open = false,
+ /** @var string|null $expandlabel The label for the expand button. */
+ protected string|null $expandlabel = null,
+ /** @var string|null $collapselabel The label for the collapse button. */
+ protected string|null $collapselabel = null,
+ ) {
+ }
+
+ /**
+ * Set the title content.
+ *
+ * @param string $titlecontent
+ */
+ public function set_title_content(string $titlecontent) {
+ $this->titlecontent = $titlecontent;
+ }
+
+ /**
+ * Sets the content for the collapsable section.
+ *
+ * @param string $sectioncontent The content to be set for the section.
+ */
+ public function set_section_content(string $sectioncontent) {
+ $this->sectioncontent = $sectioncontent;
+ }
+
+ /**
+ * Sets the CSS classes for the collapsable section.
+ *
+ * @param string $classes The CSS classes to be applied to the collapsable section.
+ */
+ public function set_classes(string $classes) {
+ $this->classes = $classes;
+ }
+
+ /**
+ * Merges the provided extras array with the existing extras array.
+ *
+ * @param array $extras The array of extra attributes => extra value.
+ */
+ public function add_extra_attributes(array $extras) {
+ $this->extras = array_merge($this->extras, $extras);
+ }
+
+ /**
+ * Sets the default open state of the collapsible section.
+ *
+ * @param bool $open
+ */
+ public function set_open(bool $open) {
+ $this->open = $open;
+ }
+
+ #[\Override]
+ public function export_for_template(\renderer_base $output): array {
+ $elementid = $this->extras['id'] ?? \html_writer::random_id('collapsableSection_');
+
+ $data = [
+ 'titlecontent' => $this->titlecontent,
+ 'sectioncontent' => $this->sectioncontent,
+ 'classes' => $this->classes,
+ 'extras' => $this->export_extras(),
+ 'elementid' => $elementid,
+ ];
+ if ($this->open) {
+ $data['open'] = 'true';
+ }
+ if ($this->expandlabel) {
+ $data['expandlabel'] = $this->expandlabel;
+ }
+ if ($this->collapselabel) {
+ $data['collapselabel'] = $this->collapselabel;
+ }
+ return $data;
+ }
+
+ /**
+ * Exports the extras as an array of attribute-value pairs.
+ *
+ * @return array An array of associative arrays, each containing 'attribute' and 'value' keys.
+ */
+ private function export_extras(): array {
+ $extras = [];
+ foreach ($this->extras as $attribute => $value) {
+ $extras[] = [
+ 'attribute' => $attribute,
+ 'value' => $value,
+ ];
+ }
+ return $extras;
+ }
+
+ #[\Override]
+ public function get_template_name(\renderer_base $renderer): string {
+ return 'core/local/collapsable_section';
+ }
+}
diff --git a/lib/templates/local/collapsable_section.mustache b/lib/templates/local/collapsable_section.mustache
new file mode 100644
index 0000000000000..6b79e58e72587
--- /dev/null
+++ b/lib/templates/local/collapsable_section.mustache
@@ -0,0 +1,147 @@
+{{!
+ This file is part of Moodle - http://moodle.org/
+
+ Moodle is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Moodle is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Moodle. If not, see .
+}}
+{{!
+ @template core/local/collapsable_section
+
+ Standard collapsible section.
+
+ Optional blocks:
+ * extraclasses - additional classes.
+ * elementid - optional element id.
+ * titlecontent - the collpasible title content.
+ * sectioncontent - the collapsible content.
+ * extras - custom HTML attributes for the component.
+ * expandlabel - the label for the expand icon.
+ * collapselabel - the label for the collapse icon.
+
+ Example context (json):
+ {
+ "titlecontent": "New content",
+ "sectioncontent": "New content",
+ "classes": "someclass",
+ "extras": [
+ {
+ "attribute": "data-example",
+ "value": "something"
+ }
+ ],
+ "open": true,
+ "expandlabel": "Expand",
+ "collapselabel": "Collapse",
+ "elementid": "someuniqueid"
+ }
+}}
+
+{{#js}}
+require(['core/local/collapsable_section/controls'], function(Controls) {
+ Controls.init();
+});
+{{/js}}
diff --git a/lib/tests/behat/collapsable_section_output.feature b/lib/tests/behat/collapsable_section_output.feature
new file mode 100644
index 0000000000000..975fc425d51a7
--- /dev/null
+++ b/lib/tests/behat/collapsable_section_output.feature
@@ -0,0 +1,66 @@
+@core @javascript
+Feature: Test collapsable section output module
+ In order to show extra information to the user
+ As a user
+ I need to interact with the collapsable section output
+
+ Background:
+ # Get to the fixture page.
+ Given I log in as "admin"
+ And I am on fixture page "/lib/tests/behat/fixtures/collapsable_section_output_testpage.php"
+
+ Scenario: Collapsable sections can be opened and closed
+ Given I should not see "Dialog content"
+ And I should not see "This is the closed section content." in the "closedsection" "region"
+ And I should see "This is the open section content." in the "opensection" "region"
+ When I click on "Expand" "button" in the "closedsection" "region"
+ And I click on "Collapse" "button" in the "opensection" "region"
+ Then I should see "This is the closed section content." in the "closedsection" "region"
+ And I should not see "This is the open section content." in the "opensection" "region"
+
+ Scenario: Collapsable sections content can have rich content inside
+ When I click on "Expand" "button" in the "closedsection" "region"
+ Then I should see "This is the closed section content." in the "closedsection" "region"
+ And "Link" "link" should exist in the "closedsection" "region"
+ And "Eye icon" "icon" should exist in the "closedsection" "region"
+
+ Scenario: Collapsable sections HTML attributtes can be overriden
+ When I click on "Expand" "button" in the "extraclasses" "region"
+ And I click on "Expand" "button" in the "extraattributes" "region"
+ Then ".extraclass" "css_element" should exist in the "extraclasses" "region"
+ And "[data-foo='bar']" "css_element" should exist in the "extraattributes" "region"
+ And "#myid" "css_element" should exist in the "extraattributes" "region"
+
+ Scenario: Collapsable sections can have custom labels for expand and collapse
+ When I click on "Custom expand" "button" in the "customlabels" "region"
+ Then I should see "This is the custom labels content." in the "customlabels" "region"
+ And I click on "Custom collapse" "button" in the "customlabels" "region"
+ And I should not see "This is the custom labels content." in the "customlabels" "region"
+
+ Scenario: Collapsable sections can be controlled via javascript
+ # Toggle.
+ Given I should not see "This is the javascript controls content." in the "jscontrols" "region"
+ When I click on "Toggle" "button" in the "jscontrols" "region"
+ Then I should see "This is the javascript controls content." in the "jscontrols" "region"
+ And I click on "Toggle" "button" in the "jscontrols" "region"
+ And I should not see "This is the javascript controls content." in the "jscontrols" "region"
+ # Show and Hide.
+ And I click on "Show" "button" in the "jscontrols" "region"
+ And I should see "This is the javascript controls content." in the "jscontrols" "region"
+ And I click on "Show" "button" in the "jscontrols" "region"
+ And I should see "This is the javascript controls content." in the "jscontrols" "region"
+ And I click on "Hide" "button" in the "jscontrols" "region"
+ And I should not see "This is the javascript controls content." in the "jscontrols" "region"
+ And I click on "Hide" "button" in the "jscontrols" "region"
+ And I should not see "This is the javascript controls content." in the "jscontrols" "region"
+ # Test state.
+ And I click on "Test state" "button" in the "jscontrols" "region"
+ And I should see "hidden" in the "state" "region"
+ And I click on "Show" "button" in the "jscontrols" "region"
+ And I click on "Test state" "button" in the "jscontrols" "region"
+ And I should see "visible" in the "state" "region"
+ # Events.
+ And I click on "Show" "button" in the "jscontrols" "region"
+ And I should see "Last event: Section shown" in the "jscontrols" "region"
+ And I click on "Hide" "button" in the "jscontrols" "region"
+ And I should see "Last event: Section hidden" in the "jscontrols" "region"
diff --git a/lib/tests/behat/fixtures/collapsable_section_output_testpage.php b/lib/tests/behat/fixtures/collapsable_section_output_testpage.php
new file mode 100644
index 0000000000000..3f29e6814d57b
--- /dev/null
+++ b/lib/tests/behat/fixtures/collapsable_section_output_testpage.php
@@ -0,0 +1,204 @@
+.
+
+/**
+ * Test page for the collapsable section output component.
+ *
+ * @copyright 2024 Ferran Recio
+ * @package core
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../../config.php');
+
+use core\output\local\collapsable_section;
+
+defined('BEHAT_SITE_RUNNING') || die();
+
+/**
+ * Generate the title content.
+ *
+ * @param string $content The content to be displayed inside the button.
+ * @return string
+ */
+function title_content(string $content): string {
+ return ucfirst($content) . ' title';
+}
+
+/**
+ * Generate the section content.
+ *
+ * @param string $content The content to be displayed inside the dialog.
+ * @return string
+ */
+function section_content(string $content): string {
+ global $OUTPUT;
+ $icon = $OUTPUT->pix_icon('t/hide', 'Eye icon');
+ return '
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
+ tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+ quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+ consequat.
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
+ tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+ quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+ consequat.
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
+ tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+ quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+ consequat.