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

Use CSS custom properties for component matchMedia() #4562

Merged
merged 7 commits into from
Dec 15, 2023
Merged

Conversation

colinrotherham
Copy link
Contributor

@colinrotherham colinrotherham commented Dec 13, 2023

This PR adds CSS custom properties for all our breakpoints alongside --govuk-frontend-version

--govuk-frontend-breakpoint-mobile: 20rem;
--govuk-frontend-breakpoint-tablet: 40.0625rem;
--govuk-frontend-breakpoint-desktop: 48.0625rem;
--govuk-frontend-version: "development";

Then uses them in window.matchMedia() instead of hard coded values in JavaScript

Closes #3764

Missing breakpoints

Throws an error if the CSS custom properties are missing:

ElementError: Tabs: CSS custom property (`--govuk-frontend-breakpoint-tablet`) on pseudo-class `:root` not found

Copy link

github-actions bot commented Dec 13, 2023

📋 Stats

File sizes

File Size
dist/govuk-frontend-development.min.css 113.85 KiB
dist/govuk-frontend-development.min.js 38.58 KiB
packages/govuk-frontend/dist/govuk/all.bundle.js 78.74 KiB
packages/govuk-frontend/dist/govuk/all.bundle.mjs 73.99 KiB
packages/govuk-frontend/dist/govuk/all.mjs 3.86 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend-component.mjs 359 B
packages/govuk-frontend/dist/govuk/govuk-frontend.min.css 113.83 KiB
packages/govuk-frontend/dist/govuk/govuk-frontend.min.js 38.57 KiB
packages/govuk-frontend/dist/govuk/i18n.mjs 5.38 KiB

Modules

File Size
all.mjs 70.32 KiB
components/accordion/accordion.mjs 21.67 KiB
components/button/button.mjs 4.7 KiB
components/character-count/character-count.mjs 21.24 KiB
components/checkboxes/checkboxes.mjs 5.83 KiB
components/error-summary/error-summary.mjs 6.57 KiB
components/exit-this-page/exit-this-page.mjs 16.08 KiB
components/header/header.mjs 4.46 KiB
components/notification-banner/notification-banner.mjs 4.93 KiB
components/radios/radios.mjs 4.83 KiB
components/skip-link/skip-link.mjs 4.39 KiB
components/tabs/tabs.mjs 10.16 KiB

View stats and visualisations on the review app


Action run for 05b819a

Copy link

github-actions bot commented Dec 13, 2023

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 7b92e7d1c..77bb7c649 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.js
@@ -34,6 +34,14 @@ function getFragmentFromUrl(t) {
     if (t.includes("#")) return t.split("#").pop()
 }
 
+function getBreakpoint(t) {
+    const e = `--govuk-frontend-breakpoint-${t}`;
+    return {
+        property: e,
+        value: window.getComputedStyle(document.documentElement).getPropertyValue(e) || void 0
+    }
+}
+
 function setFocus(t, e = {}) {
     var n;
     const i = t.getAttribute("tabindex");
@@ -697,13 +705,21 @@ class Header extends GOVUKFrontendComponent {
             element: i,
             identifier: `Navigation (\`<ul id="${n}">\`)`
         });
-        this.$menu = i, this.$menuButton = e, this.mql = window.matchMedia("(min-width: 48.0625em)"), "addEventListener" in this.mql ? this.mql.addEventListener("change", (() => this.syncState())) : this.mql.addListener((() => this.syncState())), this.syncState(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
+        this.$menu = i, this.$menuButton = e, this.setupResponsiveChecks(), this.$menuButton.addEventListener("click", (() => this.handleMenuButtonClick()))
+    }
+    setupResponsiveChecks() {
+        const t = getBreakpoint("desktop");
+        if (!t.value) throw new ElementError({
+            componentName: "Header",
+            identifier: `CSS custom property (\`${t.property}\`) on pseudo-class \`:root\``
+        });
+        this.mql = window.matchMedia(`(min-width: ${t.value})`), "addEventListener" in this.mql ? this.mql.addEventListener("change", (() => this.checkMode())) : this.mql.addListener((() => this.checkMode())), this.checkMode()
     }
