govuk-frontend
Version:
GOV.UK Frontend contains the code you need to start building a user interface for government platforms and services.
425 lines (417 loc) • 12.6 kB
JavaScript
function getBreakpoint(name) {
const property = `--govuk-breakpoint-${name}`;
const value = window.getComputedStyle(document.documentElement).getPropertyValue(property);
return {
property,
value: value || undefined
};
}
function isInitialised($root, moduleName) {
return $root instanceof HTMLElement && $root.hasAttribute(`data-${moduleName}-init`);
}
/**
* Checks if GOV.UK Frontend is supported on this page
*
* Some browsers will load and run our JavaScript but GOV.UK Frontend
* won't be supported.
*
* @param {HTMLElement | null} [$scope] - (internal) `<body>` HTML element checked for browser support
* @returns {boolean} Whether GOV.UK Frontend is supported on this page
*/
function isSupported($scope = document.body) {
if (!$scope) {
return false;
}
return $scope.classList.contains('govuk-frontend-supported');
}
function isArray(option) {
return Array.isArray(option);
}
function isObject(option) {
return !!option && typeof option === 'object' && !isArray(option);
}
function formatErrorMessage(Component, message) {
return `${Component.moduleName}: ${message}`;
}
/**
* @typedef ComponentWithModuleName
* @property {string} moduleName - Name of the component
*/
class GOVUKFrontendError extends Error {
constructor(...args) {
super(...args);
this.name = 'GOVUKFrontendError';
}
}
class SupportError extends GOVUKFrontendError {
/**
* Checks if GOV.UK Frontend is supported on this page
*
* @param {HTMLElement | null} [$scope] - HTML element `<body>` checked for browser support
*/
constructor($scope = document.body) {
const supportMessage = 'noModule' in HTMLScriptElement.prototype ? 'GOV.UK Frontend initialised without `<body class="govuk-frontend-supported">` from template `<script>` snippet' : 'GOV.UK Frontend is not supported in this browser';
super($scope ? supportMessage : 'GOV.UK Frontend initialised without `<script type="module">`');
this.name = 'SupportError';
}
}
class ElementError extends GOVUKFrontendError {
constructor(messageOrOptions) {
let message = typeof messageOrOptions === 'string' ? messageOrOptions : '';
if (isObject(messageOrOptions)) {
const {
component,
identifier,
element,
expectedType
} = messageOrOptions;
message = identifier;
message += element ? ` is not of type ${expectedType != null ? expectedType : 'HTMLElement'}` : ' not found';
if (component) {
message = formatErrorMessage(component, message);
}
}
super(message);
this.name = 'ElementError';
}
}
class InitError extends GOVUKFrontendError {
constructor(componentOrMessage) {
const message = typeof componentOrMessage === 'string' ? componentOrMessage : formatErrorMessage(componentOrMessage, `Root element (\`$root\`) already initialised`);
super(message);
this.name = 'InitError';
}
}
/**
* @import { ComponentWithModuleName } from '../common/index.mjs'
*/
class Component {
/**
* Returns the root element of the component
*
* @protected
* @returns {RootElementType} - the root element of component
*/
get $root() {
return this._$root;
}
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();
const moduleName = childConstructor.moduleName;
this.$root.setAttribute(`data-${moduleName}-init`, '');
}
checkInitialised() {
const constructor = this.constructor;
const moduleName = constructor.moduleName;
if (moduleName && isInitialised(this.$root, moduleName)) {
throw new InitError(constructor);
}
}
static checkSupport() {
if (!isSupported()) {
throw new SupportError();
}
}
}
/**
* @typedef ChildClass
* @property {string} moduleName - The module name that'll be looked for in the DOM when initialising the component
*/
/**
* @typedef {typeof Component & ChildClass} ChildClassConstructor
*/
Component.elementType = HTMLElement;
/**
* Tabs component
*
* @preserve
*/
class Tabs extends Component {
/**
* @param {Element | null} $root - HTML element to use for tabs
*/
constructor($root) {
super($root);
this.$tabs = void 0;
this.$tabList = void 0;
this.$tabListItems = void 0;
this.jsHiddenClass = 'govuk-tabs__panel--hidden';
this.changingHash = false;
this.boundTabClick = void 0;
this.boundTabKeydown = void 0;
this.boundOnHashChange = void 0;
this.mql = null;
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.$tabs = $tabs;
this.boundTabClick = this.onTabClick.bind(this);
this.boundTabKeydown = this.onTabKeydown.bind(this);
this.boundOnHashChange = this.onHashChange.bind(this);
const $tabList = this.$root.querySelector('.govuk-tabs__list');
const $tabListItems = this.$root.querySelectorAll('li.govuk-tabs__list-item');
if (!$tabList) {
throw new ElementError({
component: Tabs,
identifier: 'List (`<ul class="govuk-tabs__list">`)'
});
}
if (!$tabListItems.length) {
throw new ElementError({
component: Tabs,
identifier: 'List items (`<li class="govuk-tabs__list-item">`)'
});
}
this.$tabList = $tabList;
this.$tabListItems = $tabListItems;
this.setupResponsiveChecks();
}
setupResponsiveChecks() {
const breakpoint = getBreakpoint('tablet');
if (!breakpoint.value) {
throw new ElementError({
component: 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 {
this.mql.addListener(() => this.checkMode());
}
this.checkMode();
}
checkMode() {
var _this$mql;
if ((_this$mql = this.mql) != null && _this$mql.matches) {
this.setup();
} else {
this.teardown();
}
}
setup() {
var _this$getTab;
this.$tabList.setAttribute('role', 'tablist');
this.$tabListItems.forEach($item => {
$item.setAttribute('role', 'presentation');
});
this.$tabs.forEach($tab => {
this.setAttributes($tab);
$tab.addEventListener('click', this.boundTabClick, true);
$tab.addEventListener('keydown', this.boundTabKeydown, true);
this.hideTab($tab);
});
const $activeTab = (_this$getTab = this.getTab(window.location.hash)) != null ? _this$getTab : this.$tabs[0];
this.showTab($activeTab);
window.addEventListener('hashchange', this.boundOnHashChange, true);
}
teardown() {
this.$tabList.removeAttribute('role');
this.$tabListItems.forEach($item => {
$item.removeAttribute('role');
});
this.$tabs.forEach($tab => {
$tab.removeEventListener('click', this.boundTabClick, true);
$tab.removeEventListener('keydown', this.boundTabKeydown, true);
this.unsetAttributes($tab);
});
window.removeEventListener('hashchange', this.boundOnHashChange, true);
}
onHashChange() {
const hash = window.location.hash;
const $tabWithHash = this.getTab(hash);
if (!$tabWithHash) {
return;
}
if (this.changingHash) {
this.changingHash = false;
return;
}
const $previousTab = this.getCurrentTab();
if (!$previousTab) {
return;
}
this.hideTab($previousTab);
this.showTab($tabWithHash);
$tabWithHash.focus();
}
hideTab($tab) {
this.unhighlightTab($tab);
this.hidePanel($tab);
}
showTab($tab) {
this.highlightTab($tab);
this.showPanel($tab);
}
getTab(hash) {
return this.$root.querySelector(`a.govuk-tabs__tab[href="${hash}"]`);
}
setAttributes($tab) {
const panelId = $tab.hash.replace('#', '');
if (!panelId) {
return;
}
$tab.setAttribute('id', `tab_${panelId}`);
$tab.setAttribute('role', 'tab');
$tab.setAttribute('aria-controls', panelId);
$tab.setAttribute('aria-selected', 'false');
$tab.setAttribute('tabindex', '-1');
const $panel = this.getPanel($tab);
if (!$panel) {
return;
}
$panel.setAttribute('role', 'tabpanel');
$panel.setAttribute('aria-labelledby', $tab.id);
$panel.classList.add(this.jsHiddenClass);
}
unsetAttributes($tab) {
$tab.removeAttribute('id');
$tab.removeAttribute('role');
$tab.removeAttribute('aria-controls');
$tab.removeAttribute('aria-selected');
$tab.removeAttribute('tabindex');
const $panel = this.getPanel($tab);
if (!$panel) {
return;
}
$panel.removeAttribute('role');
$panel.removeAttribute('aria-labelledby');
$panel.classList.remove(this.jsHiddenClass);
}
onTabClick(event) {
const $currentTab = this.getCurrentTab();
const $nextTab = event.currentTarget;
if (!$currentTab || !($nextTab instanceof HTMLAnchorElement)) {
return;
}
event.preventDefault();
this.hideTab($currentTab);
this.showTab($nextTab);
this.createHistoryEntry($nextTab);
}
createHistoryEntry($tab) {
const $panel = this.getPanel($tab);
if (!$panel) {
return;
}
const panelId = $panel.id;
$panel.id = '';
this.changingHash = true;
window.location.hash = panelId;
$panel.id = panelId;
}
onTabKeydown(event) {
switch (event.key) {
case 'ArrowLeft':
case 'Left':
this.activatePreviousTab();
event.preventDefault();
break;
case 'ArrowRight':
case 'Right':
this.activateNextTab();
event.preventDefault();
break;
}
}
activateNextTab() {
const $currentTab = this.getCurrentTab();
if (!($currentTab != null && $currentTab.parentElement)) {
return;
}
const $nextTabListItem = $currentTab.parentElement.nextElementSibling;
if (!$nextTabListItem) {
return;
}
const $nextTab = $nextTabListItem.querySelector('a.govuk-tabs__tab');
if (!$nextTab) {
return;
}
this.hideTab($currentTab);
this.showTab($nextTab);
$nextTab.focus();
this.createHistoryEntry($nextTab);
}
activatePreviousTab() {
const $currentTab = this.getCurrentTab();
if (!($currentTab != null && $currentTab.parentElement)) {
return;
}
const $previousTabListItem = $currentTab.parentElement.previousElementSibling;
if (!$previousTabListItem) {
return;
}
const $previousTab = $previousTabListItem.querySelector('a.govuk-tabs__tab');
if (!$previousTab) {
return;
}
this.hideTab($currentTab);
this.showTab($previousTab);
$previousTab.focus();
this.createHistoryEntry($previousTab);
}
getPanel($tab) {
const panelId = $tab.hash.replace('#', '');
if (!panelId) {
return null;
}
return this.$root.querySelector(`#${panelId}`);
}
showPanel($tab) {
const $panel = this.getPanel($tab);
if (!$panel) {
return;
}
$panel.classList.remove(this.jsHiddenClass);
}
hidePanel($tab) {
const $panel = this.getPanel($tab);
if (!$panel) {
return;
}
$panel.classList.add(this.jsHiddenClass);
}
unhighlightTab($tab) {
if (!$tab.parentElement) {
return;
}
$tab.setAttribute('aria-selected', 'false');
$tab.parentElement.classList.remove('govuk-tabs__list-item--selected');
$tab.setAttribute('tabindex', '-1');
}
highlightTab($tab) {
if (!$tab.parentElement) {
return;
}
$tab.setAttribute('aria-selected', 'true');
$tab.parentElement.classList.add('govuk-tabs__list-item--selected');
$tab.setAttribute('tabindex', '0');
}
getCurrentTab() {
return this.$root.querySelector('.govuk-tabs__list-item--selected a.govuk-tabs__tab');
}
}
Tabs.moduleName = 'govuk-tabs';
export { Tabs };
//# sourceMappingURL=tabs.bundle.mjs.map