Skip to content
Snippets Groups Projects
Commit 80dafc21 authored by Christine Lytwynec's avatar Christine Lytwynec
Browse files

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

Manage focus on delete component modal
parents b98956bd 2dc5b8e8
No related merge requests found
......@@ -6,6 +6,17 @@
"backbone",
"text!common/templates/components/system-feedback.underscore"],
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({
options: {
title: "",
......@@ -16,7 +27,8 @@
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?
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
the expected structure. For each action, by default the framework
......@@ -65,6 +77,40 @@
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()
show: function() {
clearTimeout(this.hideTimeout);
......
......@@ -18,6 +18,15 @@
}
// super() in Javascript has awkward syntax :(
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
(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;
tpl = readFixtures('system-feedback.underscore');
beforeEach(function() {
......@@ -114,6 +115,56 @@
});
});
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() {
var view;
view = new PromptView.Confirmation({
......
......@@ -10,7 +10,8 @@ define(["jquery", "common/js/components/views/feedback_notification", "common/js
verifyFeedbackHidden, createNotificationSpy, verifyNotificationShowing,
verifyNotificationHidden, createPromptSpy, confirmPrompt, inlineEdit, verifyInlineEditChange,
installMockAnalytics, removeMockAnalytics, verifyPromptShowing, verifyPromptHidden,
clickDeleteItem, patchAndVerifyRequest, submitAndVerifyFormSuccess, submitAndVerifyFormError;
clickDeleteItem, patchAndVerifyRequest, submitAndVerifyFormSuccess, submitAndVerifyFormError,
verifyElementInFocus, verifyElementNotInFocus;
installViewTemplates = function() {
appendSetFixtures('<div id="page-notification"></div>');
......@@ -127,6 +128,22 @@ define(["jquery", "common/js/components/views/feedback_notification", "common/js
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 {
'installViewTemplates': installViewTemplates,
'createNotificationSpy': createNotificationSpy,
......@@ -143,7 +160,9 @@ define(["jquery", "common/js/components/views/feedback_notification", "common/js
'clickDeleteItem': clickDeleteItem,
'patchAndVerifyRequest': patchAndVerifyRequest,
'submitAndVerifyFormSuccess': submitAndVerifyFormSuccess,
'submitAndVerifyFormError': submitAndVerifyFormError
'submitAndVerifyFormError': submitAndVerifyFormError,
'verifyElementInFocus': verifyElementInFocus,
'verifyElementNotInFocus': verifyElementNotInFocus
};
});
}).call(this, define || RequireJS.define);
......@@ -31,6 +31,7 @@ lib_paths:
- js/vendor/jquery.min.js
- js/vendor/jasmine-jquery.js
- js/vendor/jasmine-imagediff.js
- js/vendor/jquery.simulate.js
- js/vendor/jquery.truncate.js
- js/vendor/underscore-min.js
- js/vendor/underscore.string.min.js
......
......@@ -56,6 +56,10 @@ def confirm_prompt(page, cancel=False, require_notification=None):
cancel is True.
"""
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')
page.wait_for_element_visibility(confirmation_button_css, 'Confirmation button is visible')
require_notification = (not cancel) if require_notification is None else require_notification
......
......@@ -93,7 +93,6 @@
});
}
);
$('.wrapper-prompt').focus();
}
});
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment