Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP][SPIKE] Accordion refactorings #5551

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

romaricpascal
Copy link
Member

@romaricpascal romaricpascal commented Dec 13, 2024

Note

Best reviewed commit by commit

Building upon works from @36degrees to introduce a createElement function, this PR aims to explore a couple of refactorings that look useful for the accordion.

Especially, the end goal would be to introduce an AccordionSection class representing each section of the Accordion an properly split responsibilities between:

  • The Accordion, responsible for controlling all sections at once (and the initial opening state)
  • The AccordionSection responsible for controlling an individual section

Important

This PR is far from being ready for review, just chipping at things at the moment, on the accordion-section-component-wip branch

The overall plan is to:

  • Refactor the code creating the section's header so it's a little easier to follow based on these two guidelines:
    • Extract creation of complex structures in their own methods. This allows to have a readable outline of the generated HTML structure
    • Prefer creating parent elements after their children when possible. This allows to take advantage of the children argument of createElement, as well as create a reverse pyramid mirroring the structure of the DOM being created
  • Introduce the AccordionSection class with initial aim to store key elements of the section for reuse (in setExpanded, for example) without having to query the DOM again
    • This will hopefully allow to get rid of a few more of the variables storing classes.
  • Move accessing whether a section is expanded to AccordionSection as an expanded getter
  • Move setting whether a section is expanded to AccordionSection as an expanded setter
  • Move updating rendering of the section based on expanded to AccordionSection
  • Move click handling into the AccordionSection
  • Possibly regroup the control of which section is open in its own class (to encapsulate complexity around beforematch and us storing state client side when rememberExpanded is set).

Copy link

github-actions bot commented Dec 13, 2024

📋 Stats

File sizes

File Size
dist/govuk-frontend-development.min.css 118.42 KiB
dist/govuk-frontend-development.min.js 40.81 KiB
packages/govuk-frontend/dist/govuk/all.bundle.js 92.01 KiB
packages/govuk-frontend/dist/govuk/all.bundle.mjs 86.21 KiB
packages/govuk-frontend/dist/govuk/all.mjs 1.21 KiB
packages/govuk-frontend/dist/govuk/component.mjs 1.67 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend.min.css 118.41 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend.min.js 40.8 KiB
packages/govuk-frontend/dist/govuk/i18n.mjs 5.55 KiB
packages/govuk-frontend/dist/govuk/init.mjs 6.64 KiB

Modules

File Size (bundled) Size (minified)
all.mjs 81.83 KiB 38.39 KiB
accordion.mjs 26.37 KiB 11.7 KiB
button.mjs 9.09 KiB 3.79 KiB
character-count.mjs 25.37 KiB 10.91 KiB
checkboxes.mjs 7.76 KiB 3.42 KiB
error-summary.mjs 10.99 KiB 4.55 KiB
exit-this-page.mjs 20.21 KiB 10.21 KiB
header.mjs 6.41 KiB 3.22 KiB
notification-banner.mjs 9.35 KiB 3.71 KiB
password-input.mjs 18.31 KiB 8.42 KiB
radios.mjs 6.76 KiB 2.98 KiB
service-navigation.mjs 6.39 KiB 3.26 KiB
skip-link.mjs 6.34 KiB 2.76 KiB
tabs.mjs 11.99 KiB 6.67 KiB

View stats and visualisations on the review app


Action run for 6c61aa5

Copy link

github-actions bot commented Dec 13, 2024

JavaScript changes to npm package

