From 3373529d863e6ebf5c44883fb38e4669b3086d8a Mon Sep 17 00:00:00 2001 From: Patrick Cartlidge Date: Fri, 11 Oct 2024 11:41:13 +0100 Subject: [PATCH 01/12] Make `BackToTop` extend `Component` - Rename `this.$module` to `$this.$root` - Remove initialisation checks that occur in `Component` --- src/javascripts/components/back-to-top.mjs | 40 +++++++++++++--------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/javascripts/components/back-to-top.mjs b/src/javascripts/components/back-to-top.mjs index ac2c3eb89f..513f2fc327 100644 --- a/src/javascripts/components/back-to-top.mjs +++ b/src/javascripts/components/back-to-top.mjs @@ -1,34 +1,40 @@ +import { Component } from 'govuk-frontend' + /** * Website back to top link */ -class BackToTop { +class BackToTop extends Component { + /** + * Returns the root element of the component + * + * @returns {any} - the root element of component + */ + get $root() { + // Unfortunately, govuk-frontend does not provide type definitions + // so TypeScript does not know of `this._$root` + // @ts-expect-error + return this._$root + } + static moduleName = 'app-back-to-top' /** * @param {Element} $module - HTML element */ constructor($module) { - if ( - !($module instanceof HTMLElement) || - !document.body.classList.contains('govuk-frontend-supported') - ) { - return this - } + super($module) - this.$module = $module + const $footer = document.querySelector('.app-footer') + const $subNav = document.querySelector('.app-subnav') // Check if we can use Intersection Observers if (!('IntersectionObserver' in window)) { // If there's no support fallback to regular behaviour // Since JavaScript is enabled we can remove the default hidden state - this.$module.classList.remove('app-back-to-top--hidden') + this.$root.classList.remove('app-back-to-top--hidden') return this } - const $footer = document.querySelector('.app-footer') - const $subNav = document.querySelector('.app-subnav') - - // Check if there is anything to observe if (!$footer || !$subNav) { return this } @@ -53,17 +59,17 @@ class BackToTop { // If the subnav or the footer not visible then fix the back to top link to follow the user if (subNavIsIntersecting || footerIsIntersecting) { - this.$module.classList.remove('app-back-to-top--fixed') + this.$root.classList.remove('app-back-to-top--fixed') } else { - this.$module.classList.add('app-back-to-top--fixed') + this.$root.classList.add('app-back-to-top--fixed') } // If the subnav is visible but you can see it all at once, then a back to top link is likely not as useful. // We hide the link but make it focusable for screen readers users who might still find it useful. if (subNavIsIntersecting && subNavIntersectionRatio === 1) { - this.$module.classList.add('app-back-to-top--hidden') + this.$root.classList.add('app-back-to-top--hidden') } else { - this.$module.classList.remove('app-back-to-top--hidden') + this.$root.classList.remove('app-back-to-top--hidden') } }) From 4f4035a6fd09d96bceae7bf95613ac103e62486a Mon Sep 17 00:00:00 2001 From: Patrick Cartlidge Date: Fri, 11 Oct 2024 11:41:51 +0100 Subject: [PATCH 02/12] Make `CookieBanner` extend `Component` - Rename `this.$module` to `$this.$root` - Remove initialisation checks that occur in `Component` - `onCookiesPage` now a static function - Use `isSupported` function to check component support --- src/javascripts/components/cookie-banner.mjs | 55 +++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/src/javascripts/components/cookie-banner.mjs b/src/javascripts/components/cookie-banner.mjs index 60a8bf882b..0d8887a9ef 100644 --- a/src/javascripts/components/cookie-banner.mjs +++ b/src/javascripts/components/cookie-banner.mjs @@ -1,3 +1,5 @@ +import { Component } from 'govuk-frontend' + import * as CookieFunctions from './cookie-functions.mjs' const cookieBannerAcceptSelector = '.js-cookie-banner-accept' @@ -10,26 +12,36 @@ const cookieConfirmationRejectSelector = '.js-cookie-banner-confirmation-reject' /** * Website cookie banner */ -class CookieBanner { +class CookieBanner extends Component { + /** + * Check support of CookieBanner + */ + static checkSupport() { + Component.checkSupport() + + if (CookieBanner.onCookiesPage()) { + throw Error('Cancelled initialisation as on cookie page') + } + } + + /** + * Check if on the Cookies page + * + * @returns {boolean} Returns true if on the Cookies page + */ + static onCookiesPage() { + return window.location.pathname === '/cookies/' + } + static moduleName = 'govuk-cookie-banner' /** * @param {Element} $module - HTML element */ constructor($module) { - if ( - !($module instanceof HTMLElement) || - !document.body.classList.contains('govuk-frontend-supported') || - // Exit if we're on the cookies page to avoid circular journeys - this.onCookiesPage() - ) { - return this - } + super($module) - this.$cookieBanner = $module this.cookieCategory = - (this.$cookieBanner.dataset && - this.$cookieBanner.dataset.cookieCategory) || - 'analytics' + (this.$root.dataset && this.$root.dataset.cookieCategory) || 'analytics' const $acceptButton = $module.querySelector(cookieBannerAcceptSelector) const $rejectButton = $module.querySelector(cookieBannerRejectSelector) @@ -60,7 +72,7 @@ class CookieBanner { this.$cookieMessage = $cookieMessage this.$cookieConfirmationAccept = $cookieConfirmationAccept this.$cookieConfirmationReject = $cookieConfirmationReject - this.$cookieBannerHideButtons = $cookieBannerHideButtons + this.$rootHideButtons = $cookieBannerHideButtons // Show the cookie banner to users who have not consented or have an // outdated consent cookie @@ -74,13 +86,13 @@ class CookieBanner { // set previously CookieFunctions.resetCookies() - this.$cookieBanner.removeAttribute('hidden') + this.$root.removeAttribute('hidden') } this.$acceptButton.addEventListener('click', () => this.acceptCookies()) this.$rejectButton.addEventListener('click', () => this.rejectCookies()) - this.$cookieBannerHideButtons.forEach(($cookieBannerHideButton) => { + this.$rootHideButtons.forEach(($cookieBannerHideButton) => { $cookieBannerHideButton.addEventListener('click', () => this.hideBanner()) }) } @@ -89,7 +101,7 @@ class CookieBanner { * Hide banner */ hideBanner() { - this.$cookieBanner.setAttribute('hidden', 'true') + this.$root.setAttribute('hidden', 'true') } /** @@ -135,15 +147,6 @@ class CookieBanner { confirmationMessage.focus() } - - /** - * Check if on the Cookies page - * - * @returns {boolean} Returns true if on the Cookies page - */ - onCookiesPage() { - return window.location.pathname === '/cookies/' - } } export default CookieBanner From 2ed550934342d2877664092e68c27791ad7587af Mon Sep 17 00:00:00 2001 From: Patrick Cartlidge Date: Fri, 11 Oct 2024 11:42:44 +0100 Subject: [PATCH 03/12] Make `CookiesPage` extend `Component` - Rename `this.$module` to `$this.$root` - Remove initialisation checks that occur in `Component` --- src/javascripts/components/cookies-page.mjs | 29 +++++++++++++-------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/javascripts/components/cookies-page.mjs b/src/javascripts/components/cookies-page.mjs index 58f394e6b6..7541ef7e40 100644 --- a/src/javascripts/components/cookies-page.mjs +++ b/src/javascripts/components/cookies-page.mjs @@ -1,24 +1,31 @@ +import { Component } from 'govuk-frontend' + import { getConsentCookie, setConsentCookie } from './cookie-functions.mjs' /** * Website cookies page */ -class CookiesPage { +class CookiesPage extends Component { + /** + * Returns the root element of the component + * + * @returns {any} - the root element of component + */ + get $root() { + // Unfortunately, govuk-frontend does not provide type definitions + // so TypeScript does not know of `this._$root` + // @ts-expect-error + return this._$root + } + static moduleName = 'app-cookies-page' /** * @param {Element} $module - HTML element */ constructor($module) { - if ( - !($module instanceof HTMLElement) || - !document.body.classList.contains('govuk-frontend-supported') - ) { - return this - } - - this.$page = $module + super($module) - const $cookieForm = this.$page.querySelector('.js-cookies-page-form') + const $cookieForm = this.$root.querySelector('.js-cookies-page-form') if (!($cookieForm instanceof HTMLFormElement)) { return this } @@ -43,7 +50,7 @@ class CookiesPage { this.$cookieFormFieldsets = $cookieFormFieldsets this.$cookieFormButton = $cookieFormButton - const $successNotification = this.$page.querySelector( + const $successNotification = this.$root.querySelector( '.js-cookies-page-success' ) if ($successNotification instanceof HTMLElement) { From 710fef7d212f2e90956fca9c015bf97df153469f Mon Sep 17 00:00:00 2001 From: Patrick Cartlidge Date: Fri, 11 Oct 2024 11:43:52 +0100 Subject: [PATCH 04/12] Make `Copy` extend `Component` - Rename `this.$module` to `$this.$root` - Remove initialisation checks that occur in `Component` - Use `isSupported` function to check component support --- src/javascripts/components/copy.mjs | 42 ++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/javascripts/components/copy.mjs b/src/javascripts/components/copy.mjs index 756eb15324..e435c028c4 100644 --- a/src/javascripts/components/copy.mjs +++ b/src/javascripts/components/copy.mjs @@ -1,26 +1,42 @@ import ClipboardJS from 'clipboard' +import { Component } from 'govuk-frontend' /** * Copy button for code examples */ -class Copy { +class Copy extends Component { + /** + * Returns the root element of the component + * + * @returns {any} - the root element of component + */ + get $root() { + // Unfortunately, govuk-frontend does not provide type definitions + // so TypeScript does not know of `this._$root` + // @ts-expect-error + return this._$root + } + static moduleName = 'app-copy' /** - * @param {Element} $module - HTML element + * Check if ClipboardJS is supported */ - constructor($module) { - if ( - !($module instanceof HTMLElement) || - !document.body.classList.contains('govuk-frontend-supported') || - !ClipboardJS.isSupported() - ) { - return this + static checkSupport() { + Component.checkSupport() + + if (!ClipboardJS.isSupported()) { + throw Error('ClipboardJS not supported in this browser') } + } - this.$module = $module + /** + * @param {Element} $module - HTML element + */ + constructor($module) { + super($module) - this.$pre = this.$module.querySelector('pre') + this.$pre = this.$root.querySelector('pre') // TODO: Throw once GOV.UK Frontend exports its errors /** @type {number | null} */ @@ -34,8 +50,8 @@ class Copy { this.$status.className = 'govuk-visually-hidden' this.$status.setAttribute('aria-live', 'assertive') - this.$module.prepend(this.$status) - this.$module.prepend(this.$button) + this.$root.prepend(this.$status) + this.$root.prepend(this.$button) const $clipboard = new ClipboardJS(this.$button, { target: () => this.$pre From 24a64f2ec42f11e75ae09fab906ed98e5e2f8c78 Mon Sep 17 00:00:00 2001 From: Patrick Cartlidge Date: Fri, 11 Oct 2024 11:45:43 +0100 Subject: [PATCH 05/12] Make `EmbedCard` extend `Component` - Rename `this.$module` to `$this.$root` - Remove initialisation checks that occur in `Component` - Use `isSupported` function to check component support - Update `EmbedCard` initialisation code in `application.mjs` to prevent error on multiple initialisation and catch the error thrown if user has not accepted campaign cookies --- src/javascripts/application.mjs | 6 ++- src/javascripts/components/embed-card.mjs | 49 +++++++++++++++++------ 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/src/javascripts/application.mjs b/src/javascripts/application.mjs index 9a0b48f886..8a46c3a2df 100644 --- a/src/javascripts/application.mjs +++ b/src/javascripts/application.mjs @@ -70,7 +70,11 @@ const lazyEmbedObserver = new IntersectionObserver(function ( ) { entries.forEach(function (entry) { if (entry.isIntersecting) { - new EmbedCard(entry.target) + try { + new EmbedCard(entry.target) + } catch (error) { + console.log(error) + } } }) }) diff --git a/src/javascripts/components/embed-card.mjs b/src/javascripts/components/embed-card.mjs index 2593ee78cc..937aa9950a 100644 --- a/src/javascripts/components/embed-card.mjs +++ b/src/javascripts/components/embed-card.mjs @@ -1,21 +1,43 @@ +import { Component } from 'govuk-frontend' + import { getConsentCookie } from './cookie-functions.mjs' /** * Embed Card Youtube functionality */ -class EmbedCard { +class EmbedCard extends Component { /** - * @param {Element} $module - HTML element + * Check EmbedCard support */ - constructor($module) { - if ( - !($module instanceof HTMLElement) || - !document.body.classList.contains('govuk-frontend-supported') - ) { - return this + static checkSupport() { + Component.checkSupport() + + const consentCookie = getConsentCookie() + + if (!consentCookie || (consentCookie && !consentCookie.campaign)) { + throw Error('Campaign consent cookies not accepted') } + } - this.$module = $module + /** + * Returns the root element of the component + * + * @returns {any} - the root element of component + */ + get $root() { + // Unfortunately, govuk-frontend does not provide type definitions + // so TypeScript does not know of `this._$root` + // @ts-expect-error + return this._$root + } + + static moduleName = 'app-embed-card' + + /** + * @param {Element} $module - HTML element + */ + constructor($module) { + super($module) this.replacePlaceholder() } @@ -26,17 +48,17 @@ class EmbedCard { * Replaces the placeholder with the iframe if cookies are set. */ replacePlaceholder() { - if (this.$module.querySelector('iframe')) { + if (this.$root.querySelector('iframe')) { return } const consentCookie = getConsentCookie() if (consentCookie && consentCookie.campaign) { - const placeholder = this.$module.querySelector( + const placeholder = this.$root.querySelector( '.app-embed-card__placeholder' ) - const placeholderText = this.$module.querySelector( + const placeholderText = this.$root.querySelector( '.app-embed-card__placeholder-text' ) @@ -51,7 +73,7 @@ class EmbedCard { placeholder.remove() - const iframeContainer = this.$module.querySelector( + const iframeContainer = this.$root.querySelector( '.app-embed-card__placeholder-iframe-container' ) iframeContainer.appendChild(iframe) @@ -65,6 +87,7 @@ class EmbedCard { * * @param {string} ytId - YouTube ID * @param {string} title - Title for iFrame (for screen readers) + * @returns {HTMLElement} - iframe element */ createIframe(ytId, title) { const iframe = document.createElement('IFRAME') From 4466bd9e716c01c844d5ffa3f603450833e385c1 Mon Sep 17 00:00:00 2001 From: Patrick Cartlidge Date: Fri, 11 Oct 2024 11:46:20 +0100 Subject: [PATCH 06/12] Make `ExampleFrame` extend `Component` - Rename `this.$module` to `$this.$root` - Remove initialisation checks that occur in `Component` --- src/javascripts/components/example-frame.mjs | 33 ++++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/javascripts/components/example-frame.mjs b/src/javascripts/components/example-frame.mjs index b542c53416..20087a5856 100644 --- a/src/javascripts/components/example-frame.mjs +++ b/src/javascripts/components/example-frame.mjs @@ -1,3 +1,4 @@ +import { Component } from 'govuk-frontend' import iFrameResize from 'iframe-resizer/js/iframeResizer.js' /** @@ -7,30 +8,36 @@ import iFrameResize from 'iframe-resizer/js/iframeResizer.js' * template wrappers. * * @param {Element} $module - HTML element to use for example + * @augments Component */ -class ExampleFrame { +class ExampleFrame extends Component { + /** + * Returns the root element of the component + * + * @returns {any} - the root element of component + */ + get $root() { + // Unfortunately, govuk-frontend does not provide type definitions + // so TypeScript does not know of `this._$root` + // @ts-expect-error + return this._$root + } + static moduleName = 'app-example-frame' /** * @param {Element} $module - HTML element */ constructor($module) { - if ( - !($module instanceof HTMLIFrameElement) || - !document.body.classList.contains('govuk-frontend-supported') - ) { - return - } - - this.$module = $module + super($module) // Initialise asap for eager iframes or browsers which don't support lazy loading - if (!('loading' in this.$module) || this.$module.loading !== 'lazy') { - return iFrameResize({ scrolling: 'omit' }, this.$module) + if (!('loading' in this.$root) || this.$root.loading !== 'lazy') { + return iFrameResize({ scrolling: 'omit' }, this.$root) } - this.$module.addEventListener('load', () => { + this.$root.addEventListener('load', () => { try { - iFrameResize({ scrolling: 'omit' }, this.$module) + iFrameResize({ scrolling: 'omit' }, this.$root) } catch (error) { if (error instanceof Error) { console.error(error.message) From 36936454af7dd4794a4039758b95bb1f84d2e993 Mon Sep 17 00:00:00 2001 From: Patrick Cartlidge Date: Fri, 11 Oct 2024 11:48:08 +0100 Subject: [PATCH 07/12] Make `Navigation` extend `Component` - Rename `this.$module` to `$this.$root` in `Navigation` - Remove initialisation checks that occur in `Component` in `Navigation` - Use `createAll` in `application.mjs` to initialise component (so that errors thrown are caught) - Add `data-module="app-navigation"` to `body` of page instead of initialising `Navigation` on `document` (which is now no longer supported due to the new initialisation method that sets attributes) so `createAll(Navigation)` can be used --- src/javascripts/application.mjs | 2 +- src/javascripts/components/navigation.mjs | 42 ++++++++++++++--------- views/layouts/_generic.njk | 2 ++ 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/javascripts/application.mjs b/src/javascripts/application.mjs index 8a46c3a2df..0039759f75 100644 --- a/src/javascripts/application.mjs +++ b/src/javascripts/application.mjs @@ -48,7 +48,7 @@ createAll(Copy) new OptionsTable() // Initialise mobile navigation -new Navigation(document) +createAll(Navigation) // Initialise scrollable container handling createAll(ScrollContainer) diff --git a/src/javascripts/components/navigation.mjs b/src/javascripts/components/navigation.mjs index 8309495e2d..f3d3bf570e 100644 --- a/src/javascripts/components/navigation.mjs +++ b/src/javascripts/components/navigation.mjs @@ -1,3 +1,5 @@ +import { Component } from 'govuk-frontend' + const navActiveClass = 'app-navigation--active' const navMenuButtonActiveClass = 'govuk-header__menu-button--open' const subNavActiveClass = 'app-navigation__subnav--active' @@ -7,28 +9,36 @@ const subNavJSClass = '.js-app-navigation__subnav' /** * Website navigation */ -class Navigation { +class Navigation extends Component { /** - * @param {Document} $module - HTML document + * Returns the root element of the component + * + * @returns {any} - the root element of component */ - constructor($module) { - if ( - !($module instanceof Document) || - !document.body.classList.contains('govuk-frontend-supported') - ) { - return this - } + get $root() { + // Unfortunately, govuk-frontend does not provide type definitions + // so TypeScript does not know of `this._$root` + // @ts-expect-error + return this._$root + } - this.$module = $module + /** + * Name for the component used when initialising using data-module attributes. + */ + static moduleName = 'app-navigation' - const $nav = this.$module.querySelector('.js-app-navigation') - const $navToggler = this.$module.querySelector( - '.js-app-navigation__toggler' - ) - const $navButtons = this.$module.querySelectorAll( + /** + * @param {HTMLElement} $module - HTML document + */ + constructor($module) { + super($module) + + const $nav = this.$root.querySelector('.js-app-navigation') + const $navToggler = this.$root.querySelector('.js-app-navigation__toggler') + const $navButtons = this.$root.querySelectorAll( '.js-app-navigation__button' ) - const $navLinks = this.$module.querySelectorAll('.js-app-navigation__link') + const $navLinks = this.$root.querySelectorAll('.js-app-navigation__link') if ( !($nav instanceof HTMLElement) || diff --git a/views/layouts/_generic.njk b/views/layouts/_generic.njk index a570490601..84850c14e8 100644 --- a/views/layouts/_generic.njk +++ b/views/layouts/_generic.njk @@ -3,6 +3,8 @@ {% set assetUrl = 'https://design-system.service.gov.uk/assets' %} +{% set bodyAttributes = { "data-module": "app-navigation" } %} + {% block pageTitle %}{{ title | smartypants }} – GOV.UK Design System{% endblock %} {% block head %} From 3c676b0f6d254f42f5b48511bebb36b1206e6920 Mon Sep 17 00:00:00 2001 From: Patrick Cartlidge Date: Fri, 11 Oct 2024 11:51:43 +0100 Subject: [PATCH 08/12] Make `ScrollContainer` extend `Component` - Rename `this.$module` to `$this.$root` - Remove initialisation checks that occur in `Component` - Use `isSupported` function to check component support --- .../components/scroll-container.mjs | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/src/javascripts/components/scroll-container.mjs b/src/javascripts/components/scroll-container.mjs index 5616b0d02b..61791872c9 100644 --- a/src/javascripts/components/scroll-container.mjs +++ b/src/javascripts/components/scroll-container.mjs @@ -1,3 +1,5 @@ +import { Component } from 'govuk-frontend' + const scrollContainerResizeObserver = new window.ResizeObserver((entries) => { for (const entry of entries) { if (ScrollContainer.isOverflowing(entry.target)) { @@ -11,22 +13,38 @@ const scrollContainerResizeObserver = new window.ResizeObserver((entries) => { /** * */ -class ScrollContainer { +class ScrollContainer extends Component { + /** + * Returns the root element of the component + * + * @returns {any} - the root element of component + */ + get $root() { + // Unfortunately, govuk-frontend does not provide type definitions + // so TypeScript does not know of `this._$root` + // @ts-expect-error + return this._$root + } + static moduleName = 'app-scroll-container' /** - * @param {Element} $module - HTML element + * Checks if ResizeObserver supported */ - constructor($module) { - if ( - !($module instanceof HTMLElement) || - !document.body.classList.contains('govuk-frontend-supported') || - !('ResizeObserver' in window) - ) { - return this + static isSupported() { + Component.checkSupport() + + if (!('ResizeObserver' in window)) { + throw Error('Browser does not support ResizeObserver') } + } - scrollContainerResizeObserver.observe($module) + /** + * @param {Element} $module - HTML element + */ + constructor($module) { + super($module) + scrollContainerResizeObserver.observe(this.$root) } /** From b1ff1c553f37f542ef9f079dabed54c975554d83 Mon Sep 17 00:00:00 2001 From: Patrick Cartlidge Date: Fri, 11 Oct 2024 11:53:03 +0100 Subject: [PATCH 09/12] Make `Search` extend `Component` - Rename `this.$module` to `$this.$root` - Remove initialisation checks that occur in `Component` --- src/javascripts/components/search.mjs | 30 ++++++++++++++++----------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/javascripts/components/search.mjs b/src/javascripts/components/search.mjs index 6dd3bdb07e..64f2c5d0ce 100644 --- a/src/javascripts/components/search.mjs +++ b/src/javascripts/components/search.mjs @@ -1,5 +1,6 @@ /* global XMLHttpRequest */ import accessibleAutocomplete from 'accessible-autocomplete' +import { Component } from 'govuk-frontend' import lunr from 'lunr' import { trackSearchResults, trackConfirm } from './search.tracking.mjs' @@ -36,23 +37,28 @@ const DEBOUNCE_TIME_TO_WAIT = () => { /** * Website search */ -class Search { +class Search extends Component { + /** + * Returns the root element of the component + * + * @returns {any} - the root element of component + */ + get $root() { + // Unfortunately, govuk-frontend does not provide type definitions + // so TypeScript does not know of `this._$root` + // @ts-expect-error + return this._$root + } + static moduleName = 'app-search' /** * @param {Element} $module - HTML element */ constructor($module) { - if ( - !($module instanceof HTMLElement) || - !document.body.classList.contains('govuk-frontend-supported') - ) { - return this - } - - this.$module = $module + super($module) accessibleAutocomplete({ - element: this.$module, + element: this.$root, id: 'app-site-search__input', cssNamespace: 'app-site-search', displayMenu: 'overlay', @@ -68,7 +74,7 @@ class Search { tNoResults: () => statusMessage }) - const $input = this.$module.querySelector('.app-site-search__input') + const $input = this.$root.querySelector('.app-site-search__input') if (!$input) { return this } @@ -78,7 +84,7 @@ class Search { clearTimeout(inputDebounceTimer) }) - const searchIndexUrl = this.$module.getAttribute('data-search-index') + const searchIndexUrl = this.$root.getAttribute('data-search-index') this.fetchSearchIndex(searchIndexUrl, () => { this.renderResults() }) From ec595ff46d4453ddc49ebf1c40ccaf57b054cacd Mon Sep 17 00:00:00 2001 From: Patrick Cartlidge Date: Fri, 11 Oct 2024 11:53:44 +0100 Subject: [PATCH 10/12] Make `Tabs` extend `Component` - Rename `this.$module` to `$this.$root` - Remove initialisation checks that occur in `Component` --- src/javascripts/components/tabs.mjs | 36 ++++++++++++++++++----------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/javascripts/components/tabs.mjs b/src/javascripts/components/tabs.mjs index 2ba79ee417..36e4bb1ba4 100644 --- a/src/javascripts/components/tabs.mjs +++ b/src/javascripts/components/tabs.mjs @@ -1,3 +1,5 @@ +import { Component } from 'govuk-frontend' + /** * The naming of things is a little complicated in here. * For reference: @@ -8,24 +10,30 @@ * - desktop tabs - the controls to show, hide or switch panels on tablet/desktop * - panels - the content that is shown/hidden/switched; same across all breakpoints */ -class AppTabs { +class AppTabs extends Component { + /** + * Returns the root element of the component + * + * @returns {any} - the root element of component + */ + get $root() { + // Unfortunately, govuk-frontend does not provide type definitions + // so TypeScript does not know of `this._$root` + // @ts-expect-error + return this._$root + } + static moduleName = 'app-tabs' /** * @param {Element} $module - HTML element */ constructor($module) { - if ( - !($module instanceof HTMLElement) || - !document.body.classList.contains('govuk-frontend-supported') - ) { - return this - } + super($module) - this.$module = $module - this.$mobileTabs = this.$module.querySelectorAll('.js-tabs__heading a') - this.$desktopTabs = this.$module.querySelectorAll('.js-tabs__item a') - this.$panels = this.$module.querySelectorAll('.js-tabs__container') + this.$mobileTabs = this.$root.querySelectorAll('.js-tabs__heading a') + this.$desktopTabs = this.$root.querySelectorAll('.js-tabs__item a') + this.$panels = this.$root.querySelectorAll('.js-tabs__container') // Enhance mobile tabs into buttons this.enhanceMobileTabs() @@ -40,7 +48,7 @@ class AppTabs { this.resetTabs() // Show the first panel already open if the `open` attribute is present - if (this.$module.hasAttribute('data-open')) { + if (this.$root.hasAttribute('data-open')) { this.openPanel(this.$panels[0].id) } } @@ -104,7 +112,7 @@ class AppTabs { }) // Replace the value of $mobileTabs with the new buttons - this.$mobileTabs = this.$module.querySelectorAll('.js-tabs__heading button') + this.$mobileTabs = this.$root.querySelectorAll('.js-tabs__heading button') } /** @@ -212,7 +220,7 @@ class AppTabs { * @returns {HTMLAnchorElement | null} Desktop tab link */ getDesktopTab(panelId) { - const $desktopTabContainer = this.$module.querySelector('.app-tabs') + const $desktopTabContainer = this.$root.querySelector('.app-tabs') if ($desktopTabContainer) { return $desktopTabContainer.querySelector(`[aria-controls="${panelId}"]`) } From 90394d3d5322d2e91cf9f4372e8c7235dd554387 Mon Sep 17 00:00:00 2001 From: Romaric Pascal Date: Thu, 10 Oct 2024 19:44:57 +0100 Subject: [PATCH 11/12] Configure TypeScript to look up for .ts files in --- src/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tsconfig.json b/src/tsconfig.json index 899a61af13..1eb13b59d7 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -1,6 +1,6 @@ { "extends": "../tsconfig.base.json", - "include": ["**/*.js", "**/*.mjs"], + "include": ["**/*.js", "**/*.mjs", "**/*.ts"], "compilerOptions": { "lib": ["ESNext", "DOM"], "target": "ES2015" From 04a7ba2db6bad12fc5adb8f88766ca872fb3a687 Mon Sep 17 00:00:00 2001 From: Romaric Pascal Date: Thu, 10 Oct 2024 20:20:05 +0100 Subject: [PATCH 12/12] Add shorthand ambient module definition for 'govuk-frontend' As govuk-frontend provides no types, TypeScript will type its exports as `any`, but be unable to acknowledge fields inherited from parent classes leading to errors when trying to assign or use them. TypeScript's shorthand ambient modules seem to also make inherited fields typed as `any`. https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#shorthand-ambient-module-declarations This will override any other definition of the `govuk-frontend` module, either in another `.d.ts` file or in `@types/govuk-frontend`. --- src/global.d.ts | 9 +++++++++ src/javascripts/components/back-to-top.mjs | 12 ------------ src/javascripts/components/cookies-page.mjs | 12 ------------ src/javascripts/components/copy.mjs | 12 ------------ src/javascripts/components/embed-card.mjs | 12 ------------ src/javascripts/components/example-frame.mjs | 12 ------------ src/javascripts/components/navigation.mjs | 12 ------------ src/javascripts/components/scroll-container.mjs | 12 ------------ src/javascripts/components/search.mjs | 12 ------------ src/javascripts/components/tabs.mjs | 12 ------------ 10 files changed, 9 insertions(+), 108 deletions(-) create mode 100644 src/global.d.ts diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 0000000000..ec077162db --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,9 @@ +/** + * As govuk-frontend provides no types, TypeScript will type its exports as `any`, + * but be unable to acknowledge fields inherited from parent classes + * leading to errors when trying to assign or use them. + * + * TypeScript's shorthand ambient modules seem to also make inherited fields typed as `any`. + * https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#shorthand-ambient-module-declarations + */ +declare module "govuk-frontend"; \ No newline at end of file diff --git a/src/javascripts/components/back-to-top.mjs b/src/javascripts/components/back-to-top.mjs index 513f2fc327..a49d3a69e0 100644 --- a/src/javascripts/components/back-to-top.mjs +++ b/src/javascripts/components/back-to-top.mjs @@ -4,18 +4,6 @@ import { Component } from 'govuk-frontend' * Website back to top link */ class BackToTop extends Component { - /** - * Returns the root element of the component - * - * @returns {any} - the root element of component - */ - get $root() { - // Unfortunately, govuk-frontend does not provide type definitions - // so TypeScript does not know of `this._$root` - // @ts-expect-error - return this._$root - } - static moduleName = 'app-back-to-top' /** diff --git a/src/javascripts/components/cookies-page.mjs b/src/javascripts/components/cookies-page.mjs index 7541ef7e40..dc93813247 100644 --- a/src/javascripts/components/cookies-page.mjs +++ b/src/javascripts/components/cookies-page.mjs @@ -6,18 +6,6 @@ import { getConsentCookie, setConsentCookie } from './cookie-functions.mjs' * Website cookies page */ class CookiesPage extends Component { - /** - * Returns the root element of the component - * - * @returns {any} - the root element of component - */ - get $root() { - // Unfortunately, govuk-frontend does not provide type definitions - // so TypeScript does not know of `this._$root` - // @ts-expect-error - return this._$root - } - static moduleName = 'app-cookies-page' /** * @param {Element} $module - HTML element diff --git a/src/javascripts/components/copy.mjs b/src/javascripts/components/copy.mjs index e435c028c4..66d2dbaf79 100644 --- a/src/javascripts/components/copy.mjs +++ b/src/javascripts/components/copy.mjs @@ -5,18 +5,6 @@ import { Component } from 'govuk-frontend' * Copy button for code examples */ class Copy extends Component { - /** - * Returns the root element of the component - * - * @returns {any} - the root element of component - */ - get $root() { - // Unfortunately, govuk-frontend does not provide type definitions - // so TypeScript does not know of `this._$root` - // @ts-expect-error - return this._$root - } - static moduleName = 'app-copy' /** diff --git a/src/javascripts/components/embed-card.mjs b/src/javascripts/components/embed-card.mjs index 937aa9950a..6394c99490 100644 --- a/src/javascripts/components/embed-card.mjs +++ b/src/javascripts/components/embed-card.mjs @@ -19,18 +19,6 @@ class EmbedCard extends Component { } } - /** - * Returns the root element of the component - * - * @returns {any} - the root element of component - */ - get $root() { - // Unfortunately, govuk-frontend does not provide type definitions - // so TypeScript does not know of `this._$root` - // @ts-expect-error - return this._$root - } - static moduleName = 'app-embed-card' /** diff --git a/src/javascripts/components/example-frame.mjs b/src/javascripts/components/example-frame.mjs index 20087a5856..d84e424ed8 100644 --- a/src/javascripts/components/example-frame.mjs +++ b/src/javascripts/components/example-frame.mjs @@ -11,18 +11,6 @@ import iFrameResize from 'iframe-resizer/js/iframeResizer.js' * @augments Component */ class ExampleFrame extends Component { - /** - * Returns the root element of the component - * - * @returns {any} - the root element of component - */ - get $root() { - // Unfortunately, govuk-frontend does not provide type definitions - // so TypeScript does not know of `this._$root` - // @ts-expect-error - return this._$root - } - static moduleName = 'app-example-frame' /** * @param {Element} $module - HTML element diff --git a/src/javascripts/components/navigation.mjs b/src/javascripts/components/navigation.mjs index f3d3bf570e..f941cbc407 100644 --- a/src/javascripts/components/navigation.mjs +++ b/src/javascripts/components/navigation.mjs @@ -10,18 +10,6 @@ const subNavJSClass = '.js-app-navigation__subnav' * Website navigation */ class Navigation extends Component { - /** - * Returns the root element of the component - * - * @returns {any} - the root element of component - */ - get $root() { - // Unfortunately, govuk-frontend does not provide type definitions - // so TypeScript does not know of `this._$root` - // @ts-expect-error - return this._$root - } - /** * Name for the component used when initialising using data-module attributes. */ diff --git a/src/javascripts/components/scroll-container.mjs b/src/javascripts/components/scroll-container.mjs index 61791872c9..bf9354e33b 100644 --- a/src/javascripts/components/scroll-container.mjs +++ b/src/javascripts/components/scroll-container.mjs @@ -14,18 +14,6 @@ const scrollContainerResizeObserver = new window.ResizeObserver((entries) => { * */ class ScrollContainer extends Component { - /** - * Returns the root element of the component - * - * @returns {any} - the root element of component - */ - get $root() { - // Unfortunately, govuk-frontend does not provide type definitions - // so TypeScript does not know of `this._$root` - // @ts-expect-error - return this._$root - } - static moduleName = 'app-scroll-container' /** diff --git a/src/javascripts/components/search.mjs b/src/javascripts/components/search.mjs index 64f2c5d0ce..c82780b212 100644 --- a/src/javascripts/components/search.mjs +++ b/src/javascripts/components/search.mjs @@ -38,18 +38,6 @@ const DEBOUNCE_TIME_TO_WAIT = () => { * Website search */ class Search extends Component { - /** - * Returns the root element of the component - * - * @returns {any} - the root element of component - */ - get $root() { - // Unfortunately, govuk-frontend does not provide type definitions - // so TypeScript does not know of `this._$root` - // @ts-expect-error - return this._$root - } - static moduleName = 'app-search' /** * @param {Element} $module - HTML element diff --git a/src/javascripts/components/tabs.mjs b/src/javascripts/components/tabs.mjs index 36e4bb1ba4..e5ef2c5d7e 100644 --- a/src/javascripts/components/tabs.mjs +++ b/src/javascripts/components/tabs.mjs @@ -11,18 +11,6 @@ import { Component } from 'govuk-frontend' * - panels - the content that is shown/hidden/switched; same across all breakpoints */ class AppTabs extends Component { - /** - * Returns the root element of the component - * - * @returns {any} - the root element of component - */ - get $root() { - // Unfortunately, govuk-frontend does not provide type definitions - // so TypeScript does not know of `this._$root` - // @ts-expect-error - return this._$root - } - static moduleName = 'app-tabs' /**