-    syncState() {
+    checkMode() {
         this.mql && this.$menu && this.$menuButton && (this.mql.matches ? (this.$menu.removeAttribute("hidden"), this.$menuButton.setAttribute("hidden", "")) : (this.$menuButton.removeAttribute("hidden"), this.$menuButton.setAttribute("aria-expanded", this.menuIsOpen.toString()), this.menuIsOpen ? this.$menu.removeAttribute("hidden") : this.$menu.setAttribute("hidden", "")))
     }
     handleMenuButtonClick() {
-        this.menuIsOpen = !this.menuIsOpen, this.syncState()
+        this.menuIsOpen = !this.menuIsOpen, this.checkMode()
     }
 }
 Header.moduleName = "govuk-header";
@@ -837,7 +853,12 @@ class Tabs extends GOVUKFrontendComponent {
         this.$tabList = n, this.$tabListItems = i, this.setupResponsiveChecks()
     }
     setupResponsiveChecks() {
-        this.mql = window.matchMedia("(min-width: 40.0625em)"), "addEventListener" in this.mql ? this.mql.addEventListener("change", (() => this.checkMode())) : this.mql.addListener((() => this.checkMode())), this.checkMode()
+        const t = getBreakpoint("tablet");
+        if (!t.value) throw new ElementError({
+            componentName: "Tabs",
+            identifier: `CSS custom property (\`${t.property}\`) on pseudo-class \`:root\``
+        });
+        this.mql = window.matchMedia(`(min-width: ${t.value})`), "addEventListener" in this.mql ? this.mql.addEventListener("change", (() => this.checkMode())) : this.mql.addListener((() => this.checkMode())), this.checkMode()
     }
     checkMode() {
         var t;

Action run for 05b819a

Copy link

github-actions bot commented Dec 13, 2023

Stylesheets changes to npm package

diff --git a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
index c00459365..7036eb68b 100644
--- a/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
+++ b/packages/govuk-frontend/dist/govuk/govuk-frontend.min.css
@@ -1,7 +1,10 @@
 @charset "UTF-8";
 
 :root {
-    --govuk-frontend-version: "development"
+    --govuk-frontend-version: "development";
+    --govuk-frontend-breakpoint-mobile: 20rem;
+    --govuk-frontend-breakpoint-tablet: 40.0625rem;
+    --govuk-frontend-breakpoint-desktop: 48.0625rem
 }
 
 .govuk-link {

Action run for 05b819a

Copy link

github-actions bot commented Dec 13, 2023

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 e3b6dffa8..595d5851f 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.js
@@ -51,6 +51,14 @@
     }
     return url.split('#').pop();
   }
+  function getBreakpoint(name) {
+    const property = `--govuk-frontend-breakpoint-${name}`;
+    const value = window.getComputedStyle(document.documentElement).getPropertyValue(property);
+    return {
+      property,
+      value: value || undefined
+    };
+  }
   function setFocus($element, options = {}) {
     var _options$onBeforeFocu;
     const isFocusable = $element.getAttribute('tabindex');
@@ -1551,16 +1559,26 @@
       }
       this.$menu = $menu;
       this.$menuButton = $menuButton;
-      this.mql = window.matchMedia('(min-width: 48.0625em)');
+      this.setupResponsiveChecks();
+      this.$menuButton.addEventListener('click', () => this.handleMenuButtonClick());
+    }
+    setupResponsiveChecks() {
+      const breakpoint = getBreakpoint('desktop');
+      if (!breakpoint.value) {
+        throw new ElementError({
+          componentName: 'Header',
+          identifier: `CSS custom property (\`${breakpoint.property}\`) on pseudo-class \`:root\``
+        });
+      }
+      this.mql = window.matchMedia(`(min-width: ${breakpoint.value})`);
       if ('addEventListener' in this.mql) {
-        this.mql.addEventListener('change', () => this.syncState());
+        this.mql.addEventListener('change', () => this.checkMode());
       } else {
-        this.mql.addListener(() => this.syncState());
+        this.mql.addListener(() => this.checkMode());
       }
-      this.syncState();
-      this.$menuButton.addEventListener('click', () => this.handleMenuButtonClick());
+      this.checkMode();
     }
