Skip to content

Commit 58e7c76

Browse files
authored
Merge pull request #381 from alphagov/fix-focus-issues
Fix focus issues
2 parents e65e6d0 + bcc23c5 commit 58e7c76

File tree

4 files changed

+83
-41
lines changed

4 files changed

+83
-41
lines changed

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

+12-8
Original file line numberDiff line numberDiff line change
@@ -104,33 +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)
116114
updateAriaAttributes()
117-
}
118-
119-
function toggleBackgroundVisiblity (visibility) {
120-
$('.toc-open-disabled').attr('aria-hidden', visibility ? '' : 'true')
115+
$openButton.focus()
121116
}
122117

123118
function updateAriaAttributes () {
124119
var tocIsVisible = $toc.is(':visible')
125-
var openButtonIsVisible = $openButton.is(':visible')
120+
var tocIsDialog = $openButton.is(':visible')
126121

127122
$($openButton).add($closeButton)
128123
.attr('aria-expanded', tocIsVisible ? 'true' : 'false')
129124

130125
$toc.attr({
131126
'aria-hidden': tocIsVisible ? 'false' : 'true',
132-
role: openButtonIsVisible ? 'dialog' : null
127+
role: tocIsDialog ? 'dialog' : null
133128
})
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+
}
134138
}
135139

136140
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

+63-32
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

@@ -197,7 +214,8 @@ describe('Table of contents', function () {
197214
})
198215

199216
describe('if the close button is clicked', function () {
200-
var clickEvt
217+
var openClickEvt
218+
var closeClickEvt
201219

202220
beforeEach(function () {
203221
$html.addClass('mobile-size')
@@ -206,24 +224,35 @@ describe('Table of contents', function () {
206224
module.start($toc)
207225

208226
// tocIsVisible = false // controls what $toc.is(':visible') returns, which will be controlled by CSS in a web page
209-
clickEvt = new $.Event('click')
210-
$closeButton.trigger(clickEvt)
227+
openClickEvt = new $.Event('click')
228+
closeClickEvt = new $.Event('click')
229+
230+
$openButton.trigger(openClickEvt)
231+
$closeButton.trigger(closeClickEvt)
211232
})
212233

213234
afterEach(function () {
214235
$html.removeClass('mobile-size')
215236
})
216237

217238
it('the click event should be cancelled', function () {
218-
expect(clickEvt.isDefaultPrevented()).toBe(true)
239+
expect(closeClickEvt.isDefaultPrevented()).toBe(true)
219240
})
220241

221242
it('the table of contents should be hidden', function () {
222243
expect($toc.attr('aria-hidden')).toEqual('true')
223244
})
245+
246+
it('the button that triggered the dialog is refocused', function () {
247+
expect(document.activeElement).toBe($openButton.get(0))
248+
})
249+
250+
it('the main content area should be shown', function () {
251+
expect($mainContentPane.attr('aria-hidden')).toEqual('false')
252+
})
224253
})
225254

226-
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 () {
227256
$html.addClass('mobile-size')
228257

229258
module = new GOVUK.Modules.TableOfContents()
@@ -236,6 +265,8 @@ describe('Table of contents', function () {
236265
}))
237266

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

240271
$html.removeClass('mobile-size')
241272
})

0 commit comments

Comments
 (0)