')
+ # Add the aria-required attributes to all the input elements that have the required
+ # attribute
+ $this.find('[required]').attr 'aria-required', 'true'
+ return
+ menu: ->
+ @each (index, value) ->
+ $this = jQuery(value)
+ $menu = $this
+ if value.nodeName != 'UL'
+ throw new Error('The menu container must be an unordered list')
+
+ ### First make all anchor tags in the structure non-naturally focussable ###
+
+ $this.find('a').attr 'tabindex', '-1'
+
+ ### Set the roles for the menubar ###
+
+ $this.attr('role', 'menubar').addClass 'a11yfy-top-level-menu'
+
+ ### set the aria attributes and the classes for the sub-menus ###
+
+ $this.find('>li>ul').addClass('a11yfy-second-level-menu').parent().addClass('a11yfy-has-submenu').attr 'aria-haspopup', 'true'
+ $this.find('>li>ul>li>ul').addClass('a11yfy-third-level-menu').parent().addClass('a11yfy-has-submenu').attr 'aria-haspopup', 'true'
+
+ ###
+ # Set up the keyboard and mouse handlers for all the individual menuitems
+ ###
+
+ $this.find('li').each((index, value) ->
+ `var $this`
+
+ ### Set the roles for the sub-menus and the menuitems ###
+
+ $this = jQuery(value)
+ $this.attr
+ 'role': 'menuitem'
+ 'tabindex': '-1'
+ $this.find('ul').each (index, value) ->
+ jQuery(value).attr 'role', 'menu'
+ return
+ return
+ ).on('keypress', (e) ->
+ `var $this`
+
+ ###
+ # This implements the WAI-ARIA-PRACTICES keyboard functionality where
+ # pressing the key, corresponding to the first letter of a VISIBLE element
+ # will move the focus to the first such element after the currently focussed
+ # element
+ ###
+
+ keyCode = e.charCode or e.which or e.keyCode
+ keyString = String.fromCharCode(keyCode).toLowerCase()
+ ourIndex = -1
+ currentItem = this
+ $this = jQuery(this)
+ $nextItem = undefined
+ $prevItem = undefined
+ $menuitems = $menu.find('li[role="menuitem"]:visible')
+ if keyCode == 9
+ return true
+ $menuitems.each (index, value) ->
+ if value == currentItem
+ ourIndex = index
+ if index > ourIndex and !$nextItem
+ if jQuery(value).text().trim().toLowerCase().indexOf(keyString) == 0
+ if ourIndex != -1
+ $nextItem = jQuery(value)
+ else if !$prevItem
+ $prevItem = jQuery(value)
+ return
+ if !$nextItem and $prevItem
+ $nextItem = $prevItem
+ if $nextItem
+ $nextItem.attr('tabindex', '0').focus()
+ $this.attr 'tabindex', '-1'
+ if $nextItem.parent().get(0) != $this.parent().get(0)
+ $this.parent().parent('li').removeClass 'open'
+ e.stopPropagation()
+ return
+ ).on('keydown', (e) ->
+ `var $this`
+
+ ###
+ # This implements the WAI-ARIA-PRACTICES keyboard navigation functionality
+ ###
+
+ keyCode = e.which or e.keyCode
+ handled = false
+ $this = jQuery(this)
+
+ ###
+ # Open a sub-menu and place focus on the first menuitem within it
+ ###
+
+ openMenu = ->
+ if $this.hasClass('a11yfy-has-submenu')
+ $this.addClass('open').find('>ul>li:visible').first().attr('tabindex', '0').focus()
+ $this.attr 'tabindex', '-1'
+ return
+
+ ###
+ # Move the focus to the menuitem preceding the current menuitem
+ ###
+
+ prevInMenu = ->
+ $context = $this
+ $this.attr 'tabindex', '-1'
+ loop
+ if $context.prev().is(':visible')
+ $context.prev().attr('tabindex', '0').focus()
+ return
+ $context = $context.prev()
+ if !$context.prev().length
+ $context = $this.parent().find('>li').last()
+ if $context.is(':visible')
+ $context.attr('tabindex', '0').focus()
+ return
+ if $context[0] == $this[0]
+ $this.attr 'tabindex', '0'
+ break
+ return
+
+ ###
+ # Move the focus to the next menuitem after the currently focussed menuitem
+ ###
+
+ nextInMenu = ->
+ $context = $this
+ $this.attr 'tabindex', '-1'
+ loop
+ if $context.next().is(':visible')
+ $context.next().attr('tabindex', '0').focus()
+ return
+ $context = $context.next()
+ if !$context.next().length
+ $context = $this.parent().find('>li').first()
+ if $context.is(':visible')
+ $context.attr('tabindex', '0').focus()
+ return
+ if $context[0] == $this[0]
+ $this.attr 'tabindex', '0'
+ break
+ return
+
+ if e.ctrlKey or e.shiftKey or e.altKey or e.metaKey
+ # not interested
+ return
+ switch keyCode
+ # space
+ when 32, 13
+ # enter
+ handled = true
+ if $this.find('>a').length
+ if $this.find('>a')[0].click
+
+ ### If this is a leaf node, activate it###
+
+ $this.find('>a')[0].click()
+ else
+ # This is a hack for PhantomJS
+ $this.find('>a').first().trigger 'click'
+ else
+
+ ### If it has a sub-menu, open the sub-menu ###
+
+ openMenu()
+ #left
+ when 37, 27
+ #esc
+ handled = true
+ if keyCode == 37 and $this.parent().hasClass('a11yfy-top-level-menu')
+
+ ### If in the menubar, then simply move to the previous menuitem ###
+
+ prevInMenu()
+ else
+ if $this.parent().attr('role') == 'menu'
+ # this is part of a submenu, set focus on containing li
+ $this.parent().parent().attr('tabindex', '0').focus().removeClass 'open'
+ $this.attr 'tabindex', '-1'
+ when 38
+ #up
+ handled = true
+ if $this.parent().hasClass('a11yfy-top-level-menu')
+
+ ### If in the menubar, then open the sub-menu ###
+
+ openMenu()
+ else
+
+ ### If in sub-menu, move to previous element ###
+
+ prevInMenu()
+ when 39
+ #right
+ handled = true
+ if $this.parent().hasClass('a11yfy-top-level-menu')
+
+ ### If in menubar, move to next menuitem ###
+
+ nextInMenu()
+ else
+
+ ### If in sub-menu, open sub-sub-menu ###
+
+ openMenu()
+ when 40
+ #down
+ handled = true
+ if $this.parent().hasClass('a11yfy-top-level-menu')
+
+ ### If in menubar, open sub-menu ###
+
+ openMenu()
+ else
+
+ ### If in sub-menu, move to the next menuitem ###
+
+ nextInMenu()
+ if handled
+ e.preventDefault()
+ e.stopPropagation()
+ true
+ ).on('blur', ->
+ ).on('click', ->
+ `var $this`
+ $this = jQuery(this)
+ if !$this.hasClass('open')
+ $this.addClass 'open'
+ else
+ $this.removeClass 'open'
+ return
+ ).first().attr 'tabindex', '0'
+ # Make the first menuitem in the menubar tab focussable
+ $this.on 'keydown', (e) ->
+
+ ###
+ # This callback handles the tabbing out of the widget
+ ###
+
+ focusInTopMenu = false
+ keyCode = e.which or e.keyCode
+ if e.ctrlKey or e.altKey or e.metaKey
+ # not interested
+ return
+ if keyCode != 9
+ return true
+
+ ### Find out whether we are currently in the menubar ###
+
+ $this.find('>li').each (index, value) ->
+ if jQuery(value).attr('tabindex') == '0'
+ focusInTopMenu = true
+ return
+ if !focusInTopMenu
+
+ ###
+ # If not in the menubar, close sub-menus and set the tabindex of the top item in the
+ # menubar so it receives focus when the user tabs back into the menubar
+ ###
+
+ $this.find('>li li[tabindex=0]').attr 'tabindex', '-1'
+ setTimeout (->
+ # This code is in a setTimeout so that shift tab works correctly AND
+ # because there is a Firefox (Windows) bug that
+ # causes the default event for a TAB to not happen properly if the visibility of the
+ # currently focussed node is chanhed mid event (e.g. removal of the open class)
+ $this.find('li.open').each((index, value) ->
+ if jQuery(value).parent().hasClass('a11yfy-top-level-menu')
+ jQuery(value).attr 'tabindex', '0'
+ return
+ ).removeClass 'open'
+ return
+ ), 0
+ true
+ return
+ ua = window.navigator.userAgent
+ platform = if ua.match(/iPhone|iPad|iPod/) then 'iOS' else if ua.match(/Mac OS X/) then 'OSX' else if ua.match(/Windows/) then 'Windows' else 'Other'
+
+ jQuery.a11yfy = ->
+
+ jQuery.fn.a11yfy = (method) ->
+ if methods[method]
+ return methods[method].apply(this, Array::slice.call(arguments, 1))
+ else
+ jQuery.error 'Method ' + method + ' does not exist on jQuery.a11yfy'
+ return
+
+ jQuery.fn.a11yfy.defaults =
+ strings: skipToNextError: 'skip to next field with an error'
+ validate:
+ skipLink: true
+ summary: true
+ validatorOptions: {}
+
+ jQuery.a11yfy.getI18nString = (str, values, strings) ->
+ msg = strings[str]
+ v = undefined
+ if values
+ for v of values
+ `v = v`
+ msg = msg.replace('${' + v + '}', values[v])
+ msg
+
+ jQuery.a11yfy.politeAnnounce = (msg) ->
+ $politeAnnouncer.append jQuery('
').text(msg)
+ return
+
+ jQuery.a11yfy.assertiveAnnounce = (msg) ->
+ $assertiveAnnouncer.append jQuery('
').text(msg)
+ return
+
+ # Add the polite announce div to the page
+ if !$politeAnnouncer or !$politeAnnouncer.length
+ jQuery(document).ready ->
+ $politeAnnouncer = jQuery('
').attr(
+ 'id': 'jquery-a11yfy-politeannounce'
+ 'role': 'log'
+ 'aria-live': 'polite'
+ 'aria-relevant': 'additions').addClass('offscreen')
+ jQuery('body').append $politeAnnouncer
+ return
+ # Add the polite announce div to the page
+ if !$assertiveAnnouncer or !$assertiveAnnouncer.length
+ jQuery(document).ready ->
+ $assertiveAnnouncer = jQuery('
').attr(
+ 'id': 'jquery-a11yfy-assertiveannounce'
+ 'role': 'log'
+ 'aria-live': 'assertive'
+ 'aria-relevant': 'additions').addClass('offscreen')
+ jQuery('body').append $assertiveAnnouncer
+ return
+ return
\ No newline at end of file
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 23e6325d..a3d220b2 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -4,6 +4,7 @@
#
#= require jquery
#= require jquery_ujs
+#= require turbolinks
#= require jquery-ui
#
#= require jquery-ui-bootstrap-bridge
@@ -16,6 +17,7 @@
#= require_self
#
# --- Add custom requires under here! ---
+#= require a11yfy
#= require example_script
#= require form_accessibilizer
#= require textarea_fullscreenizer
diff --git a/app/assets/javascripts/callbacks.js.coffee b/app/assets/javascripts/callbacks.js.coffee
index 1386ba99..3b5dbd33 100644
--- a/app/assets/javascripts/callbacks.js.coffee
+++ b/app/assets/javascripts/callbacks.js.coffee
@@ -1,4 +1,9 @@
# To run all JS init code over an element (e.g. after inserting it using JS), do this:
# new App.Init(element)
-$(document).ready ->
+$(document).on 'turbolinks:load', ->
new App.Init @
+
+$(document).on 'turbolinks:render', (e) ->
+ # console.log e.target.title
+ $('#headline_title').focus()
+ # jQuery.a11yfy.assertiveAnnounce 'hello!'
\ No newline at end of file
diff --git a/app/views/layouts/_jump_links.html.slim b/app/views/layouts/_jump_links.html.slim
index c15ff2c6..fa15af35 100644
--- a/app/views/layouts/_jump_links.html.slim
+++ b/app/views/layouts/_jump_links.html.slim
@@ -3,4 +3,5 @@
- unless current_page?(root_path)
- navigation.item t('.home_page'), root_path, link_html: {accesskey: 0, class: 'sr-only', id: 'jump_to_home_page'}
- - navigation.item t('.content'), '#headline_title', link_html: {accesskey: 1, class: 'sr-only', id: 'jump_to_content'}
+ - # https://github.com/turbolinks/turbolinks/issues/214
+ - navigation.item t('.content'), '#headline_title', link_html: {accesskey: 1, class: 'sr-only', id: 'jump_to_content', data: {turbolinks: false}}
diff --git a/app/views/layouts/application.html.slim b/app/views/layouts/application.html.slim
index ff42ddc2..69c4f36e 100644
--- a/app/views/layouts/application.html.slim
+++ b/app/views/layouts/application.html.slim
@@ -6,8 +6,8 @@ html lang=I18n.locale
meta[name="viewport" content="width=device-width, initial-scale=1.0"]
meta name="description" content="#{yield(:description)}"
- = stylesheet_link_tag 'application', media: 'all'
- = javascript_include_tag 'application'
+ = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
+ = javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
= csrf_meta_tags
body class=body_css_classes