Skip to content

Commit

Permalink
Merge pull request #4536 from alphagov/skip-link-urls
Browse files Browse the repository at this point in the history
Fix Skip link hash fragment check for Exit this page
  • Loading branch information
romaricpascal authored Dec 8, 2023
2 parents 23f58e7 + 5cf5c32 commit 16ae7ef
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,19 @@
redirectUrl: "https://www.gov.uk/"
}) }}
{% endblock %}

{% block bodyEnd %}
<script type="module" src="/javascripts/govuk-frontend.min.js"></script>
<script type="module">
import { ExitThisPage, SkipLink } from '/javascripts/govuk-frontend.min.js'
const $skipLinks = document.querySelectorAll('[data-module="govuk-skip-link"]')
const $exitThisPage = document.querySelector('[data-module="govuk-exit-this-page"]')
$skipLinks.forEach(($skipLink) => {
new SkipLink($skipLink)
})
new ExitThisPage($exitThisPage)
</script>
{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -36,24 +36,42 @@ export class SkipLink extends GOVUKFrontendComponent {
}

this.$module = $module
this.$linkedElement = this.getLinkedElement()

this.$module.addEventListener('click', () => this.focusLinkedElement())
}
const hash = this.$module.hash
const href = this.$module.getAttribute('href') ?? ''

/** @type {URL | undefined} */
let url

/**
* Check for valid link URL
*
* {@link https://caniuse.com/url}
* {@link https://url.spec.whatwg.org}
*
*/
try {
url = new window.URL(this.$module.href)
} catch (error) {
throw new ElementError(
`Skip link: Target link (\`href="${href}"\`) is invalid`
)
}

/**
* Get linked element
*
* @private
* @returns {HTMLElement} $linkedElement - Target of the skip link
*/
getLinkedElement() {
const linkedElementId = getFragmentFromUrl(this.$module.hash)
// Return early for external URLs or links to other pages
if (
url.origin !== window.location.origin ||
url.pathname !== window.location.pathname
) {
return
}

const linkedElementId = getFragmentFromUrl(hash)

// Check for link hash fragment
// Check link path matching current page
if (!linkedElementId) {
throw new ElementError(
'Skip link: Root element (`$module`) attribute (`href`) has no URL fragment'
`Skip link: Target link (\`href="${href}"\`) has no hash fragment`
)
}

Expand All @@ -68,7 +86,9 @@ export class SkipLink extends GOVUKFrontendComponent {
})
}

return $linkedElement
this.$linkedElement = $linkedElement

this.$module.addEventListener('click', () => this.focusLinkedElement())
}

/**
Expand All @@ -79,6 +99,10 @@ export class SkipLink extends GOVUKFrontendComponent {
* @private
*/
focusLinkedElement() {
if (!this.$linkedElement) {
return
}

if (!this.$linkedElement.getAttribute('tabindex')) {
// Set the element tabindex to -1 so it can be focused with JavaScript.
this.$linkedElement.setAttribute('tabindex', '-1')
Expand Down Expand Up @@ -107,6 +131,10 @@ export class SkipLink extends GOVUKFrontendComponent {
* @private
*/
removeFocusProperties() {
if (!this.$linkedElement) {
return
}

this.$linkedElement.removeAttribute('tabindex')
this.$linkedElement.classList.remove('govuk-skip-link-focused-element')
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,42 @@ describe('Skip Link', () => {
})

describe('errors at instantiation', () => {
it('can return early without errors for external href', async () => {
await render(page, 'skip-link', {
context: {
text: 'Exit this page',
href: 'https://www.bbc.co.uk/weather'
}
})
})

it('can return early without errors when linking to another page (without hash fragment)', async () => {
await render(page, 'skip-link', {
context: {
text: 'Exit this page',
href: '/clear-session-data'
}
})
})

it('can return early without errors when linking to another page (with hash fragment)', async () => {
await render(page, 'skip-link', {
context: {
text: 'Skip to main content',
href: '/somewhere-else#main-content'
}
})
})

it('can return early without errors when linking to the current page (with hash fragment)', async () => {
await render(page, 'skip-link', {
context: {
text: 'Skip to main content',
href: '#content'
}
})
})

it('can throw a SupportError if appropriate', async () => {
await expect(
render(page, 'skip-link', examples.default, {
Expand Down Expand Up @@ -137,14 +173,14 @@ describe('Skip Link', () => {
render(page, 'skip-link', {
context: {
text: 'Skip to main content',
href: 'this-element-does-not-exist'
href: '/components/skip-link/preview'
}
})
).rejects.toMatchObject({
cause: {
name: 'ElementError',
message:
'Skip link: Root element (`$module`) attribute (`href`) has no URL fragment'
'Skip link: Target link (`href="/components/skip-link/preview"`) has no hash fragment'
}
})
})
Expand Down

0 comments on commit 16ae7ef

Please sign in to comment.