diff --git a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
index 40ce6ccf5..2301d0305 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
@@ -13,20 +13,20 @@ function getBreakpoint(t) {
 }
 
 function setFocus(t, e = {}) {
-    var s;
-    const n = t.getAttribute("tabindex");
+    var n;
+    const i = t.getAttribute("tabindex");
 
     function onBlur() {
-        var s;
-        null == (s = e.onBlur) || s.call(t), n || t.removeAttribute("tabindex")
+        var n;
+        null == (n = e.onBlur) || n.call(t), i || t.removeAttribute("tabindex")
     }
-    n || t.setAttribute("tabindex", "-1"), t.addEventListener("focus", (function() {
+    i || t.setAttribute("tabindex", "-1"), t.addEventListener("focus", (function() {
         t.addEventListener("blur", onBlur, {
             once: !0
         })
     }), {
         once: !0
-    }), null == (s = e.onBeforeFocus) || s.call(t), t.focus()
+    }), null == (n = e.onBeforeFocus) || n.call(t), t.focus()
 }
 
 function isSupported(t = document.body) {
@@ -63,12 +63,12 @@ class ElementError extends GOVUKFrontendError {
         let e = "string" == typeof t ? t : "";
         if ("object" == typeof t) {
             const {
-                component: s,
-                identifier: n,
-                element: i,
-                expectedType: o
+                component: n,
+                identifier: i,
+                element: o,
+                expectedType: s
             } = t;
-            e = n, e += i ? ` is not of type ${null!=o?o:"HTMLElement"}` : " not found", e = formatErrorMessage(s, e)
+            e = i, e += o ? ` is not of type ${null!=s?s:"HTMLElement"}` : " not found", e = formatErrorMessage(n, e)
         }
         super(e), this.name = "ElementError"
     }
@@ -93,8 +93,8 @@ class Component {
             expectedType: e.elementType.name
         });
         this._$root = t, e.checkSupport(), this.checkInitialised();
-        const s = e.moduleName;
-        this.$root.setAttribute(`data-${s}-init`, "")
+        const n = e.moduleName;
+        this.$root.setAttribute(`data-${n}-init`, "")
     }
     checkInitialised() {
         const t = this.constructor,
@@ -116,91 +116,100 @@ class ConfigurableComponent extends Component {
     get config() {
         return this._config
     }
-    constructor(e, s) {
+    constructor(e, n) {
         super(e), this._config = void 0;
-        const n = this.constructor;
-        if (!isObject(n.defaults)) throw new ConfigError(formatErrorMessage(n, "Config passed as parameter into constructor but no defaults defined"));
-        const i = function(Component, t) {
+        const i = this.constructor;
+        if (!isObject(i.defaults)) throw new ConfigError(formatErrorMessage(i, "Config passed as parameter into constructor but no defaults defined"));
+        const o = function(Component, t) {
             if (!isObject(Component.schema)) throw new ConfigError(formatErrorMessage(Component, "Config passed as parameter into constructor but no schema defined"));
             const e = {},
-                s = Object.entries(Component.schema.properties);
-            for (const n of s) {
-                const [s, i] = n, o = s.toString();
-                o in t && (e[o] = normaliseString(t[o], i)), "object" === (null == i ? void 0 : i.type) && (e[o] = extractConfigByNamespace(Component.schema, t, s))
+                n = Object.entries(Component.schema.properties);
+            for (const i of n) {
+                const [n, o] = i, s = n.toString();
+                s in t && (e[s] = normaliseString(t[s], o)), "object" === (null == o ? void 0 : o.type) && (e[s] = extractConfigByNamespace(Component.schema, t, n))
             }
             return e
-        }(n, this._$root.dataset);
-        this._config = mergeConfigs(n.defaults, null != s ? s : {}, this[t](i), i)
+        }(i, this._$root.dataset);
+        this._config = mergeConfigs(i.defaults, null != n ? n : {}, this[t](o), o)
     }
 }
 
 function normaliseString(t, e) {
-    const s = t ? t.trim() : "";
-    let n, i = null == e ? void 0 : e.type;
-    switch (i || (["true", "false"].includes(s) && (i = "boolean"), s.length > 0 && isFinite(Number(s)) && (i = "number")), i) {
+    const n = t ? t.trim() : "";
+    let i, o = null == e ? void 0 : e.type;
+    switch (o || (["true", "false"].includes(n) && (o = "boolean"), n.length > 0 && isFinite(Number(n)) && (o = "number")), o) {
         case "boolean":
-            n = "true" === s;
+            i = "true" === n;
             break;
         case "number":
-            n = Number(s);
+            i = Number(n);
             break;
         default:
-            n = t
+            i = t
     }
-    return n
+    return i
 }
 
 function mergeConfigs(...t) {
     const e = {};
-    for (const s of t)
-        for (const t of Object.keys(s)) {
-            const n = e[t],
-                i = s[t];
-            isObject(n) && isObject(i) ? e[t] = mergeConfigs(n, i) : e[t] = i
+    for (const n of t)
+        for (const t of Object.keys(n)) {
+            const i = e[t],
+                o = n[t];
+            isObject(i) && isObject(o) ? e[t] = mergeConfigs(i, o) : e[t] = o
         }
     return e
 }
 
-function extractConfigByNamespace(t, e, s) {
-    const n = t.properties[s];
-    if ("object" !== (null == n ? void 0 : n.type)) return;
-    const i = {
-        [s]: {}
+function extractConfigByNamespace(t, e, n) {
+    const i = t.properties[n];
+    if ("object" !== (null == i ? void 0 : i.type)) return;
+    const o = {
+        [n]: {}
     };
-    for (const [o, r] of Object.entries(e)) {
-        let t = i;
-        const e = o.split(".");
-        for (const [n, i] of e.entries()) isObject(t) && (n < e.length - 1 ? (isObject(t[i]) || (t[i] = {}), t = t[i]) : o !== s && (t[i] = normaliseString(r)))
+    for (const [s, r] of Object.entries(e)) {
+        let t = o;
+        const e = s.split(".");
+        for (const [i, o] of e.entries()) isObject(t) && (i < e.length - 1 ? (isObject(t[o]) || (t[o] = {}), t = t[o]) : s !== n && (t[o] = normaliseString(r)))
     }
-    return i[s]
+    return o[n]
+}
+
+function createElement(t, e = {}, n) {
+    const i = document.createElement(t);
+    if (Object.entries(e).forEach((([t, e]) => {
+            i.setAttribute(t, e)
+        })), n)
+        for (const o of n) i.appendChild(o);
+    return i
 }
 class I18n {
     constructor(t = {}, e = {}) {
-        var s;
-        this.translations = void 0, this.locale = void 0, this.translations = t, this.locale = null != (s = e.locale) ? s : document.documentElement.lang || "en"
+        var n;
+        this.translations = void 0, this.locale = void 0, this.translations = t, this.locale = null != (n = e.locale) ? n : document.documentElement.lang || "en"
     }
     t(t, e) {
         if (!t) throw new Error("i18n: lookup key missing");
-        let s = this.translations[t];
-        if ("number" == typeof(null == e ? void 0 : e.count) && "object" == typeof s) {
-            const n = s[this.getPluralSuffix(t, e.count)];
-            n && (s = n)
+        let n = this.translations[t];
+        if ("number" == typeof(null == e ? void 0 : e.count) && "object" == typeof n) {
+            const i = n[this.getPluralSuffix(t, e.count)];
+            i && (n = i)
         }
-        if ("string" == typeof s) {
-            if (s.match(/%{(.\S+)}/)) {
+        if ("string" == typeof n) {
+            if (n.match(/%{(.\S+)}/)) {
                 if (!e) throw new Error("i18n: cannot replace placeholders in string if no option data provided");
-                return this.replacePlaceholders(s, e)
+                return this.replacePlaceholders(n, e)
             }
-            return s
+            return n
         }
         return t
     }
     replacePlaceholders(t, e) {
-        const s = Intl.NumberFormat.supportedLocalesOf(this.locale).length ? new Intl.NumberFormat(this.locale) : void 0;
-        return t.replace(/%{(.\S+)}/g, (function(t, n) {
-            if (Object.prototype.hasOwnProperty.call(e, n)) {
-                const t = e[n];
-                return !1 === t || "number" != typeof t && "string" != typeof t ? "" : "number" == typeof t ? s ? s.format(t) : `${t}` : t
+        const n = Intl.NumberFormat.supportedLocalesOf(this.locale).length ? new Intl.NumberFormat(this.locale) : void 0;
+        return t.replace(/%{(.\S+)}/g, (function(t, i) {
+            if (Object.prototype.hasOwnProperty.call(e, i)) {
+                const t = e[i];
+                return !1 === t || "number" != typeof t && "string" != typeof t ? "" : "number" == typeof t ? n ? n.format(t) : `${t}` : t
             }
             throw new Error(`i18n: no data found to replace ${t} placeholder in string`)
         }))
@@ -210,11 +219,11 @@ class I18n {
     }
     getPluralSuffix(t, e) {
         if (e = Number(e), !isFinite(e)) return "other";
-        const s = this.translations[t],
-            n = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(e) : this.selectPluralFormUsingFallbackRules(e);
-        if ("object" == typeof s) {
-            if (n in s) return n;
-            if ("other" in s) return console.warn(`i18n: Missing plural form ".${n}" for "${this.locale}" locale. Falling back to ".other".`), "other"
+        const n = this.translations[t],
+            i = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(e) : this.selectPluralFormUsingFallbackRules(e);
+        if ("object" == typeof n) {
+            if (i in n) return i;
+            if ("other" in n) return console.warn(`i18n: Missing plural form ".${i}" for "${this.locale}" locale. Falling back to ".other".`), "other"
         }
         throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`)
     }
@@ -226,8 +235,8 @@ class I18n {
     getPluralRulesForLocale() {
         const t = this.locale.split("-")[0];
         for (const e in I18n.pluralRulesMap) {
-            const s = I18n.pluralRulesMap[e];
-            if (s.includes(this.locale) || s.includes(t)) return e
+            const n = I18n.pluralRulesMap[e];
+            if (n.includes(this.locale) || n.includes(t)) return e
         }
     }
 }
@@ -249,77 +258,113 @@ I18n.pluralRulesMap = {
     irish: t => 1 === t ? "one" : 2 === t ? "two" : t >= 3 && t <= 6 ? "few" : t >= 7 && t <= 10 ? "many" : "other",
     russian(t) {
         const e = t % 100,
-            s = e % 10;
-        return 1 === s && 11 !== e ? "one" : s >= 2 && s <= 4 && !(e >= 12 && e <= 14) ? "few" : 0 === s || s >= 5 && s <= 9 || e >= 11 && e <= 14 ? "many" : "other"
+            n = e % 10;
+        return 1 === n && 11 !== e ? "one" : n >= 2 && n <= 4 && !(e >= 12 && e <= 14) ? "few" : 0 === n || n >= 5 && n <= 9 || e >= 11 && e <= 14 ? "many" : "other"
     },
     scottish: t => 1 === t || 11 === t ? "one" : 2 === t || 12 === t ? "two" : t >= 3 && t <= 10 || t >= 13 && t <= 19 ? "few" : "other",
     spanish: t => 1 === t ? "one" : t % 1e6 == 0 && 0 !== t ? "many" : "other",
     welsh: t => 0 === t ? "zero" : 1 === t ? "one" : 2 === t ? "two" : 3 === t ? "few" : 6 === t ? "many" : "other"
 };
+const e = "govuk-accordion__section",
+    n = "govuk-accordion__section--expanded",
+    i = "govuk-accordion__section-button",
+    o = "govuk-accordion__section-header",
+    s = "govuk-accordion__section-heading",
+    r = "govuk-accordion__section-heading-text",
+    a = "govuk-accordion__section-toggle-text",
+    c = "govuk-accordion-nav__chevron",
+    l = "govuk-accordion-nav__chevron--down",
+    u = "govuk-accordion__section-summary",
+    h = "govuk-accordion__section-content";
 class Accordion extends ConfigurableComponent {
-    constructor(t, e = {}) {
-        super(t, e), this.i18n = void 0, this.controlsClass = "govuk-accordion__controls", this.showAllClass = "govuk-accordion__show-all", this.showAllTextClass = "govuk-accordion__show-all-text", this.sectionClass = "govuk-accordion__section", this.sectionExpandedClass = "govuk-accordion__section--expanded", this.sectionButtonClass = "govuk-accordion__section-button", this.sectionHeaderClass = "govuk-accordion__section-header", this.sectionHeadingClass = "govuk-accordion__section-heading", this.sectionHeadingDividerClass = "govuk-accordion__section-heading-divider", this.sectionHeadingTextClass = "govuk-accordion__section-heading-text", this.sectionHeadingTextFocusClass = "govuk-accordion__section-heading-text-focus", this.sectionShowHideToggleClass = "govuk-accordion__section-toggle", this.sectionShowHideToggleFocusClass = "govuk-accordion__section-toggle-focus", this.sectionShowHideTextClass = "govuk-accordion__section-toggle-text", this.upChevronIconClass = "govuk-accordion-nav__chevron", this.downChevronIconClass = "govuk-accordion-nav__chevron--down", this.sectionSummaryClass = "govuk-accordion__section-summary", this.sectionSummaryFocusClass = "govuk-accordion__section-summary-focus", this.sectionContentClass = "govuk-accordion__section-content", this.$sections = void 0, this.$showAllButton = null, this.$showAllIcon = null, this.$showAllText = null, this.i18n = new I18n(this.config.i18n);
-        const s = this.$root.querySelectorAll(`.${this.sectionClass}`);
-        if (!s.length) throw new ElementError({
+    constructor(t, n = {}) {
+        super(t, n), this.i18n = void 0, this.$sections = void 0, this.$showAllButton = null, this.$showAllIcon = null, this.$showAllText = null, this.i18n = new I18n(this.config.i18n);
+        const i = this.$root.querySelectorAll(`.${e}`);
+        if (!i.length) throw new ElementError({
             component: Accordion,
-            identifier: `Sections (\`<div class="${this.sectionClass}">\`)`
+            identifier: `Sections (\`<div class="${e}">\`)`
         });
-        this.$sections = s, this.initControls(), this.initSectionHeaders(), this.updateShowAllButton(this.areAllSectionsOpen())
+        this.$sections = i, this.initControls(), this.initSectionHeaders(), this.updateShowAllButton(this.areAllSectionsOpen())
     }
     initControls() {
-        this.$showAllButton = document.createElement("button"), this.$showAllButton.setAttribute("type", "button"), this.$showAllButton.setAttribute("class", this.showAllClass), this.$showAllButton.setAttribute("aria-expanded", "false"), this.$showAllIcon = document.createElement("span"), this.$showAllIcon.classList.add(this.upChevronIconClass), this.$showAllButton.appendChild(this.$showAllIcon);
-        const t = document.createElement("div");
-        t.setAttribute("class", this.controlsClass), t.appendChild(this.$showAllButton), this.$root.insertBefore(t, this.$root.firstChild), this.$showAllText = document.createElement("span"), this.$showAllText.classList.add(this.showAllTextClass), this.$showAllButton.appendChild(this.$showAllText), this.$showAllButton.addEventListener("click", (() => this.onShowOrHideAllToggle())), "onbeforematch" in document && document.addEventListener("beforematch", (t => this.onBeforeMatch(t)))
+        this.$showAllButton = createElement("button", {
+            type: "button",
+            class: "govuk-accordion__show-all",
+            "aria-expanded": "false"
+        }), this.$showAllIcon = createElement("span", {
+            class: c
+        }), this.$showAllButton.appendChild(this.$showAllIcon);
+        const t = createElement("div", {
+            class: "govuk-accordion__controls"
+        });
+        t.appendChild(this.$showAllButton), this.$root.insertBefore(t, this.$root.firstChild), this.$showAllText = createElement("span", {
+            class: "govuk-accordion__show-all-text"
+        }), this.$showAllButton.appendChild(this.$showAllText), this.$showAllButton.addEventListener("click", (() => this.onShowOrHideAllToggle())), "onbeforematch" in document && document.addEventListener("beforematch", (t => this.onBeforeMatch(t)))
     }
     initSectionHeaders() {
         this.$sections.forEach(((t, e) => {
-            const s = t.querySelector(`.${this.sectionHeaderClass}`);
-            if (!s) throw new ElementError({
+            const n = t.querySelector(`.${o}`);
+            if (!n) throw new ElementError({
                 component: Accordion,
-                identifier: `Section headers (\`<div class="${this.sectionHeaderClass}">\`)`
+                identifier: `Section headers (\`<div class="${o}">\`)`
             });
-            this.constructHeaderMarkup(s, e), this.setExpanded(this.isExpanded(t), t), s.addEventListener("click", (() => this.onSectionToggle(t))), this.setInitialState(t)
+            this.constructHeaderMarkup(n, e), this.setExpanded(this.isExpanded(t), t), n.addEventListener("click", (() => this.onSectionToggle(t))), this.setInitialState(t)
         }))
     }
     constructHeaderMarkup(t, e) {
-        const s = t.querySelector(`.${this.sectionButtonClass}`),
-            n = t.querySelector(`.${this.sectionHeadingClass}`),
-            i = t.querySelector(`.${this.sectionSummaryClass}`);
-        if (!n) throw new ElementError({
+        const n = t.querySelector(`.${i}`),
+            o = t.querySelector(`.${s}`),
+            r = t.querySelector(`.${u}`);
+        if (!o) throw new ElementError({
             component: Accordion,
-            identifier: `Section heading (\`.${this.sectionHeadingClass}\`)`
+            identifier: `Section heading (\`.${s}\`)`
         });
-        if (!s) throw new ElementError({
+        if (!n) throw new ElementError({
             component: Accordion,
-            identifier: `Section button placeholder (\`<span class="${this.sectionButtonClass}">\`)`
+            identifier: `Section button placeholder (\`<span class="${i}">\`)`
         });
-        const o = document.createElement("button");
-        o.setAttribute("type", "button"), o.setAttribute("aria-controls", `${this.$root.id}-content-${e+1}`);
-        for (const d of Array.from(s.attributes)) "id" !== d.name && o.setAttribute(d.name, d.value);
-        const r = document.createElement("span");
-        r.classList.add(this.sectionHeadingTextClass), r.id = s.id;
-        const a = document.createElement("span");
-        a.classList.add(this.sectionHeadingTextFocusClass), r.appendChild(a), Array.from(s.childNodes).forEach((t => a.appendChild(t)));
-        const c = document.createElement("span");
-        c.classList.add(this.sectionShowHideToggleClass), c.setAttribute("data-nosnippet", "");
-        const l = document.createElement("span");
-        l.classList.add(this.sectionShowHideToggleFocusClass), c.appendChild(l);
-        const h = document.createElement("span"),
-            u = document.createElement("span");
-        if (u.classList.add(this.upChevronIconClass), l.appendChild(u), h.classList.add(this.sectionShowHideTextClass), l.appendChild(h), o.appendChild(r), o.appendChild(this.getButtonPunctuationEl()), i) {
-            const t = document.createElement("span"),
-                e = document.createElement("span");
-            e.classList.add(this.sectionSummaryFocusClass), t.appendChild(e);
-            for (const s of Array.from(i.attributes)) t.setAttribute(s.name, s.value);
-            Array.from(i.childNodes).forEach((t => e.appendChild(t))), i.remove(), o.appendChild(t), o.appendChild(this.getButtonPunctuationEl())
-        }
-        o.appendChild(c), n.removeChild(s), n.appendChild(o)
+        const a = createElement("button", {
+            type: "button",
+            "aria-controls": `${this.$root.id}-content-${e+1}`
+        });
+        for (const i of Array.from(n.attributes)) "id" !== i.name && a.setAttribute(i.name, i.value);
+        a.appendChild(this.createHeadingText(n)), a.appendChild(this.getButtonPunctuationEl()), r && (r.remove(), a.appendChild(this.createSummarySpan(r)), a.appendChild(this.getButtonPunctuationEl())), a.appendChild(this.createShowHideToggle()), o.removeChild(n), o.appendChild(a)
+    }
+    createShowHideToggle() {
+        const t = createElement("span", {
+            class: "govuk-accordion__section-toggle-focus"
+        }, [createElement("span", {
+            class: c
+        }), createElement("span", {
+            class: a
+        })]);
+        return createElement("span", {
+            class: "govuk-accordion__section-toggle",
+            "data-nosnippet": ""
+        }, [t])
+    }
+    createHeadingText(t) {
+        const e = createElement("span", {
+            class: "govuk-accordion__section-heading-text-focus"
+        }, Array.from(t.childNodes));
+        return createElement("span", {
+            class: r,
+            id: t.id
+        }, [e])
+    }
+    createSummarySpan(t) {
+        const e = createElement("span", {
+                class: "govuk-accordion__section-summary-focus"
+            }, Array.from(t.childNodes)),
+            n = createElement("span", {}, [e]);
+        for (const i of Array.from(t.attributes)) n.setAttribute(i.name, i.value);
+        return n
     }
     onBeforeMatch(t) {
-        const e = t.target;
-        if (!(e instanceof Element)) return;
-        const s = e.closest(`.${this.sectionClass}`);
-        s && this.setExpanded(!0, s)
+        const n = t.target;
+        if (!(n instanceof Element)) return;
+        const i = n.closest(`.${e}`);
+        i && this.setExpanded(!0, i)
     }
     onSectionToggle(t) {
         const e = !this.isExpanded(t);
@@ -332,56 +377,58 @@ class Accordion extends ConfigurableComponent {
         })), this.updateShowAllButton(t)
     }
     setExpanded(t, e) {
-        const s = e.querySelector(`.${this.upChevronIconClass}`),
-            n = e.querySelector(`.${this.sectionShowHideTextClass}`),
-            i = e.querySelector(`.${this.sectionButtonClass}`),
-            o = e.querySelector(`.${this.sectionContentClass}`);
-        if (!o) throw new ElementError({
+        const o = e.querySelector(`.${c}`),
+            s = e.querySelector(`.${a}`),
+            d = e.querySelector(`.${i}`),
+            m = e.querySelector(`.${h}`);
+        if (!m) throw new ElementError({
             component: Accordion,
-            identifier: `Section content (\`<div class="${this.sectionContentClass}">\`)`
+            identifier: `Section content (\`<div class="${h}">\`)`
         });
-        if (!s || !n || !i) return;
-        const r = t ? this.i18n.t("hideSection") : this.i18n.t("showSection");
-        n.textContent = r, i.setAttribute("aria-expanded", `${t}`);
-        const a = [],
-            c = e.querySelector(`.${this.sectionHeadingTextClass}`);
-        c && a.push(`${c.textContent}`.trim());
-        const l = e.querySelector(`.${this.sectionSummaryClass}`);
-        l && a.push(`${l.textContent}`.trim());
-        const h = t ? this.i18n.t("hideSectionAriaLabel") : this.i18n.t("showSectionAriaLabel");
-        a.push(h), i.setAttribute("aria-label", a.join(" , ")), t ? (o.removeAttribute("hidden"), e.classList.add(this.sectionExpandedClass), s.classList.remove(this.downChevronIconClass)) : (o.setAttribute("hidden", "until-found"), e.classList.remove(this.sectionExpandedClass), s.classList.add(this.downChevronIconClass)), this.updateShowAllButton(this.areAllSectionsOpen())
+        if (!o || !s || !d) return;
+        const p = t ? this.i18n.t("hideSection") : this.i18n.t("showSection");
+        s.textContent = p, d.setAttribute("aria-expanded", `${t}`);
+        const g = [],
+            f = e.querySelector(`.${r}`);
+        f && g.push(`${f.textContent}`.trim());
+        const b = e.querySelector(`.${u}`);
+        b && g.push(`${b.textContent}`.trim());
+        const v = t ? this.i18n.t("hideSectionAriaLabel") : this.i18n.t("showSectionAriaLabel");
+        g.push(v), d.setAttribute("aria-label", g.join(" , ")), t ? (m.removeAttribute("hidden"), e.classList.add(n), o.classList.remove(l)) : (m.setAttribute("hidden", "until-found"), e.classList.remove(n), o.classList.add(l)), this.updateShowAllButton(this.areAllSectionsOpen())
     }
     isExpanded(t) {
-        return t.classList.contains(this.sectionExpandedClass)
+        return t.classList.contains(n)
     }
     areAllSectionsOpen() {
         return Array.from(this.$sections).every((t => this.isExpanded(t)))
     }
     updateShowAllButton(t) {
-        this.$showAllButton && this.$showAllText && this.$showAllIcon && (this.$showAllButton.setAttribute("aria-expanded", t.toString()), this.$showAllText.textContent = t ? this.i18n.t("hideAllSections") : this.i18n.t("showAllSections"), this.$showAllIcon.classList.toggle(this.downChevronIconClass, !t))
+        this.$showAllButton && this.$showAllText && this.$showAllIcon && (this.$showAllButton.setAttribute("aria-expanded", t.toString()), this.$showAllText.textContent = t ? this.i18n.t("hideAllSections") : this.i18n.t("showAllSections"), this.$showAllIcon.classList.toggle(l, !t))
     }
     getIdentifier(t) {
-        const e = t.querySelector(`.${this.sectionButtonClass}`);
+        const e = t.querySelector(`.${i}`);
         return null == e ? void 0 : e.getAttribute("aria-controls")
     }
     storeState(t, e) {
         if (!this.config.rememberExpanded) return;
-        const s = this.getIdentifier(t);
-        if (s) try {
-            window.sessionStorage.setItem(s, e.toString())
-        } catch (n) {}
+        const n = this.getIdentifier(t);
+        if (n) try {
+            window.sessionStorage.setItem(n, e.toString())
+        } catch (i) {}
     }
     setInitialState(t) {
         if (!this.config.rememberExpanded) return;
         const e = this.getIdentifier(t);
         if (e) try {
-            const s = window.sessionStorage.getItem(e);
-            null !== s && this.setExpanded("true" === s, t)
-        } catch (s) {}
+            const n = window.sessionStorage.getItem(e);
+            null !== n && this.setExpanded("true" === n, t)
+        } catch (n) {}
     }
     getButtonPunctuationEl() {
-        const t = document.createElement("span");
-        return t.classList.add("govuk-visually-hidden", this.sectionHeadingDividerClass), t.textContent = ", ", t
+        const t = createElement("span", {
+            class: "govuk-visually-hidden govuk-accordion__section-heading-divider"
+        });
+        return t.textContent = ", ", t
     }
 }
 Accordion.moduleName = "govuk-accordion", Accordion.defaults = Object.freeze({
@@ -420,8 +467,8 @@ class Button extends ConfigurableComponent {
 }
 
 function closestAttributeValue(t, e) {
-    const s = t.closest(`[${e}]`);
-    return s ? s.getAttribute(e) : null
+    const n = t.closest(`[${e}]`);
+    return n ? n.getAttribute(e) : null
 }
 Button.moduleName = "govuk-button", Button.defaults = Object.freeze({
     preventDoubleClick: !1
@@ -441,34 +488,34 @@ class CharacterCount extends ConfigurableComponent {
         }), e
     }
     constructor(t, e = {}) {
-        var s, n;
+        var n, i;
         super(t, e), this.$textarea = void 0, this.$visibleCountMessage = void 0, this.$screenReaderCountMessage = void 0, this.lastInputTimestamp = null, this.lastInputValue = "", this.valueChecker = null, this.i18n = void 0, this.maxLength = void 0;
-        const i = this.$root.querySelector(".govuk-js-character-count");
-        if (!(i instanceof HTMLTextAreaElement || i instanceof HTMLInputElement)) throw new ElementError({
+        const o = this.$root.querySelector(".govuk-js-character-count");
+        if (!(o instanceof HTMLTextAreaElement || o instanceof HTMLInputElement)) throw new ElementError({
             component: CharacterCount,
-            element: i,
+            element: o,
             expectedType: "HTMLTextareaElement or HTMLInputElement",
             identifier: "Form field (`.govuk-js-character-count`)"
         });
-        const o = function(t, e) {
-            const s = [];
-            for (const [n, i] of Object.entries(t)) {
+        const s = function(t, e) {
+            const n = [];
+            for (const [i, o] of Object.entries(t)) {
                 const t = [];
-                if (Array.isArray(i)) {
+                if (Array.isArray(o)) {
                     for (const {
-                            required: s,
-                            errorMessage: n
+                            required: n,
+                            errorMessage: i
                         }
-                        of i) s.every((t => !!e[t])) || t.push(n);
-                    "anyOf" !== n || i.length - t.length >= 1 || s.push(...t)
+                        of o) n.every((t => !!e[t])) || t.push(i);
+                    "anyOf" !== i || o.length - t.length >= 1 || n.push(...t)
                 }
             }
-            return s
+            return n
         }(CharacterCount.schema, this.config);
-        if (o[0]) throw new ConfigError(formatErrorMessage(CharacterCount, o[0]));
+        if (s[0]) throw new ConfigError(formatErrorMessage(CharacterCount, s[0]));
         this.i18n = new I18n(this.config.i18n, {
             locale: closestAttributeValue(this.$root, "lang")
-        }), this.maxLength = null != (s = null != (n = this.config.maxwords) ? n : this.config.maxlength) ? s : 1 / 0, this.$textarea = i;
+        }), this.maxLength = null != (n = null != (i = this.config.maxwords) ? i : this.config.maxlength) ? n : 1 / 0, this.$textarea = o;
         const r = `${this.$textarea.id}-info`,
             a = document.getElementById(r);
         if (!a) throw new ElementError({
@@ -525,8 +572,8 @@ class CharacterCount extends ConfigurableComponent {
     }
     formatCountMessage(t, e) {
         if (0 === t) return this.i18n.t(`${e}AtLimit`);
-        const s = t < 0 ? "OverLimit" : "UnderLimit";
-        return this.i18n.t(`${e}${s}`, {
+        const n = t < 0 ? "OverLimit" : "UnderLimit";
+        return this.i18n.t(`${e}${n}`, {
             count: Math.abs(t)
         })
     }
@@ -609,10 +656,10 @@ class Checkboxes extends Component {
     syncConditionalRevealWithInputState(t) {
         const e = t.getAttribute("aria-controls");
         if (!e) return;
-        const s = document.getElementById(e);
-        if (null != s && s.classList.contains("govuk-checkboxes__conditional")) {
+        const n = document.getElementById(e);
+        if (null != n && n.classList.contains("govuk-checkboxes__conditional")) {
             const e = t.checked;
-            t.setAttribute("aria-expanded", e.toString()), s.classList.toggle("govuk-checkboxes__conditional--hidden", !e)
+            t.setAttribute("aria-expanded", e.toString()), n.classList.toggle("govuk-checkboxes__conditional--hidden", !e)
         }
     }
     unCheckAllInputsExcept(t) {
@@ -645,25 +692,25 @@ class ErrorSummary extends ConfigurableComponent {
         if (!(t instanceof HTMLAnchorElement)) return !1;
         const e = getFragmentFromUrl(t.href);
         if (!e) return !1;
-        const s = document.getElementById(e);
-        if (!s) return !1;
-        const n = this.getAssociatedLegendOrLabel(s);
-        return !!n && (n.scrollIntoView(), s.focus({
+        const n = document.getElementById(e);
+        if (!n) return !1;
+        const i = this.getAssociatedLegendOrLabel(n);
+        return !!i && (i.scrollIntoView(), n.focus({
             preventScroll: !0
         }), !0)
     }
     getAssociatedLegendOrLabel(t) {
         var e;
-        const s = t.closest("fieldset");
-        if (s) {
-            const e = s.getElementsByTagName("legend");
+        const n = t.closest("fieldset");
+        if (n) {
+            const e = n.getElementsByTagName("legend");
             if (e.length) {
-                const s = e[0];
-                if (t instanceof HTMLInputElement && ("checkbox" === t.type || "radio" === t.type)) return s;
-                const n = s.getBoundingClientRect().top,
-                    i = t.getBoundingClientRect();
-                if (i.height && window.innerHeight) {
-                    if (i.top + i.height - n < window.innerHeight / 2) return s
+                const n = e[0];
+                if (t instanceof HTMLInputElement && ("checkbox" === t.type || "radio" === t.type)) return n;
+                const i = n.getBoundingClientRect().top,
+                    o = t.getBoundingClientRect();
+                if (o.height && window.innerHeight) {
+                    if (o.top + o.height - i < window.innerHeight / 2) return n
                 }
             }
         }
@@ -682,29 +729,34 @@ ErrorSummary.moduleName = "govuk-error-summary", ErrorSummary.defaults = Object.
 class ExitThisPage extends ConfigurableComponent {
     constructor(t, e = {}) {
         super(t, e), this.i18n = void 0, this.$button = void 0, this.$skiplinkButton = null, this.$updateSpan = null, this.$indicatorContainer = null, this.$overlay = null, this.keypressCounter = 0, this.lastKeyWasModified = !1, this.timeoutTime = 5e3, this.keypressTimeoutId = null, this.timeoutMessageId = null;
-        const s = this.$root.querySelector(".govuk-exit-this-page__button");
-        if (!(s instanceof HTMLAnchorElement)) throw new ElementError({
+        const n = this.$root.querySelector(".govuk-exit-this-page__button");
+        if (!(n instanceof HTMLAnchorElement)) throw new ElementError({
             component: ExitThisPage,
-            element: s,
+            element: n,
             expectedType: "HTMLAnchorElement",
             identifier: "Button (`.govuk-exit-this-page__button`)"
         });
-        this.i18n = new I18n(this.config.i18n), this.$button = s;
-        const n = document.querySelector(".govuk-js-exit-this-page-skiplink");
-        n instanceof HTMLAnchorElement && (this.$skiplinkButton = n), this.buildIndicator(), this.initUpdateSpan(), this.initButtonClickHandler(), "govukFrontendExitThisPageKeypress" in document.body.dataset || (document.addEventListener("keyup", this.handleKeypress.bind(this), !0), document.body.dataset.govukFrontendExitThisPageKeypress = "true"), window.addEventListener("pageshow", this.resetPage.bind(this))
+        this.i18n = new I18n(this.config.i18n), this.$button = n;
+        const i = document.querySelector(".govuk-js-exit-this-page-skiplink");
+        i instanceof HTMLAnchorElement && (this.$skiplinkButton = i), this.buildIndicator(), this.initUpdateSpan(), this.initButtonClickHandler(), "govukFrontendExitThisPageKeypress" in document.body.dataset || (document.addEventListener("keyup", this.handleKeypress.bind(this), !0), document.body.dataset.govukFrontendExitThisPageKeypress = "true"), window.addEventListener("pageshow", this.resetPage.bind(this))
     }
     initUpdateSpan() {
-        this.$updateSpan = document.createElement("span"), this.$updateSpan.setAttribute("role", "status"), this.$updateSpan.className = "govuk-visually-hidden", this.$root.appendChild(this.$updateSpan)
+        this.$updateSpan = createElement("span", {
+            role: "status",
+            class: "govuk-visually-hidden"
+        }), this.$root.appendChild(this.$updateSpan)
     }
     initButtonClickHandler() {
         this.$button.addEventListener("click", this.handleClick.bind(this)), this.$skiplinkButton && this.$skiplinkButton.addEventListener("click", this.handleClick.bind(this))
     }
     buildIndicator() {
-        this.$indicatorContainer = document.createElement("div"), this.$indicatorContainer.className = "govuk-exit-this-page__indicator", this.$indicatorContainer.setAttribute("aria-hidden", "true");
-        for (let t = 0; t < 3; t++) {
-            const t = document.createElement("div");
-            t.className = "govuk-exit-this-page__indicator-light", this.$indicatorContainer.appendChild(t)
-        }
+        this.$indicatorContainer = createElement("div", {
+            class: "govuk-exit-this-page__indicator",
+            "aria-hidden": "true"
+        });
+        for (let t = 0; t < 3; t++) this.$indicatorContainer.appendChild(createElement("div", {
+            class: "govuk-exit-this-page__indicator-light"
+        }));
         this.$button.appendChild(this.$indicatorContainer)
     }
     updateIndicator() {
@@ -715,7 +767,10 @@ class ExitThisPage extends ConfigurableComponent {
         }))
     }
     exitPage() {
-        this.$updateSpan && (this.$updateSpan.textContent = "", document.body.classList.add("govuk-exit-this-page-hide-content"), this.$overlay = document.createElement("div"), this.$overlay.className = "govuk-exit-this-page-overlay", this.$overlay.setAttribute("role", "alert"), document.body.appendChild(this.$overlay), this.$overlay.textContent = this.i18n.t("activated"), window.location.href = this.$button.href)
+        this.$updateSpan && (this.$updateSpan.textContent = "", document.body.classList.add("govuk-exit-this-page-hide-content"), this.$overlay = createElement("div", {
+            class: "govuk-exit-this-page-overlay",
+            role: "alert"
+        }), document.body.appendChild(this.$overlay), this.$overlay.textContent = this.i18n.t("activated"), window.location.href = this.$button.href)
     }
     handleClick(t) {
         t.preventDefault(), this.exitPage()
@@ -757,18 +812,18 @@ class Header extends Component {
         super(t), this.$menuButton = void 0, this.$menu = void 0, this.menuIsOpen = !1, this.mql = null;
         const e = this.$root.querySelector(".govuk-js-header-toggle");
         if (!e) return this;
-        const s = e.getAttribute("aria-controls");
-        if (!s) throw new ElementError({
+        const n = e.getAttribute("aria-controls");
+        if (!n) throw new ElementError({
             component: Header,
             identifier: 'Navigation button (`<button class="govuk-js-header-toggle">`) attribute (`aria-controls`)'
         });
-        const n = document.getElementById(s);
-        if (!n) throw new ElementError({
+        const i = document.getElementById(n);
+        if (!i) throw new ElementError({
             component: Header,
-            element: n,
-            identifier: `Navigation (\`<ul id="${s}">\`)`
+            element: i,
+            identifier: `Navigation (\`<ul id="${n}">\`)`
         });
-        this.$menu = n, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
+        this.$menu = i, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
     }
     setupResponsiveChecks() {
         const t = getBreakpoint("desktop");
@@ -803,27 +858,30 @@ NotificationBanner.moduleName = "govuk-notification-banner", NotificationBanner.
 class PasswordInput extends ConfigurableComponent {
     constructor(t, e = {}) {
         super(t, e), this.i18n = void 0, this.$input = void 0, this.$showHideButton = void 0, this.$screenReaderStatusMessage = void 0;
-        const s = this.$root.querySelector(".govuk-js-password-input-input");
-        if (!(s instanceof HTMLInputElement)) throw new ElementError({
+        const n = this.$root.querySelector(".govuk-js-password-input-input");
+        if (!(n instanceof HTMLInputElement)) throw new ElementError({
             component: PasswordInput,
-            element: s,
+            element: n,
             expectedType: "HTMLInputElement",
             identifier: "Form field (`.govuk-js-password-input-input`)"
         });
-        if ("password" !== s.type) throw new ElementError("Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.");
-        const n = this.$root.querySelector(".govuk-js-password-input-toggle");
-        if (!(n instanceof HTMLButtonElement)) throw new ElementError({
+        if ("password" !== n.type) throw new ElementError("Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.");
+        const i = this.$root.querySelector(".govuk-js-password-input-toggle");
+        if (!(i instanceof HTMLButtonElement)) throw new ElementError({
             component: PasswordInput,
-            element: n,
+            element: i,
             expectedType: "HTMLButtonElement",
             identifier: "Button (`.govuk-js-password-input-toggle`)"
         });
-        if ("button" !== n.type) throw new ElementError("Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.");
-        this.$input = s, this.$showHideButton = n, this.i18n = new I18n(this.config.i18n, {
+        if ("button" !== i.type) throw new ElementError("Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.");
+        this.$input = n, this.$showHideButton = i, this.i18n = new I18n(this.config.i18n, {
             locale: closestAttributeValue(this.$root, "lang")
         }), this.$showHideButton.removeAttribute("hidden");
-        const i = document.createElement("div");
-        i.className = "govuk-password-input__sr-status govuk-visually-hidden", i.setAttribute("aria-live", "polite"), this.$screenReaderStatusMessage = i, this.$input.insertAdjacentElement("afterend", i), this.$showHideButton.addEventListener("click", this.toggle.bind(this)), this.$input.form && this.$input.form.addEventListener("submit", (() => this.hide())), window.addEventListener("pageshow", (t => {
+        const o = createElement("div", {
+            class: "govuk-password-input__sr-status govuk-visually-hidden",
+            "aria-live": "polite"
+        });
+        this.$screenReaderStatusMessage = o, this.$input.insertAdjacentElement("afterend", o), this.$showHideButton.addEventListener("click", this.toggle.bind(this)), this.$input.form && this.$input.form.addEventListener("submit", (() => this.hide())), window.addEventListener("pageshow", (t => {
             t.persisted && "password" !== this.$input.type && this.hide()
         })), this.hide()
     }
@@ -840,9 +898,9 @@ class PasswordInput extends ConfigurableComponent {
         if (t === this.$input.type) return;
         this.$input.setAttribute("type", t);
         const e = "password" === t,
-            s = e ? "show" : "hide",
-            n = e ? "passwordHidden" : "passwordShown";
-        this.$showHideButton.innerText = this.i18n.t(`${s}Password`), this.$showHideButton.setAttribute("aria-label", this.i18n.t(`${s}PasswordAriaLabel`)), this.$screenReaderStatusMessage.innerText = this.i18n.t(`${n}Announcement`)
+            n = e ? "show" : "hide",
+            i = e ? "passwordHidden" : "passwordShown";
+        this.$showHideButton.innerText = this.i18n.t(`${n}Password`), this.$showHideButton.setAttribute("aria-label", this.i18n.t(`${n}PasswordAriaLabel`)), this.$screenReaderStatusMessage.innerText = this.i18n.t(`${i}Announcement`)
     }
 }
 PasswordInput.moduleName = "govuk-password-input", PasswordInput.defaults = Object.freeze({
@@ -886,21 +944,21 @@ class Radios extends Component {
     syncConditionalRevealWithInputState(t) {
         const e = t.getAttribute("aria-controls");
         if (!e) return;
-        const s = document.getElementById(e);
-        if (null != s && s.classList.contains("govuk-radios__conditional")) {
+        const n = document.getElementById(e);
+        if (null != n && n.classList.contains("govuk-radios__conditional")) {
             const e = t.checked;
-            t.setAttribute("aria-expanded", e.toString()), s.classList.toggle("govuk-radios__conditional--hidden", !e)
+            t.setAttribute("aria-expanded", e.toString()), n.classList.toggle("govuk-radios__conditional--hidden", !e)
         }
     }
     handleClick(t) {
         const e = t.target;
         if (!(e instanceof HTMLInputElement) || "radio" !== e.type) return;
-        const s = document.querySelectorAll('input[type="radio"][aria-controls]'),
-            n = e.form,
-            i = e.name;
-        s.forEach((t => {
-            const e = t.form === n;
-            t.name === i && e && this.syncConditionalRevealWithInputState(t)
+        const n = document.querySelectorAll('input[type="radio"][aria-controls]'),
+            i = e.form,
+            o = e.name;
+        n.forEach((t => {
+            const e = t.form === i;
+            t.name === o && e && this.syncConditionalRevealWithInputState(t)
         }))
     }
 }
@@ -910,18 +968,18 @@ class ServiceNavigation extends Component {
         super(t), this.$menuButton = void 0, this.$menu = void 0, this.menuIsOpen = !1, this.mql = null;
         const e = this.$root.querySelector(".govuk-js-service-navigation-toggle");
         if (!e) return this;
-        const s = e.getAttribute("aria-controls");
-        if (!s) throw new ElementError({
+        const n = e.getAttribute("aria-controls");
+        if (!n) throw new ElementError({
             component: ServiceNavigation,
             identifier: 'Navigation button (`<button class="govuk-js-service-navigation-toggle">`) attribute (`aria-controls`)'
         });
-        const n = document.getElementById(s);
-        if (!n) throw new ElementError({
+        const i = document.getElementById(n);
+        if (!i) throw new ElementError({
             component: ServiceNavigation,
-            element: n,
-            identifier: `Navigation (\`<ul id="${s}">\`)`
+            element: i,
+            identifier: `Navigation (\`<ul id="${n}">\`)`
         });
-        this.$menu = n, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
+        this.$menu = i, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
     }
     setupResponsiveChecks() {
         const t = getBreakpoint("tablet");
@@ -943,22 +1001,22 @@ class SkipLink extends Component {
     constructor(t) {
         var e;
         super(t);
-        const s = this.$root.hash,
-            n = null != (e = this.$root.getAttribute("href")) ? e : "";
-        let i;
+        const n = this.$root.hash,
+            i = null != (e = this.$root.getAttribute("href")) ? e : "";
+        let o;
         try {
-            i = new window.URL(this.$root.href)
+            o = new window.URL(this.$root.href)
         } catch (a) {
-            throw new ElementError(`Skip link: Target link (\`href="${n}"\`) is invalid`)
+            throw new ElementError(`Skip link: Target link (\`href="${i}"\`) is invalid`)
         }
-        if (i.origin !== window.location.origin || i.pathname !== window.location.pathname) return;
-        const o = getFragmentFromUrl(s);
-        if (!o) throw new ElementError(`Skip link: Target link (\`href="${n}"\`) has no hash fragment`);
-        const r = document.getElementById(o);
+        if (o.origin !== window.location.origin || o.pathname !== window.location.pathname) return;
+        const s = getFragmentFromUrl(n);
+        if (!s) throw new ElementError(`Skip link: Target link (\`href="${i}"\`) has no hash fragment`);
+        const r = document.getElementById(s);
         if (!r) throw new ElementError({
             component: SkipLink,
             element: r,
-            identifier: `Target content (\`id="${o}"\`)`
+            identifier: `Target content (\`id="${s}"\`)`
         });
         this.$root.addEventListener("click", (() => setFocus(r, {
             onBeforeFocus() {
@@ -980,17 +1038,17 @@ class Tabs extends Component {
             identifier: 'Links (`<a class="govuk-tabs__tab">`)'
         });
         this.$tabs = e, this.boundTabClick = this.onTabClick.bind(this), this.boundTabKeydown = this.onTabKeydown.bind(this), this.boundOnHashChange = this.onHashChange.bind(this);
-        const s = this.$root.querySelector(".govuk-tabs__list"),
-            n = this.$root.querySelectorAll("li.govuk-tabs__list-item");
-        if (!s) throw new ElementError({
+        const n = this.$root.querySelector(".govuk-tabs__list"),
+            i = this.$root.querySelectorAll("li.govuk-tabs__list-item");
+        if (!n) throw new ElementError({
             component: Tabs,
             identifier: 'List (`<ul class="govuk-tabs__list">`)'
         });
-        if (!n.length) throw new ElementError({
+        if (!i.length) throw new ElementError({
             component: Tabs,
             identifier: 'List items (`<li class="govuk-tabs__list-item">`)'
         });
-        this.$tabList = s, this.$tabListItems = n, this.setupResponsiveChecks()
+        this.$tabList = n, this.$tabListItems = i, this.setupResponsiveChecks()
     }
     setupResponsiveChecks() {
         const t = getBreakpoint("tablet");
@@ -1026,8 +1084,8 @@ class Tabs extends Component {
             e = this.getTab(t);
         if (!e) return;
         if (this.changingHash) return void(this.changingHash = !1);
-        const s = this.getCurrentTab();
-        s && (this.hideTab(s), this.showTab(e), e.focus())
+        const n = this.getCurrentTab();
+        n && (this.hideTab(n), this.showTab(e), e.focus())
     }
     hideTab(t) {
         this.unhighlightTab(t), this.hidePanel(t)
@@ -1042,8 +1100,8 @@ class Tabs extends Component {
         const e = getFragmentFromUrl(t.href);
         if (!e) return;
         t.setAttribute("id", `tab_${e}`), t.setAttribute("role", "tab"), t.setAttribute("aria-controls", e), t.setAttribute("aria-selected", "false"), t.setAttribute("tabindex", "-1");
-        const s = this.getPanel(t);
-        s && (s.setAttribute("role", "tabpanel"), s.setAttribute("aria-labelledby", t.id), s.classList.add(this.jsHiddenClass))
+        const n = this.getPanel(t);
+        n && (n.setAttribute("role", "tabpanel"), n.setAttribute("aria-labelledby", t.id), n.classList.add(this.jsHiddenClass))
     }
     unsetAttributes(t) {
         t.removeAttribute("id"), t.removeAttribute("role"), t.removeAttribute("aria-controls"), t.removeAttribute("aria-selected"), t.removeAttribute("tabindex");
@@ -1052,14 +1110,14 @@ class Tabs extends Component {
     }
     onTabClick(t) {
         const e = this.getCurrentTab(),
-            s = t.currentTarget;
-        e && s instanceof HTMLAnchorElement && (t.preventDefault(), this.hideTab(e), this.showTab(s), this.createHistoryEntry(s))
+            n = t.currentTarget;
+        e && n instanceof HTMLAnchorElement && (t.preventDefault(), this.hideTab(e), this.showTab(n), this.createHistoryEntry(n))
     }
     createHistoryEntry(t) {
         const e = this.getPanel(t);
         if (!e) return;
-        const s = e.id;
-        e.id = "", this.changingHash = !0, window.location.hash = s, e.id = s
+        const n = e.id;
+        e.id = "", this.changingHash = !0, window.location.hash = n, e.id = n
     }
     onTabKeydown(t) {
         switch (t.key) {
@@ -1077,16 +1135,16 @@ class Tabs extends Component {
         if (null == t || !t.parentElement) return;
         const e = t.parentElement.nextElementSibling;
         if (!e) return;
-        const s = e.querySelector("a.govuk-tabs__tab");
-        s && (this.hideTab(t), this.showTab(s), s.focus(), this.createHistoryEntry(s))
+        const n = e.querySelector("a.govuk-tabs__tab");
+        n && (this.hideTab(t), this.showTab(n), n.focus(), this.createHistoryEntry(n))
     }
     activatePreviousTab() {
         const t = this.getCurrentTab();
         if (null == t || !t.parentElement) return;
         const e = t.parentElement.previousElementSibling;
         if (!e) return;
-        const s = e.querySelector("a.govuk-tabs__tab");
-        s && (this.hideTab(t), this.showTab(s), s.focus(), this.createHistoryEntry(s))
+        const n = e.querySelector("a.govuk-tabs__tab");
+        n && (this.hideTab(t), this.showTab(n), n.focus(), this.createHistoryEntry(n))
     }
     getPanel(t) {
         const e = getFragmentFromUrl(t.href);
@@ -1116,7 +1174,7 @@ function initAll(t) {
     if (t = void 0 !== t ? t : {}, !isSupported()) return void(t.onError ? t.onError(new SupportError, {
         config: t
     }) : console.log(new SupportError));
-    const s = [
+    const n = [
             [Accordion, t.accordion],
             [Button, t.button],
             [CharacterCount, t.characterCount],
@@ -1131,32 +1189,32 @@ function initAll(t) {
             [SkipLink],
             [Tabs]
         ],
-        n = {
+        i = {
             scope: null != (e = t.scope) ? e : document,
             onError: t.onError
         };
-    s.forEach((([Component, t]) => {
-        createAll(Component, t, n)
+    n.forEach((([Component, t]) => {
+        createAll(Component, t, i)
     }))
 }
 
 function createAll(Component, t, e) {
-    let s, n = document;
-    var i;
-    "object" == typeof e && (n = null != (i = e.scope) ? i : n, s = e.onError);
-    "function" == typeof e && (s = e), e instanceof HTMLElement && (n = e);
-    const o = n.querySelectorAll(`[data-module="${Component.moduleName}"]`);
-    return isSupported() ? Array.from(o).map((e => {
+    let n, i = document;
+    var o;
+    "object" == typeof e && (i = null != (o = e.scope) ? o : i, n = e.onError);
+    "function" == typeof e && (n = e), e instanceof HTMLElement && (i = e);
+    const s = i.querySelectorAll(`[data-module="${Component.moduleName}"]`);
+    return isSupported() ? Array.from(s).map((e => {
         try {
             return void 0 !== t ? new Component(e, t) : new Component(e)
-        } catch (n) {
-            return s ? s(n, {
+        } catch (i) {
+            return n ? n(i, {
                 element: e,
                 component: Component,
                 config: t
-            }) : console.log(n), null
+            }) : console.log(i), null
         }
-    })).filter(Boolean) : (s ? s(new SupportError, {
+    })).filter(Boolean) : (n ? n(new SupportError, {
         component: Component,
         config: t
     }) : console.log(new SupportError), [])

Action run for 6c61aa5

Copy link

github-actions bot commented Dec 13, 2024

Other changes to npm package

diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.js b/packages/govuk-frontend/dist/govuk/all.bundle.js
index a04a0facd..bbca92d85 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.js
@@ -351,6 +351,19 @@
    * @typedef {typeof Component & ChildClass<ConfigurationType>} ChildClassConstructor<ConfigurationType>
    */
 
+  function createElement(tagName, attributes = {}, children) {
+    const el = document.createElement(tagName);
+    Object.entries(attributes).forEach(([name, value]) => {
+      el.setAttribute(name, value);
+    });
+    if (children) {
+      for (const child of children) {
+        el.appendChild(child);
+      }
+    }
+    return el;
+  }
+
   class I18n {
     constructor(translations = {}, config = {}) {
       var _config$locale;
@@ -544,6 +557,18 @@
     }
   };
 
+  const sectionClass = 'govuk-accordion__section';
+  const sectionExpandedModifier = 'govuk-accordion__section--expanded';
+  const sectionButtonClass = 'govuk-accordion__section-button';
+  const sectionHeaderClass = 'govuk-accordion__section-header';
+  const sectionHeadingClass = 'govuk-accordion__section-heading';
+  const sectionHeadingTextClass = 'govuk-accordion__section-heading-text';
+  const sectionToggleTextClass = 'govuk-accordion__section-toggle-text';
+  const iconClass = 'govuk-accordion-nav__chevron';
+  const iconOpenModifier = 'govuk-accordion-nav__chevron--down';
+  const sectionSummaryClass = 'govuk-accordion__section-summary';
+  const sectionContentClass = 'govuk-accordion__section-content';
+
   /**
    * Accordion component
    *
@@ -567,35 +592,16 @@
     constructor($root, config = {}) {
       super($root, config);
       this.i18n = void 0;
-      this.controlsClass = 'govuk-accordion__controls';
-      this.showAllClass = 'govuk-accordion__show-all';
-      this.showAllTextClass = 'govuk-accordion__show-all-text';
-      this.sectionClass = 'govuk-accordion__section';
-      this.sectionExpandedClass = 'govuk-accordion__section--expanded';
-      this.sectionButtonClass = 'govuk-accordion__section-button';
-      this.sectionHeaderClass = 'govuk-accordion__section-header';
-      this.sectionHeadingClass = 'govuk-accordion__section-heading';
-      this.sectionHeadingDividerClass = 'govuk-accordion__section-heading-divider';
-      this.sectionHeadingTextClass = 'govuk-accordion__section-heading-text';
-      this.sectionHeadingTextFocusClass = 'govuk-accordion__section-heading-text-focus';
-      this.sectionShowHideToggleClass = 'govuk-accordion__section-toggle';
-      this.sectionShowHideToggleFocusClass = 'govuk-accordion__section-toggle-focus';
-      this.sectionShowHideTextClass = 'govuk-accordion__section-toggle-text';
-      this.upChevronIconClass = 'govuk-accordion-nav__chevron';
-      this.downChevronIconClass = 'govuk-accordion-nav__chevron--down';
-      this.sectionSummaryClass = 'govuk-accordion__section-summary';
-      this.sectionSummaryFocusClass = 'govuk-accordion__section-summary-focus';
-      this.sectionContentClass = 'govuk-accordion__section-content';
       this.$sections = void 0;
       this.$showAllButton = null;
       this.$showAllIcon = null;
       this.$showAllText = null;
       this.i18n = new I18n(this.config.i18n);
-      const $sections = this.$root.querySelectorAll(`.${this.sectionClass}`);
+      const $sections = this.$root.querySelectorAll(`.${sectionClass}`);
       if (!$sections.length) {
         throw new ElementError({
           component: Accordion,
-          identifier: `Sections (\`<div class="${this.sectionClass}">\`)`
+          identifier: `Sections (\`<div class="${sectionClass}">\`)`
         });
       }
       this.$sections = $sections;
@@ -604,19 +610,23 @@
       this.updateShowAllButton(this.areAllSectionsOpen());
     }
     initControls() {
-      this.$showAllButton = document.createElement('button');
-      this.$showAllButton.setAttribute('type', 'button');
-      this.$showAllButton.setAttribute('class', this.showAllClass);
-      this.$showAllButton.setAttribute('aria-expanded', 'false');
-      this.$showAllIcon = document.createElement('span');
-      this.$showAllIcon.classList.add(this.upChevronIconClass);
+      this.$showAllButton = createElement('button', {
+        type: 'button',
+        class: 'govuk-accordion__show-all',
+        'aria-expanded': 'false'
+      });
+      this.$showAllIcon = createElement('span', {
+        class: iconClass
+      });
       this.$showAllButton.appendChild(this.$showAllIcon);
-      const $accordionControls = document.createElement('div');
-      $accordionControls.setAttribute('class', this.controlsClass);
+      const $accordionControls = createElement('div', {
+        class: 'govuk-accordion__controls'
+      });
       $accordionControls.appendChild(this.$showAllButton);
       this.$root.insertBefore($accordionControls, this.$root.firstChild);
-      this.$showAllText = document.createElement('span');
-      this.$showAllText.classList.add(this.showAllTextClass);
+      this.$showAllText = createElement('span', {
+        class: 'govuk-accordion__show-all-text'
+      });
       this.$showAllButton.appendChild(this.$showAllText);
       this.$showAllButton.addEventListener('click', () => this.onShowOrHideAllToggle());
       if ('onbeforematch' in document) {
@@ -625,11 +635,11 @@
     }
     initSectionHeaders() {
       this.$sections.forEach(($section, i) => {
-        const $header = $section.querySelector(`.${this.sectionHeaderClass}`);
+        const $header = $section.querySelector(`.${sectionHeaderClass}`);
         if (!$header) {
           throw new ElementError({
             component: Accordion,
-            identifier: `Section headers (\`<div class="${this.sectionHeaderClass}">\`)`
+            identifier: `Section headers (\`<div class="${sectionHeaderClass}">\`)`
           });
         }
         this.constructHeaderMarkup($header, i);
@@ -639,73 +649,105 @@
       });
     }
     constructHeaderMarkup($header, index) {
-      const $span = $header.querySelector(`.${this.sectionButtonClass}`);
-      const $heading = $header.querySelector(`.${this.sectionHeadingClass}`);
-      const $summary = $header.querySelector(`.${this.sectionSummaryClass}`);
+      const $span = $header.querySelector(`.${sectionButtonClass}`);
+      const $heading = $header.querySelector(`.${sectionHeadingClass}`);
+      const $summary = $header.querySelector(`.${sectionSummaryClass}`);
       if (!$heading) {
         throw new ElementError({
           component: Accordion,
-          identifier: `Section heading (\`.${this.sectionHeadingClass}\`)`
+          identifier: `Section heading (\`.${sectionHeadingClass}\`)`
         });
       }
       if (!$span) {
         throw new ElementError({
           component: Accordion,
-          identifier: `Section button placeholder (\`<span class="${this.sectionButtonClass}">\`)`
+          identifier: `Section button placeholder (\`<span class="${sectionButtonClass}">\`)`
         });
       }
-      const $button = document.createElement('button');
-      $button.setAttribute('type', 'button');
-      $button.setAttribute('aria-controls', `${this.$root.id}-content-${index + 1}`);
+      const $button = createElement('button', {
+        type: 'button',
+        'aria-controls': `${this.$root.id}-content-${index + 1}`
+      });
       for (const attr of Array.from($span.attributes)) {
         if (attr.name !== 'id') {
           $button.setAttribute(attr.name, attr.value);
         }
       }
-      const $headingText = document.createElement('span');
-      $headingText.classList.add(this.sectionHeadingTextClass);
-      $headingText.id = $span.id;
-      const $headingTextFocus = document.createElement('span');
-      $headingTextFocus.classList.add(this.sectionHeadingTextFocusClass);
-      $headingText.appendChild($headingTextFocus);
-      Array.from($span.childNodes).forEach($child => $headingTextFocus.appendChild($child));
-      const $showHideToggle = document.createElement('span');
-      $showHideToggle.classList.add(this.sectionShowHideToggleClass);
-      $showHideToggle.setAttribute('data-nosnippet', '');
-      const $showHideToggleFocus = document.createElement('span');
-      $showHideToggleFocus.classList.add(this.sectionShowHideToggleFocusClass);
-      $showHideToggle.appendChild($showHideToggleFocus);
-      const $showHideText = document.createElement('span');
-      const $showHideIcon = document.createElement('span');
-      $showHideIcon.classList.add(this.upChevronIconClass);
-      $showHideToggleFocus.appendChild($showHideIcon);
-      $showHideText.classList.add(this.sectionShowHideTextClass);
-      $showHideToggleFocus.appendChild($showHideText);
-      $button.appendChild($headingText);
+      $button.appendChild(this.createHeadingText($span));
       $button.appendChild(this.getButtonPunctuationEl());
       if ($summary) {
-        const $summarySpan = document.createElement('span');
-        const $summarySpanFocus = document.createElement('span');
-        $summarySpanFocus.classList.add(this.sectionSummaryFocusClass);
-        $summarySpan.appendChild($summarySpanFocus);
-        for (const attr of Array.from($summary.attributes)) {
-          $summarySpan.setAttribute(attr.name, attr.value);
-        }
-        Array.from($summary.childNodes).forEach($child => $summarySpanFocus.appendChild($child));
         $summary.remove();
-        $button.appendChild($summarySpan);
+        $button.appendChild(this.createSummarySpan($summary));
         $button.appendChild(this.getButtonPunctuationEl());
       }
-      $button.appendChild($showHideToggle);
+      $button.appendChild(this.createShowHideToggle());
       $heading.removeChild($span);
       $heading.appendChild($button);
     }
+
+    /**
+     * Creates a `<span>` rendering the 'Show'/'Hide' toggle
+     *
+     * @returns {HTMLSpanElement} - The `<span>` with the visual representation of the 'Show/Hide' toggle
+     */
+    createShowHideToggle() {
+      const $showHideToggleFocus = createElement('span', {
+        class: 'govuk-accordion__section-toggle-focus'
+      }, [createElement('span', {
+        class: iconClass
+      }), createElement('span', {
+        class: sectionToggleTextClass
+      })]);
+      const $showHideToggle = createElement('span', {
+        class: 'govuk-accordion__section-toggle',
+        'data-nosnippet': ''
+      }, [$showHideToggleFocus]);
+      return $showHideToggle;
+    }
+
+    /**
+     * Creates the `<span>` containing the text of the section's heading
+     *
+     * @param {Element} $span - The heading of the span
+     * @returns {HTMLSpanElement} - The `<span>` containing the text of the section's heading
+     */
+    createHeadingText($span) {
+      const $headingTextFocus = createElement('span', {
+        class: 'govuk-accordion__section-heading-text-focus'
+      }, Array.from($span.childNodes));
+      const $headingText = createElement('span', {
+        class: sectionHeadingTextClass,
+        id: $span.id
+      }, [$headingTextFocus]);
+      return $headingText;
+    }
+
+    /**
+     * Creates the `<span>` element with the summary for the section
+     *
+     * This is necessary because the summary line text is now inside
+     * a button element, which can only contain phrasing content, and
+     * not a `<div>` element
+     *
+     * @param {Element} $summary - The original `<div>` containing the summary
+     * @returns {HTMLSpanElement} - The `<span>` element containing the summary
+     */
+    createSummarySpan($summary) {
+      const $summarySpanFocus = createElement('span', {
+        class: 'govuk-accordion__section-summary-focus'
+      }, Array.from($summary.childNodes));
+      const $summarySpan = createElement('span', {}, [$summarySpanFocus]);
+      for (const attr of Array.from($summary.attributes)) {
+        $summarySpan.setAttribute(attr.name, attr.value);
+      }
+      return $summarySpan;
+    }
     onBeforeMatch(event) {
       const $fragment = event.target;
       if (!($fragment instanceof Element)) {
         return;
       }
-      const $section = $fragment.closest(`.${this.sectionClass}`);
+      const $section = $fragment.closest(`.${sectionClass}`);
       if ($section) {
         this.setExpanded(true, $section);
       }
@@ -724,14 +766,14 @@
       this.updateShowAllButton(nowExpanded);
     }
     setExpanded(expanded, $section) {
-      const $showHideIcon = $section.querySelector(`.${this.upChevronIconClass}`);
-      const $showHideText = $section.querySelector(`.${this.sectionShowHideTextClass}`);
-      const $button = $section.querySelector(`.${this.sectionButtonClass}`);
-      const $content = $section.querySelector(`.${this.sectionContentClass}`);
+      const $showHideIcon = $section.querySelector(`.${iconClass}`);
+      const $showHideText = $section.querySelector(`.${sectionToggleTextClass}`);
+      const $button = $section.querySelector(`.${sectionButtonClass}`);
+      const $content = $section.querySelector(`.${sectionContentClass}`);
       if (!$content) {
         throw new ElementError({
           component: Accordion,
-          identifier: `Section content (\`<div class="${this.sectionContentClass}">\`)`
+          identifier: `Section content (\`<div class="${sectionContentClass}">\`)`
         });
       }
       if (!$showHideIcon || !$showHideText || !$button) {
@@ -741,11 +783,11 @@
       $showHideText.textContent = newButtonText;
       $button.setAttribute('aria-expanded', `${expanded}`);
       const ariaLabelParts = [];
-      const $headingText = $section.querySelector(`.${this.sectionHeadingTextClass}`);
+      const $headingText = $section.querySelector(`.${sectionHeadingTextClass}`);
       if ($headingText) {
         ariaLabelParts.push(`${$headingText.textContent}`.trim());
       }
-      const $summary = $section.querySelector(`.${this.sectionSummaryClass}`);
+      const $summary = $section.querySelector(`.${sectionSummaryClass}`);
       if ($summary) {
         ariaLabelParts.push(`${$summary.textContent}`.trim());
       }
@@ -754,17 +796,17 @@
       $button.setAttribute('aria-label', ariaLabelParts.join(' , '));
       if (expanded) {
         $content.removeAttribute('hidden');
-        $section.classList.add(this.sectionExpandedClass);
-        $showHideIcon.classList.remove(this.downChevronIconClass);
+        $section.classList.add(sectionExpandedModifier);
+        $showHideIcon.classList.remove(iconOpenModifier);
       } else {
         $content.setAttribute('hidden', 'until-found');
-        $section.classList.remove(this.sectionExpandedClass);
-        $showHideIcon.classList.add(this.downChevronIconClass);
+        $section.classList.remove(sectionExpandedModifier);
+        $showHideIcon.classList.add(iconOpenModifier);
       }
       this.updateShowAllButton(this.areAllSectionsOpen());
     }
     isExpanded($section) {
-      return $section.classList.contains(this.sectionExpandedClass);
+      return $section.classList.contains(sectionExpandedModifier);
     }
     areAllSectionsOpen() {
       return Array.from(this.$sections).every($section => this.isExpanded($section));
@@ -775,7 +817,7 @@
       }
       this.$showAllButton.setAttribute('aria-expanded', expanded.toString());
       this.$showAllText.textContent = expanded ? this.i18n.t('hideAllSections') : this.i18n.t('showAllSections');
-      this.$showAllIcon.classList.toggle(this.downChevronIconClass, !expanded);
+      this.$showAllIcon.classList.toggle(iconOpenModifier, !expanded);
     }
 
     /**
@@ -789,7 +831,7 @@
      * @returns {string | undefined | null} Identifier for section
      */
     getIdentifier($section) {
-      const $button = $section.querySelector(`.${this.sectionButtonClass}`);
+      const $button = $section.querySelector(`.${sectionButtonClass}`);
       return $button == null ? void 0 : $button.getAttribute('aria-controls');
     }
     storeState($section, isExpanded) {
@@ -818,10 +860,11 @@
       }
     }
     getButtonPunctuationEl() {
-      const $punctuationEl = document.createElement('span');
-      $punctuationEl.classList.add('govuk-visually-hidden', this.sectionHeadingDividerClass);
-      $punctuationEl.textContent = ', ';
-      return $punctuationEl;
+      const $element = createElement('span', {
+        class: 'govuk-visually-hidden govuk-accordion__section-heading-divider'
+      });
+      $element.textContent = ', ';
+      return $element;
     }
   }
 
@@ -1494,9 +1537,10 @@
       window.addEventListener('pageshow', this.resetPage.bind(this));
     }
     initUpdateSpan() {
-      this.$updateSpan = document.createElement('span');
-      this.$updateSpan.setAttribute('role', 'status');
-      this.$updateSpan.className = 'govuk-visually-hidden';
+      this.$updateSpan = createElement('span', {
+        role: 'status',
+        class: 'govuk-visually-hidden'
+      });
       this.$root.appendChild(this.$updateSpan);
     }
     initButtonClickHandler() {
@@ -1506,13 +1550,14 @@
       }
     }
     buildIndicator() {
-      this.$indicatorContainer = document.createElement('div');
-      this.$indicatorContainer.className = 'govuk-exit-this-page__indicator';
-      this.$indicatorContainer.setAttribute('aria-hidden', 'true');
+      this.$indicatorContainer = createElement('div', {
+        class: 'govuk-exit-this-page__indicator',
+        'aria-hidden': 'true'
+      });
       for (let i = 0; i < 3; i++) {
-        const $indicator = document.createElement('div');
-        $indicator.className = 'govuk-exit-this-page__indicator-light';
-        this.$indicatorContainer.appendChild($indicator);
+        this.$indicatorContainer.appendChild(createElement('div', {
+          class: 'govuk-exit-this-page__indicator-light'
+        }));
       }
       this.$button.appendChild(this.$indicatorContainer);
     }
@@ -1532,9 +1577,10 @@
       }
       this.$updateSpan.textContent = '';
       document.body.classList.add('govuk-exit-this-page-hide-content');
-      this.$overlay = document.createElement('div');
-      this.$overlay.className = 'govuk-exit-this-page-overlay';
-      this.$overlay.setAttribute('role', 'alert');
+      this.$overlay = createElement('div', {
+        class: 'govuk-exit-this-page-overlay',
+        role: 'alert'
+      });
       document.body.appendChild(this.$overlay);
       this.$overlay.textContent = this.i18n.t('activated');
       window.location.href = this.$button.href;
@@ -1835,9 +1881,10 @@
         locale: closestAttributeValue(this.$root, 'lang')
       });
       this.$showHideButton.removeAttribute('hidden');
-      const $screenReaderStatusMessage = document.createElement('div');
-      $screenReaderStatusMessage.className = 'govuk-password-input__sr-status govuk-visually-hidden';
-      $screenReaderStatusMessage.setAttribute('aria-live', 'polite');
+      const $screenReaderStatusMessage = createElement('div', {
+        class: 'govuk-password-input__sr-status govuk-visually-hidden',
+        'aria-live': 'polite'
+      });
       this.$screenReaderStatusMessage = $screenReaderStatusMessage;
       this.$input.insertAdjacentElement('afterend', $screenReaderStatusMessage);
       this.$showHideButton.addEventListener('click', this.toggle.bind(this));
diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.mjs b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
index adc70f031..d1aaf3f8e 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
@@ -345,6 +345,19 @@ function extractConfigByNamespace(schema, dataset, namespace) {
  * @typedef {typeof Component & ChildClass<ConfigurationType>} ChildClassConstructor<ConfigurationType>
  */
 
+function createElement(tagName, attributes = {}, children) {
+  const el = document.createElement(tagName);
+  Object.entries(attributes).forEach(([name, value]) => {
+    el.setAttribute(name, value);
+  });
+  if (children) {
+    for (const child of children) {
+      el.appendChild(child);
+    }
+  }
+  return el;
+}
+
 class I18n {
   constructor(translations = {}, config = {}) {
     var _config$locale;
@@ -538,6 +551,18 @@ I18n.pluralRules = {
   }
 };
 
+const sectionClass = 'govuk-accordion__section';
+const sectionExpandedModifier = 'govuk-accordion__section--expanded';
+const sectionButtonClass = 'govuk-accordion__section-button';
+const sectionHeaderClass = 'govuk-accordion__section-header';
+const sectionHeadingClass = 'govuk-accordion__section-heading';
+const sectionHeadingTextClass = 'govuk-accordion__section-heading-text';
+const sectionToggleTextClass = 'govuk-accordion__section-toggle-text';
+const iconClass = 'govuk-accordion-nav__chevron';
+const iconOpenModifier = 'govuk-accordion-nav__chevron--down';
+const sectionSummaryClass = 'govuk-accordion__section-summary';
+const sectionContentClass = 'govuk-accordion__section-content';
+
 /**
  * Accordion component
  *
@@ -561,35 +586,16 @@ class Accordion extends ConfigurableComponent {
   constructor($root, config = {}) {
     super($root, config);
     this.i18n = void 0;
-    this.controlsClass = 'govuk-accordion__controls';
-    this.showAllClass = 'govuk-accordion__show-all';
-    this.showAllTextClass = 'govuk-accordion__show-all-text';
-    this.sectionClass = 'govuk-accordion__section';
-    this.sectionExpandedClass = 'govuk-accordion__section--expanded';
-    this.sectionButtonClass = 'govuk-accordion__section-button';
-    this.sectionHeaderClass = 'govuk-accordion__section-header';
-    this.sectionHeadingClass = 'govuk-accordion__section-heading';
-    this.sectionHeadingDividerClass = 'govuk-accordion__section-heading-divider';
-    this.sectionHeadingTextClass = 'govuk-accordion__section-heading-text';
-    this.sectionHeadingTextFocusClass = 'govuk-accordion__section-heading-text-focus';
-    this.sectionShowHideToggleClass = 'govuk-accordion__section-toggle';
-    this.sectionShowHideToggleFocusClass = 'govuk-accordion__section-toggle-focus';
-    this.sectionShowHideTextClass = 'govuk-accordion__section-toggle-text';
-    this.upChevronIconClass = 'govuk-accordion-nav__chevron';
-    this.downChevronIconClass = 'govuk-accordion-nav__chevron--down';
-    this.sectionSummaryClass = 'govuk-accordion__section-summary';
-    this.sectionSummaryFocusClass = 'govuk-accordion__section-summary-focus';
-    this.sectionContentClass = 'govuk-accordion__section-content';
     this.$sections = void 0;
     this.$showAllButton = null;
     this.$showAllIcon = null;
     this.$showAllText = null;
     this.i18n = new I18n(this.config.i18n);
-    const $sections = this.$root.querySelectorAll(`.${this.sectionClass}`);
+    const $sections = this.$root.querySelectorAll(`.${sectionClass}`);
     if (!$sections.length) {
       throw new ElementError({
         component: Accordion,
-        identifier: `Sections (\`<div class="${this.sectionClass}">\`)`
+        identifier: `Sections (\`<div class="${sectionClass}">\`)`
       });
     }
     this.$sections = $sections;
@@ -598,19 +604,23 @@ class Accordion extends ConfigurableComponent {
     this.updateShowAllButton(this.areAllSectionsOpen());
   }
   initControls() {
-    this.$showAllButton = document.createElement('button');
-    this.$showAllButton.setAttribute('type', 'button');
-    this.$showAllButton.setAttribute('class', this.showAllClass);
-    this.$showAllButton.setAttribute('aria-expanded', 'false');
-    this.$showAllIcon = document.createElement('span');
-    this.$showAllIcon.classList.add(this.upChevronIconClass);
+    this.$showAllButton = createElement('button', {
+      type: 'button',
+      class: 'govuk-accordion__show-all',
+      'aria-expanded': 'false'
+    });
+    this.$showAllIcon = createElement('span', {
+      class: iconClass
+    });
     this.$showAllButton.appendChild(this.$showAllIcon);
-    const $accordionControls = document.createElement('div');
-    $accordionControls.setAttribute('class', this.controlsClass);
+    const $accordionControls = createElement('div', {
+      class: 'govuk-accordion__controls'
+    });
     $accordionControls.appendChild(this.$showAllButton);
     this.$root.insertBefore($accordionControls, this.$root.firstChild);
-    this.$showAllText = document.createElement('span');
-    this.$showAllText.classList.add(this.showAllTextClass);
+    this.$showAllText = createElement('span', {
+      class: 'govuk-accordion__show-all-text'
+    });
     this.$showAllButton.appendChild(this.$showAllText);
     this.$showAllButton.addEventListener('click', () => this.onShowOrHideAllToggle());
     if ('onbeforematch' in document) {
@@ -619,11 +629,11 @@ class Accordion extends ConfigurableComponent {
   }
   initSectionHeaders() {
     this.$sections.forEach(($section, i) => {
-      const $header = $section.querySelector(`.${this.sectionHeaderClass}`);
+      const $header = $section.querySelector(`.${sectionHeaderClass}`);
       if (!$header) {
         throw new ElementError({
           component: Accordion,
-          identifier: `Section headers (\`<div class="${this.sectionHeaderClass}">\`)`
+          identifier: `Section headers (\`<div class="${sectionHeaderClass}">\`)`
         });
       }
       this.constructHeaderMarkup($header, i);
@@ -633,73 +643,105 @@ class Accordion extends ConfigurableComponent {
     });
   }
   constructHeaderMarkup($header, index) {
-    const $span = $header.querySelector(`.${this.sectionButtonClass}`);
-    const $heading = $header.querySelector(`.${this.sectionHeadingClass}`);
-    const $summary = $header.querySelector(`.${this.sectionSummaryClass}`);
+    const $span = $header.querySelector(`.${sectionButtonClass}`);
+    const $heading = $header.querySelector(`.${sectionHeadingClass}`);
+    const $summary = $header.querySelector(`.${sectionSummaryClass}`);
     if (!$heading) {
       throw new ElementError({
         component: Accordion,
-        identifier: `Section heading (\`.${this.sectionHeadingClass}\`)`
+        identifier: `Section heading (\`.${sectionHeadingClass}\`)`
       });
     }
     if (!$span) {
       throw new ElementError({
         component: Accordion,
-        identifier: `Section button placeholder (\`<span class="${this.sectionButtonClass}">\`)`
+        identifier: `Section button placeholder (\`<span class="${sectionButtonClass}">\`)`
       });
     }
-    const $button = document.createElement('button');
-    $button.setAttribute('type', 'button');
-    $button.setAttribute('aria-controls', `${this.$root.id}-content-${index + 1}`);
+    const $button = createElement('button', {
+      type: 'button',
+      'aria-controls': `${this.$root.id}-content-${index + 1}`
+    });
     for (const attr of Array.from($span.attributes)) {
       if (attr.name !== 'id') {
         $button.setAttribute(attr.name, attr.value);
       }
     }
-    const $headingText = document.createElement('span');
-    $headingText.classList.add(this.sectionHeadingTextClass);
-    $headingText.id = $span.id;
-    const $headingTextFocus = document.createElement('span');
-    $headingTextFocus.classList.add(this.sectionHeadingTextFocusClass);
-    $headingText.appendChild($headingTextFocus);
-    Array.from($span.childNodes).forEach($child => $headingTextFocus.appendChild($child));
-    const $showHideToggle = document.createElement('span');
-    $showHideToggle.classList.add(this.sectionShowHideToggleClass);
-    $showHideToggle.setAttribute('data-nosnippet', '');
-    const $showHideToggleFocus = document.createElement('span');
-    $showHideToggleFocus.classList.add(this.sectionShowHideToggleFocusClass);
-    $showHideToggle.appendChild($showHideToggleFocus);
-    const $showHideText = document.createElement('span');
-    const $showHideIcon = document.createElement('span');
-    $showHideIcon.classList.add(this.upChevronIconClass);
-    $showHideToggleFocus.appendChild($showHideIcon);
-    $showHideText.classList.add(this.sectionShowHideTextClass);
-    $showHideToggleFocus.appendChild($showHideText);
-    $button.appendChild($headingText);
+    $button.appendChild(this.createHeadingText($span));
     $button.appendChild(this.getButtonPunctuationEl());
     if ($summary) {
-      const $summarySpan = document.createElement('span');
-      const $summarySpanFocus = document.createElement('span');
-      $summarySpanFocus.classList.add(this.sectionSummaryFocusClass);
-      $summarySpan.appendChild($summarySpanFocus);
-      for (const attr of Array.from($summary.attributes)) {
-        $summarySpan.setAttribute(attr.name, attr.value);
-      }
-      Array.from($summary.childNodes).forEach($child => $summarySpanFocus.appendChild($child));
       $summary.remove();
-      $button.appendChild($summarySpan);
+      $button.appendChild(this.createSummarySpan($summary));
       $button.appendChild(this.getButtonPunctuationEl());
     }
-    $button.appendChild($showHideToggle);
+    $button.appendChild(this.createShowHideToggle());
     $heading.removeChild($span);
     $heading.appendChild($button);
   }
+
+  /**
+   * Creates a `<span>` rendering the 'Show'/'Hide' toggle
+   *
+   * @returns {HTMLSpanElement} - The `<span>` with the visual representation of the 'Show/Hide' toggle
+   */
+  createShowHideToggle() {
+    const $showHideToggleFocus = createElement('span', {
+      class: 'govuk-accordion__section-toggle-focus'
+    }, [createElement('span', {
+      class: iconClass
+    }), createElement('span', {
+      class: sectionToggleTextClass
+    })]);
+    const $showHideToggle = createElement('span', {
+      class: 'govuk-accordion__section-toggle',
+      'data-nosnippet': ''
+    }, [$showHideToggleFocus]);
+    return $showHideToggle;
+  }
+
+  /**
+   * Creates the `<span>` containing the text of the section's heading
+   *
+   * @param {Element} $span - The heading of the span
+   * @returns {HTMLSpanElement} - The `<span>` containing the text of the section's heading
+   */
+  createHeadingText($span) {
+    const $headingTextFocus = createElement('span', {
+      class: 'govuk-accordion__section-heading-text-focus'
+    }, Array.from($span.childNodes));
+    const $headingText = createElement('span', {
+      class: sectionHeadingTextClass,
+      id: $span.id
+    }, [$headingTextFocus]);
+    return $headingText;
+  }
+
+  /**
+   * Creates the `<span>` element with the summary for the section
+   *
+   * This is necessary because the summary line text is now inside
+   * a button element, which can only contain phrasing content, and
+   * not a `<div>` element
+   *
+   * @param {Element} $summary - The original `<div>` containing the summary
+   * @returns {HTMLSpanElement} - The `<span>` element containing the summary
+   */
+  createSummarySpan($summary) {
+    const $summarySpanFocus = createElement('span', {
+      class: 'govuk-accordion__section-summary-focus'
+    }, Array.from($summary.childNodes));
+    const $summarySpan = createElement('span', {}, [$summarySpanFocus]);
+    for (const attr of Array.from($summary.attributes)) {
+      $summarySpan.setAttribute(attr.name, attr.value);
+    }
+    return $summarySpan;
+  }
   onBeforeMatch(event) {
     const $fragment = event.target;
     if (!($fragment instanceof Element)) {
       return;
     }
-    const $section = $fragment.closest(`.${this.sectionClass}`);
+    const $section = $fragment.closest(`.${sectionClass}`);
     if ($section) {
       this.setExpanded(true, $section);
     }
@@ -718,14 +760,14 @@ class Accordion extends ConfigurableComponent {
     this.updateShowAllButton(nowExpanded);
   }
   setExpanded(expanded, $section) {
-    const $showHideIcon = $section.querySelector(`.${this.upChevronIconClass}`);
-    const $showHideText = $section.querySelector(`.${this.sectionShowHideTextClass}`);
-    const $button = $section.querySelector(`.${this.sectionButtonClass}`);
-    const $content = $section.querySelector(`.${this.sectionContentClass}`);
+    const $showHideIcon = $section.querySelector(`.${iconClass}`);
+    const $showHideText = $section.querySelector(`.${sectionToggleTextClass}`);
+    const $button = $section.querySelector(`.${sectionButtonClass}`);
+    const $content = $section.querySelector(`.${sectionContentClass}`);
     if (!$content) {
       throw new ElementError({
         component: Accordion,
-        identifier: `Section content (\`<div class="${this.sectionContentClass}">\`)`
+        identifier: `Section content (\`<div class="${sectionContentClass}">\`)`
       });
     }
     if (!$showHideIcon || !$showHideText || !$button) {
@@ -735,11 +777,11 @@ class Accordion extends ConfigurableComponent {
     $showHideText.textContent = newButtonText;
     $button.setAttribute('aria-expanded', `${expanded}`);
     const ariaLabelParts = [];
-    const $headingText = $section.querySelector(`.${this.sectionHeadingTextClass}`);
+    const $headingText = $section.querySelector(`.${sectionHeadingTextClass}`);
     if ($headingText) {
       ariaLabelParts.push(`${$headingText.textContent}`.trim());
     }
-    const $summary = $section.querySelector(`.${this.sectionSummaryClass}`);
+    const $summary = $section.querySelector(`.${sectionSummaryClass}`);
     if ($summary) {
       ariaLabelParts.push(`${$summary.textContent}`.trim());
     }
@@ -748,17 +790,17 @@ class Accordion extends ConfigurableComponent {
     $button.setAttribute('aria-label', ariaLabelParts.join(' , '));
     if (expanded) {
       $content.removeAttribute('hidden');
-      $section.classList.add(this.sectionExpandedClass);
-      $showHideIcon.classList.remove(this.downChevronIconClass);
+      $section.classList.add(sectionExpandedModifier);
+      $showHideIcon.classList.remove(iconOpenModifier);
     } else {
       $content.setAttribute('hidden', 'until-found');
-      $section.classList.remove(this.sectionExpandedClass);
-      $showHideIcon.classList.add(this.downChevronIconClass);
+      $section.classList.remove(sectionExpandedModifier);
+      $showHideIcon.classList.add(iconOpenModifier);
     }
     this.updateShowAllButton(this.areAllSectionsOpen());
   }
   isExpanded($section) {
-    return $section.classList.contains(this.sectionExpandedClass);
+    return $section.classList.contains(sectionExpandedModifier);
   }
   areAllSectionsOpen() {
     return Array.from(this.$sections).every($section => this.isExpanded($section));
@@ -769,7 +811,7 @@ class Accordion extends ConfigurableComponent {
     }
     this.$showAllButton.setAttribute('aria-expanded', expanded.toString());
     this.$showAllText.textContent = expanded ? this.i18n.t('hideAllSections') : this.i18n.t('showAllSections');
-    this.$showAllIcon.classList.toggle(this.downChevronIconClass, !expanded);
+    this.$showAllIcon.classList.toggle(iconOpenModifier, !expanded);
   }
 
   /**
@@ -783,7 +825,7 @@ class Accordion extends ConfigurableComponent {
    * @returns {string | undefined | null} Identifier for section
    */
   getIdentifier($section) {
-    const $button = $section.querySelector(`.${this.sectionButtonClass}`);
+    const $button = $section.querySelector(`.${sectionButtonClass}`);
     return $button == null ? void 0 : $button.getAttribute('aria-controls');
   }
   storeState($section, isExpanded) {
@@ -812,10 +854,11 @@ class Accordion extends ConfigurableComponent {
     }
   }
   getButtonPunctuationEl() {
-    const $punctuationEl = document.createElement('span');
-    $punctuationEl.classList.add('govuk-visually-hidden', this.sectionHeadingDividerClass);
-    $punctuationEl.textContent = ', ';
-    return $punctuationEl;
+    const $element = createElement('span', {
+      class: 'govuk-visually-hidden govuk-accordion__section-heading-divider'
+    });
+    $element.textContent = ', ';
+    return $element;
   }
 }
 
@@ -1488,9 +1531,10 @@ class ExitThisPage extends ConfigurableComponent {
     window.addEventListener('pageshow', this.resetPage.bind(this));
   }
   initUpdateSpan() {
-    this.$updateSpan = document.createElement('span');
-    this.$updateSpan.setAttribute('role', 'status');
-    this.$updateSpan.className = 'govuk-visually-hidden';
+    this.$updateSpan = createElement('span', {
+      role: 'status',
+      class: 'govuk-visually-hidden'
+    });
     this.$root.appendChild(this.$updateSpan);
   }
   initButtonClickHandler() {
@@ -1500,13 +1544,14 @@ class ExitThisPage extends ConfigurableComponent {
     }
   }
   buildIndicator() {
-    this.$indicatorContainer = document.createElement('div');
-    this.$indicatorContainer.className = 'govuk-exit-this-page__indicator';
-    this.$indicatorContainer.setAttribute('aria-hidden', 'true');
+    this.$indicatorContainer = createElement('div', {
+      class: 'govuk-exit-this-page__indicator',
+      'aria-hidden': 'true'
+    });
     for (let i = 0; i < 3; i++) {
-      const $indicator = document.createElement('div');
-      $indicator.className = 'govuk-exit-this-page__indicator-light';
-      this.$indicatorContainer.appendChild($indicator);
+      this.$indicatorContainer.appendChild(createElement('div', {
+        class: 'govuk-exit-this-page__indicator-light'
+      }));
     }
     this.$button.appendChild(this.$indicatorContainer);
   }
@@ -1526,9 +1571,10 @@ class ExitThisPage extends ConfigurableComponent {
     }
     this.$updateSpan.textContent = '';
     document.body.classList.add('govuk-exit-this-page-hide-content');
-    this.$overlay = document.createElement('div');
-    this.$overlay.className = 'govuk-exit-this-page-overlay';
-    this.$overlay.setAttribute('role', 'alert');
+    this.$overlay = createElement('div', {
+      class: 'govuk-exit-this-page-overlay',
+      role: 'alert'
+    });
     document.body.appendChild(this.$overlay);
     this.$overlay.textContent = this.i18n.t('activated');
     window.location.href = this.$button.href;
@@ -1829,9 +1875,10 @@ class PasswordInput extends ConfigurableComponent {
       locale: closestAttributeValue(this.$root, 'lang')
     });
     this.$showHideButton.removeAttribute('hidden');
-    const $screenReaderStatusMessage = document.createElement('div');
-    $screenReaderStatusMessage.className = 'govuk-password-input__sr-status govuk-visually-hidden';
-    $screenReaderStatusMessage.setAttribute('aria-live', 'polite');
+    const $screenReaderStatusMessage = createElement('div', {
+      class: 'govuk-password-input__sr-status govuk-visually-hidden',
+      'aria-live': 'polite'
+    });
     this.$screenReaderStatusMessage = $screenReaderStatusMessage;
     this.$input.insertAdjacentElement('afterend', $screenReaderStatusMessage);
     this.$showHideButton.addEventListener('click', this.toggle.bind(this));
diff --git a/packages/govuk-frontend/dist/govuk/common/create-element.mjs b/packages/govuk-frontend/dist/govuk/common/create-element.mjs
new file mode 100644
index 000000000..ed0e48c0a
--- /dev/null
+++ b/packages/govuk-frontend/dist/govuk/common/create-element.mjs
@@ -0,0 +1,15 @@
+function createElement(tagName, attributes = {}, children) {
+  const el = document.createElement(tagName);
+  Object.entries(attributes).forEach(([name, value]) => {
+    el.setAttribute(name, value);
+  });
+  if (children) {
+    for (const child of children) {
+      el.appendChild(child);
+    }
+  }
+  return el;
+}
+
+export { createElement };
+//# sourceMappingURL=create-element.mjs.map
diff --git a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js
index e80a3ac42..7c6b0a5eb 100644
--- a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js
@@ -291,6 +291,19 @@
    * @typedef {typeof Component & ChildClass<ConfigurationType>} ChildClassConstructor<ConfigurationType>
    */
 
+  function createElement(tagName, attributes = {}, children) {
+    const el = document.createElement(tagName);
+    Object.entries(attributes).forEach(([name, value]) => {
+      el.setAttribute(name, value);
+    });
+    if (children) {
+      for (const child of children) {
+        el.appendChild(child);
+      }
+    }
+    return el;
+  }
+
   class I18n {
     constructor(translations = {}, config = {}) {
       var _config$locale;
@@ -484,6 +497,18 @@
     }
   };
 
+  const sectionClass = 'govuk-accordion__section';
+  const sectionExpandedModifier = 'govuk-accordion__section--expanded';
+  const sectionButtonClass = 'govuk-accordion__section-button';
+  const sectionHeaderClass = 'govuk-accordion__section-header';
+  const sectionHeadingClass = 'govuk-accordion__section-heading';
+  const sectionHeadingTextClass = 'govuk-accordion__section-heading-text';
+  const sectionToggleTextClass = 'govuk-accordion__section-toggle-text';
+  const iconClass = 'govuk-accordion-nav__chevron';
+  const iconOpenModifier = 'govuk-accordion-nav__chevron--down';
+  const sectionSummaryClass = 'govuk-accordion__section-summary';
+  const sectionContentClass = 'govuk-accordion__section-content';
+
   /**
    * Accordion component
    *
@@ -507,35 +532,16 @@
     constructor($root, config = {}) {
       super($root, config);
       this.i18n = void 0;
-      this.controlsClass = 'govuk-accordion__controls';
-      this.showAllClass = 'govuk-accordion__show-all';
-      this.showAllTextClass = 'govuk-accordion__show-all-text';
-      this.sectionClass = 'govuk-accordion__section';
-      this.sectionExpandedClass = 'govuk-accordion__section--expanded';
-      this.sectionButtonClass = 'govuk-accordion__section-button';
-      this.sectionHeaderClass = 'govuk-accordion__section-header';
-      this.sectionHeadingClass = 'govuk-accordion__section-heading';
-      this.sectionHeadingDividerClass = 'govuk-accordion__section-heading-divider';
-      this.sectionHeadingTextClass = 'govuk-accordion__section-heading-text';
-      this.sectionHeadingTextFocusClass = 'govuk-accordion__section-heading-text-focus';
-      this.sectionShowHideToggleClass = 'govuk-accordion__section-toggle';
-      this.sectionShowHideToggleFocusClass = 'govuk-accordion__section-toggle-focus';
-      this.sectionShowHideTextClass = 'govuk-accordion__section-toggle-text';
-      this.upChevronIconClass = 'govuk-accordion-nav__chevron';
-      this.downChevronIconClass = 'govuk-accordion-nav__chevron--down';
-      this.sectionSummaryClass = 'govuk-accordion__section-summary';
-      this.sectionSummaryFocusClass = 'govuk-accordion__section-summary-focus';
-      this.sectionContentClass = 'govuk-accordion__section-content';
       this.$sections = void 0;
       this.$showAllButton = null;
       this.$showAllIcon = null;
       this.$showAllText = null;
       this.i18n = new I18n(this.config.i18n);
-      const $sections = this.$root.querySelectorAll(`.${this.sectionClass}`);
+      const $sections = this.$root.querySelectorAll(`.${sectionClass}`);
       if (!$sections.length) {
         throw new ElementError({
           component: Accordion,
-          identifier: `Sections (\`<div class="${this.sectionClass}">\`)`
+          identifier: `Sections (\`<div class="${sectionClass}">\`)`
         });
       }
       this.$sections = $sections;
@@ -544,19 +550,23 @@
       this.updateShowAllButton(this.areAllSectionsOpen());
     }
     initControls() {
-      this.$showAllButton = document.createElement('button');
-      this.$showAllButton.setAttribute('type', 'button');
-      this.$showAllButton.setAttribute('class', this.showAllClass);
-      this.$showAllButton.setAttribute('aria-expanded', 'false');
-      this.$showAllIcon = document.createElement('span');
-      this.$showAllIcon.classList.add(this.upChevronIconClass);
+      this.$showAllButton = createElement('button', {
+        type: 'button',
+        class: 'govuk-accordion__show-all',
+        'aria-expanded': 'false'
+      });
+      this.$showAllIcon = createElement('span', {
+        class: iconClass
+      });
       this.$showAllButton.appendChild(this.$showAllIcon);
-      const $accordionControls = document.createElement('div');
-      $accordionControls.setAttribute('class', this.controlsClass);
+      const $accordionControls = createElement('div', {
+        class: 'govuk-accordion__controls'
+      });
       $accordionControls.appendChild(this.$showAllButton);
       this.$root.insertBefore($accordionControls, this.$root.firstChild);
-      this.$showAllText = document.createElement('span');
-      this.$showAllText.classList.add(this.showAllTextClass);
+      this.$showAllText = createElement('span', {
+        class: 'govuk-accordion__show-all-text'
+      });
       this.$showAllButton.appendChild(this.$showAllText);
       this.$showAllButton.addEventListener('click', () => this.onShowOrHideAllToggle());
       if ('onbeforematch' in document) {
@@ -565,11 +575,11 @@
     }
     initSectionHeaders() {
       this.$sections.forEach(($section, i) => {
-        const $header = $section.querySelector(`.${this.sectionHeaderClass}`);
+        const $header = $section.querySelector(`.${sectionHeaderClass}`);
         if (!$header) {
           throw new ElementError({
             component: Accordion,
-            identifier: `Section headers (\`<div class="${this.sectionHeaderClass}">\`)`
+            identifier: `Section headers (\`<div class="${sectionHeaderClass}">\`)`
           });
         }
         this.constructHeaderMarkup($header, i);
@@ -579,73 +589,105 @@
       });
     }
     constructHeaderMarkup($header, index) {
-      const $span = $header.querySelector(`.${this.sectionButtonClass}`);
-      const $heading = $header.querySelector(`.${this.sectionHeadingClass}`);
-      const $summary = $header.querySelector(`.${this.sectionSummaryClass}`);
+      const $span = $header.querySelector(`.${sectionButtonClass}`);
+      const $heading = $header.querySelector(`.${sectionHeadingClass}`);
+      const $summary = $header.querySelector(`.${sectionSummaryClass}`);
       if (!$heading) {
         throw new ElementError({
           component: Accordion,
-          identifier: `Section heading (\`.${this.sectionHeadingClass}\`)`
+          identifier: `Section heading (\`.${sectionHeadingClass}\`)`
         });
       }
       if (!$span) {
         throw new ElementError({
           component: Accordion,
-          identifier: `Section button placeholder (\`<span class="${this.sectionButtonClass}">\`)`
+          identifier: `Section button placeholder (\`<span class="${sectionButtonClass}">\`)`
         });
       }
-      const $button = document.createElement('button');
-      $button.setAttribute('type', 'button');
-      $button.setAttribute('aria-controls', `${this.$root.id}-content-${index + 1}`);
+      const $button = createElement('button', {
+        type: 'button',
+        'aria-controls': `${this.$root.id}-content-${index + 1}`
+      });
       for (const attr of Array.from($span.attributes)) {
         if (attr.name !== 'id') {
           $button.setAttribute(attr.name, attr.value);
         }
       }
-      const $headingText = document.createElement('span');
-      $headingText.classList.add(this.sectionHeadingTextClass);
-      $headingText.id = $span.id;
-      const $headingTextFocus = document.createElement('span');
-      $headingTextFocus.classList.add(this.sectionHeadingTextFocusClass);
-      $headingText.appendChild($headingTextFocus);
-      Array.from($span.childNodes).forEach($child => $headingTextFocus.appendChild($child));
-      const $showHideToggle = document.createElement('span');
-      $showHideToggle.classList.add(this.sectionShowHideToggleClass);
-      $showHideToggle.setAttribute('data-nosnippet', '');
-      const $showHideToggleFocus = document.createElement('span');
-      $showHideToggleFocus.classList.add(this.sectionShowHideToggleFocusClass);
-      $showHideToggle.appendChild($showHideToggleFocus);
-      const $showHideText = document.createElement('span');
-      const $showHideIcon = document.createElement('span');
-      $showHideIcon.classList.add(this.upChevronIconClass);
-      $showHideToggleFocus.appendChild($showHideIcon);
-      $showHideText.classList.add(this.sectionShowHideTextClass);
-      $showHideToggleFocus.appendChild($showHideText);
-      $button.appendChild($headingText);
+      $button.appendChild(this.createHeadingText($span));
       $button.appendChild(this.getButtonPunctuationEl());
       if ($summary) {
-        const $summarySpan = document.createElement('span');
-        const $summarySpanFocus = document.createElement('span');
-        $summarySpanFocus.classList.add(this.sectionSummaryFocusClass);
-        $summarySpan.appendChild($summarySpanFocus);
-        for (const attr of Array.from($summary.attributes)) {
-          $summarySpan.setAttribute(attr.name, attr.value);
-        }
-        Array.from($summary.childNodes).forEach($child => $summarySpanFocus.appendChild($child));
         $summary.remove();
-        $button.appendChild($summarySpan);
+        $button.appendChild(this.createSummarySpan($summary));
         $button.appendChild(this.getButtonPunctuationEl());
       }
-      $button.appendChild($showHideToggle);
+      $button.appendChild(this.createShowHideToggle());
       $heading.removeChild($span);
       $heading.appendChild($button);
     }
+
+    /**
+     * Creates a `<span>` rendering the 'Show'/'Hide' toggle
+     *
+     * @returns {HTMLSpanElement} - The `<span>` with the visual representation of the 'Show/Hide' toggle
+     */
+    createShowHideToggle() {
+      const $showHideToggleFocus = createElement('span', {
+        class: 'govuk-accordion__section-toggle-focus'
+      }, [createElement('span', {
+        class: iconClass
+      }), createElement('span', {
+        class: sectionToggleTextClass
+      })]);
+      const $showHideToggle = createElement('span', {
+        class: 'govuk-accordion__section-toggle',
+        'data-nosnippet': ''
+      }, [$showHideToggleFocus]);
+      return $showHideToggle;
+    }
+
+    /**
+     * Creates the `<span>` containing the text of the section's heading
+     *
+     * @param {Element} $span - The heading of the span
+     * @returns {HTMLSpanElement} - The `<span>` containing the text of the section's heading
+     */
+    createHeadingText($span) {
+      const $headingTextFocus = createElement('span', {
+        class: 'govuk-accordion__section-heading-text-focus'
+      }, Array.from($span.childNodes));
+      const $headingText = createElement('span', {
+        class: sectionHeadingTextClass,
+        id: $span.id
+      }, [$headingTextFocus]);
+      return $headingText;
+    }
+
+    /**
+     * Creates the `<span>` element with the summary for the section
+     *
+     * This is necessary because the summary line text is now inside
+     * a button element, which can only contain phrasing content, and
+     * not a `<div>` element
+     *
+     * @param {Element} $summary - The original `<div>` containing the summary
+     * @returns {HTMLSpanElement} - The `<span>` element containing the summary
+     */
+    createSummarySpan($summary) {
+      const $summarySpanFocus = createElement('span', {
+        class: 'govuk-accordion__section-summary-focus'
+      }, Array.from($summary.childNodes));
+      const $summarySpan = createElement('span', {}, [$summarySpanFocus]);
+      for (const attr of Array.from($summary.attributes)) {
+        $summarySpan.setAttribute(attr.name, attr.value);
+      }
+      return $summarySpan;
+    }
     onBeforeMatch(event) {
       const $fragment = event.target;
       if (!($fragment instanceof Element)) {
         return;
       }
-      const $section = $fragment.closest(`.${this.sectionClass}`);
+      const $section = $fragment.closest(`.${sectionClass}`);
       if ($section) {
         this.setExpanded(true, $section);
       }
@@ -664,14 +706,14 @@
       this.updateShowAllButton(nowExpanded);
     }
     setExpanded(expanded, $section) {
-      const $showHideIcon = $section.querySelector(`.${this.upChevronIconClass}`);
-      const $showHideText = $section.querySelector(`.${this.sectionShowHideTextClass}`);
-      const $button = $section.querySelector(`.${this.sectionButtonClass}`);
-      const $content = $section.querySelector(`.${this.sectionContentClass}`);
+      const $showHideIcon = $section.querySelector(`.${iconClass}`);
+      const $showHideText = $section.querySelector(`.${sectionToggleTextClass}`);
+      const $button = $section.querySelector(`.${sectionButtonClass}`);
+      const $content = $section.querySelector(`.${sectionContentClass}`);
       if (!$content) {
         throw new ElementError({
           component: Accordion,
-          identifier: `Section content (\`<div class="${this.sectionContentClass}">\`)`
+          identifier: `Section content (\`<div class="${sectionContentClass}">\`)`
         });
       }
       if (!$showHideIcon || !$showHideText || !$button) {
@@ -681,11 +723,11 @@
       $showHideText.textContent = newButtonText;
       $button.setAttribute('aria-expanded', `${expanded}`);
       const ariaLabelParts = [];
-      const $headingText = $section.querySelector(`.${this.sectionHeadingTextClass}`);
+      const $headingText = $section.querySelector(`.${sectionHeadingTextClass}`);
       if ($headingText) {
         ariaLabelParts.push(`${$headingText.textContent}`.trim());
       }
-      const $summary = $section.querySelector(`.${this.sectionSummaryClass}`);
+      const $summary = $section.querySelector(`.${sectionSummaryClass}`);
       if ($summary) {
         ariaLabelParts.push(`${$summary.textContent}`.trim());
       }
@@ -694,17 +736,17 @@
       $button.setAttribute('aria-label', ariaLabelParts.join(' , '));
       if (expanded) {
         $content.removeAttribute('hidden');
-        $section.classList.add(this.sectionExpandedClass);
-        $showHideIcon.classList.remove(this.downChevronIconClass);
+        $section.classList.add(sectionExpandedModifier);
+        $showHideIcon.classList.remove(iconOpenModifier);
       } else {
         $content.setAttribute('hidden', 'until-found');
-        $section.classList.remove(this.sectionExpandedClass);
-        $showHideIcon.classList.add(this.downChevronIconClass);
+        $section.classList.remove(sectionExpandedModifier);
+        $showHideIcon.classList.add(iconOpenModifier);
       }
       this.updateShowAllButton(this.areAllSectionsOpen());
     }
     isExpanded($section) {
-      return $section.classList.contains(this.sectionExpandedClass);
+      return $section.classList.contains(sectionExpandedModifier);
     }
     areAllSectionsOpen() {
       return Array.from(this.$sections).every($section => this.isExpanded($section));
@@ -715,7 +757,7 @@
       }
       this.$showAllButton.setAttribute('aria-expanded', expanded.toString());
       this.$showAllText.textContent = expanded ? this.i18n.t('hideAllSections') : this.i18n.t('showAllSections');
-      this.$showAllIcon.classList.toggle(this.downChevronIconClass, !expanded);
+      this.$showAllIcon.classList.toggle(iconOpenModifier, !expanded);
     }
 
     /**
@@ -729,7 +771,7 @@
      * @returns {string | undefined | null} Identifier for section
      */
     getIdentifier($section) {
-      const $button = $section.querySelector(`.${this.sectionButtonClass}`);
+      const $button = $section.querySelector(`.${sectionButtonClass}`);
       return $button == null ? void 0 : $button.getAttribute('aria-controls');
     }
     storeState($section, isExpanded) {
@@ -758,10 +800,11 @@
       }
     }
     getButtonPunctuationEl() {
-      const $punctuationEl = document.createElement('span');
-      $punctuationEl.classList.add('govuk-visually-hidden', this.sectionHeadingDividerClass);
-      $punctuationEl.textContent = ', ';
-      return $punctuationEl;
+      const $element = createElement('span', {
+        class: 'govuk-visually-hidden govuk-accordion__section-heading-divider'
+      });
+      $element.textContent = ', ';
+      return $element;
     }
   }
 
diff --git a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs
index b36f3fcab..e1f6eb729 100644
--- a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs
@@ -285,6 +285,19 @@ function extractConfigByNamespace(schema, dataset, namespace) {
  * @typedef {typeof Component & ChildClass<ConfigurationType>} ChildClassConstructor<ConfigurationType>
  */
 
+function createElement(tagName, attributes = {}, children) {
+  const el = document.createElement(tagName);
+  Object.entries(attributes).forEach(([name, value]) => {
+    el.setAttribute(name, value);
+  });
+  if (children) {
+    for (const child of children) {
+      el.appendChild(child);
+    }
+  }
+  return el;
+}
+
 class I18n {
   constructor(translations = {}, config = {}) {
     var _config$locale;
@@ -478,6 +491,18 @@ I18n.pluralRules = {
   }
 };
 
+const sectionClass = 'govuk-accordion__section';
+const sectionExpandedModifier = 'govuk-accordion__section--expanded';
+const sectionButtonClass = 'govuk-accordion__section-button';
+const sectionHeaderClass = 'govuk-accordion__section-header';
+const sectionHeadingClass = 'govuk-accordion__section-heading';
+const sectionHeadingTextClass = 'govuk-accordion__section-heading-text';
+const sectionToggleTextClass = 'govuk-accordion__section-toggle-text';
+const iconClass = 'govuk-accordion-nav__chevron';
+const iconOpenModifier = 'govuk-accordion-nav__chevron--down';
+const sectionSummaryClass = 'govuk-accordion__section-summary';
+const sectionContentClass = 'govuk-accordion__section-content';
+
 /**
  * Accordion component
  *
@@ -501,35 +526,16 @@ class Accordion extends ConfigurableComponent {
   constructor($root, config = {}) {
     super($root, config);
     this.i18n = void 0;
-    this.controlsClass = 'govuk-accordion__controls';
-    this.showAllClass = 'govuk-accordion__show-all';
-    this.showAllTextClass = 'govuk-accordion__show-all-text';
-    this.sectionClass = 'govuk-accordion__section';
-    this.sectionExpandedClass = 'govuk-accordion__section--expanded';
-    this.sectionButtonClass = 'govuk-accordion__section-button';
-    this.sectionHeaderClass = 'govuk-accordion__section-header';
-    this.sectionHeadingClass = 'govuk-accordion__section-heading';
-    this.sectionHeadingDividerClass = 'govuk-accordion__section-heading-divider';
-    this.sectionHeadingTextClass = 'govuk-accordion__section-heading-text';
-    this.sectionHeadingTextFocusClass = 'govuk-accordion__section-heading-text-focus';
-    this.sectionShowHideToggleClass = 'govuk-accordion__section-toggle';
-    this.sectionShowHideToggleFocusClass = 'govuk-accordion__section-toggle-focus';
-    this.sectionShowHideTextClass = 'govuk-accordion__section-toggle-text';
-    this.upChevronIconClass = 'govuk-accordion-nav__chevron';
-    this.downChevronIconClass = 'govuk-accordion-nav__chevron--down';
-    this.sectionSummaryClass = 'govuk-accordion__section-summary';
-    this.sectionSummaryFocusClass = 'govuk-accordion__section-summary-focus';
-    this.sectionContentClass = 'govuk-accordion__section-content';
     this.$sections = void 0;
     this.$showAllButton = null;
     this.$showAllIcon = null;
     this.$showAllText = null;
     this.i18n = new I18n(this.config.i18n);
-    const $sections = this.$root.querySelectorAll(`.${this.sectionClass}`);
+    const $sections = this.$root.querySelectorAll(`.${sectionClass}`);
     if (!$sections.length) {
       throw new ElementError({
         component: Accordion,
-        identifier: `Sections (\`<div class="${this.sectionClass}">\`)`
+        identifier: `Sections (\`<div class="${sectionClass}">\`)`
       });
     }
     this.$sections = $sections;
@@ -538,19 +544,23 @@ class Accordion extends ConfigurableComponent {
     this.updateShowAllButton(this.areAllSectionsOpen());
   }
   initControls() {
-    this.$showAllButton = document.createElement('button');
-    this.$showAllButton.setAttribute('type', 'button');
-    this.$showAllButton.setAttribute('class', this.showAllClass);
-    this.$showAllButton.setAttribute('aria-expanded', 'false');
-    this.$showAllIcon = document.createElement('span');
-    this.$showAllIcon.classList.add(this.upChevronIconClass);
+    this.$showAllButton = createElement('button', {
+      type: 'button',
+      class: 'govuk-accordion__show-all',
+      'aria-expanded': 'false'
+    });
+    this.$showAllIcon = createElement('span', {
+      class: iconClass
+    });
     this.$showAllButton.appendChild(this.$showAllIcon);
-    const $accordionControls = document.createElement('div');
-    $accordionControls.setAttribute('class', this.controlsClass);
+    const $accordionControls = createElement('div', {
+      class: 'govuk-accordion__controls'
+    });
     $accordionControls.appendChild(this.$showAllButton);
     this.$root.insertBefore($accordionControls, this.$root.firstChild);
-    this.$showAllText = document.createElement('span');
-    this.$showAllText.classList.add(this.showAllTextClass);
+    this.$showAllText = createElement('span', {
+      class: 'govuk-accordion__show-all-text'
+    });
     this.$showAllButton.appendChild(this.$showAllText);
     this.$showAllButton.addEventListener('click', () => this.onShowOrHideAllToggle());
     if ('onbeforematch' in document) {
@@ -559,11 +569,11 @@ class Accordion extends ConfigurableComponent {
   }
   initSectionHeaders() {
     this.$sections.forEach(($section, i) => {
-      const $header = $section.querySelector(`.${this.sectionHeaderClass}`);
+      const $header = $section.querySelector(`.${sectionHeaderClass}`);
       if (!$header) {
         throw new ElementError({
           component: Accordion,
-          identifier: `Section headers (\`<div class="${this.sectionHeaderClass}">\`)`
+          identifier: `Section headers (\`<div class="${sectionHeaderClass}">\`)`
         });
       }
       this.constructHeaderMarkup($header, i);
@@ -573,73 +583,105 @@ class Accordion extends ConfigurableComponent {
     });
   }
   constructHeaderMarkup($header, index) {
-    const $span = $header.querySelector(`.${this.sectionButtonClass}`);
-    const $heading = $header.querySelector(`.${this.sectionHeadingClass}`);
-    const $summary = $header.querySelector(`.${this.sectionSummaryClass}`);
+    const $span = $header.querySelector(`.${sectionButtonClass}`);
+    const $heading = $header.querySelector(`.${sectionHeadingClass}`);
+    const $summary = $header.querySelector(`.${sectionSummaryClass}`);
     if (!$heading) {
       throw new ElementError({
         component: Accordion,
-        identifier: `Section heading (\`.${this.sectionHeadingClass}\`)`
+        identifier: `Section heading (\`.${sectionHeadingClass}\`)`
       });
     }
     if (!$span) {
       throw new ElementError({
         component: Accordion,
-        identifier: `Section button placeholder (\`<span class="${this.sectionButtonClass}">\`)`
+        identifier: `Section button placeholder (\`<span class="${sectionButtonClass}">\`)`
       });
     }
-    const $button = document.createElement('button');
-    $button.setAttribute('type', 'button');
-    $button.setAttribute('aria-controls', `${this.$root.id}-content-${index + 1}`);
+    const $button = createElement('button', {
+      type: 'button',
+      'aria-controls': `${this.$root.id}-content-${index + 1}`
+    });
     for (const attr of Array.from($span.attributes)) {
       if (attr.name !== 'id') {
         $button.setAttribute(attr.name, attr.value);
       }
     }
-    const $headingText = document.createElement('span');
-    $headingText.classList.add(this.sectionHeadingTextClass);
-    $headingText.id = $span.id;
-    const $headingTextFocus = document.createElement('span');
-    $headingTextFocus.classList.add(this.sectionHeadingTextFocusClass);
-    $headingText.appendChild($headingTextFocus);
-    Array.from($span.childNodes).forEach($child => $headingTextFocus.appendChild($child));
-    const $showHideToggle = document.createElement('span');
-    $showHideToggle.classList.add(this.sectionShowHideToggleClass);
-    $showHideToggle.setAttribute('data-nosnippet', '');
-    const $showHideToggleFocus = document.createElement('span');
-    $showHideToggleFocus.classList.add(this.sectionShowHideToggleFocusClass);
-    $showHideToggle.appendChild($showHideToggleFocus);
-    const $showHideText = document.createElement('span');
-    const $showHideIcon = document.createElement('span');
-    $showHideIcon.classList.add(this.upChevronIconClass);
-    $showHideToggleFocus.appendChild($showHideIcon);
-    $showHideText.classList.add(this.sectionShowHideTextClass);
-    $showHideToggleFocus.appendChild($showHideText);
-    $button.appendChild($headingText);
+    $button.appendChild(this.createHeadingText($span));
     $button.appendChild(this.getButtonPunctuationEl());
     if ($summary) {
-      const $summarySpan = document.createElement('span');
-      const $summarySpanFocus = document.createElement('span');
-      $summarySpanFocus.classList.add(this.sectionSummaryFocusClass);
-      $summarySpan.appendChild($summarySpanFocus);
-      for (const attr of Array.from($summary.attributes)) {
-        $summarySpan.setAttribute(attr.name, attr.value);
-      }
-      Array.from($summary.childNodes).forEach($child => $summarySpanFocus.appendChild($child));
       $summary.remove();
-      $button.appendChild($summarySpan);
+      $button.appendChild(this.createSummarySpan($summary));
       $button.appendChild(this.getButtonPunctuationEl());
     }
-    $button.appendChild($showHideToggle);
+    $button.appendChild(this.createShowHideToggle());
     $heading.removeChild($span);
     $heading.appendChild($button);
   }
+
+  /**
+   * Creates a `<span>` rendering the 'Show'/'Hide' toggle
+   *
+   * @returns {HTMLSpanElement} - The `<span>` with the visual representation of the 'Show/Hide' toggle
+   */
+  createShowHideToggle() {
+    const $showHideToggleFocus = createElement('span', {
+      class: 'govuk-accordion__section-toggle-focus'
+    }, [createElement('span', {
+      class: iconClass
+    }), createElement('span', {
+      class: sectionToggleTextClass
+    })]);
+    const $showHideToggle = createElement('span', {
+      class: 'govuk-accordion__section-toggle',
+      'data-nosnippet': ''
+    }, [$showHideToggleFocus]);
+    return $showHideToggle;
+  }
+
+  /**
+   * Creates the `<span>` containing the text of the section's heading
+   *
+   * @param {Element} $span - The heading of the span
+   * @returns {HTMLSpanElement} - The `<span>` containing the text of the section's heading
+   */
+  createHeadingText($span) {
+    const $headingTextFocus = createElement('span', {
+      class: 'govuk-accordion__section-heading-text-focus'
+    }, Array.from($span.childNodes));
+    const $headingText = createElement('span', {
+      class: sectionHeadingTextClass,
+      id: $span.id
+    }, [$headingTextFocus]);
+    return $headingText;
+  }
+
+  /**
+   * Creates the `<span>` element with the summary for the section
+   *
+   * This is necessary because the summary line text is now inside
+   * a button element, which can only contain phrasing content, and
+   * not a `<div>` element
+   *
+   * @param {Element} $summary - The original `<div>` containing the summary
+   * @returns {HTMLSpanElement} - The `<span>` element containing the summary
+   */
+  createSummarySpan($summary) {
+    const $summarySpanFocus = createElement('span', {
+      class: 'govuk-accordion__section-summary-focus'
+    }, Array.from($summary.childNodes));
+    const $summarySpan = createElement('span', {}, [$summarySpanFocus]);
+    for (const attr of Array.from($summary.attributes)) {
+      $summarySpan.setAttribute(attr.name, attr.value);
+    }
+    return $summarySpan;
+  }
   onBeforeMatch(event) {
     const $fragment = event.target;
     if (!($fragment instanceof Element)) {
       return;
     }
-    const $section = $fragment.closest(`.${this.sectionClass}`);
+    const $section = $fragment.closest(`.${sectionClass}`);
     if ($section) {
       this.setExpanded(true, $section);
     }
@@ -658,14 +700,14 @@ class Accordion extends ConfigurableComponent {
     this.updateShowAllButton(nowExpanded);
   }
   setExpanded(expanded, $section) {
-    const $showHideIcon = $section.querySelector(`.${this.upChevronIconClass}`);
-    const $showHideText = $section.querySelector(`.${this.sectionShowHideTextClass}`);
-    const $button = $section.querySelector(`.${this.sectionButtonClass}`);
-    const $content = $section.querySelector(`.${this.sectionContentClass}`);
+    const $showHideIcon = $section.querySelector(`.${iconClass}`);
+    const $showHideText = $section.querySelector(`.${sectionToggleTextClass}`);
+    const $button = $section.querySelector(`.${sectionButtonClass}`);
+    const $content = $section.querySelector(`.${sectionContentClass}`);
     if (!$content) {
       throw new ElementError({
         component: Accordion,
-        identifier: `Section content (\`<div class="${this.sectionContentClass}">\`)`
+        identifier: `Section content (\`<div class="${sectionContentClass}">\`)`
       });
     }
     if (!$showHideIcon || !$showHideText || !$button) {
@@ -675,11 +717,11 @@ class Accordion extends ConfigurableComponent {
     $showHideText.textContent = newButtonText;
     $button.setAttribute('aria-expanded', `${expanded}`);
     const ariaLabelParts = [];
-    const $headingText = $section.querySelector(`.${this.sectionHeadingTextClass}`);
+    const $headingText = $section.querySelector(`.${sectionHeadingTextClass}`);
     if ($headingText) {
       ariaLabelParts.push(`${$headingText.textContent}`.trim());
     }
-    const $summary = $section.querySelector(`.${this.sectionSummaryClass}`);
+    const $summary = $section.querySelector(`.${sectionSummaryClass}`);
     if ($summary) {
       ariaLabelParts.push(`${$summary.textContent}`.trim());
     }
@@ -688,17 +730,17 @@ class Accordion extends ConfigurableComponent {
     $button.setAttribute('aria-label', ariaLabelParts.join(' , '));
     if (expanded) {
       $content.removeAttribute('hidden');
-      $section.classList.add(this.sectionExpandedClass);
-      $showHideIcon.classList.remove(this.downChevronIconClass);
+      $section.classList.add(sectionExpandedModifier);
+      $showHideIcon.classList.remove(iconOpenModifier);
     } else {
       $content.setAttribute('hidden', 'until-found');
-      $section.classList.remove(this.sectionExpandedClass);
-      $showHideIcon.classList.add(this.downChevronIconClass);
+      $section.classList.remove(sectionExpandedModifier);
+      $showHideIcon.classList.add(iconOpenModifier);
     }
     this.updateShowAllButton(this.areAllSectionsOpen());
   }
   isExpanded($section) {
-    return $section.classList.contains(this.sectionExpandedClass);
+    return $section.classList.contains(sectionExpandedModifier);
   }
   areAllSectionsOpen() {
     return Array.from(this.$sections).every($section => this.isExpanded($section));
@@ -709,7 +751,7 @@ class Accordion extends ConfigurableComponent {
     }
     this.$showAllButton.setAttribute('aria-expanded', expanded.toString());
     this.$showAllText.textContent = expanded ? this.i18n.t('hideAllSections') : this.i18n.t('showAllSections');
-    this.$showAllIcon.classList.toggle(this.downChevronIconClass, !expanded);
+    this.$showAllIcon.classList.toggle(iconOpenModifier, !expanded);
   }
 
   /**
@@ -723,7 +765,7 @@ class Accordion extends ConfigurableComponent {
    * @returns {string | undefined | null} Identifier for section
    */
   getIdentifier($section) {
-    const $button = $section.querySelector(`.${this.sectionButtonClass}`);
+    const $button = $section.querySelector(`.${sectionButtonClass}`);
     return $button == null ? void 0 : $button.getAttribute('aria-controls');
   }
   storeState($section, isExpanded) {
@@ -752,10 +794,11 @@ class Accordion extends ConfigurableComponent {
     }
   }
   getButtonPunctuationEl() {
-    const $punctuationEl = document.createElement('span');
-    $punctuationEl.classList.add('govuk-visually-hidden', this.sectionHeadingDividerClass);
-    $punctuationEl.textContent = ', ';
-    return $punctuationEl;
+    const $element = createElement('span', {
+      class: 'govuk-visually-hidden govuk-accordion__section-heading-divider'
+    });
+    $element.textContent = ', ';
+    return $element;
   }
 }
 
diff --git a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs
index bb0c3776c..1b7bb226a 100644
--- a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs
@@ -1,7 +1,20 @@
 import { ConfigurableComponent } from '../../common/configuration.mjs';
+import { createElement } from '../../common/create-element.mjs';
 import { ElementError } from '../../errors/index.mjs';
 import { I18n } from '../../i18n.mjs';
 
+const sectionClass = 'govuk-accordion__section';
+const sectionExpandedModifier = 'govuk-accordion__section--expanded';
+const sectionButtonClass = 'govuk-accordion__section-button';
+const sectionHeaderClass = 'govuk-accordion__section-header';
+const sectionHeadingClass = 'govuk-accordion__section-heading';
+const sectionHeadingTextClass = 'govuk-accordion__section-heading-text';
+const sectionToggleTextClass = 'govuk-accordion__section-toggle-text';
+const iconClass = 'govuk-accordion-nav__chevron';
+const iconOpenModifier = 'govuk-accordion-nav__chevron--down';
+const sectionSummaryClass = 'govuk-accordion__section-summary';
+const sectionContentClass = 'govuk-accordion__section-content';
+
 /**
  * Accordion component
  *
@@ -25,35 +38,16 @@ class Accordion extends ConfigurableComponent {
   constructor($root, config = {}) {
     super($root, config);
     this.i18n = void 0;
-    this.controlsClass = 'govuk-accordion__controls';
-    this.showAllClass = 'govuk-accordion__show-all';
-    this.showAllTextClass = 'govuk-accordion__show-all-text';
-    this.sectionClass = 'govuk-accordion__section';
-    this.sectionExpandedClass = 'govuk-accordion__section--expanded';
-    this.sectionButtonClass = 'govuk-accordion__section-button';
-    this.sectionHeaderClass = 'govuk-accordion__section-header';
-    this.sectionHeadingClass = 'govuk-accordion__section-heading';
-    this.sectionHeadingDividerClass = 'govuk-accordion__section-heading-divider';
-    this.sectionHeadingTextClass = 'govuk-accordion__section-heading-text';
-    this.sectionHeadingTextFocusClass = 'govuk-accordion__section-heading-text-focus';
-    this.sectionShowHideToggleClass = 'govuk-accordion__section-toggle';
-    this.sectionShowHideToggleFocusClass = 'govuk-accordion__section-toggle-focus';
-    this.sectionShowHideTextClass = 'govuk-accordion__section-toggle-text';
-    this.upChevronIconClass = 'govuk-accordion-nav__chevron';
-    this.downChevronIconClass = 'govuk-accordion-nav__chevron--down';
-    this.sectionSummaryClass = 'govuk-accordion__section-summary';
-    this.sectionSummaryFocusClass = 'govuk-accordion__section-summary-focus';
-    this.sectionContentClass = 'govuk-accordion__section-content';
     this.$sections = void 0;
     this.$showAllButton = null;
     this.$showAllIcon = null;
     this.$showAllText = null;
     this.i18n = new I18n(this.config.i18n);
-    const $sections = this.$root.querySelectorAll(`.${this.sectionClass}`);
+    const $sections = this.$root.querySelectorAll(`.${sectionClass}`);
     if (!$sections.length) {
       throw new ElementError({
         component: Accordion,
-        identifier: `Sections (\`<div class="${this.sectionClass}">\`)`
+        identifier: `Sections (\`<div class="${sectionClass}">\`)`
       });
     }
     this.$sections = $sections;
@@ -62,19 +56,23 @@ class Accordion extends ConfigurableComponent {
     this.updateShowAllButton(this.areAllSectionsOpen());
   }
   initControls() {
-    this.$showAllButton = document.createElement('button');
-    this.$showAllButton.setAttribute('type', 'button');
-    this.$showAllButton.setAttribute('class', this.showAllClass);
-    this.$showAllButton.setAttribute('aria-expanded', 'false');
-    this.$showAllIcon = document.createElement('span');
-    this.$showAllIcon.classList.add(this.upChevronIconClass);
+    this.$showAllButton = createElement('button', {
+      type: 'button',
+      class: 'govuk-accordion__show-all',
+      'aria-expanded': 'false'
+    });
+    this.$showAllIcon = createElement('span', {
+      class: iconClass
+    });
     this.$showAllButton.appendChild(this.$showAllIcon);
-    const $accordionControls = document.createElement('div');
-    $accordionControls.setAttribute('class', this.controlsClass);
+    const $accordionControls = createElement('div', {
+      class: 'govuk-accordion__controls'
+    });
     $accordionControls.appendChild(this.$showAllButton);
     this.$root.insertBefore($accordionControls, this.$root.firstChild);
-    this.$showAllText = document.createElement('span');
-    this.$showAllText.classList.add(this.showAllTextClass);
+    this.$showAllText = createElement('span', {
+      class: 'govuk-accordion__show-all-text'
+    });
     this.$showAllButton.appendChild(this.$showAllText);
     this.$showAllButton.addEventListener('click', () => this.onShowOrHideAllToggle());
     if ('onbeforematch' in document) {
@@ -83,11 +81,11 @@ class Accordion extends ConfigurableComponent {
   }
   initSectionHeaders() {
     this.$sections.forEach(($section, i) => {
-      const $header = $section.querySelector(`.${this.sectionHeaderClass}`);
+      const $header = $section.querySelector(`.${sectionHeaderClass}`);
       if (!$header) {
         throw new ElementError({
           component: Accordion,
-          identifier: `Section headers (\`<div class="${this.sectionHeaderClass}">\`)`
+          identifier: `Section headers (\`<div class="${sectionHeaderClass}">\`)`
         });
       }
       this.constructHeaderMarkup($header, i);
@@ -97,73 +95,105 @@ class Accordion extends ConfigurableComponent {
     });
   }
   constructHeaderMarkup($header, index) {
-    const $span = $header.querySelector(`.${this.sectionButtonClass}`);
-    const $heading = $header.querySelector(`.${this.sectionHeadingClass}`);
-    const $summary = $header.querySelector(`.${this.sectionSummaryClass}`);
+    const $span = $header.querySelector(`.${sectionButtonClass}`);
+    const $heading = $header.querySelector(`.${sectionHeadingClass}`);
+    const $summary = $header.querySelector(`.${sectionSummaryClass}`);
     if (!$heading) {
       throw new ElementError({
         component: Accordion,
-        identifier: `Section heading (\`.${this.sectionHeadingClass}\`)`
+        identifier: `Section heading (\`.${sectionHeadingClass}\`)`
       });
     }
     if (!$span) {
       throw new ElementError({
         component: Accordion,
-        identifier: `Section button placeholder (\`<span class="${this.sectionButtonClass}">\`)`
+        identifier: `Section button placeholder (\`<span class="${sectionButtonClass}">\`)`
       });
     }
-    const $button = document.createElement('button');
-    $button.setAttribute('type', 'button');
-    $button.setAttribute('aria-controls', `${this.$root.id}-content-${index + 1}`);
+    const $button = createElement('button', {
+      type: 'button',
+      'aria-controls': `${this.$root.id}-content-${index + 1}`
+    });
     for (const attr of Array.from($span.attributes)) {
       if (attr.name !== 'id') {
         $button.setAttribute(attr.name, attr.value);
       }
     }
-    const $headingText = document.createElement('span');
-    $headingText.classList.add(this.sectionHeadingTextClass);
-    $headingText.id = $span.id;
-    const $headingTextFocus = document.createElement('span');
-    $headingTextFocus.classList.add(this.sectionHeadingTextFocusClass);
-    $headingText.appendChild($headingTextFocus);
-    Array.from($span.childNodes).forEach($child => $headingTextFocus.appendChild($child));
-    const $showHideToggle = document.createElement('span');
-    $showHideToggle.classList.add(this.sectionShowHideToggleClass);
-    $showHideToggle.setAttribute('data-nosnippet', '');
-    const $showHideToggleFocus = document.createElement('span');
-    $showHideToggleFocus.classList.add(this.sectionShowHideToggleFocusClass);
-    $showHideToggle.appendChild($showHideToggleFocus);
-    const $showHideText = document.createElement('span');
-    const $showHideIcon = document.createElement('span');
-    $showHideIcon.classList.add(this.upChevronIconClass);
-    $showHideToggleFocus.appendChild($showHideIcon);
-    $showHideText.classList.add(this.sectionShowHideTextClass);
-    $showHideToggleFocus.appendChild($showHideText);
-    $button.appendChild($headingText);
+    $button.appendChild(this.createHeadingText($span));
     $button.appendChild(this.getButtonPunctuationEl());
     if ($summary) {
-      const $summarySpan = document.createElement('span');
-      const $summarySpanFocus = document.createElement('span');
-      $summarySpanFocus.classList.add(this.sectionSummaryFocusClass);
-      $summarySpan.appendChild($summarySpanFocus);
-      for (const attr of Array.from($summary.attributes)) {
-        $summarySpan.setAttribute(attr.name, attr.value);
-      }
-      Array.from($summary.childNodes).forEach($child => $summarySpanFocus.appendChild($child));
       $summary.remove();
-      $button.appendChild($summarySpan);
+      $button.appendChild(this.createSummarySpan($summary));
       $button.appendChild(this.getButtonPunctuationEl());
     }
-    $button.appendChild($showHideToggle);
+    $button.appendChild(this.createShowHideToggle());
     $heading.removeChild($span);
     $heading.appendChild($button);
   }
+
+  /**
+   * Creates a `<span>` rendering the 'Show'/'Hide' toggle
+   *
+   * @returns {HTMLSpanElement} - The `<span>` with the visual representation of the 'Show/Hide' toggle
+   */
+  createShowHideToggle() {
+    const $showHideToggleFocus = createElement('span', {
+      class: 'govuk-accordion__section-toggle-focus'
+    }, [createElement('span', {
+      class: iconClass
+    }), createElement('span', {
+      class: sectionToggleTextClass
+    })]);
+    const $showHideToggle = createElement('span', {
+      class: 'govuk-accordion__section-toggle',
+      'data-nosnippet': ''
+    }, [$showHideToggleFocus]);
+    return $showHideToggle;
+  }
+
+  /**
+   * Creates the `<span>` containing the text of the section's heading
+   *
+   * @param {Element} $span - The heading of the span
+   * @returns {HTMLSpanElement} - The `<span>` containing the text of the section's heading
+   */
+  createHeadingText($span) {
+    const $headingTextFocus = createElement('span', {
+      class: 'govuk-accordion__section-heading-text-focus'
+    }, Array.from($span.childNodes));
+    const $headingText = createElement('span', {
+      class: sectionHeadingTextClass,
+      id: $span.id
+    }, [$headingTextFocus]);
+    return $headingText;
+  }
+
+  /**
+   * Creates the `<span>` element with the summary for the section
+   *
+   * This is necessary because the summary line text is now inside
+   * a button element, which can only contain phrasing content, and
+   * not a `<div>` element
+   *
+   * @param {Element} $summary - The original `<div>` containing the summary
+   * @returns {HTMLSpanElement} - The `<span>` element containing the summary
+   */
+  createSummarySpan($summary) {
+    const $summarySpanFocus = createElement('span', {
+      class: 'govuk-accordion__section-summary-focus'
+    }, Array.from($summary.childNodes));
+    const $summarySpan = createElement('span', {}, [$summarySpanFocus]);
+    for (const attr of Array.from($summary.attributes)) {
+      $summarySpan.setAttribute(attr.name, attr.value);
+    }
+    return $summarySpan;
+  }
   onBeforeMatch(event) {
     const $fragment = event.target;
     if (!($fragment instanceof Element)) {
       return;
     }
-    const $section = $fragment.closest(`.${this.sectionClass}`);
+    const $section = $fragment.closest(`.${sectionClass}`);
     if ($section) {
       this.setExpanded(true, $section);
     }
@@ -182,14 +212,14 @@ class Accordion extends ConfigurableComponent {
     this.updateShowAllButton(nowExpanded);
   }
   setExpanded(expanded, $section) {
-    const $showHideIcon = $section.querySelector(`.${this.upChevronIconClass}`);
-    const $showHideText = $section.querySelector(`.${this.sectionShowHideTextClass}`);
-    const $button = $section.querySelector(`.${this.sectionButtonClass}`);
-    const $content = $section.querySelector(`.${this.sectionContentClass}`);
+    const $showHideIcon = $section.querySelector(`.${iconClass}`);
+    const $showHideText = $section.querySelector(`.${sectionToggleTextClass}`);
+    const $button = $section.querySelector(`.${sectionButtonClass}`);
+    const $content = $section.querySelector(`.${sectionContentClass}`);
     if (!$content) {
       throw new ElementError({
         component: Accordion,
-        identifier: `Section content (\`<div class="${this.sectionContentClass}">\`)`
+        identifier: `Section content (\`<div class="${sectionContentClass}">\`)`
       });
     }
     if (!$showHideIcon || !$showHideText || !$button) {
@@ -199,11 +229,11 @@ class Accordion extends ConfigurableComponent {
     $showHideText.textContent = newButtonText;
     $button.setAttribute('aria-expanded', `${expanded}`);
     const ariaLabelParts = [];
-    const $headingText = $section.querySelector(`.${this.sectionHeadingTextClass}`);
+    const $headingText = $section.querySelector(`.${sectionHeadingTextClass}`);
     if ($headingText) {
       ariaLabelParts.push(`${$headingText.textContent}`.trim());
     }
-    const $summary = $section.querySelector(`.${this.sectionSummaryClass}`);
+    const $summary = $section.querySelector(`.${sectionSummaryClass}`);
     if ($summary) {
       ariaLabelParts.push(`${$summary.textContent}`.trim());
     }
@@ -212,17 +242,17 @@ class Accordion extends ConfigurableComponent {
     $button.setAttribute('aria-label', ariaLabelParts.join(' , '));
     if (expanded) {
       $content.removeAttribute('hidden');
-      $section.classList.add(this.sectionExpandedClass);
-      $showHideIcon.classList.remove(this.downChevronIconClass);
+      $section.classList.add(sectionExpandedModifier);
+      $showHideIcon.classList.remove(iconOpenModifier);
     } else {
       $content.setAttribute('hidden', 'until-found');
-      $section.classList.remove(this.sectionExpandedClass);
-      $showHideIcon.classList.add(this.downChevronIconClass);
+      $section.classList.remove(sectionExpandedModifier);
+      $showHideIcon.classList.add(iconOpenModifier);
     }
     this.updateShowAllButton(this.areAllSectionsOpen());
   }
   isExpanded($section) {
-    return $section.classList.contains(this.sectionExpandedClass);
+    return $section.classList.contains(sectionExpandedModifier);
   }
   areAllSectionsOpen() {
     return Array.from(this.$sections).every($section => this.isExpanded($section));
@@ -233,7 +263,7 @@ class Accordion extends ConfigurableComponent {
     }
     this.$showAllButton.setAttribute('aria-expanded', expanded.toString());
     this.$showAllText.textContent = expanded ? this.i18n.t('hideAllSections') : this.i18n.t('showAllSections');
-    this.$showAllIcon.classList.toggle(this.downChevronIconClass, !expanded);
+    this.$showAllIcon.classList.toggle(iconOpenModifier, !expanded);
   }
 
   /**
@@ -247,7 +277,7 @@ class Accordion extends ConfigurableComponent {
    * @returns {string | undefined | null} Identifier for section
    */
   getIdentifier($section) {
-    const $button = $section.querySelector(`.${this.sectionButtonClass}`);
+    const $button = $section.querySelector(`.${sectionButtonClass}`);
     return $button == null ? void 0 : $button.getAttribute('aria-controls');
   }
   storeState($section, isExpanded) {
@@ -276,10 +306,11 @@ class Accordion extends ConfigurableComponent {
     }
   }
   getButtonPunctuationEl() {
-    const $punctuationEl = document.createElement('span');
-    $punctuationEl.classList.add('govuk-visually-hidden', this.sectionHeadingDividerClass);
-    $punctuationEl.textContent = ', ';
-    return $punctuationEl;
+    const $element = createElement('span', {
+      class: 'govuk-visually-hidden govuk-accordion__section-heading-divider'
+    });
+    $element.textContent = ', ';
+    return $element;
   }
 }
 
diff --git a/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.js b/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.js
index 04aca7263..902df5230 100644
--- a/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.js
@@ -291,6 +291,14 @@
    * @typedef {typeof Component & ChildClass<ConfigurationType>} ChildClassConstructor<ConfigurationType>
    */
 
+  function createElement(tagName, attributes = {}, children) {
+    const el = document.createElement(tagName);
+    Object.entries(attributes).forEach(([name, value]) => {
+      el.setAttribute(name, value);
+    });
+    return el;
+  }
+
   class I18n {
     constructor(translations = {}, config = {}) {
       var _config$locale;
@@ -533,9 +541,10 @@
       window.addEventListener('pageshow', this.resetPage.bind(this));
     }
     initUpdateSpan() {
-      this.$updateSpan = document.createElement('span');
-      this.$updateSpan.setAttribute('role', 'status');
-      this.$updateSpan.className = 'govuk-visually-hidden';
+      this.$updateSpan = createElement('span', {
+        role: 'status',
+        class: 'govuk-visually-hidden'
+      });
       this.$root.appendChild(this.$updateSpan);
     }
     initButtonClickHandler() {
@@ -545,13 +554,14 @@
       }
     }
     buildIndicator() {
-      this.$indicatorContainer = document.createElement('div');
-      this.$indicatorContainer.className = 'govuk-exit-this-page__indicator';
-      this.$indicatorContainer.setAttribute('aria-hidden', 'true');
+      this.$indicatorContainer = createElement('div', {
+        class: 'govuk-exit-this-page__indicator',
+        'aria-hidden': 'true'
+      });
       for (let i = 0; i < 3; i++) {
-        const $indicator = document.createElement('div');
-        $indicator.className = 'govuk-exit-this-page__indicator-light';
-        this.$indicatorContainer.appendChild($indicator);
+        this.$indicatorContainer.appendChild(createElement('div', {
+          class: 'govuk-exit-this-page__indicator-light'
+        }));
       }
       this.$button.appendChild(this.$indicatorContainer);
     }
@@ -571,9 +581,10 @@
       }
       this.$updateSpan.textContent = '';
       document.body.classList.add('govuk-exit-this-page-hide-content');
-      this.$overlay = document.createElement('div');
-      this.$overlay.className = 'govuk-exit-this-page-overlay';
-      this.$overlay.setAttribute('role', 'alert');
+      this.$overlay = createElement('div', {
+        class: 'govuk-exit-this-page-overlay',
+        role: 'alert'
+      });
       document.body.appendChild(this.$overlay);
       this.$overlay.textContent = this.i18n.t('activated');
       window.location.href = this.$button.href;
diff --git a/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.mjs
index e66c5301d..3b319bd8f 100644
--- a/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.bundle.mjs
@@ -285,6 +285,14 @@ function extractConfigByNamespace(schema, dataset, namespace) {
  * @typedef {typeof Component & ChildClass<ConfigurationType>} ChildClassConstructor<ConfigurationType>
  */
 
+function createElement(tagName, attributes = {}, children) {
+  const el = document.createElement(tagName);
+  Object.entries(attributes).forEach(([name, value]) => {
+    el.setAttribute(name, value);
+  });
+  return el;
+}
+
 class I18n {
   constructor(translations = {}, config = {}) {
     var _config$locale;
@@ -527,9 +535,10 @@ class ExitThisPage extends ConfigurableComponent {
     window.addEventListener('pageshow', this.resetPage.bind(this));
   }
   initUpdateSpan() {
-    this.$updateSpan = document.createElement('span');
-    this.$updateSpan.setAttribute('role', 'status');
-    this.$updateSpan.className = 'govuk-visually-hidden';
+    this.$updateSpan = createElement('span', {
+      role: 'status',
+      class: 'govuk-visually-hidden'
+    });
     this.$root.appendChild(this.$updateSpan);
   }
   initButtonClickHandler() {
@@ -539,13 +548,14 @@ class ExitThisPage extends ConfigurableComponent {
     }
   }
   buildIndicator() {
-    this.$indicatorContainer = document.createElement('div');
-    this.$indicatorContainer.className = 'govuk-exit-this-page__indicator';
-    this.$indicatorContainer.setAttribute('aria-hidden', 'true');
+    this.$indicatorContainer = createElement('div', {
+      class: 'govuk-exit-this-page__indicator',
+      'aria-hidden': 'true'
+    });
     for (let i = 0; i < 3; i++) {
-      const $indicator = document.createElement('div');
-      $indicator.className = 'govuk-exit-this-page__indicator-light';
-      this.$indicatorContainer.appendChild($indicator);
+      this.$indicatorContainer.appendChild(createElement('div', {
+        class: 'govuk-exit-this-page__indicator-light'
+      }));
     }
     this.$button.appendChild(this.$indicatorContainer);
   }
@@ -565,9 +575,10 @@ class ExitThisPage extends ConfigurableComponent {
     }
     this.$updateSpan.textContent = '';
     document.body.classList.add('govuk-exit-this-page-hide-content');
-    this.$overlay = document.createElement('div');
-    this.$overlay.className = 'govuk-exit-this-page-overlay';
-    this.$overlay.setAttribute('role', 'alert');
+    this.$overlay = createElement('div', {
+      class: 'govuk-exit-this-page-overlay',
+      role: 'alert'
+    });
     document.body.appendChild(this.$overlay);
     this.$overlay.textContent = this.i18n.t('activated');
     window.location.href = this.$button.href;
diff --git a/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.mjs b/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.mjs
index a16cca753..4247162cd 100644
--- a/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/exit-this-page/exit-this-page.mjs
@@ -1,4 +1,5 @@
 import { ConfigurableComponent } from '../../common/configuration.mjs';
+import { createElement } from '../../common/create-element.mjs';
 import { ElementError } from '../../errors/index.mjs';
 import { I18n } from '../../i18n.mjs';
 
@@ -51,9 +52,10 @@ class ExitThisPage extends ConfigurableComponent {
     window.addEventListener('pageshow', this.resetPage.bind(this));
   }
   initUpdateSpan() {
-    this.$updateSpan = document.createElement('span');
-    this.$updateSpan.setAttribute('role', 'status');
-    this.$updateSpan.className = 'govuk-visually-hidden';
+    this.$updateSpan = createElement('span', {
+      role: 'status',
+      class: 'govuk-visually-hidden'
+    });
     this.$root.appendChild(this.$updateSpan);
   }
   initButtonClickHandler() {
@@ -63,13 +65,14 @@ class ExitThisPage extends ConfigurableComponent {
     }
   }
   buildIndicator() {
-    this.$indicatorContainer = document.createElement('div');
-    this.$indicatorContainer.className = 'govuk-exit-this-page__indicator';
-    this.$indicatorContainer.setAttribute('aria-hidden', 'true');
+    this.$indicatorContainer = createElement('div', {
+      class: 'govuk-exit-this-page__indicator',
+      'aria-hidden': 'true'
+    });
     for (let i = 0; i < 3; i++) {
-      const $indicator = document.createElement('div');
-      $indicator.className = 'govuk-exit-this-page__indicator-light';
-      this.$indicatorContainer.appendChild($indicator);
+      this.$indicatorContainer.appendChild(createElement('div', {
+        class: 'govuk-exit-this-page__indicator-light'
+      }));
     }
     this.$button.appendChild(this.$indicatorContainer);
   }
@@ -89,9 +92,10 @@ class ExitThisPage extends ConfigurableComponent {
     }
     this.$updateSpan.textContent = '';
     document.body.classList.add('govuk-exit-this-page-hide-content');
-    this.$overlay = document.createElement('div');
-    this.$overlay.className = 'govuk-exit-this-page-overlay';
-    this.$overlay.setAttribute('role', 'alert');
+    this.$overlay = createElement('div', {
+      class: 'govuk-exit-this-page-overlay',
+      role: 'alert'
+    });
     document.body.appendChild(this.$overlay);
     this.$overlay.textContent = this.i18n.t('activated');
     window.location.href = this.$button.href;
diff --git a/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.js b/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.js
index ba44a62b9..86d0dea29 100644
--- a/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.js
@@ -296,6 +296,14 @@
    * @typedef {typeof Component & ChildClass<ConfigurationType>} ChildClassConstructor<ConfigurationType>
    */
 
+  function createElement(tagName, attributes = {}, children) {
+    const el = document.createElement(tagName);
+    Object.entries(attributes).forEach(([name, value]) => {
+      el.setAttribute(name, value);
+    });
+    return el;
+  }
+
   class I18n {
     constructor(translations = {}, config = {}) {
       var _config$locale;
@@ -536,9 +544,10 @@
         locale: closestAttributeValue(this.$root, 'lang')
       });
       this.$showHideButton.removeAttribute('hidden');
-      const $screenReaderStatusMessage = document.createElement('div');
-      $screenReaderStatusMessage.className = 'govuk-password-input__sr-status govuk-visually-hidden';
-      $screenReaderStatusMessage.setAttribute('aria-live', 'polite');
+      const $screenReaderStatusMessage = createElement('div', {
+        class: 'govuk-password-input__sr-status govuk-visually-hidden',
+        'aria-live': 'polite'
+      });
       this.$screenReaderStatusMessage = $screenReaderStatusMessage;
       this.$input.insertAdjacentElement('afterend', $screenReaderStatusMessage);
       this.$showHideButton.addEventListener('click', this.toggle.bind(this));
diff --git a/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.mjs
index 2e2e65f28..5f6a12425 100644
--- a/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/password-input/password-input.bundle.mjs
@@ -290,6 +290,14 @@ function extractConfigByNamespace(schema, dataset, namespace) {
  * @typedef {typeof Component & ChildClass<ConfigurationType>} ChildClassConstructor<ConfigurationType>
  */
 
+function createElement(tagName, attributes = {}, children) {
+  const el = document.createElement(tagName);
+  Object.entries(attributes).forEach(([name, value]) => {
+    el.setAttribute(name, value);
+  });
+  return el;
+}
+
 class I18n {
   constructor(translations = {}, config = {}) {
     var _config$locale;
@@ -530,9 +538,10 @@ class PasswordInput extends ConfigurableComponent {
       locale: closestAttributeValue(this.$root, 'lang')
     });
     this.$showHideButton.removeAttribute('hidden');
-    const $screenReaderStatusMessage = document.createElement('div');
-    $screenReaderStatusMessage.className = 'govuk-password-input__sr-status govuk-visually-hidden';
-    $screenReaderStatusMessage.setAttribute('aria-live', 'polite');
+    const $screenReaderStatusMessage = createElement('div', {
+      class: 'govuk-password-input__sr-status govuk-visually-hidden',
+      'aria-live': 'polite'
+    });
     this.$screenReaderStatusMessage = $screenReaderStatusMessage;
     this.$input.insertAdjacentElement('afterend', $screenReaderStatusMessage);
     this.$showHideButton.addEventListener('click', this.toggle.bind(this));
diff --git a/packages/govuk-frontend/dist/govuk/components/password-input/password-input.mjs b/packages/govuk-frontend/dist/govuk/components/password-input/password-input.mjs
index d246fcb7a..87fa6992a 100644
--- a/packages/govuk-frontend/dist/govuk/components/password-input/password-input.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/password-input/password-input.mjs
@@ -1,5 +1,6 @@
 import { closestAttributeValue } from '../../common/closest-attribute-value.mjs';
 import { ConfigurableComponent } from '../../common/configuration.mjs';
+import { createElement } from '../../common/create-element.mjs';
 import { ElementError } from '../../errors/index.mjs';
 import { I18n } from '../../i18n.mjs';
 
@@ -50,9 +51,10 @@ class PasswordInput extends ConfigurableComponent {
       locale: closestAttributeValue(this.$root, 'lang')
     });
     this.$showHideButton.removeAttribute('hidden');
-    const $screenReaderStatusMessage = document.createElement('div');
-    $screenReaderStatusMessage.className = 'govuk-password-input__sr-status govuk-visually-hidden';
-    $screenReaderStatusMessage.setAttribute('aria-live', 'polite');
+    const $screenReaderStatusMessage = createElement('div', {
+      class: 'govuk-password-input__sr-status govuk-visually-hidden',
+      'aria-live': 'polite'
+    });
     this.$screenReaderStatusMessage = $screenReaderStatusMessage;
     this.$input.insertAdjacentElement('afterend', $screenReaderStatusMessage);
     this.$showHideButton.addEventListener('click', this.toggle.bind(this));

Action run for 6c61aa5

36degrees and others added 9 commits February 28, 2025 17:32
This will help sharing them with the upcoming AccordionSection class.
The ultimate goal is to remove all those used to create then find back elements,
in favour of storing these elements in the AccordionSection instance or the Accordion instance itself.
This makes the construction of the header markup easier to follow
Simplifies the constructHeaderMarkup function
Allows regrouping code for creating components a little more,
as well as getting rid of a couple of loops over child nodes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants