Merge pull request #9506 from edx/clytwynec/AC-157
Manage focus on delete component modal
This commit is contained in:
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// 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();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user