Commit 80dafc21 by Christine Lytwynec

Merge pull request #9506 from edx/clytwynec/AC-157

Manage focus on delete component modal
parents b98956bd 2dc5b8e8
...@@ -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", 'common/js/spec_helpers/view_helpers', "sinon", "jquery.simulate"],
function($, SystemFeedback, NotificationView, AlertView, PromptView, ViewHelpers, sinon) {
var tpl; var tpl;
tpl = readFixtures('system-feedback.underscore'); tpl = readFixtures('system-feedback.underscore');
beforeEach(function() { beforeEach(function() {
...@@ -114,6 +115,56 @@ ...@@ -114,6 +115,56 @@
}); });
}); });
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 ViewHelpers.verifyElementInFocus(view, ".wrapper-prompt")
});
it("is not focused on hide", function() {
var view;
view = new PromptView.Confirmation(this.options).hide();
expect(this.outFocusSpy).toHaveBeenCalled();
return ViewHelpers.verifyElementNotInFocus(view, ".wrapper-prompt")
});
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 ViewHelpers.verifyElementInFocus(view, ".action-primary")
});
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 ViewHelpers.verifyElementInFocus(view, ".action-secondary")
});
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({
......
...@@ -10,7 +10,8 @@ define(["jquery", "common/js/components/views/feedback_notification", "common/js ...@@ -10,7 +10,8 @@ define(["jquery", "common/js/components/views/feedback_notification", "common/js
verifyFeedbackHidden, createNotificationSpy, verifyNotificationShowing, verifyFeedbackHidden, createNotificationSpy, verifyNotificationShowing,
verifyNotificationHidden, createPromptSpy, confirmPrompt, inlineEdit, verifyInlineEditChange, verifyNotificationHidden, createPromptSpy, confirmPrompt, inlineEdit, verifyInlineEditChange,
installMockAnalytics, removeMockAnalytics, verifyPromptShowing, verifyPromptHidden, installMockAnalytics, removeMockAnalytics, verifyPromptShowing, verifyPromptHidden,
clickDeleteItem, patchAndVerifyRequest, submitAndVerifyFormSuccess, submitAndVerifyFormError; clickDeleteItem, patchAndVerifyRequest, submitAndVerifyFormSuccess, submitAndVerifyFormError,
verifyElementInFocus, verifyElementNotInFocus;
installViewTemplates = function() { installViewTemplates = function() {
appendSetFixtures('<div id="page-notification"></div>'); appendSetFixtures('<div id="page-notification"></div>');
...@@ -127,6 +128,22 @@ define(["jquery", "common/js/components/views/feedback_notification", "common/js ...@@ -127,6 +128,22 @@ define(["jquery", "common/js/components/views/feedback_notification", "common/js
verifyNotificationShowing(notificationSpy, /Saving/); verifyNotificationShowing(notificationSpy, /Saving/);
}; };
verifyElementInFocus = function(view, selector) {
waitsFor(
function() { return view.$(selector + ':focus').length === 1; },
"element to have focus: " + selector,
500
);
};
verifyElementNotInFocus = function(view, selector) {
waitsFor(
function() { return view.$(selector + ':focus').length === 0; },
"element to not have focus: " + selector,
500
);
};
return { return {
'installViewTemplates': installViewTemplates, 'installViewTemplates': installViewTemplates,
'createNotificationSpy': createNotificationSpy, 'createNotificationSpy': createNotificationSpy,
...@@ -143,7 +160,9 @@ define(["jquery", "common/js/components/views/feedback_notification", "common/js ...@@ -143,7 +160,9 @@ define(["jquery", "common/js/components/views/feedback_notification", "common/js
'clickDeleteItem': clickDeleteItem, 'clickDeleteItem': clickDeleteItem,
'patchAndVerifyRequest': patchAndVerifyRequest, 'patchAndVerifyRequest': patchAndVerifyRequest,
'submitAndVerifyFormSuccess': submitAndVerifyFormSuccess, 'submitAndVerifyFormSuccess': submitAndVerifyFormSuccess,
'submitAndVerifyFormError': submitAndVerifyFormError 'submitAndVerifyFormError': submitAndVerifyFormError,
'verifyElementInFocus': verifyElementInFocus,
'verifyElementNotInFocus': verifyElementNotInFocus
}; };
}); });
}).call(this, define || RequireJS.define); }).call(this, define || RequireJS.define);
...@@ -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