Skip to content

Commit d7f16a6

Browse files
committed
Give main content area a focus style
The main content area has the following markup: ``` <div class="app-pane__content" tabindex="0"> <main> ... </main> </div> ``` `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 `<span>`.
1 parent a64cf29 commit d7f16a6

File tree

4 files changed

+71
-38
lines changed

4 files changed

+71
-38
lines changed

lib/assets/javascripts/_modules/table-of-contents.js

+12-9
Original file line numberDiff line numberDiff line change
@@ -104,34 +104,37 @@
104104
function openNavigation () {
105105
$html.addClass('toc-open')
106106

107-
toggleBackgroundVisiblity(false)
108107
updateAriaAttributes()
109108
$toc.focus()
110109
}
111110

112111
function closeNavigation () {
113112
$html.removeClass('toc-open')
114113

115-
toggleBackgroundVisiblity(true)
116-
$openButton.focus()
117114
updateAriaAttributes()
118-
}
119-
120-
function toggleBackgroundVisiblity (visibility) {
121-
$('.toc-open-disabled').attr('aria-hidden', visibility ? '' : 'true')
115+
$openButton.focus()
122116
}
123117

124118
function updateAriaAttributes () {
125119
var tocIsVisible = $toc.is(':visible')
126-
var openButtonIsVisible = $openButton.is(':visible')
120+
var tocIsDialog = $openButton.is(':visible')
127121

128122
$($openButton).add($closeButton)
129123
.attr('aria-expanded', tocIsVisible ? 'true' : 'false')
130124

131125
$toc.attr({
132126
'aria-hidden': tocIsVisible ? 'false' : 'true',
133-
role: openButtonIsVisible ? 'dialog' : null
127+
role: tocIsDialog ? 'dialog' : null
134128
})
129+
130+
$('.app-pane__content').attr('aria-hidden', (tocIsDialog && tocIsVisible) ? 'true' : 'false')
131+
132+
// only make main content pane focusable if it scrolls independently of the toc
133+
if (!tocIsDialog) {
134+
$('.app-pane__content').attr('tabindex', '0')
135+
} else {
136+
$('.app-pane__content').removeAttr('tabindex')
137+
}
135138
}
136139

137140
function preventingScrolling (callback) {

lib/assets/stylesheets/modules/_app-pane.scss

+7
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,10 @@
6161
}
6262
}
6363

64+
.app-pane__content:focus-visible,
65+
.app-pane__content:has(main:focus-visible) {
66+
outline: $govuk-focus-width solid transparent;
67+
box-shadow:
68+
0 0 0 4px $govuk-focus-colour,
69+
0 0 0 8px $govuk-focus-text-colour;
70+
}

lib/source/layouts/core.erb

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
</div>
5959
<% end %>
6060

61-
<div class="app-pane__content toc-open-disabled" aria-label="Content" tabindex="0">
61+
<div class="app-pane__content toc-open-disabled" aria-label="Content">
6262
<main id="content" class="technical-documentation" data-module="anchored-headings">
6363
<%= yield %>
6464
<%= partial "layouts/page_review" %>

spec/javascripts/table-of-contents-spec.js

+51-28
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ describe('Table of contents', function () {
55
var $html
66
var $tocBase
77
var $toc
8+
var $mainContentPane
89
var $closeButton
910
var $openButton
1011
var $tocStickyHeader
@@ -13,28 +14,33 @@ describe('Table of contents', function () {
1314
beforeAll(function () {
1415
$html = $('html')
1516
$tocBase = $(
16-
'<div class="toc" data-module="table-of-contents" tabindex="-1" aria-label="Table of contents">' +
17-
'<div class="search" data-module="search" data-path-to-site-root="/">' +
18-
'<form action="/search/index.html" method="get" role="search" class="search__form govuk-!-margin-bottom-4">' +
19-
'<label class="govuk-label search__label" for="search">Search this documentation</label>' +
20-
'<input type="text" id="search" name="q" class="govuk-input govuk-!-margin-bottom-0 search__input" aria-controls="search-results" placeholder="Search">' +
21-
'<button type="submit" class="search__button">Search</button>' +
22-
'</form>' +
17+
'<div class="app-pane__toc">' +
18+
'<div class="toc" data-module="table-of-contents" tabindex="-1" aria-label="Table of contents">' +
19+
'<div class="search" data-module="search" data-path-to-site-root="/">' +
20+
'<form action="/search/index.html" method="get" role="search" class="search__form govuk-!-margin-bottom-4">' +
21+
'<label class="govuk-label search__label" for="search">Search this documentation</label>' +
22+
'<input type="text" id="search" name="q" class="govuk-input govuk-!-margin-bottom-0 search__input" aria-controls="search-results" placeholder="Search">' +
23+
'<button type="submit" class="search__button">Search</button>' +
24+
'</form>' +
25+
'</div>' +
26+
'<button type="button" class="toc__close js-toc-close" aria-controls="toc" aria-label="Hide table of contents"></button>' +
27+
'<nav id="toc" class="js-toc-list toc__list" aria-labelledby="toc-heading" data-module="collapsible-navigation">' +
28+
'<ul>' +
29+
'<li>' +
30+
'<a href="/"><span>Technical Documentation Template</span></a>' +
31+
'</li>' +
32+
'<li>' +
33+
'<a href="/"><span>Get started</span></a>' +
34+
'</li>' +
35+
'<li>' +
36+
'<a href="/"><span>Configure your documentation site</span></a>' +
37+
'</li>' +
38+
'</ul>' +
39+
'</nav>' +
2340
'</div>' +
24-
'<button type="button" class="toc__close js-toc-close" aria-controls="toc" aria-label="Hide table of contents"></button>' +
25-
'<nav id="toc" class="js-toc-list toc__list" aria-labelledby="toc-heading" data-module="collapsible-navigation">' +
26-
'<ul>' +
27-
'<li>' +
28-
'<a href="/"><span>Technical Documentation Template</span></a>' +
29-
'</li>' +
30-
'<li>' +
31-
'<a href="/"><span>Get started</span></a>' +
32-
'</li>' +
33-
'<li>' +
34-
'<a href="/"><span>Configure your documentation site</span></a>' +
35-
'</li>' +
36-
'</ul>' +
37-
'</nav>' +
41+
'</div>' +
42+
'<div class="app-pane__content toc-open-disabled" aria-label="content">' +
43+
'<main id="content"></main>' +
3844
'</div>'
3945
)
4046

@@ -51,7 +57,7 @@ describe('Table of contents', function () {
5157
})
5258

5359
beforeEach(function () {
54-
$toc = $tocBase.clone()
60+
var $tocClone = $tocBase.clone()
5561

5662
$html.find('body')
5763
.append(
@@ -61,8 +67,10 @@ describe('Table of contents', function () {
6167
'</button>' +
6268
'</div>'
6369
)
64-
.append($toc)
70+
.append($tocClone)
6571

72+
$toc = $tocClone.eq(0).find('.toc')
73+
$mainContentPane = $tocClone.eq(1)
6674
$closeButton = $toc.find('.js-toc-close')
6775
$openButton = $html.find('.js-toc-show')
6876

@@ -73,14 +81,15 @@ describe('Table of contents', function () {
7381
// clear up any classes left on <html>
7482
$html.removeClass('toc-open')
7583
$html.find('body #toc-heading').remove()
76-
$html.find('body .toc').remove()
84+
$html.find('body .app-pane__toc').remove()
85+
$html.find('body .app-pane__content').remove()
7786
if ($tocStickyHeader && $tocStickyHeader.length) {
7887
$tocStickyHeader.remove()
7988
}
8089
})
8190

8291
describe('when the module is started', function () {
83-
it('on a mobile-size screen, it should mark the table of contents as hidden', function () {
92+
it('on a mobile-size screen, it should hide the table of contents and stop the main content pane being focusable', function () {
8493
// styles applied by this test simulate the styles media-queries will apply on real web pages
8594
// the .mobile-size class hides the table of contents and the open button
8695
$html.addClass('mobile-size') // simulate the styles media-queries will apply on real web pages
@@ -89,18 +98,20 @@ describe('Table of contents', function () {
8998
module.start($toc)
9099

91100
expect($toc.attr('aria-hidden')).toEqual('true')
101+
expect($mainContentPane.get(0).hasAttribute('tabindex')).toBe(false)
92102

93103
$html.removeClass('mobile-size')
94104
})
95105

96-
it('on a desktop-size screen, it should mark the table of contents as visible', function () {
106+
it('on a desktop-size screen, it should show the table of contents and make the main content pane focusable', function () {
97107
// styles applied by this test simulate the styles media-queries will apply on real web pages
98108
// by default, they show the table of contents
99109

100110
module = new GOVUK.Modules.TableOfContents()
101111
module.start($toc)
102112

103113
expect($toc.attr('aria-hidden')).toEqual('false')
114+
expect($mainContentPane.attr('tabindex')).toEqual('0')
104115
})
105116
})
106117

@@ -156,10 +167,15 @@ describe('Table of contents', function () {
156167

157168
describe('if the open button is clicked', function () {
158169
beforeEach(function () {
170+
$html.addClass('mobile-size')
159171
module = new GOVUK.Modules.TableOfContents()
160172
module.start($toc)
161173
})
162174

175+
afterEach(function () {
176+
$html.removeClass('toc-open mobile-size')
177+
})
178+
163179
it('the click event should be cancelled', function () {
164180
var clickEvt = new $.Event('click')
165181

@@ -168,7 +184,7 @@ describe('Table of contents', function () {
168184
expect(clickEvt.isDefaultPrevented()).toBe(true)
169185
})
170186

171-
it('the table of contents should show and be focused', function () {
187+
it('the table of contents should show and be focused and the main content hidden', function () {
172188
// detecting focus has proved unreliable so track calls to $toc.focus instead
173189
var _focus = $.fn.focus
174190
var tocFocusSpy = jasmine.createSpy('tocFocusSpy')
@@ -188,6 +204,7 @@ describe('Table of contents', function () {
188204
$openButton.trigger(clickEvt)
189205

190206
expect($toc.attr('aria-hidden')).toEqual('false')
207+
expect($mainContentPane.attr('aria-hidden')).toEqual('true')
191208

192209
expect(tocFocusSpy).toHaveBeenCalled()
193210

@@ -229,9 +246,13 @@ describe('Table of contents', function () {
229246
it('the button that triggered the dialog is refocused', function () {
230247
expect(document.activeElement).toBe($openButton.get(0))
231248
})
249+
250+
it('the main content area should be shown', function () {
251+
expect($mainContentPane.attr('aria-hidden')).toEqual('false')
252+
})
232253
})
233254

234-
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 () {
255+
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 () {
235256
$html.addClass('mobile-size')
236257

237258
module = new GOVUK.Modules.TableOfContents()
@@ -244,6 +265,8 @@ describe('Table of contents', function () {
244265
}))
245266

246267
expect($html.hasClass('toc-open')).toBe(false)
268+
expect($toc.attr('aria-hidden')).toEqual('true')
269+
expect($mainContentPane.attr('aria-hidden')).toEqual('false')
247270

248271
$html.removeClass('mobile-size')
249272
})

0 commit comments

Comments
 (0)