-    syncState() {
+    checkMode() {
       if (!this.mql || !this.$menu || !this.$menuButton) {
         return;
       }
@@ -1579,7 +1597,7 @@
     }
     handleMenuButtonClick() {
       this.menuIsOpen = !this.menuIsOpen;
-      this.syncState();
+      this.checkMode();
     }
   }
   Header.moduleName = 'govuk-header';
@@ -1844,7 +1862,14 @@
       this.setupResponsiveChecks();
     }
     setupResponsiveChecks() {
-      this.mql = window.matchMedia('(min-width: 40.0625em)');
+      const breakpoint = getBreakpoint('tablet');
+      if (!breakpoint.value) {
+        throw new ElementError({
+          componentName: 'Tabs',
+          identifier: `CSS custom property (\`${breakpoint.property}\`) on pseudo-class \`:root\``
+        });
+      }
+      this.mql = window.matchMedia(`(min-width: ${breakpoint.value})`);
       if ('addEventListener' in this.mql) {
         this.mql.addEventListener('change', () => this.checkMode());
       } else {
diff --git a/packages/govuk-frontend/dist/govuk/all.bundle.mjs b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
index 52c2325ba..7375fe834 100644
--- a/packages/govuk-frontend/dist/govuk/all.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/all.bundle.mjs
@@ -45,6 +45,14 @@ function getFragmentFromUrl(url) {
   }
   return url.split('#').pop();
 }
+function getBreakpoint(name) {
+  const property = `--govuk-frontend-breakpoint-${name}`;
+  const value = window.getComputedStyle(document.documentElement).getPropertyValue(property);
+  return {
+    property,
+    value: value || undefined
+  };
+}
 function setFocus($element, options = {}) {
   var _options$onBeforeFocu;
   const isFocusable = $element.getAttribute('tabindex');
@@ -1545,16 +1553,26 @@ class Header extends GOVUKFrontendComponent {
     }
     this.$menu = $menu;
     this.$menuButton = $menuButton;
-    this.mql = window.matchMedia('(min-width: 48.0625em)');
+    this.setupResponsiveChecks();
+    this.$menuButton.addEventListener('click', () => this.handleMenuButtonClick());
+  }
+  setupResponsiveChecks() {
+    const breakpoint = getBreakpoint('desktop');
+    if (!breakpoint.value) {
+      throw new ElementError({
+        componentName: 'Header',
+        identifier: `CSS custom property (\`${breakpoint.property}\`) on pseudo-class \`:root\``
+      });
+    }
+    this.mql = window.matchMedia(`(min-width: ${breakpoint.value})`);
     if ('addEventListener' in this.mql) {
-      this.mql.addEventListener('change', () => this.syncState());
+      this.mql.addEventListener('change', () => this.checkMode());
     } else {
-      this.mql.addListener(() => this.syncState());
+      this.mql.addListener(() => this.checkMode());
     }
-    this.syncState();
-    this.$menuButton.addEventListener('click', () => this.handleMenuButtonClick());
+    this.checkMode();
   }
-  syncState() {
+  checkMode() {
     if (!this.mql || !this.$menu || !this.$menuButton) {
       return;
     }
@@ -1573,7 +1591,7 @@ class Header extends GOVUKFrontendComponent {
   }
   handleMenuButtonClick() {
     this.menuIsOpen = !this.menuIsOpen;
-    this.syncState();
+    this.checkMode();
   }
 }
 Header.moduleName = 'govuk-header';
@@ -1838,7 +1856,14 @@ class Tabs extends GOVUKFrontendComponent {
     this.setupResponsiveChecks();
   }
   setupResponsiveChecks() {
-    this.mql = window.matchMedia('(min-width: 40.0625em)');
+    const breakpoint = getBreakpoint('tablet');
+    if (!breakpoint.value) {
+      throw new ElementError({
+        componentName: 'Tabs',
+        identifier: `CSS custom property (\`${breakpoint.property}\`) on pseudo-class \`:root\``
+      });
+    }
+    this.mql = window.matchMedia(`(min-width: ${breakpoint.value})`);
     if ('addEventListener' in this.mql) {
       this.mql.addEventListener('change', () => this.checkMode());
     } else {
diff --git a/packages/govuk-frontend/dist/govuk/common/index.mjs b/packages/govuk-frontend/dist/govuk/common/index.mjs
index aa2548704..1d272c1ba 100644
--- a/packages/govuk-frontend/dist/govuk/common/index.mjs
+++ b/packages/govuk-frontend/dist/govuk/common/index.mjs
@@ -43,6 +43,14 @@ function getFragmentFromUrl(url) {
   }
   return url.split('#').pop();
 }
+function getBreakpoint(name) {
+  const property = `--govuk-frontend-breakpoint-${name}`;
+  const value = window.getComputedStyle(document.documentElement).getPropertyValue(property);
+  return {
+    property,
+    value: value || undefined
+  };
+}
 function setFocus($element, options = {}) {
   var _options$onBeforeFocu;
   const isFocusable = $element.getAttribute('tabindex');
@@ -107,5 +115,5 @@ function validateConfig(schema, config) {
  * @property {string} errorMessage - Error message when required config fields not provided
  */
 
-export { extractConfigByNamespace, getFragmentFromUrl, isSupported, mergeConfigs, setFocus, validateConfig };
+export { extractConfigByNamespace, getBreakpoint, getFragmentFromUrl, isSupported, mergeConfigs, setFocus, validateConfig };
 //# sourceMappingURL=index.mjs.map
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 89d981d4d..ee3b1e1c6 100644
--- a/packages/govuk-frontend/dist/govuk/components/header/header.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/header/header.bundle.js
@@ -4,6 +4,36 @@
   (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.GOVUKFrontend = {}));
 })(this, (function (exports) { 'use strict';
 
+  function getBreakpoint(name) {
+    const property = `--govuk-frontend-breakpoint-${name}`;
+    const value = window.getComputedStyle(document.documentElement).getPropertyValue(property);
+    return {
+      property,
+      value: value || undefined
+    };
+  }
+  function isSupported($scope = document.body) {
+    if (!$scope) {
+      return false;
+    }
+    return $scope.classList.contains('govuk-frontend-supported');
+  }
+
+  /**
+   * Schema for component config
+   *
+   * @typedef {object} Schema
+   * @property {SchemaCondition[]} [anyOf] - List of schema conditions
+   */
+
+  /**
+   * Schema condition for component config
+   *
+   * @typedef {object} SchemaCondition
+   * @property {string[]} required - List of required config fields
+   * @property {string} errorMessage - Error message when required config fields not provided
+   */
+
   class GOVUKFrontendError extends Error {
     constructor(...args) {
       super(...args);
@@ -40,28 +70,6 @@
     }
   }
 
-  function isSupported($scope = document.body) {
-    if (!$scope) {
-      return false;
-    }
-    return $scope.classList.contains('govuk-frontend-supported');
-  }
-
-  /**
-   * Schema for component config
-   *
-   * @typedef {object} Schema
-   * @property {SchemaCondition[]} [anyOf] - List of schema conditions
-   */
-
-  /**
-   * Schema condition for component config
-   *
-   * @typedef {object} SchemaCondition
-   * @property {string[]} required - List of required config fields
-   * @property {string} errorMessage - Error message when required config fields not provided
-   */
-
   class GOVUKFrontendComponent {
     constructor() {
       this.checkSupport();
@@ -121,16 +129,26 @@
       }
       this.$menu = $menu;
       this.$menuButton = $menuButton;
-      this.mql = window.matchMedia('(min-width: 48.0625em)');
+      this.setupResponsiveChecks();
+      this.$menuButton.addEventListener('click', () => this.handleMenuButtonClick());
+    }
+    setupResponsiveChecks() {
+      const breakpoint = getBreakpoint('desktop');
+      if (!breakpoint.value) {
+        throw new ElementError({
+          componentName: 'Header',
+          identifier: `CSS custom property (\`${breakpoint.property}\`) on pseudo-class \`:root\``
+        });
+      }
+      this.mql = window.matchMedia(`(min-width: ${breakpoint.value})`);
       if ('addEventListener' in this.mql) {
-        this.mql.addEventListener('change', () => this.syncState());
+        this.mql.addEventListener('change', () => this.checkMode());
       } else {
-        this.mql.addListener(() => this.syncState());
+        this.mql.addListener(() => this.checkMode());
       }
-      this.syncState();
-      this.$menuButton.addEventListener('click', () => this.handleMenuButtonClick());
+      this.checkMode();
     }
-    syncState() {
+    checkMode() {
       if (!this.mql || !this.$menu || !this.$menuButton) {
         return;
       }
@@ -149,7 +167,7 @@
     }
     handleMenuButtonClick() {
       this.menuIsOpen = !this.menuIsOpen;
-      this.syncState();
+      this.checkMode();
     }
   }
   Header.moduleName = 'govuk-header';
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 f0060ade1..98e82f86d 100644
--- a/packages/govuk-frontend/dist/govuk/components/header/header.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/header/header.bundle.mjs
@@ -1,3 +1,33 @@
+function getBreakpoint(name) {
+  const property = `--govuk-frontend-breakpoint-${name}`;
+  const value = window.getComputedStyle(document.documentElement).getPropertyValue(property);
+  return {
+    property,
+    value: value || undefined
+  };
+}
+function isSupported($scope = document.body) {
+  if (!$scope) {
+    return false;
+  }
+  return $scope.classList.contains('govuk-frontend-supported');
+}
+
+/**
+ * Schema for component config
+ *
+ * @typedef {object} Schema
+ * @property {SchemaCondition[]} [anyOf] - List of schema conditions
+ */
+
+/**
+ * Schema condition for component config
+ *
+ * @typedef {object} SchemaCondition
+ * @property {string[]} required - List of required config fields
+ * @property {string} errorMessage - Error message when required config fields not provided
+ */
+
 class GOVUKFrontendError extends Error {
   constructor(...args) {
     super(...args);
@@ -34,28 +64,6 @@ class ElementError extends GOVUKFrontendError {
   }
 }
 
-function isSupported($scope = document.body) {
-  if (!$scope) {
-    return false;
-  }
-  return $scope.classList.contains('govuk-frontend-supported');
-}
-
-/**
- * Schema for component config
- *
- * @typedef {object} Schema
- * @property {SchemaCondition[]} [anyOf] - List of schema conditions
- */
-
-/**
- * Schema condition for component config
- *
- * @typedef {object} SchemaCondition
- * @property {string[]} required - List of required config fields
- * @property {string} errorMessage - Error message when required config fields not provided
- */
-
 class GOVUKFrontendComponent {
   constructor() {
     this.checkSupport();
@@ -115,16 +123,26 @@ class Header extends GOVUKFrontendComponent {
     }
     this.$menu = $menu;
     this.$menuButton = $menuButton;
-    this.mql = window.matchMedia('(min-width: 48.0625em)');
+    this.setupResponsiveChecks();
+    this.$menuButton.addEventListener('click', () => this.handleMenuButtonClick());
+  }
+  setupResponsiveChecks() {
+    const breakpoint = getBreakpoint('desktop');
+    if (!breakpoint.value) {
+      throw new ElementError({
+        componentName: 'Header',
+        identifier: `CSS custom property (\`${breakpoint.property}\`) on pseudo-class \`:root\``
+      });
+    }
+    this.mql = window.matchMedia(`(min-width: ${breakpoint.value})`);
     if ('addEventListener' in this.mql) {
-      this.mql.addEventListener('change', () => this.syncState());
+      this.mql.addEventListener('change', () => this.checkMode());
     } else {
-      this.mql.addListener(() => this.syncState());
+      this.mql.addListener(() => this.checkMode());
     }
-    this.syncState();
-    this.$menuButton.addEventListener('click', () => this.handleMenuButtonClick());
+    this.checkMode();
   }
-  syncState() {
+  checkMode() {
     if (!this.mql || !this.$menu || !this.$menuButton) {
       return;
     }
@@ -143,7 +161,7 @@ class Header extends GOVUKFrontendComponent {
   }
   handleMenuButtonClick() {
     this.menuIsOpen = !this.menuIsOpen;
-    this.syncState();
+    this.checkMode();
   }
 }
 Header.moduleName = 'govuk-header';
diff --git a/packages/govuk-frontend/dist/govuk/components/header/header.mjs b/packages/govuk-frontend/dist/govuk/components/header/header.mjs
index 329e89bda..cd6832271 100644
--- a/packages/govuk-frontend/dist/govuk/components/header/header.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/header/header.mjs
@@ -1,3 +1,4 @@
+import { getBreakpoint } from '../../common/index.mjs';
 import { ElementError } from '../../errors/index.mjs';
 import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs';
 
@@ -49,16 +50,26 @@ class Header extends GOVUKFrontendComponent {
     }
     this.$menu = $menu;
     this.$menuButton = $menuButton;
-    this.mql = window.matchMedia('(min-width: 48.0625em)');
+    this.setupResponsiveChecks();
+    this.$menuButton.addEventListener('click', () => this.handleMenuButtonClick());
+  }
+  setupResponsiveChecks() {
+    const breakpoint = getBreakpoint('desktop');
+    if (!breakpoint.value) {
+      throw new ElementError({
+        componentName: 'Header',
+        identifier: `CSS custom property (\`${breakpoint.property}\`) on pseudo-class \`:root\``
+      });
+    }
+    this.mql = window.matchMedia(`(min-width: ${breakpoint.value})`);
     if ('addEventListener' in this.mql) {
-      this.mql.addEventListener('change', () => this.syncState());
+      this.mql.addEventListener('change', () => this.checkMode());
     } else {
-      this.mql.addListener(() => this.syncState());
+      this.mql.addListener(() => this.checkMode());
     }
-    this.syncState();
-    this.$menuButton.addEventListener('click', () => this.handleMenuButtonClick());
+    this.checkMode();
   }
-  syncState() {
+  checkMode() {
     if (!this.mql || !this.$menu || !this.$menuButton) {
       return;
     }
@@ -77,7 +88,7 @@ class Header extends GOVUKFrontendComponent {
   }
   handleMenuButtonClick() {
     this.menuIsOpen = !this.menuIsOpen;
-    this.syncState();
+    this.checkMode();
   }
 }
 Header.moduleName = 'govuk-header';
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 af2dde6c2..bc832d2f8 100644
--- a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js
+++ b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.js
@@ -10,6 +10,14 @@
     }
     return url.split('#').pop();
   }
+  function getBreakpoint(name) {
+    const property = `--govuk-frontend-breakpoint-${name}`;
+    const value = window.getComputedStyle(document.documentElement).getPropertyValue(property);
+    return {
+      property,
+      value: value || undefined
+    };
+  }
   function isSupported($scope = document.body) {
     if (!$scope) {
       return false;
@@ -144,7 +152,14 @@
       this.setupResponsiveChecks();
     }
     setupResponsiveChecks() {
-      this.mql = window.matchMedia('(min-width: 40.0625em)');
+      const breakpoint = getBreakpoint('tablet');
+      if (!breakpoint.value) {
+        throw new ElementError({
+          componentName: 'Tabs',
+          identifier: `CSS custom property (\`${breakpoint.property}\`) on pseudo-class \`:root\``
+        });
+      }
+      this.mql = window.matchMedia(`(min-width: ${breakpoint.value})`);
       if ('addEventListener' in this.mql) {
         this.mql.addEventListener('change', () => this.checkMode());
       } else {
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 01113f302..ba301105f 100644
--- a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.bundle.mjs
@@ -4,6 +4,14 @@ function getFragmentFromUrl(url) {
   }
   return url.split('#').pop();
 }
+function getBreakpoint(name) {
+  const property = `--govuk-frontend-breakpoint-${name}`;
+  const value = window.getComputedStyle(document.documentElement).getPropertyValue(property);
+  return {
+    property,
+    value: value || undefined
+  };
+}
 function isSupported($scope = document.body) {
   if (!$scope) {
     return false;
@@ -138,7 +146,14 @@ class Tabs extends GOVUKFrontendComponent {
     this.setupResponsiveChecks();
   }
   setupResponsiveChecks() {
-    this.mql = window.matchMedia('(min-width: 40.0625em)');
+    const breakpoint = getBreakpoint('tablet');
+    if (!breakpoint.value) {
+      throw new ElementError({
+        componentName: 'Tabs',
+        identifier: `CSS custom property (\`${breakpoint.property}\`) on pseudo-class \`:root\``
+      });
+    }
+    this.mql = window.matchMedia(`(min-width: ${breakpoint.value})`);
     if ('addEventListener' in this.mql) {
       this.mql.addEventListener('change', () => this.checkMode());
     } else {
diff --git a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.mjs b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.mjs
index 943c4238a..f1c6e6ef2 100644
--- a/packages/govuk-frontend/dist/govuk/components/tabs/tabs.mjs
+++ b/packages/govuk-frontend/dist/govuk/components/tabs/tabs.mjs
@@ -1,4 +1,4 @@
-import { getFragmentFromUrl } from '../../common/index.mjs';
+import { getBreakpoint, getFragmentFromUrl } from '../../common/index.mjs';
 import { ElementError } from '../../errors/index.mjs';
 import { GOVUKFrontendComponent } from '../../govuk-frontend-component.mjs';
 
@@ -67,7 +67,14 @@ class Tabs extends GOVUKFrontendComponent {
     this.setupResponsiveChecks();
   }
   setupResponsiveChecks() {
-    this.mql = window.matchMedia('(min-width: 40.0625em)');
+    const breakpoint = getBreakpoint('tablet');
+    if (!breakpoint.value) {
+      throw new ElementError({
+        componentName: 'Tabs',
+        identifier: `CSS custom property (\`${breakpoint.property}\`) on pseudo-class \`:root\``
+      });
+    }
+    this.mql = window.matchMedia(`(min-width: ${breakpoint.value})`);
     if ('addEventListener' in this.mql) {
       this.mql.addEventListener('change', () => this.checkMode());
     } else {
diff --git a/packages/govuk-frontend/dist/govuk/core/_all.scss b/packages/govuk-frontend/dist/govuk/core/_all.scss
index a94480352..a9c87c554 100644
--- a/packages/govuk-frontend/dist/govuk/core/_all.scss
+++ b/packages/govuk-frontend/dist/govuk/core/_all.scss
@@ -1,4 +1,4 @@
-@import "govuk-frontend-version";
+@import "govuk-frontend-properties";
 @import "links";
 @import "lists";
 @import "typography";
diff --git a/packages/govuk-frontend/dist/govuk/core/_govuk-frontend-version.scss b/packages/govuk-frontend/dist/govuk/core/_govuk-frontend-properties.scss
similarity index 43%
rename from packages/govuk-frontend/dist/govuk/core/_govuk-frontend-version.scss
rename to packages/govuk-frontend/dist/govuk/core/_govuk-frontend-properties.scss
index 5d939df71..88f50e349 100644
--- a/packages/govuk-frontend/dist/govuk/core/_govuk-frontend-version.scss
+++ b/packages/govuk-frontend/dist/govuk/core/_govuk-frontend-properties.scss
@@ -2,6 +2,11 @@
   // This variable is automatically overwritten during builds and releases.
   // It doesn't need to be updated manually.
   --govuk-frontend-version: "development";
+
+  // CSS custom property for each breakpoint
+  @each $name, $value in $govuk-breakpoints {
+    --govuk-frontend-breakpoint-#{$name}: #{govuk-px-to-rem($value)};
+  }
 }
 
-/*# sourceMappingURL=_govuk-frontend-version.scss.map */
+/*# sourceMappingURL=_govuk-frontend-properties.scss.map */

Action run for 05b819a

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.

I like the pattern for passing values from CSS to JavaScript that's enabled by our new browser support!

Listed a couple of suggestions (only the message wording is really blocking). Also thinking it's a pattern worth sharing with the team at dev catch-up as it may be useful in other occasions 😊

Comment on lines 99 to 102
const breakpointProperty = '--govuk-frontend-breakpoint-desktop'
const breakpointValue = getComputedStyle(document.body).getPropertyValue(
breakpointProperty
)
Copy link
Member

Choose a reason for hiding this comment

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

question How would you feel about encapsulating that into a getBreakpointValue(breakpointName). Both to wrap what's needed to access the value out of each class and to see if services maybe start picking up the pattern by digging into our private code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Absolutely fine by me

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added getBreakpoint() in bf55711 as we'll still need the CSS property name for the error

const breakpoint = getBreakpoint('tablet')

// breakpoint.property
// breakpoint.value

*/
setupResponsiveChecks() {
const breakpointProperty = '--govuk-frontend-breakpoint-desktop'
const breakpointValue = getComputedStyle(document.body).getPropertyValue(
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: :root corresponds to document.documentElement. It would make the mapping between CSS and JS easier if they both reference the same element, even though the custom property value will cascade down to document.body.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah the issue said window.getComputedStyle() and I grabbed document.body from Slack

Shall I use document.documentElement for <html> instead?

You've suggested <body> below in #4562 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've gone with document.documentElement to represent :root

Copy link
Member

Choose a reason for hiding this comment

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

Oh damn sorry for the confusion in the following comment, I did indeed mean to align CSS and JS on the :root/document.documentElement.

if (!breakpointValue) {
throw new ElementError({
componentName: 'Header',
identifier: `CSS custom property (\`${breakpointProperty}\`)`
Copy link
Member

Choose a reason for hiding this comment

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

issue Thinking the message may lead to people looking to add a custom property on the root of the component's markup, while we look it up on the body 😊

Suggested change
identifier: `CSS custom property (\`${breakpointProperty}\`)`
identifier: `CSS custom property (\`${breakpointProperty}\`) on document's \`body\``

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How's this? Just pushed it up in 21a519d

ElementError: Tabs: CSS custom property (`--govuk-frontend-breakpoint-tablet`) on pseudo-class :root not found

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! Thanks for introducing the helper and picking document.documentElement. Approving, but I think there's a couple of backticks that are missing in the error messages around :root (won't need another review).

if (!breakpoint.value) {
throw new ElementError({
componentName: 'Header',
identifier: `CSS custom property (\`${breakpoint.property}\`) on pseudo-class :root`
Copy link
Member

Choose a reason for hiding this comment

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

nitpick I think there's a couple of backticks missing.

Suggested change
identifier: `CSS custom property (\`${breakpoint.property}\`) on pseudo-class :root`
identifier: `CSS custom property (\`${breakpoint.property}\`) on pseudo-class \`:root\``

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, left them off but will add 👍

Is it just types that we don't wrap in backticks?

Radios: Root element (`$module`) is not of type HTMLElement

@govuk-design-system-ci govuk-design-system-ci temporarily deployed to govuk-frontend-pr-4562 December 15, 2023 15:15 Inactive
@colinrotherham colinrotherham merged commit 3baa804 into main Dec 15, 2023
46 checks passed
@colinrotherham colinrotherham deleted the match-media branch December 15, 2023 15:40
owenatgov added a commit that referenced this pull request Jan 9, 2024
owenatgov added a commit that referenced this pull request Jan 10, 2024
owenatgov added a commit that referenced this pull request Jan 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.

Remove hardcoded media query breakpoints from the JavaScript for the header and tabs components
3 participants