Skip to content

Commit

Permalink
Merge pull request #358 from alphagov/only-use-dialog-on-mobile
Browse files Browse the repository at this point in the history
Only use dialog on mobile
  • Loading branch information
tombye authored Sep 18, 2024
2 parents 45d4cf8 + 280ce40 commit 13ada11
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Declare some missing indirect dependencies to prepare for Ruby 3.4. This also resolves some warnings about this at build time.
- Remove aria-hidden from search label to let assistive technologies see its accessible name
- Use hidden attribute to show/hide expiry notices instead of just CSS
- Only use dialog role for table of contents when it behaves like one (accessibility fix)

## 3.5.0

Expand Down
6 changes: 5 additions & 1 deletion lib/assets/javascripts/_modules/table-of-contents.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,15 @@

function updateAriaAttributes () {
var tocIsVisible = $toc.is(':visible')
var openButtonIsVisible = $openButton.is(':visible')

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

$toc.attr('aria-hidden', tocIsVisible ? 'false' : 'true')
$toc.attr({
'aria-hidden': tocIsVisible ? 'false' : 'true',
role: openButtonIsVisible ? 'dialog' : null
})
}

function preventingScrolling (callback) {
Expand Down
2 changes: 1 addition & 1 deletion lib/source/layouts/core.erb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
<div class="app-pane__body"<%= " data-module=\"#{yield_content(:toc_module)}\"" if content_for? :toc_module %>>
<% if content_for? :sidebar %>
<div class="app-pane__toc">
<div class="toc" data-module="table-of-contents" tabindex="-1" aria-label="Table of contents" role="dialog">
<div class="toc" data-module="table-of-contents" tabindex="-1" aria-label="Table of contents">
<%= partial "layouts/search" %>
<button type="button" class="toc__close js-toc-close" aria-controls="toc" aria-label="Hide table of contents"></button>
<nav id="toc" class="js-toc-list toc__list" aria-labelledby="toc-heading"<%= " data-module=\"collapsible-navigation\"" if config[:tech_docs][:collapsible_nav] %>>
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
},
"standard": {
"globals": [
"jasmine",
"describe",
"before",
"after",
"beforeEach",
"afterEach",
"beforeAll",
"it",
"assert",
"expect",
Expand Down
303 changes: 303 additions & 0 deletions spec/javascripts/table-of-contents-spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
describe('Table of contents', function () {
'use strict'

// global variables
var $html
var $tocBase
var $toc
var $closeButton
var $openButton
var module

beforeAll(function () {
$html = $('html')
$tocBase = $(
'<div class="toc" data-module="table-of-contents" tabindex="-1" aria-label="Table of contents">' +
'<div class="search" data-module="search" data-path-to-site-root="/">' +
'<form action="/search/index.html" method="get" role="search" class="search__form govuk-!-margin-bottom-4">' +
'<label class="govuk-label search__label" for="search">Search this documentation</label>' +
'<input type="text" id="search" name="q" class="govuk-input govuk-!-margin-bottom-0 search__input" aria-controls="search-results" placeholder="Search">' +
'<button type="submit" class="search__button">Search</button>' +
'</form>' +
'</div>' +
'<button type="button" class="toc__close js-toc-close" aria-controls="toc" aria-label="Hide table of contents"></button>' +
'<nav id="toc" class="js-toc-list toc__list" aria-labelledby="toc-heading" data-module="collapsible-navigation">' +
'<ul>' +
'<li>' +
'<a href="/"><span>Technical Documentation Template</span></a>' +
'</li>' +
'<li>' +
'<a href="/"><span>Get started</span></a>' +
'</li>' +
'<li>' +
'<a href="/"><span>Configure your documentation site</span></a>' +
'</li>' +
'</ul>' +
'</nav>' +
'</div>'
)

// some of the module's logic depends on the display style of the table of contents and the open button (.toc-show)
// this is set in the CSS so replicate that, with classes we control for screen size
$('head').append(
'<style>' +
'.toc-show { display: none; }' +
'.mobile-size .toc-show { display: block; }' +
'.toc { display: block; }' +
'.mobile-size .toc { display: none; }' +
'.mobile-size.toc-open .toc { display: block; }' +
'</style>')
})

beforeEach(function () {
$toc = $tocBase.clone()

$html.find('body')
.append(
'<div id="toc-heading" class="toc-show fixedsticky">' +
'<button type="button" class="toc-show__label js-toc-show" aria-controls="toc">' +
'Table of contents <span class="toc-show__icon"></span>' +
'</button>' +
'</div>'
)
.append($toc)

$closeButton = $toc.find('.js-toc-close')
$openButton = $html.find('.js-toc-show')
})

afterEach(function () {
// clear up any classes left on <html>
$html.removeClass('.toc-open')
$html.find('body #toc-heading').remove()
$html.find('body .toc').remove()
})

describe('when the module is started', function () {
it('on a mobile-size screen, it should mark the table of contents as hidden', 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

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

expect($toc.attr('aria-hidden')).toEqual('true')

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

it('on a desktop-size screen, it should mark the table of contents as visible', 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

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

expect($toc.attr('aria-hidden')).toEqual('false')
})
})

describe('when the screen resizes', function () {
describe('on a mobile-size screen', function () {
beforeEach(function () {
$html.addClass('mobile-size')

module = new GOVUK.Modules.TableOfContents()
module.start($toc)
})

afterEach(function () {
$html.removeClass('mobile-size')
})

it("the table of contents should have a role of 'dialog'", function () {
$(window).trigger('resize')

expect($toc.attr('role')).toEqual('dialog')
})

it('if the table of contents is closed, it should mark the buttons as not expanded', function () {
// the table of contents is closed by default, set by CSS styles

$(window).trigger('resize')

expect($closeButton.attr('aria-expanded')).toEqual('false')
expect($openButton.attr('aria-expanded')).toEqual('false')
})

it('if the table of contents is open, it should mark the buttons as expanded', function () {
$html.addClass('toc-open')

$(window).trigger('resize')

expect($closeButton.attr('aria-expanded')).toEqual('true')
expect($openButton.attr('aria-expanded')).toEqual('true')

$html.removeClass('toc-open')
})
})

it('on a desktop-size screen, the table of contents should have no role', function () {
module = new GOVUK.Modules.TableOfContents()
module.start($toc)

$(window).trigger('resize')

expect($toc.attr('role')).toEqual(undefined)
})
})

describe('if the open button is clicked', function () {
beforeEach(function () {
module = new GOVUK.Modules.TableOfContents()
module.start($toc)
})

it('the click event should be cancelled', function () {
var clickEvt = new $.Event('click')

$openButton.trigger(clickEvt)

expect(clickEvt.isDefaultPrevented()).toBe(true)
})

it('the table of contents should show and be focused', function () {
// detecting focus has proved unreliable so track calls to $toc.focus instead
var _focus = $.fn.focus
var tocFocusSpy = jasmine.createSpy('tocFocusSpy')
var clickEvt

$.fn.extend({
focus: function () {
if (this === $toc) {
tocFocusSpy()
} else {
_focus.call($toc)
}
}
})

clickEvt = new $.Event('click')
$openButton.trigger(clickEvt)

expect($toc.attr('aria-hidden')).toEqual('false')

expect(tocFocusSpy).toHaveBeenCalled()

// reset .focus method
$.fn.extend({ focus: _focus })
})
})

describe('if the close button is clicked', function () {
var clickEvt

beforeEach(function () {
$html.addClass('mobile-size')

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

// tocIsVisible = false // controls what $toc.is(':visible') returns, which will be controlled by CSS in a web page
clickEvt = new $.Event('click')
$closeButton.trigger(clickEvt)
})

afterEach(function () {
$html.removeClass('mobile-size')
})

it('the click event should be cancelled', function () {
expect(clickEvt.isDefaultPrevented()).toBe(true)
})

it('the table of contents should be hidden', function () {
expect($toc.attr('aria-hidden')).toEqual('true')
})
})

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

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

$openButton.trigger('click')

$(document).trigger(new $.Event('keydown', {
keyCode: 27
}))

expect($html.hasClass('toc-open')).toBe(false)

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

describe("Fix for iOS 'rubber banding'", function () {
var _scrollTop
var _prop
var scrollTop
var scrollHeight
var offsetHeight
var scrollTopSpy

beforeEach(function () {
// stub out jQuery methods
_scrollTop = $.fn.scrollTop
_prop = $.fn.prop

scrollTopSpy = jasmine.createSpy('scrollTopSpy')

$.fn.extend({
scrollTop: function (val) {
if (val !== undefined) {
return scrollTopSpy(val)
}
return scrollTop
}
})

$.fn.extend({
prop: function (key) {
if (key === 'scrollHeight') {
return scrollHeight
}
if (key === 'offsetHeight') {
return offsetHeight
}
return _prop.call($toc, key)
}
})

module = new GOVUK.Modules.TableOfContents()
module.start($toc)
})

afterEach(function () {
// reset jQuery methods
$.fn.extend({ prop: _prop })
$.fn.extend({ scrollTop: _scrollTop })
})

it('should stop the scroll reaching the top edge if at the top of the page', function () {
scrollTop = 0
scrollHeight = 1000
offsetHeight = 600

$toc.trigger('touchstart')

expect(scrollTopSpy).toHaveBeenCalledWith(1)
})

it('should stop the scroll reaching the bottom edge if at the bottom of the page', function () {
scrollTop = 400
scrollHeight = 1000
offsetHeight = 600

$toc.trigger('touchstart')

expect(scrollTopSpy).toHaveBeenCalledWith(399)
})
})
})

0 comments on commit 13ada11

Please sign in to comment.