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

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 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).

36degrees and others added 8 commits December 12, 2024 17:44
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
Copy link

github-actions bot commented Dec 13, 2024

📋 Stats

File sizes

File Size
dist/govuk-frontend-development.min.css 118.41 KiB
dist/govuk-frontend-development.min.js 40.92 KiB
packages/govuk-frontend/dist/govuk/all.bundle.js 92.4 KiB
packages/govuk-frontend/dist/govuk/all.bundle.mjs 86.63 KiB
packages/govuk-frontend/dist/govuk/all.mjs 1.25 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs 1.74 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend.min.css 118.4 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend.min.js 40.91 KiB
packages/govuk-frontend/dist/govuk/i18n.mjs 5.55 KiB
packages/govuk-frontend/dist/govuk/init.mjs 7.13 KiB

Modules

File Size (bundled) Size (minified)
all.mjs 82.08 KiB 38.39 KiB
accordion.mjs 26.36 KiB 11.69 KiB
button.mjs 9.09 KiB 3.78 KiB
character-count.mjs 25.39 KiB 10.9 KiB
checkboxes.mjs 7.81 KiB 3.42 KiB
error-summary.mjs 10.99 KiB 4.54 KiB
exit-this-page.mjs 20.21 KiB 10.2 KiB
header.mjs 6.46 KiB 3.22 KiB
notification-banner.mjs 9.35 KiB 3.7 KiB
password-input.mjs 18.39 KiB 8.41 KiB
radios.mjs 6.81 KiB 2.98 KiB
service-navigation.mjs 6.44 KiB 3.26 KiB
skip-link.mjs 6.4 KiB 2.76 KiB
tabs.mjs 12.04 KiB 6.67 KiB

View stats and visualisations on the review app


Action run for d7f1a99

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 5a4b212e3..266a24bd0 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
@@ -14,13 +14,13 @@ function getBreakpoint(t) {
 
 function setFocus(t, e = {}) {
     var n;
-    const s = t.getAttribute("tabindex");
+    const i = t.getAttribute("tabindex");
 
     function onBlur() {
         var n;
-        null == (n = e.onBlur) || n.call(t), s || t.removeAttribute("tabindex")
+        null == (n = e.onBlur) || n.call(t), i || t.removeAttribute("tabindex")
     }
-    s || t.setAttribute("tabindex", "-1"), t.addEventListener("focus", (function() {
+    i || t.setAttribute("tabindex", "-1"), t.addEventListener("focus", (function() {
         t.addEventListener("blur", onBlur, {
             once: !0
         })
@@ -64,11 +64,11 @@ class ElementError extends GOVUKFrontendError {
         if ("object" == typeof t) {
             const {
                 component: n,
-                identifier: s,
-                element: i,
-                expectedType: o
+                identifier: i,
+                element: o,
+                expectedType: s
             } = t;
-            e = s, e += i ? ` is not of type ${null!=o?o:"HTMLElement"}` : " not found", e = formatErrorMessage(n, e)
+            e = i, e += o ? ` is not of type ${null!=s?s:"HTMLElement"}` : " not found", e = formatErrorMessage(n, e)
         }
         super(e), this.name = "ElementError"
     }
@@ -118,57 +118,66 @@ class ConfigurableComponent extends GOVUKFrontendComponent {
     }
     constructor(e, n) {
         super(e), this._config = void 0;
-        const s = this.constructor;
-        if (void 0 === s.defaults) throw new ConfigError(formatErrorMessage(s, "Config passed as parameter into constructor but no defaults defined"));
-        const i = function(Component, t) {
+        const i = this.constructor;
+        if (void 0 === i.defaults) throw new ConfigError(formatErrorMessage(i, "Config passed as parameter into constructor but no defaults defined"));
+        const o = function(Component, t) {
             if (void 0 === Component.schema) throw new ConfigError(formatErrorMessage(Component, "Config passed as parameter into constructor but no schema defined"));
             const e = {};
-            for (const [n, s] of Object.entries(Component.schema.properties)) n in t && (e[n] = normaliseString(t[n], s)), "object" === (null == s ? void 0 : s.type) && (e[n] = extractConfigByNamespace(Component.schema, t, n));
+            for (const [n, i] of Object.entries(Component.schema.properties)) n in t && (e[n] = normaliseString(t[n], i)), "object" === (null == i ? void 0 : i.type) && (e[n] = extractConfigByNamespace(Component.schema, t, n));
             return e
-        }(s, this._$root.dataset);
-        this._config = mergeConfigs(s.defaults, null != n ? n : {}, 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 n = t ? t.trim() : "";
-    let s, i = null == e ? void 0 : e.type;
-    switch (i || (["true", "false"].includes(n) && (i = "boolean"), n.length > 0 && isFinite(Number(n)) && (i = "number")), i) {
+    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":
-            s = "true" === n;
+            i = "true" === n;
             break;
         case "number":
-            s = Number(n);
+            i = Number(n);
             break;
         default:
-            s = t
+            i = t
     }
-    return s
+    return i
 }
 
 function mergeConfigs(...t) {
     const e = {};
     for (const n of t)
         for (const t of Object.keys(n)) {
-            const s = e[t],
-                i = n[t];
-            isObject(s) && isObject(i) ? e[t] = mergeConfigs(s, i) : e[t] = i
+            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, n) {
-    const s = t.properties[n];
-    if ("object" !== (null == s ? void 0 : s.type)) return;
-    const i = {
+    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 [s, i] of e.entries()) "object" == typeof t && (s < e.length - 1 ? (isObject(t[i]) || (t[i] = {}), t = t[i]) : o !== n && (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()) "object" == typeof t && (i < e.length - 1 ? (isObject(t[o]) || (t[o] = {}), t = t[o]) : s !== n && (t[o] = normaliseString(r)))
     }
-    return i[n]
+    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 = {}) {
@@ -179,8 +188,8 @@ class I18n {
         if (!t) throw new Error("i18n: lookup key missing");
         let n = this.translations[t];
         if ("number" == typeof(null == e ? void 0 : e.count) && "object" == typeof n) {
-            const s = n[this.getPluralSuffix(t, e.count)];
-            s && (n = s)
+            const i = n[this.getPluralSuffix(t, e.count)];
+            i && (n = i)
         }
         if ("string" == typeof n) {
             if (n.match(/%{(.\S+)}/)) {
@@ -193,9 +202,9 @@ class I18n {
     }
     replacePlaceholders(t, e) {
         const n = Intl.NumberFormat.supportedLocalesOf(this.locale).length ? new Intl.NumberFormat(this.locale) : void 0;
-        return t.replace(/%{(.\S+)}/g, (function(t, s) {
-            if (Object.prototype.hasOwnProperty.call(e, s)) {
-                const t = e[s];
+        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`)
@@ -207,10 +216,10 @@ class I18n {
     getPluralSuffix(t, e) {
         if (e = Number(e), !isFinite(e)) return "other";
         const n = this.translations[t],
-            s = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(e) : this.selectPluralFormUsingFallbackRules(e);
+            i = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(e) : this.selectPluralFormUsingFallbackRules(e);
         if ("object" == typeof n) {
-            if (s in n) return s;
-            if ("other" in n) return console.warn(`i18n: Missing plural form ".${s}" for "${this.locale}" locale. Falling back to ".other".`), "other"
+            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`)
     }
@@ -252,70 +261,106 @@ I18n.pluralRulesMap = {
     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 n = this.$root.querySelectorAll(`.${this.sectionClass}`);
-        if (!n.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 = n, 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 n = t.querySelector(`.${this.sectionHeaderClass}`);
+            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(n, e), this.setExpanded(this.isExpanded(t), t), n.addEventListener("click", (() => this.onSectionToggle(t))), this.setInitialState(t)
         }))
     }
     constructHeaderMarkup(t, e) {
-        const n = t.querySelector(`.${this.sectionButtonClass}`),
-            s = t.querySelector(`.${this.sectionHeadingClass}`),
-            i = t.querySelector(`.${this.sectionSummaryClass}`);
-        if (!s) 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 (!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(n.attributes)) "id" !== d.name && o.setAttribute(d.name, d.value);
-        const r = document.createElement("span");
-        r.classList.add(this.sectionHeadingTextClass), r.id = n.id;
-        const a = document.createElement("span");
-        a.classList.add(this.sectionHeadingTextFocusClass), r.appendChild(a), Array.from(n.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 n of Array.from(i.attributes)) t.setAttribute(n.name, n.value);
-            Array.from(i.childNodes).forEach((t => e.appendChild(t))), i.remove(), o.appendChild(t), o.appendChild(this.getButtonPunctuationEl())
-        }
-        o.appendChild(c), s.removeChild(n), s.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 n = e.closest(`.${this.sectionClass}`);
-        n && this.setExpanded(!0, n)
+        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);
@@ -328,36 +373,36 @@ class Accordion extends ConfigurableComponent {
         })), this.updateShowAllButton(t)
     }
     setExpanded(t, e) {
-        const n = e.querySelector(`.${this.upChevronIconClass}`),
-            s = 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 (!n || !s || !i) return;
-        const r = t ? this.i18n.t("hideSection") : this.i18n.t("showSection");
-        s.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), n.classList.remove(this.downChevronIconClass)) : (o.setAttribute("hidden", "until-found"), e.classList.remove(this.sectionExpandedClass), n.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) {
@@ -365,7 +410,7 @@ class Accordion extends ConfigurableComponent {
         const n = this.getIdentifier(t);
         if (n) try {
             window.sessionStorage.setItem(n, e.toString())
-        } catch (s) {}
+        } catch (i) {}
     }
     setInitialState(t) {
         if (!this.config.rememberExpanded) return;
@@ -376,8 +421,10 @@ class Accordion extends ConfigurableComponent {
         } 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({
@@ -437,34 +484,34 @@ class CharacterCount extends ConfigurableComponent {
         }), e
     }
     constructor(t, e = {}) {
-        var n, s;
+        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 = function(t, e) {
             const n = [];
-            for (const [s, i] of Object.entries(t)) {
+            for (const [i, o] of Object.entries(t)) {
                 const t = [];
-                if (Array.isArray(i)) {
+                if (Array.isArray(o)) {
                     for (const {
                             required: n,
-                            errorMessage: s
+                            errorMessage: i
                         }
-                        of i) n.every((t => !!e[t])) || t.push(s);
-                    "anyOf" !== s || i.length - t.length >= 1 || n.push(...t)
+                        of o) n.every((t => !!e[t])) || t.push(i);
+                    "anyOf" !== i || o.length - t.length >= 1 || n.push(...t)
                 }
             }
             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 != (n = null != (s = this.config.maxwords) ? s : this.config.maxlength) ? n : 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({
@@ -643,8 +690,8 @@ class ErrorSummary extends ConfigurableComponent {
         if (!e) return !1;
         const n = document.getElementById(e);
         if (!n) return !1;
-        const s = this.getAssociatedLegendOrLabel(n);
-        return !!s && (s.scrollIntoView(), n.focus({
+        const i = this.getAssociatedLegendOrLabel(n);
+        return !!i && (i.scrollIntoView(), n.focus({
             preventScroll: !0
         }), !0)
     }
@@ -656,10 +703,10 @@ class ErrorSummary extends ConfigurableComponent {
             if (e.length) {
                 const n = e[0];
                 if (t instanceof HTMLInputElement && ("checkbox" === t.type || "radio" === t.type)) return n;
-                const s = n.getBoundingClientRect().top,
-                    i = t.getBoundingClientRect();
-                if (i.height && window.innerHeight) {
-                    if (i.top + i.height - s < window.innerHeight / 2) 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
                 }
             }
         }
@@ -686,21 +733,26 @@ class ExitThisPage extends ConfigurableComponent {
             identifier: "Button (`.govuk-exit-this-page__button`)"
         });
         this.i18n = new I18n(this.config.i18n), this.$button = n;
-        const s = document.querySelector(".govuk-js-exit-this-page-skiplink");
-        s instanceof HTMLAnchorElement && (this.$skiplinkButton = s), 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))
+        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() {
@@ -711,7 +763,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()
@@ -758,13 +813,13 @@ class Header extends GOVUKFrontendComponent {
             component: Header,
             identifier: 'Navigation button (`<button class="govuk-js-header-toggle">`) attribute (`aria-controls`)'
         });
-        const s = document.getElementById(n);
-        if (!s) throw new ElementError({
+        const i = document.getElementById(n);
+        if (!i) throw new ElementError({
             component: Header,
-            element: s,
+            element: i,
             identifier: `Navigation (\`<ul id="${n}">\`)`
         });
-        this.$menu = s, 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");
@@ -807,19 +862,22 @@ class PasswordInput extends ConfigurableComponent {
             identifier: "Form field (`.govuk-js-password-input-input`)"
         });
         if ("password" !== n.type) throw new ElementError("Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.");
-        const s = this.$root.querySelector(".govuk-js-password-input-toggle");
-        if (!(s instanceof HTMLButtonElement)) throw new ElementError({
+        const i = this.$root.querySelector(".govuk-js-password-input-toggle");
+        if (!(i instanceof HTMLButtonElement)) throw new ElementError({
             component: PasswordInput,
-            element: s,
+            element: i,
             expectedType: "HTMLButtonElement",
             identifier: "Button (`.govuk-js-password-input-toggle`)"
         });
-        if ("button" !== s.type) throw new ElementError("Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.");
-        this.$input = n, this.$showHideButton = s, 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()
     }
@@ -837,8 +895,8 @@ class PasswordInput extends ConfigurableComponent {
         this.$input.setAttribute("type", t);
         const e = "password" === t,
             n = e ? "show" : "hide",
-            s = 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(`${s}Announcement`)
+            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({
@@ -892,11 +950,11 @@ class Radios extends GOVUKFrontendComponent {
         const e = t.target;
         if (!(e instanceof HTMLInputElement) || "radio" !== e.type) return;
         const n = document.querySelectorAll('input[type="radio"][aria-controls]'),
-            s = e.form,
-            i = e.name;
+            i = e.form,
+            o = e.name;
         n.forEach((t => {
-            const e = t.form === s;
-            t.name === i && e && this.syncConditionalRevealWithInputState(t)
+            const e = t.form === i;
+            t.name === o && e && this.syncConditionalRevealWithInputState(t)
         }))
     }
 }
@@ -911,13 +969,13 @@ class ServiceNavigation extends GOVUKFrontendComponent {
             component: ServiceNavigation,
             identifier: 'Navigation button (`<button class="govuk-js-service-navigation-toggle">`) attribute (`aria-controls`)'
         });
-        const s = document.getElementById(n);
-        if (!s) throw new ElementError({
+        const i = document.getElementById(n);
+        if (!i) throw new ElementError({
             component: ServiceNavigation,
-            element: s,
+            element: i,
             identifier: `Navigation (\`<ul id="${n}">\`)`
         });
-        this.$menu = s, 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");
@@ -940,21 +998,21 @@ class SkipLink extends GOVUKFrontendComponent {
         var e;
         super(t);
         const n = this.$root.hash,
-            s = null != (e = this.$root.getAttribute("href")) ? e : "";
-        let i;
+            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="${s}"\`) 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(n);
-        if (!o) throw new ElementError(`Skip link: Target link (\`href="${s}"\`) 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() {
@@ -977,16 +1035,16 @@ class Tabs extends GOVUKFrontendComponent {
         });
         this.$tabs = e, this.boundTabClick = this.onTabClick.bind(this), this.boundTabKeydown = this.onTabKeydown.bind(this), this.boundOnHashChange = this.onHashChange.bind(this);
         const n = this.$root.querySelector(".govuk-tabs__list"),
-            s = this.$root.querySelectorAll("li.govuk-tabs__list-item");
+            i = this.$root.querySelectorAll("li.govuk-tabs__list-item");
         if (!n) throw new ElementError({
             component: Tabs,
             identifier: 'List (`<ul class="govuk-tabs__list">`)'
         });
-        if (!s.length) throw new ElementError({
+        if (!i.length) throw new ElementError({
             component: Tabs,
             identifier: 'List items (`<li class="govuk-tabs__list-item">`)'
         });
-        this.$tabList = n, this.$tabListItems = s, this.setupResponsiveChecks()
+        this.$tabList = n, this.$tabListItems = i, this.setupResponsiveChecks()
     }
     setupResponsiveChecks() {
         const t = getBreakpoint("tablet");
@@ -1127,30 +1185,30 @@ function initAll(t) {
             [SkipLink],
             [Tabs]
         ],
-        s = {
+        i = {
             scope: null != (e = t.scope) ? e : document,
             onError: t.onError
         };
     n.forEach((([Component, t]) => {
-        createAll(Component, t, s)
+        createAll(Component, t, i)
     }))
 }
 
 function createAll(Component, t, e) {
-    let n, s = document;
-    var i;
-    "object" == typeof e && (s = null != (i = e.scope) ? i : s, n = e.onError);
-    "function" == typeof e && (n = e), e instanceof HTMLElement && (s = e);
-    const o = s.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 (s) {
-            return n ? n(s, {
+        } catch (i) {
+            return n ? n(i, {
                 element: e,
                 component: Component,
                 config: t
-            }) : console.log(s), null
+            }) : console.log(i), null
         }
     })).filter(Boolean) : (n ? n(new SupportError, {
         component: Component,

Action run for d7f1a99

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 7b511f0cb..48ec3e764 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.js
@@ -343,6 +343,19 @@
    * @typedef {typeof GOVUKFrontendComponent & 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;
@@ -536,6 +549,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
    *
@@ -559,35 +584,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;
@@ -596,19 +602,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) {
@@ -617,11 +627,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);
@@ -631,73 +641,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);
       }
@@ -716,14 +758,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) {
@@ -733,11 +775,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());
       }
@@ -746,17 +788,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));
@@ -767,7 +809,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);
     }
 
     /**
@@ -781,7 +823,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) {
@@ -810,10 +852,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;
     }
   }
 
@@ -1486,9 +1529,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() {
@@ -1498,13 +1542,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);
     }
@@ -1524,9 +1569,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;
@@ -1827,9 +1873,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 536f4b22d..9be57b023 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
@@ -337,6 +337,19 @@ function extractConfigByNamespace(schema, dataset, namespace) {
  * @typedef {typeof GOVUKFrontendComponent & 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;
@@ -530,6 +543,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
  *
@@ -553,35 +578,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;
@@ -590,19 +596,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) {
@@ -611,11 +621,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);
@@ -625,73 +635,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);
     }
@@ -710,14 +752,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) {
@@ -727,11 +769,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());
     }
@@ -740,17 +782,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));
@@ -761,7 +803,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);
   }
 
   /**
@@ -775,7 +817,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) {
@@ -804,10 +846,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;
   }
 }
 
@@ -1480,9 +1523,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() {
@@ -1492,13 +1536,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);
   }
@@ -1518,9 +1563,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;
@@ -1821,9 +1867,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 a427fd6a9..906a27ef0 100644
--- a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js
@@ -283,6 +283,19 @@
    * @typedef {typeof GOVUKFrontendComponent & 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;
@@ -476,6 +489,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
    *
@@ -499,35 +524,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;
@@ -536,19 +542,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) {
@@ -557,11 +567,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);
@@ -571,73 +581,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);
       }
@@ -656,14 +698,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) {
@@ -673,11 +715,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());
       }
@@ -686,17 +728,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));
@@ -707,7 +749,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);
     }
 
     /**
@@ -721,7 +763,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) {
@@ -750,10 +792,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 f16d67139..66dd11b75 100644
--- a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs
@@ -277,6 +277,19 @@ function extractConfigByNamespace(schema, dataset, namespace) {
  * @typedef {typeof GOVUKFrontendComponent & 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;
@@ -470,6 +483,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
  *
@@ -493,35 +518,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;
@@ -530,19 +536,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) {
@@ -551,11 +561,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);
@@ -565,73 +575,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);
     }
@@ -650,14 +692,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) {
@@ -667,11 +709,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());
     }
@@ -680,17 +722,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));
@@ -701,7 +743,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);
   }
 
   /**
@@ -715,7 +757,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) {
@@ -744,10 +786,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 57b4b84d0..5e367ea04 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 7268abbf0..92f35dc0b 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
@@ -283,6 +283,14 @@
    * @typedef {typeof GOVUKFrontendComponent & 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;
@@ -525,9 +533,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() {
@@ -537,13 +546,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);
     }
@@ -563,9 +573,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 bba2c7eae..ab05e73b1 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
@@ -277,6 +277,14 @@ function extractConfigByNamespace(schema, dataset, namespace) {
  * @typedef {typeof GOVUKFrontendComponent & 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;
@@ -519,9 +527,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() {
@@ -531,13 +540,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);
   }
@@ -557,9 +567,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 7e6bc888e..bf3fda9a9 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 68ac91f75..c9f0764e1 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
@@ -288,6 +288,14 @@
    * @typedef {typeof GOVUKFrontendComponent & 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;
@@ -528,9 +536,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 768df71f6..105580dcc 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
@@ -282,6 +282,14 @@ function extractConfigByNamespace(schema, dataset, namespace) {
  * @typedef {typeof GOVUKFrontendComponent & 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;
@@ -522,9 +530,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 6e23e64b1..e622bfd83 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 d7f1a99

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