diff --git a/packages/govuk-frontend-review/src/views/examples/exit-this-page-with-skiplink/index.njk b/packages/govuk-frontend-review/src/views/examples/exit-this-page-with-skiplink/index.njk index 898d54a3fe..419c2a6e57 100644 --- a/packages/govuk-frontend-review/src/views/examples/exit-this-page-with-skiplink/index.njk +++ b/packages/govuk-frontend-review/src/views/examples/exit-this-page-with-skiplink/index.njk @@ -20,3 +20,19 @@ redirectUrl: "https://www.gov.uk/" }) }} {% endblock %} + +{% block bodyEnd %} + + +{% endblock %} diff --git a/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.mjs b/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.mjs index fbac41f9a9..c81f2e8eab 100644 --- a/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.mjs +++ b/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.mjs @@ -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` ) } @@ -68,7 +86,9 @@ export class SkipLink extends GOVUKFrontendComponent { }) } - return $linkedElement + this.$linkedElement = $linkedElement + + this.$module.addEventListener('click', () => this.focusLinkedElement()) } /** @@ -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') @@ -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') } diff --git a/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.puppeteer.test.js b/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.puppeteer.test.js index 55b0ae2ce6..cd3b7f8cc1 100644 --- a/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.puppeteer.test.js +++ b/packages/govuk-frontend/src/govuk/components/skip-link/skip-link.puppeteer.test.js @@ -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, { @@ -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' } }) })