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

Refactor the root type check in GOVUKFrontendComponent #5354

Conversation

patrickpatrickpatrick
Copy link
Contributor

@patrickpatrickpatrick patrickpatrickpatrick commented Sep 25, 2024

What

  • Moves the type check of root into GOVUKFrontendComponent base class which components in the Design System extend
  • Moves assignment of root into the GOVUKFrontendComponent base class which components in the Design System extend
  • Adds elementType which specifies type of root to be checked and can be overloaded by child classes
  • Add getGOVUKFrontendExportsNames to rollup.release.config.mjs which ensures that we don't run into an error in which window.* is undefined during build
  • Updates the Skeleton section of Javascript documentation to use new format
  • Remove root from each component (now defined and assigned in GOVUKFrontendComponent)
  • Add augments when component requires typing of the root to be set

Why

Follows on from #5334 and fixes #5326

@patrickpatrickpatrick patrickpatrickpatrick changed the base branch from main to public-js-api September 25, 2024 15:39
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-5354 September 25, 2024 15:39 Inactive
Copy link

github-actions bot commented Sep 25, 2024

📋 Stats

File sizes

File Size
dist/govuk-frontend-development.min.css 118.62 KiB
dist/govuk-frontend-development.min.js 42.9 KiB
packages/govuk-frontend/dist/govuk/all.bundle.js 91.13 KiB
packages/govuk-frontend/dist/govuk/all.bundle.mjs 85.59 KiB
packages/govuk-frontend/dist/govuk/all.mjs 1.18 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs 1.55 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend.min.css 118.6 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend.min.js 42.88 KiB
packages/govuk-frontend/dist/govuk/i18n.mjs 5.55 KiB
packages/govuk-frontend/dist/govuk/init.mjs 6.85 KiB

Modules

File Size (bundled) Size (minified)
all.mjs 81.72 KiB 40.34 KiB
accordion.mjs 25.27 KiB 12.9 KiB
button.mjs 7.78 KiB 3.28 KiB
character-count.mjs 24.2 KiB 10.47 KiB
checkboxes.mjs 7.62 KiB 3.39 KiB
error-summary.mjs 9.68 KiB 4.04 KiB
exit-this-page.mjs 18.9 KiB 9.83 KiB
header.mjs 6.28 KiB 3.19 KiB
notification-banner.mjs 8.05 KiB 3.2 KiB
password-input.mjs 16.94 KiB 7.83 KiB
radios.mjs 6.62 KiB 2.95 KiB
service-navigation.mjs 6.26 KiB 3.23 KiB
skip-link.mjs 6.22 KiB 2.73 KiB
tabs.mjs 11.85 KiB 6.64 KiB

View stats and visualisations on the review app


Action run for 14962d4

@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-5354 September 25, 2024 15:41 Inactive
Copy link

