Commit f96d4b71 by Christine Lytwynec

Manage focus on modal confirmation prompt

parent 8797a4f6
...@@ -6,6 +6,17 @@ ...@@ -6,6 +6,17 @@
"backbone", "backbone",
"text!common/templates/components/system-feedback.underscore"], "text!common/templates/components/system-feedback.underscore"],
function($, _, str, Backbone, systemFeedbackTemplate) { function($, _, str, Backbone, systemFeedbackTemplate) {
var tabbable_elements = [
"a[href]:not([tabindex='-1'])",
"area[href]:not([tabindex='-1'])",
"input:not([disabled]):not([tabindex='-1'])",
"select:not([disabled]):not([tabindex='-1'])",
"textarea:not([disabled]):not([tabindex='-1'])",
"button:not([disabled]):not([tabindex='-1'])",
"iframe:not([tabindex='-1'])",
"[tabindex]:not([tabindex='-1'])",
"[contentEditable=true]:not([tabindex='-1'])"
];
var SystemFeedback = Backbone.View.extend({ var SystemFeedback = Backbone.View.extend({
options: { options: {
title: "", title: "",
...@@ -16,7 +27,8 @@ ...@@ -16,7 +27,8 @@
icon: true, // should we render an icon related to the message intent? icon: true, // should we render an icon related to the message intent?
closeIcon: true, // should we render a close button in the top right corner? closeIcon: true, // should we render a close button in the top right corner?
minShown: 0, // length of time after this view has been shown before it can be hidden (milliseconds) minShown: 0, // length of time after this view has been shown before it can be hidden (milliseconds)
maxShown: Infinity // length of time after this view has been shown before it will be automatically hidden (milliseconds) maxShown: Infinity, // length of time after this view has been shown before it will be automatically hidden (milliseconds)
outFocusElement: null // element to send focus to on hide
/* Could also have an "actions" hash: here is an example demonstrating /* Could also have an "actions" hash: here is an example demonstrating
the expected structure. For each action, by default the framework the expected structure. For each action, by default the framework
...@@ -65,6 +77,40 @@ ...@@ -65,6 +77,40 @@
return this; return this;
}, },
inFocus: function() {
this.options.outFocusElement = this.options.outFocusElement || document.activeElement;
// Set focus to the container.
this.$(".wrapper").first().focus();
// Make tabs within the prompt loop rather than setting focus
// back to the main content of the page.
var tabbables = this.$(tabbable_elements.join());
tabbables.on("keydown", function (event) {
// On tab backward from the first tabbable item in the prompt
if (event.which === 9 && event.shiftKey && event.target === tabbables.first()[0]) {
event.preventDefault();
tabbables.last().focus();
}
// On tab forward from the last tabbable item in the prompt
else if (event.which === 9 && !event.shiftKey && event.target === tabbables.last()[0]) {
event.preventDefault();
tabbables.first().focus();
}
});
return this;
},
outFocus: function() {
var tabbables = this.$(tabbable_elements.join()).off("keydown");
if (this.options.outFocusElement) {
this.options.outFocusElement.focus();
}
return this;
},
// public API: show() and hide() // public API: show() and hide()
show: function() { show: function() {
clearTimeout(this.hideTimeout); clearTimeout(this.hideTimeout);
......
...@@ -18,6 +18,15 @@ ...@@ -18,6 +18,15 @@
} }
// super() in Javascript has awkward syntax :( // super() in Javascript has awkward syntax :(
return SystemFeedbackView.prototype.render.apply(this, arguments); return SystemFeedbackView.prototype.render.apply(this, arguments);
},
show: function() {
SystemFeedbackView.prototype.show.apply(this, arguments);
return this.inFocus();
},
hide: function() {
SystemFeedbackView.prototype.hide.apply(this, arguments);
return this.outFocus();
} }
}); });
......
// Generated by CoffeeScript 1.6.1 // Generated by CoffeeScript 1.6.1
(function() { (function() {
define(["jquery", "common/js/components/views/feedback", "common/js/components/views/feedback_notification", "common/js/components/views/feedback_alert", "common/js/components/views/feedback_prompt", "sinon"], function($, SystemFeedback, NotificationView, AlertView, PromptView, sinon) { define(["jquery", "common/js/components/views/feedback", "common/js/components/views/feedback_notification", "common/js/components/views/feedback_alert", "common/js/components/views/feedback_prompt", "sinon", "jquery.simulate"],
function($, SystemFeedback, NotificationView, AlertView, PromptView, sinon) {
var tpl; var tpl;
tpl = readFixtures('system-feedback.underscore'); tpl = readFixtures('system-feedback.underscore');
beforeEach(function() { beforeEach(function() {
...@@ -114,6 +115,72 @@ ...@@ -114,6 +115,72 @@
}); });
}); });
describe("PromptView", function() { describe("PromptView", function() {
beforeEach(function() {
this.options = {
title: "Confirming Something",
message: "Are you sure you want to do this?",
actions: {
primary: {
text: "Yes, I'm sure.",
"class": "confirm-button",
},
secondary: {
text: "Cancel",
"class": "cancel-button",
}
}
}
this.inFocusSpy = spyOn(PromptView.Confirmation.prototype, 'inFocus').andCallThrough();
return this.outFocusSpy = spyOn(PromptView.Confirmation.prototype, 'outFocus').andCallThrough();
});
it("is focused on show", function() {
var view;
view = new PromptView.Confirmation(this.options).show();
expect(this.inFocusSpy).toHaveBeenCalled();
return waitsFor(
function() { return view.$('.wrapper-prompt:focus').length === 1; },
"The modal should have focus",
500
);
});
it("is not focused on hide", function() {
var view;
view = new PromptView.Confirmation(this.options).hide();
expect(this.outFocusSpy).toHaveBeenCalled();
return waitsFor(
function() { return view.$('.wrapper-prompt:focus').length === 0; },
"The modal should not have focus",
500
);
});
it("traps keyboard focus when moving forward", function() {
var view;
view = new PromptView.Confirmation(this.options).show();
expect(this.inFocusSpy).toHaveBeenCalled();
$('.action-secondary').first().simulate(
"keydown",
{ keyCode: $.simulate.keyCode.TAB }
);
return waitsFor(
function() { return view.$('.action-primary:focus').length === 1; },
"The first action button should have focus",
500
);
});
it("traps keyboard focus when moving backward", function() {
var view;
view = new PromptView.Confirmation(this.options).show();
expect(this.inFocusSpy).toHaveBeenCalled();
$('.action-primary').first().simulate(
"keydown",
{ keyCode: $.simulate.keyCode.TAB, shiftKey: true }
);
return waitsFor(
function() { return view.$('.action-secondary:focus').length === 1; },
"The last action button should have focus",
500
);
});
return it("changes class on body", function() { return it("changes class on body", function() {
var view; var view;
view = new PromptView.Confirmation({ view = new PromptView.Confirmation({
......
...@@ -31,6 +31,7 @@ lib_paths: ...@@ -31,6 +31,7 @@ lib_paths:
- js/vendor/jquery.min.js - js/vendor/jquery.min.js
- js/vendor/jasmine-jquery.js - js/vendor/jasmine-jquery.js
- js/vendor/jasmine-imagediff.js - js/vendor/jasmine-imagediff.js
- js/vendor/jquery.simulate.js
- js/vendor/jquery.truncate.js - js/vendor/jquery.truncate.js
- js/vendor/underscore-min.js - js/vendor/underscore-min.js
- js/vendor/underscore.string.min.js - js/vendor/underscore.string.min.js
......
...@@ -56,6 +56,10 @@ def confirm_prompt(page, cancel=False, require_notification=None): ...@@ -56,6 +56,10 @@ def confirm_prompt(page, cancel=False, require_notification=None):
cancel is True. cancel is True.
""" """
page.wait_for_element_visibility('.prompt', 'Prompt is visible') page.wait_for_element_visibility('.prompt', 'Prompt is visible')
page.wait_for_element_visibility(
'.wrapper-prompt:focus',
'Prompt is in focus'
)
confirmation_button_css = '.prompt .action-' + ('secondary' if cancel else 'primary') confirmation_button_css = '.prompt .action-' + ('secondary' if cancel else 'primary')
page.wait_for_element_visibility(confirmation_button_css, 'Confirmation button is visible') page.wait_for_element_visibility(confirmation_button_css, 'Confirmation button is visible')
require_notification = (not cancel) if require_notification is None else require_notification require_notification = (not cancel) if require_notification is None else require_notification
......
...@@ -93,7 +93,6 @@ ...@@ -93,7 +93,6 @@
}); });
} }
); );
$('.wrapper-prompt').focus();
} }
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment