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

Throw errors during Tabs initialisation if key HTML elements are missing #4263

Merged
merged 6 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 42 additions & 28 deletions packages/govuk-frontend/src/govuk/components/tabs/tabs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ export class Tabs extends GOVUKFrontendComponent {
/** @private */
$tabs

/** @private */
$tabList

/** @private */
$tabListItems

/** @private */
keys = { left: 37, right: 39, up: 38, down: 40 }

Expand Down Expand Up @@ -53,7 +59,10 @@ export class Tabs extends GOVUKFrontendComponent {
/** @satisfies {NodeListOf<HTMLAnchorElement>} */
const $tabs = $module.querySelectorAll('a.govuk-tabs__tab')
if (!$tabs.length) {
return this
throw new ElementError(null, {
componentName: 'Tabs',
identifier: `a.govuk-tabs__tab`
})
}

this.$module = $module
Expand All @@ -64,6 +73,28 @@ export class Tabs extends GOVUKFrontendComponent {
this.boundTabKeydown = this.onTabKeydown.bind(this)
this.boundOnHashChange = this.onHashChange.bind(this)

const $tabList = this.$module.querySelector('.govuk-tabs__list')
const $tabListItems = this.$module.querySelectorAll(
'li.govuk-tabs__list-item'
)

if (!$tabList) {
throw new ElementError(null, {
componentName: 'Tabs',
identifier: `.govuk-tabs__list`
})
}

if (!$tabListItems.length) {
throw new ElementError(null, {
componentName: 'Tabs',
identifier: `.govuk-tabs__list-item`
})
}

this.$tabList = $tabList
this.$tabListItems = $tabListItems
colinrotherham marked this conversation as resolved.
Show resolved Hide resolved

this.setupResponsiveChecks()
}

Expand All @@ -78,11 +109,15 @@ export class Tabs extends GOVUKFrontendComponent {
// MediaQueryList.addEventListener isn't supported by Safari < 14 so we need
// to be able to fall back to the deprecated MediaQueryList.addListener
if ('addEventListener' in this.mql) {
this.mql.addEventListener('change', () => this.checkMode())
this.mql.addEventListener('change', () => {
this.checkMode()
})
} else {
// @ts-expect-error Property 'addListener' does not exist
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
this.mql.addListener(() => this.checkMode())
this.mql.addListener(() => {
this.checkMode()
})
}

this.checkMode()
Expand All @@ -107,18 +142,9 @@ export class Tabs extends GOVUKFrontendComponent {
* @private
*/
setup() {
const $tabList = this.$module.querySelector('.govuk-tabs__list')
const $tabListItems = this.$module.querySelectorAll(
'.govuk-tabs__list-item'
)

if (!this.$tabs || !$tabList || !$tabListItems) {
Copy link
Contributor

Choose a reason for hiding this comment

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

🙌

return
}

$tabList.setAttribute('role', 'tablist')
this.$tabList.setAttribute('role', 'tablist')

$tabListItems.forEach(($item) => {
this.$tabListItems.forEach(($item) => {
$item.setAttribute('role', 'presentation')
})

Expand All @@ -136,9 +162,6 @@ export class Tabs extends GOVUKFrontendComponent {

// Show either the active tab according to the URL's hash or the first tab
const $activeTab = this.getTab(window.location.hash) || this.$tabs[0]
if (!$activeTab) {
return
}

this.showTab($activeTab)

Expand All @@ -152,18 +175,9 @@ export class Tabs extends GOVUKFrontendComponent {
* @private
*/
teardown() {
const $tabList = this.$module.querySelector('.govuk-tabs__list')
const $tabListItems = this.$module.querySelectorAll(
'a.govuk-tabs__list-item'
)

if (!this.$tabs || !$tabList || !$tabListItems) {
return
}

$tabList.removeAttribute('role')
this.$tabList.removeAttribute('role')

$tabListItems.forEach(($item) => {
this.$tabListItems.forEach(($item) => {
$item.removeAttribute('role')
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,56 @@ describe('/components/tabs', () => {
message: 'Tabs: $module is not an instance of HTMLElement'
})
})

it('throws when there are no tabs', async () => {
await expect(
renderAndInitialise(page, 'tabs', {
params: examples.default,
beforeInitialisation($module) {
$module.querySelectorAll('a.govuk-tabs__tab').forEach((item) => {
item.remove()
})
}
})
).rejects.toEqual({
name: 'ElementError',
message: 'Tabs: a.govuk-tabs__tab not found'
})
})

it('throws when the tab list is missing', async () => {
await expect(
renderAndInitialise(page, 'tabs', {
params: examples.default,
beforeInitialisation($module) {
$module
.querySelector('.govuk-tabs__list')
.setAttribute('class', 'govuk-tabs__typo')
}
})
).rejects.toEqual({
name: 'ElementError',
message: 'Tabs: .govuk-tabs__list not found'
})
})

it('throws when there the tab list is empty', async () => {
await expect(
renderAndInitialise(page, 'tabs', {
params: examples.default,
beforeInitialisation($module) {
$module
.querySelectorAll('.govuk-tabs__list-item')
.forEach((item) =>
item.setAttribute('class', '.govuk-tabs__list-typo')
)
}
})
).rejects.toEqual({
name: 'ElementError',
message: 'Tabs: .govuk-tabs__list-item not found'
})
})
})
})
})
2 changes: 1 addition & 1 deletion packages/govuk-frontend/src/govuk/errors/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export class ElementError extends GOVUKFrontendError {
name = 'ElementError'

/**
* @param {Element} element - The element in error
* @param {Element | null} element - The element in error
* @param {object} options - Element error options
* @param {string} options.componentName - The name of the component throwing the error
* @param {string} options.identifier - An identifier that'll let the user understand which element has an error (variable name, CSS selector)
Expand Down