I am adding Accessibility to a legacy app which uses frames. I am presently working on the skip-nav feature.
window.top
keydown
listener to catch a Tab
press and show the SkipNav - it worksdocument
, not the SkipNav, but it only responds to clicks within the SkipNavI have tried various combinations of options to the event listener. Currently the options are: { capture:true, passive:true }
, so as I understand it, this listener will get called first and the event cannot get cancelled by any bubbling handlers.
Here is the create
method of my SkipNavigation
object:
// creates DOM elements, sets up event listeners
create()
{
this.div = document.createElement('div');
this.div.setAttribute('id', 'skip_nav_div');
this.div.setAttribute('role', 'navigation');
this.div.setAttribute('aria-label', 'skip navigation');
this.div.setAttribute('tabindex', '-1');
this.div.className = 'SkipNavigation';
this.button = document.createElement('button');
this.button.setAttribute('id', 'skip_nav_button');
this.button.textContent = 'skip to main content';
this.button.classList = 'btn btn-light';
this.button.addEventListener('click', () => { this.skipNav(this) });
this.div.appendChild(this.button);
document.body.appendChild(this.div);
this.current_element = null;
// This event listener will abort itself after a Tab key is pressed for the
// first time.
// It will display a button to skip to main content if a Tab is pressed.
this.tab_controller = new AbortController();
document.addEventListener('keydown', (e) =>
{
switch (e.key)
{
case 'Tab':
e.preventDefault();
// save currently focused element
this.current_element = document.activeElement;
// check if there's content to skip to
if (this.getContent())
{
// show the SkipNav
this.show(e);
}
// turn off this event listener
this.tab_controller.abort();
break;
default:
// do nothing
}
}, {signal: this.tab_controller.signal});
this.abortOnClick = function (e)
{
// don't intercept clicks on the button itself
if (e.target === this.button)
{
return;
}
this.tab_controller.abort();
// also hide the SkipNav if currently shown
if (this.div.classList.contains('is-open'))
{
this.hide();
// and reset focus to where the user was
if (this.current_element) this.current_element.focus();
}
document.removeEventListener('click', this.abortOnClick.bind(this), { capture:true, passive:true });
}.bind(this);
document.addEventListener('click', this.abortOnClick.bind(this), { capture:true, passive:true });
}
In one of our inner frames, I have an event listener fulfilling a similar purpose for a different object which is added to document
and that one properly responds to any click (and it does not use capture).
I am at a loss as to why this abortOnClick
handler is only responding to the SkipNav div.
Any help is greatly appreciated.
The less you depend on scripting, the better.
If I understand correctly, you are currently trying to add state to the skip navigation, making it usable only once. This adds additional complexity to the interface and is unexpected.
It is unclear how the current solution actually hides the navigation via this.hide
, it’s important that it’s not hidden from assistive technology, as display: none
would do.
My recommendation is to stick to the best practice of visually hiding the skip button via CSS and relying on the natural tab order.
The following code is invalid HTML and will not work with a HTML 5 doctype. In a frameset doctype, no elements other than frameset and frame are allowed.
const div = document.createElement('div');
div.setAttribute('id', 'skip_nav_div');
div.setAttribute('role', 'navigation');
div.setAttribute('aria-label', 'skip navigation');
div.className = 'SkipNavigation';
const button = document.createElement('button');
button.setAttribute('id', 'skip_nav_button');
button.textContent = 'skip to main content';
button.classList = 'btn btn-light visually-hidden';
button.addEventListener('click', () => document.querySelector('#main').focus());
div.appendChild(button);
document.body.insertBefore(div, document.body.firstChild);
frame {
border: 1px dashed grey;
}
.SkipNavigation {
position: absolute;
}
/* https://www.scottohara.me/blog/2017/04/14/inclusively-hidden.html#hiding-content-visually */
.visually-hidden:not(:focus):not(:active) {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">
<html>
<!--
<frameset rows="100,">
<frame title="Nav" src="https://example.com/" />
<frame title="Main content" id="main" src="https://example.com/" />
</frameset>
-->
<div id="main" tabindex="-1">Main for demonstration</div>
<p><a href="#">Some link to demonstrate focus order</a></p>
</html>
As far as I know, screen readers already have a frame-navigation built in, so we should support this by providing useful names.
This example uses the attribute selector to find the frames by their src attribute, in case they don’t offer names or IDs.
document.querySelector('frame[src*="navigation"]').title = "Navigation";
document.querySelector('frame[src*="index"]').title ="Main content";
…