github-actions bot commented Sep 25, 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 c5915fe03..780b75b2b 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
@@ -1,44 +1,44 @@
 const version = "development";
 
 function normaliseString(t, e) {
-    const n = t ? t.trim() : "";
-    let i, o = null == e ? void 0 : e.type;
-    switch (o || (["true", "false"].includes(n) && (o = "boolean"), n.length > 0 && isFinite(Number(n)) && (o = "number")), o) {
+    const s = t ? t.trim() : "";
+    let n, i = null == e ? void 0 : e.type;
+    switch (i || (["true", "false"].includes(s) && (i = "boolean"), s.length > 0 && isFinite(Number(s)) && (i = "number")), i) {
         case "boolean":
-            i = "true" === n;
+            n = "true" === s;
             break;
         case "number":
-            i = Number(n);
+            n = Number(s);
             break;
         default:
-            i = t
+            n = t
     }
-    return i
+    return n
 }
 
 function mergeConfigs(...t) {
     const e = {};
-    for (const n of t)
-        for (const t of Object.keys(n)) {
-            const i = e[t],
-                o = n[t];
-            isObject(i) && isObject(o) ? e[t] = mergeConfigs(i, o) : e[t] = o
+    for (const s of t)
+        for (const t of Object.keys(s)) {
+            const n = e[t],
+                i = s[t];
+            isObject(n) && isObject(i) ? e[t] = mergeConfigs(n, i) : e[t] = i
         }
     return e
 }
 
 function extractConfigByNamespace(Component, t, e) {
-    const n = Component.schema.properties[e];
-    if ("object" !== (null == n ? void 0 : n.type)) return;
-    const i = {
+    const s = Component.schema.properties[e];
+    if ("object" !== (null == s ? void 0 : s.type)) return;
+    const n = {
         [e]: {}
     };
-    for (const [o, s] of Object.entries(t)) {
-        let t = i;
-        const n = o.split(".");
-        for (const [i, r] of n.entries()) "object" == typeof t && (i < n.length - 1 ? (isObject(t[r]) || (t[r] = {}), t = t[r]) : o !== e && (t[r] = normaliseString(s)))
+    for (const [i, o] of Object.entries(t)) {
+        let t = n;
+        const s = i.split(".");
+        for (const [n, r] of s.entries()) "object" == typeof t && (n < s.length - 1 ? (isObject(t[r]) || (t[r] = {}), t = t[r]) : i !== e && (t[r] = normaliseString(o)))
     }
-    return i[e]
+    return n[e]
 }
 
 function getFragmentFromUrl(t) {
@@ -54,20 +54,20 @@ function getBreakpoint(t) {
 }
 
 function setFocus(t, e = {}) {
-    var n;
-    const i = t.getAttribute("tabindex");
+    var s;
+    const n = t.getAttribute("tabindex");
 
     function onBlur() {
-        var n;
-        null == (n = e.onBlur) || n.call(t), i || t.removeAttribute("tabindex")
+        var s;
+        null == (s = e.onBlur) || s.call(t), n || t.removeAttribute("tabindex")
     }
-    i || t.setAttribute("tabindex", "-1"), t.addEventListener("focus", (function() {
+    n || t.setAttribute("tabindex", "-1"), t.addEventListener("focus", (function() {
         t.addEventListener("blur", onBlur, {
             once: !0
         })
     }), {
         once: !0
-    }), null == (n = e.onBeforeFocus) || n.call(t), t.focus()
+    }), null == (s = e.onBeforeFocus) || s.call(t), t.focus()
 }
 
 function isSupported(t = document.body) {
@@ -86,7 +86,7 @@ function formatErrorMessage(Component, t) {
 
 function normaliseDataset(Component, t) {
     const e = {};
-    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, t, n));
+    for (const [s, n] of Object.entries(Component.schema.properties)) s in t && (e[s] = normaliseString(t[s], n)), "object" === (null == n ? void 0 : n.type) && (e[s] = extractConfigByNamespace(Component, t, s));
     return e
 }
 class GOVUKFrontendError extends Error {
@@ -110,12 +110,12 @@ class ElementError extends GOVUKFrontendError {
         let e = "string" == typeof t ? t : "";
         if ("object" == typeof t) {
             const {
-                component: n,
-                identifier: i,
-                element: o,
-                expectedType: s
+                component: s,
+                identifier: n,
+                element: i,
+                expectedType: o
             } = t;
-            e = i, e += o ? ` is not of type ${null!=s?s:"HTMLElement"}` : " not found", e = formatErrorMessage(n, e)
+            e = n, e += i ? ` is not of type ${null!=o?o:"HTMLElement"}` : " not found", e = formatErrorMessage(s, e)
         }
         super(e), this.name = "ElementError"
     }
@@ -127,50 +127,58 @@ class InitError extends GOVUKFrontendError {
 }
 class GOVUKFrontendComponent {
     constructor(t) {
+        this.$root = void 0;
         const e = this.constructor;
         if ("string" != typeof e.moduleName) throw new InitError("`moduleName` not defined in component");
-        e.checkSupport(), this.checkInitialised(t);
-        const n = e.moduleName;
-        null == t || t.setAttribute(`data-${n}-init`, "")
-    }
-    checkInitialised(t) {
-        const e = this.constructor,
-            n = e.moduleName;
-        if (t && n && function(t, e) {
+        if (!(t instanceof e.elementType)) throw new ElementError({
+            element: t,
+            component: e,
+            identifier: "Root element (`$root`)",
+            expectedType: e.elementType.name
+        });
+        this.$root = t, e.checkSupport(), this.checkInitialised();
+        const s = e.moduleName;
+        this.$root.setAttribute(`data-${s}-init`, "")
+    }
+    checkInitialised() {
+        const t = this.constructor,
+            e = t.moduleName;
+        if (e && function(t, e) {
                 return t instanceof HTMLElement && t.hasAttribute(`data-${e}-init`)
-            }(t, n)) throw new InitError(e)
+            }(this.$root, e)) throw new InitError(t)
     }
     static checkSupport() {
         if (!isSupported()) throw new SupportError
     }
 }
+GOVUKFrontendComponent.elementType = HTMLElement;
 class I18n {
     constructor(t = {}, e = {}) {
-        var n;
-        this.translations = void 0, this.locale = void 0, this.translations = t, this.locale = null != (n = e.locale) ? n : document.documentElement.lang || "en"
+        var s;
+        this.translations = void 0, this.locale = void 0, this.translations = t, this.locale = null != (s = e.locale) ? s : document.documentElement.lang || "en"
     }
     t(t, e) {
         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 i = n[this.getPluralSuffix(t, e.count)];
-            i && (n = i)
+        let s = this.translations[t];
+        if ("number" == typeof(null == e ? void 0 : e.count) && "object" == typeof s) {
+            const n = s[this.getPluralSuffix(t, e.count)];
+            n && (s = n)
         }
-        if ("string" == typeof n) {
-            if (n.match(/%{(.\S+)}/)) {
+        if ("string" == typeof s) {
+            if (s.match(/%{(.\S+)}/)) {
                 if (!e) throw new Error("i18n: cannot replace placeholders in string if no option data provided");
-                return this.replacePlaceholders(n, e)
+                return this.replacePlaceholders(s, e)
             }
-            return n
+            return s
         }
         return t
     }
     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, 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
+        const s = Intl.NumberFormat.supportedLocalesOf(this.locale).length ? new Intl.NumberFormat(this.locale) : void 0;
+        return t.replace(/%{(.\S+)}/g, (function(t, n) {
+            if (Object.prototype.hasOwnProperty.call(e, n)) {
+                const t = e[n];
+                return !1 === t || "number" != typeof t && "string" != typeof t ? "" : "number" == typeof t ? s ? s.format(t) : `${t}` : t
             }
             throw new Error(`i18n: no data found to replace ${t} placeholder in string`)
         }))
@@ -180,11 +188,11 @@ class I18n {
     }
     getPluralSuffix(t, e) {
         if (e = Number(e), !isFinite(e)) return "other";
-        const n = this.translations[t],
-            i = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(e) : this.selectPluralFormUsingFallbackRules(e);
-        if ("object" == typeof n) {
-            if (i in n) return i;
-            if ("other" in n) return console.warn(`i18n: Missing plural form ".${i}" for "${this.locale}" locale. Falling back to ".other".`), "other"
+        const s = this.translations[t],
+            n = this.hasIntlPluralRulesSupport() ? new Intl.PluralRules(this.locale).select(e) : this.selectPluralFormUsingFallbackRules(e);
+        if ("object" == typeof s) {
+            if (n in s) return n;
+            if ("other" in s) return console.warn(`i18n: Missing plural form ".${n}" for "${this.locale}" locale. Falling back to ".other".`), "other"
         }
         throw new Error(`i18n: Plural form ".other" is required for "${this.locale}" locale`)
     }
@@ -196,8 +204,8 @@ class I18n {
     getPluralRulesForLocale() {
         const t = this.locale.split("-")[0];
         for (const e in I18n.pluralRulesMap) {
-            const n = I18n.pluralRulesMap[e];
-            if (n.includes(this.locale) || n.includes(t)) return e
+            const s = I18n.pluralRulesMap[e];
+            if (s.includes(this.locale) || s.includes(t)) return e
         }
     }
 }
@@ -219,8 +227,8 @@ I18n.pluralRulesMap = {
     irish: t => 1 === t ? "one" : 2 === t ? "two" : t >= 3 && t <= 6 ? "few" : t >= 7 && t <= 10 ? "many" : "other",
     russian(t) {
         const e = t % 100,
-            n = e % 10;
-        return 1 === n && 11 !== e ? "one" : n >= 2 && n <= 4 && !(e >= 12 && e <= 14) ? "few" : 0 === n || n >= 5 && n <= 9 || e >= 11 && e <= 14 ? "many" : "other"
+            s = e % 10;
+        return 1 === s && 11 !== e ? "one" : s >= 2 && s <= 4 && !(e >= 12 && e <= 14) ? "few" : 0 === s || s >= 5 && s <= 9 || e >= 11 && e <= 14 ? "many" : "other"
     },
     scottish: t => 1 === t || 11 === t ? "one" : 2 === t || 12 === t ? "two" : t >= 3 && t <= 10 || t >= 13 && t <= 19 ? "few" : "other",
     spanish: t => 1 === t ? "one" : t % 1e6 == 0 && 0 !== t ? "many" : "other",
@@ -228,18 +236,13 @@ I18n.pluralRulesMap = {
 };
 class Accordion extends GOVUKFrontendComponent {
     constructor(t, e = {}) {
-        if (super(t), this.$root = void 0, this.config = void 0, 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, !(t instanceof HTMLElement)) throw new ElementError({
-            component: Accordion,
-            element: t,
-            identifier: "Root element (`$root`)"
-        });
-        this.$root = t, this.config = mergeConfigs(Accordion.defaults, e, normaliseDataset(Accordion, t.dataset)), this.i18n = new I18n(this.config.i18n);
-        const n = this.$root.querySelectorAll(`.${this.sectionClass}`);
-        if (!n.length) throw new ElementError({
+        super(t), this.config = void 0, 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.config = mergeConfigs(Accordion.defaults, e, normaliseDataset(Accordion, this.$root.dataset)), this.i18n = new I18n(this.config.i18n);
+        const s = this.$root.querySelectorAll(`.${this.sectionClass}`);
+        if (!s.length) throw new ElementError({
             component: Accordion,
             identifier: `Sections (\`<div class="${this.sectionClass}">\`)`
         });
-        this.$sections = n, this.initControls(), this.initSectionHeaders(), this.updateShowAllButton(this.areAllSectionsOpen())
+        this.$sections = s, 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);
@@ -248,53 +251,53 @@ class Accordion extends GOVUKFrontendComponent {
     }
     initSectionHeaders() {
         this.$sections.forEach(((t, e) => {
-            const n = t.querySelector(`.${this.sectionHeaderClass}`);
-            if (!n) throw new ElementError({
+            const s = t.querySelector(`.${this.sectionHeaderClass}`);
+            if (!s) throw new ElementError({
                 component: Accordion,
                 identifier: `Section headers (\`<div class="${this.sectionHeaderClass}">\`)`
             });
-            this.constructHeaderMarkup(n, e), this.setExpanded(this.isExpanded(t), t), n.addEventListener("click", (() => this.onSectionToggle(t))), this.setInitialState(t)
+            this.constructHeaderMarkup(s, e), this.setExpanded(this.isExpanded(t), t), s.addEventListener("click", (() => this.onSectionToggle(t))), this.setInitialState(t)
         }))
     }
     constructHeaderMarkup(t, e) {
-        const n = t.querySelector(`.${this.sectionButtonClass}`),
-            i = t.querySelector(`.${this.sectionHeadingClass}`),
-            o = t.querySelector(`.${this.sectionSummaryClass}`);
-        if (!i) throw new ElementError({
+        const s = t.querySelector(`.${this.sectionButtonClass}`),
+            n = t.querySelector(`.${this.sectionHeadingClass}`),
+            i = t.querySelector(`.${this.sectionSummaryClass}`);
+        if (!n) throw new ElementError({
             component: Accordion,
             identifier: `Section heading (\`.${this.sectionHeadingClass}\`)`
         });
-        if (!n) throw new ElementError({
+        if (!s) throw new ElementError({
             component: Accordion,
             identifier: `Section button placeholder (\`<span class="${this.sectionButtonClass}">\`)`
         });
-        const s = document.createElement("button");
-        s.setAttribute("type", "button"), s.setAttribute("aria-controls", `${this.$root.id}-content-${e+1}`);
-        for (const d of Array.from(n.attributes)) "id" !== d.name && s.setAttribute(d.name, d.value);
+        const o = document.createElement("button");
+        o.setAttribute("type", "button"), o.setAttribute("aria-controls", `${this.$root.id}-content-${e+1}`);
+        for (const d of Array.from(s.attributes)) "id" !== d.name && o.setAttribute(d.name, d.value);
         const r = document.createElement("span");
-        r.classList.add(this.sectionHeadingTextClass), r.id = n.id;
+        r.classList.add(this.sectionHeadingTextClass), r.id = s.id;
         const a = document.createElement("span");
-        a.classList.add(this.sectionHeadingTextFocusClass), r.appendChild(a), Array.from(n.childNodes).forEach((t => a.appendChild(t)));
+        a.classList.add(this.sectionHeadingTextFocusClass), r.appendChild(a), Array.from(s.childNodes).forEach((t => a.appendChild(t)));
         const c = document.createElement("span");
         c.classList.add(this.sectionShowHideToggleClass), c.setAttribute("data-nosnippet", "");
         const l = document.createElement("span");
         l.classList.add(this.sectionShowHideToggleFocusClass), c.appendChild(l);
         const h = document.createElement("span"),
             u = document.createElement("span");
-        if (u.classList.add(this.upChevronIconClass), l.appendChild(u), h.classList.add(this.sectionShowHideTextClass), l.appendChild(h), s.appendChild(r), s.appendChild(this.getButtonPunctuationEl()), o) {
+        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(o.attributes)) t.setAttribute(n.name, n.value);
-            Array.from(o.childNodes).forEach((t => e.appendChild(t))), o.remove(), s.appendChild(t), s.appendChild(this.getButtonPunctuationEl())
+            for (const s of Array.from(i.attributes)) t.setAttribute(s.name, s.value);
+            Array.from(i.childNodes).forEach((t => e.appendChild(t))), i.remove(), o.appendChild(t), o.appendChild(this.getButtonPunctuationEl())
         }
-        s.appendChild(c), i.removeChild(n), i.appendChild(s)
+        o.appendChild(c), n.removeChild(s), n.appendChild(o)
     }
     onBeforeMatch(t) {
         const e = t.target;
         if (!(e instanceof Element)) return;
-        const n = e.closest(`.${this.sectionClass}`);
-        n && this.setExpanded(!0, n)
+        const s = e.closest(`.${this.sectionClass}`);
+        s && this.setExpanded(!0, s)
     }
     onSectionToggle(t) {
         const e = !this.isExpanded(t);
@@ -307,24 +310,24 @@ class Accordion extends GOVUKFrontendComponent {
         })), this.updateShowAllButton(t)
     }
     setExpanded(t, e) {
-        const n = e.querySelector(`.${this.upChevronIconClass}`),
-            i = e.querySelector(`.${this.sectionShowHideTextClass}`),
-            o = e.querySelector(`.${this.sectionButtonClass}`),
-            s = e.querySelector(`.${this.sectionContentClass}`);
-        if (!s) throw new ElementError({
+        const s = e.querySelector(`.${this.upChevronIconClass}`),
+            n = e.querySelector(`.${this.sectionShowHideTextClass}`),
+            i = e.querySelector(`.${this.sectionButtonClass}`),
+            o = e.querySelector(`.${this.sectionContentClass}`);
+        if (!o) throw new ElementError({
             component: Accordion,
             identifier: `Section content (\`<div class="${this.sectionContentClass}">\`)`
         });
-        if (!n || !i || !o) return;
+        if (!s || !n || !i) return;
         const r = t ? this.i18n.t("hideSection") : this.i18n.t("showSection");
-        i.textContent = r, o.setAttribute("aria-expanded", `${t}`);
+        n.textContent = r, i.setAttribute("aria-expanded", `${t}`);
         const a = [],
             c = e.querySelector(`.${this.sectionHeadingTextClass}`);
         c && a.push(`${c.textContent}`.trim());
         const l = e.querySelector(`.${this.sectionSummaryClass}`);
         l && a.push(`${l.textContent}`.trim());
         const h = t ? this.i18n.t("hideSectionAriaLabel") : this.i18n.t("showSectionAriaLabel");
-        a.push(h), o.setAttribute("aria-label", a.join(" , ")), t ? (s.removeAttribute("hidden"), e.classList.add(this.sectionExpandedClass), n.classList.remove(this.downChevronIconClass)) : (s.setAttribute("hidden", "until-found"), e.classList.remove(this.sectionExpandedClass), n.classList.add(this.downChevronIconClass)), this.updateShowAllButton(this.areAllSectionsOpen())
+        a.push(h), i.setAttribute("aria-label", a.join(" , ")), t ? (o.removeAttribute("hidden"), e.classList.add(this.sectionExpandedClass), s.classList.remove(this.downChevronIconClass)) : (o.setAttribute("hidden", "until-found"), e.classList.remove(this.sectionExpandedClass), s.classList.add(this.downChevronIconClass)), this.updateShowAllButton(this.areAllSectionsOpen())
     }
     isExpanded(t) {
         return t.classList.contains(this.sectionExpandedClass)
@@ -341,18 +344,18 @@ class Accordion extends GOVUKFrontendComponent {
     }
     storeState(t, e) {
         if (!this.config.rememberExpanded) return;
-        const n = this.getIdentifier(t);
-        if (n) try {
-            window.sessionStorage.setItem(n, e.toString())
-        } catch (i) {}
+        const s = this.getIdentifier(t);
+        if (s) try {
+            window.sessionStorage.setItem(s, e.toString())
+        } catch (n) {}
     }
     setInitialState(t) {
         if (!this.config.rememberExpanded) return;
         const e = this.getIdentifier(t);
         if (e) try {
-            const n = window.sessionStorage.getItem(e);
-            null !== n && this.setExpanded("true" === n, t)
-        } catch (n) {}
+            const s = window.sessionStorage.getItem(e);
+            null !== s && this.setExpanded("true" === s, t)
+        } catch (s) {}
     }
     getButtonPunctuationEl() {
         const t = document.createElement("span");
@@ -381,12 +384,7 @@ Accordion.moduleName = "govuk-accordion", Accordion.defaults = Object.freeze({
 });
 class Button extends GOVUKFrontendComponent {
     constructor(t, e = {}) {
-        if (super(t), this.$root = void 0, this.config = void 0, this.debounceFormSubmitTimer = null, !(t instanceof HTMLElement)) throw new ElementError({
-            component: Button,
-            element: t,
-            identifier: "Root element (`$root`)"
-        });
-        this.$root = t, this.config = mergeConfigs(Button.defaults, e, normaliseDataset(Button, t.dataset)), this.$root.addEventListener("keydown", (t => this.handleKeyDown(t))), this.$root.addEventListener("click", (t => this.debounce(t)))
+        super(t), this.config = void 0, this.debounceFormSubmitTimer = null, this.config = mergeConfigs(Button.defaults, e, normaliseDataset(Button, this.$root.dataset)), this.$root.addEventListener("keydown", (t => this.handleKeyDown(t))), this.$root.addEventListener("click", (t => this.debounce(t)))
     }
     handleKeyDown(t) {
         const e = t.target;
@@ -400,8 +398,8 @@ class Button extends GOVUKFrontendComponent {
 }
 
 function closestAttributeValue(t, e) {
-    const n = t.closest(`[${e}]`);
-    return n ? n.getAttribute(e) : null
+    const s = t.closest(`[${e}]`);
+    return s ? s.getAttribute(e) : null
 }
 Button.moduleName = "govuk-button", Button.defaults = Object.freeze({
     preventDoubleClick: !1
@@ -414,44 +412,40 @@ Button.moduleName = "govuk-button", Button.defaults = Object.freeze({
 });
 class CharacterCount extends GOVUKFrontendComponent {
     constructor(t, e = {}) {
-        var n, i;
-        if (super(t), this.$root = void 0, this.$textarea = void 0, this.$visibleCountMessage = void 0, this.$screenReaderCountMessage = void 0, this.lastInputTimestamp = null, this.lastInputValue = "", this.valueChecker = null, this.config = void 0, this.i18n = void 0, this.maxLength = void 0, !(t instanceof HTMLElement)) throw new ElementError({
-            component: CharacterCount,
-            element: t,
-            identifier: "Root element (`$root`)"
-        });
-        const o = t.querySelector(".govuk-js-character-count");
-        if (!(o instanceof HTMLTextAreaElement || o instanceof HTMLInputElement)) throw new ElementError({
+        var s, n;
+        super(t), this.$textarea = void 0, this.$visibleCountMessage = void 0, this.$screenReaderCountMessage = void 0, this.lastInputTimestamp = null, this.lastInputValue = "", this.valueChecker = null, this.config = void 0, 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({
             component: CharacterCount,
-            element: o,
+            element: i,
             expectedType: "HTMLTextareaElement or HTMLInputElement",
             identifier: "Form field (`.govuk-js-character-count`)"
         });
-        const s = normaliseDataset(CharacterCount, t.dataset);
+        const o = normaliseDataset(CharacterCount, this.$root.dataset);
         let r = {};
-        ("maxwords" in s || "maxlength" in s) && (r = {
+        ("maxwords" in o || "maxlength" in o) && (r = {
             maxlength: void 0,
             maxwords: void 0
-        }), this.config = mergeConfigs(CharacterCount.defaults, e, r, s);
+        }), this.config = mergeConfigs(CharacterCount.defaults, e, r, o);
         const a = function(t, e) {
-            const n = [];
-            for (const [i, o] of Object.entries(t)) {
+            const s = [];
+            for (const [n, i] of Object.entries(t)) {
                 const t = [];
-                if (Array.isArray(o)) {
+                if (Array.isArray(i)) {
                     for (const {
-                            required: n,
-                            errorMessage: i
+                            required: s,
+                            errorMessage: n
                         }
-                        of o) n.every((t => !!e[t])) || t.push(i);
-                    "anyOf" !== i || o.length - t.length >= 1 || n.push(...t)
+                        of i) s.every((t => !!e[t])) || t.push(n);
+                    "anyOf" !== n || i.length - t.length >= 1 || s.push(...t)
                 }
             }
-            return n
+            return s
         }(CharacterCount.schema, this.config);
         if (a[0]) throw new ConfigError(formatErrorMessage(CharacterCount, a[0]));
         this.i18n = new I18n(this.config.i18n, {
-            locale: closestAttributeValue(t, "lang")
-        }), this.maxLength = null != (n = null != (i = this.config.maxwords) ? i : this.config.maxlength) ? n : 1 / 0, this.$root = t, this.$textarea = o;
+            locale: closestAttributeValue(this.$root, "lang")
+        }), this.maxLength = null != (s = null != (n = this.config.maxwords) ? n : this.config.maxlength) ? s : 1 / 0, this.$textarea = i;
         const c = `${this.$textarea.id}-info`,
             l = document.getElementById(c);
         if (!l) throw new ElementError({
@@ -508,8 +502,8 @@ class CharacterCount extends GOVUKFrontendComponent {
     }
     formatCountMessage(t, e) {
         if (0 === t) return this.i18n.t(`${e}AtLimit`);
-        const n = t < 0 ? "OverLimit" : "UnderLimit";
-        return this.i18n.t(`${e}${n}`, {
+        const s = t < 0 ? "OverLimit" : "UnderLimit";
+        return this.i18n.t(`${e}${s}`, {
             count: Math.abs(t)
         })
     }
@@ -569,17 +563,13 @@ CharacterCount.moduleName = "govuk-character-count", CharacterCount.defaults = O
 });
 class Checkboxes extends GOVUKFrontendComponent {
     constructor(t) {
-        if (super(t), this.$root = void 0, this.$inputs = void 0, !(t instanceof HTMLElement)) throw new ElementError({
-            component: Checkboxes,
-            element: t,
-            identifier: "Root element (`$root`)"
-        });
-        const e = t.querySelectorAll('input[type="checkbox"]');
+        super(t), this.$inputs = void 0;
+        const e = this.$root.querySelectorAll('input[type="checkbox"]');
         if (!e.length) throw new ElementError({
             component: Checkboxes,
             identifier: 'Form inputs (`<input type="checkbox">`)'
         });
-        this.$root = t, this.$inputs = e, this.$inputs.forEach((t => {
+        this.$inputs = e, this.$inputs.forEach((t => {
             const e = t.getAttribute("data-aria-controls");
             if (e) {
                 if (!document.getElementById(e)) throw new ElementError({
@@ -596,10 +586,10 @@ class Checkboxes extends GOVUKFrontendComponent {
     syncConditionalRevealWithInputState(t) {
         const e = t.getAttribute("aria-controls");
         if (!e) return;
-        const n = document.getElementById(e);
-        if (null != n && n.classList.contains("govuk-checkboxes__conditional")) {
+        const s = document.getElementById(e);
+        if (null != s && s.classList.contains("govuk-checkboxes__conditional")) {
             const e = t.checked;
-            t.setAttribute("aria-expanded", e.toString()), n.classList.toggle("govuk-checkboxes__conditional--hidden", !e)
+            t.setAttribute("aria-expanded", e.toString()), s.classList.toggle("govuk-checkboxes__conditional--hidden", !e)
         }
     }
     unCheckAllInputsExcept(t) {
@@ -622,12 +612,7 @@ class Checkboxes extends GOVUKFrontendComponent {
 Checkboxes.moduleName = "govuk-checkboxes";
 class ErrorSummary extends GOVUKFrontendComponent {
     constructor(t, e = {}) {
-        if (super(t), this.$root = void 0, this.config = void 0, !(t instanceof HTMLElement)) throw new ElementError({
-            component: ErrorSummary,
-            element: t,
-            identifier: "Root element (`$root`)"
-        });
-        this.$root = t, this.config = mergeConfigs(ErrorSummary.defaults, e, normaliseDataset(ErrorSummary, t.dataset)), this.config.disableAutoFocus || setFocus(this.$root), this.$root.addEventListener("click", (t => this.handleClick(t)))
+        super(t), this.config = void 0, this.config = mergeConfigs(ErrorSummary.defaults, e, normaliseDataset(ErrorSummary, this.$root.dataset)), this.config.disableAutoFocus || setFocus(this.$root), this.$root.addEventListener("click", (t => this.handleClick(t)))
     }
     handleClick(t) {
         const e = t.target;
@@ -637,25 +622,25 @@ class ErrorSummary extends GOVUKFrontendComponent {
         if (!(t instanceof HTMLAnchorElement)) return !1;
         const e = getFragmentFromUrl(t.href);
         if (!e) return !1;
-        const n = document.getElementById(e);
-        if (!n) return !1;
-        const i = this.getAssociatedLegendOrLabel(n);
-        return !!i && (i.scrollIntoView(), n.focus({
+        const s = document.getElementById(e);
+        if (!s) return !1;
+        const n = this.getAssociatedLegendOrLabel(s);
+        return !!n && (n.scrollIntoView(), s.focus({
             preventScroll: !0
         }), !0)
     }
     getAssociatedLegendOrLabel(t) {
         var e;
-        const n = t.closest("fieldset");
-        if (n) {
-            const e = n.getElementsByTagName("legend");
+        const s = t.closest("fieldset");
+        if (s) {
+            const e = s.getElementsByTagName("legend");
             if (e.length) {
-                const n = e[0];
-                if (t instanceof HTMLInputElement && ("checkbox" === t.type || "radio" === t.type)) return n;
-                const i = n.getBoundingClientRect().top,
-                    o = t.getBoundingClientRect();
-                if (o.height && window.innerHeight) {
-                    if (o.top + o.height - i < window.innerHeight / 2) return n
+                const s = e[0];
+                if (t instanceof HTMLInputElement && ("checkbox" === t.type || "radio" === t.type)) return s;
+                const n = s.getBoundingClientRect().top,
+                    i = t.getBoundingClientRect();
+                if (i.height && window.innerHeight) {
+                    if (i.top + i.height - n < window.innerHeight / 2) return s
                 }
             }
         }
@@ -673,21 +658,17 @@ ErrorSummary.moduleName = "govuk-error-summary", ErrorSummary.defaults = Object.
 });
 class ExitThisPage extends GOVUKFrontendComponent {
     constructor(t, e = {}) {
-        if (super(t), this.$root = void 0, this.config = void 0, this.i18n = void 0, this.$button = void 0, this.$skiplinkButton = null, this.$updateSpan = null, this.$indicatorContainer = null, this.$overlay = null, this.keypressCounter = 0, this.lastKeyWasModified = !1, this.timeoutTime = 5e3, this.keypressTimeoutId = null, this.timeoutMessageId = null, !(t instanceof HTMLElement)) throw new ElementError({
-            component: ExitThisPage,
-            element: t,
-            identifier: "Root element (`$root`)"
-        });
-        const n = t.querySelector(".govuk-exit-this-page__button");
-        if (!(n instanceof HTMLAnchorElement)) throw new ElementError({
+        super(t), this.config = void 0, this.i18n = void 0, this.$button = void 0, this.$skiplinkButton = null, this.$updateSpan = null, this.$indicatorContainer = null, this.$overlay = null, this.keypressCounter = 0, this.lastKeyWasModified = !1, this.timeoutTime = 5e3, this.keypressTimeoutId = null, this.timeoutMessageId = null;
+        const s = this.$root.querySelector(".govuk-exit-this-page__button");
+        if (!(s instanceof HTMLAnchorElement)) throw new ElementError({
             component: ExitThisPage,
-            element: n,
+            element: s,
             expectedType: "HTMLAnchorElement",
             identifier: "Button (`.govuk-exit-this-page__button`)"
         });
-        this.config = mergeConfigs(ExitThisPage.defaults, e, normaliseDataset(ExitThisPage, t.dataset)), this.i18n = new I18n(this.config.i18n), this.$root = t, this.$button = n;
-        const i = document.querySelector(".govuk-js-exit-this-page-skiplink");
-        i instanceof HTMLAnchorElement && (this.$skiplinkButton = i), this.buildIndicator(), this.initUpdateSpan(), this.initButtonClickHandler(), "govukFrontendExitThisPageKeypress" in document.body.dataset || (document.addEventListener("keyup", this.handleKeypress.bind(this), !0), document.body.dataset.govukFrontendExitThisPageKeypress = "true"), window.addEventListener("pageshow", this.resetPage.bind(this))
+        this.config = mergeConfigs(ExitThisPage.defaults, e, normaliseDataset(ExitThisPage, this.$root.dataset)), this.i18n = new I18n(this.config.i18n), this.$button = s;
+        const n = document.querySelector(".govuk-js-exit-this-page-skiplink");
+        n instanceof HTMLAnchorElement && (this.$skiplinkButton = n), this.buildIndicator(), this.initUpdateSpan(), this.initButtonClickHandler(), "govukFrontendExitThisPageKeypress" in document.body.dataset || (document.addEventListener("keyup", this.handleKeypress.bind(this), !0), document.body.dataset.govukFrontendExitThisPageKeypress = "true"), window.addEventListener("pageshow", this.resetPage.bind(this))
     }
     initUpdateSpan() {
         this.$updateSpan = document.createElement("span"), this.$updateSpan.setAttribute("role", "status"), this.$updateSpan.className = "govuk-visually-hidden", this.$root.appendChild(this.$updateSpan)
@@ -750,26 +731,21 @@ ExitThisPage.moduleName = "govuk-exit-this-page", ExitThisPage.defaults = Object
 });
 class Header extends GOVUKFrontendComponent {
     constructor(t) {
-        if (super(t), this.$root = void 0, this.$menuButton = void 0, this.$menu = void 0, this.menuIsOpen = !1, this.mql = null, !t) throw new ElementError({
-            component: Header,
-            element: t,
-            identifier: "Root element (`$root`)"
-        });
-        this.$root = t;
-        const e = t.querySelector(".govuk-js-header-toggle");
+        super(t), this.$menuButton = void 0, this.$menu = void 0, this.menuIsOpen = !1, this.mql = null;
+        const e = this.$root.querySelector(".govuk-js-header-toggle");
         if (!e) return this;
-        const n = e.getAttribute("aria-controls");
-        if (!n) throw new ElementError({
+        const s = e.getAttribute("aria-controls");
+        if (!s) throw new ElementError({
             component: Header,
             identifier: 'Navigation button (`<button class="govuk-js-header-toggle">`) attribute (`aria-controls`)'
         });
-        const i = document.getElementById(n);
-        if (!i) throw new ElementError({
+        const n = document.getElementById(s);
+        if (!n) throw new ElementError({
             component: Header,
-            element: i,
-            identifier: `Navigation (\`<ul id="${n}">\`)`
+            element: n,
+            identifier: `Navigation (\`<ul id="${s}">\`)`
         });
-        this.$menu = i, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
+        this.$menu = n, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
     }
     setupResponsiveChecks() {
         const t = getBreakpoint("desktop");
@@ -789,12 +765,7 @@ class Header extends GOVUKFrontendComponent {
 Header.moduleName = "govuk-header";
 class NotificationBanner extends GOVUKFrontendComponent {
     constructor(t, e = {}) {
-        if (super(t), this.$root = void 0, this.config = void 0, !(t instanceof HTMLElement)) throw new ElementError({
-            component: NotificationBanner,
-            element: t,
-            identifier: "Root element (`$root`)"
-        });
-        this.$root = t, this.config = mergeConfigs(NotificationBanner.defaults, e, normaliseDataset(NotificationBanner, t.dataset)), "alert" !== this.$root.getAttribute("role") || this.config.disableAutoFocus || setFocus(this.$root)
+        super(t), this.config = void 0, this.config = mergeConfigs(NotificationBanner.defaults, e, normaliseDataset(NotificationBanner, this.$root.dataset)), "alert" !== this.$root.getAttribute("role") || this.config.disableAutoFocus || setFocus(this.$root)
     }
 }
 NotificationBanner.moduleName = "govuk-notification-banner", NotificationBanner.defaults = Object.freeze({
@@ -808,32 +779,28 @@ NotificationBanner.moduleName = "govuk-notification-banner", NotificationBanner.
 });
 class PasswordInput extends GOVUKFrontendComponent {
     constructor(t, e = {}) {
-        if (super(), this.$root = void 0, this.config = void 0, this.i18n = void 0, this.$input = void 0, this.$showHideButton = void 0, this.$screenReaderStatusMessage = void 0, !(t instanceof HTMLElement)) throw new ElementError({
-            component: PasswordInput,
-            element: t,
-            identifier: "Root element (`$root`)"
-        });
-        const n = t.querySelector(".govuk-js-password-input-input");
-        if (!(n instanceof HTMLInputElement)) throw new ElementError({
+        super(t), this.config = void 0, this.i18n = void 0, this.$input = void 0, this.$showHideButton = void 0, this.$screenReaderStatusMessage = void 0;
+        const s = this.$root.querySelector(".govuk-js-password-input-input");
+        if (!(s instanceof HTMLInputElement)) throw new ElementError({
             component: PasswordInput,
-            element: n,
+            element: s,
             expectedType: "HTMLInputElement",
             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 i = t.querySelector(".govuk-js-password-input-toggle");
-        if (!(i instanceof HTMLButtonElement)) throw new ElementError({
+        if ("password" !== s.type) throw new ElementError("Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.");
+        const n = this.$root.querySelector(".govuk-js-password-input-toggle");
+        if (!(n instanceof HTMLButtonElement)) throw new ElementError({
             component: PasswordInput,
-            element: i,
+            element: n,
             expectedType: "HTMLButtonElement",
             identifier: "Button (`.govuk-js-password-input-toggle`)"
         });
-        if ("button" !== i.type) throw new ElementError("Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.");
-        this.$root = t, this.$input = n, this.$showHideButton = i, this.config = mergeConfigs(PasswordInput.defaults, e, normaliseDataset(PasswordInput, t.dataset)), this.i18n = new I18n(this.config.i18n, {
-            locale: closestAttributeValue(t, "lang")
+        if ("button" !== n.type) throw new ElementError("Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.");
+        this.$input = s, this.$showHideButton = n, this.config = mergeConfigs(PasswordInput.defaults, e, normaliseDataset(PasswordInput, this.$root.dataset)), this.i18n = new I18n(this.config.i18n, {
+            locale: closestAttributeValue(this.$root, "lang")
         }), this.$showHideButton.removeAttribute("hidden");
-        const o = document.createElement("div");
-        o.className = "govuk-password-input__sr-status govuk-visually-hidden", o.setAttribute("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 => {
+        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 => {
             t.persisted && "password" !== this.$input.type && this.hide()
         })), this.hide()
     }
@@ -850,9 +817,9 @@ class PasswordInput extends GOVUKFrontendComponent {
         if (t === this.$input.type) return;
         this.$input.setAttribute("type", t);
         const e = "password" === t,
-            n = e ? "show" : "hide",
-            i = e ? "passwordHidden" : "passwordShown";
-        this.$showHideButton.innerText = this.i18n.t(`${n}Password`), this.$showHideButton.setAttribute("aria-label", this.i18n.t(`${n}PasswordAriaLabel`)), this.$screenReaderStatusMessage.innerText = this.i18n.t(`${i}Announcement`)
+            s = e ? "show" : "hide",
+            n = e ? "passwordHidden" : "passwordShown";
+        this.$showHideButton.innerText = this.i18n.t(`${s}Password`), this.$showHideButton.setAttribute("aria-label", this.i18n.t(`${s}PasswordAriaLabel`)), this.$screenReaderStatusMessage.innerText = this.i18n.t(`${n}Announcement`)
     }
 }
 PasswordInput.moduleName = "govuk-password-input", PasswordInput.defaults = Object.freeze({
@@ -873,17 +840,13 @@ PasswordInput.moduleName = "govuk-password-input", PasswordInput.defaults = Obje
 });
 class Radios extends GOVUKFrontendComponent {
     constructor(t) {
-        if (super(t), this.$root = void 0, this.$inputs = void 0, !(t instanceof HTMLElement)) throw new ElementError({
-            component: Radios,
-            element: t,
-            identifier: "Root element (`$root`)"
-        });
-        const e = t.querySelectorAll('input[type="radio"]');
+        super(t), this.$inputs = void 0;
+        const e = this.$root.querySelectorAll('input[type="radio"]');
         if (!e.length) throw new ElementError({
             component: Radios,
             identifier: 'Form inputs (`<input type="radio">`)'
         });
-        this.$root = t, this.$inputs = e, this.$inputs.forEach((t => {
+        this.$inputs = e, this.$inputs.forEach((t => {
             const e = t.getAttribute("data-aria-controls");
             if (e) {
                 if (!document.getElementById(e)) throw new ElementError({
@@ -900,47 +863,42 @@ class Radios extends GOVUKFrontendComponent {
     syncConditionalRevealWithInputState(t) {
         const e = t.getAttribute("aria-controls");
         if (!e) return;
-        const n = document.getElementById(e);
-        if (null != n && n.classList.contains("govuk-radios__conditional")) {
+        const s = document.getElementById(e);
+        if (null != s && s.classList.contains("govuk-radios__conditional")) {
             const e = t.checked;
-            t.setAttribute("aria-expanded", e.toString()), n.classList.toggle("govuk-radios__conditional--hidden", !e)
+            t.setAttribute("aria-expanded", e.toString()), s.classList.toggle("govuk-radios__conditional--hidden", !e)
         }
     }
     handleClick(t) {
         const e = t.target;
         if (!(e instanceof HTMLInputElement) || "radio" !== e.type) return;
-        const n = document.querySelectorAll('input[type="radio"][aria-controls]'),
-            i = e.form,
-            o = e.name;
-        n.forEach((t => {
-            const e = t.form === i;
-            t.name === o && e && this.syncConditionalRevealWithInputState(t)
+        const s = document.querySelectorAll('input[type="radio"][aria-controls]'),
+            n = e.form,
+            i = e.name;
+        s.forEach((t => {
+            const e = t.form === n;
+            t.name === i && e && this.syncConditionalRevealWithInputState(t)
         }))
     }
 }
 Radios.moduleName = "govuk-radios";
 class ServiceNavigation extends GOVUKFrontendComponent {
     constructor(t) {
-        if (super(), this.$root = void 0, this.$menuButton = void 0, this.$menu = void 0, this.menuIsOpen = !1, this.mql = null, !t) throw new ElementError({
-            component: ServiceNavigation,
-            element: t,
-            identifier: "Root element (`$root`)"
-        });
-        this.$root = t;
-        const e = t.querySelector(".govuk-js-service-navigation-toggle");
+        super(t), this.$menuButton = void 0, this.$menu = void 0, this.menuIsOpen = !1, this.mql = null;
+        const e = this.$root.querySelector(".govuk-js-service-navigation-toggle");
         if (!e) return this;
-        const n = e.getAttribute("aria-controls");
-        if (!n) throw new ElementError({
+        const s = e.getAttribute("aria-controls");
+        if (!s) throw new ElementError({
             component: ServiceNavigation,
             identifier: 'Navigation button (`<button class="govuk-js-service-navigation-toggle">`) attribute (`aria-controls`)'
         });
-        const i = document.getElementById(n);
-        if (!i) throw new ElementError({
+        const n = document.getElementById(s);
+        if (!n) throw new ElementError({
             component: ServiceNavigation,
-            element: i,
-            identifier: `Navigation (\`<ul id="${n}">\`)`
+            element: n,
+            identifier: `Navigation (\`<ul id="${s}">\`)`
         });
-        this.$menu = i, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
+        this.$menu = n, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
     }
     setupResponsiveChecks() {
         const t = getBreakpoint("tablet");
@@ -961,29 +919,23 @@ ServiceNavigation.moduleName = "govuk-service-navigation";
 class SkipLink extends GOVUKFrontendComponent {
     constructor(t) {
         var e;
-        if (super(t), this.$root = void 0, !(t instanceof HTMLAnchorElement)) throw new ElementError({
-            component: SkipLink,
-            element: t,
-            expectedType: "HTMLAnchorElement",
-            identifier: "Root element (`$root`)"
-        });
-        this.$root = t;
-        const n = this.$root.hash,
-            i = null != (e = this.$root.getAttribute("href")) ? e : "";
-        let o;
+        super(t);
+        const s = this.$root.hash,
+            n = null != (e = this.$root.getAttribute("href")) ? e : "";
+        let i;
         try {
-            o = new window.URL(this.$root.href)
+            i = new window.URL(this.$root.href)
         } catch (a) {
-            throw new ElementError(`Skip link: Target link (\`href="${i}"\`) is invalid`)
+            throw new ElementError(`Skip link: Target link (\`href="${n}"\`) is invalid`)
         }
-        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 (i.origin !== window.location.origin || i.pathname !== window.location.pathname) return;
+        const o = getFragmentFromUrl(s);
+        if (!o) throw new ElementError(`Skip link: Target link (\`href="${n}"\`) has no hash fragment`);
+        const r = document.getElementById(o);
         if (!r) throw new ElementError({
             component: SkipLink,
             element: r,
-            identifier: `Target content (\`id="${s}"\`)`
+            identifier: `Target content (\`id="${o}"\`)`
         });
         this.$root.addEventListener("click", (() => setFocus(r, {
             onBeforeFocus() {
@@ -995,31 +947,27 @@ class SkipLink extends GOVUKFrontendComponent {
         })))
     }
 }
-SkipLink.moduleName = "govuk-skip-link";
+SkipLink.elementType = HTMLAnchorElement, SkipLink.moduleName = "govuk-skip-link";
 class Tabs extends GOVUKFrontendComponent {
     constructor(t) {
-        if (super(t), this.$root = void 0, this.$tabs = void 0, this.$tabList = void 0, this.$tabListItems = void 0, this.jsHiddenClass = "govuk-tabs__panel--hidden", this.changingHash = !1, this.boundTabClick = void 0, this.boundTabKeydown = void 0, this.boundOnHashChange = void 0, this.mql = null, !t) throw new ElementError({
-            component: Tabs,
-            element: t,
-            identifier: "Root element (`$root`)"
-        });
-        const e = t.querySelectorAll("a.govuk-tabs__tab");
+        super(t), this.$tabs = void 0, this.$tabList = void 0, this.$tabListItems = void 0, this.jsHiddenClass = "govuk-tabs__panel--hidden", this.changingHash = !1, this.boundTabClick = void 0, this.boundTabKeydown = void 0, this.boundOnHashChange = void 0, this.mql = null;
+        const e = this.$root.querySelectorAll("a.govuk-tabs__tab");
         if (!e.length) throw new ElementError({
             component: Tabs,
             identifier: 'Links (`<a class="govuk-tabs__tab">`)'
         });
-        this.$root = t, 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"),
-            i = this.$root.querySelectorAll("li.govuk-tabs__list-item");
-        if (!n) throw new ElementError({
+        this.$tabs = e, this.boundTabClick = this.onTabClick.bind(this), this.boundTabKeydown = this.onTabKeydown.bind(this), this.boundOnHashChange = this.onHashChange.bind(this);
+        const s = this.$root.querySelector(".govuk-tabs__list"),
+            n = this.$root.querySelectorAll("li.govuk-tabs__list-item");
+        if (!s) throw new ElementError({
             component: Tabs,
             identifier: 'List (`<ul class="govuk-tabs__list">`)'
         });
-        if (!i.length) throw new ElementError({
+        if (!n.length) throw new ElementError({
             component: Tabs,
             identifier: 'List items (`<li class="govuk-tabs__list-item">`)'
         });
-        this.$tabList = n, this.$tabListItems = i, this.setupResponsiveChecks()
+        this.$tabList = s, this.$tabListItems = n, this.setupResponsiveChecks()
     }
     setupResponsiveChecks() {
         const t = getBreakpoint("tablet");
@@ -1055,8 +1003,8 @@ class Tabs extends GOVUKFrontendComponent {
             e = this.getTab(t);
         if (!e) return;
         if (this.changingHash) return void(this.changingHash = !1);
-        const n = this.getCurrentTab();
-        n && (this.hideTab(n), this.showTab(e), e.focus())
+        const s = this.getCurrentTab();
+        s && (this.hideTab(s), this.showTab(e), e.focus())
     }
     hideTab(t) {
         this.unhighlightTab(t), this.hidePanel(t)
@@ -1071,8 +1019,8 @@ class Tabs extends GOVUKFrontendComponent {
         const e = getFragmentFromUrl(t.href);
         if (!e) return;
         t.setAttribute("id", `tab_${e}`), t.setAttribute("role", "tab"), t.setAttribute("aria-controls", e), t.setAttribute("aria-selected", "false"), t.setAttribute("tabindex", "-1");
-        const n = this.getPanel(t);
-        n && (n.setAttribute("role", "tabpanel"), n.setAttribute("aria-labelledby", t.id), n.classList.add(this.jsHiddenClass))
+        const s = this.getPanel(t);
+        s && (s.setAttribute("role", "tabpanel"), s.setAttribute("aria-labelledby", t.id), s.classList.add(this.jsHiddenClass))
     }
     unsetAttributes(t) {
         t.removeAttribute("id"), t.removeAttribute("role"), t.removeAttribute("aria-controls"), t.removeAttribute("aria-selected"), t.removeAttribute("tabindex");
@@ -1081,14 +1029,14 @@ class Tabs extends GOVUKFrontendComponent {
     }
     onTabClick(t) {
         const e = this.getCurrentTab(),
-            n = t.currentTarget;
-        e && n instanceof HTMLAnchorElement && (t.preventDefault(), this.hideTab(e), this.showTab(n), this.createHistoryEntry(n))
+            s = t.currentTarget;
+        e && s instanceof HTMLAnchorElement && (t.preventDefault(), this.hideTab(e), this.showTab(s), this.createHistoryEntry(s))
     }
     createHistoryEntry(t) {
         const e = this.getPanel(t);
         if (!e) return;
-        const n = e.id;
-        e.id = "", this.changingHash = !0, window.location.hash = n, e.id = n
+        const s = e.id;
+        e.id = "", this.changingHash = !0, window.location.hash = s, e.id = s
     }
     onTabKeydown(t) {
         switch (t.key) {
@@ -1106,16 +1054,16 @@ class Tabs extends GOVUKFrontendComponent {
         if (null == t || !t.parentElement) return;
         const e = t.parentElement.nextElementSibling;
         if (!e) return;
-        const n = e.querySelector("a.govuk-tabs__tab");
-        n && (this.hideTab(t), this.showTab(n), n.focus(), this.createHistoryEntry(n))
+        const s = e.querySelector("a.govuk-tabs__tab");
+        s && (this.hideTab(t), this.showTab(s), s.focus(), this.createHistoryEntry(s))
     }
     activatePreviousTab() {
         const t = this.getCurrentTab();
         if (null == t || !t.parentElement) return;
         const e = t.parentElement.previousElementSibling;
         if (!e) return;
-        const n = e.querySelector("a.govuk-tabs__tab");
-        n && (this.hideTab(t), this.showTab(n), n.focus(), this.createHistoryEntry(n))
+        const s = e.querySelector("a.govuk-tabs__tab");
+        s && (this.hideTab(t), this.showTab(s), s.focus(), this.createHistoryEntry(s))
     }
     getPanel(t) {
         const e = getFragmentFromUrl(t.href);
@@ -1145,7 +1093,7 @@ function initAll(t) {
     if (t = void 0 !== t ? t : {}, !isSupported()) return void(t.onError ? t.onError(new SupportError, {
         config: t
     }) : console.log(new SupportError));
-    const n = [
+    const s = [
             [Accordion, t.accordion],
             [Button, t.button],
             [CharacterCount, t.characterCount],
@@ -1160,32 +1108,32 @@ function initAll(t) {
             [SkipLink],
             [Tabs]
         ],
-        i = {
+        n = {
             scope: null != (e = t.scope) ? e : document,
             onError: t.onError
         };
-    n.forEach((([Component, t]) => {
-        createAll(Component, t, i)
+    s.forEach((([Component, t]) => {
+        createAll(Component, t, n)
     }))
 }
 
 function createAll(Component, t, 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 => {
+    let s, n = document;
+    var i;
+    "object" == typeof e && (n = null != (i = e.scope) ? i : n, s = e.onError);
+    "function" == typeof e && (s = e), e instanceof HTMLElement && (n = e);
+    const o = n.querySelectorAll(`[data-module="${Component.moduleName}"]`);
+    return isSupported() ? Array.from(o).map((e => {
         try {
             return void 0 !== t ? new Component(e, t) : new Component(e)
-        } catch (i) {
-            return n ? n(i, {
+        } catch (n) {
+            return s ? s(n, {
                 element: e,
                 component: Component,
                 config: t
-            }) : console.log(i), null
+            }) : console.log(n), null
         }
-    })).filter(Boolean) : (n ? n(new SupportError, {
+    })).filter(Boolean) : (s ? s(new SupportError, {
         component: Component,
         config: t
     }) : console.log(new SupportError), [])

Action run for 14962d4

Copy link

github-actions bot commented Sep 25, 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 9ec658d92..c5e713d0a 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.js
@@ -258,19 +258,30 @@
 
   class GOVUKFrontendComponent {
     constructor($root) {
+      this.$root = void 0;
       const childConstructor = this.constructor;
       if (typeof childConstructor.moduleName !== 'string') {
         throw new InitError(`\`moduleName\` not defined in component`);
       }
+      if (!($root instanceof childConstructor.elementType)) {
+        throw new ElementError({
+          element: $root,
+          component: childConstructor,
+          identifier: 'Root element (`$root`)',
+          expectedType: childConstructor.elementType.name
+        });
+      } else {
+        this.$root = $root;
+      }
       childConstructor.checkSupport();
-      this.checkInitialised($root);
+      this.checkInitialised();
       const moduleName = childConstructor.moduleName;
-      $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+      this.$root.setAttribute(`data-${moduleName}-init`, '');
     }
-    checkInitialised($root) {
+    checkInitialised() {
       const constructor = this.constructor;
       const moduleName = constructor.moduleName;
-      if ($root && moduleName && isInitialised($root, moduleName)) {
+      if (moduleName && isInitialised(this.$root, moduleName)) {
         throw new InitError(constructor);
       }
     }
@@ -289,6 +300,7 @@
   /**
    * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
    */
+  GOVUKFrontendComponent.elementType = HTMLElement;
 
   class I18n {
     constructor(translations = {}, config = {}) {
@@ -504,7 +516,6 @@
      */
     constructor($root, config = {}) {
       super($root);
-      this.$root = void 0;
       this.config = void 0;
       this.i18n = void 0;
       this.controlsClass = 'govuk-accordion__controls';
@@ -530,15 +541,7 @@
       this.$showAllButton = null;
       this.$showAllIcon = null;
       this.$showAllText = null;
-      if (!($root instanceof HTMLElement)) {
-        throw new ElementError({
-          component: Accordion,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      this.$root = $root;
-      this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, $root.dataset));
+      this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, this.$root.dataset));
       this.i18n = new I18n(this.config.i18n);
       const $sections = this.$root.querySelectorAll(`.${this.sectionClass}`);
       if (!$sections.length) {
@@ -847,18 +850,9 @@
      */
     constructor($root, config = {}) {
       super($root);
-      this.$root = void 0;
       this.config = void 0;
       this.debounceFormSubmitTimer = null;
-      if (!($root instanceof HTMLElement)) {
-        throw new ElementError({
-          component: Button,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      this.$root = $root;
-      this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, $root.dataset));
+      this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, this.$root.dataset));
       this.$root.addEventListener('keydown', event => this.handleKeyDown(event));
       this.$root.addEventListener('click', event => this.debounce(event));
     }
@@ -934,7 +928,6 @@
     constructor($root, config = {}) {
       var _ref, _this$config$maxwords;
       super($root);
-      this.$root = void 0;
       this.$textarea = void 0;
       this.$visibleCountMessage = void 0;
       this.$screenReaderCountMessage = void 0;
@@ -944,14 +937,7 @@
       this.config = void 0;
       this.i18n = void 0;
       this.maxLength = void 0;
-      if (!($root instanceof HTMLElement)) {
-        throw new ElementError({
-          component: CharacterCount,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      const $textarea = $root.querySelector('.govuk-js-character-count');
+      const $textarea = this.$root.querySelector('.govuk-js-character-count');
       if (!($textarea instanceof HTMLTextAreaElement || $textarea instanceof HTMLInputElement)) {
         throw new ElementError({
           component: CharacterCount,
@@ -960,7 +946,7 @@
           identifier: 'Form field (`.govuk-js-character-count`)'
         });
       }
-      const datasetConfig = normaliseDataset(CharacterCount, $root.dataset);
+      const datasetConfig = normaliseDataset(CharacterCount, this.$root.dataset);
       let configOverrides = {};
       if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
         configOverrides = {
@@ -974,10 +960,9 @@
         throw new ConfigError(formatErrorMessage(CharacterCount, errors[0]));
       }
       this.i18n = new I18n(this.config.i18n, {
-        locale: closestAttributeValue($root, 'lang')
+        locale: closestAttributeValue(this.$root, 'lang')
       });
       this.maxLength = (_ref = (_this$config$maxwords = this.config.maxwords) != null ? _this$config$maxwords : this.config.maxlength) != null ? _ref : Infinity;
-      this.$root = $root;
       this.$textarea = $textarea;
       const textareaDescriptionId = `${this.$textarea.id}-info`;
       const $textareaDescription = document.getElementById(textareaDescriptionId);
@@ -1231,23 +1216,14 @@
      */
     constructor($root) {
       super($root);
-      this.$root = void 0;
       this.$inputs = void 0;
-      if (!($root instanceof HTMLElement)) {
-        throw new ElementError({
-          component: Checkboxes,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      const $inputs = $root.querySelectorAll('input[type="checkbox"]');
+      const $inputs = this.$root.querySelectorAll('input[type="checkbox"]');
       if (!$inputs.length) {
         throw new ElementError({
           component: Checkboxes,
           identifier: 'Form inputs (`<input type="checkbox">`)'
         });
       }
-      this.$root = $root;
       this.$inputs = $inputs;
       this.$inputs.forEach($input => {
         const targetId = $input.getAttribute('data-aria-controls');
@@ -1339,17 +1315,8 @@
      */
     constructor($root, config = {}) {
       super($root);
-      this.$root = void 0;
       this.config = void 0;
-      if (!($root instanceof HTMLElement)) {
-        throw new ElementError({
-          component: ErrorSummary,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      this.$root = $root;
-      this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, $root.dataset));
+      this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, this.$root.dataset));
       if (!this.config.disableAutoFocus) {
         setFocus(this.$root);
       }
@@ -1442,7 +1409,6 @@
      */
     constructor($root, config = {}) {
       super($root);
-      this.$root = void 0;
       this.config = void 0;
       this.i18n = void 0;
       this.$button = void 0;
@@ -1455,14 +1421,7 @@
       this.timeoutTime = 5000;
       this.keypressTimeoutId = null;
       this.timeoutMessageId = null;
-      if (!($root instanceof HTMLElement)) {
-        throw new ElementError({
-          component: ExitThisPage,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      const $button = $root.querySelector('.govuk-exit-this-page__button');
+      const $button = this.$root.querySelector('.govuk-exit-this-page__button');
       if (!($button instanceof HTMLAnchorElement)) {
         throw new ElementError({
           component: ExitThisPage,
@@ -1471,9 +1430,8 @@
           identifier: 'Button (`.govuk-exit-this-page__button`)'
         });
       }
-      this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset(ExitThisPage, $root.dataset));
+      this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset(ExitThisPage, this.$root.dataset));
       this.i18n = new I18n(this.config.i18n);
-      this.$root = $root;
       this.$button = $button;
       const $skiplinkButton = document.querySelector('.govuk-js-exit-this-page-skiplink');
       if ($skiplinkButton instanceof HTMLAnchorElement) {
@@ -1671,20 +1629,11 @@
      */
     constructor($root) {
       super($root);
-      this.$root = void 0;
       this.$menuButton = void 0;
       this.$menu = void 0;
       this.menuIsOpen = false;
       this.mql = null;
-      if (!$root) {
-        throw new ElementError({
-          component: Header,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      this.$root = $root;
-      const $menuButton = $root.querySelector('.govuk-js-header-toggle');
+      const $menuButton = this.$root.querySelector('.govuk-js-header-toggle');
       if (!$menuButton) {
         return this;
       }
@@ -1760,17 +1709,8 @@
      */
     constructor($root, config = {}) {
       super($root);
-      this.$root = void 0;
       this.config = void 0;
-      if (!($root instanceof HTMLElement)) {
-        throw new ElementError({
-          component: NotificationBanner,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      this.$root = $root;
-      this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset(NotificationBanner, $root.dataset));
+      this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset(NotificationBanner, this.$root.dataset));
       if (this.$root.getAttribute('role') === 'alert' && !this.config.disableAutoFocus) {
         setFocus(this.$root);
       }
@@ -1813,21 +1753,13 @@
      * @param {PasswordInputConfig} [config] - Password input config
      */
     constructor($root, config = {}) {
-      super();
-      this.$root = void 0;
+      super($root);
       this.config = void 0;
       this.i18n = void 0;
       this.$input = void 0;
       this.$showHideButton = void 0;
       this.$screenReaderStatusMessage = void 0;
-      if (!($root instanceof HTMLElement)) {
-        throw new ElementError({
-          component: PasswordInput,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      const $input = $root.querySelector('.govuk-js-password-input-input');
+      const $input = this.$root.querySelector('.govuk-js-password-input-input');
       if (!($input instanceof HTMLInputElement)) {
         throw new ElementError({
           component: PasswordInput,
@@ -1839,7 +1771,7 @@
       if ($input.type !== 'password') {
         throw new ElementError('Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.');
       }
-      const $showHideButton = $root.querySelector('.govuk-js-password-input-toggle');
+      const $showHideButton = this.$root.querySelector('.govuk-js-password-input-toggle');
       if (!($showHideButton instanceof HTMLButtonElement)) {
         throw new ElementError({
           component: PasswordInput,
@@ -1851,12 +1783,11 @@
       if ($showHideButton.type !== 'button') {
         throw new ElementError('Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.');
       }
-      this.$root = $root;
       this.$input = $input;
       this.$showHideButton = $showHideButton;
-      this.config = mergeConfigs(PasswordInput.defaults, config, normaliseDataset(PasswordInput, $root.dataset));
+      this.config = mergeConfigs(PasswordInput.defaults, config, normaliseDataset(PasswordInput, this.$root.dataset));
       this.i18n = new I18n(this.config.i18n, {
-        locale: closestAttributeValue($root, 'lang')
+        locale: closestAttributeValue(this.$root, 'lang')
       });
       this.$showHideButton.removeAttribute('hidden');
       const $screenReaderStatusMessage = document.createElement('div');
@@ -1978,23 +1909,14 @@
      */
     constructor($root) {
       super($root);
-      this.$root = void 0;
       this.$inputs = void 0;
-      if (!($root instanceof HTMLElement)) {
-        throw new ElementError({
-          component: Radios,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      const $inputs = $root.querySelectorAll('input[type="radio"]');
+      const $inputs = this.$root.querySelectorAll('input[type="radio"]');
       if (!$inputs.length) {
         throw new ElementError({
           component: Radios,
           identifier: 'Form inputs (`<input type="radio">`)'
         });
       }
-      this.$root = $root;
       this.$inputs = $inputs;
       this.$inputs.forEach($input => {
         const targetId = $input.getAttribute('data-aria-controls');
@@ -2058,21 +1980,12 @@
      * @param {Element | null} $root - HTML element to use for header
      */
     constructor($root) {
-      super();
-      this.$root = void 0;
+      super($root);
       this.$menuButton = void 0;
       this.$menu = void 0;
       this.menuIsOpen = false;
       this.mql = null;
-      if (!$root) {
-        throw new ElementError({
-          component: ServiceNavigation,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      this.$root = $root;
-      const $menuButton = $root.querySelector('.govuk-js-service-navigation-toggle');
+      const $menuButton = this.$root.querySelector('.govuk-js-service-navigation-toggle');
       if (!$menuButton) {
         return this;
       }
@@ -2140,6 +2053,7 @@
    * Skip link component
    *
    * @preserve
+   * @augments GOVUKFrontendComponent<HTMLAnchorElement>
    */
   class SkipLink extends GOVUKFrontendComponent {
     /**
@@ -2151,16 +2065,6 @@
     constructor($root) {
       var _this$$root$getAttrib;
       super($root);
-      this.$root = void 0;
-      if (!($root instanceof HTMLAnchorElement)) {
-        throw new ElementError({
-          component: SkipLink,
-          element: $root,
-          expectedType: 'HTMLAnchorElement',
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      this.$root = $root;
       const hash = this.$root.hash;
       const href = (_this$$root$getAttrib = this.$root.getAttribute('href')) != null ? _this$$root$getAttrib : '';
       let url;
@@ -2194,6 +2098,7 @@
       }));
     }
   }
+  SkipLink.elementType = HTMLAnchorElement;
   SkipLink.moduleName = 'govuk-skip-link';
 
   /**
@@ -2207,7 +2112,6 @@
      */
     constructor($root) {
       super($root);
-      this.$root = void 0;
       this.$tabs = void 0;
       this.$tabList = void 0;
       this.$tabListItems = void 0;
@@ -2217,21 +2121,13 @@
       this.boundTabKeydown = void 0;
       this.boundOnHashChange = void 0;
       this.mql = null;
-      if (!$root) {
-        throw new ElementError({
-          component: Tabs,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      const $tabs = $root.querySelectorAll('a.govuk-tabs__tab');
+      const $tabs = this.$root.querySelectorAll('a.govuk-tabs__tab');
       if (!$tabs.length) {
         throw new ElementError({
           component: Tabs,
           identifier: 'Links (`<a class="govuk-tabs__tab">`)'
         });
       }
-      this.$root = $root;
       this.$tabs = $tabs;
       this.boundTabClick = this.onTabClick.bind(this);
       this.boundTabKeydown = this.onTabKeydown.bind(this);
diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.mjs b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
index 5976fbe5e..d12361b48 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
@@ -252,19 +252,30 @@ class InitError extends GOVUKFrontendError {
 
 class GOVUKFrontendComponent {
   constructor($root) {
+    this.$root = void 0;
     const childConstructor = this.constructor;
     if (typeof childConstructor.moduleName !== 'string') {
       throw new InitError(`\`moduleName\` not defined in component`);
     }
+    if (!($root instanceof childConstructor.elementType)) {
+      throw new ElementError({
+        element: $root,
+        component: childConstructor,
+        identifier: 'Root element (`$root`)',
+        expectedType: childConstructor.elementType.name
+      });
+    } else {
+      this.$root = $root;
+    }
     childConstructor.checkSupport();
-    this.checkInitialised($root);
+    this.checkInitialised();
     const moduleName = childConstructor.moduleName;
-    $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+    this.$root.setAttribute(`data-${moduleName}-init`, '');
   }
-  checkInitialised($root) {
+  checkInitialised() {
     const constructor = this.constructor;
     const moduleName = constructor.moduleName;
-    if ($root && moduleName && isInitialised($root, moduleName)) {
+    if (moduleName && isInitialised(this.$root, moduleName)) {
       throw new InitError(constructor);
     }
   }
@@ -283,6 +294,7 @@ class GOVUKFrontendComponent {
 /**
  * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
  */
+GOVUKFrontendComponent.elementType = HTMLElement;
 
 class I18n {
   constructor(translations = {}, config = {}) {
@@ -498,7 +510,6 @@ class Accordion extends GOVUKFrontendComponent {
    */
   constructor($root, config = {}) {
     super($root);
-    this.$root = void 0;
     this.config = void 0;
     this.i18n = void 0;
     this.controlsClass = 'govuk-accordion__controls';
@@ -524,15 +535,7 @@ class Accordion extends GOVUKFrontendComponent {
     this.$showAllButton = null;
     this.$showAllIcon = null;
     this.$showAllText = null;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: Accordion,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
-    this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, $root.dataset));
+    this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, this.$root.dataset));
     this.i18n = new I18n(this.config.i18n);
     const $sections = this.$root.querySelectorAll(`.${this.sectionClass}`);
     if (!$sections.length) {
@@ -841,18 +844,9 @@ class Button extends GOVUKFrontendComponent {
    */
   constructor($root, config = {}) {
     super($root);
-    this.$root = void 0;
     this.config = void 0;
     this.debounceFormSubmitTimer = null;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: Button,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
-    this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, $root.dataset));
+    this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, this.$root.dataset));
     this.$root.addEventListener('keydown', event => this.handleKeyDown(event));
     this.$root.addEventListener('click', event => this.debounce(event));
   }
@@ -928,7 +922,6 @@ class CharacterCount extends GOVUKFrontendComponent {
   constructor($root, config = {}) {
     var _ref, _this$config$maxwords;
     super($root);
-    this.$root = void 0;
     this.$textarea = void 0;
     this.$visibleCountMessage = void 0;
     this.$screenReaderCountMessage = void 0;
@@ -938,14 +931,7 @@ class CharacterCount extends GOVUKFrontendComponent {
     this.config = void 0;
     this.i18n = void 0;
     this.maxLength = void 0;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: CharacterCount,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    const $textarea = $root.querySelector('.govuk-js-character-count');
+    const $textarea = this.$root.querySelector('.govuk-js-character-count');
     if (!($textarea instanceof HTMLTextAreaElement || $textarea instanceof HTMLInputElement)) {
       throw new ElementError({
         component: CharacterCount,
@@ -954,7 +940,7 @@ class CharacterCount extends GOVUKFrontendComponent {
         identifier: 'Form field (`.govuk-js-character-count`)'
       });
     }
-    const datasetConfig = normaliseDataset(CharacterCount, $root.dataset);
+    const datasetConfig = normaliseDataset(CharacterCount, this.$root.dataset);
     let configOverrides = {};
     if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
       configOverrides = {
@@ -968,10 +954,9 @@ class CharacterCount extends GOVUKFrontendComponent {
       throw new ConfigError(formatErrorMessage(CharacterCount, errors[0]));
     }
     this.i18n = new I18n(this.config.i18n, {
-      locale: closestAttributeValue($root, 'lang')
+      locale: closestAttributeValue(this.$root, 'lang')
     });
     this.maxLength = (_ref = (_this$config$maxwords = this.config.maxwords) != null ? _this$config$maxwords : this.config.maxlength) != null ? _ref : Infinity;
-    this.$root = $root;
     this.$textarea = $textarea;
     const textareaDescriptionId = `${this.$textarea.id}-info`;
     const $textareaDescription = document.getElementById(textareaDescriptionId);
@@ -1225,23 +1210,14 @@ class Checkboxes extends GOVUKFrontendComponent {
    */
   constructor($root) {
     super($root);
-    this.$root = void 0;
     this.$inputs = void 0;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: Checkboxes,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    const $inputs = $root.querySelectorAll('input[type="checkbox"]');
+    const $inputs = this.$root.querySelectorAll('input[type="checkbox"]');
     if (!$inputs.length) {
       throw new ElementError({
         component: Checkboxes,
         identifier: 'Form inputs (`<input type="checkbox">`)'
       });
     }
-    this.$root = $root;
     this.$inputs = $inputs;
     this.$inputs.forEach($input => {
       const targetId = $input.getAttribute('data-aria-controls');
@@ -1333,17 +1309,8 @@ class ErrorSummary extends GOVUKFrontendComponent {
    */
   constructor($root, config = {}) {
     super($root);
-    this.$root = void 0;
     this.config = void 0;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: ErrorSummary,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
-    this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, $root.dataset));
+    this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, this.$root.dataset));
     if (!this.config.disableAutoFocus) {
       setFocus(this.$root);
     }
@@ -1436,7 +1403,6 @@ class ExitThisPage extends GOVUKFrontendComponent {
    */
   constructor($root, config = {}) {
     super($root);
-    this.$root = void 0;
     this.config = void 0;
     this.i18n = void 0;
     this.$button = void 0;
@@ -1449,14 +1415,7 @@ class ExitThisPage extends GOVUKFrontendComponent {
     this.timeoutTime = 5000;
     this.keypressTimeoutId = null;
     this.timeoutMessageId = null;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: ExitThisPage,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    const $button = $root.querySelector('.govuk-exit-this-page__button');
+    const $button = this.$root.querySelector('.govuk-exit-this-page__button');
     if (!($button instanceof HTMLAnchorElement)) {
       throw new ElementError({
         component: ExitThisPage,
@@ -1465,9 +1424,8 @@ class ExitThisPage extends GOVUKFrontendComponent {
         identifier: 'Button (`.govuk-exit-this-page__button`)'
       });
     }
-    this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset(ExitThisPage, $root.dataset));
+    this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset(ExitThisPage, this.$root.dataset));
     this.i18n = new I18n(this.config.i18n);
-    this.$root = $root;
     this.$button = $button;
     const $skiplinkButton = document.querySelector('.govuk-js-exit-this-page-skiplink');
     if ($skiplinkButton instanceof HTMLAnchorElement) {
@@ -1665,20 +1623,11 @@ class Header extends GOVUKFrontendComponent {
    */
   constructor($root) {
     super($root);
-    this.$root = void 0;
     this.$menuButton = void 0;
     this.$menu = void 0;
     this.menuIsOpen = false;
     this.mql = null;
-    if (!$root) {
-      throw new ElementError({
-        component: Header,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
-    const $menuButton = $root.querySelector('.govuk-js-header-toggle');
+    const $menuButton = this.$root.querySelector('.govuk-js-header-toggle');
     if (!$menuButton) {
       return this;
     }
@@ -1754,17 +1703,8 @@ class NotificationBanner extends GOVUKFrontendComponent {
    */
   constructor($root, config = {}) {
     super($root);
-    this.$root = void 0;
     this.config = void 0;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: NotificationBanner,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
-    this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset(NotificationBanner, $root.dataset));
+    this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset(NotificationBanner, this.$root.dataset));
     if (this.$root.getAttribute('role') === 'alert' && !this.config.disableAutoFocus) {
       setFocus(this.$root);
     }
@@ -1807,21 +1747,13 @@ class PasswordInput extends GOVUKFrontendComponent {
    * @param {PasswordInputConfig} [config] - Password input config
    */
   constructor($root, config = {}) {
-    super();
-    this.$root = void 0;
+    super($root);
     this.config = void 0;
     this.i18n = void 0;
     this.$input = void 0;
     this.$showHideButton = void 0;
     this.$screenReaderStatusMessage = void 0;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: PasswordInput,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    const $input = $root.querySelector('.govuk-js-password-input-input');
+    const $input = this.$root.querySelector('.govuk-js-password-input-input');
     if (!($input instanceof HTMLInputElement)) {
       throw new ElementError({
         component: PasswordInput,
@@ -1833,7 +1765,7 @@ class PasswordInput extends GOVUKFrontendComponent {
     if ($input.type !== 'password') {
       throw new ElementError('Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.');
     }
-    const $showHideButton = $root.querySelector('.govuk-js-password-input-toggle');
+    const $showHideButton = this.$root.querySelector('.govuk-js-password-input-toggle');
     if (!($showHideButton instanceof HTMLButtonElement)) {
       throw new ElementError({
         component: PasswordInput,
@@ -1845,12 +1777,11 @@ class PasswordInput extends GOVUKFrontendComponent {
     if ($showHideButton.type !== 'button') {
       throw new ElementError('Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.');
     }
-    this.$root = $root;
     this.$input = $input;
     this.$showHideButton = $showHideButton;
-    this.config = mergeConfigs(PasswordInput.defaults, config, normaliseDataset(PasswordInput, $root.dataset));
+    this.config = mergeConfigs(PasswordInput.defaults, config, normaliseDataset(PasswordInput, this.$root.dataset));
     this.i18n = new I18n(this.config.i18n, {
-      locale: closestAttributeValue($root, 'lang')
+      locale: closestAttributeValue(this.$root, 'lang')
     });
     this.$showHideButton.removeAttribute('hidden');
     const $screenReaderStatusMessage = document.createElement('div');
@@ -1972,23 +1903,14 @@ class Radios extends GOVUKFrontendComponent {
    */
   constructor($root) {
     super($root);
-    this.$root = void 0;
     this.$inputs = void 0;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: Radios,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    const $inputs = $root.querySelectorAll('input[type="radio"]');
+    const $inputs = this.$root.querySelectorAll('input[type="radio"]');
     if (!$inputs.length) {
       throw new ElementError({
         component: Radios,
         identifier: 'Form inputs (`<input type="radio">`)'
       });
     }
-    this.$root = $root;
     this.$inputs = $inputs;
     this.$inputs.forEach($input => {
       const targetId = $input.getAttribute('data-aria-controls');
@@ -2052,21 +1974,12 @@ class ServiceNavigation extends GOVUKFrontendComponent {
    * @param {Element | null} $root - HTML element to use for header
    */
   constructor($root) {
-    super();
-    this.$root = void 0;
+    super($root);
     this.$menuButton = void 0;
     this.$menu = void 0;
     this.menuIsOpen = false;
     this.mql = null;
-    if (!$root) {
-      throw new ElementError({
-        component: ServiceNavigation,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
-    const $menuButton = $root.querySelector('.govuk-js-service-navigation-toggle');
+    const $menuButton = this.$root.querySelector('.govuk-js-service-navigation-toggle');
     if (!$menuButton) {
       return this;
     }
@@ -2134,6 +2047,7 @@ ServiceNavigation.moduleName = 'govuk-service-navigation';
  * Skip link component
  *
  * @preserve
+ * @augments GOVUKFrontendComponent<HTMLAnchorElement>
  */
 class SkipLink extends GOVUKFrontendComponent {
   /**
@@ -2145,16 +2059,6 @@ class SkipLink extends GOVUKFrontendComponent {
   constructor($root) {
     var _this$$root$getAttrib;
     super($root);
-    this.$root = void 0;
-    if (!($root instanceof HTMLAnchorElement)) {
-      throw new ElementError({
-        component: SkipLink,
-        element: $root,
-        expectedType: 'HTMLAnchorElement',
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
     const hash = this.$root.hash;
     const href = (_this$$root$getAttrib = this.$root.getAttribute('href')) != null ? _this$$root$getAttrib : '';
     let url;
@@ -2188,6 +2092,7 @@ class SkipLink extends GOVUKFrontendComponent {
     }));
   }
 }
+SkipLink.elementType = HTMLAnchorElement;
 SkipLink.moduleName = 'govuk-skip-link';
 
 /**
@@ -2201,7 +2106,6 @@ class Tabs extends GOVUKFrontendComponent {
    */
   constructor($root) {
     super($root);
-    this.$root = void 0;
     this.$tabs = void 0;
     this.$tabList = void 0;
     this.$tabListItems = void 0;
@@ -2211,21 +2115,13 @@ class Tabs extends GOVUKFrontendComponent {
     this.boundTabKeydown = void 0;
     this.boundOnHashChange = void 0;
     this.mql = null;
-    if (!$root) {
-      throw new ElementError({
-        component: Tabs,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    const $tabs = $root.querySelectorAll('a.govuk-tabs__tab');
+    const $tabs = this.$root.querySelectorAll('a.govuk-tabs__tab');
     if (!$tabs.length) {
       throw new ElementError({
         component: Tabs,
         identifier: 'Links (`<a class="govuk-tabs__tab">`)'
       });
     }
-    this.$root = $root;
     this.$tabs = $tabs;
     this.boundTabClick = this.onTabClick.bind(this);
     this.boundTabKeydown = this.onTabKeydown.bind(this);
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 b00a99a02..72b42a9e4 100644
--- a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.js
@@ -192,19 +192,30 @@
 
   class GOVUKFrontendComponent {
     constructor($root) {
+      this.$root = void 0;
       const childConstructor = this.constructor;
       if (typeof childConstructor.moduleName !== 'string') {
         throw new InitError(`\`moduleName\` not defined in component`);
       }
+      if (!($root instanceof childConstructor.elementType)) {
+        throw new ElementError({
+          element: $root,
+          component: childConstructor,
+          identifier: 'Root element (`$root`)',
+          expectedType: childConstructor.elementType.name
+        });
+      } else {
+        this.$root = $root;
+      }
       childConstructor.checkSupport();
-      this.checkInitialised($root);
+      this.checkInitialised();
       const moduleName = childConstructor.moduleName;
-      $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+      this.$root.setAttribute(`data-${moduleName}-init`, '');
     }
-    checkInitialised($root) {
+    checkInitialised() {
       const constructor = this.constructor;
       const moduleName = constructor.moduleName;
-      if ($root && moduleName && isInitialised($root, moduleName)) {
+      if (moduleName && isInitialised(this.$root, moduleName)) {
         throw new InitError(constructor);
       }
     }
@@ -223,6 +234,7 @@
   /**
    * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
    */
+  GOVUKFrontendComponent.elementType = HTMLElement;
 
   class I18n {
     constructor(translations = {}, config = {}) {
@@ -438,7 +450,6 @@
      */
     constructor($root, config = {}) {
       super($root);
-      this.$root = void 0;
       this.config = void 0;
       this.i18n = void 0;
       this.controlsClass = 'govuk-accordion__controls';
@@ -464,15 +475,7 @@
       this.$showAllButton = null;
       this.$showAllIcon = null;
       this.$showAllText = null;
-      if (!($root instanceof HTMLElement)) {
-        throw new ElementError({
-          component: Accordion,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      this.$root = $root;
-      this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, $root.dataset));
+      this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, this.$root.dataset));
       this.i18n = new I18n(this.config.i18n);
       const $sections = this.$root.querySelectorAll(`.${this.sectionClass}`);
       if (!$sections.length) {
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 4d348f991..a3fca77f7 100644
--- a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.bundle.mjs
@@ -186,19 +186,30 @@ class InitError extends GOVUKFrontendError {
 
 class GOVUKFrontendComponent {
   constructor($root) {
+    this.$root = void 0;
     const childConstructor = this.constructor;
     if (typeof childConstructor.moduleName !== 'string') {
       throw new InitError(`\`moduleName\` not defined in component`);
     }
+    if (!($root instanceof childConstructor.elementType)) {
+      throw new ElementError({
+        element: $root,
+        component: childConstructor,
+        identifier: 'Root element (`$root`)',
+        expectedType: childConstructor.elementType.name
+      });
+    } else {
+      this.$root = $root;
+    }
     childConstructor.checkSupport();
-    this.checkInitialised($root);
+    this.checkInitialised();
     const moduleName = childConstructor.moduleName;
-    $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+    this.$root.setAttribute(`data-${moduleName}-init`, '');
   }
-  checkInitialised($root) {
+  checkInitialised() {
     const constructor = this.constructor;
     const moduleName = constructor.moduleName;
-    if ($root && moduleName && isInitialised($root, moduleName)) {
+    if (moduleName && isInitialised(this.$root, moduleName)) {
       throw new InitError(constructor);
     }
   }
@@ -217,6 +228,7 @@ class GOVUKFrontendComponent {
 /**
  * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
  */
+GOVUKFrontendComponent.elementType = HTMLElement;
 
 class I18n {
   constructor(translations = {}, config = {}) {
@@ -432,7 +444,6 @@ class Accordion extends GOVUKFrontendComponent {
    */
   constructor($root, config = {}) {
     super($root);
-    this.$root = void 0;
     this.config = void 0;
     this.i18n = void 0;
     this.controlsClass = 'govuk-accordion__controls';
@@ -458,15 +469,7 @@ class Accordion extends GOVUKFrontendComponent {
     this.$showAllButton = null;
     this.$showAllIcon = null;
     this.$showAllText = null;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: Accordion,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
-    this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, $root.dataset));
+    this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, this.$root.dataset));
     this.i18n = new I18n(this.config.i18n);
     const $sections = this.$root.querySelectorAll(`.${this.sectionClass}`);
     if (!$sections.length) {
diff --git a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs
index 038e1f52e..528bdad14 100644
--- a/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/accordion/accordion.mjs
@@ -25,7 +25,6 @@ class Accordion extends GOVUKFrontendComponent {
    */
   constructor($root, config = {}) {
     super($root);
-    this.$root = void 0;
     this.config = void 0;
     this.i18n = void 0;
     this.controlsClass = 'govuk-accordion__controls';
@@ -51,15 +50,7 @@ class Accordion extends GOVUKFrontendComponent {
     this.$showAllButton = null;
     this.$showAllIcon = null;
     this.$showAllText = null;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: Accordion,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
-    this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, $root.dataset));
+    this.config = mergeConfigs(Accordion.defaults, config, normaliseDataset(Accordion, this.$root.dataset));
     this.i18n = new I18n(this.config.i18n);
     const $sections = this.$root.querySelectorAll(`.${this.sectionClass}`);
     if (!$sections.length) {
diff --git a/packages/govuk-frontend/dist/govuk/components/button/button.bundle.js b/packages/govuk-frontend/dist/govuk/components/button/button.bundle.js
index b275dda25..3876c06fe 100644
--- a/packages/govuk-frontend/dist/govuk/components/button/button.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/button/button.bundle.js
@@ -192,19 +192,30 @@
 
   class GOVUKFrontendComponent {
     constructor($root) {
+      this.$root = void 0;
       const childConstructor = this.constructor;
       if (typeof childConstructor.moduleName !== 'string') {
         throw new InitError(`\`moduleName\` not defined in component`);
       }
+      if (!($root instanceof childConstructor.elementType)) {
+        throw new ElementError({
+          element: $root,
+          component: childConstructor,
+          identifier: 'Root element (`$root`)',
+          expectedType: childConstructor.elementType.name
+        });
+      } else {
+        this.$root = $root;
+      }
       childConstructor.checkSupport();
-      this.checkInitialised($root);
+      this.checkInitialised();
       const moduleName = childConstructor.moduleName;
-      $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+      this.$root.setAttribute(`data-${moduleName}-init`, '');
     }
-    checkInitialised($root) {
+    checkInitialised() {
       const constructor = this.constructor;
       const moduleName = constructor.moduleName;
-      if ($root && moduleName && isInitialised($root, moduleName)) {
+      if (moduleName && isInitialised(this.$root, moduleName)) {
         throw new InitError(constructor);
       }
     }
@@ -223,6 +234,7 @@
   /**
    * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
    */
+  GOVUKFrontendComponent.elementType = HTMLElement;
 
   const DEBOUNCE_TIMEOUT_IN_SECONDS = 1;
 
@@ -238,18 +250,9 @@
      */
     constructor($root, config = {}) {
       super($root);
-      this.$root = void 0;
       this.config = void 0;
       this.debounceFormSubmitTimer = null;
-      if (!($root instanceof HTMLElement)) {
-        throw new ElementError({
-          component: Button,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      this.$root = $root;
-      this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, $root.dataset));
+      this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, this.$root.dataset));
       this.$root.addEventListener('keydown', event => this.handleKeyDown(event));
       this.$root.addEventListener('click', event => this.debounce(event));
     }
diff --git a/packages/govuk-frontend/dist/govuk/components/button/button.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/button/button.bundle.mjs
index ba3248405..6e94943c4 100644
--- a/packages/govuk-frontend/dist/govuk/components/button/button.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/button/button.bundle.mjs
@@ -186,19 +186,30 @@ class InitError extends GOVUKFrontendError {
 
 class GOVUKFrontendComponent {
   constructor($root) {
+    this.$root = void 0;
     const childConstructor = this.constructor;
     if (typeof childConstructor.moduleName !== 'string') {
       throw new InitError(`\`moduleName\` not defined in component`);
     }
+    if (!($root instanceof childConstructor.elementType)) {
+      throw new ElementError({
+        element: $root,
+        component: childConstructor,
+        identifier: 'Root element (`$root`)',
+        expectedType: childConstructor.elementType.name
+      });
+    } else {
+      this.$root = $root;
+    }
     childConstructor.checkSupport();
-    this.checkInitialised($root);
+    this.checkInitialised();
     const moduleName = childConstructor.moduleName;
-    $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+    this.$root.setAttribute(`data-${moduleName}-init`, '');
   }
-  checkInitialised($root) {
+  checkInitialised() {
     const constructor = this.constructor;
     const moduleName = constructor.moduleName;
-    if ($root && moduleName && isInitialised($root, moduleName)) {
+    if (moduleName && isInitialised(this.$root, moduleName)) {
       throw new InitError(constructor);
     }
   }
@@ -217,6 +228,7 @@ class GOVUKFrontendComponent {
 /**
  * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
  */
+GOVUKFrontendComponent.elementType = HTMLElement;
 
 const DEBOUNCE_TIMEOUT_IN_SECONDS = 1;
 
@@ -232,18 +244,9 @@ class Button extends GOVUKFrontendComponent {
    */
   constructor($root, config = {}) {
     super($root);
-    this.$root = void 0;
     this.config = void 0;
     this.debounceFormSubmitTimer = null;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: Button,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
-    this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, $root.dataset));
+    this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, this.$root.dataset));
     this.$root.addEventListener('keydown', event => this.handleKeyDown(event));
     this.$root.addEventListener('click', event => this.debounce(event));
   }
diff --git a/packages/govuk-frontend/dist/govuk/components/button/button.mjs b/packages/govuk-frontend/dist/govuk/components/button/button.mjs
index cea3901e6..b25a2f3a4 100644
--- a/packages/govuk-frontend/dist/govuk/components/button/button.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/button/button.mjs
@@ -1,6 +1,5 @@
 import { mergeConfigs } from '../../common/index.mjs';
 import { normaliseDataset } from '../../common/normalise-dataset.mjs';
-import { ElementError } from '../../errors/index.mjs';
 import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs';
 
 const DEBOUNCE_TIMEOUT_IN_SECONDS = 1;
@@ -17,18 +16,9 @@ class Button extends GOVUKFrontendComponent {
    */
   constructor($root, config = {}) {
     super($root);
-    this.$root = void 0;
     this.config = void 0;
     this.debounceFormSubmitTimer = null;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: Button,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
-    this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, $root.dataset));
+    this.config = mergeConfigs(Button.defaults, config, normaliseDataset(Button, this.$root.dataset));
     this.$root.addEventListener('keydown', event => this.handleKeyDown(event));
     this.$root.addEventListener('click', event => this.debounce(event));
   }
diff --git a/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.js b/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.js
index 6012a2e72..e7253b42d 100644
--- a/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.js
@@ -223,19 +223,30 @@
 
   class GOVUKFrontendComponent {
     constructor($root) {
+      this.$root = void 0;
       const childConstructor = this.constructor;
       if (typeof childConstructor.moduleName !== 'string') {
         throw new InitError(`\`moduleName\` not defined in component`);
       }
+      if (!($root instanceof childConstructor.elementType)) {
+        throw new ElementError({
+          element: $root,
+          component: childConstructor,
+          identifier: 'Root element (`$root`)',
+          expectedType: childConstructor.elementType.name
+        });
+      } else {
+        this.$root = $root;
+      }
       childConstructor.checkSupport();
-      this.checkInitialised($root);
+      this.checkInitialised();
       const moduleName = childConstructor.moduleName;
-      $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+      this.$root.setAttribute(`data-${moduleName}-init`, '');
     }
-    checkInitialised($root) {
+    checkInitialised() {
       const constructor = this.constructor;
       const moduleName = constructor.moduleName;
-      if ($root && moduleName && isInitialised($root, moduleName)) {
+      if (moduleName && isInitialised(this.$root, moduleName)) {
         throw new InitError(constructor);
       }
     }
@@ -254,6 +265,7 @@
   /**
    * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
    */
+  GOVUKFrontendComponent.elementType = HTMLElement;
 
   class I18n {
     constructor(translations = {}, config = {}) {
@@ -468,7 +480,6 @@
     constructor($root, config = {}) {
       var _ref, _this$config$maxwords;
       super($root);
-      this.$root = void 0;
       this.$textarea = void 0;
       this.$visibleCountMessage = void 0;
       this.$screenReaderCountMessage = void 0;
@@ -478,14 +489,7 @@
       this.config = void 0;
       this.i18n = void 0;
       this.maxLength = void 0;
-      if (!($root instanceof HTMLElement)) {
-        throw new ElementError({
-          component: CharacterCount,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      const $textarea = $root.querySelector('.govuk-js-character-count');
+      const $textarea = this.$root.querySelector('.govuk-js-character-count');
       if (!($textarea instanceof HTMLTextAreaElement || $textarea instanceof HTMLInputElement)) {
         throw new ElementError({
           component: CharacterCount,
@@ -494,7 +498,7 @@
           identifier: 'Form field (`.govuk-js-character-count`)'
         });
       }
-      const datasetConfig = normaliseDataset(CharacterCount, $root.dataset);
+      const datasetConfig = normaliseDataset(CharacterCount, this.$root.dataset);
       let configOverrides = {};
       if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
         configOverrides = {
@@ -508,10 +512,9 @@
         throw new ConfigError(formatErrorMessage(CharacterCount, errors[0]));
       }
       this.i18n = new I18n(this.config.i18n, {
-        locale: closestAttributeValue($root, 'lang')
+        locale: closestAttributeValue(this.$root, 'lang')
       });
       this.maxLength = (_ref = (_this$config$maxwords = this.config.maxwords) != null ? _this$config$maxwords : this.config.maxlength) != null ? _ref : Infinity;
-      this.$root = $root;
       this.$textarea = $textarea;
       const textareaDescriptionId = `${this.$textarea.id}-info`;
       const $textareaDescription = document.getElementById(textareaDescriptionId);
diff --git a/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.mjs
index b4df99f57..f38d1cc7b 100644
--- a/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/character-count/character-count.bundle.mjs
@@ -217,19 +217,30 @@ class InitError extends GOVUKFrontendError {
 
 class GOVUKFrontendComponent {
   constructor($root) {
+    this.$root = void 0;
     const childConstructor = this.constructor;
     if (typeof childConstructor.moduleName !== 'string') {
       throw new InitError(`\`moduleName\` not defined in component`);
     }
+    if (!($root instanceof childConstructor.elementType)) {
+      throw new ElementError({
+        element: $root,
+        component: childConstructor,
+        identifier: 'Root element (`$root`)',
+        expectedType: childConstructor.elementType.name
+      });
+    } else {
+      this.$root = $root;
+    }
     childConstructor.checkSupport();
-    this.checkInitialised($root);
+    this.checkInitialised();
     const moduleName = childConstructor.moduleName;
-    $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+    this.$root.setAttribute(`data-${moduleName}-init`, '');
   }
-  checkInitialised($root) {
+  checkInitialised() {
     const constructor = this.constructor;
     const moduleName = constructor.moduleName;
-    if ($root && moduleName && isInitialised($root, moduleName)) {
+    if (moduleName && isInitialised(this.$root, moduleName)) {
       throw new InitError(constructor);
     }
   }
@@ -248,6 +259,7 @@ class GOVUKFrontendComponent {
 /**
  * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
  */
+GOVUKFrontendComponent.elementType = HTMLElement;
 
 class I18n {
   constructor(translations = {}, config = {}) {
@@ -462,7 +474,6 @@ class CharacterCount extends GOVUKFrontendComponent {
   constructor($root, config = {}) {
     var _ref, _this$config$maxwords;
     super($root);
-    this.$root = void 0;
     this.$textarea = void 0;
     this.$visibleCountMessage = void 0;
     this.$screenReaderCountMessage = void 0;
@@ -472,14 +483,7 @@ class CharacterCount extends GOVUKFrontendComponent {
     this.config = void 0;
     this.i18n = void 0;
     this.maxLength = void 0;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: CharacterCount,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    const $textarea = $root.querySelector('.govuk-js-character-count');
+    const $textarea = this.$root.querySelector('.govuk-js-character-count');
     if (!($textarea instanceof HTMLTextAreaElement || $textarea instanceof HTMLInputElement)) {
       throw new ElementError({
         component: CharacterCount,
@@ -488,7 +492,7 @@ class CharacterCount extends GOVUKFrontendComponent {
         identifier: 'Form field (`.govuk-js-character-count`)'
       });
     }
-    const datasetConfig = normaliseDataset(CharacterCount, $root.dataset);
+    const datasetConfig = normaliseDataset(CharacterCount, this.$root.dataset);
     let configOverrides = {};
     if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
       configOverrides = {
@@ -502,10 +506,9 @@ class CharacterCount extends GOVUKFrontendComponent {
       throw new ConfigError(formatErrorMessage(CharacterCount, errors[0]));
     }
     this.i18n = new I18n(this.config.i18n, {
-      locale: closestAttributeValue($root, 'lang')
+      locale: closestAttributeValue(this.$root, 'lang')
     });
     this.maxLength = (_ref = (_this$config$maxwords = this.config.maxwords) != null ? _this$config$maxwords : this.config.maxlength) != null ? _ref : Infinity;
-    this.$root = $root;
     this.$textarea = $textarea;
     const textareaDescriptionId = `${this.$textarea.id}-info`;
     const $textareaDescription = document.getElementById(textareaDescriptionId);
diff --git a/packages/govuk-frontend/dist/govuk/components/character-count/character-count.mjs b/packages/govuk-frontend/dist/govuk/components/character-count/character-count.mjs
index af34aa19c..84cd54d37 100644
--- a/packages/govuk-frontend/dist/govuk/components/character-count/character-count.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/character-count/character-count.mjs
@@ -25,7 +25,6 @@ class CharacterCount extends GOVUKFrontendComponent {
   constructor($root, config = {}) {
     var _ref, _this$config$maxwords;
     super($root);
-    this.$root = void 0;
     this.$textarea = void 0;
     this.$visibleCountMessage = void 0;
     this.$screenReaderCountMessage = void 0;
@@ -35,14 +34,7 @@ class CharacterCount extends GOVUKFrontendComponent {
     this.config = void 0;
     this.i18n = void 0;
     this.maxLength = void 0;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: CharacterCount,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    const $textarea = $root.querySelector('.govuk-js-character-count');
+    const $textarea = this.$root.querySelector('.govuk-js-character-count');
     if (!($textarea instanceof HTMLTextAreaElement || $textarea instanceof HTMLInputElement)) {
       throw new ElementError({
         component: CharacterCount,
@@ -51,7 +43,7 @@ class CharacterCount extends GOVUKFrontendComponent {
         identifier: 'Form field (`.govuk-js-character-count`)'
       });
     }
-    const datasetConfig = normaliseDataset(CharacterCount, $root.dataset);
+    const datasetConfig = normaliseDataset(CharacterCount, this.$root.dataset);
     let configOverrides = {};
     if ('maxwords' in datasetConfig || 'maxlength' in datasetConfig) {
       configOverrides = {
@@ -65,10 +57,9 @@ class CharacterCount extends GOVUKFrontendComponent {
       throw new ConfigError(formatErrorMessage(CharacterCount, errors[0]));
     }
     this.i18n = new I18n(this.config.i18n, {
-      locale: closestAttributeValue($root, 'lang')
+      locale: closestAttributeValue(this.$root, 'lang')
     });
     this.maxLength = (_ref = (_this$config$maxwords = this.config.maxwords) != null ? _this$config$maxwords : this.config.maxlength) != null ? _ref : Infinity;
-    this.$root = $root;
     this.$textarea = $textarea;
     const textareaDescriptionId = `${this.$textarea.id}-info`;
     const $textareaDescription = document.getElementById(textareaDescriptionId);
diff --git a/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.js b/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.js
index c5d11670d..efb73657f 100644
--- a/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.js
@@ -103,19 +103,30 @@
 
   class GOVUKFrontendComponent {
     constructor($root) {
+      this.$root = void 0;
       const childConstructor = this.constructor;
       if (typeof childConstructor.moduleName !== 'string') {
         throw new InitError(`\`moduleName\` not defined in component`);
       }
+      if (!($root instanceof childConstructor.elementType)) {
+        throw new ElementError({
+          element: $root,
+          component: childConstructor,
+          identifier: 'Root element (`$root`)',
+          expectedType: childConstructor.elementType.name
+        });
+      } else {
+        this.$root = $root;
+      }
       childConstructor.checkSupport();
-      this.checkInitialised($root);
+      this.checkInitialised();
       const moduleName = childConstructor.moduleName;
-      $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+      this.$root.setAttribute(`data-${moduleName}-init`, '');
     }
-    checkInitialised($root) {
+    checkInitialised() {
       const constructor = this.constructor;
       const moduleName = constructor.moduleName;
-      if ($root && moduleName && isInitialised($root, moduleName)) {
+      if (moduleName && isInitialised(this.$root, moduleName)) {
         throw new InitError(constructor);
       }
     }
@@ -134,6 +145,7 @@
   /**
    * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
    */
+  GOVUKFrontendComponent.elementType = HTMLElement;
 
   /**
    * Checkboxes component
@@ -157,23 +169,14 @@
      */
     constructor($root) {
       super($root);
-      this.$root = void 0;
       this.$inputs = void 0;
-      if (!($root instanceof HTMLElement)) {
-        throw new ElementError({
-          component: Checkboxes,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      const $inputs = $root.querySelectorAll('input[type="checkbox"]');
+      const $inputs = this.$root.querySelectorAll('input[type="checkbox"]');
       if (!$inputs.length) {
         throw new ElementError({
           component: Checkboxes,
           identifier: 'Form inputs (`<input type="checkbox">`)'
         });
       }
-      this.$root = $root;
       this.$inputs = $inputs;
       this.$inputs.forEach($input => {
         const targetId = $input.getAttribute('data-aria-controls');
diff --git a/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.mjs
index db0837d00..47df6a035 100644
--- a/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.bundle.mjs
@@ -97,19 +97,30 @@ class InitError extends GOVUKFrontendError {
 
 class GOVUKFrontendComponent {
   constructor($root) {
+    this.$root = void 0;
     const childConstructor = this.constructor;
     if (typeof childConstructor.moduleName !== 'string') {
       throw new InitError(`\`moduleName\` not defined in component`);
     }
+    if (!($root instanceof childConstructor.elementType)) {
+      throw new ElementError({
+        element: $root,
+        component: childConstructor,
+        identifier: 'Root element (`$root`)',
+        expectedType: childConstructor.elementType.name
+      });
+    } else {
+      this.$root = $root;
+    }
     childConstructor.checkSupport();
-    this.checkInitialised($root);
+    this.checkInitialised();
     const moduleName = childConstructor.moduleName;
-    $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+    this.$root.setAttribute(`data-${moduleName}-init`, '');
   }
-  checkInitialised($root) {
+  checkInitialised() {
     const constructor = this.constructor;
     const moduleName = constructor.moduleName;
-    if ($root && moduleName && isInitialised($root, moduleName)) {
+    if (moduleName && isInitialised(this.$root, moduleName)) {
       throw new InitError(constructor);
     }
   }
@@ -128,6 +139,7 @@ class GOVUKFrontendComponent {
 /**
  * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
  */
+GOVUKFrontendComponent.elementType = HTMLElement;
 
 /**
  * Checkboxes component
@@ -151,23 +163,14 @@ class Checkboxes extends GOVUKFrontendComponent {
    */
   constructor($root) {
     super($root);
-    this.$root = void 0;
     this.$inputs = void 0;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: Checkboxes,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    const $inputs = $root.querySelectorAll('input[type="checkbox"]');
+    const $inputs = this.$root.querySelectorAll('input[type="checkbox"]');
     if (!$inputs.length) {
       throw new ElementError({
         component: Checkboxes,
         identifier: 'Form inputs (`<input type="checkbox">`)'
       });
     }
-    this.$root = $root;
     this.$inputs = $inputs;
     this.$inputs.forEach($input => {
       const targetId = $input.getAttribute('data-aria-controls');
diff --git a/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.mjs b/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.mjs
index 20ff125b7..455c9c70c 100644
--- a/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/checkboxes/checkboxes.mjs
@@ -23,23 +23,14 @@ class Checkboxes extends GOVUKFrontendComponent {
    */
   constructor($root) {
     super($root);
-    this.$root = void 0;
     this.$inputs = void 0;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: Checkboxes,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    const $inputs = $root.querySelectorAll('input[type="checkbox"]');
+    const $inputs = this.$root.querySelectorAll('input[type="checkbox"]');
     if (!$inputs.length) {
       throw new ElementError({
         component: Checkboxes,
         identifier: 'Form inputs (`<input type="checkbox">`)'
       });
     }
-    this.$root = $root;
     this.$inputs = $inputs;
     this.$inputs.forEach($input => {
       const targetId = $input.getAttribute('data-aria-controls');
diff --git a/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.js b/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.js
index dd6d7f516..7cb5cf05a 100644
--- a/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.js
@@ -222,19 +222,30 @@
 
   class GOVUKFrontendComponent {
     constructor($root) {
+      this.$root = void 0;
       const childConstructor = this.constructor;
       if (typeof childConstructor.moduleName !== 'string') {
         throw new InitError(`\`moduleName\` not defined in component`);
       }
+      if (!($root instanceof childConstructor.elementType)) {
+        throw new ElementError({
+          element: $root,
+          component: childConstructor,
+          identifier: 'Root element (`$root`)',
+          expectedType: childConstructor.elementType.name
+        });
+      } else {
+        this.$root = $root;
+      }
       childConstructor.checkSupport();
-      this.checkInitialised($root);
+      this.checkInitialised();
       const moduleName = childConstructor.moduleName;
-      $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+      this.$root.setAttribute(`data-${moduleName}-init`, '');
     }
-    checkInitialised($root) {
+    checkInitialised() {
       const constructor = this.constructor;
       const moduleName = constructor.moduleName;
-      if ($root && moduleName && isInitialised($root, moduleName)) {
+      if (moduleName && isInitialised(this.$root, moduleName)) {
         throw new InitError(constructor);
       }
     }
@@ -253,6 +264,7 @@
   /**
    * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
    */
+  GOVUKFrontendComponent.elementType = HTMLElement;
 
   /**
    * Error summary component
@@ -269,17 +281,8 @@
      */
     constructor($root, config = {}) {
       super($root);
-      this.$root = void 0;
       this.config = void 0;
-      if (!($root instanceof HTMLElement)) {
-        throw new ElementError({
-          component: ErrorSummary,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      this.$root = $root;
-      this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, $root.dataset));
+      this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, this.$root.dataset));
       if (!this.config.disableAutoFocus) {
         setFocus(this.$root);
       }
diff --git a/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.mjs
index d5cd760d9..02c8b247a 100644
--- a/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.bundle.mjs
@@ -216,19 +216,30 @@ class InitError extends GOVUKFrontendError {
 
 class GOVUKFrontendComponent {
   constructor($root) {
+    this.$root = void 0;
     const childConstructor = this.constructor;
     if (typeof childConstructor.moduleName !== 'string') {
       throw new InitError(`\`moduleName\` not defined in component`);
     }
+    if (!($root instanceof childConstructor.elementType)) {
+      throw new ElementError({
+        element: $root,
+        component: childConstructor,
+        identifier: 'Root element (`$root`)',
+        expectedType: childConstructor.elementType.name
+      });
+    } else {
+      this.$root = $root;
+    }
     childConstructor.checkSupport();
-    this.checkInitialised($root);
+    this.checkInitialised();
     const moduleName = childConstructor.moduleName;
-    $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+    this.$root.setAttribute(`data-${moduleName}-init`, '');
   }
-  checkInitialised($root) {
+  checkInitialised() {
     const constructor = this.constructor;
     const moduleName = constructor.moduleName;
-    if ($root && moduleName && isInitialised($root, moduleName)) {
+    if (moduleName && isInitialised(this.$root, moduleName)) {
       throw new InitError(constructor);
     }
   }
@@ -247,6 +258,7 @@ class GOVUKFrontendComponent {
 /**
  * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
  */
+GOVUKFrontendComponent.elementType = HTMLElement;
 
 /**
  * Error summary component
@@ -263,17 +275,8 @@ class ErrorSummary extends GOVUKFrontendComponent {
    */
   constructor($root, config = {}) {
     super($root);
-    this.$root = void 0;
     this.config = void 0;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: ErrorSummary,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
-    this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, $root.dataset));
+    this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, this.$root.dataset));
     if (!this.config.disableAutoFocus) {
       setFocus(this.$root);
     }
diff --git a/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.mjs b/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.mjs
index 1f60c2395..c3de88214 100644
--- a/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/error-summary/error-summary.mjs
@@ -1,6 +1,5 @@
 import { mergeConfigs, setFocus, getFragmentFromUrl } from '../../common/index.mjs';
 import { normaliseDataset } from '../../common/normalise-dataset.mjs';
-import { ElementError } from '../../errors/index.mjs';
 import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs';
 
 /**
@@ -18,17 +17,8 @@ class ErrorSummary extends GOVUKFrontendComponent {
    */
   constructor($root, config = {}) {
     super($root);
-    this.$root = void 0;
     this.config = void 0;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: ErrorSummary,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
-    this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, $root.dataset));
+    this.config = mergeConfigs(ErrorSummary.defaults, config, normaliseDataset(ErrorSummary, this.$root.dataset));
     if (!this.config.disableAutoFocus) {
       setFocus(this.$root);
     }
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 2c62842b7..25d660df4 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
@@ -192,19 +192,30 @@
 
   class GOVUKFrontendComponent {
     constructor($root) {
+      this.$root = void 0;
       const childConstructor = this.constructor;
       if (typeof childConstructor.moduleName !== 'string') {
         throw new InitError(`\`moduleName\` not defined in component`);
       }
+      if (!($root instanceof childConstructor.elementType)) {
+        throw new ElementError({
+          element: $root,
+          component: childConstructor,
+          identifier: 'Root element (`$root`)',
+          expectedType: childConstructor.elementType.name
+        });
+      } else {
+        this.$root = $root;
+      }
       childConstructor.checkSupport();
-      this.checkInitialised($root);
+      this.checkInitialised();
       const moduleName = childConstructor.moduleName;
-      $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+      this.$root.setAttribute(`data-${moduleName}-init`, '');
     }
-    checkInitialised($root) {
+    checkInitialised() {
       const constructor = this.constructor;
       const moduleName = constructor.moduleName;
-      if ($root && moduleName && isInitialised($root, moduleName)) {
+      if (moduleName && isInitialised(this.$root, moduleName)) {
         throw new InitError(constructor);
       }
     }
@@ -223,6 +234,7 @@
   /**
    * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
    */
+  GOVUKFrontendComponent.elementType = HTMLElement;
 
   class I18n {
     constructor(translations = {}, config = {}) {
@@ -429,7 +441,6 @@
      */
     constructor($root, config = {}) {
       super($root);
-      this.$root = void 0;
       this.config = void 0;
       this.i18n = void 0;
       this.$button = void 0;
@@ -442,14 +453,7 @@
       this.timeoutTime = 5000;
       this.keypressTimeoutId = null;
       this.timeoutMessageId = null;
-      if (!($root instanceof HTMLElement)) {
-        throw new ElementError({
-          component: ExitThisPage,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      const $button = $root.querySelector('.govuk-exit-this-page__button');
+      const $button = this.$root.querySelector('.govuk-exit-this-page__button');
       if (!($button instanceof HTMLAnchorElement)) {
         throw new ElementError({
           component: ExitThisPage,
@@ -458,9 +462,8 @@
           identifier: 'Button (`.govuk-exit-this-page__button`)'
         });
       }
-      this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset(ExitThisPage, $root.dataset));
+      this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset(ExitThisPage, this.$root.dataset));
       this.i18n = new I18n(this.config.i18n);
-      this.$root = $root;
       this.$button = $button;
       const $skiplinkButton = document.querySelector('.govuk-js-exit-this-page-skiplink');
       if ($skiplinkButton instanceof HTMLAnchorElement) {
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 d101649ae..06a7d95dd 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
@@ -186,19 +186,30 @@ class InitError extends GOVUKFrontendError {
 
 class GOVUKFrontendComponent {
   constructor($root) {
+    this.$root = void 0;
     const childConstructor = this.constructor;
     if (typeof childConstructor.moduleName !== 'string') {
       throw new InitError(`\`moduleName\` not defined in component`);
     }
+    if (!($root instanceof childConstructor.elementType)) {
+      throw new ElementError({
+        element: $root,
+        component: childConstructor,
+        identifier: 'Root element (`$root`)',
+        expectedType: childConstructor.elementType.name
+      });
+    } else {
+      this.$root = $root;
+    }
     childConstructor.checkSupport();
-    this.checkInitialised($root);
+    this.checkInitialised();
     const moduleName = childConstructor.moduleName;
-    $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+    this.$root.setAttribute(`data-${moduleName}-init`, '');
   }
-  checkInitialised($root) {
+  checkInitialised() {
     const constructor = this.constructor;
     const moduleName = constructor.moduleName;
-    if ($root && moduleName && isInitialised($root, moduleName)) {
+    if (moduleName && isInitialised(this.$root, moduleName)) {
       throw new InitError(constructor);
     }
   }
@@ -217,6 +228,7 @@ class GOVUKFrontendComponent {
 /**
  * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
  */
+GOVUKFrontendComponent.elementType = HTMLElement;
 
 class I18n {
   constructor(translations = {}, config = {}) {
@@ -423,7 +435,6 @@ class ExitThisPage extends GOVUKFrontendComponent {
    */
   constructor($root, config = {}) {
     super($root);
-    this.$root = void 0;
     this.config = void 0;
     this.i18n = void 0;
     this.$button = void 0;
@@ -436,14 +447,7 @@ class ExitThisPage extends GOVUKFrontendComponent {
     this.timeoutTime = 5000;
     this.keypressTimeoutId = null;
     this.timeoutMessageId = null;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: ExitThisPage,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    const $button = $root.querySelector('.govuk-exit-this-page__button');
+    const $button = this.$root.querySelector('.govuk-exit-this-page__button');
     if (!($button instanceof HTMLAnchorElement)) {
       throw new ElementError({
         component: ExitThisPage,
@@ -452,9 +456,8 @@ class ExitThisPage extends GOVUKFrontendComponent {
         identifier: 'Button (`.govuk-exit-this-page__button`)'
       });
     }
-    this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset(ExitThisPage, $root.dataset));
+    this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset(ExitThisPage, this.$root.dataset));
     this.i18n = new I18n(this.config.i18n);
-    this.$root = $root;
     this.$button = $button;
     const $skiplinkButton = document.querySelector('.govuk-js-exit-this-page-skiplink');
     if ($skiplinkButton instanceof HTMLAnchorElement) {
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 9eb28dba2..e1793c22d 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
@@ -16,7 +16,6 @@ class ExitThisPage extends GOVUKFrontendComponent {
    */
   constructor($root, config = {}) {
     super($root);
-    this.$root = void 0;
     this.config = void 0;
     this.i18n = void 0;
     this.$button = void 0;
@@ -29,14 +28,7 @@ class ExitThisPage extends GOVUKFrontendComponent {
     this.timeoutTime = 5000;
     this.keypressTimeoutId = null;
     this.timeoutMessageId = null;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: ExitThisPage,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    const $button = $root.querySelector('.govuk-exit-this-page__button');
+    const $button = this.$root.querySelector('.govuk-exit-this-page__button');
     if (!($button instanceof HTMLAnchorElement)) {
       throw new ElementError({
         component: ExitThisPage,
@@ -45,9 +37,8 @@ class ExitThisPage extends GOVUKFrontendComponent {
         identifier: 'Button (`.govuk-exit-this-page__button`)'
       });
     }
-    this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset(ExitThisPage, $root.dataset));
+    this.config = mergeConfigs(ExitThisPage.defaults, config, normaliseDataset(ExitThisPage, this.$root.dataset));
     this.i18n = new I18n(this.config.i18n);
-    this.$root = $root;
     this.$button = $button;
     const $skiplinkButton = document.querySelector('.govuk-js-exit-this-page-skiplink');
     if ($skiplinkButton instanceof HTMLAnchorElement) {
diff --git a/packages/govuk-frontend/dist/govuk/components/header/header.bundle.js b/packages/govuk-frontend/dist/govuk/components/header/header.bundle.js
index 87387c539..c006625b5 100644
--- a/packages/govuk-frontend/dist/govuk/components/header/header.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/header/header.bundle.js
@@ -111,19 +111,30 @@
 
   class GOVUKFrontendComponent {
     constructor($root) {
+      this.$root = void 0;
       const childConstructor = this.constructor;
       if (typeof childConstructor.moduleName !== 'string') {
         throw new InitError(`\`moduleName\` not defined in component`);
       }
+      if (!($root instanceof childConstructor.elementType)) {
+        throw new ElementError({
+          element: $root,
+          component: childConstructor,
+          identifier: 'Root element (`$root`)',
+          expectedType: childConstructor.elementType.name
+        });
+      } else {
+        this.$root = $root;
+      }
       childConstructor.checkSupport();
-      this.checkInitialised($root);
+      this.checkInitialised();
       const moduleName = childConstructor.moduleName;
-      $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+      this.$root.setAttribute(`data-${moduleName}-init`, '');
     }
-    checkInitialised($root) {
+    checkInitialised() {
       const constructor = this.constructor;
       const moduleName = constructor.moduleName;
-      if ($root && moduleName && isInitialised($root, moduleName)) {
+      if (moduleName && isInitialised(this.$root, moduleName)) {
         throw new InitError(constructor);
       }
     }
@@ -142,6 +153,7 @@
   /**
    * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
    */
+  GOVUKFrontendComponent.elementType = HTMLElement;
 
   /**
    * Header component
@@ -157,20 +169,11 @@
      */
     constructor($root) {
       super($root);
-      this.$root = void 0;
       this.$menuButton = void 0;
       this.$menu = void 0;
       this.menuIsOpen = false;
       this.mql = null;
-      if (!$root) {
-        throw new ElementError({
-          component: Header,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      this.$root = $root;
-      const $menuButton = $root.querySelector('.govuk-js-header-toggle');
+      const $menuButton = this.$root.querySelector('.govuk-js-header-toggle');
       if (!$menuButton) {
         return this;
       }
diff --git a/packages/govuk-frontend/dist/govuk/components/header/header.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/header/header.bundle.mjs
index 1107d1c8d..56701060a 100644
--- a/packages/govuk-frontend/dist/govuk/components/header/header.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/header/header.bundle.mjs
@@ -105,19 +105,30 @@ class InitError extends GOVUKFrontendError {
 
 class GOVUKFrontendComponent {
   constructor($root) {
+    this.$root = void 0;
     const childConstructor = this.constructor;
     if (typeof childConstructor.moduleName !== 'string') {
       throw new InitError(`\`moduleName\` not defined in component`);
     }
+    if (!($root instanceof childConstructor.elementType)) {
+      throw new ElementError({
+        element: $root,
+        component: childConstructor,
+        identifier: 'Root element (`$root`)',
+        expectedType: childConstructor.elementType.name
+      });
+    } else {
+      this.$root = $root;
+    }
     childConstructor.checkSupport();
-    this.checkInitialised($root);
+    this.checkInitialised();
     const moduleName = childConstructor.moduleName;
-    $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+    this.$root.setAttribute(`data-${moduleName}-init`, '');
   }
-  checkInitialised($root) {
+  checkInitialised() {
     const constructor = this.constructor;
     const moduleName = constructor.moduleName;
-    if ($root && moduleName && isInitialised($root, moduleName)) {
+    if (moduleName && isInitialised(this.$root, moduleName)) {
       throw new InitError(constructor);
     }
   }
@@ -136,6 +147,7 @@ class GOVUKFrontendComponent {
 /**
  * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
  */
+GOVUKFrontendComponent.elementType = HTMLElement;
 
 /**
  * Header component
@@ -151,20 +163,11 @@ class Header extends GOVUKFrontendComponent {
    */
   constructor($root) {
     super($root);
-    this.$root = void 0;
     this.$menuButton = void 0;
     this.$menu = void 0;
     this.menuIsOpen = false;
     this.mql = null;
-    if (!$root) {
-      throw new ElementError({
-        component: Header,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
-    const $menuButton = $root.querySelector('.govuk-js-header-toggle');
+    const $menuButton = this.$root.querySelector('.govuk-js-header-toggle');
     if (!$menuButton) {
       return this;
     }
diff --git a/packages/govuk-frontend/dist/govuk/components/header/header.mjs b/packages/govuk-frontend/dist/govuk/components/header/header.mjs
index e6bbbf295..5b25b2f69 100644
--- a/packages/govuk-frontend/dist/govuk/components/header/header.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/header/header.mjs
@@ -16,20 +16,11 @@ class Header extends GOVUKFrontendComponent {
    */
   constructor($root) {
     super($root);
-    this.$root = void 0;
     this.$menuButton = void 0;
     this.$menu = void 0;
     this.menuIsOpen = false;
     this.mql = null;
-    if (!$root) {
-      throw new ElementError({
-        component: Header,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
-    const $menuButton = $root.querySelector('.govuk-js-header-toggle');
+    const $menuButton = this.$root.querySelector('.govuk-js-header-toggle');
     if (!$menuButton) {
       return this;
     }
diff --git a/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.js b/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.js
index 6e8d9590b..4c34dee8d 100644
--- a/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.js
@@ -216,19 +216,30 @@
 
   class GOVUKFrontendComponent {
     constructor($root) {
+      this.$root = void 0;
       const childConstructor = this.constructor;
       if (typeof childConstructor.moduleName !== 'string') {
         throw new InitError(`\`moduleName\` not defined in component`);
       }
+      if (!($root instanceof childConstructor.elementType)) {
+        throw new ElementError({
+          element: $root,
+          component: childConstructor,
+          identifier: 'Root element (`$root`)',
+          expectedType: childConstructor.elementType.name
+        });
+      } else {
+        this.$root = $root;
+      }
       childConstructor.checkSupport();
-      this.checkInitialised($root);
+      this.checkInitialised();
       const moduleName = childConstructor.moduleName;
-      $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+      this.$root.setAttribute(`data-${moduleName}-init`, '');
     }
-    checkInitialised($root) {
+    checkInitialised() {
       const constructor = this.constructor;
       const moduleName = constructor.moduleName;
-      if ($root && moduleName && isInitialised($root, moduleName)) {
+      if (moduleName && isInitialised(this.$root, moduleName)) {
         throw new InitError(constructor);
       }
     }
@@ -247,6 +258,7 @@
   /**
    * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
    */
+  GOVUKFrontendComponent.elementType = HTMLElement;
 
   /**
    * Notification Banner component
@@ -260,17 +272,8 @@
      */
     constructor($root, config = {}) {
       super($root);
-      this.$root = void 0;
       this.config = void 0;
-      if (!($root instanceof HTMLElement)) {
-        throw new ElementError({
-          component: NotificationBanner,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      this.$root = $root;
-      this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset(NotificationBanner, $root.dataset));
+      this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset(NotificationBanner, this.$root.dataset));
       if (this.$root.getAttribute('role') === 'alert' && !this.config.disableAutoFocus) {
         setFocus(this.$root);
       }
diff --git a/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.mjs
index d276a5b33..85fbd4e71 100644
--- a/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.bundle.mjs
@@ -210,19 +210,30 @@ class InitError extends GOVUKFrontendError {
 
 class GOVUKFrontendComponent {
   constructor($root) {
+    this.$root = void 0;
     const childConstructor = this.constructor;
     if (typeof childConstructor.moduleName !== 'string') {
       throw new InitError(`\`moduleName\` not defined in component`);
     }
+    if (!($root instanceof childConstructor.elementType)) {
+      throw new ElementError({
+        element: $root,
+        component: childConstructor,
+        identifier: 'Root element (`$root`)',
+        expectedType: childConstructor.elementType.name
+      });
+    } else {
+      this.$root = $root;
+    }
     childConstructor.checkSupport();
-    this.checkInitialised($root);
+    this.checkInitialised();
     const moduleName = childConstructor.moduleName;
-    $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+    this.$root.setAttribute(`data-${moduleName}-init`, '');
   }
-  checkInitialised($root) {
+  checkInitialised() {
     const constructor = this.constructor;
     const moduleName = constructor.moduleName;
-    if ($root && moduleName && isInitialised($root, moduleName)) {
+    if (moduleName && isInitialised(this.$root, moduleName)) {
       throw new InitError(constructor);
     }
   }
@@ -241,6 +252,7 @@ class GOVUKFrontendComponent {
 /**
  * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
  */
+GOVUKFrontendComponent.elementType = HTMLElement;
 
 /**
  * Notification Banner component
@@ -254,17 +266,8 @@ class NotificationBanner extends GOVUKFrontendComponent {
    */
   constructor($root, config = {}) {
     super($root);
-    this.$root = void 0;
     this.config = void 0;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: NotificationBanner,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
-    this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset(NotificationBanner, $root.dataset));
+    this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset(NotificationBanner, this.$root.dataset));
     if (this.$root.getAttribute('role') === 'alert' && !this.config.disableAutoFocus) {
       setFocus(this.$root);
     }
diff --git a/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.mjs b/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.mjs
index 576f2b0ae..95f89c435 100644
--- a/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/notification-banner/notification-banner.mjs
@@ -1,6 +1,5 @@
 import { mergeConfigs, setFocus } from '../../common/index.mjs';
 import { normaliseDataset } from '../../common/normalise-dataset.mjs';
-import { ElementError } from '../../errors/index.mjs';
 import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs';
 
 /**
@@ -15,17 +14,8 @@ class NotificationBanner extends GOVUKFrontendComponent {
    */
   constructor($root, config = {}) {
     super($root);
-    this.$root = void 0;
     this.config = void 0;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: NotificationBanner,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
-    this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset(NotificationBanner, $root.dataset));
+    this.config = mergeConfigs(NotificationBanner.defaults, config, normaliseDataset(NotificationBanner, this.$root.dataset));
     if (this.$root.getAttribute('role') === 'alert' && !this.config.disableAutoFocus) {
       setFocus(this.$root);
     }
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 8b54e720e..4804a21bd 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
@@ -197,19 +197,30 @@
 
   class GOVUKFrontendComponent {
     constructor($root) {
+      this.$root = void 0;
       const childConstructor = this.constructor;
       if (typeof childConstructor.moduleName !== 'string') {
         throw new InitError(`\`moduleName\` not defined in component`);
       }
+      if (!($root instanceof childConstructor.elementType)) {
+        throw new ElementError({
+          element: $root,
+          component: childConstructor,
+          identifier: 'Root element (`$root`)',
+          expectedType: childConstructor.elementType.name
+        });
+      } else {
+        this.$root = $root;
+      }
       childConstructor.checkSupport();
-      this.checkInitialised($root);
+      this.checkInitialised();
       const moduleName = childConstructor.moduleName;
-      $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+      this.$root.setAttribute(`data-${moduleName}-init`, '');
     }
-    checkInitialised($root) {
+    checkInitialised() {
       const constructor = this.constructor;
       const moduleName = constructor.moduleName;
-      if ($root && moduleName && isInitialised($root, moduleName)) {
+      if (moduleName && isInitialised(this.$root, moduleName)) {
         throw new InitError(constructor);
       }
     }
@@ -228,6 +239,7 @@
   /**
    * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
    */
+  GOVUKFrontendComponent.elementType = HTMLElement;
 
   class I18n {
     constructor(translations = {}, config = {}) {
@@ -433,21 +445,13 @@
      * @param {PasswordInputConfig} [config] - Password input config
      */
     constructor($root, config = {}) {
-      super();
-      this.$root = void 0;
+      super($root);
       this.config = void 0;
       this.i18n = void 0;
       this.$input = void 0;
       this.$showHideButton = void 0;
       this.$screenReaderStatusMessage = void 0;
-      if (!($root instanceof HTMLElement)) {
-        throw new ElementError({
-          component: PasswordInput,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      const $input = $root.querySelector('.govuk-js-password-input-input');
+      const $input = this.$root.querySelector('.govuk-js-password-input-input');
       if (!($input instanceof HTMLInputElement)) {
         throw new ElementError({
           component: PasswordInput,
@@ -459,7 +463,7 @@
       if ($input.type !== 'password') {
         throw new ElementError('Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.');
       }
-      const $showHideButton = $root.querySelector('.govuk-js-password-input-toggle');
+      const $showHideButton = this.$root.querySelector('.govuk-js-password-input-toggle');
       if (!($showHideButton instanceof HTMLButtonElement)) {
         throw new ElementError({
           component: PasswordInput,
@@ -471,12 +475,11 @@
       if ($showHideButton.type !== 'button') {
         throw new ElementError('Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.');
       }
-      this.$root = $root;
       this.$input = $input;
       this.$showHideButton = $showHideButton;
-      this.config = mergeConfigs(PasswordInput.defaults, config, normaliseDataset(PasswordInput, $root.dataset));
+      this.config = mergeConfigs(PasswordInput.defaults, config, normaliseDataset(PasswordInput, this.$root.dataset));
       this.i18n = new I18n(this.config.i18n, {
-        locale: closestAttributeValue($root, 'lang')
+        locale: closestAttributeValue(this.$root, 'lang')
       });
       this.$showHideButton.removeAttribute('hidden');
       const $screenReaderStatusMessage = document.createElement('div');
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 c9645cd58..ee6f71d5d 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
@@ -191,19 +191,30 @@ class InitError extends GOVUKFrontendError {
 
 class GOVUKFrontendComponent {
   constructor($root) {
+    this.$root = void 0;
     const childConstructor = this.constructor;
     if (typeof childConstructor.moduleName !== 'string') {
       throw new InitError(`\`moduleName\` not defined in component`);
     }
+    if (!($root instanceof childConstructor.elementType)) {
+      throw new ElementError({
+        element: $root,
+        component: childConstructor,
+        identifier: 'Root element (`$root`)',
+        expectedType: childConstructor.elementType.name
+      });
+    } else {
+      this.$root = $root;
+    }
     childConstructor.checkSupport();
-    this.checkInitialised($root);
+    this.checkInitialised();
     const moduleName = childConstructor.moduleName;
-    $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+    this.$root.setAttribute(`data-${moduleName}-init`, '');
   }
-  checkInitialised($root) {
+  checkInitialised() {
     const constructor = this.constructor;
     const moduleName = constructor.moduleName;
-    if ($root && moduleName && isInitialised($root, moduleName)) {
+    if (moduleName && isInitialised(this.$root, moduleName)) {
       throw new InitError(constructor);
     }
   }
@@ -222,6 +233,7 @@ class GOVUKFrontendComponent {
 /**
  * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
  */
+GOVUKFrontendComponent.elementType = HTMLElement;
 
 class I18n {
   constructor(translations = {}, config = {}) {
@@ -427,21 +439,13 @@ class PasswordInput extends GOVUKFrontendComponent {
    * @param {PasswordInputConfig} [config] - Password input config
    */
   constructor($root, config = {}) {
-    super();
-    this.$root = void 0;
+    super($root);
     this.config = void 0;
     this.i18n = void 0;
     this.$input = void 0;
     this.$showHideButton = void 0;
     this.$screenReaderStatusMessage = void 0;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: PasswordInput,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    const $input = $root.querySelector('.govuk-js-password-input-input');
+    const $input = this.$root.querySelector('.govuk-js-password-input-input');
     if (!($input instanceof HTMLInputElement)) {
       throw new ElementError({
         component: PasswordInput,
@@ -453,7 +457,7 @@ class PasswordInput extends GOVUKFrontendComponent {
     if ($input.type !== 'password') {
       throw new ElementError('Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.');
     }
-    const $showHideButton = $root.querySelector('.govuk-js-password-input-toggle');
+    const $showHideButton = this.$root.querySelector('.govuk-js-password-input-toggle');
     if (!($showHideButton instanceof HTMLButtonElement)) {
       throw new ElementError({
         component: PasswordInput,
@@ -465,12 +469,11 @@ class PasswordInput extends GOVUKFrontendComponent {
     if ($showHideButton.type !== 'button') {
       throw new ElementError('Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.');
     }
-    this.$root = $root;
     this.$input = $input;
     this.$showHideButton = $showHideButton;
-    this.config = mergeConfigs(PasswordInput.defaults, config, normaliseDataset(PasswordInput, $root.dataset));
+    this.config = mergeConfigs(PasswordInput.defaults, config, normaliseDataset(PasswordInput, this.$root.dataset));
     this.i18n = new I18n(this.config.i18n, {
-      locale: closestAttributeValue($root, 'lang')
+      locale: closestAttributeValue(this.$root, 'lang')
     });
     this.$showHideButton.removeAttribute('hidden');
     const $screenReaderStatusMessage = document.createElement('div');
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 60a61acfc..46a53d206 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
@@ -16,21 +16,13 @@ class PasswordInput extends GOVUKFrontendComponent {
    * @param {PasswordInputConfig} [config] - Password input config
    */
   constructor($root, config = {}) {
-    super();
-    this.$root = void 0;
+    super($root);
     this.config = void 0;
     this.i18n = void 0;
     this.$input = void 0;
     this.$showHideButton = void 0;
     this.$screenReaderStatusMessage = void 0;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: PasswordInput,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    const $input = $root.querySelector('.govuk-js-password-input-input');
+    const $input = this.$root.querySelector('.govuk-js-password-input-input');
     if (!($input instanceof HTMLInputElement)) {
       throw new ElementError({
         component: PasswordInput,
@@ -42,7 +34,7 @@ class PasswordInput extends GOVUKFrontendComponent {
     if ($input.type !== 'password') {
       throw new ElementError('Password input: Form field (`.govuk-js-password-input-input`) must be of type `password`.');
     }
-    const $showHideButton = $root.querySelector('.govuk-js-password-input-toggle');
+    const $showHideButton = this.$root.querySelector('.govuk-js-password-input-toggle');
     if (!($showHideButton instanceof HTMLButtonElement)) {
       throw new ElementError({
         component: PasswordInput,
@@ -54,12 +46,11 @@ class PasswordInput extends GOVUKFrontendComponent {
     if ($showHideButton.type !== 'button') {
       throw new ElementError('Password input: Button (`.govuk-js-password-input-toggle`) must be of type `button`.');
     }
-    this.$root = $root;
     this.$input = $input;
     this.$showHideButton = $showHideButton;
-    this.config = mergeConfigs(PasswordInput.defaults, config, normaliseDataset(PasswordInput, $root.dataset));
+    this.config = mergeConfigs(PasswordInput.defaults, config, normaliseDataset(PasswordInput, this.$root.dataset));
     this.i18n = new I18n(this.config.i18n, {
-      locale: closestAttributeValue($root, 'lang')
+      locale: closestAttributeValue(this.$root, 'lang')
     });
     this.$showHideButton.removeAttribute('hidden');
     const $screenReaderStatusMessage = document.createElement('div');
diff --git a/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.js b/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.js
index 40ea3fa82..da0305b37 100644
--- a/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.js
@@ -103,19 +103,30 @@
 
   class GOVUKFrontendComponent {
     constructor($root) {
+      this.$root = void 0;
       const childConstructor = this.constructor;
       if (typeof childConstructor.moduleName !== 'string') {
         throw new InitError(`\`moduleName\` not defined in component`);
       }
+      if (!($root instanceof childConstructor.elementType)) {
+        throw new ElementError({
+          element: $root,
+          component: childConstructor,
+          identifier: 'Root element (`$root`)',
+          expectedType: childConstructor.elementType.name
+        });
+      } else {
+        this.$root = $root;
+      }
       childConstructor.checkSupport();
-      this.checkInitialised($root);
+      this.checkInitialised();
       const moduleName = childConstructor.moduleName;
-      $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+      this.$root.setAttribute(`data-${moduleName}-init`, '');
     }
-    checkInitialised($root) {
+    checkInitialised() {
       const constructor = this.constructor;
       const moduleName = constructor.moduleName;
-      if ($root && moduleName && isInitialised($root, moduleName)) {
+      if (moduleName && isInitialised(this.$root, moduleName)) {
         throw new InitError(constructor);
       }
     }
@@ -134,6 +145,7 @@
   /**
    * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
    */
+  GOVUKFrontendComponent.elementType = HTMLElement;
 
   /**
    * Radios component
@@ -157,23 +169,14 @@
      */
     constructor($root) {
       super($root);
-      this.$root = void 0;
       this.$inputs = void 0;
-      if (!($root instanceof HTMLElement)) {
-        throw new ElementError({
-          component: Radios,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      const $inputs = $root.querySelectorAll('input[type="radio"]');
+      const $inputs = this.$root.querySelectorAll('input[type="radio"]');
       if (!$inputs.length) {
         throw new ElementError({
           component: Radios,
           identifier: 'Form inputs (`<input type="radio">`)'
         });
       }
-      this.$root = $root;
       this.$inputs = $inputs;
       this.$inputs.forEach($input => {
         const targetId = $input.getAttribute('data-aria-controls');
diff --git a/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.mjs
index 71b576f0e..a8677e325 100644
--- a/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/radios/radios.bundle.mjs
@@ -97,19 +97,30 @@ class InitError extends GOVUKFrontendError {
 
 class GOVUKFrontendComponent {
   constructor($root) {
+    this.$root = void 0;
     const childConstructor = this.constructor;
     if (typeof childConstructor.moduleName !== 'string') {
       throw new InitError(`\`moduleName\` not defined in component`);
     }
+    if (!($root instanceof childConstructor.elementType)) {
+      throw new ElementError({
+        element: $root,
+        component: childConstructor,
+        identifier: 'Root element (`$root`)',
+        expectedType: childConstructor.elementType.name
+      });
+    } else {
+      this.$root = $root;
+    }
     childConstructor.checkSupport();
-    this.checkInitialised($root);
+    this.checkInitialised();
     const moduleName = childConstructor.moduleName;
-    $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+    this.$root.setAttribute(`data-${moduleName}-init`, '');
   }
-  checkInitialised($root) {
+  checkInitialised() {
     const constructor = this.constructor;
     const moduleName = constructor.moduleName;
-    if ($root && moduleName && isInitialised($root, moduleName)) {
+    if (moduleName && isInitialised(this.$root, moduleName)) {
       throw new InitError(constructor);
     }
   }
@@ -128,6 +139,7 @@ class GOVUKFrontendComponent {
 /**
  * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
  */
+GOVUKFrontendComponent.elementType = HTMLElement;
 
 /**
  * Radios component
@@ -151,23 +163,14 @@ class Radios extends GOVUKFrontendComponent {
    */
   constructor($root) {
     super($root);
-    this.$root = void 0;
     this.$inputs = void 0;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: Radios,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    const $inputs = $root.querySelectorAll('input[type="radio"]');
+    const $inputs = this.$root.querySelectorAll('input[type="radio"]');
     if (!$inputs.length) {
       throw new ElementError({
         component: Radios,
         identifier: 'Form inputs (`<input type="radio">`)'
       });
     }
-    this.$root = $root;
     this.$inputs = $inputs;
     this.$inputs.forEach($input => {
       const targetId = $input.getAttribute('data-aria-controls');
diff --git a/packages/govuk-frontend/dist/govuk/components/radios/radios.mjs b/packages/govuk-frontend/dist/govuk/components/radios/radios.mjs
index 4839009f2..55f6eb727 100644
--- a/packages/govuk-frontend/dist/govuk/components/radios/radios.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/radios/radios.mjs
@@ -23,23 +23,14 @@ class Radios extends GOVUKFrontendComponent {
    */
   constructor($root) {
     super($root);
-    this.$root = void 0;
     this.$inputs = void 0;
-    if (!($root instanceof HTMLElement)) {
-      throw new ElementError({
-        component: Radios,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    const $inputs = $root.querySelectorAll('input[type="radio"]');
+    const $inputs = this.$root.querySelectorAll('input[type="radio"]');
     if (!$inputs.length) {
       throw new ElementError({
         component: Radios,
         identifier: 'Form inputs (`<input type="radio">`)'
       });
     }
-    this.$root = $root;
     this.$inputs = $inputs;
     this.$inputs.forEach($input => {
       const targetId = $input.getAttribute('data-aria-controls');
diff --git a/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.js b/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.js
index 199dfe565..56d34ddb0 100644
--- a/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.js
@@ -111,19 +111,30 @@
 
   class GOVUKFrontendComponent {
     constructor($root) {
+      this.$root = void 0;
       const childConstructor = this.constructor;
       if (typeof childConstructor.moduleName !== 'string') {
         throw new InitError(`\`moduleName\` not defined in component`);
       }
+      if (!($root instanceof childConstructor.elementType)) {
+        throw new ElementError({
+          element: $root,
+          component: childConstructor,
+          identifier: 'Root element (`$root`)',
+          expectedType: childConstructor.elementType.name
+        });
+      } else {
+        this.$root = $root;
+      }
       childConstructor.checkSupport();
-      this.checkInitialised($root);
+      this.checkInitialised();
       const moduleName = childConstructor.moduleName;
-      $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+      this.$root.setAttribute(`data-${moduleName}-init`, '');
     }
-    checkInitialised($root) {
+    checkInitialised() {
       const constructor = this.constructor;
       const moduleName = constructor.moduleName;
-      if ($root && moduleName && isInitialised($root, moduleName)) {
+      if (moduleName && isInitialised(this.$root, moduleName)) {
         throw new InitError(constructor);
       }
     }
@@ -142,6 +153,7 @@
   /**
    * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
    */
+  GOVUKFrontendComponent.elementType = HTMLElement;
 
   /**
    * Service Navigation component
@@ -153,21 +165,12 @@
      * @param {Element | null} $root - HTML element to use for header
      */
     constructor($root) {
-      super();
-      this.$root = void 0;
+      super($root);
       this.$menuButton = void 0;
       this.$menu = void 0;
       this.menuIsOpen = false;
       this.mql = null;
-      if (!$root) {
-        throw new ElementError({
-          component: ServiceNavigation,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      this.$root = $root;
-      const $menuButton = $root.querySelector('.govuk-js-service-navigation-toggle');
+      const $menuButton = this.$root.querySelector('.govuk-js-service-navigation-toggle');
       if (!$menuButton) {
         return this;
       }
diff --git a/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.mjs
index d524d126e..ddd07f2e1 100644
--- a/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.bundle.mjs
@@ -105,19 +105,30 @@ class InitError extends GOVUKFrontendError {
 
 class GOVUKFrontendComponent {
   constructor($root) {
+    this.$root = void 0;
     const childConstructor = this.constructor;
     if (typeof childConstructor.moduleName !== 'string') {
       throw new InitError(`\`moduleName\` not defined in component`);
     }
+    if (!($root instanceof childConstructor.elementType)) {
+      throw new ElementError({
+        element: $root,
+        component: childConstructor,
+        identifier: 'Root element (`$root`)',
+        expectedType: childConstructor.elementType.name
+      });
+    } else {
+      this.$root = $root;
+    }
     childConstructor.checkSupport();
-    this.checkInitialised($root);
+    this.checkInitialised();
     const moduleName = childConstructor.moduleName;
-    $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+    this.$root.setAttribute(`data-${moduleName}-init`, '');
   }
-  checkInitialised($root) {
+  checkInitialised() {
     const constructor = this.constructor;
     const moduleName = constructor.moduleName;
-    if ($root && moduleName && isInitialised($root, moduleName)) {
+    if (moduleName && isInitialised(this.$root, moduleName)) {
       throw new InitError(constructor);
     }
   }
@@ -136,6 +147,7 @@ class GOVUKFrontendComponent {
 /**
  * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
  */
+GOVUKFrontendComponent.elementType = HTMLElement;
 
 /**
  * Service Navigation component
@@ -147,21 +159,12 @@ class ServiceNavigation extends GOVUKFrontendComponent {
    * @param {Element | null} $root - HTML element to use for header
    */
   constructor($root) {
-    super();
-    this.$root = void 0;
+    super($root);
     this.$menuButton = void 0;
     this.$menu = void 0;
     this.menuIsOpen = false;
     this.mql = null;
-    if (!$root) {
-      throw new ElementError({
-        component: ServiceNavigation,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
-    const $menuButton = $root.querySelector('.govuk-js-service-navigation-toggle');
+    const $menuButton = this.$root.querySelector('.govuk-js-service-navigation-toggle');
     if (!$menuButton) {
       return this;
     }
diff --git a/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.mjs b/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.mjs
index 9a100a87a..37033c8e4 100644
--- a/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/service-navigation/service-navigation.mjs
@@ -12,21 +12,12 @@ class ServiceNavigation extends GOVUKFrontendComponent {
    * @param {Element | null} $root - HTML element to use for header
    */
   constructor($root) {
-    super();
-    this.$root = void 0;
+    super($root);
     this.$menuButton = void 0;
     this.$menu = void 0;
     this.menuIsOpen = false;
     this.mql = null;
-    if (!$root) {
-      throw new ElementError({
-        component: ServiceNavigation,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
-    const $menuButton = $root.querySelector('.govuk-js-service-navigation-toggle');
+    const $menuButton = this.$root.querySelector('.govuk-js-service-navigation-toggle');
     if (!$menuButton) {
       return this;
     }
diff --git a/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.js b/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.js
index 0dc097245..21a4211f6 100644
--- a/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.js
@@ -133,19 +133,30 @@
 
   class GOVUKFrontendComponent {
     constructor($root) {
+      this.$root = void 0;
       const childConstructor = this.constructor;
       if (typeof childConstructor.moduleName !== 'string') {
         throw new InitError(`\`moduleName\` not defined in component`);
       }
+      if (!($root instanceof childConstructor.elementType)) {
+        throw new ElementError({
+          element: $root,
+          component: childConstructor,
+          identifier: 'Root element (`$root`)',
+          expectedType: childConstructor.elementType.name
+        });
+      } else {
+        this.$root = $root;
+      }
       childConstructor.checkSupport();
-      this.checkInitialised($root);
+      this.checkInitialised();
       const moduleName = childConstructor.moduleName;
-      $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+      this.$root.setAttribute(`data-${moduleName}-init`, '');
     }
-    checkInitialised($root) {
+    checkInitialised() {
       const constructor = this.constructor;
       const moduleName = constructor.moduleName;
-      if ($root && moduleName && isInitialised($root, moduleName)) {
+      if (moduleName && isInitialised(this.$root, moduleName)) {
         throw new InitError(constructor);
       }
     }
@@ -164,11 +175,13 @@
   /**
    * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
    */
+  GOVUKFrontendComponent.elementType = HTMLElement;
 
   /**
    * Skip link component
    *
    * @preserve
+   * @augments GOVUKFrontendComponent<HTMLAnchorElement>
    */
   class SkipLink extends GOVUKFrontendComponent {
     /**
@@ -180,16 +193,6 @@
     constructor($root) {
       var _this$$root$getAttrib;
       super($root);
-      this.$root = void 0;
-      if (!($root instanceof HTMLAnchorElement)) {
-        throw new ElementError({
-          component: SkipLink,
-          element: $root,
-          expectedType: 'HTMLAnchorElement',
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      this.$root = $root;
       const hash = this.$root.hash;
       const href = (_this$$root$getAttrib = this.$root.getAttribute('href')) != null ? _this$$root$getAttrib : '';
       let url;
@@ -223,6 +226,7 @@
       }));
     }
   }
+  SkipLink.elementType = HTMLAnchorElement;
   SkipLink.moduleName = 'govuk-skip-link';
 
   exports.SkipLink = SkipLink;
diff --git a/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.mjs
index 3a7efc940..231c4e259 100644
--- a/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.bundle.mjs
@@ -127,19 +127,30 @@ class InitError extends GOVUKFrontendError {
 
 class GOVUKFrontendComponent {
   constructor($root) {
+    this.$root = void 0;
     const childConstructor = this.constructor;
     if (typeof childConstructor.moduleName !== 'string') {
       throw new InitError(`\`moduleName\` not defined in component`);
     }
+    if (!($root instanceof childConstructor.elementType)) {
+      throw new ElementError({
+        element: $root,
+        component: childConstructor,
+        identifier: 'Root element (`$root`)',
+        expectedType: childConstructor.elementType.name
+      });
+    } else {
+      this.$root = $root;
+    }
     childConstructor.checkSupport();
-    this.checkInitialised($root);
+    this.checkInitialised();
     const moduleName = childConstructor.moduleName;
-    $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+    this.$root.setAttribute(`data-${moduleName}-init`, '');
   }
-  checkInitialised($root) {
+  checkInitialised() {
     const constructor = this.constructor;
     const moduleName = constructor.moduleName;
-    if ($root && moduleName && isInitialised($root, moduleName)) {
+    if (moduleName && isInitialised(this.$root, moduleName)) {
       throw new InitError(constructor);
     }
   }
@@ -158,11 +169,13 @@ class GOVUKFrontendComponent {
 /**
  * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
  */
+GOVUKFrontendComponent.elementType = HTMLElement;
 
 /**
  * Skip link component
  *
  * @preserve
+ * @augments GOVUKFrontendComponent<HTMLAnchorElement>
  */
 class SkipLink extends GOVUKFrontendComponent {
   /**
@@ -174,16 +187,6 @@ class SkipLink extends GOVUKFrontendComponent {
   constructor($root) {
     var _this$$root$getAttrib;
     super($root);
-    this.$root = void 0;
-    if (!($root instanceof HTMLAnchorElement)) {
-      throw new ElementError({
-        component: SkipLink,
-        element: $root,
-        expectedType: 'HTMLAnchorElement',
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
     const hash = this.$root.hash;
     const href = (_this$$root$getAttrib = this.$root.getAttribute('href')) != null ? _this$$root$getAttrib : '';
     let url;
@@ -217,6 +220,7 @@ class SkipLink extends GOVUKFrontendComponent {
     }));
   }
 }
+SkipLink.elementType = HTMLAnchorElement;
 SkipLink.moduleName = 'govuk-skip-link';
 
 export { SkipLink };
diff --git a/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.mjs b/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.mjs
index f433b5876..fa814a73f 100644
--- a/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/skip-link/skip-link.mjs
@@ -6,6 +6,7 @@ import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs';
  * Skip link component
  *
  * @preserve
+ * @augments GOVUKFrontendComponent<HTMLAnchorElement>
  */
 class SkipLink extends GOVUKFrontendComponent {
   /**
@@ -17,16 +18,6 @@ class SkipLink extends GOVUKFrontendComponent {
   constructor($root) {
     var _this$$root$getAttrib;
     super($root);
-    this.$root = void 0;
-    if (!($root instanceof HTMLAnchorElement)) {
-      throw new ElementError({
-        component: SkipLink,
-        element: $root,
-        expectedType: 'HTMLAnchorElement',
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    this.$root = $root;
     const hash = this.$root.hash;
     const href = (_this$$root$getAttrib = this.$root.getAttribute('href')) != null ? _this$$root$getAttrib : '';
     let url;
@@ -60,6 +51,7 @@ class SkipLink extends GOVUKFrontendComponent {
     }));
   }
 }
+SkipLink.elementType = HTMLAnchorElement;
 SkipLink.moduleName = 'govuk-skip-link';
 
 export { SkipLink };
diff --git a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js
index e636ee5f6..aaf2c261b 100644
--- a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js
@@ -117,19 +117,30 @@
 
   class GOVUKFrontendComponent {
     constructor($root) {
+      this.$root = void 0;
       const childConstructor = this.constructor;
       if (typeof childConstructor.moduleName !== 'string') {
         throw new InitError(`\`moduleName\` not defined in component`);
       }
+      if (!($root instanceof childConstructor.elementType)) {
+        throw new ElementError({
+          element: $root,
+          component: childConstructor,
+          identifier: 'Root element (`$root`)',
+          expectedType: childConstructor.elementType.name
+        });
+      } else {
+        this.$root = $root;
+      }
       childConstructor.checkSupport();
-      this.checkInitialised($root);
+      this.checkInitialised();
       const moduleName = childConstructor.moduleName;
-      $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+      this.$root.setAttribute(`data-${moduleName}-init`, '');
     }
-    checkInitialised($root) {
+    checkInitialised() {
       const constructor = this.constructor;
       const moduleName = constructor.moduleName;
-      if ($root && moduleName && isInitialised($root, moduleName)) {
+      if (moduleName && isInitialised(this.$root, moduleName)) {
         throw new InitError(constructor);
       }
     }
@@ -148,6 +159,7 @@
   /**
    * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
    */
+  GOVUKFrontendComponent.elementType = HTMLElement;
 
   /**
    * Tabs component
@@ -160,7 +172,6 @@
      */
     constructor($root) {
       super($root);
-      this.$root = void 0;
       this.$tabs = void 0;
       this.$tabList = void 0;
       this.$tabListItems = void 0;
@@ -170,21 +181,13 @@
       this.boundTabKeydown = void 0;
       this.boundOnHashChange = void 0;
       this.mql = null;
-      if (!$root) {
-        throw new ElementError({
-          component: Tabs,
-          element: $root,
-          identifier: 'Root element (`$root`)'
-        });
-      }
-      const $tabs = $root.querySelectorAll('a.govuk-tabs__tab');
+      const $tabs = this.$root.querySelectorAll('a.govuk-tabs__tab');
       if (!$tabs.length) {
         throw new ElementError({
           component: Tabs,
           identifier: 'Links (`<a class="govuk-tabs__tab">`)'
         });
       }
-      this.$root = $root;
       this.$tabs = $tabs;
       this.boundTabClick = this.onTabClick.bind(this);
       this.boundTabKeydown = this.onTabKeydown.bind(this);
diff --git a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs
index 238bc76ac..ccb20e7c2 100644
--- a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs
@@ -111,19 +111,30 @@ class InitError extends GOVUKFrontendError {
 
 class GOVUKFrontendComponent {
   constructor($root) {
+    this.$root = void 0;
     const childConstructor = this.constructor;
     if (typeof childConstructor.moduleName !== 'string') {
       throw new InitError(`\`moduleName\` not defined in component`);
     }
+    if (!($root instanceof childConstructor.elementType)) {
+      throw new ElementError({
+        element: $root,
+        component: childConstructor,
+        identifier: 'Root element (`$root`)',
+        expectedType: childConstructor.elementType.name
+      });
+    } else {
+      this.$root = $root;
+    }
     childConstructor.checkSupport();
-    this.checkInitialised($root);
+    this.checkInitialised();
     const moduleName = childConstructor.moduleName;
-    $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+    this.$root.setAttribute(`data-${moduleName}-init`, '');
   }
-  checkInitialised($root) {
+  checkInitialised() {
     const constructor = this.constructor;
     const moduleName = constructor.moduleName;
-    if ($root && moduleName && isInitialised($root, moduleName)) {
+    if (moduleName && isInitialised(this.$root, moduleName)) {
       throw new InitError(constructor);
     }
   }
@@ -142,6 +153,7 @@ class GOVUKFrontendComponent {
 /**
  * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
  */
+GOVUKFrontendComponent.elementType = HTMLElement;
 
 /**
  * Tabs component
@@ -154,7 +166,6 @@ class Tabs extends GOVUKFrontendComponent {
    */
   constructor($root) {
     super($root);
-    this.$root = void 0;
     this.$tabs = void 0;
     this.$tabList = void 0;
     this.$tabListItems = void 0;
@@ -164,21 +175,13 @@ class Tabs extends GOVUKFrontendComponent {
     this.boundTabKeydown = void 0;
     this.boundOnHashChange = void 0;
     this.mql = null;
-    if (!$root) {
-      throw new ElementError({
-        component: Tabs,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    const $tabs = $root.querySelectorAll('a.govuk-tabs__tab');
+    const $tabs = this.$root.querySelectorAll('a.govuk-tabs__tab');
     if (!$tabs.length) {
       throw new ElementError({
         component: Tabs,
         identifier: 'Links (`<a class="govuk-tabs__tab">`)'
       });
     }
-    this.$root = $root;
     this.$tabs = $tabs;
     this.boundTabClick = this.onTabClick.bind(this);
     this.boundTabKeydown = this.onTabKeydown.bind(this);
diff --git a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.mjs b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.mjs
index 642383117..f42ab670a 100644
--- a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.mjs
@@ -13,7 +13,6 @@ class Tabs extends GOVUKFrontendComponent {
    */
   constructor($root) {
     super($root);
-    this.$root = void 0;
     this.$tabs = void 0;
     this.$tabList = void 0;
     this.$tabListItems = void 0;
@@ -23,21 +22,13 @@ class Tabs extends GOVUKFrontendComponent {
     this.boundTabKeydown = void 0;
     this.boundOnHashChange = void 0;
     this.mql = null;
-    if (!$root) {
-      throw new ElementError({
-        component: Tabs,
-        element: $root,
-        identifier: 'Root element (`$root`)'
-      });
-    }
-    const $tabs = $root.querySelectorAll('a.govuk-tabs__tab');
+    const $tabs = this.$root.querySelectorAll('a.govuk-tabs__tab');
     if (!$tabs.length) {
       throw new ElementError({
         component: Tabs,
         identifier: 'Links (`<a class="govuk-tabs__tab">`)'
       });
     }
-    this.$root = $root;
     this.$tabs = $tabs;
     this.boundTabClick = this.onTabClick.bind(this);
     this.boundTabKeydown = this.onTabKeydown.bind(this);
diff --git a/packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs b/packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs
index 2e5786db9..d76befba4 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs
@@ -1,21 +1,32 @@
 import { isInitialised, isSupported } from './common/index.mjs';
-import { InitError, SupportError } from './errors/index.mjs';
+import { InitError, ElementError, SupportError } from './errors/index.mjs';
 
 class GOVUKFrontendComponent {
   constructor($root) {
+    this.$root = void 0;
     const childConstructor = this.constructor;
     if (typeof childConstructor.moduleName !== 'string') {
       throw new InitError(`\`moduleName\` not defined in component`);
     }
+    if (!($root instanceof childConstructor.elementType)) {
+      throw new ElementError({
+        element: $root,
+        component: childConstructor,
+        identifier: 'Root element (`$root`)',
+        expectedType: childConstructor.elementType.name
+      });
+    } else {
+      this.$root = $root;
+    }
     childConstructor.checkSupport();
-    this.checkInitialised($root);
+    this.checkInitialised();
     const moduleName = childConstructor.moduleName;
-    $root == null || $root.setAttribute(`data-${moduleName}-init`, '');
+    this.$root.setAttribute(`data-${moduleName}-init`, '');
   }
-  checkInitialised($root) {
+  checkInitialised() {
     const constructor = this.constructor;
     const moduleName = constructor.moduleName;
-    if ($root && moduleName && isInitialised($root, moduleName)) {
+    if (moduleName && isInitialised(this.$root, moduleName)) {
       throw new InitError(constructor);
     }
   }
@@ -34,6 +45,7 @@ class GOVUKFrontendComponent {
 /**
  * @typedef {typeof GOVUKFrontendComponent & ChildClass} ChildClassConstructor
  */
+GOVUKFrontendComponent.elementType = HTMLElement;
 
 export { GOVUKFrontendComponent };
 //# sourceMappingURL=govuk-frontend-component.mjs.map

Action run for 14962d4

Copy link
Member

@romaricpascal romaricpascal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's going in the right direction, dropped a few thoughts on issues I noticed as well as suggestions 😊

@@ -8,27 +8,43 @@ import { InitError, SupportError } from './errors/index.mjs'
*
* @internal
* @virtual
* @template [T=HTMLElement]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question That's a new syntax to me. Is there a difference with @template {HTMLElement} T?

suggestion Could we name the T as RootElementType as it needs to be refered in other places than the same block of documentation?

Copy link
Contributor Author

@patrickpatrickpatrick patrickpatrickpatrick Sep 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@template {type} T means that type acts as a constraint on what T is, whereas @template [T=type] means type is the default type of T and can be overloaded (so it's GOVUKFrontendComponent<T=HTMLElement>.

Ah yeah, renaming T makes sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah cool, thanks for the explanation, wasn't aware you could give a default without constraining the type. We may need to constrain the type, though, as we call setAttribute in the constructor, which requires the type at least to offer that (so be an Element at least)?

@@ -110,26 +107,16 @@ export class Accordion extends GOVUKFrontendComponent {
$showAllText = null

/**
* @param {Element | null} $root - HTML element to use for accordion
* @param {HTMLElement | null} $root - HTML element to use for accordion
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue I think this will cause issue with the createAll function or more generally document.querySelectorAll as the later returns Element (unless the selector is a tag name, but we use [data-module="..."] in createAll).

Could the child class constructors be kept as Element?

/**
* Constructs a new component, validating that GOV.UK Frontend is supported
*
* @internal
* @param {Element | null} [$root] - HTML element to use for component
* @param {T | null} [$root] - HTML element to use for component
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue Could this be kept as Element as well, for a similar reason as the comment on the Accordion class.

*/
constructor($root) {
const childConstructor = /** @type {ChildClassConstructor} */ (
this.constructor
)

if (!($root instanceof HTMLElement)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue We'll need the HTMLElement in that check to be provided by child classes (as a static property) so we can ensure that SkipLink indeed receives an HTMLAnchorElement.

In the current state, if someone sets data-module="govuk-skip-link" on a <div>, for example, the component would initialise properly as TypeScript doesn't add any checks for when the code runs.

} else {
this.$root = $root
}

childConstructor.checkSupport()

this.checkInitialised($root)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question I think we have a choice to make as to which feature runs first between checking initialisation and storing the root:

  1. If we store the root first, checkInitialised can then benefit from having the root stored and not receive an argument

  2. If we check initialisation first, we need to keep the argument, but we'll only store the root if we're actually initialising the component.

  3. feels the more natural order to me, what are your thoughts?

childConstructor.checkSupport()

this.checkInitialised($root)

const moduleName = childConstructor.moduleName

if (typeof moduleName === 'string') {
moduleName && $root?.setAttribute(`data-${moduleName}-init`, '')
moduleName && $root.setAttribute(`data-${moduleName}-init`, '')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion Using this.$root here will allow to drop the ?. in the same way, but give us flexibility to refactor the storage of the root and/or the marking as initialised in a method if the constructor starts becoming too big.

@@ -64,7 +80,7 @@ export class GOVUKFrontendComponent {

/**
* @typedef ChildClass
* @property {string} [moduleName] - The module name that'll be looked for in the DOM when initialising the component
* @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question Do we need to do this because the typeof moduleName comes later in the constructor than the throwing of the ElementError which requires component to have a moduleName attribute?

If that's the case, it could be a sign we need to reorder some code in the constructor (eg. throw an InitError early) 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the InitError is moved before the ElementError, there's still complaining about the ElementError not having a valid component because ElementError expects a component with a defined moduleName. This is the case even if throwing the ElementError is wrapped in a if statement that checks that type of moduleName is not undefined.

@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-5354 September 26, 2024 17:04 Inactive
@patrickpatrickpatrick patrickpatrickpatrick force-pushed the refactor-check-in-govuk-frontend-component branch from bd4efe6 to ac1e9bd Compare September 27, 2024 13:25
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-5354 September 27, 2024 13:25 Inactive
@patrickpatrickpatrick patrickpatrickpatrick force-pushed the refactor-check-in-govuk-frontend-component branch from ac1e9bd to 1a5e324 Compare September 27, 2024 15:17
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-5354 September 27, 2024 15:17 Inactive
@patrickpatrickpatrick patrickpatrickpatrick changed the title refactor check in govuk frontend component Refactor the root type check in GOVUKFrontendComponent Sep 27, 2024
@patrickpatrickpatrick patrickpatrickpatrick force-pushed the refactor-check-in-govuk-frontend-component branch from 1a5e324 to 61ea385 Compare September 30, 2024 10:02
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-5354 September 30, 2024 10:03 Inactive
@patrickpatrickpatrick patrickpatrickpatrick force-pushed the refactor-check-in-govuk-frontend-component branch from 61ea385 to daee146 Compare September 30, 2024 10:20
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-5354 September 30, 2024 10:21 Inactive
@patrickpatrickpatrick patrickpatrickpatrick marked this pull request as ready for review October 2, 2024 15:39
@patrickpatrickpatrick patrickpatrickpatrick force-pushed the refactor-check-in-govuk-frontend-component branch from daee146 to b948582 Compare October 2, 2024 15:40
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-5354 October 2, 2024 15:40 Inactive
@patrickpatrickpatrick patrickpatrickpatrick force-pushed the refactor-check-in-govuk-frontend-component branch from b948582 to e016d43 Compare October 2, 2024 15:45
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-5354 October 2, 2024 15:46 Inactive
@patrickpatrickpatrick
Copy link
Contributor Author

Rollup is throwing an error during compilation that it doesn't know what HTMLElement is as defined in line 16 of govuk-frontend-component.mjs. I'm not sure if this means we need to use a library like jsdom but this isn't ideal.

@romaricpascal
Copy link
Member

Rollup is throwing an error during compilation that it doesn't know what HTMLElement is as defined in line 16 of govuk-frontend-component.mjs. I'm not sure if this means we need to use a library like jsdom but this isn't ideal.

After investigating with @patrickpatrickpatrick, we found out that the issue is because we import GOVUKFrontend in rollup.release.config.mjs.

Before, it was fine because any DOM APIs (like HTMLElement) were only used within the component's methods, so not executed when the file was imported. With the changes of this PR, HTMLElement and HTMLAnchorElement are now set as static properties on classes, which is code that'll run at import... except in Node, there's no DOM APIs.

This import is used to help us set a list of reserved words for Terser to prevent the mangling of the exports of GOV.UK Frontend. This makes stack traces clearer when using the minified file (having Accordion referenced as 'Accordion', rather than whatever letter Terser transformed the name to for minification).

We now need to find a solution for getting that list (ideally without maintaining it manually).

@patrickpatrickpatrick patrickpatrickpatrick force-pushed the refactor-check-in-govuk-frontend-component branch from e016d43 to 9460779 Compare October 4, 2024 12:39
@patrickpatrickpatrick patrickpatrickpatrick force-pushed the refactor-check-in-govuk-frontend-component branch from 9460779 to 1dcbbef Compare October 4, 2024 13:34
@patrickpatrickpatrick patrickpatrickpatrick force-pushed the refactor-check-in-govuk-frontend-component branch from 1dcbbef to dee4e44 Compare October 4, 2024 13:39
Copy link
Member

@romaricpascal romaricpascal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat that our issues with Rollup are resolved, thanks for implementing a solution 🙌🏻

This is going in the right direction, as well, there's just a couple of bits that need tweaking before we can merge it.

Main one is that quite a few component only had $root removed from their instance properties, but kept the if($root instanceof ...) check and/or this.$root = root assignment in their constructor. As this is now handled by GOVUKFrontendComponent, we can remove that from their constructor and let it be handled by GOVUKFrontendComponent. This should solve the couple of @augments GOVUKFrontendComponent<Element> that had to be added on some of them.

This means that a couple of components will move from if (!$root) to if($root instanceof HTMLElement), but this should be fine given they're meant to be instanciated on HTMLElement anyways. If this becomes an issue, we can ship a patch where we use elementType = Element for those.

@@ -17,11 +17,9 @@ import { I18n } from '../../i18n.mjs'
* attribute, which also provides accessibility.
*
* @preserve
* @augments GOVUKFrontendComponent
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question The commit message mentions "Add augments when component requires typing of the root to be set", does this mean we can we do without the @augments for components that don't need a custom elementType? If they're not necessary, we may as well drop them.

export default defineConfig(({ i: input }) => ({
input,
export default async (input) => {
const GOVUKFrontendComponents = await getGOVUKFrontendExportsNames()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: There's a mix of components, variables and functions in GOV.UK Frontend's exports, so it'd be best to name the variable more generically.

Suggested change
const GOVUKFrontendComponents = await getGOVUKFrontendExportsNames()
const GOVUKFrontendExportsNames = await getGOVUKFrontendExportsNames()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, we could also save ourselves the naming alltogether by calling the function straight when we set reserved with reserved: await getGOVUKFrontendExportsNames().

*/
export default defineConfig(({ i: input }) => ({
input,
export default async (input) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion I think the function passed to defineConfig can be asynchronous, which would allow us to run the getGOVUKFrontendExportsNames() in it and keep a familiar shape for a Rollup config file exporting the result of defineConfig.

Comment on lines 45 to 46
if (!($root instanceof HTMLElement)) {
throw new ElementError({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue This is now handled by GOVUKFrontendComponent so we can remove it from this class.

@@ -719,7 +719,7 @@ describe('/components/accordion', () => {
render(page, 'accordion', examples.default, {
async afterInitialisation($root) {
const { Accordion } = await import('govuk-frontend')
new Accordion($root)
new Accordion(/** @type {HTMLElement} */ ($root))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue It'd be better to avoid that casting and type the constructor to receive any Element.

@@ -6,11 +6,9 @@ import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs'
* Header component
*
* @preserve
* @augments GOVUKFrontendComponent<Element>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue I think the source of the issue here is with the this.$root = root remaining in the constructor (which would try to assign an Element to an HTMLElement).

That assignment, along with the check for if (!$root) that precedes it are no longer necessary now the component extends GOVUKFrontendComponent. That should save us from having an explicit @augments to set some types.

@patrickpatrickpatrick patrickpatrickpatrick force-pushed the refactor-check-in-govuk-frontend-component branch from dee4e44 to 05ff355 Compare October 4, 2024 16:23
@patrickpatrickpatrick patrickpatrickpatrick force-pushed the refactor-check-in-govuk-frontend-component branch from 05ff355 to 706fe86 Compare October 4, 2024 16:31
@patrickpatrickpatrick patrickpatrickpatrick force-pushed the refactor-check-in-govuk-frontend-component branch from 706fe86 to 2166b25 Compare October 4, 2024 16:42
- Moves the type check of `root` into `GOVUKFrontendComponent` base
class which components in the Design System extend
- Moves assignment of `root` into the `GOVUKFrontendComponent` base
class which components in the Design System extend
- Adds `elementType` which specifies `type` of `root` to be checked and
can be overloaded by child classes
- Add `getGOVUKFrontendExportsNames` to `rollup.release.config.mjs`
which ensures that we don't run into an error in which `window.*` is
undefined during build
- Update `Skeleton` section of js documentation to reflect new changes
@patrickpatrickpatrick patrickpatrickpatrick force-pushed the refactor-check-in-govuk-frontend-component branch from 2166b25 to 85bbb44 Compare October 7, 2024 08:20
- Remove `root` from each component (now defined and assigned in
GOVUKFrontendComponent)
- Change references to `root` to be `this.root`
- Add `augments` when component requires typing of the `root` to be set
@patrickpatrickpatrick patrickpatrickpatrick force-pushed the refactor-check-in-govuk-frontend-component branch from 85bbb44 to 14962d4 Compare October 7, 2024 08:30
@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-5354 October 7, 2024 08:30 Inactive
Copy link
Member

@romaricpascal romaricpascal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for bearing with all the comments! 🙌🏻 ⛵

@patrickpatrickpatrick patrickpatrickpatrick merged commit 74c8102 into public-js-api Oct 7, 2024
49 checks passed
@patrickpatrickpatrick patrickpatrickpatrick deleted the refactor-check-in-govuk-frontend-component branch October 7, 2024 09:24
@owenatgov owenatgov mentioned this pull request Oct 10, 2024
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