From bcc23c537959b8621544168fe717df9e94768a8b Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Thu, 9 Jan 2025 14:45:01 +0000 Subject: [PATCH] Give main content area a focus style The main content area has the following markup: ```
...
``` `div.app-pane__content` has `tabindex=0` to make it focusable (`div`s are not by default). It is focusable so keyboard users can scroll it, using their arrow keys when it is tabbed to. This is important because the docs' page is split into 2 panes that scroll independently. `main` is focused when you click the 'skip to main content' link, at the start of the page, using the design system skip link component: https://design-system.service.gov.uk/components/skip-link/ These changes don't try to merge these tags but rather make the visuals show a single focus style for both, because users shouldn't care which one is focused. Both alow you to scroll the pane and represent the main content area. This commit also includes a change that removes the outline from links in the table of contents. Testing the other changes in this commit, I saw the outline style from the browser styles applied to table of contents links. focus-visible styles are displayed based on browser heuristics, which seem to kick in when the other changes are applied. This cancels them on the link without removing them from the child ``. --- .../javascripts/_modules/table-of-contents.js | 21 ++--- lib/assets/stylesheets/modules/_app-pane.scss | 7 ++ lib/source/layouts/core.erb | 2 +- spec/javascripts/table-of-contents-spec.js | 79 ++++++++++++------- 4 files changed, 71 insertions(+), 38 deletions(-) diff --git a/lib/assets/javascripts/_modules/table-of-contents.js b/lib/assets/javascripts/_modules/table-of-contents.js index 32229f48..ea703493 100644 --- a/lib/assets/javascripts/_modules/table-of-contents.js +++ b/lib/assets/javascripts/_modules/table-of-contents.js @@ -104,7 +104,6 @@ function openNavigation () { $html.addClass('toc-open') - toggleBackgroundVisiblity(false) updateAriaAttributes() $toc.focus() } @@ -112,26 +111,30 @@ function closeNavigation () { $html.removeClass('toc-open') - toggleBackgroundVisiblity(true) - $openButton.focus() updateAriaAttributes() - } - - function toggleBackgroundVisiblity (visibility) { - $('.toc-open-disabled').attr('aria-hidden', visibility ? '' : 'true') + $openButton.focus() } function updateAriaAttributes () { var tocIsVisible = $toc.is(':visible') - var openButtonIsVisible = $openButton.is(':visible') + var tocIsDialog = $openButton.is(':visible') $($openButton).add($closeButton) .attr('aria-expanded', tocIsVisible ? 'true' : 'false') $toc.attr({ 'aria-hidden': tocIsVisible ? 'false' : 'true', - role: openButtonIsVisible ? 'dialog' : null + role: tocIsDialog ? 'dialog' : null }) + + $('.app-pane__content').attr('aria-hidden', (tocIsDialog && tocIsVisible) ? 'true' : 'false') + + // only make main content pane focusable if it scrolls independently of the toc + if (!tocIsDialog) { + $('.app-pane__content').attr('tabindex', '0') + } else { + $('.app-pane__content').removeAttr('tabindex') + } } function preventingScrolling (callback) { diff --git a/lib/assets/stylesheets/modules/_app-pane.scss b/lib/assets/stylesheets/modules/_app-pane.scss index 9e82194c..1874a2c8 100644 --- a/lib/assets/stylesheets/modules/_app-pane.scss +++ b/lib/assets/stylesheets/modules/_app-pane.scss @@ -61,3 +61,10 @@ } } +.app-pane__content:focus-visible, +.app-pane__content:has(main:focus-visible) { + outline: $govuk-focus-width solid transparent; + box-shadow: + 0 0 0 4px $govuk-focus-colour, + 0 0 0 8px $govuk-focus-text-colour; +} diff --git a/lib/source/layouts/core.erb b/lib/source/layouts/core.erb index d86aa1d3..499083dd 100644 --- a/lib/source/layouts/core.erb +++ b/lib/source/layouts/core.erb @@ -58,7 +58,7 @@ <% end %> -
+
<%= yield %> <%= partial "layouts/page_review" %> diff --git a/spec/javascripts/table-of-contents-spec.js b/spec/javascripts/table-of-contents-spec.js index c750c302..dc9412a3 100644 --- a/spec/javascripts/table-of-contents-spec.js +++ b/spec/javascripts/table-of-contents-spec.js @@ -5,6 +5,7 @@ describe('Table of contents', function () { var $html var $tocBase var $toc + var $mainContentPane var $closeButton var $openButton var $tocStickyHeader @@ -13,28 +14,33 @@ describe('Table of contents', function () { beforeAll(function () { $html = $('html') $tocBase = $( - '
' + - '' ) - .append($toc) + .append($tocClone) + $toc = $tocClone.eq(0).find('.toc') + $mainContentPane = $tocClone.eq(1) $closeButton = $toc.find('.js-toc-close') $openButton = $html.find('.js-toc-show') @@ -73,14 +81,15 @@ describe('Table of contents', function () { // clear up any classes left on $html.removeClass('toc-open') $html.find('body #toc-heading').remove() - $html.find('body .toc').remove() + $html.find('body .app-pane__toc').remove() + $html.find('body .app-pane__content').remove() if ($tocStickyHeader && $tocStickyHeader.length) { $tocStickyHeader.remove() } }) describe('when the module is started', function () { - it('on a mobile-size screen, it should mark the table of contents as hidden', function () { + it('on a mobile-size screen, it should hide the table of contents and stop the main content pane being focusable', function () { // styles applied by this test simulate the styles media-queries will apply on real web pages // the .mobile-size class hides the table of contents and the open button $html.addClass('mobile-size') // simulate the styles media-queries will apply on real web pages @@ -89,11 +98,12 @@ describe('Table of contents', function () { module.start($toc) expect($toc.attr('aria-hidden')).toEqual('true') + expect($mainContentPane.get(0).hasAttribute('tabindex')).toBe(false) $html.removeClass('mobile-size') }) - it('on a desktop-size screen, it should mark the table of contents as visible', function () { + it('on a desktop-size screen, it should show the table of contents and make the main content pane focusable', function () { // styles applied by this test simulate the styles media-queries will apply on real web pages // by default, they show the table of contents @@ -101,6 +111,7 @@ describe('Table of contents', function () { module.start($toc) expect($toc.attr('aria-hidden')).toEqual('false') + expect($mainContentPane.attr('tabindex')).toEqual('0') }) }) @@ -156,10 +167,15 @@ describe('Table of contents', function () { describe('if the open button is clicked', function () { beforeEach(function () { + $html.addClass('mobile-size') module = new GOVUK.Modules.TableOfContents() module.start($toc) }) + afterEach(function () { + $html.removeClass('toc-open mobile-size') + }) + it('the click event should be cancelled', function () { var clickEvt = new $.Event('click') @@ -168,7 +184,7 @@ describe('Table of contents', function () { expect(clickEvt.isDefaultPrevented()).toBe(true) }) - it('the table of contents should show and be focused', function () { + it('the table of contents should show and be focused and the main content hidden', function () { // detecting focus has proved unreliable so track calls to $toc.focus instead var _focus = $.fn.focus var tocFocusSpy = jasmine.createSpy('tocFocusSpy') @@ -188,6 +204,7 @@ describe('Table of contents', function () { $openButton.trigger(clickEvt) expect($toc.attr('aria-hidden')).toEqual('false') + expect($mainContentPane.attr('aria-hidden')).toEqual('true') expect(tocFocusSpy).toHaveBeenCalled() @@ -229,9 +246,13 @@ describe('Table of contents', function () { it('the button that triggered the dialog is refocused', function () { expect(document.activeElement).toBe($openButton.get(0)) }) + + it('the main content area should be shown', function () { + expect($mainContentPane.attr('aria-hidden')).toEqual('false') + }) }) - it('on mobile-size screens, when the table of contents is open and the escape key is activated, the table of contents should be hidden', function () { + it('on mobile-size screens, when the table of contents is open and the escape key is activated, the table of contents should be hidden and the main content shown', function () { $html.addClass('mobile-size') module = new GOVUK.Modules.TableOfContents() @@ -244,6 +265,8 @@ describe('Table of contents', function () { })) expect($html.hasClass('toc-open')).toBe(false) + expect($toc.attr('aria-hidden')).toEqual('true') + expect($mainContentPane.attr('aria-hidden')).toEqual('false') $html.removeClass('mobile-size') })