I know it's a little late but that's how I resolve the issue of background focus on the modal. I will provide two solutions one for "talkback" and another one is for "Switch Access" which will work for the tab key too.
For Talkback:
function preventFocusOnBackground(ariaHide) {
$("body > *").not("#modalId").attr("aria-hidden", ariaHide);
}
// when you close the modal
preventFocusOnBackground(false);
// when you open the modal
preventFocusOnBackground(true)
For Switch Access/Control copy/paste this code in your file:
var aria = aria || {};
aria.Utils = aria.Utils || {};
(function () {
/*
* When util functions move focus around, set this true so the focus
listener
* can ignore the events.
*/
aria.Utils.IgnoreUtilFocusChanges = false;
aria.Utils.dialogOpenClass = 'has-dialog';
/**
* @desc Set focus on descendant nodes until the first focusable
element is
* found.
* @param element
* DOM node for which to find the first focusable descendant.
* @returns
* true if a focusable element is found and focus is set.
*/
aria.Utils.focusFirstDescendant = function (element) {
for (var i = 0; i < element.childNodes.length; i++) {
var child = element.childNodes[i];
if (aria.Utils.attemptFocus(child) ||
aria.Utils.focusFirstDescendant(child)) {
return true;
}
}
return false;
}; // end focusFirstDescendant
/**
* @desc Find the last descendant node that is focusable.
* @param element
* DOM node for which to find the last focusable descendant.
* @returns
* true if a focusable element is found and focus is set.
*/
aria.Utils.focusLastDescendant = function (element) {
for (var i = element.childNodes.length - 1; i >= 0; i--) {
var child = element.childNodes[i];
if (aria.Utils.attemptFocus(child) ||
aria.Utils.focusLastDescendant(child)) {
return true;
}
}
return false;
}; // end focusLastDescendant
/**
* @desc Set Attempt to set focus on the current node.
* @param element
* The node to attempt to focus on.
* @returns
* true if element is focused.
*/
aria.Utils.attemptFocus = function (element) {
if (!aria.Utils.isFocusable(element)) {
return false;
}
aria.Utils.IgnoreUtilFocusChanges = true;
try {
element.focus();
}
catch (e) {
}
aria.Utils.IgnoreUtilFocusChanges = false;
return (document.activeElement === element);
}; // end attemptFocus
/* Modals can open modals. Keep track of them with this array. */
aria.OpenDialogList = aria.OpenDialogList || new Array(0);
/**
* @returns the last opened dialog (the current dialog)
*/
aria.getCurrentDialog = function () {
if (aria.OpenDialogList && aria.OpenDialogList.length) {
return aria.OpenDialogList[aria.OpenDialogList.length - 1];
}
};
aria.Utils.isFocusable = function(element) {
return element.classList && element.classList.contains('focusable');
}
aria.closeCurrentDialog = function () {
var currentDialog = aria.getCurrentDialog();
if (currentDialog) {
currentDialog.close();
return true;
}
return false;
};
document.addEventListener('keyup', aria.handleEscape);
/**
* @constructor
* @desc Dialog object providing modal focus management.
*
* Assumptions: The element serving as the dialog container is present
in the
* DOM and hidden. The dialog container has role='dialog'.
*
* @param dialogId
* The ID of the element serving as the dialog container.
* @param focusAfterClosed
* Either the DOM node or the ID of the DOM node to focus
* when the dialog closes.
* @param focusFirst
* Optional parameter containing either the DOM node or the
ID of the
* DOM node to focus when the dialog opens. If not specified, the
* first focusable element in the dialog will receive focus.
*/
aria.Dialog = function (dialogId, focusAfterClosed, focusFirst) {
this.dialogNode = document.getElementById(dialogId);
if (this.dialogNode === null) {
throw new Error('No element found with id="' + dialogId + '".');
}
var validRoles = ['dialog', 'alertdialog'];
var isDialog = (this.dialogNode.getAttribute('role') || '')
.trim()
.split(/\s+/g)
.some(function (token) {
return validRoles.some(function (role) {
return token === role;
});
});
if (!isDialog) {
throw new Error(
'Dialog() requires a DOM element with ARIA role of dialog or
alertdialog.');
}
// Wrap in an individual backdrop element if one doesn't exist
// Native <dialog> elements use the ::backdrop pseudo-element, which
// works similarly.
var backdropClass = 'dialog-backdrop';
if (this.dialogNode.parentNode.classList.contains(backdropClass)) {
this.backdropNode = this.dialogNode.parentNode;
}
else {
this.backdropNode = document.createElement('div');
this.backdropNode.className = backdropClass;
this.dialogNode.parentNode.insertBefore(this.backdropNode,
this.dialogNode);
this.backdropNode.appendChild(this.dialogNode);
}
this.backdropNode.classList.add('active');
// Disable scroll on the body element
document.body.classList.add(aria.Utils.dialogOpenClass);
if (typeof focusAfterClosed === 'string') {
this.focusAfterClosed = document.getElementById(focusAfterClosed);
}
else if (typeof focusAfterClosed === 'object') {
this.focusAfterClosed = focusAfterClosed;
}
else {
throw new Error(
'the focusAfterClosed parameter is required for the aria.Dialog
constructor.');
}
if (typeof focusFirst === 'string') {
this.focusFirst = document.getElementById(focusFirst);
}
else if (typeof focusFirst === 'object') {
this.focusFirst = focusFirst;
}
else {
this.focusFirst = null;
}
// If this modal is opening on top of one that is already open,
// get rid of the document focus listener of the open dialog.
if (aria.OpenDialogList.length > 0) {
aria.getCurrentDialog().removeListeners();
}
this.addListeners();
aria.OpenDialogList.push(this);
this.clearDialog();
this.dialogNode.className = 'default_dialog'; // make visible
if (this.focusFirst) {
this.focusFirst.focus();
}
else {
aria.Utils.focusFirstDescendant(this.dialogNode);
}
this.lastFocus = document.activeElement;
}; // end Dialog constructor
aria.Dialog.prototype.clearDialog = function () {
Array.prototype.map.call(
this.dialogNode.querySelectorAll('input'),
function (input) {
input.value = '';
}
);
};
/**
* @desc
* Hides the current top dialog,
* removes listeners of the top dialog,
* restore listeners of a parent dialog if one was open under the one
that just closed,
* and sets focus on the element specified for focusAfterClosed.
*/
aria.Dialog.prototype.close = function () {
aria.OpenDialogList.pop();
this.removeListeners();
aria.Utils.remove(this.preNode);
aria.Utils.remove(this.postNode);
this.dialogNode.className = 'hidden';
this.backdropNode.classList.remove('active');
this.focusAfterClosed.focus();
// If a dialog was open underneath this one, restore its listeners.
if (aria.OpenDialogList.length > 0) {
aria.getCurrentDialog().addListeners();
}
else {
document.body.classList.remove(aria.Utils.dialogOpenClass);
}
}; // end close
/**
* @desc
* Hides the current dialog and replaces it with another.
*
* @param newDialogId
* ID of the dialog that will replace the currently open top dialog.
* @param newFocusAfterClosed
* Optional ID or DOM node specifying where to place focus when the
new dialog closes.
* If not specified, focus will be placed on the element specified by
the dialog being replaced.
* @param newFocusFirst
* Optional ID or DOM node specifying where to place focus in the new
dialog when it opens.
* If not specified, the first focusable element will receive focus.
*/
aria.Dialog.prototype.replace = function (newDialogId,
newFocusAfterClosed,
newFocusFirst) {
var closedDialog = aria.getCurrentDialog();
aria.OpenDialogList.pop();
this.removeListeners();
aria.Utils.remove(this.preNode);
aria.Utils.remove(this.postNode);
this.dialogNode.className = 'hidden';
this.backdropNode.classList.remove('active');
var focusAfterClosed = newFocusAfterClosed || this.focusAfterClosed;
var dialog = new aria.Dialog(newDialogId, focusAfterClosed,
newFocusFirst);
}; // end replace
aria.Dialog.prototype.addListeners = function () {
document.addEventListener('focus', this.trapFocus, true);
}; // end addListeners
aria.Dialog.prototype.removeListeners = function () {
document.removeEventListener('focus', this.trapFocus, true);
}; // end removeListeners
aria.Dialog.prototype.trapFocus = function (event) {
if (aria.Utils.IgnoreUtilFocusChanges) {
return;
}
var currentDialog = aria.getCurrentDialog();
if (currentDialog.dialogNode.contains(event.target)) {
currentDialog.lastFocus = event.target;
}
else {
aria.Utils.focusFirstDescendant(currentDialog.dialogNode);
if (currentDialog.lastFocus == document.activeElement) {
aria.Utils.focusLastDescendant(currentDialog.dialogNode);
}
currentDialog.lastFocus = document.activeElement;
}
}; // end trapFocus
window.openDialog = function (dialogId, focusAfterClosed, focusFirst){
var dialog = new aria.Dialog(dialogId, focusAfterClosed,focusFirst);
};
window.closeDialog = function (closeButton) {
var topDialog = aria.getCurrentDialog();
if (topDialog.dialogNode.contains(closeButton)) {
topDialog.close();
}
}; // end closeDialog
window.replaceDialog = function (newDialogId, newFocusAfterClosed,
newFocusFirst) {
var topDialog = aria.getCurrentDialog();
if (topDialog.dialogNode.contains(document.activeElement)) {
topDialog.replace(newDialogId, newFocusAfterClosed,newFocusFirst);
}
}; // end replaceDialog
}());
And call it where you open the modal like this:
openDialog('modalID', this);
Add these attributes in the modal div tag:
<div id="modalId" aria-modal="true" role="dialog">
Add "tabindex" attributes on all the elements where you want the focus. Like this:
<a href="#" onclick="resizeTextFixed(1.4);return false;" tabindex="1"
aria-label="Some text">A</a>
<a href="#" onclick="resizeTextFixed(1.2);return false;" tabindex="2"
aria-label="Some text">A</a>
Add "focusable" class to the first focusable element:
<div class="focuable"></div>
That's it.